/**
 * Media/metadata index (Section 2): read-only scan of Lake/ to build
 * MediaIndex (MediaRecord, JSONRecord, hashes, preliminary EXIF, albums, lookup maps, stats).
 * No filesystem changes. Used as a side product; merge does not consume this yet.
 */
(function (global) {
  'use strict';

  var IMAGE_EXT = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff', '.tif', '.heic', '.heif', '.raw', '.dng', '.cr2', '.nef', '.arw'];
  var VIDEO_EXT = ['.mp4', '.mov', '.avi', '.mkv', '.m4v', '.flv', '.wmv', '.webm'];
  var SUPPLEMENTAL = '.supplemental-metadata.json';
  var SUPPLEMENTAL_NO_JSON = '.supplemental-metadata';
  var SYSTEM_FILES = ['thumbs.db', '.ds_store'];

  function ext(name) {
    var i = name.lastIndexOf('.');
    return i >= 0 ? name.substring(i).toLowerCase() : '';
  }

  function baseNameNoExt(name) {
    var dot = name.lastIndexOf('.');
    return dot >= 0 ? name.slice(0, dot) : name;
  }

  function stem(name) {
    var i = name.indexOf('.');
    return i >= 0 ? name.slice(0, i) : name;
  }

  function isMediaFile(name) {
    if (SYSTEM_FILES.indexOf(name.toLowerCase()) >= 0) return false;
    var e = ext(name);
    return IMAGE_EXT.indexOf(e) >= 0 || VIDEO_EXT.indexOf(e) >= 0 || name.indexOf('.') < 0;
  }

  function isMetaFile(name) {
    return name.endsWith(SUPPLEMENTAL) || name.endsWith(SUPPLEMENTAL_NO_JSON) || name.toLowerCase().endsWith('.json');
  }

  /** Normalize base name for lookup: remove (1), (2), -edited, case insensitive, strip extension. */
  function normalizeBaseName(filename) {
    var base = baseNameNoExt(filename);
    base = base.replace(/\s*\(\d+\)\s*$/i, '').replace(/-edited\s*$/i, '').trim();
    return base.toLowerCase();
  }

  function readFileToArrayBuffer(file) {
    var size = Number(file.size) || 0;
    if (size === 0) return Promise.resolve(new ArrayBuffer(0));
    return file.arrayBuffer();
  }

  /** Stream file in chunks, then hash full buffer. Skips hashing if file size > maxBytes. */
  function hashFileStreamed(fileHandle, maxBytes, log) {
    return fileHandle.getFile().then(function (file) {
      var size = Number(file.size) || 0;
      if (size === 0) return Promise.resolve(null);
      if (maxBytes > 0 && size > maxBytes) return Promise.resolve(null);
      return readFileToArrayBuffer(file).then(function (buf) {
        return crypto.subtle.digest('SHA-256', buf).then(function (hashBuf) {
          var arr = new Uint8Array(hashBuf);
          var hex = '';
          for (var i = 0; i < arr.length; i++) hex += ('0' + arr[i].toString(16)).slice(-2);
          return hex;
        });
      }).catch(function (err) {
        if (log) log('Hash failed: ' + (err.message || err));
        return null;
      });
    }).catch(function () { return null; });
  }

  /** Extract timestamp string from JSON (photoTakenTime / creationTime). */
  function parsedTimestampFromMeta(meta) {
    if (!meta || typeof meta !== 'object') return null;
    var m = meta.metadata || meta;
    var pt = m.photoTakenTime || m.PhotoTakenTime;
    var ct = m.creationTime || m.CreationTime;
    if (pt && (pt.formatted || pt.timestamp != null)) return pt.formatted || String(pt.timestamp);
    if (ct && (ct.formatted || ct.timestamp != null)) return ct.formatted || String(ct.timestamp);
    if (m.photoTakenTimeTimestamp != null) return String(m.photoTakenTimeTimestamp);
    if (m.creationTimeTimestamp != null) return String(m.creationTimeTimestamp);
    return null;
  }

  /** Extract { lat, lon } from JSON (geoDataExif / geoData). Tries multiple Takeout/variant shapes. Corrects swapped lat/lon. */
  function parsedGpsFromMeta(meta) {
    if (!meta || typeof meta !== 'object') return null;
    var m = meta.metadata || meta;
    var geo = (m && (m.geoDataExif || m.geoData)) || (meta.geoDataExif || meta.geoData);
    if (!geo || typeof geo !== 'object') return null;
    var lat = Number(geo.latitude != null ? geo.latitude : (geo.latitudeDecimal != null ? geo.latitudeDecimal : geo.lat));
    var lon = Number(geo.longitude != null ? geo.longitude : (geo.longitudeDecimal != null ? geo.longitudeDecimal : geo.lon));
    if (isNaN(lat) && isNaN(lon) && Array.isArray(geo.coordinates) && geo.coordinates.length >= 2) {
      var a = Number(geo.coordinates[0]);
      var b = Number(geo.coordinates[1]);
      if (!isNaN(a) && !isNaN(b)) {
        if (Math.abs(a) <= 90 && Math.abs(b) <= 180) { lat = a; lon = b; }
        else if (Math.abs(b) <= 90 && Math.abs(a) <= 180) { lat = b; lon = a; }
      }
    }
    if (isNaN(lat) || isNaN(lon)) return null;
    var refLat = geo.latitudeRef || geo.latRef;
    var refLon = geo.longitudeRef || geo.lonRef;
    if (refLat === 'S' || refLat === 's') lat = -Math.abs(lat);
    if (refLon === 'W' || refLon === 'w') lon = -Math.abs(lon);
    if (Math.abs(lat) > 90 && Math.abs(lon) <= 90) { var t = lat; lat = lon; lon = t; }
    if (Math.abs(lon) > 180 && Math.abs(lat) <= 180) { var t = lat; lat = lon; lon = t; }
    return { lat: lat, lon: lon };
  }

  /**
   * Serialize index for remaining Lake files to a cache object (no handles).
   * remainingFileNames: array of filenames that stayed in Lake after merge (media + json).
   */
  function serializeIndexForCache(index, remainingFileNames) {
    if (!index || !index.records) return null;
    var set = {};
    for (var i = 0; i < remainingFileNames.length; i++) set[remainingFileNames[i]] = true;
    var records = [];
    for (var r = 0; r < index.records.length; r++) {
      var rec = index.records[r];
      if (set[rec.filename]) records.push({
        id: rec.id,
        original_path: rec.original_path,
        relative_path: rec.relative_path,
        filename: rec.filename,
        base_name: rec.base_name,
        extension: rec.extension,
        size_bytes: rec.size_bytes,
        file_hash: rec.file_hash,
        exif_timestamp: rec.exif_timestamp,
        exif_gps: rec.exif_gps,
        json_timestamp: rec.json_timestamp,
        json_gps: rec.json_gps,
        final_timestamp: rec.final_timestamp,
        albums: rec.albums ? rec.albums.slice() : [],
        has_json: rec.has_json,
        json_path: rec.json_path,
        is_duplicate: rec.is_duplicate,
        duplicate_of: rec.duplicate_of
      });
    }
    var jsonRecords = [];
    var jbf = index.json_by_filename || {};
    for (var norm in jbf) {
      if (!Object.prototype.hasOwnProperty.call(jbf, norm)) continue;
      var j = jbf[norm];
      if (set[j.filename]) jsonRecords.push({
        filename: j.filename,
        base_name: j.base_name,
        norm: norm,
        original_path: j.original_path,
        parsed_timestamp: j.parsed_timestamp,
        parsed_gps: j.parsed_gps
      });
    }
    return {
      version: 1,
      generated_at: new Date().toISOString(),
      remaining_file_names: remainingFileNames.slice(),
      records: records,
      json_records: jsonRecords
    };
  }

  /**
   * Parse cached index JSON for reuse. Returns null if invalid or wrong version.
   */
  function parseCachedIndex(cachedJson) {
    if (!cachedJson || typeof cachedJson !== 'object') return null;
    if (cachedJson.version !== 1) return null;
    var set = {};
    var names = cachedJson.remaining_file_names;
    if (!Array.isArray(names)) return null;
    for (var i = 0; i < names.length; i++) set[names[i]] = true;
    var recordsByFilename = {};
    var recs = cachedJson.records;
    if (Array.isArray(recs)) {
      for (var r = 0; r < recs.length; r++) {
        var rec = recs[r];
        if (rec && rec.filename) recordsByFilename[rec.filename] = rec;
      }
    }
    var jsonByNorm = {};
    var jsonByFilenameKey = {};
    var jrecs = cachedJson.json_records;
    if (Array.isArray(jrecs)) {
      for (var j = 0; j < jrecs.length; j++) {
        var jr = jrecs[j];
        if (jr && jr.norm != null) {
          jsonByNorm[jr.norm] = jr;
          if (jr.filename) jsonByFilenameKey[jr.filename] = jr;
        }
      }
    }
    return { recordsByFilename: recordsByFilename, jsonByNorm: jsonByNorm, jsonByFilenameKey: jsonByFilenameKey, cachedNamesSet: set, remaining_file_names: names };
  }

  /**
   * Build MediaIndex from lake file list and fileMap.
   * options: { computeHashes: true, maxHashBytes: 100*1024*1024, extractExif: true }
   * cached: optional from parseCachedIndex() to reuse index for files already in cache.
   */
  function buildMediaIndex(lakeHandle, lakeFiles, fileMap, options, log, cached) {
    log = log || function () {};
    options = options || {};
    var computeHashes = options.computeHashes !== false;
    var maxHashBytes = typeof options.maxHashBytes === 'number' ? options.maxHashBytes : 100 * 1024 * 1024;
    var extractExif = options.extractExif !== false;

    var records = [];
    var mediaById = {};
    var mediaByHash = {};
    var mediaByFilename = {};
    var jsonByFilename = {};
    var idCounter = 0;
    var oldIdToNewId = {};

    var mediaList = [];
    var jsonList = [];
    var cachedSet = cached && cached.cachedNamesSet ? cached.cachedNamesSet : {};
    var jsonRecords = [];

    for (var i = 0; i < lakeFiles.length; i++) {
      var entry = lakeFiles[i];
      var name = entry.name;
      var full = entry.full || name;
      if (SYSTEM_FILES.indexOf(name.toLowerCase()) >= 0) continue;
      var fileMapRec = fileMap[name];
      var originalPath = (fileMapRec && fileMapRec.original_path) ? fileMapRec.original_path : full;
      var parentFolder = (fileMapRec && fileMapRec.parent_folder) ? fileMapRec.parent_folder : '';

      if (isMediaFile(name)) {
        if (cachedSet[name] && cached && cached.recordsByFilename[name]) {
          mediaList.push({ fromCache: true, entry: entry, name: name, full: full, originalPath: originalPath, parentFolder: parentFolder, extension: ext(name), baseName: baseNameNoExt(name) });
        } else {
          mediaList.push({
            name: name,
            full: full,
            handle: entry.handle,
            originalPath: originalPath,
            parentFolder: parentFolder,
            extension: ext(name),
            baseName: baseNameNoExt(name)
          });
        }
      } else if (isMetaFile(name)) {
        if (cachedSet[name] && cached && cached.jsonByFilenameKey && cached.jsonByFilenameKey[name]) {
          jsonList.push({ fromCache: true, name: name, full: full, originalPath: originalPath, baseName: baseNameNoExt(name) });
        } else {
          jsonList.push({
            name: name,
            full: full,
            handle: entry.handle,
            originalPath: originalPath,
            baseName: baseNameNoExt(name)
          });
        }
      }
    }

    function processMedia(item) {
      if (item.fromCache && cached && cached.recordsByFilename[item.name]) {
        var oldRec = cached.recordsByFilename[item.name];
        var newId = ++idCounter;
        oldIdToNewId[oldRec.id] = newId;
        var rec = {
          id: newId,
          original_path: item.originalPath,
          relative_path: item.name,
          filename: item.name,
          base_name: item.baseName,
          extension: item.extension,
          size_bytes: oldRec.size_bytes,
          file_hash: oldRec.file_hash,
          exif_timestamp: oldRec.exif_timestamp,
          exif_gps: oldRec.exif_gps,
          json_timestamp: oldRec.json_timestamp,
          json_gps: oldRec.json_gps,
          final_timestamp: oldRec.final_timestamp,
          albums: oldRec.albums ? oldRec.albums.slice() : [],
          has_json: oldRec.has_json,
          json_path: oldRec.json_path,
          is_duplicate: oldRec.is_duplicate,
          duplicate_of: oldRec.duplicate_of
        };
        records.push(rec);
        mediaById[rec.id] = rec;
        if (rec.file_hash) {
          if (!mediaByHash[rec.file_hash]) mediaByHash[rec.file_hash] = [];
          mediaByHash[rec.file_hash].push(rec);
        }
        return Promise.resolve();
      }
      var rec = {
        id: ++idCounter,
        original_path: item.originalPath,
        relative_path: item.name,
        filename: item.name,
        base_name: item.baseName,
        extension: item.extension,
        size_bytes: null,
        file_hash: null,
        exif_timestamp: null,
        exif_gps: null,
        json_timestamp: null,
        json_gps: null,
        final_timestamp: null,
        albums: item.parentFolder ? [item.parentFolder] : [],
        has_json: false,
        json_path: null,
        is_duplicate: false,
        duplicate_of: null
      };
      records.push(rec);
      mediaById[rec.id] = rec;
      return item.handle.getFile().then(function (file) {
        rec.size_bytes = file.size;
        if (computeHashes) {
          return hashFileStreamed(item.handle, maxHashBytes, log).then(function (hash) {
            rec.file_hash = hash;
            if (hash) {
              if (!mediaByHash[hash]) mediaByHash[hash] = [];
              mediaByHash[hash].push(rec);
            }
            if (extractExif && (IMAGE_EXT.indexOf(rec.extension) >= 0 || rec.extension === '')) {
              return readFileToArrayBuffer(file).then(function (buf) {
                try {
                  if (typeof GphotoExif !== 'undefined') {
                    if (GphotoExif.getExifDateTimeOriginal) {
                      var dt = GphotoExif.getExifDateTimeOriginal(buf);
                      if (dt && dt.indexOf('(') !== 0) rec.exif_timestamp = dt;
                    }
                    if (GphotoExif.getExifGps) {
                      var gps = GphotoExif.getExifGps(buf);
                      if (gps) rec.exif_gps = gps;
                    }
                  }
                } catch (e) {}
                return Promise.resolve();
              }).catch(function () { return Promise.resolve(); });
            }
            return Promise.resolve();
          });
        }
        if (extractExif && (IMAGE_EXT.indexOf(rec.extension) >= 0 || rec.extension === '')) {
          return readFileToArrayBuffer(file).then(function (buf) {
            try {
              if (typeof GphotoExif !== 'undefined') {
                if (GphotoExif.getExifDateTimeOriginal) {
                  var dt = GphotoExif.getExifDateTimeOriginal(buf);
                  if (dt && dt.indexOf('(') !== 0) rec.exif_timestamp = dt;
                }
                if (GphotoExif.getExifGps) {
                  var gps = GphotoExif.getExifGps(buf);
                  if (gps) rec.exif_gps = gps;
                }
              }
            } catch (e) {}
            return Promise.resolve();
          }).catch(function () { return Promise.resolve(); });
        }
        return Promise.resolve();
      }).catch(function () { return Promise.resolve(); });
    }

    function processJson(item) {
      if (item.fromCache && cached && cached.jsonByFilenameKey && cached.jsonByFilenameKey[item.name]) {
        var jr = cached.jsonByFilenameKey[item.name];
        var jsonRec = {
          original_path: jr.original_path || item.originalPath,
          filename: jr.filename,
          base_name: jr.base_name,
          parsed_timestamp: jr.parsed_timestamp,
          parsed_gps: jr.parsed_gps,
          matched_media_id: null
        };
        if (!jsonByFilename[jr.norm]) jsonByFilename[jr.norm] = jsonRec;
        jsonRecords.push(jsonRec);
        return Promise.resolve();
      }
      var jsonRec = {
        original_path: item.originalPath,
        filename: item.name,
        base_name: item.baseName,
        parsed_timestamp: null,
        parsed_gps: null,
        matched_media_id: null
      };
      return item.handle.getFile().then(function (f) {
        return f.text().then(function (t) {
          try {
            var meta = JSON.parse(t);
            jsonRec.parsed_timestamp = parsedTimestampFromMeta(meta);
            jsonRec.parsed_gps = parsedGpsFromMeta(meta);
          } catch (e) {}
          var nameWithoutSuffix = item.name
            .replace(/\.json$/i, '')
            .replace(/\.metadata\.json$/i, '')
            .replace(/\.supplemental-metadata\.json$/i, '')
            .replace(/\.supplemental-metadata$/i, '');
          var norm = normalizeBaseName(nameWithoutSuffix);
          if (!jsonByFilename[norm]) jsonByFilename[norm] = jsonRec;
          jsonRecords.push(jsonRec);
          return Promise.resolve();
        });
      }).catch(function () { return Promise.resolve(); });
    }

    var seq = Promise.resolve();
    for (var m = 0; m < mediaList.length; m++) {
      (function (it) {
        seq = seq.then(function () { return processMedia(it); });
      })(mediaList[m]);
    }
    for (var j = 0; j < jsonList.length; j++) {
      (function (it) {
        seq = seq.then(function () { return processJson(it); });
      })(jsonList[j]);
    }

    return seq.then(function () {
      if (cached && Object.keys(oldIdToNewId).length > 0) {
        records.forEach(function (rec) {
          if (rec.duplicate_of != null && oldIdToNewId[rec.duplicate_of] != null) rec.duplicate_of = oldIdToNewId[rec.duplicate_of];
        });
      }
      records.forEach(function (rec) {
        var norm = normalizeBaseName(rec.filename);
        if (!mediaByFilename[norm]) mediaByFilename[norm] = [];
        mediaByFilename[norm].push(rec);
        var jsonRec = jsonByFilename[norm];
        if (jsonRec) {
          rec.has_json = true;
          rec.json_path = jsonRec.original_path;
          rec.json_timestamp = jsonRec.parsed_timestamp;
          rec.json_gps = jsonRec.parsed_gps;
          jsonRec.matched_media_id = rec.id;
        }
      });

      var potentialDuplicates = 0;
      for (var h in mediaByHash) {
        if (mediaByHash[h].length > 1) potentialDuplicates += mediaByHash[h].length;
      }
      var missingExif = 0;
      var mediaWithoutJson = 0;
      records.forEach(function (r) {
        if (IMAGE_EXT.indexOf(r.extension) >= 0 && !r.exif_timestamp) missingExif++;
        if (!r.has_json) mediaWithoutJson++;
      });

      var stats = {
        total_media_files: records.length,
        total_json_files: jsonRecords.length,
        total_hash_groups: Object.keys(mediaByHash).length,
        potential_duplicates_count: potentialDuplicates,
        missing_exif_count: missingExif,
        media_without_json_count: mediaWithoutJson
      };

      return {
        records: records,
        media_by_hash: mediaByHash,
        media_by_filename: mediaByFilename,
        json_by_filename: jsonByFilename,
        stats: stats
      };
    });
  }

  /** True if filename indicates an edited version (Takeout: keep both, do not mark as duplicate of non-edited). */
  function isEditedFilename(filename) {
    return filename && filename.toLowerCase().indexOf('-edited') >= 0;
  }

  /** True if base name ends with _N or (N) (likely a duplicate copy rather than original). */
  function isLikelyDuplicateFilename(filename) {
    if (!filename || typeof filename !== 'string') return false;
    var base = baseNameNoExt(filename);
    return /_\d+$/i.test(base) || /\s*\(\d+\)\s*$/i.test(base);
  }

  /**
   * Level 1 duplicate detection: use media_by_hash, mark is_duplicate/duplicate_of.
   * -edited rule: do not mark edited vs non-edited as duplicates; treat as versions.
   * Mutates index.records and index.stats. Call only when options.detectDuplicates is true.
   */
  function runDuplicateDetection(index, options, log) {
    log = log || function () {};
    var records = index.records;
    var mediaByHash = index.media_by_hash;
    var stats = index.stats;

    var exactDuplicateGroups = 0;
    var exactDuplicateCount = 0;
    var versionPairsCount = 0;

    for (var hash in mediaByHash) {
      var group = mediaByHash[hash];
      if (group.length <= 1) continue;

      var edited = [];
      var nonEdited = [];
      for (var g = 0; g < group.length; g++) {
        if (isEditedFilename(group[g].filename)) edited.push(group[g]);
        else nonEdited.push(group[g]);
      }

      if (edited.length > 0 && nonEdited.length > 0) versionPairsCount += 1;

      function markCanonicalAndDuplicates(list) {
        if (list.length <= 1) return;
        /* Prefer larger file (original) as canonical; when equal, prefer name without _N or (N); then lower id. */
        list.sort(function (a, b) {
          var sa = Number(a.size_bytes) || 0;
          var sb = Number(b.size_bytes) || 0;
          if (sb !== sa) return sb - sa;
          var aDup = isLikelyDuplicateFilename(a.filename) ? 1 : 0;
          var bDup = isLikelyDuplicateFilename(b.filename) ? 1 : 0;
          if (aDup !== bDup) return aDup - bDup;
          return a.id - b.id;
        });
        var canonical = list[0];
        canonical.is_duplicate = false;
        canonical.duplicate_of = null;
        for (var i = 1; i < list.length; i++) {
          list[i].is_duplicate = true;
          list[i].duplicate_of = canonical.id;
          exactDuplicateCount += 1;
        }
        exactDuplicateGroups += 1;
      }

      markCanonicalAndDuplicates(nonEdited);
      markCanonicalAndDuplicates(edited);
    }

    stats.exact_duplicate_groups = exactDuplicateGroups;
    stats.exact_duplicate_count = exactDuplicateCount;
    stats.version_pairs_count = versionPairsCount;

    return index;
  }

  global.GphotoMediaIndex = {
    buildMediaIndex: buildMediaIndex,
    runDuplicateDetection: runDuplicateDetection,
    serializeIndexForCache: serializeIndexForCache,
    parseCachedIndex: parseCachedIndex
  };
})(typeof self !== 'undefined' ? self : this);
