/**
 * Merge phase: match media to meta, EXIF inject, write to Processed, build canonicalPathMap.
 * Expects ctx: lakeHandle, processedHandle, outputHandle, fileMap, index, report, options, canonicalPathMap,
 * not_merged (optional), listTopLevel (optional), and helpers: matchMediaToMeta, stem, readJsonFile, getOrCreatePath, writeFileToDir, yearFromRecord, isImage, timestampsMatch, safeErrorPath, pathBasename, sanitizeNameForFS, SUPPLEMENTAL, SUPPLEMENTAL_NO_JSON.
 * Sets ctx.mergeResult = { report, canonicalPathMap, index, processedHandle }.
 */
(function () {
  'use strict';

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

  g.GphotoPipelinePhases.runMerge = function (ctx, log) {
    log = log || function () {};
    var lakeHandle = ctx.lakeHandle;
    var processedHandle = ctx.processedHandle;
    var outputHandle = ctx.outputHandle;
    var fileMap = ctx.fileMap;
    var index = ctx.index;
    var report = ctx.report;
    var options = ctx.options || {};
    var dryRun = options.dryRun === true;
    var canonicalPathMap = ctx.canonicalPathMap;
    var lakeFiles = ctx.lakeFiles || [];
    var matchMediaToMeta = ctx.matchMediaToMeta;
    var stem = ctx.stem;
    var readJsonFile = ctx.readJsonFile;
    var getOrCreatePath = ctx.getOrCreatePath;
    var writeFileToDir = ctx.writeFileToDir;
    var yearFromRecord = ctx.yearFromRecord;
    var isImage = ctx.isImage;
    var isVideo = ctx.isVideo;
    var timestampsMatch = ctx.timestampsMatch;
    var safeErrorPath = ctx.safeErrorPath;
    var pathBasename = ctx.pathBasename;
    var sanitizeNameForFS = ctx.sanitizeNameForFS;
    var SUPPLEMENTAL = ctx.SUPPLEMENTAL || '.supplemental-metadata.json';
    var SUPPLEMENTAL_NO_JSON = ctx.SUPPLEMENTAL_NO_JSON || '.supplemental-metadata';

    if (!report.remerged) report.remerged = [];
    if (!report.moved_to_duplicates) report.moved_to_duplicates = [];
    if (options.verbose) log('Lake files: ' + lakeFiles.length);
    var matched = matchMediaToMeta(lakeFiles);
    function findMetaForStem(stemKey, fileList) {
      for (var i = 0; i < fileList.length; i++) {
        var f = fileList[i];
        var n = f.name;
        if (stem(n) !== stemKey) continue;
        if (n.endsWith(SUPPLEMENTAL) || n.endsWith(SUPPLEMENTAL_NO_JSON) || n.endsWith('.json')) return f;
      }
      return null;
    }
    function findIndexRecordForMedia(mediaName) {
      if (!index || !index.media_by_filename) return null;
      var norm = ctx.normalizeBaseName(mediaName);
      var arr = index.media_by_filename[norm];
      if (!arr) return null;
      for (var i = 0; i < arr.length; i++) {
        if (arr[i].filename === mediaName) return arr[i];
      }
      return arr[0] || null;
    }
    function findRecordById(recordId) {
      if (!index || !index.records) return null;
      for (var r = 0; r < index.records.length; r++) {
        if (index.records[r].id === recordId) return index.records[r];
      }
      return null;
    }
    function findLakeFileByName(name) {
      for (var l = 0; l < lakeFiles.length; l++) {
        if (lakeFiles[l].name === name) return lakeFiles[l];
      }
      return null;
    }
    var toProcess = [];
    var withMeta = 0;
    Object.keys(matched.byBase).forEach(function (base) {
      var rec = matched.byBase[base];
      if (!rec.media) return;
      var mediaName = rec.media.name;
      var mediaEntry = rec.media;
      var indexRecord = findIndexRecordForMedia(mediaName);
      if (index && indexRecord && indexRecord.is_duplicate === true && indexRecord.duplicate_of != null) {
        var canonicalRec = findRecordById(indexRecord.duplicate_of);
        if (canonicalRec) {
          var canonicalFile = findLakeFileByName(canonicalRec.filename);
          if (canonicalFile) {
            mediaEntry = canonicalFile;
            mediaName = canonicalRec.filename;
          } else {
            /* Canonical (original) not in lake; skip merging this duplicate so original can be merged when present. */
            return;
          }
        }
      }
      var metaFile = rec.meta || findMetaForStem(base, matched.files);
      if (metaFile) withMeta++;
      var existing = toProcess.find(function (x) { return x.mediaName === mediaName; });
      if (!existing) {
        toProcess.push({ mediaEntry: mediaEntry, metaEntry: metaFile, mediaName: mediaName });
      } else if (metaFile && !existing.metaEntry) {
        existing.metaEntry = metaFile;
      }
    });
    if (options.verbose) log('Matched stems with media+meta: ' + withMeta + ', toProcess: ' + toProcess.length + ' (with meta: ' + toProcess.filter(function (x) { return x.metaEntry; }).length + ')');
    var totalToProcess = toProcess.length;
    log('Matching metadata and media. Found ' + totalToProcess + ' file(s) to process.');
    var matchList = {
      summary: {
        lake_file_count: lakeFiles.length,
        stem_count: Object.keys(matched.byBase).length,
        to_process_count: toProcess.length,
        to_process_with_meta_count: toProcess.filter(function (x) { return x.metaEntry; }).length
      },
      matches: toProcess.map(function (item) {
        return {
          media: item.mediaName,
          meta: item.metaEntry ? item.metaEntry.name : null,
          stem: stem(item.mediaName),
          will_merge: !!(item.metaEntry && (isImage(item.mediaName) || item.mediaName.indexOf('.') < 0))
        };
      })
    };
    var matchListJson = JSON.stringify(matchList, null, 2);
    var matchingPairs = toProcess.filter(function (item) { return item.metaEntry; }).map(function (item) {
      return { media: item.mediaName, json: item.metaEntry.name };
    });
    var matchingMediaJson = {
      generated_at: new Date().toISOString(),
      total_matching_pairs: matchingPairs.length,
      pairs: matchingPairs
    };
    var matchingMediaJsonJson = JSON.stringify(matchingMediaJson, null, 2);
    var matchingMediaJsonTxt = ['Matching media–JSON pairs', 'Generated: ' + matchingMediaJson.generated_at, 'Total: ' + matchingMediaJson.total_matching_pairs, ''].concat(matchingPairs.map(function (p) { return p.media + ' <-> ' + p.json; })).join('\n');
    var deleteOriginals = options.deleteOriginalsAfterMerge === true && !dryRun;
    if (options.onMergeProgress) options.onMergeProgress(totalToProcess === 0 ? 100 : 0);
    if (totalToProcess > 0) log('Starting merge/transfer (' + totalToProcess + ' file(s)).');
    var listTopLevel = ctx.listTopLevel || function () { return Promise.resolve([]); };
    function findProcessedFile(name) {
      var safe = sanitizeNameForFS(name);
      return processedHandle.getFileHandle(safe).then(function () {
        return { dir: processedHandle, name: name, relativePath: name };
      }).catch(function () {
        return listTopLevel(processedHandle).then(function (entries) {
          var p = Promise.resolve(null);
          for (var i = 0; i < entries.length; i++) {
            if (entries[i].kind !== 'directory') continue;
            (function (entry) {
              p = p.then(function (found) {
                if (found) return found;
                return entry.handle.getFileHandle(safe).then(function () {
                  return { dir: entry.handle, name: name, relativePath: entry.name + '/' + name };
                }).catch(function () { return null; });
              });
            })(entries[i]);
          }
          return p;
        });
      });
    }
    var notMergedNames = ctx.not_merged || [];
    var remergeListPromise = Promise.all(notMergedNames.map(function (name) {
      var metaEntry = findMetaForStem(stem(name), matched.files);
      if (!metaEntry || !isImage(name)) return Promise.resolve(null);
      return findProcessedFile(name).then(function (loc) {
        return loc ? { mediaName: name, metaEntry: metaEntry, processedDir: loc.dir, processedName: loc.name, relativePath: loc.relativePath } : null;
      });
    })).then(function (results) { return results.filter(Boolean); });

    var mergeSeq = writeFileToDir(outputHandle, 'match_list.json', new TextEncoder().encode(matchListJson)).then(function () {
      return writeFileToDir(outputHandle, 'matching_media_json.json', new TextEncoder().encode(matchingMediaJsonJson)).then(function () {
        return writeFileToDir(outputHandle, 'matching_media_json.txt', new TextEncoder().encode(matchingMediaJsonTxt));
      });
    }).then(function () {
      return remergeListPromise;
    }).then(function (remergeList) {
      if (remergeList.length > 0) {
        log('Re-merge: found ' + remergeList.length + ' previously not-merged file(s) with a JSON match in the lake.');
        if (totalToProcess === 0 && options.onMergeProgress) options.onMergeProgress(0);
      }
      var seq = Promise.resolve();
      var processedCount = 0;
      remergeList.forEach(function (item) {
        seq = seq.then(function () {
          var injectFn = (typeof options.injectExif === 'function') ? options.injectExif : (typeof GphotoExif !== 'undefined' && GphotoExif.injectExifFromGoogleJson);
          if (!injectFn) {
            report.errors.push(safeErrorPath(item.mediaName) + ': EXIF injector not available (re-merge skipped).');
            return Promise.resolve();
          }
          if (dryRun) return Promise.resolve();
          return readJsonFile(item.metaEntry.handle).then(function (meta) {
            return item.processedDir.getFileHandle(sanitizeNameForFS(item.processedName)).then(function (h) { return h.getFile(); }).then(function (f) { return f.arrayBuffer(); }).then(function (imageBuffer) {
              var newImageBuffer = injectFn(imageBuffer, meta);
              if (!newImageBuffer || !(newImageBuffer instanceof ArrayBuffer)) {
                report.errors.push(safeErrorPath(item.mediaName) + ': EXIF injection did not return a new image buffer (re-merge).');
                return Promise.resolve();
              }
              return writeFileToDir(item.processedDir, item.processedName, newImageBuffer);
            });
          }).then(function () {
            if (!report.merged) report.merged = []; report.merged.push(item.mediaName);
            report.remerged.push(item.mediaName);
            if (ctx.canonicalPathMap) ctx.canonicalPathMap[item.mediaName] = item.relativePath;
            if (options.verbose) log('Re-merged (JSON match from lake): ' + item.mediaName);
            return Promise.resolve();
          }).catch(function (err) {
            var msg = err && err.message ? err.message : String(err);
            report.errors.push(safeErrorPath(item.mediaName) + ' (re-merge): ' + msg);
            if (!report.problematic_files) report.problematic_files = [];
            report.problematic_files.push({ file: safeErrorPath(item.mediaName), reason: msg });
            return Promise.resolve();
          });
        }).then(function () {
          processedCount++;
          if (options.onMergeProgress && totalToProcess + remergeList.length > 0) options.onMergeProgress(Math.min(100, 100 * processedCount / (totalToProcess + remergeList.length)));
          return new Promise(function (r) { setTimeout(r, 0); });
        });
      });
      toProcess.forEach(function (item) {
        var mediaEntry = item.mediaEntry;
        var metaEntry = item.metaEntry;
        var mediaName = item.mediaName;
        var mediaHandle = mediaEntry.handle;
        var indexRecord = findIndexRecordForMedia(mediaName);
        seq = seq.then(function () {
          function getProcessedDirAndPath() {
            if (!options.singleLake && indexRecord) {
              var year = yearFromRecord(indexRecord);
              if (year) {
                return getOrCreatePath(processedHandle, year).then(function (dir) {
                  return { dir: dir, relativePath: year + '/' + mediaName };
                });
              }
            }
            return Promise.resolve({ dir: processedHandle, relativePath: mediaName });
          }
          function getDuplicateDirAndPath() {
            if (!options.singleLake && indexRecord) {
              var year = yearFromRecord(indexRecord);
              if (year) {
                return getOrCreatePath(processedHandle, year + '/Duplicates').then(function (dir) {
                  return { dir: dir, relativePath: year + '/Duplicates/' + mediaName };
                });
              }
            }
            return getOrCreatePath(processedHandle, 'Duplicates').then(function (dir) {
              return { dir: dir, relativePath: 'Duplicates/' + mediaName };
            });
          }
          function getDuplicateDirForRecord(rec, fileName) {
            if (!options.singleLake && rec) {
              var y = yearFromRecord(rec);
              if (y) return getOrCreatePath(processedHandle, y + '/Duplicates').then(function (dir) { return dir; });
            }
            return getOrCreatePath(processedHandle, 'Duplicates');
          }
          function moveDuplicatesOfCanonicalToDuplicates(canonicalRec) {
            if (!index || !index.records || !canonicalRec || dryRun) return Promise.resolve();
            var dupes = index.records.filter(function (r) { return r.duplicate_of === canonicalRec.id; });
            var p = Promise.resolve();
            dupes.forEach(function (r) {
              var dupName = r.filename;
              var dupFile = findLakeFileByName(dupName);
              if (!dupFile) return;
              p = p.then(function () {
                return getDuplicateDirForRecord(r, dupName).then(function (dupDir) {
                  return dupFile.handle.getFile().then(function (f) { return f.arrayBuffer(); }).then(function (buf) {
                    return writeFileToDir(dupDir, dupName, buf).then(function () {
                      return lakeHandle.removeEntry(dupName).catch(function (e) {
                        report.errors.push('move duplicate to Duplicates ' + safeErrorPath(dupName) + ': ' + (e && e.message ? e.message : String(e)));
                      });
                    });
                  });
                }).catch(function (e) {
                  report.errors.push('move duplicate ' + safeErrorPath(dupName) + ': ' + (e && e.message ? e.message : String(e)));
                });
              });
            });
            return p;
          }
          var isImg = isImage(mediaName) || (metaEntry && mediaName.indexOf('.') < 0);
          var injectFn = (typeof options.injectExif === 'function') ? options.injectExif : (typeof GphotoExif !== 'undefined' && GphotoExif.injectExifFromGoogleJson);
          var useCanonicalLogic = index && indexRecord && isImg && metaEntry && injectFn;
          var exifMatchedJson = useCanonicalLogic && timestampsMatch(indexRecord.exif_timestamp, indexRecord.json_timestamp);
          if (isImg && metaEntry && !injectFn) {
            report.errors.push(safeErrorPath(mediaName) + ': EXIF injector not available (transferred without merge). Open extension in new tab, select folders, then Apply.');
          }
          if (isImg && metaEntry && injectFn) {
            return mediaHandle.getFile().then(function (f) {
              return f.arrayBuffer();
            }).then(function (imageBuffer) {
              return readJsonFile(metaEntry.handle).then(function (meta) {
                if (indexRecord && indexRecord.albums && indexRecord.albums.length) {
                  meta.albums = indexRecord.albums;
                }
                var newImageBuffer = injectFn(imageBuffer, meta);
                if (!newImageBuffer || !(newImageBuffer instanceof ArrayBuffer)) {
                  report.errors.push(safeErrorPath(mediaName) + ': EXIF injection did not return a new image buffer');
                  return Promise.resolve();
                }
                return getProcessedDirAndPath().then(function (out) {
                  return out.dir.getFileHandle(sanitizeNameForFS(mediaName)).then(function () { return { exists: true, out: out }; }).catch(function () { return { exists: false, out: out }; }).then(function (check) {
                    if (check.exists && !dryRun) {
                      return out.dir.getFileHandle(sanitizeNameForFS(mediaName)).then(function (h) { return h.getFile(); }).then(function (f) { return f.arrayBuffer(); }).then(function (existingBuf) {
                        return getDuplicateDirAndPath().then(function (dup) {
                          return writeFileToDir(dup.dir, mediaName, existingBuf).then(function () {
                            return out.dir.removeEntry(sanitizeNameForFS(mediaName)).then(function () {
                              report.moved_to_duplicates.push(mediaName);
                              if (options.verbose) log('Moved existing to Duplicates, replacing with: ' + mediaName);
                              return writeFileToDir(out.dir, mediaName, newImageBuffer).then(function () { return out; });
                            });
                          });
                        });
                      }).catch(function (err) {
                        report.errors.push(safeErrorPath(mediaName) + ' (move to Duplicates): ' + (err && err.message ? err.message : String(err)));
                        return Promise.resolve();
                      });
                    }
                    if (check.exists && dryRun) {
                      report.skipped_existing.push(mediaName);
                      canonicalPathMap[mediaName] = check.out.relativePath;
                      return Promise.resolve();
                    }
                    return writeFileToDir(check.out.dir, mediaName, newImageBuffer).then(function () { return check.out; });
                  }).then(function (out) {
                    if (!out) return Promise.resolve();
                    canonicalPathMap[mediaName] = out.relativePath;
                    report.merged.push(mediaName);
                    if (exifMatchedJson) {
                      report.exif_restored.push(mediaName);
                      if (options.verbose) log('Merged and Transferred: ' + mediaName);
                    } else {
                      report.exif_replaced.push(mediaName);
                      if (options.verbose) log('Merged and Transferred: ' + mediaName);
                    }
                    return moveDuplicatesOfCanonicalToDuplicates(indexRecord).then(function () {
                      if (deleteOriginals) {
                        return lakeHandle.removeEntry(mediaName).catch(function (e) {
                          report.errors.push('delete original ' + safeErrorPath(mediaName) + ': ' + (e && e.message ? e.message : String(e)));
                        }).then(function () {
                          return lakeHandle.removeEntry(metaEntry.name).catch(function (e) {
                            report.errors.push('delete meta ' + safeErrorPath(metaEntry.name) + ': ' + (e && e.message ? e.message : String(e)));
                          });
                        });
                      }
                      return Promise.resolve();
                    });
                  });
                });
              });
            }).catch(function (err) {
              var msg = err && err.message ? err.message : String(err);
              report.errors.push(safeErrorPath(mediaName) + ': ' + msg);
              if (!report.problematic_files) report.problematic_files = [];
              report.problematic_files.push({ file: safeErrorPath(mediaName), reason: msg });
            });
          }
          if (!metaEntry && options.transferUnmatchedMedia !== true) {
            return Promise.resolve();
          }
          return getProcessedDirAndPath().then(function (out) {
            return out.dir.getFileHandle(sanitizeNameForFS(mediaName)).then(function () { return { exists: true, out: out }; }).catch(function () { return { exists: false, out: out }; }).then(function (check) {
              if (check.exists && !dryRun) {
                return out.dir.getFileHandle(sanitizeNameForFS(mediaName)).then(function (h) { return h.getFile(); }).then(function (f) { return f.arrayBuffer(); }).then(function (existingBuf) {
                  return getDuplicateDirAndPath().then(function (dup) {
                    return writeFileToDir(dup.dir, mediaName, existingBuf).then(function () {
                      return out.dir.removeEntry(sanitizeNameForFS(mediaName)).then(function () {
                        report.moved_to_duplicates.push(mediaName);
                        if (options.verbose) log('Moved existing to Duplicates, replacing with: ' + mediaName);
                        return mediaHandle.getFile().then(function (f) {
                          return writeFileToDir(out.dir, mediaName, f).then(function () { return out; });
                        });
                      });
                    });
                  });
                }).catch(function (err) {
                  report.errors.push(safeErrorPath(mediaName) + ' (move to Duplicates): ' + (err && err.message ? err.message : String(err)));
                  return Promise.resolve();
                });
              }
              if (check.exists && dryRun) {
                report.skipped_existing.push(mediaName);
                canonicalPathMap[mediaName] = check.out.relativePath;
                return Promise.resolve();
              }
              return mediaHandle.getFile().then(function (f) {
                return writeFileToDir(check.out.dir, mediaName, f).then(function () { return check.out; });
              });
            }).then(function (out) {
              if (!out) return Promise.resolve();
              canonicalPathMap[mediaName] = out.relativePath;
              report.transferred.push(mediaName);
              if (options.verbose) log('Transferred: ' + mediaName);
              return moveDuplicatesOfCanonicalToDuplicates(indexRecord).then(function () { return out; });
            }).then(function (out) {
              if (!out) return Promise.resolve();
              if (isVideo(mediaName) && metaEntry) {
                return metaEntry.handle.getFile().then(function (jsonFile) {
                  return jsonFile.arrayBuffer().then(function (buf) {
                    return writeFileToDir(out.dir, metaEntry.name, buf);
                  });
                }).then(function () {
                  if (options.verbose) log('Moved matched JSON with video: ' + metaEntry.name);
                }).catch(function (e) {
                  report.errors.push('move JSON ' + safeErrorPath(metaEntry.name) + ': ' + (e && e.message ? e.message : String(e)));
                }).then(function () {
                  if (dryRun) return Promise.resolve();
                  return lakeHandle.removeEntry(mediaName).catch(function (e) {
                    report.errors.push('move video ' + safeErrorPath(mediaName) + ': ' + (e && e.message ? e.message : String(e)));
                  }).then(function () {
                    return lakeHandle.removeEntry(metaEntry.name).catch(function (e) {
                      report.errors.push('move meta ' + safeErrorPath(metaEntry.name) + ': ' + (e && e.message ? e.message : String(e)));
                    });
                  });
                });
              }
              if (deleteOriginals) {
                return lakeHandle.removeEntry(mediaName).catch(function (e) {
                  report.errors.push('delete original ' + safeErrorPath(mediaName) + ': ' + (e && e.message ? e.message : String(e)));
                }).then(function () {
                  if (metaEntry) {
                    return lakeHandle.removeEntry(metaEntry.name).catch(function (e) {
                      report.errors.push('delete meta ' + safeErrorPath(metaEntry.name) + ': ' + (e && e.message ? e.message : String(e)));
                    });
                  }
                  return Promise.resolve();
                });
              }
              return Promise.resolve();
            });
          }).catch(function (err) {
              var msg = err && err.message ? err.message : String(err);
              report.errors.push(safeErrorPath(mediaName) + ': ' + msg);
              if (!report.problematic_files) report.problematic_files = [];
              report.problematic_files.push({ file: safeErrorPath(mediaName), reason: msg });
              return Promise.resolve();
            });
        }).catch(function (err) {
          var msg = err && err.message ? err.message : String(err);
          report.errors.push(safeErrorPath(mediaName) + ': ' + msg);
          if (!report.problematic_files) report.problematic_files = [];
          report.problematic_files.push({ file: safeErrorPath(mediaName), reason: msg });
          return Promise.resolve();
        }).then(function () {
          processedCount++;
          if (options.onMergeProgress && totalToProcess + remergeList.length > 0) options.onMergeProgress(Math.min(100, 100 * processedCount / (totalToProcess + remergeList.length)));
          return new Promise(function (r) { setTimeout(r, 0); });
        });
      });
      return seq;
    }).then(function () {
      var remergedCount = (report.remerged && report.remerged.length) || 0;
      if (totalToProcess > 0 || remergedCount > 0) log('Merge complete. Merged: ' + (report.merged || []).length + ', Transferred: ' + (report.transferred || []).length + ', Skipped (existing): ' + (report.skipped_existing || []).length + (report.moved_to_duplicates && report.moved_to_duplicates.length > 0 ? ', Moved to Duplicates: ' + report.moved_to_duplicates.length : '') + (remergedCount > 0 ? ', Re-merged: ' + remergedCount : '') + '.');
      ctx.mergeResult = { report: report, canonicalPathMap: canonicalPathMap, index: index, options: options, processedHandle: processedHandle };
      return ctx.mergeResult;
    });
    return mergeSeq;
  };
})();
