/**
 * Extract phase: populate lake (zip extraction + non-zip copy), move other folders, list lake files.
 * Expects ctx: inputHandle, outputHandle, lakeHandle, processedHandle, fileMap, collisionCounter, takenSet, report, options,
 * and helpers: listDir, listTopLevel, getOrCreatePath, writeFileToDir, resolveLakeName, pathBasename, pathDirname, sanitizeNameForFS, safeErrorPath, parentFolderFromPath.
 * Sets ctx.lakeFiles, ctx.lakeHandle on completion.
 */
(function () {
  'use strict';

  var g = typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : this);
  g.GphotoPipelinePhases = g.GphotoPipelinePhases || {};

  g.GphotoPipelinePhases.runExtract = function (ctx, log) {
    log = log || function () {};
    var inputHandle = ctx.inputHandle;
    var outputHandle = ctx.outputHandle;
    var lakeHandle = ctx.lakeHandle;
    var fileMap = ctx.fileMap;
    var collisionCounter = ctx.collisionCounter;
    var takenSet = ctx.takenSet;
    var report = ctx.report;
    var options = ctx.options || {};
    var dryRun = options.dryRun === true;
    var listDir = ctx.listDir;
    var listTopLevel = ctx.listTopLevel;
    var getOrCreatePath = ctx.getOrCreatePath;
    var writeFileToDir = ctx.writeFileToDir;
    var resolveLakeName = ctx.resolveLakeName;
    var pathBasename = ctx.pathBasename;
    var pathDirname = ctx.pathDirname;
    var sanitizeNameForFS = ctx.sanitizeNameForFS;
    var safeErrorPath = ctx.safeErrorPath;
    var isCorruptZipEntryName = ctx.isCorruptZipEntryName;
    var parentFolderFromPath = ctx.parentFolderFromPath;
    var isImage = ctx.isImage;
    var isVideo = ctx.isVideo;

    function zipEntryLabel(pathInZip) {
      return (isCorruptZipEntryName && isCorruptZipEntryName(pathInZip)) ? '(invalid or corrupt entry name)' : safeErrorPath(pathInZip);
    }
    function reportExtractError(msg) {
      report.extraction_entries_failed = (report.extraction_entries_failed || 0) + 1;
      report.errors.push(msg);
      if (options.onExtractError) options.onExtractError(msg);
    }
    function notifyExtractError(msg) {
      report.errors.push(msg);
      if (options.onExtractError) options.onExtractError(msg);
    }
    function formatBytes(n) {
      if (n == null || n < 0) return '0 B';
      if (n < 1024) return n + ' B';
      if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
      return (n / (1024 * 1024)).toFixed(1) + ' MB';
    }

    report.extraction_entries_attempted = 0;
    report.extraction_entries_failed = 0;
    report.extraction_zip_entries_total = 0;
    report.extraction_zip_entries_images = 0;
    report.extraction_zip_entries_videos = 0;
    report.extraction_zip_entries_other = 0;

    function countZipEntryType(basename) {
      report.extraction_zip_entries_total = (report.extraction_zip_entries_total || 0) + 1;
      if (isImage && isImage(basename)) report.extraction_zip_entries_images = (report.extraction_zip_entries_images || 0) + 1;
      else if (isVideo && isVideo(basename)) report.extraction_zip_entries_videos = (report.extraction_zip_entries_videos || 0) + 1;
      else report.extraction_zip_entries_other = (report.extraction_zip_entries_other || 0) + 1;
    }
    var listPromise = inputHandle ? listDir(inputHandle, '', log) : Promise.resolve([]);
    return listPromise.then(function (files) {
      var extractZip = options.extractZip !== false;
      var zipHandles = extractZip
        ? ((options.directZipBuffers && options.directZipBuffers.length > 0)
            ? options.directZipBuffers.map(function (b) {
                return { name: b.name, full: b.name, buffer: b.buffer };
              })
            : (options.directZipFiles && options.directZipFiles.length > 0)
              ? options.directZipFiles.map(function (f) {
                  return { name: f.name, full: f.name, file: f };
                })
              : files.filter(function (f) {
                  return f.name.toLowerCase().endsWith('.zip');
                }))
        : [];
      var outputFilenames = {
        'report.json': true,
        'file_map.json': true,
        'report.csv': true,
        'content_directory_paths.json': true,
        'not_merged_list.json': true,
        'summary_report.json': true,
        'summary_report.txt': true,
        'media_db.json': true,
        'media_index_summary.json': true,
        'duplicates_list.json': true,
        'duplicates_list.txt': true,
        'match_list.json': true,
        'matching_media_json.json': true,
        'matching_media_json.txt': true,
        'lake_index_cache.json': true
      };
      var nonZipFiles = inputHandle
        ? files.filter(function (f) {
            if (f.name.toLowerCase().endsWith('.zip')) return false;
            var pathNorm = (f.full || f.name || '').replace(/\\/g, '/').toLowerCase();
            if (pathNorm.startsWith('processed/')) return false;
            if (pathNorm.startsWith('lake/')) return false;
            var base = pathBasename(f.full || f.name || '').toLowerCase();
            if (outputFilenames[base]) return false;
            return true;
          })
        : [];

      function extractOneZip(zipEntry, onChunkRead) {
        var zipName = zipEntry.name;
        if (typeof GphotoZip === 'undefined') {
          log('Skip zip (GphotoZip not loaded): ' + zipName);
          return Promise.resolve();
        }
        if (zipEntry.buffer) {
          return extractOneZipFromBuffer(zipEntry, zipName, onChunkRead);
        }
        var filePromise = zipEntry.file
          ? Promise.resolve(zipEntry.file)
          : zipEntry.handle.getFile();
        return filePromise.then(function (file) {
          return extractOneZipStreaming(file, zipName, onChunkRead);
        }).catch(function (err) {
          notifyExtractError('zip ' + safeErrorPath(zipName) + ': ' + (err && err.message ? err.message : String(err)));
        });
      }

      function extractOneZipFromBuffer(zipEntry, zipName, onChunkRead) {
        var buf = zipEntry.buffer;
        if (onChunkRead) onChunkRead(0);
        var entries = GphotoZip.unzipToFiles(buf);
        var items = [];
        for (var pathInZip in entries) {
          if (!Object.prototype.hasOwnProperty.call(entries, pathInZip) || pathInZip.endsWith('/')) continue;
          countZipEntryType(pathBasename(pathInZip));
          var baseName = sanitizeNameForFS(pathBasename(pathInZip));
          var resolvedName = resolveLakeName(collisionCounter, baseName, takenSet);
          var data = entries[pathInZip];
          var bufToWrite = data instanceof ArrayBuffer ? data : (data && data.buffer) || data;
          fileMap[resolvedName] = {
            original_path: pathInZip,
            parent_folder: parentFolderFromPath(pathInZip),
            from_zip: zipName
          };
          items.push({ name: resolvedName, buf: bufToWrite });
        }
        report.extraction_entries_attempted = (report.extraction_entries_attempted || 0) + items.length;
        var p = Promise.resolve();
        for (var i = 0; i < items.length; i++) {
          (function (item) {
            p = p.then(function () {
              if (dryRun) return Promise.resolve();
              return writeFileToDir(lakeHandle, item.name, item.buf).catch(function (err) {
                var msg = err && err.message ? err.message : String(err);
                reportExtractError(safeErrorPath(item.name) + ': ' + msg);
                if (!report.problematic_files) report.problematic_files = [];
                report.problematic_files.push({ file: safeErrorPath(item.name), reason: msg });
                return Promise.resolve();
              });
            });
          })(items[i]);
        }
        if (options.verbose) log('Extracted zip: ' + zipName);
        if (onChunkRead) onChunkRead(buf.byteLength || 0);
        return p;
      }

      function extractOneZipStreaming(file, zipName, onChunkRead) {
        if (typeof fflate === 'undefined' || typeof fflate.Unzip !== 'function') {
          return Promise.reject(new Error('fflate.Unzip not available'));
        }
        var pendingCloses = [];
        var unzip = new fflate.Unzip(function (zipFile) {
          var pathInZip = zipFile.name;
          if (pathInZip.endsWith('/')) return;
          countZipEntryType(pathBasename(pathInZip));
          report.extraction_entries_attempted = (report.extraction_entries_attempted || 0) + 1;
          var baseName = sanitizeNameForFS(pathBasename(pathInZip));
          var resolvedName = resolveLakeName(collisionCounter, baseName, takenSet);
          fileMap[resolvedName] = {
            original_path: pathInZip,
            parent_folder: parentFolderFromPath(pathInZip),
            from_zip: zipName
          };
          if (dryRun) {
            zipFile.start();
            return;
          }
          var closeResolver;
          var closePromise = new Promise(function (r) { closeResolver = r; });
          pendingCloses.push(closePromise);
          var bytesWrittenForEntry = 0;
          lakeHandle.getFileHandle(resolvedName, { create: true }).then(function (h) {
            return h.createWritable();
          }).then(function (writable) {
            var closed = false;
            zipFile.ondata = function (err, chunk, final) {
              if (err) {
                reportExtractError('zip ' + safeErrorPath(zipName) + ' ' + zipEntryLabel(pathInZip) + ': ' + (err && err.message ? err.message : String(err)) + ' (wrote ' + formatBytes(bytesWrittenForEntry) + ' before error)');
                closeResolver();
                return;
              }
              if (chunk && chunk.length) {
                bytesWrittenForEntry += chunk.length;
                var w = writable.write(chunk);
                if (w && typeof w.then === 'function') w.catch(function (e) {
                  reportExtractError(zipEntryLabel(pathInZip) + ': ' + (e && e.message ? e.message : String(e)) + ' (wrote ' + formatBytes(bytesWrittenForEntry) + ' before error)');
                  closeResolver();
                });
              }
              if (final && !closed) {
                closed = true;
                var closeP = writable.close();
                (closeP && typeof closeP.then === 'function' ? closeP : Promise.resolve()).then(closeResolver).catch(function (e) {
                  reportExtractError(zipEntryLabel(pathInZip) + ': ' + (e && e.message ? e.message : String(e)) + ' (wrote ' + formatBytes(bytesWrittenForEntry) + ' before error)');
                  closeResolver();
                });
              }
            };
            zipFile.start();
          }).catch(function (err) {
            reportExtractError('zip ' + safeErrorPath(zipName) + ' ' + zipEntryLabel(pathInZip) + ': ' + (err && err.message ? err.message : String(err)) + (bytesWrittenForEntry ? ' (wrote ' + formatBytes(bytesWrittenForEntry) + ' before error)' : ''));
            closeResolver();
          });
        });
        if (typeof fflate.AsyncUnzipInflate === 'function') {
          unzip.register(fflate.AsyncUnzipInflate);
        } else if (typeof fflate.UnzipInflate === 'function') {
          unzip.register(fflate.UnzipInflate);
        }
        var reader = file.stream().getReader();
        var STREAM_CHUNK = 200 * 1024 * 1024;
        var acc = [];
        var accLen = 0;
        var bytesReadFromZip = 0;
        function pump() {
          return reader.read().then(function (result) {
            if (result.value && result.value.length) {
              acc.push(result.value instanceof Uint8Array ? result.value : new Uint8Array(result.value));
              accLen += result.value.length;
              bytesReadFromZip += result.value.length;
              if (onChunkRead) onChunkRead(bytesReadFromZip);
            }
            if (result.done && accLen === 0) {
              unzip.push(new Uint8Array(0), true);
              if (onChunkRead) onChunkRead(file.size || bytesReadFromZip);
              if (options.verbose) log('Extracted zip (stream): ' + zipName);
              return Promise.all(pendingCloses);
            }
            var shouldPush = result.done || accLen >= STREAM_CHUNK;
            if (shouldPush && accLen > 0) {
              var combined = new Uint8Array(accLen);
              var off = 0;
              for (var j = 0; j < acc.length; j++) {
                combined.set(acc[j], off);
                off += acc[j].length;
              }
              unzip.push(combined, result.done);
              acc = [];
              accLen = 0;
            }
            if (result.done) {
              if (onChunkRead) onChunkRead(file.size || bytesReadFromZip);
              if (options.verbose) log('Extracted zip (stream): ' + zipName);
              return Promise.all(pendingCloses);
            }
            return pump();
          });
        }
        return pump();
      }

      function copyOneToLake(fileEntry) {
        var full = fileEntry.full;
        var baseName = sanitizeNameForFS(fileEntry.name);
        var resolvedName = resolveLakeName(collisionCounter, baseName, takenSet);
        fileMap[resolvedName] = {
          original_path: full,
          parent_folder: parentFolderFromPath(full),
          from_zip: false
        };
        if (dryRun) return Promise.resolve();
        return fileEntry.handle.getFile().then(function (f) {
          return f.arrayBuffer();
        }).then(function (buf) {
          return writeFileToDir(lakeHandle, resolvedName, buf);
        }).then(function () {
          if (options.deleteZipAfterExtract && inputHandle && !dryRun) {
            var parentPath = pathDirname(full);
            var fileName = pathBasename(full);
            if (parentPath) {
              return getOrCreatePath(inputHandle, parentPath).then(function (parentDir) {
                return parentDir.removeEntry(fileName);
              }).catch(function (err) {
                report.errors.push('delete source ' + safeErrorPath(full) + ': ' + (err && err.message ? err.message : String(err)));
              });
            }
            return inputHandle.removeEntry(fileName).catch(function (err) {
              report.errors.push('delete source ' + safeErrorPath(full) + ': ' + (err && err.message ? err.message : String(err)));
            });
          }
          return Promise.resolve();
        }).catch(function (err) {
          var msg = err && err.message ? err.message : String(err);
          report.errors.push(safeErrorPath(full) + ': ' + msg);
          if (!report.problematic_files) report.problematic_files = [];
          report.problematic_files.push({ file: safeErrorPath(full), reason: msg });
          return Promise.resolve();
        });
      }

      function getZipSize(zipEntry) {
        if (zipEntry.buffer) return Promise.resolve(zipEntry.buffer.byteLength || 0);
        if (zipEntry.file) return Promise.resolve(zipEntry.file.size || 0);
        if (zipEntry.handle) return zipEntry.handle.getFile().then(function (f) { return f.size || 0; }).catch(function () { return 0; });
        return Promise.resolve(0);
      }

      function moveOtherFoldersToLake() {
        return listTopLevel(outputHandle).then(function (top) {
          var otherDirs = top.filter(function (e) {
            var nameLower = (e.name || '').toLowerCase();
            return e.kind === 'directory' && nameLower !== 'lake' && nameLower !== 'processed';
          });
          if (otherDirs.length === 0) return Promise.resolve();
          return Promise.all(otherDirs.map(function (d) {
            return listDir(d.handle, d.name, log);
          })).then(function (arrays) {
            var otherFiles = [];
            for (var a = 0; a < arrays.length; a++) otherFiles.push.apply(otherFiles, arrays[a]);
            var toCopy = otherFiles.filter(function (fileEntry) {
              return !Object.prototype.hasOwnProperty.call(fileMap, fileEntry.name);
            });
            report.extraction_entries_attempted = (report.extraction_entries_attempted || 0) + toCopy.length;
            var skipped = otherFiles.length - toCopy.length;
            if (options.verbose && skipped) log('Skipped ' + skipped + ' files already in lake (from other folders).');
            var p = Promise.resolve();
            toCopy.forEach(function (fileEntry) {
              p = p.then(function () {
                return copyOneToLake(fileEntry).catch(function (err) {
                  var msg = err && err.message ? err.message : String(err);
                  reportExtractError(safeErrorPath(fileEntry.full || fileEntry.name) + ': ' + msg);
                  if (!report.problematic_files) report.problematic_files = [];
                  report.problematic_files.push({ file: safeErrorPath(fileEntry.full || fileEntry.name), reason: msg });
                  return Promise.resolve();
                });
              });
            });
            if (options.verbose && toCopy.length) log('Moved ' + toCopy.length + ' files from other folders to lake.');
            return p;
          });
        });
      }

      var seq = Promise.resolve();
      if (zipHandles.length === 0) {
        log('No zip files to extract.');
        if (options.onExtractProgress) options.onExtractProgress(100);
      } else {
        seq = Promise.all(zipHandles.map(function (z) { return getZipSize(z); })).then(function (sizes) {
          var totalExtractBytes = 0;
          for (var si = 0; si < sizes.length; si++) totalExtractBytes += sizes[si];
          if (totalExtractBytes <= 0) totalExtractBytes = 1;
          var cumulativeBefore = [];
          for (var c = 0, sum = 0; c < sizes.length; c++) {
            cumulativeBefore[c] = sum;
            sum += sizes[c];
          }
          var totalMB = Math.round(totalExtractBytes / 1024 / 1024);
          log('Starting zip extraction (' + zipHandles.length + ' zip(s), ~' + totalMB + ' MB total).');
          var completedExtractBytes = 0;
          var zipSeq = Promise.resolve();
          for (var zi = 0; zi < zipHandles.length; zi++) {
            (function (zipEntry, size, index) {
              var completedBeforeThisZip = cumulativeBefore[index];
              var onChunkRead = options.onExtractProgress ? function (bytesRead) {
                options.onExtractProgress(Math.min(100, 100 * (completedBeforeThisZip + bytesRead) / totalExtractBytes));
              } : null;
              zipSeq = zipSeq.then(function () {
                log('Extracting zip ' + (index + 1) + '/' + zipHandles.length + ': ' + zipEntry.name);
                return extractOneZip(zipEntry, onChunkRead).catch(function (err) {
                  var msg = err && err.message ? err.message : String(err);
                  notifyExtractError('zip ' + safeErrorPath(zipEntry.name) + ': ' + msg);
                  if (!report.problematic_files) report.problematic_files = [];
                  report.problematic_files.push({ file: safeErrorPath(zipEntry.name), reason: msg });
                  return Promise.resolve();
                });
              }).then(function () {
                log('Finished zip ' + (index + 1) + '/' + zipHandles.length + ': ' + zipEntry.name);
                completedExtractBytes += size;
                if (options.onExtractProgress) options.onExtractProgress(Math.min(100, 100 * completedExtractBytes / totalExtractBytes));
                if (options.deleteZipAfterExtract && inputHandle && !dryRun && zipEntry.handle) {
                  return inputHandle.removeEntry(zipEntry.name).catch(function (err) {
                    notifyExtractError('delete zip ' + safeErrorPath(zipEntry.name) + ': ' + (err && err.message ? err.message : String(err)));
                  });
                }
                return Promise.resolve();
              });
            })(zipHandles[zi], sizes[zi], zi);
          }
          return zipSeq;
        });
      }
      if (nonZipFiles.length > 0) {
        report.extraction_entries_attempted = (report.extraction_entries_attempted || 0) + nonZipFiles.length;
        log('Copying ' + nonZipFiles.length + ' non-zip file(s) to lake...');
      }
      nonZipFiles.forEach(function (fileEntry) {
        seq = seq.then(function () {
          return copyOneToLake(fileEntry).catch(function (err) {
            var msg = err && err.message ? err.message : String(err);
            reportExtractError(safeErrorPath(fileEntry.full || fileEntry.name) + ': ' + msg);
            if (!report.problematic_files) report.problematic_files = [];
            report.problematic_files.push({ file: safeErrorPath(fileEntry.full || fileEntry.name), reason: msg });
            return Promise.resolve();
          });
        });
      });

      return seq.then(moveOtherFoldersToLake).then(function () {
        var att = report.extraction_entries_attempted || 0;
        var fail = report.extraction_entries_failed || 0;
        var ok = att - fail;
        var pct = att > 0 ? Math.round(100 * ok / att) : 100;
        var zipTotal = report.extraction_zip_entries_total || 0;
        var zipImg = report.extraction_zip_entries_images || 0;
        var zipVid = report.extraction_zip_entries_videos || 0;
        var zipOther = report.extraction_zip_entries_other || 0;
        report.extraction_summary = {
          entries_attempted: att,
          entries_failed: fail,
          entries_ok: ok,
          success_percent: pct,
          zip_entries_total: zipTotal,
          zip_entries_images: zipImg,
          zip_entries_videos: zipVid,
          zip_entries_other: zipOther
        };
        if (fail > 0) {
          log('Extraction finished with errors: ' + pct + '% of entries succeeded (' + fail + ' failed).');
        } else {
          log('Extraction finished: ' + (att > 0 ? pct + '% of entries succeeded.' : 'No entries to extract.'));
        }
        log('Extraction: ' + zipTotal + ' entries in zips (' + zipImg + ' images, ' + zipVid + ' videos, ' + zipOther + ' other). Extracted: ' + ok + ' ok, ' + fail + ' failed.');
        if (options.onExtractionSummary) options.onExtractionSummary(report.extraction_summary);
        log('Lake built. Listing files...');
        return outputHandle.getDirectoryHandle('lake').then(function (lakeHandleForList) {
          return listTopLevel(lakeHandleForList).then(function (entries) {
            var list = [];
            for (var i = 0; i < entries.length; i++) {
              if (entries[i].kind !== 'file') continue;
              list.push({ name: entries[i].name, full: entries[i].name, handle: entries[i].handle });
            }
            ctx.lakeFiles = list;
            ctx.lakeHandle = lakeHandleForList;
            return undefined;
          });
        });
      });
    });
  };
})();
