import { readFile, readdir, writeFile, stat } from "fs/promises";
import { join, relative } from "path";
import ts from "typescript";
import { runShell } from "../util/shell.js";
import { getProjectRoot, pathExists, ensureDir } from "../util/fs.js";
import { style, header, status, label } from "../util/theme.js";
const INDEX_DIR = ".omk/index";
async function detectPackageManager(root) {
    if (await pathExists(join(root, "pnpm-lock.yaml")))
        return "pnpm";
    if (await pathExists(join(root, "yarn.lock")))
        return "yarn";
    if (await pathExists(join(root, "package-lock.json")))
        return "npm";
    if (await pathExists(join(root, "bun.lockb")))
        return "bun";
    if (await pathExists(join(root, "uv.lock")))
        return "uv";
    if (await pathExists(join(root, "poetry.lock")))
        return "poetry";
    if (await pathExists(join(root, "requirements.txt")))
        return "pip";
    return "unknown";
}
async function readPackageScripts(root) {
    const pkgPath = join(root, "package.json");
    if (!(await pathExists(pkgPath)))
        return {};
    try {
        const content = await readFile(pkgPath, "utf-8");
        const pkg = JSON.parse(content);
        return pkg.scripts ?? {};
    }
    catch {
        return {};
    }
}
async function readPackageDeps(root) {
    const pkgPath = join(root, "package.json");
    if (!(await pathExists(pkgPath)))
        return {};
    try {
        const content = await readFile(pkgPath, "utf-8");
        const pkg = JSON.parse(content);
        return {
            deps: pkg.dependencies ? Object.keys(pkg.dependencies) : undefined,
            devDeps: pkg.devDependencies ? Object.keys(pkg.devDependencies) : undefined,
        };
    }
    catch {
        return {};
    }
}
async function getGitIndex(root, includeChanged) {
    const branchResult = await runShell("git", ["branch", "--show-current"], { cwd: root, timeout: 5000 });
    const branch = branchResult.failed ? null : branchResult.stdout.trim() || null;
    const logResult = await runShell("git", ["log", "--oneline", "-10"], { cwd: root, timeout: 5000 });
    const recentCommits = logResult.failed
        ? []
        : logResult.stdout.split("\n").filter((l) => l.trim().length > 0);
    const changedFiles = [];
    const untrackedFiles = [];
    if (includeChanged) {
        const statusResult = await runShell("git", ["status", "--porcelain"], { cwd: root, timeout: 5000 });
        if (!statusResult.failed) {
            const lines = statusResult.stdout.split("\n").filter((l) => l.trim().length > 0);
            for (const line of lines) {
                const code = line.slice(0, 2);
                const file = line.slice(3);
                if (code.includes("?")) {
                    untrackedFiles.push(file);
                }
                else {
                    changedFiles.push(file);
                }
            }
        }
    }
    return { branch, recentCommits, changedFiles, untrackedFiles };
}
const EXCLUDE_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", ".output", "coverage"]);
const TEST_RE = /\.(test|spec)\.(ts|tsx|js|jsx|py|go|rs|java)$/;
const SOURCE_RE = /\.(ts|tsx|js|jsx|py|go|rs|java|rb|php)$/;
async function walkDir(dir, root, opts = {}) {
    const testFiles = [];
    let sourceFiles = 0;
    const maxDepth = opts.maxDepth ?? 4;
    async function recurse(current, depth) {
        if (depth > maxDepth)
            return;
        let entries;
        try {
            entries = await readdir(current);
        }
        catch {
            return;
        }
        for (const entry of entries) {
            if (EXCLUDE_DIRS.has(entry))
                continue;
            const full = join(current, entry);
            let info;
            try {
                info = await stat(full);
            }
            catch {
                continue;
            }
            if (info.isDirectory()) {
                await recurse(full, depth + 1);
            }
            else if (info.isFile()) {
                const rel = relative(root, full);
                if (TEST_RE.test(entry)) {
                    testFiles.push(rel);
                }
                else if (SOURCE_RE.test(entry)) {
                    sourceFiles++;
                }
            }
        }
    }
    await recurse(dir, 0);
    return { testFiles, sourceFiles };
}
async function getFilesIndex(root) {
    const hasTsconfig = await pathExists(join(root, "tsconfig.json"));
    const srcDir = await pathExists(join(root, "src"));
    const testDir = await pathExists(join(root, "test"));
    let testFiles = [];
    let sourceFiles = 0;
    // Walk src/ if it exists
    if (srcDir) {
        const srcResult = await walkDir(join(root, "src"), root, { maxDepth: 5 });
        testFiles = srcResult.testFiles;
        sourceFiles = srcResult.sourceFiles;
    }
    // Walk test/ or tests/ if they exist
    for (const td of ["test", "tests"]) {
        const p = join(root, td);
        if (await pathExists(p)) {
            const r = await walkDir(p, root, { maxDepth: 4 });
            for (const f of r.testFiles) {
                if (!testFiles.includes(f))
                    testFiles.push(f);
            }
        }
    }
    return { hasTsconfig, srcDir, testDir, testFiles, sourceFiles };
}
function hasExportModifier(node) {
    const modifiers = node.modifiers;
    if (!modifiers)
        return false;
    return modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
}
async function collectTsFiles(dir, root) {
    const files = [];
    let entries;
    try {
        entries = await readdir(dir, { withFileTypes: true });
    }
    catch {
        entries = [];
    }
    for (const entry of entries) {
        const full = join(dir, entry.name);
        if (entry.isDirectory()) {
            if (["node_modules", "dist", "build", ".next", ".output", "coverage", ".omk"].includes(entry.name))
                continue;
            files.push(...(await collectTsFiles(full, root)));
        }
        else if (entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) {
            files.push(full);
        }
    }
    return files;
}
export async function buildSymbolIndex(root) {
    const srcDir = join(root, "src");
    const tsFiles = (await pathExists(srcDir)) ? await collectTsFiles(srcDir, root) : [];
    const symbols = [];
    for (const filePath of tsFiles) {
        const content = await readFile(filePath, "utf-8");
        const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
        const visit = (node) => {
            let name;
            let kind = "unknown";
            if (ts.isFunctionDeclaration(node)) {
                kind = "function";
                name = node.name?.text;
            }
            else if (ts.isClassDeclaration(node)) {
                kind = "class";
                name = node.name?.text;
            }
            else if (ts.isInterfaceDeclaration(node)) {
                kind = "interface";
                name = node.name?.text;
            }
            else if (ts.isTypeAliasDeclaration(node)) {
                kind = "type";
                name = node.name?.text;
            }
            else if (ts.isEnumDeclaration(node)) {
                kind = "enum";
                name = node.name?.text;
            }
            else if (ts.isVariableStatement(node)) {
                kind = "variable";
                const decl = node.declarationList.declarations[0];
                if (decl && ts.isIdentifier(decl.name)) {
                    name = decl.name.text;
                }
            }
            if (name) {
                const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
                symbols.push({
                    name,
                    kind,
                    file: relative(root, filePath).replace(/\\/g, "/"),
                    line: line + 1,
                    exported: hasExportModifier(node),
                });
            }
            ts.forEachChild(node, visit);
        };
        ts.forEachChild(sourceFile, visit);
    }
    return { generatedAt: new Date().toISOString(), symbols };
}
async function buildIndex(options) {
    const root = await getProjectRoot();
    const manager = await detectPackageManager(root);
    const scripts = await readPackageScripts(root);
    const { deps, devDeps } = await readPackageDeps(root);
    const git = await getGitIndex(root, Boolean(options.changed));
    const files = await getFilesIndex(root);
    const index = {
        generatedAt: new Date().toISOString(),
        package: { manager, scripts, dependencies: deps, devDependencies: devDeps },
        git,
        files,
    };
    if (options.symbols) {
        index.symbols = await buildSymbolIndex(root);
    }
    return index;
}
async function saveIndex(index, root) {
    const dir = join(root, INDEX_DIR);
    await ensureDir(dir);
    await writeFile(join(dir, "package.json"), JSON.stringify(index.package, null, 2));
    await writeFile(join(dir, "git.json"), JSON.stringify(index.git, null, 2));
    await writeFile(join(dir, "files.json"), JSON.stringify(index.files, null, 2));
    if (index.symbols) {
        await writeFile(join(dir, "symbols.json"), JSON.stringify(index.symbols, null, 2));
    }
    await writeFile(join(dir, "summary.json"), JSON.stringify({
        generatedAt: index.generatedAt,
        packageManager: index.package.manager,
        scriptCount: Object.keys(index.package.scripts).length,
        branch: index.git.branch,
        changedFiles: index.git.changedFiles.length,
        untrackedFiles: index.git.untrackedFiles.length,
        testFiles: index.files.testFiles.length,
        sourceFiles: index.files.sourceFiles,
        hasTsconfig: index.files.hasTsconfig,
        symbolCount: index.symbols?.symbols.length ?? 0,
    }, null, 2));
}
export async function indexCommand(options) {
    const root = await getProjectRoot();
    console.log(header("Project Index"));
    console.log(style.gray("  Scanning..."));
    const index = await buildIndex(options);
    await saveIndex(index, root);
    console.log(status.success("Index saved to .omk/index/"));
    console.log("");
    console.log(label("Package manager", index.package.manager));
    console.log(label("Scripts", String(Object.keys(index.package.scripts).length)));
    console.log(label("Branch", index.git.branch ?? style.gray("(none)")));
    console.log(label("Recent commits", String(index.git.recentCommits.length)));
    console.log(label("Changed files", String(index.git.changedFiles.length)));
    console.log(label("Untracked files", String(index.git.untrackedFiles.length)));
    console.log(label("Test files", String(index.files.testFiles.length)));
    console.log(label("Source files", String(index.files.sourceFiles)));
    console.log(label("tsconfig", index.files.hasTsconfig ? "yes" : "no"));
    if (options.symbols) {
        console.log(label("Symbols", String(index.symbols?.symbols.length ?? 0)));
    }
}
export async function indexShowCommand() {
    const root = await getProjectRoot();
    const summaryPath = join(root, INDEX_DIR, "summary.json");
    if (!(await pathExists(summaryPath))) {
        console.error(status.error("No index found. Run 'omk index' first."));
        process.exit(1);
    }
    const summary = JSON.parse(await readFile(summaryPath, "utf-8"));
    console.log(header("Project Index"));
    console.log("");
    console.log(label("Generated", summary.generatedAt ?? "unknown"));
    console.log(label("Package manager", summary.packageManager));
    console.log(label("Scripts", String(summary.scriptCount)));
    console.log(label("Branch", summary.branch ?? "(none)"));
    console.log(label("Changed files", String(summary.changedFiles)));
    console.log(label("Untracked files", String(summary.untrackedFiles)));
    console.log(label("Test files", String(summary.testFiles)));
    console.log(label("Source files", String(summary.sourceFiles)));
    console.log(label("tsconfig", summary.hasTsconfig ? "yes" : "no"));
}
