import { Command, Option } from 'commander';
import { readFile, writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import { z } from 'zod';
import { findFiles } from './walker.js';
import { parseFiles } from './parsers/index.js';
import { aggregate } from './stats.js';
import { formatDocumentTable, formatSccTable, formatSummaryLine } from './output/tabular.js';
import { formatJson } from './output/json.js';
import { checkScc, runScc } from './scc.js';
import { createProgress } from './progress.js';
import { documentToMarkdown } from './markdown/convert.js';
import { extractFromMarkdown } from './structure/index.js';
import { formatStructureTree } from './output/tree.js';
import { registerCodeCommands } from './code/command.js';
import { registerCacheCommand } from './code/cache-command.js';
import { registerGitCommands } from './git/command.js';
import { registerSheetCommands } from './sheet/command.js';
import { registerDocCommands } from './doc/command.js';
import { registerSlideCommands } from './slide/command.js';
import { registerTableCommands } from './table/command.js';
import { registerDescribeCommand, registerWorkspaceCommands } from './workspace/command.js';
import { registerSkillsCommands } from './skills/command.js';
import { registerMcpCommand } from './mcp/command.js';
import { compareCodeUnits, formatBytes, formatPayloadJson, getExtension, writeStream } from './utils.js';
import { validateLargeFileLimit } from './cli-validation.js';
import { createTokenizer, resolveTokenizerName } from './tokens.js';
const CliOptionsSchema = z
    .object({
    byFile: z.boolean().optional(),
    format: z.string(),
    includeExt: z.string().optional(),
    excludeExt: z.string().optional(),
    excludeDir: z.string().optional(),
    ignorePattern: z.array(z.string()).optional(),
    gitignore: z.boolean(),
    sort: z.string(),
    output: z.string().optional(),
    ci: z.boolean().optional(),
    largeFileLimit: z.string(),
    code: z.boolean(),
    structure: z.boolean().optional(),
    tokenBudget: z.string().optional(),
    tokenizer: z.string().optional(),
    quick: z.boolean().optional(),
    showConfidence: z.boolean().optional(),
    pretty: z.boolean().optional(),
})
    .passthrough();
function collectOption(value, previous = []) {
    return [...previous, value];
}
/**
 * Resolve the scc binary, degrading to null (documents-only) when scc is not
 * available instead of aborting the whole scan. Returns a warning string when
 * scc could not be found so the caller can surface it without failing.
 */
export async function resolveSccBinary(resolve = checkScc) {
    try {
        return { binary: await resolve(), warning: null };
    }
    catch {
        return {
            binary: null,
            warning: 'scc not found; skipping code analysis (documents only). ' +
                'Run "npm install" to auto-download it, or install from https://github.com/boyter/scc.',
        };
    }
}
/**
 * Translate directory-style ignore patterns into scc --exclude-dir names. scc
 * excludes by directory basename, so only single-segment directory patterns
 * (optionally with a trailing slash, `/**`, or `/*`) can be forwarded; negations,
 * multi-segment paths, and wildcard patterns are left to apply to documents only.
 */
export function deriveSccExcludeDirs(ignorePatterns) {
    const dirs = [];
    for (const raw of ignorePatterns) {
        const pattern = raw.trim();
        if (!pattern || pattern.startsWith('!') || pattern.startsWith('#'))
            continue;
        const stripped = pattern.replace(/^\.?\/+/, '').replace(/\/(\*\*|\*)?$/, '');
        if (!stripped || stripped === '.' || stripped === '..')
            continue;
        if (/[/*?[\]{}]/.test(stripped))
            continue;
        if (!dirs.includes(stripped))
            dirs.push(stripped);
    }
    return dirs;
}
const PackageJsonSchema = z.object({ version: z.string() }).passthrough();
// Find package.json — works from both src/ (dev) and dist/src/ (built)
const __dirname = path.dirname(fileURLToPath(import.meta.url));
async function loadPkg() {
    for (const rel of ['..', '../..']) {
        try {
            return PackageJsonSchema.parse(JSON.parse(await readFile(path.resolve(__dirname, rel, 'package.json'), 'utf8')));
        }
        catch {
            /* try next */
        }
    }
    return { version: '0.0.0' };
}
const pkg = await loadPkg();
export async function run(argv) {
    const program = new Command();
    program
        .name('occ')
        .description('Office Cloc and Count — scc-style summary tables for office documents')
        .version(pkg.version)
        .argument('[directories...]', 'directories to scan', [])
        .option('-f, --by-file', 'show a row per file instead of grouped by type')
        .option('--format <type>', 'output format: tabular or json', 'tabular')
        .option('--include-ext <exts>', 'comma-separated extensions to include')
        .option('--exclude-ext <exts>', 'comma-separated extensions to exclude')
        .option('--exclude-dir <dirs>', 'directories to skip (comma-separated)', 'node_modules,.git')
        .option('--ignore-pattern <pattern>', 'gitignore-style pattern to ignore (directory-name patterns also excluded from code metrics)', collectOption, [])
        .option('--no-gitignore', 'disable .gitignore respect')
        // Note: this is a global option, so it is inherited by every subcommand. It is
        // intentionally not constrained with .choices() because subcommands (e.g. `git churn`)
        // expose their own `--sort` keys; the default scan falls back to `files` for any value
        // it does not recognize (see sortRows in stats.ts).
        .option('--sort <col>', 'sort by: files, name, words, size', 'files')
        .option('-o, --output <file>', 'write output to file')
        .option('--ci', 'ASCII-only output, no colors')
        .option('--large-file-limit <mb>', 'skip files over this size in MB', '50')
        .option('--no-code', 'skip scc code analysis')
        .option('--structure', 'extract and display document structure')
        .option('--token-budget <n>', 'fit the complete emitted output within N tokens')
        .addOption(new Option('--tokenizer <name>', 'token counter for --token-budget: heuristic (default), o200k_base, or cl100k_base').choices(['heuristic', 'o200k_base', 'cl100k_base']))
        .option('--quick', 'quick scan mode: file count and size by extension (skip parsing)')
        .option('--show-confidence', 'show confidence levels for each metric')
        .option('--pretty', 'pretty-print machine JSON output')
        .action(async (directories, opts) => {
        try {
            if (opts.quick) {
                await executeQuick(directories, opts);
            }
            else {
                await execute(directories, opts);
            }
        }
        catch (err) {
            const error = err;
            await writeStream(process.stderr, `Error: ${error.message}\n`);
            process.exit(1);
        }
    });
    registerCodeCommands(program);
    registerCacheCommand(program);
    registerGitCommands(program);
    registerSheetCommands(program);
    registerDocCommands(program);
    registerSlideCommands(program);
    registerTableCommands(program);
    registerDescribeCommand(program);
    registerWorkspaceCommands(program);
    registerSkillsCommands(program);
    registerMcpCommand(program);
    registerDiagnosticsCommands(program);
    await program.parseAsync(argv);
}
function formatHealthText(manifest) {
    const capabilities = Object.entries(manifest.capabilities)
        .filter(([, available]) => available)
        .map(([name]) => name)
        .join(', ');
    const features = Object.entries(manifest.features).sort(([a], [b]) => compareCodeUnits(a, b));
    return [
        `OCC ${manifest.version}: ${manifest.available ? 'available' : 'unavailable'}`,
        `Capabilities: ${capabilities}`,
        `Features (${features.length}):`,
        ...features.map(([name, value]) => `  ${name}${Array.isArray(value) ? ` = ${value.join(', ')}` : ''}`),
    ].join('\n');
}
function formatProbeText(result) {
    const head = result.git.head?.slice(0, 12) ?? '(none)';
    const grammarState = result.grammars.loaded.length === 0 && result.grammars.failed.length === 0
        ? 'not checked (pass --grammars)'
        : `${result.grammars.loaded.length} loaded, ${result.grammars.failed.length} failed`;
    const parserState = result.documentParsers.checked
        ? `${result.documentParsers.formats.filter(format => format.ready).length}/${result.documentParsers.formats.length} ready`
        : 'not checked (pass --document-parsers)';
    const lines = [
        `Repository: ${result.repoRoot}`,
        `Git: ${result.git.available ? 'available' : 'unavailable'}, repository=${result.git.isRepository}, head=${head}, shallow=${result.git.shallow}`,
        `Entity cache: ${result.entityCache.present ? (result.entityCache.fresh ? 'fresh' : 'stale') : 'missing'} at ${result.entityCache.directory || '(disabled)'}`,
        `Grammars: ${grammarState}`,
        `Document parsers: ${parserState}`,
    ];
    if (result.problems.length > 0) {
        lines.push('Problems:', ...result.problems.map(problem => `  ${problem}`));
    }
    else {
        lines.push('Problems: none');
    }
    return lines.join('\n');
}
async function emitDiagnosticsOutput(command, output) {
    const options = CliOptionsSchema.parse(command.optsWithGlobals());
    const rendered = output.endsWith('\n') ? output : `${output}\n`;
    if (options.output)
        await writeFile(options.output, rendered);
    else
        await writeStream(process.stdout, rendered);
}
function registerDiagnosticsCommands(program) {
    const healthCommand = program
        .command('health')
        .description("Print this build's static feature manifest")
        .option('--format <type>', 'output format: tabular or json', 'tabular')
        .option('-o, --output <file>', 'write output to file');
    healthCommand.action(async () => {
        try {
            const options = CliOptionsSchema.parse(healthCommand.optsWithGlobals());
            const { health } = await import('./health.js');
            const result = await health();
            await emitDiagnosticsOutput(healthCommand, options.format === 'json' ? formatPayloadJson(result, options.pretty) : formatHealthText(result));
        }
        catch (error) {
            await writeStream(process.stderr, `Error: ${error.message}\n`);
            process.exitCode = 1;
        }
    });
    const probeCommand = program
        .command('probe [directory]')
        .description('Inspect runtime readiness without building a code or entity index')
        .option('--format <type>', 'output format: tabular or json', 'tabular')
        .option('-o, --output <file>', 'write output to file')
        .option('--cache-dir <path>', 'where the lineage cache lives (defaults outside the repository)')
        .option('--grammars', 'load and verify all vendored tree-sitter grammars')
        .option('--document-parsers', 'load document parser modules to verify readiness');
    probeCommand.action(async (directory) => {
        try {
            const options = CliOptionsSchema.parse(probeCommand.optsWithGlobals());
            const local = z
                .object({
                cacheDir: z.string().optional(),
                grammars: z.boolean().optional(),
                documentParsers: z.boolean().optional(),
            })
                .parse(probeCommand.opts());
            const { probe } = await import('./probe.js');
            const result = await probe({
                repoRoot: directory,
                cacheDir: local.cacheDir,
                // Library callers retain their existing opt-out defaults. The CLI
                // makes both heavyweight probes explicitly opt-in.
                grammars: local.grammars === true,
                documentParsers: local.documentParsers === true,
            });
            await emitDiagnosticsOutput(probeCommand, options.format === 'json' ? formatPayloadJson(result, options.pretty) : formatProbeText(result));
        }
        catch (error) {
            await writeStream(process.stderr, `Error: ${error.message}\n`);
            process.exitCode = 1;
        }
    });
}
async function executeQuick(directories, opts) {
    const excludeDirs = opts.excludeDir ? opts.excludeDir.split(',').map(d => d.trim()) : ['node_modules', '.git'];
    const { files, skipped } = await findFiles(directories, {
        includeExt: opts.includeExt,
        excludeExt: opts.excludeExt,
        excludeDir: excludeDirs,
        ignorePatterns: opts.ignorePattern,
        noGitignore: !opts.gitignore,
        largeFileLimit: validateLargeFileLimit(opts.largeFileLimit),
    });
    // Group by extension
    const byExt = {};
    for (const f of files) {
        const ext = getExtension(f.path);
        if (!byExt[ext])
            byExt[ext] = { count: 0, totalSize: 0 };
        byExt[ext].count++;
        byExt[ext].totalSize += f.size;
    }
    const result = {
        totalFiles: files.length,
        totalSize: files.reduce((sum, f) => sum + f.size, 0),
        skippedFiles: skipped.length,
        byExtension: byExt,
    };
    let output;
    if (opts.format === 'json') {
        output = formatPayloadJson(result, opts.pretty);
    }
    else {
        const lines = ['Quick Scan Results:', ''];
        const sorted = Object.entries(byExt).sort(([, a], [, b]) => b.count - a.count);
        for (const [ext, data] of sorted) {
            lines.push(`  .${ext}: ${data.count} files, ${formatBytes(data.totalSize)}`);
        }
        lines.push('', `Total: ${files.length} files, ${formatBytes(result.totalSize)}`);
        if (skipped.length > 0)
            lines.push(`Skipped: ${skipped.length} file(s)`);
        output = lines.join('\n');
    }
    output = await applyTokenBudget(output.endsWith('\n') ? output : `${output}\n`, opts);
    if (opts.output) {
        await writeFile(opts.output, output);
    }
    else {
        await writeStream(process.stdout, output);
    }
}
export async function applyTokenBudget(output, opts) {
    if (!opts.tokenBudget)
        return output;
    const budget = parseInt(opts.tokenBudget, 10);
    if (isNaN(budget) || budget <= 0)
        return output;
    const tokenizer = await createTokenizer(resolveTokenizerName(opts.tokenizer));
    const currentTokens = tokenizer.count(output);
    if (currentTokens <= budget)
        return output;
    if (opts.format === 'json')
        return fitStructuredJson(output, budget, text => tokenizer.count(text), opts.pretty);
    const marker = `\n[truncated at ${budget} tokens]`;
    const markerTokens = tokenizer.count(marker);
    if (markerTokens > budget) {
        throw new Error(`The truncation marker requires ${markerTokens} tokens, exceeding the ${budget}-token budget; increase --token-budget.`);
    }
    let prefixBudget = budget - markerTokens;
    while (prefixBudget >= 0) {
        const prefix = tokenizer.slice(output, 0, prefixBudget);
        const rendered = `${prefix}${marker}`;
        const measured = tokenizer.count(rendered);
        if (measured <= budget)
            return rendered;
        prefixBudget -= Math.max(1, measured - budget);
    }
    throw new Error(`Unable to fit marked text inside the ${budget}-token budget; increase --token-budget.`);
}
function isJsonObject(value) {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function finiteNumber(value) {
    return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}
function rowPath(value, fallback) {
    if (!isJsonObject(value))
        return fallback;
    for (const key of ['path', 'file', 'relativePath', 'Location', 'Filename', 'name', 'Name', 'type']) {
        if (typeof value[key] === 'string')
            return value[key];
    }
    return fallback;
}
function structuredOmitted(root) {
    const current = isJsonObject(root.omitted) ? root.omitted : {};
    const omitted = {
        files: finiteNumber(current.files) ?? 0,
        codeRows: finiteNumber(current.codeRows) ?? 0,
        structures: finiteNumber(current.structures) ?? 0,
        extensions: finiteNumber(current.extensions) ?? 0,
    };
    root.omitted = omitted;
    return omitted;
}
function structuredCandidates(root, omitted, countTokens) {
    const candidates = [];
    const documents = isJsonObject(root.documents) ? root.documents : undefined;
    const files = Array.isArray(documents?.files) ? documents.files : undefined;
    if (files) {
        for (let index = 0; index < files.length; index++) {
            const row = files[index];
            candidates.push({
                path: rowPath(row, `documents:${index}`),
                tokens: countTokens(JSON.stringify(row)),
                remove: () => {
                    const removed = files.splice(index, 1)[0];
                    omitted.files += isJsonObject(removed) ? (finiteNumber(removed.count) ?? 1) : 1;
                },
            });
        }
    }
    const code = Array.isArray(root.code) ? root.code : undefined;
    if (code) {
        for (let index = 0; index < code.length; index++) {
            const row = code[index];
            candidates.push({
                path: rowPath(row, `code:${index}`),
                tokens: countTokens(JSON.stringify(row)),
                remove: () => {
                    const removed = code.splice(index, 1)[0];
                    omitted.codeRows++;
                    omitted.files += isJsonObject(removed) ? (finiteNumber(removed.Count) ?? 1) : 1;
                },
            });
        }
    }
    const structures = Array.isArray(root.structures) ? root.structures : undefined;
    if (structures) {
        for (let index = 0; index < structures.length; index++) {
            const row = structures[index];
            candidates.push({
                path: rowPath(row, `structures:${index}`),
                tokens: countTokens(JSON.stringify(row)),
                remove: () => {
                    structures.splice(index, 1);
                    omitted.structures++;
                },
            });
        }
    }
    const byExtension = isJsonObject(root.byExtension) ? root.byExtension : undefined;
    if (byExtension) {
        for (const [extension, row] of Object.entries(byExtension)) {
            candidates.push({
                path: `.${extension}`,
                tokens: countTokens(JSON.stringify({ [extension]: row })),
                remove: () => {
                    delete byExtension[extension];
                    omitted.extensions++;
                    omitted.files += isJsonObject(row) ? (finiteNumber(row.count) ?? 1) : 1;
                },
            });
        }
    }
    return candidates;
}
function fitStructuredJson(output, budget, countTokens, pretty = false) {
    let parsed;
    try {
        parsed = JSON.parse(output);
    }
    catch (error) {
        throw new Error(`Cannot apply a structured JSON token budget to invalid JSON: ${error.message}`);
    }
    if (!isJsonObject(parsed)) {
        throw new Error('Structured JSON token budgeting requires an object envelope; use text output or increase the budget.');
    }
    parsed.truncated = true;
    const omitted = structuredOmitted(parsed);
    let rendered = formatPayloadJson(parsed, pretty);
    while (countTokens(rendered) > budget) {
        const candidates = structuredCandidates(parsed, omitted, countTokens).sort((left, right) => right.tokens - left.tokens || compareCodeUnits(left.path, right.path));
        const largest = candidates[0];
        if (!largest) {
            const required = countTokens(rendered);
            throw new Error(`The empty structured JSON envelope requires ${required} tokens, exceeding the ${budget}-token budget; increase --token-budget or use tabular output.`);
        }
        largest.remove();
        rendered = formatPayloadJson(parsed, pretty);
    }
    return rendered;
}
const STRUCTURABLE_EXTS = new Set(['docx', 'pdf', 'pptx', 'odt', 'odp']);
async function extractStructures(files, concurrency, onProgress) {
    const results = [];
    for (let i = 0; i < files.length; i += concurrency) {
        const batch = files.slice(i, i + concurrency);
        const batchResults = await Promise.allSettled(batch.map(async (f) => {
            const markdown = await documentToMarkdown(f.path);
            if (markdown == null)
                return null;
            const structure = extractFromMarkdown(markdown);
            return { file: f.path, structure, markdown };
        }));
        for (let j = 0; j < batchResults.length; j++) {
            const r = batchResults[j];
            if (r.status === 'fulfilled' && r.value) {
                results.push(r.value);
            }
            if (onProgress)
                onProgress(1, batch[j]?.path);
        }
    }
    return results;
}
async function execute(directories, opts) {
    const startTime = Date.now();
    const excludeDirs = opts.excludeDir ? opts.excludeDir.split(',').map(d => d.trim()) : ['node_modules', '.git'];
    const includeCode = opts.code !== false;
    const diagnostics = [];
    let sccBinary = null;
    if (includeCode) {
        const resolved = await resolveSccBinary();
        sccBinary = resolved.binary;
        if (resolved.warning) {
            diagnostics.push({ code: 'scc-unavailable', message: resolved.warning });
            await writeStream(process.stderr, `Warning: ${resolved.warning}\n`);
        }
    }
    // Find and parse office documents
    const { files, skipped } = await findFiles(directories, {
        includeExt: opts.includeExt,
        excludeExt: opts.excludeExt,
        excludeDir: excludeDirs,
        ignorePatterns: opts.ignorePattern,
        noGitignore: !opts.gitignore,
        largeFileLimit: validateLargeFileLimit(opts.largeFileLimit),
    });
    const showProgress = opts.format !== 'json' && process.stderr.isTTY;
    let results = [];
    if (files.length > 0) {
        const progress = createProgress({ total: files.length, label: 'Parsing', enabled: showProgress });
        results = await parseFiles(files, 10, (inc, detail) => progress.update(inc, detail));
        progress.done();
    }
    const stats = aggregate(results, {
        byFile: opts.byFile,
        sort: opts.sort,
        showConfidence: opts.showConfidence,
    });
    let sccData = null;
    if (includeCode && sccBinary) {
        if (showProgress)
            process.stderr.write('\rAnalyzing code with scc...');
        sccData = await runScc(sccBinary, directories, {
            byFile: opts.byFile,
            excludeDir: [...excludeDirs, ...deriveSccExcludeDirs(opts.ignorePattern ?? [])],
            sort: opts.sort,
            ci: opts.ci,
            noGitignore: !opts.gitignore,
        });
        if (showProgress) {
            const cols = process.stderr.columns || 80;
            process.stderr.write('\r' + ' '.repeat(cols) + '\r');
        }
    }
    // Structure extraction
    let structureResults = [];
    if (opts.structure) {
        const structurableFiles = files.filter(f => STRUCTURABLE_EXTS.has(getExtension(f.path)));
        if (structurableFiles.length > 0) {
            const progress = createProgress({
                total: structurableFiles.length,
                label: 'Extracting structure',
                enabled: showProgress,
            });
            structureResults = await extractStructures(structurableFiles, 10, (inc, detail) => progress.update(inc, detail));
            progress.done();
        }
    }
    // Format output
    let output;
    if (opts.format === 'json') {
        output = formatJson(stats, sccData, opts.structure ? structureResults : undefined, {
            skippedFiles: skipped.length,
            diagnostics,
            pretty: opts.pretty,
        });
    }
    else {
        const parts = [];
        if (files.length === 0 && (!sccData || sccData.length === 0)) {
            parts.push('No files found.');
        }
        else {
            if (files.length > 0) {
                parts.push(formatDocumentTable(stats, { ci: opts.ci }));
            }
            if (sccData && sccData.length > 0) {
                parts.push(formatSccTable(sccData, { ci: opts.ci, byFile: opts.byFile }));
            }
            // Structure trees
            if (structureResults.length > 0) {
                for (const sr of structureResults) {
                    parts.push(formatStructureTree(sr, { ci: opts.ci }));
                }
            }
            const elapsed = Date.now() - startTime;
            const summary = formatSummaryLine(stats, sccData, elapsed, { ci: opts.ci });
            if (summary)
                parts.push(summary);
        }
        if (skipped.length > 0) {
            parts.push(`\n${skipped.length} file(s) skipped (use --large-file-limit to adjust)`);
        }
        output = parts.join('\n') + '\n';
    }
    output = await applyTokenBudget(output, opts);
    if (opts.output) {
        await writeFile(opts.output, output);
    }
    else {
        await writeStream(process.stdout, output);
    }
}
//# sourceMappingURL=cli.js.map