import { createHash } from "node:crypto";
import { readFile, readdir, stat } from "node:fs/promises";
import path from "node:path";

import { FACT_KINDS, InitializationStore, type FactInput, type FactKind, type ScanConfigInput } from "./initialization-store.ts";
import type { ProjectContext } from "./project-context.ts";

const DEFAULT_EXCLUDED = new Set([".git", ".devflow", ".planning", "node_modules", "dist", "build", "coverage", "target", "vendor", ".next", ".nuxt"]);
const TEXT_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".rb", ".go", ".java", ".kt", ".rs", ".php", ".sql", ".json", ".yaml", ".yml", ".toml", ".md"]);

type SourceFile = { repository: string; path: string; absolute: string; contentHash: string; size: number; text?: string };
type ScanDiagnostic = { repository: string; path: string; reason: "symlink" | "excluded" | "too_large" | "unreadable"; message?: string };
export type InitializationProgress = {
  runId: string;
  snapshotId?: string;
  completeness?: "complete" | "partial";
  diagnostics: string[];
  documents?: Array<{ targetPath: string; status: string; outputPath?: string; error?: string }>;
  database: string;
  projectId: string;
  workspaceId: string;
  planningRoot: string;
};

function digest(value: string | Uint8Array): string { return `sha256:${createHash("sha256").update(value).digest("hex")}`; }
function extension(file: string): string { return path.extname(file).toLowerCase(); }
function globMatches(value: string, pattern: string): boolean {
  const compile = (candidate: string) => candidate.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("**", "\0").replaceAll("*", "[^/]*").replaceAll("\0", ".*");
  return new RegExp(`^${compile(pattern)}$`).test(value)
    || (pattern.startsWith("**/") && new RegExp(`^${compile(pattern.slice(3))}$`).test(value));
}
function redactCommand(command: unknown): string {
  if (typeof command !== "string") return "";
  return command
    .replace(/\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|API_KEY)[A-Z0-9_]*)=([^\s]+)/gi, "$1=<redacted>")
    .replace(/(--?(?:token|secret|password|passwd|api[-_]?key))(?:=|\s+)([^\s]+)/gi, "$1=<redacted>")
    .replace(/(authorization\s*[:=]\s*bearer\s+)([^\s]+)/gi, "$1<redacted>")
    .replace(/(https?:\/\/)[^\s/@:]+:[^\s/@]+@/gi, "$1<redacted>@");
}
function source(file: SourceFile, kind: FactKind, objectKey: string, value: Record<string, unknown>, extractor: string, line?: number): FactInput {
  return { kind, objectKey, repository: file.repository, value, evidenceLevel: "observed", sourcePath: file.path, sourceLine: line, sourceFingerprint: file.contentHash, extractor, confidence: "heuristic" };
}

async function walk(repository: string, repositoryRoot: string, includes: string[], excludes: string[], diagnostics: ScanDiagnostic[], relative = ""): Promise<SourceFile[]> {
  const directory = path.join(repositoryRoot, relative);
  let entries;
  try { entries = await readdir(directory, { withFileTypes: true }); }
  catch (error) { diagnostics.push({ repository, path: relative || ".", reason: "unreadable", message: error instanceof Error ? error.message : String(error) }); return []; }
  const result: SourceFile[] = [];
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
    if (entry.isSymbolicLink()) { diagnostics.push({ repository, path: path.join(relative, entry.name), reason: "symlink" }); continue; }
    if (DEFAULT_EXCLUDED.has(entry.name)) continue;
    const child = path.join(relative, entry.name);
    const normalized = child.replaceAll("\\", "/");
    if (excludes.some(pattern => globMatches(normalized, pattern) || globMatches(`${normalized}/`, pattern))) { if (diagnostics.length < 1000) diagnostics.push({ repository, path: normalized, reason: "excluded" }); continue; }
    if (entry.isDirectory()) { result.push(...await walk(repository, repositoryRoot, includes, excludes, diagnostics, child)); continue; }
    if (!entry.isFile()) continue;
    if (includes.length && !includes.some(pattern => globMatches(normalized, pattern))) continue;
    const absolute = path.join(repositoryRoot, child); const info = await stat(absolute);
    if (info.size > 2 * 1024 * 1024) { diagnostics.push({ repository, path: normalized, reason: "too_large" }); continue; }
    let bytes: Buffer;
    try { bytes = await readFile(absolute); }
    catch (error) { diagnostics.push({ repository, path: normalized, reason: "unreadable", message: error instanceof Error ? error.message : String(error) }); continue; }
    result.push({ repository, path: child.replaceAll("\\", "/"), absolute, size: info.size, contentHash: digest(bytes), text: TEXT_EXTENSIONS.has(extension(child)) ? bytes.toString("utf8") : undefined });
  }
  return result;
}

async function gitRevision(root: string): Promise<string | null> {
  const child = Bun.spawn(["git", "-C", root, "rev-parse", "HEAD"], { stdout: "pipe", stderr: "ignore" });
  if (await child.exited !== 0) return null;
  const revision = (await new Response(child.stdout).text()).trim();
  return /^[0-9a-f]{40,64}$/i.test(revision) ? revision : null;
}

async function scan(context: ProjectContext, config: ScanConfigInput): Promise<{ files: SourceFile[]; diagnostics: ScanDiagnostic[]; repositoryVersions: Array<{ repository: string; revision: string | null }> }> {
  const all: SourceFile[] = [];
  const diagnostics: ScanDiagnostic[] = [];
  const repositoryVersions: Array<{ repository: string; revision: string | null }> = [];
  const includes = config.includes ?? ["**/*"];
  const excludes = config.excludes ?? [".git/**", ".planning/**", ".devflow/**"];
  for (const repository of config.repositories) {
    const root = path.resolve(context.root, repository.relativePath);
    if (!path.isAbsolute(repository.relativePath) && root !== context.root && !root.startsWith(`${context.root}${path.sep}`)) throw new Error(`Repository escapes project root: ${repository.relativePath}`);
    repositoryVersions.push({ repository: repository.name, revision: await gitRevision(root) });
    all.push(...await walk(repository.name, root, includes, excludes, diagnostics));
  }
  return { files: all.sort((a, b) => `${a.repository}/${a.path}`.localeCompare(`${b.repository}/${b.path}`)), diagnostics, repositoryVersions };
}

function frozenManifest(scanResult: Awaited<ReturnType<typeof scan>>): Array<{ repository: string; path: string; contentHash: string; size?: number }> {
  return [
    ...scanResult.files.map(({ repository, path: sourcePath, contentHash, size }) => ({ repository, path: sourcePath, contentHash, size })),
    ...scanResult.repositoryVersions.filter(item => item.revision).map(item => ({ repository: item.repository, path: "@source/git-head", contentHash: digest(item.revision!), size: item.revision!.length })),
  ];
}

function lineMatches(file: SourceFile, expression: RegExp): Array<{ line: number; match: RegExpMatchArray }> {
  return (file.text ?? "").split(/\r?\n/).flatMap((text, index) => {
    const match = text.match(expression); return match ? [{ line: index + 1, match }] : [];
  });
}

function collect(files: SourceFile[], context: ProjectContext, config: ScanConfigInput): Map<FactKind, FactInput[]> {
  const facts = new Map<FactKind, FactInput[]>(FACT_KINDS.map(kind => [kind, []]));
  const extensions = new Map<string, number>();
  for (const file of files) {
    facts.get("asset")!.push(source(file, "asset", `file:${file.repository}:${file.path}`, { path: file.path, repository: file.repository, size: file.size, extension: extension(file.path) || null }, "filesystem@1"));
    const extensionKey = `${file.repository}\0${extension(file.path) || "none"}`;
    extensions.set(extensionKey, (extensions.get(extensionKey) ?? 0) + 1);
    for (const { line, match } of lineMatches(file, /\b(?:app|router)\.(get|post|put|patch|delete)\s*\(\s*["'`]([^"'`]+)/i)) {
      facts.get("api")!.push(source(file, "api", `route:${match[1].toUpperCase()}:${match[2]}`, { method: match[1].toUpperCase(), path: match[2], runtimeVerified: false }, "static-route@1", line));
    }
    for (const { line, match } of lineMatches(file, /@(Get|Post|Put|Patch|Delete)Mapping\s*\(\s*["'`]([^"'`]+)/i)) {
      facts.get("api")!.push(source(file, "api", `route:${match[1].toUpperCase()}:${match[2]}`, { method: match[1].toUpperCase(), path: match[2], framework: "spring", runtimeVerified: false }, "static-route@1", line));
    }
    for (const { line, match } of lineMatches(file, /^\s*(get|post|put|patch|delete)\s+["'`]([^"'`]+)/i)) {
      facts.get("api")!.push(source(file, "api", `route:${match[1].toUpperCase()}:${match[2]}`, { method: match[1].toUpperCase(), path: match[2], framework: "rails", runtimeVerified: false }, "static-route@1", line));
    }
    for (const { line, match } of lineMatches(file, /\bHandleFunc\s*\(\s*["'`]([^"'`]+)/)) {
      facts.get("api")!.push(source(file, "api", `route:ANY:${match[1]}`, { method: null, path: match[1], framework: "go-http", runtimeVerified: false }, "static-route@1", line));
    }
    for (const { line, match } of lineMatches(file, /(?:path\s*[:=]|<Route[^>]+path=)\s*["'`]([^"'`]+)["'`]/i)) {
      facts.get("page")!.push(source(file, "page", `page:${match[1]}`, { route: match[1], renderedVerified: false }, "static-page@1", line));
    }
    const role = config.repositories.find(item => item.name === file.repository)?.role;
    const pageMatch = file.path.match(/(?:^|\/)(?:pages|app)\/(.+?)\.(?:tsx?|jsx?|vue)$/i);
    if (role === "frontend" && pageMatch) facts.get("page")!.push(source(file, "page", `page-file:${file.repository}:${file.path}`, { route: `/${pageMatch[1].replace(/\/index$/, "").replace(/\[(.+?)\]/g, ":$1")}`, componentPath: file.path, renderedVerified: false }, "filesystem-page@1"));
    for (const { line, match } of lineMatches(file, /\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?([A-Za-z_][\w.]*)/i)) {
      facts.get("data_model")!.push(source(file, "data_model", `model:${match[1]}`, { name: match[1], sourceType: extension(file.path) === ".sql" ? "migration" : "code" }, "static-model@1", line));
    }
    for (const { line, match } of lineMatches(file, /^\s*class\s+([A-Za-z_]\w*)\s*(?:extends\s+(?:Model|Entity)|\([^)]*(?:Model|Entity|Base)[^)]*\))/i)) {
      facts.get("data_model")!.push(source(file, "data_model", `model:${file.repository}:${match[1]}`, { name: match[1], sourceType: "code" }, "static-model@1", line));
    }
    for (const { line, match } of lineMatches(file, /https?:\/\/([^\s/"'`]+)/i)) {
      const host = match[1].replace(/^.*@/, "").toLowerCase();
      facts.get("integration")!.push(source(file, "integration", `host:${host}`, { host, enabledInEnvironment: null }, "static-integration@1", line));
    }
    for (const { line, match } of lineMatches(file, /\b(?:cron|schedule|subscribe|consumer)\s*\(/i)) {
      facts.get("integration")!.push(source(file, "integration", `task:${file.repository}:${file.path}:${line}`, { kind: match[0].split("(")[0].trim().toLowerCase(), enabledInEnvironment: null }, "static-task@1", line));
    }
    for (const { line, match } of lineMatches(file, /(?:process\.env\.|os\.getenv\s*\(\s*["']|ENV\s*\[\s*["']|System\.getenv\s*\(\s*["'])([A-Za-z_][A-Za-z0-9_]*)/)) {
      facts.get("integration")!.push(source(file, "integration", `config:${file.repository}:${match[1]}`, { name: match[1], valueStored: false, enabledInEnvironment: null }, "static-config@1", line));
    }
    if (/(^|\/)(AGENTS|CLAUDE|CONTRIBUTING)\.md$/i.test(file.path) || /(^|\/)\.cursorrules$/i.test(file.path)) {
      facts.get("rule")!.push(source(file, "rule", `rule:${file.repository}:${file.path}`, { path: file.path, scope: file.repository, explicit: true }, "rule-file@1"));
    }
    if (path.basename(file.path) === "package.json" && file.text) {
      try {
        const manifest = JSON.parse(file.text) as Record<string, any>;
        for (const group of ["dependencies", "devDependencies", "peerDependencies"]) for (const name of Object.keys(manifest[group] ?? {})) {
          facts.get("technology")!.push(source(file, "technology", `dependency:${file.repository}:${name}`, { name, declaration: group, locked: false }, "package-json@1"));
        }
        for (const [name, command] of Object.entries(manifest.scripts ?? {})) facts.get("toolchain")!.push(source(file, "toolchain", `command:${file.repository}:${name}`, { name, command: redactCommand(command), workingDirectory: path.dirname(file.path), executed: false }, "package-json@1"));
      } catch { /* diagnostics are attached by the caller's collection status */ }
    }
    if (/^(?:package-lock\.json|bun\.lockb?|pnpm-lock\.yaml|yarn\.lock|poetry\.lock|Gemfile\.lock)$/i.test(path.basename(file.path))) facts.get("technology")!.push(source(file, "technology", `lockfile:${file.repository}:${file.path}`, { path: file.path, locked: true }, "lockfile@1"));
    if (/(?:^|\/)(?:test|tests|spec|__tests__)(?:\/|$)|\.(?:test|spec)\.[^.]+$/i.test(file.path)) facts.get("toolchain")!.push(source(file, "toolchain", `test-definition:${file.repository}:${file.path}`, { testDefinition: file.path, executed: false }, "test-discovery@1"));
  }
  for (const [key, count] of extensions) {
    const [repository, ext] = key.split("\0");
    facts.get("technology")!.push({ kind: "technology", repository, objectKey: `language-extension:${ext}`, value: { extension: ext, fileCount: count }, evidenceLevel: "candidate", extractor: "extension-summary@1", confidence: "heuristic" });
  }
  for (const repository of config.repositories) {
    facts.get("evidence")!.push({ kind: "evidence", repository: repository.name, objectKey: "input-policy", value: { source: "local-static", discoveredCommandsExecuted: false, gitMetadataRead: true, secretsStored: false }, evidenceLevel: "explicit_rule", extractor: "initializer@1", confidence: "exact" });
    facts.get("capability")!.push({ kind: "capability", repository: repository.name, objectKey: "collector:static@1", value: { supported: ["asset", "technology", "api", "page", "data_model", "integration", "rule", "toolchain"], limitations: ["static declarations only", "dynamic behavior unresolved", "no environment enablement inference"], project: context.manifest.name }, evidenceLevel: "explicit_rule", extractor: "initializer@1", confidence: "exact" });
  }
  return facts;
}

async function runProcess(argv: string[], cwd: string): Promise<{ stdout: string; stderr: string }> {
  const child = Bun.spawn(argv, { cwd, stdout: "pipe", stderr: "pipe" });
  const [exitCode, stdout, stderr] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()]);
  if (exitCode !== 0) throw new Error(`${path.basename(argv[1] ?? argv[0])} failed (${exitCode}): ${(stderr || stdout).trim()}`);
  return { stdout: stdout.trim(), stderr: stderr.trim() };
}

async function buildCodeIndex(context: ProjectContext, config: ScanConfigInput): Promise<Record<string, unknown>> {
  const script = path.resolve(import.meta.dir, "../plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py");
  const output = ".planning/code-index";
  const codebases = config.repositories.flatMap(repository => ["--codebase", `${repository.name}:${path.resolve(context.root, repository.relativePath)}:${repository.role}`]);
  await runProcess(["python3", script, "scan", "--root", context.root, "--output", output, ...codebases], context.root);
  await runProcess(["python3", script, "validate", "--index", output], context.root);
  await runProcess(["python3", script, "audit", "--root", context.root, "--index", output, "--skill-root", path.resolve(import.meta.dir, "../plugins/devflow-intel/skills")], context.root);
  const snapshotPath = path.join(context.root, output, "SNAPSHOT.json");
  const bytes = await readFile(snapshotPath); const snapshot = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
  if (!snapshot.generation || !snapshot.immutablePath) throw new Error("Code index did not publish an immutable generation");
  return { path: output, generation: snapshot.generation, immutablePath: snapshot.immutablePath, snapshotHash: digest(bytes), status: "complete" };
}

export async function runProjectInitialization(context: ProjectContext, input: { config?: ScanConfigInput; requestId?: string; documentMode?: "low-tier-model" | "template" | "none"; organizer?: import("./project-documents.ts").LowTierDocumentOrganizer; switchAuthority?: boolean } = {}): Promise<InitializationProgress> {
  const config: ScanConfigInput = input.config ?? { mode: "full", repositories: [{ name: context.manifest.name, relativePath: ".", role: "other" }], requiredKinds: [...FACT_KINDS], adapterVersions: { static: "1" } };
  const store = await InitializationStore.open(context); const diagnostics: string[] = [];
  let activeRun: { id: string; leaseToken: string } | undefined;
  try {
    const workspace = await store.registerWorkspace(); const storedConfig = store.putScanConfig(config);
    const initialScan = await scan(context, config); const files = initialScan.files;
    const manifest = frozenManifest(initialScan);
    const inputSnapshot = store.createInputSnapshot(workspace.id, storedConfig.id, manifest, false);
    const run = store.startRun({ workspaceId: workspace.id, configId: storedConfig.id, inputSnapshotId: inputSnapshot.id, requestId: input.requestId });
    activeRun = run;
    if (run.reused) return {
      runId: run.id,
      snapshotId: store.getRun(run.id)?.publishedSnapshotId as string | undefined,
      diagnostics,
      database: context.controlDatabasePath,
      projectId: context.manifest.projectId,
      workspaceId: context.workspaceId,
      planningRoot: context.planningRoot,
    };
    const grouped = collect(files, context, config); const required = new Set(config.requiredKinds ?? FACT_KINDS);
    for (const repository of config.repositories) for (const kind of FACT_KINDS) {
      const repositoryFacts = grouped.get(kind)!.filter(fact => fact.repository === repository.name);
      const requiredKind = required.has(kind);
      const applicable = kind === "page"
        ? repository.role === "frontend" || repository.role === "other"
        : kind === "api" || kind === "data_model"
          ? repository.role === "backend" || repository.role === "other"
          : true;
      const repositoryDiagnostics = initialScan.diagnostics.filter(item => item.repository === repository.name);
      const hasBlockingScanFailure = repositoryDiagnostics.some(item => item.reason === "unreadable");
      const status = !requiredKind
        ? "skipped"
        : !applicable
          ? "not_applicable"
          : hasBlockingScanFailure
            ? "partial"
            : "complete";
      store.recordCollection(run.id, run.leaseToken, {
        repository: repository.name,
        kind,
        status,
        adapter: "static@1",
        capability: { static: true, applicable, completeAbsenceIsObserved: status === "complete" && repositoryFacts.length === 0, scanDiagnostics: repositoryDiagnostics.length },
        diagnostics: repositoryDiagnostics,
        facts: status === "complete" ? repositoryFacts : [],
      });
    }
    const indexRef = config.mode === "full" ? await buildCodeIndex(context, config) : { status: "skipped", reason: "explicit_fast_mode" };
    const after = await scan(context, config);
    const afterManifest = frozenManifest(after);
    const verified = store.createInputSnapshot(workspace.id, storedConfig.id, afterManifest, false);
    if (verified.fingerprint !== inputSnapshot.fingerprint) throw new Error("Source changed during initialization; refusing to publish mixed input");
    store.confirmInputSnapshot(inputSnapshot.id, inputSnapshot.fingerprint);
    const published = store.publishRun(run.id, run.leaseToken, { expectedRevision: undefined, indexRef });
    if (input.switchAuthority !== false) {
      const authority = store.getAuthority();
      if (authority.authority === "legacy") store.switchAuthority("database", authority.revision);
    }
    const result = store.queryContext({ snapshotId: published.snapshotId, limit: 1 }) as any;
    const documentMode = input.documentMode ?? "template";
    let documents: InitializationProgress["documents"];
    if (documentMode !== "none") {
      try {
        documents = await (await import("./project-documents.ts")).generateProjectDocuments(context, published.snapshotId, { mode: documentMode, organizer: input.organizer });
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        diagnostics.push(`Document generation failed independently: ${message}`);
        documents = [{ targetPath: ".planning/initialization/report.md", status: "failed", error: message }];
      }
    }
    return {
      runId: run.id,
      snapshotId: published.snapshotId,
      completeness: result.snapshot.completeness,
      diagnostics,
      documents,
      database: context.controlDatabasePath,
      projectId: context.manifest.projectId,
      workspaceId: context.workspaceId,
      planningRoot: context.planningRoot,
    };
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    diagnostics.push(message);
    if (activeRun) store.failRun(activeRun.id, activeRun.leaseToken, message);
    throw error;
  } finally { store.close(); }
}
