import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { isAbsolute, join, resolve, sep } from "node:path";
import * as p from "@clack/prompts";
import { colors, symbols } from "./theme.js";
// ── Helpers ─────────────────────────────────────────────────────────────────
const IGNORED_DIRS = new Set([
    "node_modules",
    ".git",
    "dist",
    "build",
    ".next",
    ".nuxt",
    ".output",
    ".vercel",
    ".turbo",
    ".cache",
    "coverage",
    ".harmony-worktrees",
    "__pycache__",
    "target",
    "vendor",
]);
/** Read a JSON file safely, returning null on failure. */
function readJson(filePath) {
    try {
        return JSON.parse(readFileSync(filePath, "utf-8"));
    }
    catch {
        return null;
    }
}
/** Read a text file safely, returning null on failure. */
function readText(filePath) {
    try {
        return readFileSync(filePath, "utf-8");
    }
    catch {
        return null;
    }
}
/** List immediate subdirectories, excluding ignored names. */
function listDirs(dirPath) {
    try {
        return readdirSync(dirPath).filter((entry) => {
            if (IGNORED_DIRS.has(entry) || entry.startsWith("."))
                return false;
            try {
                return statSync(join(dirPath, entry)).isDirectory();
            }
            catch {
                return false;
            }
        });
    }
    catch {
        return [];
    }
}
/** Map of common directory names to short descriptions. */
const DIR_DESCRIPTIONS = {
    components: "UI components",
    pages: "Route-level pages",
    routes: "Route-level pages",
    views: "Route-level pages",
    hooks: "Custom hooks",
    lib: "Utilities",
    utils: "Utilities",
    api: "API / server code",
    server: "API / server code",
    contexts: "State management",
    store: "State management",
    stores: "State management",
    types: "Type definitions",
    styles: "Stylesheets",
    public: "Static assets",
    static: "Static assets",
    assets: "Static assets",
    supabase: "Supabase backend",
    functions: "Edge functions",
    packages: "Monorepo packages",
    apps: "Monorepo applications",
    src: "Source code",
    test: "Tests",
    tests: "Tests",
    __tests__: "Tests",
    scripts: "Build / utility scripts",
    config: "Configuration",
    docs: "Documentation",
    migrations: "Database migrations",
    prisma: "Prisma schema & migrations",
    e2e: "End-to-end tests",
    cypress: "Cypress tests",
};
function describeDir(name) {
    return DIR_DESCRIPTIONS[name.toLowerCase()] ?? name;
}
// ── Core functions ──────────────────────────────────────────────────────────
/**
 * Scan the project directory and return structured metadata.
 * Pure static analysis — no AI, no network calls.
 */
export function scanProject(cwd) {
    // Package manager
    let packageManager = null;
    if (existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb"))) {
        packageManager = "bun";
    }
    else if (existsSync(join(cwd, "pnpm-lock.yaml"))) {
        packageManager = "pnpm";
    }
    else if (existsSync(join(cwd, "yarn.lock"))) {
        packageManager = "yarn";
    }
    else if (existsSync(join(cwd, "package.json"))) {
        packageManager = "npm";
    }
    // Scripts
    const pkg = readJson(join(cwd, "package.json"));
    const scripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null
        ? pkg.scripts
        : {};
    // Language
    let language = "unknown";
    if (existsSync(join(cwd, "tsconfig.json"))) {
        language = "typescript";
    }
    else if (existsSync(join(cwd, "go.mod"))) {
        language = "go";
    }
    else if (existsSync(join(cwd, "Cargo.toml"))) {
        language = "rust";
    }
    else if (existsSync(join(cwd, "setup.py")) ||
        existsSync(join(cwd, "pyproject.toml"))) {
        language = "python";
    }
    else if (pkg) {
        language = "javascript";
    }
    // Framework detection (from package.json dependencies)
    let framework = null;
    if (pkg) {
        const deps = {
            ...(typeof pkg.dependencies === "object"
                ? pkg.dependencies
                : {}),
            ...(typeof pkg.devDependencies === "object"
                ? pkg.devDependencies
                : {}),
        };
        if (deps.next) {
            framework = "next";
        }
        else if (deps.react && deps.vite) {
            framework = "react+vite";
        }
        else if (deps.react) {
            framework = "react";
        }
        else if (deps.vue && deps.vite) {
            framework = "vue+vite";
        }
        else if (deps.vue && deps.nuxt) {
            framework = "nuxt";
        }
        else if (deps.vue) {
            framework = "vue";
        }
        else if (deps.astro) {
            framework = "astro";
        }
        else if (deps.svelte) {
            framework = "svelte";
        }
        else if (deps.express) {
            framework = "express";
        }
        else if (deps.fastify) {
            framework = "fastify";
        }
        else if (deps.hono) {
            framework = "hono";
        }
    }
    // Linter
    let linter = null;
    if (existsSync(join(cwd, "biome.json")) ||
        existsSync(join(cwd, "biome.jsonc"))) {
        linter = "biome";
    }
    else {
        const eslintFiles = [
            ".eslintrc",
            ".eslintrc.js",
            ".eslintrc.cjs",
            ".eslintrc.json",
            ".eslintrc.yml",
            ".eslintrc.yaml",
            "eslint.config.js",
            "eslint.config.mjs",
            "eslint.config.cjs",
            "eslint.config.ts",
        ];
        if (eslintFiles.some((f) => existsSync(join(cwd, f)))) {
            linter = "eslint";
        }
        else {
            const prettierFiles = [
                ".prettierrc",
                ".prettierrc.js",
                ".prettierrc.json",
                ".prettierrc.yml",
                ".prettierrc.yaml",
                "prettier.config.js",
                "prettier.config.mjs",
            ];
            if (prettierFiles.some((f) => existsSync(join(cwd, f)))) {
                linter = "prettier";
            }
        }
    }
    // Indent style
    let indentStyle = null;
    const biome = readJson(join(cwd, "biome.json")) ?? readJson(join(cwd, "biome.jsonc"));
    if (biome) {
        const formatter = biome.formatter;
        if (formatter) {
            const type = formatter.indentStyle === "tab" ? "tab" : "space";
            const width = typeof formatter.indentWidth === "number" ? formatter.indentWidth : 2;
            indentStyle = { type, width };
        }
    }
    if (!indentStyle) {
        const editorConfig = readText(join(cwd, ".editorconfig"));
        if (editorConfig) {
            const styleMatch = editorConfig.match(/indent_style\s*=\s*(space|tab)/);
            const sizeMatch = editorConfig.match(/indent_size\s*=\s*(\d+)/);
            if (styleMatch) {
                indentStyle = {
                    type: styleMatch[1],
                    width: sizeMatch ? Number.parseInt(sizeMatch[1], 10) : 2,
                };
            }
        }
    }
    // Directories
    const dirs = listDirs(cwd);
    const srcDirs = existsSync(join(cwd, "src"))
        ? listDirs(join(cwd, "src"))
        : [];
    // Monorepo
    const monorepo = existsSync(join(cwd, "packages")) || existsSync(join(cwd, "apps"));
    // Existing docs
    const existingDocs = {
        agentsMd: existsSync(join(cwd, "AGENTS.md")),
        claudeMd: existsSync(join(cwd, "CLAUDE.md")),
        docsDir: existsSync(join(cwd, "docs")),
        architectureMd: existsSync(join(cwd, "docs", "architecture.md")),
    };
    return {
        packageManager,
        scripts,
        language,
        framework,
        linter,
        indentStyle,
        dirs,
        srcDirs,
        monorepo,
        existingDocs,
    };
}
// ── Generators ──────────────────────────────────────────────────────────────
/** Build a human-friendly run command prefix. */
function runCmd(pm) {
    if (pm === "bun")
        return "bun run";
    if (pm === "pnpm")
        return "pnpm run";
    if (pm === "yarn")
        return "yarn";
    return "npm run";
}
/** Guess a short description for a package.json script based on its name. */
function describeScript(name) {
    const map = {
        dev: "Dev server",
        start: "Start server",
        build: "Production build",
        lint: "Lint",
        "lint:fix": "Lint + autofix",
        format: "Format code",
        test: "Run tests",
        "test:watch": "Run tests (watch)",
        "test:e2e": "End-to-end tests",
        typecheck: "Type-check",
        "type-check": "Type-check",
        preview: "Preview production build",
        deploy: "Deploy",
        generate: "Code generation",
        migrate: "Run migrations",
        seed: "Seed database",
        clean: "Clean build artifacts",
        prepare: "Prepare (husky, etc.)",
    };
    return map[name] ?? "";
}
/**
 * Generate a scaffold AGENTS.md from project metadata.
 */
export function generateAgentsMd(info, _cwd) {
    const lang = info.language === "typescript"
        ? "TypeScript"
        : info.language === "javascript"
            ? "JavaScript"
            : info.language;
    const frameworkLabel = info.framework ? `${info.framework} ` : "";
    const monoLabel = info.monorepo ? " (monorepo)" : "";
    const lines = [];
    lines.push("# AGENTS.md");
    lines.push("");
    lines.push(`${frameworkLabel}${lang} project${monoLabel}.`);
    lines.push("");
    // Commands
    const scriptEntries = Object.entries(info.scripts);
    if (scriptEntries.length > 0 && info.packageManager) {
        const prefix = runCmd(info.packageManager);
        lines.push("## Commands");
        lines.push("");
        lines.push("```bash");
        // Calculate padding for alignment
        const commands = scriptEntries.map(([name]) => `${prefix} ${name}`);
        const maxLen = Math.max(...commands.map((c) => c.length));
        for (let i = 0; i < scriptEntries.length; i++) {
            const [name] = scriptEntries[i];
            const cmd = commands[i];
            const desc = describeScript(name);
            if (desc) {
                lines.push(`${cmd}${" ".repeat(maxLen - cmd.length + 4)}# ${desc}`);
            }
            else {
                lines.push(cmd);
            }
        }
        lines.push("```");
        lines.push("");
    }
    // Code standards
    lines.push("## Code Standards");
    lines.push("");
    const langLabel = info.language === "typescript" ? "TypeScript" : "JavaScript";
    if (info.language === "typescript" || info.language === "javascript") {
        lines.push(`- ${langLabel} with ES modules`);
    }
    if (info.indentStyle) {
        const unit = info.indentStyle.type === "tab" ? "tab" : "space";
        lines.push(`- ${info.indentStyle.width}-${unit} indentation`);
    }
    if (info.linter) {
        lines.push(`- Linted with ${info.linter}`);
    }
    lines.push("");
    // Architecture
    lines.push("## Architecture");
    lines.push("");
    for (const dir of info.dirs) {
        lines.push(`- \`${dir}/\` — ${describeDir(dir)}`);
    }
    if (info.srcDirs.length > 0) {
        for (const sub of info.srcDirs) {
            lines.push(`  - \`src/${sub}/\` — ${describeDir(sub)}`);
        }
    }
    lines.push("");
    return lines.join("\n");
}
/**
 * Generate a lean CLAUDE.md pointer file.
 */
export function generateClaudeMd(info) {
    const lines = [];
    lines.push("# CLAUDE.md");
    lines.push("");
    lines.push("@AGENTS.md");
    if (info.existingDocs.architectureMd || info.dirs.includes("docs")) {
        lines.push("@docs/architecture.md");
    }
    lines.push("");
    return lines.join("\n");
}
/**
 * Generate an architecture.md scaffold.
 */
export function generateArchitectureMd(info, _cwd) {
    const lines = [];
    lines.push("# Architecture");
    lines.push("");
    lines.push("## Directory Structure");
    lines.push("");
    for (const dir of info.dirs) {
        lines.push(`- \`${dir}/\` — ${describeDir(dir)}`);
    }
    if (info.srcDirs.length > 0) {
        lines.push("");
        lines.push("### `src/`");
        lines.push("");
        for (const sub of info.srcDirs) {
            lines.push(`- \`src/${sub}/\` — ${describeDir(sub)}`);
        }
    }
    lines.push("");
    return lines.join("\n");
}
// ── Verification ────────────────────────────────────────────────────────────
/** Vague phrases that provide no actionable guidance to agents. */
const VAGUE_STANDARDS = [
    "follow best practices",
    "use best practices",
    "keep it clean",
    "write clean code",
    "maintain code quality",
    "ensure quality",
    "use proper naming",
    "follow conventions",
    "be consistent",
];
/**
 * Verify existing docs for broken references, stale commands, dead paths,
 * and structural quality issues from the setup-agent-docs quality checks.
 */
export function verifyDocs(cwd) {
    const issues = [];
    const claudeMd = readText(join(cwd, "CLAUDE.md"));
    const agentsMd = readText(join(cwd, "AGENTS.md"));
    const pkg = readJson(join(cwd, "package.json"));
    const pkgScripts = pkg && typeof pkg.scripts === "object" && pkg.scripts !== null
        ? pkg.scripts
        : {};
    // ── CLAUDE.md checks ──────────────────────────────────────────────────
    const projectRoot = resolve(cwd);
    if (claudeMd) {
        // Check @-references exist on disk (with path traversal protection)
        const importedFiles = [];
        for (const line of claudeMd.split("\n")) {
            const match = line.match(/^@(.+)$/);
            if (match) {
                const refPath = match[1].trim();
                // Reject absolute paths
                if (isAbsolute(refPath)) {
                    issues.push({
                        severity: "error",
                        file: "CLAUDE.md",
                        message: `@ reference uses an absolute path: ${refPath}`,
                        fix: "Use a project-relative path under the repository root",
                    });
                    continue;
                }
                // Resolve and ensure the path stays inside the project root
                const resolvedPath = resolve(projectRoot, refPath);
                if (resolvedPath !== projectRoot &&
                    !resolvedPath.startsWith(projectRoot + sep)) {
                    issues.push({
                        severity: "error",
                        file: "CLAUDE.md",
                        message: `@ reference escapes project root: ${refPath}`,
                        fix: "Remove traversal segments (../) and keep references inside the repo",
                    });
                    continue;
                }
                importedFiles.push({ ref: refPath, resolved: resolvedPath });
                if (!existsSync(resolvedPath)) {
                    issues.push({
                        severity: "error",
                        file: "CLAUDE.md",
                        message: `Referenced file does not exist: ${refPath}`,
                        fix: `Remove the @${refPath} line or create the file`,
                    });
                }
            }
        }
        // Line count — CLAUDE.md should be under 100 lines (lean, @imports only)
        const claudeLines = claudeMd.split("\n").length;
        if (claudeLines > 100) {
            issues.push({
                severity: "warning",
                file: "CLAUDE.md",
                message: `CLAUDE.md is ${claudeLines} lines (recommended: under 100)`,
                fix: "Move detailed content to AGENTS.md or docs/ files and use @imports",
            });
        }
        // Duplication — check if CLAUDE.md duplicates content from imported files
        if (importedFiles.length > 0) {
            const claudeHeadings = extractHeadings(claudeMd);
            for (const { ref: refPath, resolved: resolvedPath } of importedFiles) {
                const refContent = readText(resolvedPath);
                if (!refContent)
                    continue;
                const refHeadings = extractHeadings(refContent);
                // Flag if CLAUDE.md repeats section headings from imported files
                for (const heading of claudeHeadings) {
                    if (refHeadings.has(heading)) {
                        issues.push({
                            severity: "warning",
                            file: "CLAUDE.md",
                            message: `Section "${heading}" duplicates content from @${refPath}`,
                            fix: `Remove the "${heading}" section — it's already included via @import`,
                        });
                    }
                }
            }
        }
    }
    // ── AGENTS.md checks ──────────────────────────────────────────────────
    if (agentsMd) {
        // Project Context — first non-heading, non-blank line should be exactly one line
        const agentsLines = agentsMd.split("\n");
        const contextLines = [];
        let pastFirstHeading = false;
        let hitNextSection = false;
        for (const line of agentsLines) {
            if (!pastFirstHeading) {
                if (line.startsWith("# ")) {
                    pastFirstHeading = true;
                }
                continue;
            }
            // Stop at next ## heading
            if (line.startsWith("## ")) {
                hitNextSection = true;
                break;
            }
            const trimmed = line.trim();
            if (trimmed)
                contextLines.push(trimmed);
        }
        if (pastFirstHeading && !hitNextSection && contextLines.length === 0) {
            issues.push({
                severity: "warning",
                file: "AGENTS.md",
                message: "Missing project context line after the title heading",
                fix: "Add a single-line description: stack + what the project does",
            });
        }
        else if (contextLines.length > 1) {
            issues.push({
                severity: "warning",
                file: "AGENTS.md",
                message: `Project context should be exactly 1 line, found ${contextLines.length}`,
                fix: "Condense to a single line: stack + what the project does",
            });
        }
        // Commands — check against package.json scripts
        const codeBlockRe = /```[\s\S]*?```/g;
        let blockMatch;
        while ((blockMatch = codeBlockRe.exec(agentsMd)) !== null) {
            const block = blockMatch[0];
            const cmdRe = /(?:bun|npm|pnpm|yarn)\s+(?:run\s+)?(\S+)/g;
            let cmdMatch;
            while ((cmdMatch = cmdRe.exec(block)) !== null) {
                const scriptName = cmdMatch[1];
                const builtins = new Set([
                    "install",
                    "init",
                    "create",
                    "exec",
                    "dlx",
                    "x",
                    "test",
                    "start",
                ]);
                if (builtins.has(scriptName))
                    continue;
                if (Object.keys(pkgScripts).length > 0 && !(scriptName in pkgScripts)) {
                    issues.push({
                        severity: "warning",
                        file: "AGENTS.md",
                        message: `Command references script "${scriptName}" which is not in package.json`,
                        fix: `Update the command or add "${scriptName}" to package.json scripts`,
                    });
                }
            }
        }
        // Code Standards — flag vague, non-actionable phrases
        const standardsSection = extractSection(agentsMd, "Code Standards");
        if (standardsSection) {
            const lower = standardsSection.toLowerCase();
            for (const phrase of VAGUE_STANDARDS) {
                if (lower.includes(phrase)) {
                    issues.push({
                        severity: "warning",
                        file: "AGENTS.md",
                        message: `Code Standards contains vague phrase: "${phrase}"`,
                        fix: "Replace with specific, verifiable conventions derived from config files",
                    });
                }
            }
        }
        // Missing test command — if no test script exists, AGENTS.md should say so
        if (Object.keys(pkgScripts).length > 0) {
            const hasTestScript = Object.keys(pkgScripts).some((k) => k === "test" || k.startsWith("test:"));
            if (!hasTestScript) {
                const mentionsNoTest = agentsMd.toLowerCase().includes("no test");
                if (!mentionsNoTest) {
                    issues.push({
                        severity: "warning",
                        file: "AGENTS.md",
                        message: "No test script in package.json and AGENTS.md doesn't mention it",
                        fix: 'Add a note like "No test framework. Verify changes with `bun run build`."',
                    });
                }
            }
        }
        // Check backtick-quoted paths
        checkBacktickPaths(agentsMd, "AGENTS.md", cwd, issues);
    }
    // ── docs/architecture.md checks ───────────────────────────────────────
    const archMd = readText(join(cwd, "docs", "architecture.md"));
    if (archMd) {
        checkBacktickPaths(archMd, "docs/architecture.md", cwd, issues);
    }
    return issues;
}
/** Extract ## headings from markdown content. */
function extractHeadings(content) {
    const headings = new Set();
    for (const line of content.split("\n")) {
        const match = line.match(/^#{2,3}\s+(.+)$/);
        if (match) {
            headings.add(match[1].trim());
        }
    }
    return headings;
}
/** Extract content under a specific ## section heading. */
function extractSection(content, heading) {
    const lines = content.split("\n");
    let capturing = false;
    const result = [];
    for (const line of lines) {
        if (capturing) {
            // Stop at next ## heading
            if (line.match(/^#{1,2}\s/))
                break;
            result.push(line);
        }
        else if (line.match(new RegExp(`^##\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i"))) {
            capturing = true;
        }
    }
    return result.length > 0 ? result.join("\n") : null;
}
/** Scan markdown for backtick-quoted paths and check they exist. */
function checkBacktickPaths(content, file, cwd, issues) {
    const pathRe = /`((?:src\/|packages\/|apps\/|supabase\/|docs\/)[^`]+)`/g;
    let match;
    const checked = new Set();
    const root = resolve(cwd);
    while ((match = pathRe.exec(content)) !== null) {
        const refPath = match[1].replace(/\/$/, ""); // strip trailing slash
        if (checked.has(refPath))
            continue;
        checked.add(refPath);
        // Skip paths that escape the project root (e.g. src/../../etc/hosts)
        const resolvedRef = resolve(root, refPath);
        if (resolvedRef !== root && !resolvedRef.startsWith(root + sep))
            continue;
        if (!existsSync(resolvedRef)) {
            issues.push({
                severity: "warning",
                file,
                message: `Referenced path does not exist: ${refPath}`,
                fix: `Update or remove the \`${refPath}\` reference`,
            });
        }
    }
}
// ── TUI Entry Point ─────────────────────────────────────────────────────────
/**
 * Run the docs step of the setup wizard.
 * Scans the project, then either scaffolds new docs or verifies existing ones.
 */
export async function runDocsStep(cwd) {
    const info = scanProject(cwd);
    const hasDocs = info.existingDocs.agentsMd || info.existingDocs.claudeMd;
    if (!hasDocs) {
        // No docs — offer to generate
        const shouldGenerate = await p.confirm({
            message: "No project docs found. Generate AGENTS.md and CLAUDE.md?",
            initialValue: true,
        });
        if (p.isCancel(shouldGenerate) || !shouldGenerate) {
            return { files: [], issues: [], skipped: true };
        }
        const files = [];
        files.push({
            path: join(cwd, "AGENTS.md"),
            content: generateAgentsMd(info, cwd),
            type: "text",
        });
        files.push({
            path: join(cwd, "CLAUDE.md"),
            content: generateClaudeMd(info),
            type: "text",
        });
        // Generate architecture.md if docs/ exists or we referenced it
        if (info.dirs.includes("docs") || info.srcDirs.length > 0) {
            files.push({
                path: join(cwd, "docs", "architecture.md"),
                content: generateArchitectureMd(info, cwd),
                type: "text",
            });
        }
        p.log.success(`Generated ${files.length} doc file(s): ${files.map((f) => f.path.replace(cwd + "/", "")).join(", ")}`);
        return { files, issues: [], skipped: false };
    }
    // Docs exist — offer to verify
    const shouldVerify = await p.confirm({
        message: "Project docs found. Verify for issues?",
        initialValue: false,
    });
    if (p.isCancel(shouldVerify) || !shouldVerify) {
        return { files: [], issues: [], skipped: true };
    }
    const issues = verifyDocs(cwd);
    if (issues.length === 0) {
        p.log.success("No issues found in project docs.");
    }
    else {
        for (const issue of issues) {
            const prefix = `${colors.bold(issue.file)}:`;
            if (issue.severity === "error") {
                p.log.error(`${prefix} ${issue.message}`);
            }
            else {
                p.log.warning(`${prefix} ${issue.message}`);
            }
            if (issue.fix) {
                p.log.message(`  ${symbols.arrow} ${colors.dim(issue.fix)}`);
            }
        }
        p.log.info(`Found ${issues.length} issue(s) (${issues.filter((i) => i.severity === "error").length} errors, ${issues.filter((i) => i.severity === "warning").length} warnings)`);
    }
    return { files: [], issues, skipped: false };
}
