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 { 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 { formatBytes, 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(),
})
    .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>', 'truncate output to approximately 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')
        .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);
    registerGitCommands(program);
    registerSheetCommands(program);
    registerDocCommands(program);
    registerSlideCommands(program);
    registerTableCommands(program);
    registerDescribeCommand(program);
    registerWorkspaceCommands(program);
    registerSkillsCommands(program);
    await program.parseAsync(argv);
}
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 = JSON.stringify(result, null, 2);
    }
    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, opts);
    if (opts.output) {
        await writeFile(opts.output, output);
    }
    else {
        await writeStream(process.stdout, output + '\n');
    }
}
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;
    return tokenizer.slice(output, 0, budget);
}
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;
    let sccBinary = null;
    if (includeCode) {
        const resolved = await resolveSccBinary();
        sccBinary = resolved.binary;
        if (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);
    }
    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