import { promises as fs } from "node:fs";
import path from "node:path";
import { developerSearchIndex, resolveStarter, starterKits } from "./catalog.mjs";
import { selectIntegrationManifest, validateIntegrationManifestFile, writeIntegrationManifest } from "./integration-manifest.mjs";
import { validateLaunchDossierFile } from "./launch-dossier.mjs";
import { inspectStarterProject, isSafeRelativePath } from "./project-doctor.mjs";

const VERSION = "0.1.0";
const help = `Mappls Developer CLI ${VERSION}

Offline, source-bounded project discovery and scaffolding.

Usage:
  mappls-developer platforms [--json]
  mappls-developer search <query> [--limit <1-50>] [--json]
  mappls-developer starter list [--json]
  mappls-developer starter show <slug|platform> [--json]
  mappls-developer starter create <slug|platform> [--dir <path>] [--json]
  mappls-developer manifest create --use-case <slug> --platform <platform> --mode <mode> [--out <path>] [--json]
  mappls-developer manifest validate [path] [--json]
  mappls-developer dossier validate [path] [--manifest <path>] [--json]
  mappls-developer doctor [path] [--json]

This CLI does not accept provider credentials or make live Mappls calls.`;

function parseOptions(args, valueOptions = []) {
  const values = {};
  const positionals = [];
  for (let index = 0; index < args.length; index += 1) {
    const value = args[index];
    if (value === "--json") { values.json = true; continue; }
    if (value.startsWith("--")) {
      if (!valueOptions.includes(value)) throw new Error(`unknown_option:${value}`);
      const next = args[++index];
      if (!next || next.startsWith("--")) throw new Error(`missing_option_value:${value}`);
      values[value.slice(2)] = next;
      continue;
    }
    positionals.push(value);
  }
  return { values, positionals };
}

function renderRows(rows, columns) {
  const widths = columns.map(({ key, label }) => Math.max(label.length, ...rows.map((row) => String(row[key] ?? "").length)));
  const line = (row) => columns.map(({ key }, index) => String(row[key] ?? "").padEnd(widths[index])).join("  ").trimEnd();
  return [columns.map(({ label }, index) => label.padEnd(widths[index])).join("  ").trimEnd(), columns.map((_, index) => "-".repeat(widths[index])).join("  "), ...rows.map(line)].join("\n");
}

function searchCatalog(query, limit) {
  const phrase = query.trim().toLowerCase();
  const terms = [...new Set(phrase.split(/\s+/).filter(Boolean))];
  return developerSearchIndex.map((entry) => {
    const title = entry.title.toLowerCase();
    const description = entry.description.toLowerCase();
    const keywords = entry.keywords.toLowerCase();
    const eyebrow = entry.eyebrow.toLowerCase();
    let score = title.includes(phrase) ? 50 : 0;
    for (const term of terms) score += (title.includes(term) ? 12 : 0) + (keywords.includes(term) ? 5 : 0) + (description.includes(term) ? 3 : 0) + (eyebrow.includes(term) ? 1 : 0);
    return { ...entry, score };
  }).filter((entry) => entry.score > 0).sort((left, right) => right.score - left.score || left.title.localeCompare(right.title)).slice(0, limit);
}

async function createStarter(kit, target) {
  const root = path.resolve(target);
  const existing = await fs.lstat(root).catch(() => undefined);
  if (existing?.isSymbolicLink()) throw new Error("target_is_symbolic_link");
  if (existing && !existing.isDirectory()) throw new Error("target_is_not_directory");
  if (existing && (await fs.readdir(root)).length) throw new Error("target_is_not_empty");
  const createdRoot = !existing;
  if (createdRoot) await fs.mkdir(root, { recursive: true });
  try {
    for (const file of kit.files) {
      if (!isSafeRelativePath(file.path)) throw new Error(`unsafe_starter_path:${file.path}`);
      const destination = path.resolve(root, ...file.path.split("/"));
      if (!destination.startsWith(`${root}${path.sep}`)) throw new Error(`unsafe_starter_path:${file.path}`);
      await fs.mkdir(path.dirname(destination), { recursive: true });
      await fs.writeFile(destination, file.content, { encoding: "utf8", flag: "wx" });
    }
  } catch (error) {
    if (createdRoot) await fs.rm(root, { recursive: true, force: true });
    throw error;
  }
  return { root, files: kit.files.length };
}

function defaultContext(context = {}) {
  return {
    cwd: context.cwd ?? process.cwd(),
    stdout: context.stdout ?? ((value) => process.stdout.write(value)),
    stderr: context.stderr ?? ((value) => process.stderr.write(value)),
  };
}

export async function runCli(argv, suppliedContext) {
  const context = defaultContext(suppliedContext);
  const out = (value) => context.stdout(`${value}\n`);
  const fail = (message) => { context.stderr(`Error: ${message}\n`); return 1; };
  try {
    const [command, ...rest] = argv;
    if (!command || command === "help" || command === "--help" || command === "-h") { out(help); return 0; }
    if (command === "--version" || command === "-v") { out(VERSION); return 0; }

    if (command === "platforms") {
      const { values, positionals } = parseOptions(rest);
      if (positionals.length) return fail("platforms accepts no positional values");
      const rows = starterKits.map(({ platform, slug, readiness, language }) => ({ platform, starter: slug, readiness, language }));
      out(values.json ? JSON.stringify({ count: rows.length, platforms: rows }, null, 2) : renderRows(rows, [{ key: "platform", label: "PLATFORM" }, { key: "starter", label: "STARTER" }, { key: "readiness", label: "READINESS" }, { key: "language", label: "LANGUAGE" }]));
      return 0;
    }

    if (command === "search") {
      const { values, positionals } = parseOptions(rest, ["--limit"]);
      const query = positionals.join(" ").trim();
      if (query.length < 2 || query.length > 160) return fail("search query must contain 2-160 characters");
      const limit = values.limit === undefined ? 10 : Number(values.limit);
      if (!Number.isInteger(limit) || limit < 1 || limit > 50) return fail("--limit must be an integer from 1 to 50");
      const results = searchCatalog(query, limit);
      out(values.json ? JSON.stringify({ query, count: results.length, results }, null, 2) : results.length ? renderRows(results.map(({ kind, title, href }) => ({ kind, title, href })), [{ key: "kind", label: "KIND" }, { key: "title", label: "TITLE" }, { key: "href", label: "PORTAL PATH" }]) : `No catalog result for “${query}”.`);
      return 0;
    }

    if (command === "starter") {
      const [action, ...actionArgs] = rest;
      if (action === "list") return runCli(["platforms", ...actionArgs], context);
      if (action === "show") {
        const { values, positionals } = parseOptions(actionArgs);
        if (positionals.length !== 1) return fail("starter show requires one slug or platform");
        const kit = resolveStarter(positionals[0]);
        if (!kit) return fail("unknown starter; run `mappls-developer starter list`");
        const detail = { ...kit, files: kit.files.map(({ path: filePath, language, purpose }) => ({ path: filePath, language, purpose })) };
        out(values.json ? JSON.stringify(detail, null, 2) : `${kit.name}\n${kit.platform} · ${kit.language} · ${kit.readiness}\n${kit.summary}\n\nCredential boundary: ${kit.credentialBoundary}\nEvidence boundary: ${kit.evidenceBoundary}\nFiles: ${kit.files.length}\nPortal: /starter-kits/${kit.slug}`);
        return 0;
      }
      if (action === "create") {
        const { values, positionals } = parseOptions(actionArgs, ["--dir"]);
        if (positionals.length !== 1) return fail("starter create requires one slug or platform");
        const kit = resolveStarter(positionals[0]);
        if (!kit) return fail("unknown starter; run `mappls-developer starter list`");
        const target = values.dir ? path.resolve(context.cwd, values.dir) : path.resolve(context.cwd, kit.slug);
        const created = await createStarter(kit, target);
        const result = { status: "created", starter: kit.slug, platform: kit.platform, readiness: kit.readiness, path: created.root, files: created.files, liveProviderCallsEnabled: false };
        out(values.json ? JSON.stringify(result, null, 2) : `Created ${kit.name}\nPath: ${created.root}\nFiles: ${created.files}\nMode: fixture/contract-only\nNext: mappls-developer doctor ${JSON.stringify(created.root)}`);
        return 0;
      }
      return fail("starter requires list, show, or create");
    }

    if (command === "manifest") {
      const [action, ...actionArgs] = rest;
      if (action === "create") {
        const { values, positionals } = parseOptions(actionArgs, ["--use-case", "--platform", "--mode", "--out"]);
        if (positionals.length || !values["use-case"] || !values.platform || !values.mode) return fail("manifest create requires --use-case, --platform, and --mode");
        const manifest = selectIntegrationManifest({ useCaseSlug: values["use-case"], platform: values.platform, mode: values.mode });
        const target = path.resolve(context.cwd, values.out ?? "mappls.integration.json");
        const file = await writeIntegrationManifest(manifest, target);
        const result = { status: "created", file, manifestId: manifest.manifestId, schemaVersion: manifest.schemaVersion, contracts: manifest.contracts.length, sourceFingerprints: manifest.sourceEvidence.fingerprints.length, providerNetworkCalls: 0 };
        out(values.json ? JSON.stringify(result, null, 2) : `Created ${manifest.manifestId}\nFile: ${file}\nContracts: ${result.contracts}\nSource fingerprints: ${result.sourceFingerprints}\nNext: mappls-developer manifest validate ${JSON.stringify(file)}`);
        return 0;
      }
      if (action === "validate") {
        const { values, positionals } = parseOptions(actionArgs);
        if (positionals.length > 1) return fail("manifest validate accepts at most one file path");
        const report = await validateIntegrationManifestFile(path.resolve(context.cwd, positionals[0] ?? "mappls.integration.json"));
        if (values.json) out(JSON.stringify(report, null, 2));
        else {
          out(`Mappls integration manifest: ${report.status.toUpperCase()}\nFile: ${report.file}${report.manifest ? `\nIdentity: ${report.manifest.id}\nSelection: ${report.manifest.useCase} · ${report.manifest.platform} · ${report.manifest.mode}\nEvidence: ${report.manifest.contracts} contracts · ${report.manifest.sourceFingerprints} source fingerprints\nApplication overlay: ${report.applicationBound ? "present" : "none"}` : ""}`);
          for (const failure of report.failures) out(`FAIL  ${failure}`);
        }
        return report.status === "pass" ? 0 : 1;
      }
      return fail("manifest requires create or validate");
    }

    if (command === "dossier") {
      const [action, ...actionArgs] = rest;
      if (action !== "validate") return fail("dossier requires validate");
      const { values, positionals } = parseOptions(actionArgs, ["--manifest"]);
      if (positionals.length > 1) return fail("dossier validate accepts at most one file path");
      const report = await validateLaunchDossierFile(path.resolve(context.cwd, positionals[0] ?? "mappls.launch.json"), { manifestFile: values.manifest ? path.resolve(context.cwd, values.manifest) : undefined });
      if (values.json) out(JSON.stringify(report, null, 2));
      else {
        out(`Mappls application launch dossier: ${report.status.toUpperCase()}\nFile: ${report.file}${report.dossier ? `\nIdentity: ${report.dossier.id}\nApplication: ${report.dossier.applicationId}\nDecision: ${report.dossier.decision}\nAuthorities: ${report.dossier.satisfiedAuthorities}/${report.dossier.requiredAuthorities} satisfied\nProduction ready: no\nManifest cross-check: ${report.dossier.manifestCrossChecked ? "pass" : "not requested"}\nIntegrity: sha256:${report.dossier.payloadSha256}` : ""}`);
        for (const failure of report.failures) out(`FAIL  ${failure}`);
      }
      return report.status === "pass" ? 0 : 1;
    }

    if (command === "doctor") {
      const { values, positionals } = parseOptions(rest);
      if (positionals.length > 1) return fail("doctor accepts at most one project path");
      const report = await inspectStarterProject(path.resolve(context.cwd, positionals[0] ?? "."));
      if (values.json) out(JSON.stringify(report, null, 2));
      else {
        out(`Mappls project doctor: ${report.status.toUpperCase()}\nProject: ${report.root}${report.starter ? `\nStarter: ${report.starter.platform} · ${report.starter.readiness}` : ""}`);
        for (const item of report.passes) out(`PASS  ${item}`);
        for (const item of report.warnings) out(`WARN  ${item}`);
        for (const item of report.failures) out(`FAIL  ${item}`);
      }
      return report.status === "fail" ? 1 : 0;
    }

    return fail(`unknown command: ${command}`);
  } catch (error) {
    return fail(error instanceof Error ? error.message.replaceAll("_", " ") : "unexpected failure");
  }
}
