// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

/**
 * Source operations: list, remove, update.
 *
 * Provides unified operations across all source kinds (local, managed, remote).
 * The CLI's `akm list`, `akm remove`, and `akm update` commands are wired here.
 */

import fs from "node:fs";
import path from "node:path";
import { isWithin, resolveStashDir } from "../../core/common";
import { getSources, loadConfig } from "../../core/config/config";
import { NotFoundError, UsageError } from "../../core/errors";
import { akmIndex } from "../../indexer/indexer";
import { removeLockEntry, upsertLockEntry } from "../../integrations/lockfile";
import { parseRegistryRef } from "../../registry/resolve";
import type { InstalledStashEntry } from "../../registry/types";
import { parseGitRepoUrl, syncMirroredRepo } from "../../sources/providers/git";
import { syncFromRef } from "../../sources/providers/sync-from-ref";
import type { RemoveResponse, SourceEntry, SourceKind, SourceListResponse, UpdateResponse } from "../../sources/types";
import { ensureWebsiteMirror } from "../../sources/website-ingest";
import { listWikis, resolveWikisRoot } from "../../wiki/wiki";
import { removeInstalledRegistryEntry, upsertInstalledRegistryEntry } from "./source-add";
import { removeStash } from "./source-manage";

export async function akmListSources(input?: { stashDir?: string; kind?: SourceKind[] }): Promise<SourceListResponse> {
  const stashDir = input?.stashDir ?? resolveStashDir();
  const config = loadConfig();
  const kindFilter = input?.kind;

  const sources: SourceEntry[] = [];

  // Stash entries — each entry exposes its provider type as kind (spec §2.1).
  // Writable defaults: true for filesystem, false for git/npm/website (CLAUDE.md "Writes").
  for (const stash of getSources(config)) {
    const kind: SourceKind = (stash.type as SourceKind) ?? "filesystem";
    if (kindFilter && !kindFilter.includes(kind)) continue;

    const isFilesystem = kind === "filesystem";
    const writableDefault = isFilesystem;
    const name = stash.name ?? stash.path ?? stash.url ?? "unknown";
    sources.push({
      name,
      kind,
      wiki: stash.wikiName,
      path: stash.path,
      provider: stash.url != null ? stash.type : undefined,
      writable: stash.writable !== undefined ? stash.writable : writableDefault,
      status: { exists: stash.path ? directoryExists(stash.path) : true },
    });
  }

  // Installed entries → managed sources
  for (const entry of config.installed ?? []) {
    const kind: SourceKind = "managed";
    if (kindFilter && !kindFilter.includes(kind)) continue;

    sources.push({
      name: entry.id,
      kind,
      wiki: entry.wikiName,
      path: entry.stashRoot,
      ref: entry.ref,
      version: entry.resolvedVersion,
      writable: entry.writable === true,
      status: { exists: directoryExists(entry.stashRoot) },
    });
  }

  if (!kindFilter || kindFilter.includes("filesystem")) {
    const wikisRoot = resolveWikisRoot(stashDir);
    const seenPaths = new Set(
      sources
        .map((source) => source.path)
        .filter((sourcePath): sourcePath is string => typeof sourcePath === "string")
        .map((sourcePath) => path.resolve(sourcePath)),
    );
    for (const wiki of listWikis(stashDir)) {
      // `listWikis()` also includes externally-registered wikis. `akm list`
      // should synthesize source entries here only for stash-owned wiki dirs.
      if (!isWithin(wiki.path, wikisRoot)) continue;
      const resolvedPath = path.resolve(wiki.path);
      if (seenPaths.has(resolvedPath)) continue;
      seenPaths.add(resolvedPath);
      sources.push({
        name: wiki.name,
        kind: "filesystem",
        wiki: wiki.name,
        path: wiki.path,
        writable: true,
        status: { exists: directoryExists(wiki.path) },
      });
    }
  }

  return {
    schemaVersion: 1,
    stashDir,
    sources,
    totalSources: sources.length,
  };
}

export async function akmRemove(input: { target: string; stashDir?: string }): Promise<RemoveResponse> {
  const target = input.target.trim();
  if (!target)
    throw new UsageError(
      "Target is required. Provide the source id, ref, path, URL, or name (e.g. `akm remove npm:@scope/stash` or `akm remove ~/my-stash`).",
    );

  const stashDir = input.stashDir ?? resolveStashDir();
  const config = loadConfig();
  const installed = config.installed ?? [];

  // Try installed[] first (managed sources)
  const entry = tryResolveInstalledTarget(installed, target);

  if (entry) {
    const updatedConfig = removeInstalledRegistryEntry(entry.id);
    await removeLockEntry(entry.id);
    if (entry.source !== "local") {
      cleanupDirectoryBestEffort(entry.cacheDir);
    }
    const index = await akmIndex({ stashDir });

    return {
      schemaVersion: 1,
      stashDir,
      target,
      removed: {
        id: entry.id,
        source: entry.source,
        ref: entry.ref,
        cacheDir: entry.cacheDir,
        stashRoot: entry.stashRoot,
      },
      config: {
        sourceCount: getSources(updatedConfig).length,
        installedKitCount: updatedConfig.installed?.length ?? 0,
      },
      index: {
        mode: index.mode,
        totalEntries: index.totalEntries,
        directoriesScanned: index.directoriesScanned,
        directoriesSkipped: index.directoriesSkipped,
      },
    };
  }

  // Fall through to stashes[] (local/remote sources)
  const stashResult = removeStash(target);
  if (!stashResult.removed || !stashResult.entry) {
    throw new NotFoundError(`No matching source for target: ${target}`, "SOURCE_NOT_FOUND");
  }

  const removedEntry = stashResult.entry;
  const index = await akmIndex({ stashDir });
  const updatedConfig = loadConfig();

  return {
    schemaVersion: 1,
    stashDir,
    target,
    removed: {
      id: removedEntry.name ?? removedEntry.path ?? removedEntry.url ?? target,
      source: removedEntry.type,
      ref: removedEntry.path ?? removedEntry.url ?? target,
      cacheDir: "",
      stashRoot: removedEntry.path ?? "",
    },
    config: {
      sourceCount: getSources(updatedConfig).length,
      installedKitCount: updatedConfig.installed?.length ?? 0,
    },
    index: {
      mode: index.mode,
      totalEntries: index.totalEntries,
      directoriesScanned: index.directoriesScanned,
      directoriesSkipped: index.directoriesSkipped,
    },
  };
}

// ── akmUpdate helpers ────────────────────────────────────────────────────────

/** Build a standard UpdateResponse summary block from the current config and index run. */
async function buildUpdateResponse(
  stashDir: string,
  target: string | undefined,
  all: boolean,
  processed: UpdateResponse["processed"],
  full = false,
): Promise<UpdateResponse> {
  const index = await akmIndex({ stashDir, ...(full ? { full: true } : {}) });
  const finalConfig = loadConfig();
  return {
    schemaVersion: 1,
    stashDir,
    target,
    all,
    processed,
    config: {
      sourceCount: getSources(finalConfig).length,
      installedKitCount: finalConfig.installed?.length ?? 0,
    },
    index: {
      mode: index.mode,
      totalEntries: index.totalEntries,
      directoriesScanned: index.directoriesScanned,
      directoriesSkipped: index.directoriesSkipped,
    },
  };
}

/** Sync a git-mirrored source and return an UpdateResponse. */
async function updateGitSource(
  stashDir: string,
  target: string,
  all: boolean,
  gitSource: ReturnType<typeof getSources>[number],
): Promise<UpdateResponse> {
  await syncMirroredRepo(gitSource, { force: true, writable: gitSource.writable === true });
  return buildUpdateResponse(stashDir, target, all, [], true);
}

/** Re-crawl a website source and return an UpdateResponse. */
async function updateWebsiteSource(
  stashDir: string,
  target: string,
  all: boolean,
  websiteSource: ReturnType<typeof getSources>[number],
): Promise<UpdateResponse> {
  // TODO: full incremental re-crawl with delta tracking (#19)
  await ensureWebsiteMirror(websiteSource, { requireStashDir: true, force: true });
  return buildUpdateResponse(stashDir, target, all, []);
}

/** Sync a single installed registry entry and return the processed record. */
async function updateRegistryEntry(
  entry: InstalledStashEntry,
  force: boolean,
): Promise<UpdateResponse["processed"][number]> {
  if (force && shouldCleanupCache(entry)) {
    cleanupDirectoryBestEffort(entry.cacheDir);
  }
  const synced = await syncFromRef(entry.ref, { force });

  const installedEntry: InstalledStashEntry = {
    id: synced.id,
    source: synced.source,
    ref: synced.ref,
    artifactUrl: synced.artifactUrl,
    resolvedVersion: synced.resolvedVersion,
    resolvedRevision: synced.resolvedRevision,
    stashRoot: synced.contentDir,
    cacheDir: synced.cacheDir,
    installedAt: synced.syncedAt,
    writable: synced.writable ?? entry.writable,
    ...(entry.wikiName ? { wikiName: entry.wikiName } : {}),
  };
  upsertInstalledRegistryEntry(installedEntry);
  await upsertLockEntry({
    id: synced.id,
    source: synced.source,
    ref: synced.ref,
    resolvedVersion: synced.resolvedVersion,
    resolvedRevision: synced.resolvedRevision,
    integrity: synced.integrity ?? (synced.source === "local" ? "local" : undefined),
  });
  if (entry.cacheDir !== synced.cacheDir && shouldCleanupCache(entry)) {
    cleanupDirectoryBestEffort(entry.cacheDir);
  }

  const versionChanged = (entry.resolvedVersion ?? "") !== (synced.resolvedVersion ?? "");
  const revisionChanged = (entry.resolvedRevision ?? "") !== (synced.resolvedRevision ?? "");

  return {
    id: entry.id,
    source: entry.source,
    ref: entry.ref,
    previous: {
      resolvedVersion: entry.resolvedVersion,
      resolvedRevision: entry.resolvedRevision,
      cacheDir: entry.cacheDir,
    },
    installed: { ...installedEntry, extractedDir: synced.extractedDir },
    changed: {
      version: versionChanged,
      revision: revisionChanged,
      any: versionChanged || revisionChanged,
    },
  };
}

// ── akmUpdate dispatcher ─────────────────────────────────────────────────────

export async function akmUpdate(input?: {
  target?: string;
  all?: boolean;
  force?: boolean;
  stashDir?: string;
}): Promise<UpdateResponse> {
  const stashDir = input?.stashDir ?? resolveStashDir();
  const target = input?.target?.trim();
  const all = input?.all === true;
  const force = input?.force === true;
  const config = loadConfig();
  const installedEntries = config.installed ?? [];

  // Check if the target refers to a git or website source — those are stored
  // in sources[] not installed[] and need a different update path.
  if (target && !all) {
    const stashes = getSources(config);
    const isUrl = target.startsWith("http://") || target.startsWith("https://");
    const resolvedPath = !isUrl ? path.resolve(target) : undefined;
    const gitMatch = stashes.find((s) => {
      if (s.type !== "git") return false;
      if (isUrl && s.url === target) return true;
      if (resolvedPath && s.path && path.resolve(s.path) === resolvedPath) return true;
      if (s.name === target) return true;
      if (s.url) {
        try {
          const repo = parseGitRepoUrl(s.url);
          if (repo.canonicalUrl === target) return true;
        } catch {
          // Ignore malformed config here; later provider sync will surface it.
        }
      }
      return false;
    });
    if (gitMatch) return updateGitSource(stashDir, target, all, gitMatch);

    const websiteMatch = stashes.find((s) => {
      if (s.type !== "website") return false;
      if (isUrl && s.url === target) return true;
      if (s.name === target) return true;
      if (resolvedPath && s.path && path.resolve(s.path) === resolvedPath) return true;
      return false;
    });
    if (websiteMatch) return updateWebsiteSource(stashDir, target, all, websiteMatch);
  }

  const selectedEntries = selectTargets(installedEntries, target, all);
  const processed: UpdateResponse["processed"] = [];
  for (const entry of selectedEntries) {
    processed.push(await updateRegistryEntry(entry, force));
  }

  return buildUpdateResponse(stashDir, target, all, processed);
}

function selectTargets(
  installed: InstalledStashEntry[],
  target: string | undefined,
  all: boolean,
): InstalledStashEntry[] {
  if (all && target) {
    throw new UsageError("Specify either <target> or --all, not both.", "MISSING_OR_AMBIGUOUS_TARGET");
  }
  if (all) return installed;
  if (!target) {
    throw new UsageError("Either <target> or --all is required.", "MISSING_OR_AMBIGUOUS_TARGET");
  }

  const found = tryResolveInstalledTarget(installed, target);
  if (found) return [found];

  // Check if target matches a stash source and give a helpful message
  const config = loadConfig();
  const stashes = getSources(config);
  const isUrl = target.startsWith("http://") || target.startsWith("https://");
  const resolvedPath = !isUrl ? path.resolve(target) : undefined;
  const stashMatch = stashes.find((s) => {
    if (isUrl && s.url === target) return true;
    if (resolvedPath && s.path && path.resolve(s.path) === resolvedPath) return true;
    if (s.name === target) return true;
    return false;
  });

  if (stashMatch) {
    if (stashMatch.type === "website") {
      // Website sources should be handled before reaching selectTargets.
      // This path should not be reached; surface a clear message if it is.
      throw new UsageError(
        `"${target}" is a website source — website caching not yet implemented for --all. ` +
          `Run \`akm update ${target}\` to re-mirror this source individually.`,
        "TARGET_NOT_UPDATABLE",
      );
    }
    throw new UsageError(
      `"${target}" is a local directory — it reflects your files in place. To refresh the search index, run: akm index`,
      "TARGET_NOT_UPDATABLE",
    );
  }

  throw new NotFoundError(`No matching source for target: ${target}`, "SOURCE_NOT_FOUND");
}

function tryResolveInstalledTarget(installed: InstalledStashEntry[], target: string): InstalledStashEntry | undefined {
  const byId = installed.find((entry) => entry.id === target);
  if (byId) return byId;

  const byRef = installed.find((entry) => entry.ref === target);
  if (byRef) return byRef;

  let parsedId: string | undefined;
  try {
    parsedId = parseRegistryRef(target).id;
  } catch {
    parsedId = undefined;
  }
  if (parsedId) {
    const byParsedId = installed.find((entry) => entry.id === parsedId);
    if (byParsedId) return byParsedId;
  }

  return undefined;
}

function cleanupDirectoryBestEffort(target: string): void {
  try {
    fs.rmSync(target, { recursive: true, force: true });
  } catch {
    // Best-effort cleanup only.
  }
}

function shouldCleanupCache(entry: InstalledStashEntry): boolean {
  return entry.source !== "local";
}

function directoryExists(target: string): boolean {
  try {
    return fs.statSync(target).isDirectory();
  } catch {
    return false;
  }
}
