import { createHash, randomUUID } from "node:crypto";
import { createReadStream, watch } from "node:fs";
import { mkdir, open, readFile, readdir, rename, rm, stat, utimes, writeFile } from "node:fs/promises";
import { createServer } from "node:http";
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { Readable } from "node:stream";
import { spawn } from "node:child_process";
import { ifNoneMatchHits, strongETag } from "./catalog-http.mjs";
import { buildCookingCatalog, resolveCookingFile, srtToVtt } from "./cooking-library.mjs";
import { isAllowedGalleryOrigin } from "./gallery-origins.mjs";
import { KeepError, createPeerKeepRegistry, keepCookingCategory } from "./peer-keep.mjs";
import {
  isKeptSeriesPath,
  resolveCookingDirectory,
  resolveKeepDirectory,
  resolveLibraryDirectories,
} from "./keep-directories.mjs";
import { hasConfiguredAccess, hasConfiguredWriteAccess } from "./network-access.mjs";
import { discoverTailscalePeerAgents } from "./peer-discovery.mjs";
import { PhonePeerError, PhonePeerRegistry } from "./phone-peer.mjs";
import { resolveAgentRuntimePaths } from "./runtime-paths.mjs";

const { cacheDirectory, configPath } = resolveAgentRuntimePaths();
const thumbnailDirectory = join(cacheDirectory, "thumbnails");
const optimizedDirectory = join(cacheDirectory, "optimized");
const displayDirectory = join(cacheDirectory, "display");
const inventoryManifestPath = join(cacheDirectory, "inventory.json");
const videoExtensions = new Set([".mov", ".mp4", ".m4v", ".mkv", ".webm"]);
const imageExtensions = new Set([".jpg", ".jpeg", ".png", ".webp", ".gif", ".heic", ".heif", ".tif", ".tiff"]);
const browserImageExtensions = new Set([".jpg", ".jpeg", ".png", ".webp", ".gif"]);
const supportedExtensions = new Set([...videoExtensions, ...imageExtensions]);
const fastStartExtensions = new Set([".mov", ".mp4", ".m4v"]);
const customCoverExtensions = [".jpg", ".jpeg", ".png", ".webp"];

const mimeTypes = {
  ".mov": "video/quicktime",
  ".mp4": "video/mp4",
  ".m4v": "video/x-m4v",
  ".mkv": "video/x-matroska",
  ".webm": "video/webm",
};

const imageMimeTypes = {
  ".jpg": "image/jpeg",
  ".jpeg": "image/jpeg",
  ".png": "image/png",
  ".webp": "image/webp",
  ".gif": "image/gif",
  ".heic": "image/heic",
  ".heif": "image/heif",
  ".tif": "image/tiff",
  ".tiff": "image/tiff",
};

const mediaMimeTypes = { ...mimeTypes, ...imageMimeTypes };

const cookingMimeTypes = {
  ...mediaMimeTypes,
  ".pdf": "application/pdf",
  ".md": "text/markdown; charset=utf-8",
  ".txt": "text/plain; charset=utf-8",
  ".srt": "text/vtt; charset=utf-8",
  ".vtt": "text/vtt; charset=utf-8",
  ".json": "application/json; charset=utf-8",
};

let config;
let catalog = { source: { name: "Mac", scannedAt: new Date(0).toISOString() }, items: [] };
let catalogJson = JSON.stringify(catalog);
let catalogTag = strongETag(catalogJson);
let cookingCatalog = null;
const phonePeer = new PhonePeerRegistry();
let peerKeep = null;
let itemsById = new Map();
let keepContentById = new Map();
let discoveredPeerCache = { expiresAt: 0, peers: [] };
let scanPromise = null;
let scanQueued = false;
let scanTimer = null;
let cookingScanTimer = null;
let optimizingCount = 0;
let optimizationChain = Promise.resolve();
const queuedOptimizationIds = new Set();
const watchers = [];
const inspectionCache = new Map();
let inventoryManifest = null;

function run(command, args) {
  return new Promise((resolvePromise, reject) => {
    const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
    const stdout = [];
    const stderr = [];
    child.stdout.on("data", (chunk) => stdout.push(chunk));
    child.stderr.on("data", (chunk) => stderr.push(chunk));
    child.once("error", reject);
    child.once("close", (code) => {
      if (code === 0) {
        resolvePromise(Buffer.concat(stdout).toString("utf8"));
      } else {
        reject(new Error(`${command} exited with ${code}: ${Buffer.concat(stderr).toString("utf8").trim()}`));
      }
    });
  });
}

function emptyInventoryManifest() {
  return {
    version: 1,
    peer: {
      id: String(config.peerId || "").trim() || randomUUID(),
      name: config.name || "Mac",
    },
    hashCache: {},
    contentObjects: {},
    replicas: {},
    updatedAt: new Date(0).toISOString(),
  };
}

async function loadInventoryManifest() {
  try {
    const parsed = JSON.parse(await readFile(inventoryManifestPath, "utf8"));
    inventoryManifest = {
      ...emptyInventoryManifest(),
      ...parsed,
      peer: {
        id: String(config.peerId || parsed?.peer?.id || "").trim() || randomUUID(),
        name: config.name || parsed?.peer?.name || "Mac",
      },
      hashCache: parsed?.hashCache && typeof parsed.hashCache === "object" ? parsed.hashCache : {},
      contentObjects:
        parsed?.contentObjects && typeof parsed.contentObjects === "object" ? parsed.contentObjects : {},
      replicas: parsed?.replicas && typeof parsed.replicas === "object" ? parsed.replicas : {},
    };
  } catch {
    inventoryManifest = emptyInventoryManifest();
  }
}

async function saveInventoryManifest() {
  if (!inventoryManifest) return;
  inventoryManifest.updatedAt = new Date().toISOString();
  await mkdir(dirname(inventoryManifestPath), { recursive: true });
  await writeFile(inventoryManifestPath, `${JSON.stringify(inventoryManifest, null, 2)}\n`);
}

function contentIdentity(sha256, size) {
  const contentKey = `${sha256}:${size}`;
  return {
    contentKey,
    contentId: createHash("sha256").update(contentKey).digest("hex").slice(0, 20),
  };
}

async function fileSha256(path, fileStat) {
  const cache = inventoryManifest?.hashCache?.[path];
  if (
    cache &&
    cache.size === fileStat.size &&
    cache.mtimeMs === fileStat.mtimeMs &&
    cache.ctimeMs === fileStat.ctimeMs &&
    /^[a-f0-9]{64}$/i.test(cache.sha256)
  ) {
    return cache.sha256.toLowerCase();
  }

  const hash = createHash("sha256");
  await new Promise((resolvePromise, reject) => {
    const stream = createReadStream(path);
    stream.on("data", (chunk) => hash.update(chunk));
    stream.once("error", reject);
    stream.once("end", resolvePromise);
  });
  const sha256 = hash.digest("hex");
  if (inventoryManifest) {
    inventoryManifest.hashCache[path] = {
      size: fileStat.size,
      mtimeMs: fileStat.mtimeMs,
      ctimeMs: fileStat.ctimeMs,
      sha256,
    };
  }
  return sha256;
}

function managedStorageLabel(item) {
  const parts = [item.public.library, item.public.playlist].filter(Boolean);
  return parts.length ? parts.join(" / ") : "Configured files";
}

function recordReplica(item, now) {
  if (!inventoryManifest || !item.contentKey) return;
  const existingContent = inventoryManifest.contentObjects[item.contentKey];
  inventoryManifest.contentObjects[item.contentKey] = {
    id: item.contentId,
    sha256: item.contentSha256,
    size: item.public.size,
    mediaType: item.public.kind,
    firstSeenAt: existingContent?.firstSeenAt || now,
    lastSeenAt: now,
  };

  const replicaId = createHash("sha256")
    .update(`${inventoryManifest.peer.id}:${item.contentKey}:${item.path}`)
    .digest("hex")
    .slice(0, 24);
  inventoryManifest.replicas[replicaId] = {
    id: replicaId,
    peerId: inventoryManifest.peer.id,
    contentId: item.contentId,
    contentKey: item.contentKey,
    state: "available",
    verifiedAt: now,
    lastSeenAt: now,
    managedStorage: managedStorageLabel(item),
    fileName: basename(item.path),
    size: item.public.size,
  };
}

function pruneInventoryManifest(activePaths, activeContentKeys) {
  if (!inventoryManifest) return;

  for (const path of Object.keys(inventoryManifest.hashCache)) {
    if (!activePaths.has(path)) delete inventoryManifest.hashCache[path];
  }

  for (const key of Object.keys(inventoryManifest.contentObjects)) {
    if (!activeContentKeys.has(key)) delete inventoryManifest.contentObjects[key];
  }

  for (const [replicaId, replica] of Object.entries(inventoryManifest.replicas)) {
    if (!activeContentKeys.has(replica.contentKey)) delete inventoryManifest.replicas[replicaId];
  }
}

function inventorySummary(scannedAt = catalog.source.scannedAt) {
  const contentObjects = Object.keys(inventoryManifest?.contentObjects || {}).length;
  const replicas = Object.values(inventoryManifest?.replicas || {}).filter(
    (replica) => replica.state === "available",
  );
  const replicaFiles = replicas.length;
  const duplicateFiles = Math.max(0, replicaFiles - contentObjects);
  const duplicateSavingsBytes = Math.max(
    0,
    replicas.reduce((total, replica) => total + Number(replica.size || 0), 0) -
      Object.values(inventoryManifest?.contentObjects || {}).reduce(
        (total, content) => total + Number(content.size || 0),
        0,
      ),
  );

  return {
    peer: inventoryManifest?.peer || { id: "unknown", name: config.name || "Mac" },
    scannedAt,
    contentObjects,
    replicaFiles,
    duplicateFiles,
    duplicateSavingsBytes,
  };
}

function readJsonBody(request, limitBytes = 64 * 1024) {
  return new Promise((resolvePromise, reject) => {
    const chunks = [];
    let size = 0;
    request.on("data", (chunk) => {
      size += chunk.length;
      if (size > limitBytes) {
        reject(new Error("Request body is too large."));
        request.destroy();
        return;
      }
      chunks.push(chunk);
    });
    request.once("error", reject);
    request.once("end", () => {
      try {
        const text = Buffer.concat(chunks).toString("utf8").trim();
        resolvePromise(text ? JSON.parse(text) : {});
      } catch {
        reject(new Error("Request body must be valid JSON."));
      }
    });
  });
}

async function readFinderValue(path, key) {
  try {
    const output = (await run("mdls", ["-raw", "-name", key, path])).trim();
    if (!output || output === "(null)") return null;
    return output;
  } catch {
    return null;
  }
}

function parseFinderTags(value) {
  if (!value) return [];
  return value
    .replace(/^\(|\)$/g, "")
    .split("\n")
    .map((line) => line.trim().replace(/^"|",?$|,$/g, ""))
    .filter(Boolean);
}

function tagFromFolderName(name) {
  return name
    .trim()
    .toLowerCase()
    .replace(/&/g, " and ")
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "");
}

function folderTags(path) {
  if (config.deriveTagsFromFolders === false) return [];
  const directories = resolveLibraryDirectories(config);
  const tags = [];

  for (const directory of directories) {
    if (!path.startsWith(`${directory}/`)) continue;
    const relative = path.slice(directory.length + 1);
    const folderNames = [basename(directory), relative.split("/")[0]].filter(Boolean);
    for (const folderName of folderNames) {
      const tag = tagFromFolderName(folderName);
      if (!tag) continue;
      tags.push(tag);
      if (tag === "current-series") tags.push("series");
    }
  }

  return tags;
}

function uniqueTags(tags) {
  const seen = new Set();
  const nextTags = [];
  for (const tag of tags) {
    const normalized = tag.trim();
    const key = normalized.toLowerCase();
    if (!normalized || seen.has(key)) continue;
    seen.add(key);
    nextTags.push(normalized);
  }
  return nextTags;
}

function cleanTitle(path) {
  return basename(path, extname(path))
    .replace(/[._]+/g, " ")
    .replace(/\s+/g, " ")
    .trim();
}

function catalogLocation(path) {
  for (const root of resolveLibraryDirectories(config)) {
    const relativePath = relative(root, path);
    if (!relativePath || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
      continue;
    }

    const relativeDirectory = dirname(relativePath);
    return {
      library: basename(root),
      playlist: relativeDirectory === "."
        ? null
        : relativeDirectory.split(sep).filter(Boolean).join(" / "),
      playlistPosition: Number(/^\s*(\d+)\s*[-._]/.exec(basename(path))?.[1]) || null,
    };
  }

  return { library: null, playlist: null, playlistPosition: null };
}

function applyCatalogLocation(item, path) {
  const location = catalogLocation(path);
  Object.assign(item.public, location);
  if (
    item.public.kind === "video" &&
    location.library?.toLowerCase() === "youtube" &&
    location.playlist
  ) {
    item.public.type = "series";
  }
  return item;
}

async function walkDirectory(directory) {
  const entries = await readdir(directory, { withFileTypes: true });
  const files = [];
  for (const entry of entries) {
    if (entry.name.startsWith(".")) continue;
    const path = join(directory, entry.name);
    if (entry.isDirectory()) files.push(...(await walkDirectory(path)));
    else if (entry.isFile() && supportedExtensions.has(extname(entry.name).toLowerCase())) files.push(path);
  }
  return files;
}

async function configuredFiles() {
  const files = new Set((config.files || []).map((path) => resolve(path)));
  for (const directory of resolveLibraryDirectories(config)) {
    try {
      for (const path of await walkDirectory(resolve(directory))) files.add(path);
    } catch (error) {
      console.warn(`Unable to scan ${directory}: ${error.message}`);
    }
  }
  const supportedFiles = [...files].filter((path) => supportedExtensions.has(extname(path).toLowerCase()));
  const videoBases = new Set(
    supportedFiles
      .filter((path) => videoExtensions.has(extname(path).toLowerCase()))
      .map((path) => join(dirname(path), basename(path, extname(path))).toLowerCase()),
  );

  return supportedFiles.filter((path) => {
    if (!imageExtensions.has(extname(path).toLowerCase())) return true;
    const base = join(dirname(path), basename(path, extname(path))).toLowerCase();
    return !videoBases.has(base);
  });
}

function extractMetadata(probe) {
  const format = probe.format || {};
  const video = (probe.streams || []).find((stream) => stream.codec_type === "video");
  const rotation = Number(
    video?.side_data_list?.find((sideData) => sideData.rotation !== undefined)?.rotation || 0,
  );
  const swapsDimensions = Math.abs(rotation) % 180 === 90;
  const tags = format.tags || {};
  const createdAt =
    tags["com.apple.quicktime.creationdate"] ||
    tags.creation_time ||
    video?.tags?.creation_time ||
    null;

  return {
    duration: Number(format.duration || 0),
    size: Number(format.size || 0),
    width: swapsDimensions ? video?.height || null : video?.width || null,
    height: swapsDimensions ? video?.width || null : video?.height || null,
    codec: video?.codec_name || null,
    createdAt,
  };
}

async function extractImageMetadata(path, fileStat) {
  const extension = extname(path).toLowerCase();
  let handle;
  try {
    handle = await open(path, "r");
    if (extension === ".png") {
      const header = Buffer.alloc(24);
      const { bytesRead } = await handle.read(header, 0, header.length, 0);
      if (bytesRead === header.length && header.subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex"))) {
        return {
          duration: 0,
          size: fileStat.size,
          width: header.readUInt32BE(16),
          height: header.readUInt32BE(20),
          codec: "png",
          createdAt: null,
        };
      }
    }
    if (extension === ".gif") {
      const header = Buffer.alloc(10);
      const { bytesRead } = await handle.read(header, 0, header.length, 0);
      if (bytesRead === header.length && header.toString("ascii", 0, 3) === "GIF") {
        return {
          duration: 0,
          size: fileStat.size,
          width: header.readUInt16LE(6),
          height: header.readUInt16LE(8),
          codec: "gif",
          createdAt: null,
        };
      }
    }
  } finally {
    await handle?.close();
  }

  const probe = JSON.parse(
    await run("ffprobe", [
      "-v", "error",
      "-show_entries",
      "format=size:format_tags:stream=codec_type,codec_name,width,height:stream_tags:stream_side_data=rotation",
      "-of", "json",
      path,
    ]),
  );
  return extractMetadata(probe);
}

async function customCover(path) {
  const base = join(dirname(path), basename(path, extname(path)));
  for (const extension of customCoverExtensions) {
    const candidate = `${base}${extension}`;
    try {
      if ((await stat(candidate)).isFile()) return candidate;
    } catch {
      // Try the next supported cover extension.
    }
  }
  return null;
}

async function ensureThumbnail(path, id, duration, fileStat, kind) {
  const cover = kind === "video" ? await customCover(path) : null;
  if (cover) return cover;

  await mkdir(thumbnailDirectory, { recursive: true });
  const thumbnailPath = join(thumbnailDirectory, `${id}.jpg`);
  try {
    const thumbnailStat = await stat(thumbnailPath);
    if (thumbnailStat.mtimeMs >= fileStat.mtimeMs) return thumbnailPath;
  } catch {
    // Generate it below.
  }

  const inputArgs = kind === "video"
    ? ["-ss", Math.max(0, Math.min(duration * 0.25, 30)).toFixed(3), "-i", path]
    : ["-i", path];
  try {
    await run("ffmpeg", [
      "-v", "error",
      ...inputArgs,
      "-frames:v", "1",
      "-vf", "scale='min(720,iw)':-2",
      "-q:v", "3",
      "-y",
      thumbnailPath,
    ]);
    return thumbnailPath;
  } catch (error) {
    console.warn(`Unable to create thumbnail for ${basename(path)}: ${error.message}`);
    return null;
  }
}

async function ensureImageDisplay(path, id, fileStat) {
  if (browserImageExtensions.has(extname(path).toLowerCase())) return path;

  await mkdir(displayDirectory, { recursive: true });
  const displayPath = join(displayDirectory, `${id}.jpg`);
  try {
    const displayStat = await stat(displayPath);
    if (displayStat.size > 0 && displayStat.mtimeMs >= fileStat.mtimeMs) return displayPath;
  } catch {
    // Generate a browser-compatible display image below.
  }

  try {
    await run("ffmpeg", [
      "-v", "error",
      "-i", path,
      "-frames:v", "1",
      "-vf", "scale='min(2560,iw)':-2",
      "-q:v", "2",
      "-y",
      displayPath,
    ]);
    return displayPath;
  } catch (error) {
    console.warn(`Unable to create browser preview for ${basename(path)}: ${error.message}`);
    return null;
  }
}

async function needsFastStart(path) {
  if (!fastStartExtensions.has(extname(path).toLowerCase())) return false;
  let handle;
  try {
    handle = await open(path, "r");
    const fileStat = await handle.stat();
    let offset = 0;
    let sawMediaData = false;
    const header = Buffer.alloc(16);

    while (offset + 8 <= fileStat.size) {
      const { bytesRead } = await handle.read(header, 0, 16, offset);
      if (bytesRead < 8) break;
      let atomSize = header.readUInt32BE(0);
      const atomType = header.toString("ascii", 4, 8);
      if (atomSize === 1 && bytesRead >= 16) atomSize = Number(header.readBigUInt64BE(8));
      else if (atomSize === 0) atomSize = fileStat.size - offset;
      if (!Number.isSafeInteger(atomSize) || atomSize < 8) break;

      if (atomType === "moov") return sawMediaData;
      if (atomType === "mdat") sawMediaData = true;
      offset += atomSize;
    }
  } catch (error) {
    console.warn(`Unable to inspect fast-start layout for ${basename(path)}: ${error.message}`);
  } finally {
    await handle?.close();
  }
  return false;
}

async function prepareOptimizedStream(item) {
  if (item.public.kind !== "video" || config.optimizeForStreaming === false || !(await needsFastStart(item.path))) return;
  const extension = extname(item.path).toLowerCase();
  await mkdir(optimizedDirectory, { recursive: true });
  const optimizedPath = join(optimizedDirectory, `${item.public.id}${extension}`);

  try {
    const optimizedStat = await stat(optimizedPath);
    if (optimizedStat.size > 0 && optimizedStat.mtimeMs >= item.fileMtimeMs) {
      await utimes(optimizedPath, new Date(), new Date());
      item.streamPath = optimizedPath;
      item.public.streamOptimized = true;
      item.public.optimizationStatus = "ready";
      await enforceOptimizedCacheBudget();
      return;
    }
  } catch {
    // Build the optimized copy below.
  }

  const temporaryPath = join(
    optimizedDirectory,
    `${item.public.id}.partial-${process.pid}-${Date.now()}${extension}`,
  );
  optimizingCount += 1;
  item.public.optimizationStatus = "preparing";
  try {
    await run("ffmpeg", [
      "-v", "error",
      "-i", item.path,
      "-map", "0",
      "-map_metadata", "0",
      "-c", "copy",
      "-movflags", "+faststart",
      "-y",
      temporaryPath,
    ]);
    await rename(temporaryPath, optimizedPath);
    item.streamPath = optimizedPath;
    item.public.streamOptimized = true;
    item.public.optimizationStatus = "ready";
    await enforceOptimizedCacheBudget();
    console.log(`Prepared fast-start stream for ${basename(item.path)}.`);
  } catch (error) {
    item.public.optimizationStatus = "original";
    console.warn(`Unable to prepare fast-start stream for ${basename(item.path)}: ${error.message}`);
    await rm(temporaryPath, { force: true });
  } finally {
    optimizingCount -= 1;
  }
}

async function enforceOptimizedCacheBudget() {
  const maximumBytes = Math.max(0, Number(config.maxOptimizedCacheBytes ?? 25 * 1024 ** 3));
  if (!Number.isFinite(maximumBytes)) return;

  try {
    const cachedFiles = [];
    for (const entry of await readdir(optimizedDirectory, { withFileTypes: true })) {
      if (!entry.isFile() || entry.name.includes(".partial-")) continue;
      const path = join(optimizedDirectory, entry.name);
      const fileStat = await stat(path);
      cachedFiles.push({ id: entry.name.slice(0, 20), path, size: fileStat.size, usedAt: fileStat.mtimeMs });
    }

    let totalBytes = cachedFiles.reduce((total, file) => total + file.size, 0);
    cachedFiles.sort((a, b) => a.usedAt - b.usedAt);
    for (const file of cachedFiles) {
      if (totalBytes <= maximumBytes) break;
      await rm(file.path, { force: true });
      totalBytes -= file.size;
      const item = itemsById.get(file.id);
      if (item?.streamPath === file.path) {
        item.streamPath = item.path;
        item.public.streamOptimized = false;
        item.public.optimizationStatus = "original";
      }
    }
  } catch (error) {
    if (error.code !== "ENOENT") console.warn(`Unable to enforce optimized cache size: ${error.message}`);
  }
}

function queueOptimizedStream(item) {
  if (queuedOptimizationIds.has(item.id)) return;
  queuedOptimizationIds.add(item.id);
  optimizationChain = optimizationChain
    .then(() => prepareOptimizedStream(item))
    .catch((error) => console.warn(`Stream preparation failed: ${error.message}`))
    .finally(() => queuedOptimizationIds.delete(item.id));
}

async function pruneGeneratedCache(directory, validIds) {
  try {
    for (const entry of await readdir(directory, { withFileTypes: true })) {
      if (!entry.isFile()) continue;
      const id = entry.name.slice(0, 20);
      if (!validIds.has(id) || entry.name.includes(".partial-")) {
        await rm(join(directory, entry.name), { force: true });
      }
    }
  } catch (error) {
    if (error.code !== "ENOENT") console.warn(`Unable to prune ${directory}: ${error.message}`);
  }
}

async function inspectFile(path) {
  const fileStat = await stat(path);
  if (!fileStat.isFile()) return null;

  const kind = imageExtensions.has(extname(path).toLowerCase()) ? "image" : "video";
  const cached = inspectionCache.get(path);
  if (
    cached &&
    cached.size === fileStat.size &&
    cached.mtimeMs === fileStat.mtimeMs &&
    cached.ctimeMs === fileStat.ctimeMs
  ) {
    return applyCatalogLocation(cached.item, path);
  }

  const contentSha256 = await fileSha256(path, fileStat);
  const { contentKey, contentId } = contentIdentity(contentSha256, fileStat.size);
  const id = contentId;
  const metadata = kind === "image"
    ? await extractImageMetadata(path, fileStat)
    : extractMetadata(JSON.parse(
      await run("ffprobe", [
        "-v", "error",
        "-show_entries",
        "format=duration,size:format_tags:stream=codec_type,codec_name,width,height:stream_tags:stream_side_data=rotation",
        "-of", "json",
        path,
      ]),
    ));
  const shouldReadFinderMetadata = kind === "video" || config.readImageFinderMetadata !== false;
  const [finderTagsValue, finderCommentValue] = shouldReadFinderMetadata
    ? await Promise.all([
      readFinderValue(path, "kMDItemUserTags"),
      readFinderValue(path, "kMDItemFinderComment"),
    ])
    : [null, null];
  const tags = uniqueTags([...parseFinderTags(finderTagsValue), ...folderTags(path)]);
  const ratingTag = tags.find((tag) => tag.toLowerCase().startsWith("rating:"));
  const location = catalogLocation(path);
  const keptSeries = isKeptSeriesPath(path, config);
  const type = kind === "image"
    ? "photo"
    : keptSeries || tags.some((tag) => tag.toLowerCase() === "series") ||
        (location.library?.toLowerCase() === "youtube" && location.playlist)
      ? "series"
      : "movie";
  const [thumbnailPath, displayPath] = await Promise.all([
    kind === "image"
      ? Promise.resolve(join(thumbnailDirectory, `${id}.jpg`))
      : ensureThumbnail(path, id, metadata.duration, fileStat, kind),
    kind === "image" ? ensureImageDisplay(path, id, fileStat) : Promise.resolve(path),
  ]);

  const item = {
    id,
    contentId,
    contentKey,
    contentSha256,
    path,
    streamPath: displayPath || path,
    fileMtimeMs: fileStat.mtimeMs,
    thumbnailPath,
    public: {
      id,
      contentId,
      kind,
      title: cleanTitle(path),
      description: finderCommentValue || "",
      tags,
      type,
      rating: kind === "video" && ratingTag ? ratingTag.slice("rating:".length).trim() || null : null,
      createdAt: metadata.createdAt || fileStat.birthtime.toISOString(),
      duration: metadata.duration,
      width: metadata.width,
      height: metadata.height,
      size: metadata.size || fileStat.size,
      replicaCount: 1,
      codec: metadata.codec,
      videoCodec: kind === "video" ? metadata.codec : null,
      streamOptimized: false,
      optimizationStatus: "original",
      thumbnailUrl: thumbnailPath ? `/media/${id}/thumbnail` : null,
      streamUrl: `/media/${id}/stream`,
      downloadUrl: `/media/${id}/download`,
      ...location,
    },
  };
  inspectionCache.set(path, {
    size: fileStat.size,
    mtimeMs: fileStat.mtimeMs,
    ctimeMs: fileStat.ctimeMs,
    item,
  });
  return item;
}

function mergeDuplicateItems(items) {
  const groupedItems = new Map();
  for (const item of items) {
    const itemCopy = {
      ...item,
      public: {
        ...item.public,
        tags: [...item.public.tags],
        replicaCount: 1,
      },
    };
    const current = groupedItems.get(itemCopy.contentKey);
    if (!current) {
      groupedItems.set(itemCopy.contentKey, { primary: itemCopy, replicas: [itemCopy] });
      continue;
    }

    current.replicas.push(itemCopy);
    current.primary.public.tags = uniqueTags([
      ...current.primary.public.tags,
      ...itemCopy.public.tags,
    ]);
    if (!current.primary.public.description && itemCopy.public.description) {
      current.primary.public.description = itemCopy.public.description;
    }
    if (current.primary.public.type !== "series" && itemCopy.public.type === "series") {
      current.primary.public.type = "series";
    }
    current.primary.public.replicaCount = current.replicas.length;
  }

  return [...groupedItems.values()].map(({ primary }) => primary);
}

async function inspectFiles(paths) {
  const concurrency = Math.max(1, Math.min(Number(config.scanConcurrency || 2), 6));
  const results = [];
  let cursor = 0;

  async function worker() {
    while (cursor < paths.length) {
      const path = paths[cursor];
      cursor += 1;
      try {
        const item = await inspectFile(path);
        if (item) results.push(item);
      } catch (error) {
        console.warn(`Unable to inspect ${path}: ${error.message}`);
      }
    }
  }

  await Promise.all(Array.from({ length: Math.min(concurrency, paths.length) }, () => worker()));
  return results;
}

async function scanLibrary() {
  if (scanPromise) {
    scanQueued = true;
    return scanPromise;
  }
  scanPromise = (async () => {
    const inspectedItems = await inspectFiles(await configuredFiles());
    const scannedAt = new Date().toISOString();
    const activePaths = new Set(inspectedItems.map((item) => item.path));
    const activeContentKeys = new Set(inspectedItems.map((item) => item.contentKey));
    for (const item of inspectedItems) recordReplica(item, scannedAt);
    pruneInventoryManifest(activePaths, activeContentKeys);
    await saveInventoryManifest();
    const summary = inventorySummary(scannedAt);
    const nextItems = mergeDuplicateItems(inspectedItems);
    nextItems.sort((a, b) => {
      const aTime = a.public.createdAt ? new Date(a.public.createdAt).getTime() : 0;
      const bTime = b.public.createdAt ? new Date(b.public.createdAt).getTime() : 0;
      return bTime - aTime;
    });
    itemsById = new Map(nextItems.map((item) => [item.id, item]));
    rebuildKeepContentIndex();
    rememberCatalog({
      source: {
        name: config.name || "Mac",
        peerId: summary.peer.id,
        scannedAt,
        watching: watchers.length > 0,
        dropFolders: resolveLibraryDirectories(config).map((path) => basename(path)),
        duplicateItems: inspectedItems.length - nextItems.length,
        replicaFiles: inspectedItems.length,
        contentObjects: summary.contentObjects,
        duplicateSavingsBytes: summary.duplicateSavingsBytes,
        phoneItems: 0,
        phoneConnected: false,
      },
      items: nextItems.map((item) => item.public),
    });
    console.log(`Indexed ${nextItems.length} ${nextItems.length === 1 ? "item" : "items"}.`);
    const validIds = new Set(nextItems.map((item) => item.id));
    for (const path of inspectionCache.keys()) {
      if (!activePaths.has(path)) inspectionCache.delete(path);
    }
    void pruneGeneratedCache(thumbnailDirectory, validIds);
    void pruneGeneratedCache(optimizedDirectory, validIds);
    void pruneGeneratedCache(displayDirectory, validIds);
    for (const item of nextItems) queueOptimizedStream(item);
    return catalog;
  })().finally(() => {
    scanPromise = null;
    if (scanQueued) {
      scanQueued = false;
      scheduleScan("queued filesystem change", 250);
    }
  });
  return scanPromise;
}

async function catalogWithLivePhone() {
  let phoneItems = [];
  try {
    phoneItems = await phonePeer.catalogItems();
  } catch (error) {
    if (!(error instanceof PhonePeerError)) throw error;
  }
  const peerStatus = phonePeer.status();
  if (!peerStatus.connected && phoneItems.length === 0) return { live: false, body: catalog };

  const items = [...catalog.items, ...phoneItems].sort((a, b) => {
    const aTime = a.createdAt ? new Date(a.createdAt).getTime() : 0;
    const bTime = b.createdAt ? new Date(b.createdAt).getTime() : 0;
    return bTime - aTime;
  });
  return {
    live: true,
    body: {
      source: {
        ...catalog.source,
        scannedAt: new Date().toISOString(),
        phoneItems: phoneItems.length,
        phoneConnected: peerStatus.connected,
      },
      items,
    },
  };
}

async function scanCookingLibrary() {
  cookingCatalog = await buildCookingCatalog(resolveCookingDirectory(config), config.name || "Mac");
  rebuildKeepContentIndex();
  console.log(
    `Indexed ${cookingCatalog.stats.recipes} cooking recipes, ${cookingCatalog.stats.variants} variants, and ${cookingCatalog.stats.files} files.`,
  );
  return cookingCatalog;
}

function rebuildKeepContentIndex() {
  const next = new Map();
  for (const item of itemsById.values()) {
    if (item.public.kind === "image") continue;
    const category = item.public.type === "series" ? "series" : "movies";
    next.set(item.id, {
      id: item.id,
      category,
      path: item.path,
      relativePath: null,
      title: item.public.title,
      filename: basename(item.path),
      extension: extname(item.path).toLowerCase(),
      kind: item.public.kind,
      playlist: item.public.playlist || null,
      size: item.public.size,
      sha256: item.contentSha256,
      contentId: item.contentId || item.id,
    });
  }

  if (cookingCatalog?.files) {
    for (const file of cookingCatalog.files) {
      const relativePath = String(file.path || "");
      const category = keepCookingCategory(relativePath);
      if (!category) continue;
      const path = resolveCookingFile(resolveCookingDirectory(config), file.path);
      if (!path) continue;
      const idPrefix = category === "recipes" ? "recipe" : "course";
      const id = createHash("sha256").update(`${idPrefix}:${file.path}`).digest("hex").slice(0, 20);
      next.set(id, {
        id,
        category,
        path,
        relativePath: file.path,
        title: file.path,
        filename: file.name,
        extension: file.extension,
        kind: file.kind === "video" || file.kind === "image" ? file.kind : "file",
        playlist: null,
        size: file.size,
        sha256: null,
        contentId: null,
      });
    }
  }
  keepContentById = next;
}

function publicKeepContent(item) {
  return {
    id: item.id,
    category: item.category,
    title: item.title,
    size: item.size,
    kind: item.kind,
    playlist: item.playlist,
  };
}

async function keepContentInfo(item) {
  const fileStat = await stat(item.path);
  const sha256 = item.sha256 || await fileSha256(item.path, fileStat);
  const { contentId } = contentIdentity(sha256, fileStat.size);
  return {
    ...publicKeepContent({ ...item, size: fileStat.size }),
    contentId,
    sha256,
    filename: item.filename,
    extension: item.extension,
    relativePath: item.relativePath,
    downloadUrl: `/api/keep/content/${item.id}/file`,
  };
}

async function discoveredPeers() {
  if (Date.now() < discoveredPeerCache.expiresAt) return discoveredPeerCache.peers;
  try {
    const peers = await discoverTailscalePeerAgents();
    discoveredPeerCache = { expiresAt: Date.now() + 15_000, peers };
  } catch (error) {
    console.warn(`Peer discovery failed: ${error.message}`);
    discoveredPeerCache = { expiresAt: Date.now() + 5_000, peers: [] };
  }
  return discoveredPeerCache.peers;
}

function scheduleCookingScan(reason, delay = Number(config.watchDebounceMs || 1200)) {
  if (cookingScanTimer) clearTimeout(cookingScanTimer);
  cookingScanTimer = setTimeout(() => {
    cookingScanTimer = null;
    console.log(`Refreshing cooking library after ${reason}.`);
    void scanCookingLibrary().catch((error) => console.warn(`Cooking refresh failed: ${error.message}`));
  }, delay);
  cookingScanTimer.unref();
}

function scheduleScan(reason, delay = Number(config.watchDebounceMs || 1200)) {
  if (scanTimer) clearTimeout(scanTimer);
  scanTimer = setTimeout(() => {
    scanTimer = null;
    console.log(`Refreshing library after ${reason}.`);
    void scanLibrary();
  }, delay);
  scanTimer.unref();
}

function startWatchers() {
  for (const directory of resolveLibraryDirectories(config)) {
    try {
      const watcher = watch(resolve(directory), { recursive: true }, () => {
        scheduleScan(`a change in ${basename(directory)}`);
      });
      watcher.on("error", (error) => console.warn(`Folder watcher error for ${directory}: ${error.message}`));
      watchers.push(watcher);
      console.log(`Watching ${resolve(directory)} for media changes.`);
    } catch (error) {
      console.warn(`Unable to watch ${directory}: ${error.message}`);
    }
  }

  for (const path of config.files || []) {
    try {
      const watcher = watch(resolve(path), () => scheduleScan(`a change to ${basename(path)}`));
      watcher.on("error", (error) => console.warn(`File watcher error for ${path}: ${error.message}`));
      watchers.push(watcher);
    } catch (error) {
      console.warn(`Unable to watch ${path}: ${error.message}`);
    }
  }

  try {
    const directory = resolveCookingDirectory(config);
    const watcher = watch(directory, { recursive: true }, () => {
      scheduleCookingScan(`a change in ${basename(directory)}`);
    });
    watcher.on("error", (error) => console.warn(`Cooking folder watcher error: ${error.message}`));
    watchers.push(watcher);
    console.log(`Watching ${directory} for cooking changes.`);
  } catch (error) {
    console.warn(`Unable to watch cooking directory: ${error.message}`);
  }

  const interval = Math.max(15_000, Number(config.rescanIntervalMs || 60_000));
  setInterval(() => scheduleScan("the periodic safety check", 0), interval).unref();
}

function allowedOrigin(request) {
  const origin = request.headers.origin;
  if (!origin) return null;
  if (isAllowedGalleryOrigin(origin, config.allowedOrigins || [])) return origin;
  return false;
}

function applyCors(response, origin) {
  if (origin) response.setHeader("Access-Control-Allow-Origin", origin);
  response.setHeader("Vary", "Origin");
  response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, DELETE, OPTIONS");
  response.setHeader("Access-Control-Allow-Headers", "Content-Type, Range, X-Private-Reel-Token, If-None-Match");
  response.setHeader("Access-Control-Expose-Headers", "Accept-Ranges, Content-Length, Content-Range, ETag");
  response.setHeader("Access-Control-Allow-Private-Network", "true");
}

function hasAccess(request, url) {
  return hasConfiguredAccess(request, url, config.accessToken);
}

function hasWriteAccess(request) {
  return hasConfiguredWriteAccess(request, config.accessToken);
}

function json(response, status, body, origin) {
  applyCors(response, origin);
  response.writeHead(status, {
    "Content-Type": "application/json; charset=utf-8",
    "Cache-Control": "no-store",
  });
  response.end(JSON.stringify(body));
}

function rememberCatalog(next) {
  catalog = next;
  catalogJson = JSON.stringify(catalog);
  catalogTag = strongETag(catalogJson);
}

function sendCatalog(request, response, origin) {
  applyCors(response, origin);
  response.setHeader("ETag", catalogTag);
  response.setHeader("Cache-Control", "private, no-cache");
  if (ifNoneMatchHits(request.headers["if-none-match"], catalogTag)) {
    response.writeHead(304);
    response.end();
    return;
  }
  response.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
  response.end(catalogJson);
}

function parseRange(header, size) {
  if (!header) return null;
  const match = /^bytes=(\d*)-(\d*)$/.exec(header);
  if (!match) return false;
  let start = match[1] ? Number(match[1]) : null;
  let end = match[2] ? Number(match[2]) : null;
  if (start === null && end !== null) {
    start = Math.max(0, size - end);
    end = size - 1;
  } else {
    start ??= 0;
    end ??= size - 1;
  }
  if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || start >= size) return false;
  return { start, end: Math.min(end, size - 1) };
}

async function sendFile(request, response, path, origin, disposition = "inline", displayName = basename(path)) {
  const fileStat = await stat(path);
  const size = fileStat.size;
  const range = parseRange(request.headers.range, size);
  applyCors(response, origin);
  response.setHeader("Accept-Ranges", "bytes");
  response.setHeader("Content-Type", cookingMimeTypes[extname(path).toLowerCase()] || "application/octet-stream");
  response.setHeader("Content-Disposition", `${disposition}; filename*=UTF-8''${encodeURIComponent(displayName)}`);
  response.setHeader("Cache-Control", "private, no-store");

  if (range === false) {
    response.writeHead(416, { "Content-Range": `bytes */${size}` });
    response.end();
    return;
  }

  if (range) {
    const length = range.end - range.start + 1;
    response.writeHead(206, {
      "Content-Length": length,
      "Content-Range": `bytes ${range.start}-${range.end}/${size}`,
    });
    if (request.method === "HEAD") return response.end();
    createReadStream(path, range).pipe(response);
    return;
  }

  response.writeHead(200, { "Content-Length": size });
  if (request.method === "HEAD") return response.end();
  createReadStream(path).pipe(response);
}

async function sendCookingFile(request, response, path, origin) {
  if (extname(path).toLowerCase() !== ".srt") {
    await sendFile(request, response, path, origin, "inline", basename(path));
    return;
  }
  const body = srtToVtt(await readFile(path, "utf8"));
  applyCors(response, origin);
  response.writeHead(200, {
    "Content-Type": "text/vtt; charset=utf-8",
    "Content-Length": Buffer.byteLength(body),
    "Cache-Control": "private, no-store",
  });
  if (request.method === "HEAD") return response.end();
  response.end(body);
}

async function proxyPhoneMedia(request, response, origin, id, variant) {
  const upstream = await phonePeer.request(`/media/${id}/${variant}`, {
    method: request.method,
    range: request.headers.range,
  });
  applyCors(response, origin);
  for (const name of ["accept-ranges", "cache-control", "content-disposition", "content-length", "content-range", "content-type"]) {
    const value = upstream.headers.get(name);
    if (value) response.setHeader(name, value);
  }
  response.setHeader("Cache-Control", "private, no-store");
  response.writeHead(upstream.status);
  if (request.method === "HEAD" || !upstream.body) return response.end();
  Readable.fromWeb(upstream.body).pipe(response);
}

function prepareInventoryResponse(body) {
  const sha256 = String(body.sha256 || "").trim().toLowerCase();
  const size = Number(body.size);
  if (!/^[a-f0-9]{64}$/.test(sha256)) {
    return { status: 400, body: { error: "sha256 must be a 64-character hex digest." } };
  }
  if (!Number.isSafeInteger(size) || size <= 0) {
    return { status: 400, body: { error: "size must be a positive integer byte count." } };
  }

  const { contentKey, contentId } = contentIdentity(sha256, size);
  const content = inventoryManifest?.contentObjects?.[contentKey] || null;
  const replicas = Object.values(inventoryManifest?.replicas || {}).filter(
    (replica) => replica.contentKey === contentKey && replica.state === "available",
  );

  return {
    status: 200,
    body: {
      contentId,
      alreadyAvailable: Boolean(content && replicas.length > 0),
      expectedOffset: content && replicas.length > 0 ? size : 0,
      replicaCount: replicas.length,
      mediaType: body.mediaType || content?.mediaType || null,
      sources: replicas.length > 0
        ? [{
            peerId: inventoryManifest.peer.id,
            peerName: inventoryManifest.peer.name,
            state: "available",
          }]
        : [],
    },
  };
}

async function handler(request, response) {
  const origin = allowedOrigin(request);
  if (origin === false) {
    json(response, 403, { error: "This website is not allowed to read the private library." }, null);
    return;
  }

  if (request.method === "OPTIONS") {
    applyCors(response, origin);
    response.writeHead(204);
    response.end();
    return;
  }

  const url = new URL(request.url, "http://127.0.0.1");

  if (!hasAccess(request, url)) {
    json(response, 401, { error: "This private library requires an access token." }, origin);
    return;
  }

  if (request.method === "GET" && url.pathname === "/api/health") {
    const peerStatus = phonePeer.status();
    json(response, 200, {
      ok: true,
      appVersion: process.env.PRIVATE_REEL_APP_VERSION || null,
      source: config.name,
      items: catalog.items.length + (peerStatus.connected ? peerStatus.photos + peerStatus.videos : 0),
      watching: watchers.length > 0,
      optimizing: optimizingCount,
      phoneItems: peerStatus.connected ? peerStatus.photos + peerStatus.videos : 0,
      phoneConnected: peerStatus.connected,
      phoneName: peerStatus.connected ? peerStatus.deviceName : null,
    }, origin);
    return;
  }

  if (request.method === "GET" && url.pathname === "/api/discovery") {
    json(response, 200, {
      product: "private-reel",
      protocolVersion: 1,
      peerId: inventoryManifest.peer.id,
      name: config.name || inventoryManifest.peer.name || "Mac",
      capabilities: {
        federation: true,
        keep: true,
        keepCategories: ["movies", "series", "recipes", "courses"],
        sourceRemoval: true,
      },
    }, origin);
    return;
  }

  if (request.method === "GET" && url.pathname === "/api/peers") {
    json(response, 200, { peers: await discoveredPeers() }, origin);
    return;
  }

  if (request.method === "GET" && url.pathname === "/api/catalog") {
    const combined = await catalogWithLivePhone();
    if (!combined.live) {
      sendCatalog(request, response, origin);
      return;
    }
    json(response, 200, combined.body, origin);
    return;
  }

  if (request.method === "GET" && url.pathname === "/api/phone/status") {
    json(response, 200, phonePeer.status(), origin);
    return;
  }

  if (request.method === "POST" && url.pathname === "/api/phone/peer") {
    if (!hasWriteAccess(request)) {
      json(response, 403, { error: "Phone sharing requires a trusted local/Tailscale connection or the legacy access token." }, origin);
      return;
    }
    try {
      json(response, 200, phonePeer.register(await readJsonBody(request, 64 * 1024)), origin);
    } catch (error) {
      if (error instanceof PhonePeerError) throw error;
      throw new PhonePeerError(400, error instanceof Error ? error.message : "Invalid phone peer registration.");
    }
    return;
  }

  if (request.method === "DELETE" && url.pathname === "/api/phone/peer") {
    if (!hasWriteAccess(request)) {
      json(response, 403, { error: "Phone sharing requires a trusted local/Tailscale connection or the legacy access token." }, origin);
      return;
    }
    json(response, 200, { disconnected: phonePeer.clear(url.searchParams.get("deviceId") || "") }, origin);
    return;
  }

  if (url.pathname.startsWith("/api/phone/assets")) {
    json(response, 410, { error: "Copy-based phone ingest is disabled. Start live Wi-Fi sharing in the iPhone app." }, origin);
    return;
  }

  if (request.method === "GET" && url.pathname === "/api/inventory/summary") {
    json(response, 200, inventorySummary(), origin);
    return;
  }

  const inventoryContentMatch = /^\/api\/inventory\/content\/([a-f0-9]{20})$/.exec(url.pathname);
  if (request.method === "GET" && inventoryContentMatch) {
    const item = itemsById.get(inventoryContentMatch[1]);
    if (!item?.contentSha256) {
      json(response, 404, { error: "Media item not found in this peer inventory." }, origin);
      return;
    }
    json(response, 200, {
      id: item.id,
      contentId: item.contentId || item.id,
      sha256: item.contentSha256,
      size: item.public.size,
      kind: item.public.kind,
      title: item.public.title,
      filename: basename(item.path),
      extension: extname(item.path).toLowerCase(),
      downloadUrl: `/media/${item.id}/download`,
      playlist: item.public.playlist || null,
    }, origin);
    return;
  }

  if (request.method === "GET" && url.pathname === "/api/keep/catalog") {
    const items = [...keepContentById.values()]
      .filter((item) => item.size > 0)
      .map(publicKeepContent)
      .sort((left, right) => left.category.localeCompare(right.category) || left.title.localeCompare(right.title));
    json(response, 200, {
      source: { name: config.name || "Mac", peerId: inventoryManifest.peer.id },
      categories: [
        { id: "movies", label: "Movies", count: items.filter((item) => item.category === "movies").length },
        { id: "series", label: "Series", count: items.filter((item) => item.category === "series").length },
        { id: "recipes", label: "Recipes", count: items.filter((item) => item.category === "recipes").length },
        { id: "courses", label: "Courses", count: items.filter((item) => item.category === "courses").length },
      ],
      items,
    }, origin);
    return;
  }

  const keepContentMatch = /^\/api\/keep\/content\/([a-f0-9]{20})$/.exec(url.pathname);
  if (request.method === "GET" && keepContentMatch) {
    const item = keepContentById.get(keepContentMatch[1]);
    if (!item) {
      json(response, 404, { error: "Keep content was not found on this peer." }, origin);
      return;
    }
    json(response, 200, await keepContentInfo(item), origin);
    return;
  }

  if (request.method === "DELETE" && keepContentMatch) {
    if (!hasWriteAccess(request)) {
      json(response, 403, { error: "Removing source data requires a trusted local or Tailscale connection." }, origin);
      return;
    }
    const item = keepContentById.get(keepContentMatch[1]);
    if (!item) {
      json(response, 404, { error: "Keep content was not found on this peer." }, origin);
      return;
    }
    await rm(item.path);
    keepContentById.delete(item.id);
    if (item.category === "recipes" || item.category === "courses") {
      scheduleCookingScan("source cooking content was moved to another peer", 50);
    } else {
      scheduleScan("source media was moved to another peer", 50);
    }
    json(response, 200, { deleted: true, id: item.id }, origin);
    return;
  }

  const keepContentFileMatch = /^\/api\/keep\/content\/([a-f0-9]{20})\/file$/.exec(url.pathname);
  if (keepContentFileMatch && (request.method === "GET" || request.method === "HEAD")) {
    const item = keepContentById.get(keepContentFileMatch[1]);
    if (!item) {
      json(response, 404, { error: "Keep content was not found on this peer." }, origin);
      return;
    }
    await sendFile(request, response, item.path, origin, "attachment", item.filename);
    return;
  }

  if (request.method === "GET" && url.pathname === "/api/keep/jobs") {
    json(response, 200, { jobs: peerKeep ? peerKeep.list() : [], keepDirectory: resolveKeepDirectory(config) }, origin);
    return;
  }

  const keepJobMatch = /^\/api\/keep\/jobs\/([a-z0-9]+)$/i.exec(url.pathname);
  if (request.method === "GET" && keepJobMatch) {
    json(response, 200, peerKeep.get(keepJobMatch[1]), origin);
    return;
  }

  if (request.method === "POST" && url.pathname === "/api/keep/jobs") {
    if (!hasWriteAccess(request)) {
      json(response, 403, { error: "Keep requires a trusted local/Tailscale connection or the legacy access token." }, origin);
      return;
    }
    try {
      const body = await readJsonBody(request, 64 * 1024);
      if (body?.local) {
        const itemId = typeof body.itemId === "string" ? body.itemId.trim().toLowerCase() : "";
        const item = itemsById.get(itemId);
        if (!item?.path || !item?.public?.size) {
          throw new KeepError(404, "Media item not found in this peer inventory.");
        }
        json(response, 202, await peerKeep.startLocal({
          itemId: item.id,
          contentId: item.contentId || item.id,
          title: item.public.title,
          size: item.public.size,
          sha256: item.contentSha256 || body.sha256 || null,
          filename: basename(item.path),
          extension: extname(item.path).toLowerCase(),
          kind: item.public.kind,
          seriesName: body.seriesName || item.public.playlist || null,
          sourcePath: item.path,
          sourceName: typeof body.sourceName === "string" ? body.sourceName : "This Mac",
        }), origin);
        return;
      }
      json(response, 202, await peerKeep.start(body), origin);
    } catch (error) {
      if (error instanceof KeepError) throw error;
      throw new KeepError(400, error instanceof Error ? error.message : "Invalid keep request.");
    }
    return;
  }

  const keepCancelMatch = /^\/api\/keep\/jobs\/([a-z0-9]+)\/cancel$/i.exec(url.pathname);
  if (request.method === "POST" && keepCancelMatch) {
    if (!hasWriteAccess(request)) {
      json(response, 403, { error: "Keep requires a trusted local/Tailscale connection or the legacy access token." }, origin);
      return;
    }
    json(response, 200, await peerKeep.cancel(keepCancelMatch[1]), origin);
    return;
  }

  const keepResumeMatch = /^\/api\/keep\/jobs\/([a-z0-9]+)\/resume$/i.exec(url.pathname);
  if (request.method === "POST" && keepResumeMatch) {
    if (!hasWriteAccess(request)) {
      json(response, 403, { error: "Keep requires a trusted local/Tailscale connection or the legacy access token." }, origin);
      return;
    }
    json(response, 200, await peerKeep.resume(keepResumeMatch[1]), origin);
    return;
  }

  if (request.method === "DELETE" && keepJobMatch) {
    if (!hasWriteAccess(request)) {
      json(response, 403, { error: "Keep requires a trusted local/Tailscale connection or the legacy access token." }, origin);
      return;
    }
    json(response, 200, await peerKeep.deleteKept(keepJobMatch[1]), origin);
    return;
  }

  if (request.method === "POST" && url.pathname === "/api/inventory/prepare") {
    try {
      const result = prepareInventoryResponse(await readJsonBody(request));
      json(response, result.status, result.body, origin);
    } catch (error) {
      json(response, 400, { error: error instanceof Error ? error.message : "Invalid inventory request." }, origin);
    }
    return;
  }

  if (request.method === "GET" && url.pathname === "/api/cooking/catalog") {
    json(response, 200, cookingCatalog || await scanCookingLibrary(), origin);
    return;
  }

  if (request.method === "POST" && url.pathname === "/api/cooking/refresh") {
    json(response, 200, await scanCookingLibrary(), origin);
    return;
  }

  if (url.pathname === "/cooking/file" && (request.method === "GET" || request.method === "HEAD")) {
    const path = resolveCookingFile(resolveCookingDirectory(config), url.searchParams.get("path"));
    if (!path) {
      json(response, 400, { error: "Invalid cooking file path." }, origin);
      return;
    }
    try {
      if (!(await stat(path)).isFile()) throw new Error("Not a file");
      await sendCookingFile(request, response, path, origin);
    } catch {
      json(response, 404, { error: "Cooking file not found." }, origin);
    }
    return;
  }

  if (request.method === "POST" && url.pathname === "/api/refresh") {
    await scanLibrary();
    const combined = await catalogWithLivePhone();
    json(response, 200, combined.body, origin);
    return;
  }

  const match = /^\/media\/([a-f0-9]{20})\/(thumbnail|stream|download)$/.exec(url.pathname);
  if (match && (request.method === "GET" || request.method === "HEAD")) {
    const item = itemsById.get(match[1]);
    if (!item) {
      json(response, 404, { error: "Media item not found." }, origin);
      return;
    }
    if (match[2] === "thumbnail") {
      if (item.public.kind === "image") {
        item.thumbnailPath = await ensureThumbnail(
          item.path,
          item.id,
          item.public.duration,
          { mtimeMs: item.fileMtimeMs },
          "image",
        );
      }
      if (!item.thumbnailPath) {
        json(response, 404, { error: "Thumbnail not available." }, origin);
        return;
      }
      applyCors(response, origin);
      const thumbnailStat = await stat(item.thumbnailPath);
      response.writeHead(200, {
        "Content-Type": imageMimeTypes[extname(item.thumbnailPath).toLowerCase()] || "image/jpeg",
        "Content-Length": thumbnailStat.size,
        "Cache-Control": "private, max-age=86400",
      });
      if (request.method === "HEAD") return response.end();
      createReadStream(item.thumbnailPath).pipe(response);
      return;
    }
    const mediaPath = match[2] === "download" ? item.path : item.streamPath;
    await sendFile(
      request,
      response,
      mediaPath,
      origin,
      match[2] === "download" ? "attachment" : "inline",
      basename(item.path),
    );
    return;
  }

  const phoneMediaMatch = /^\/phone\/live\/([a-f0-9]{20})\/(thumbnail|stream|download)$/.exec(url.pathname);
  if (phoneMediaMatch && (request.method === "GET" || request.method === "HEAD")) {
    await proxyPhoneMedia(request, response, origin, phoneMediaMatch[1], phoneMediaMatch[2]);
    return;
  }

  json(response, 404, { error: "Not found." }, origin);
}

async function start() {
  config = JSON.parse(await readFile(configPath, "utf8"));
  await Promise.all([
    mkdir(resolveKeepDirectory(config), { recursive: true }),
    mkdir(join(resolveCookingDirectory(config), "Recipes"), { recursive: true }),
  ]);
  await loadInventoryManifest();
  startWatchers();
  await Promise.all([scanLibrary(), scanCookingLibrary()]);
  const port = Number(process.env.PRIVATE_REEL_PORT || config.port || 47821);
  const server = createServer((request, response) => {
    handler(request, response).catch((error) => {
      console.error(error);
      const status = error instanceof PhonePeerError || error instanceof KeepError ? error.status : 500;
      const body = error instanceof PhonePeerError || error instanceof KeepError
        ? { error: error.message }
        : { error: "The media agent encountered an error." };
      if (!response.headersSent) json(response, status, body, allowedOrigin(request) || null);
      else response.destroy(error);
    });
  });
  peerKeep = createPeerKeepRegistry({
    keepDirectory: resolveKeepDirectory(config),
    categoryDirectories: {
      recipes: resolveCookingDirectory(config),
      courses: resolveCookingDirectory(config),
    },
    partialsDirectory: join(cacheDirectory, "keep-partials"),
    statePath: join(cacheDirectory, "keep-jobs.json"),
    prepareLocal: async ({ sha256, size, mediaType }) => prepareInventoryResponse({ sha256, size, mediaType }).body,
    onComplete: async (job) => {
      if (job.category === "recipes" || job.category === "courses") {
        scheduleCookingScan("a completed cooking keep", 250);
      } else {
        scheduleScan("a completed keep", 250);
      }
    },
  });
  await peerKeep.ready();
  console.log(`Keep target directory: ${resolveKeepDirectory(config)}`);
  server.listen(port, "127.0.0.1", () => {
    console.log(`Private Reel agent is ready at http://127.0.0.1:${port}`);
    console.log("It listens on localhost only; use Tailscale Serve for private device access.");
  });
}

start().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
