import { promises as fs } from "node:fs";
import path from "node:path";
import { resolveStarter } from "./catalog.mjs";

const MAX_FILES = 500;
const MAX_FILE_BYTES = 512 * 1024;
const MAX_TOTAL_BYTES = 8 * 1024 * 1024;
const ignoredDirectories = new Set([".git", ".next", "build", "coverage", "dist", "node_modules"]);
const requiredControls = ["README.md", "STARTER_MANIFEST.json", ".env.example", "EVIDENCE.md", "ACCEPTANCE.md"];

export function isSafeRelativePath(value) {
  return /^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))[A-Za-z0-9._/-]+$/.test(value);
}

function looksLikePlaceholder(value) {
  return /^(?:YOUR_|REPLACE_|CHANGEME|EXAMPLE|FIXTURE|TEST_|\$\{|<|\{)/i.test(value);
}

function findCredentialLiterals(content, relativePath) {
  const findings = [];
  const patterns = [
    ["bearer credential", /Bearer\s+[A-Za-z0-9._~-]{20,}/gi],
    ["production-style key", /(?:sk|mmp)_(?:live|prod)_[A-Za-z0-9_-]{8,}/gi],
    ["private key material", /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g],
  ];
  for (const [kind, pattern] of patterns) if (pattern.test(content)) findings.push(`${relativePath}: ${kind}`);
  const assignment = /^[ \t]*(?:export[ \t]+)?(?:MAPPLS_[A-Z0-9_]*(?:KEY|TOKEN|SECRET)|CLIENT_SECRET|ACCESS_TOKEN|API_KEY)[ \t]*=[ \t]*["']?([^"'\s#]+)["']?/gmi;
  for (const match of content.matchAll(assignment)) if (!looksLikePlaceholder(match[1])) findings.push(`${relativePath}: populated secret-like environment value`);
  const structured = /["']?(?:client_secret|access_token|api_key|mappls_static_key)["']?\s*[:=]\s*["']([^"']{8,})["']/gi;
  for (const match of content.matchAll(structured)) if (!looksLikePlaceholder(match[1])) findings.push(`${relativePath}: populated secret-like configuration value`);
  return findings;
}

async function collectTextFiles(root) {
  const files = [];
  const warnings = [];
  let totalBytes = 0;
  async function visit(directory) {
    for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
      if (entry.isDirectory() && ignoredDirectories.has(entry.name)) continue;
      const absolute = path.join(directory, entry.name);
      const relative = path.relative(root, absolute).split(path.sep).join("/");
      if (entry.isSymbolicLink()) { warnings.push(`${relative}: symbolic link skipped`); continue; }
      if (entry.isDirectory()) { await visit(absolute); continue; }
      if (!entry.isFile()) continue;
      if (files.length >= MAX_FILES) throw new Error("project_file_limit_exceeded");
      const stat = await fs.stat(absolute);
      totalBytes += stat.size;
      if (totalBytes > MAX_TOTAL_BYTES) throw new Error("project_scan_limit_exceeded");
      if (stat.size > MAX_FILE_BYTES) { warnings.push(`${relative}: file exceeds scan limit`); continue; }
      const content = await fs.readFile(absolute, "utf8").catch(() => undefined);
      if (content !== undefined && !content.includes("\u0000")) files.push({ relative, content });
    }
  }
  await visit(root);
  return { files, warnings };
}

export async function inspectStarterProject(projectPath) {
  const root = path.resolve(projectPath);
  const failures = [];
  const warnings = [];
  const passes = [];
  const rootStat = await fs.lstat(root).catch(() => undefined);
  if (!rootStat?.isDirectory() || rootStat.isSymbolicLink()) return { status: "fail", root, starter: null, passes, warnings, failures: ["Project path must be a real directory."] };

  const manifestPath = path.join(root, "STARTER_MANIFEST.json");
  let manifest;
  try { manifest = JSON.parse(await fs.readFile(manifestPath, "utf8")); } catch { failures.push("STARTER_MANIFEST.json is missing or invalid JSON."); }
  const kit = manifest ? resolveStarter(manifest.slug) : undefined;
  if (manifest && manifest.schemaVersion !== "mappls.starter-kit.v1") failures.push("Starter manifest schema is not recognized.");
  if (manifest && !kit) failures.push("Starter slug is not present in the canonical CLI catalog.");
  if (kit && manifest.platform !== kit.platform) failures.push("Starter manifest platform does not match its canonical catalog identity.");
  if (kit && manifest.quickstartSlug !== kit.quickstartSlug) failures.push("Starter manifest quickstart identity does not match the canonical catalog.");
  if (kit && manifest.liveProviderCallsEnabled !== false) warnings.push("Live provider calls are not declared disabled; attach entitlement and release evidence before execution.");
  if (kit) passes.push(`Recognized ${kit.platform} starter ${kit.slug}.`);

  const expectedPaths = kit ? kit.files.map((file) => file.path) : requiredControls;
  for (const relative of expectedPaths) {
    if (!isSafeRelativePath(relative)) { failures.push(`Unsafe canonical path: ${relative}`); continue; }
    const stat = await fs.lstat(path.join(root, ...relative.split("/"))).catch(() => undefined);
    if (!stat?.isFile() || stat.isSymbolicLink()) failures.push(`Missing required regular file: ${relative}`);
  }
  if (!failures.some((failure) => failure.startsWith("Missing required"))) passes.push(`All ${expectedPaths.length} canonical project files are present.`);

  try {
    const scan = await collectTextFiles(root);
    warnings.push(...scan.warnings);
    const credentials = scan.files.flatMap((file) => findCredentialLiterals(file.content, file.relative));
    if (credentials.length) failures.push(...credentials.map((finding) => `Credential hygiene: ${finding}`));
    else passes.push(`No credential literal found across ${scan.files.length} bounded text files.`);
    const workstation = scan.files.flatMap((file) => [...file.content.matchAll(/\/(?:Users|home)\/[A-Za-z0-9._-]+\//g)].map(() => file.relative));
    if (workstation.length) failures.push(...[...new Set(workstation)].map((file) => `Workstation path found in ${file}.`));
    else passes.push("No developer-workstation path found.");
  } catch (error) { failures.push(`Project scan stopped: ${error instanceof Error ? error.message : "unknown_error"}.`); }

  if (kit && kit.readiness !== "public-source") warnings.push(`${kit.readiness}: resolve this evidence boundary before live binding — ${kit.evidenceBoundary}`);
  const status = failures.length ? "fail" : warnings.length ? "warn" : "pass";
  return { status, root, starter: kit ? { slug: kit.slug, platform: kit.platform, readiness: kit.readiness } : null, passes, warnings, failures };
}
