#!/usr/bin/env node
'use strict';

const fs = require('node:fs');
const path = require('node:path');
const { discoverCommands, loadCommand } = require('./lib/command-discovery');
const { parseArgs } = require('./lib/arg-parser');
const { generateGeneralHelp, generateCommandHelp } = require('./lib/help-generator');

const DEFAULT_CLI_DIR = path.resolve(__dirname, 'cli');

function getDefaultVersion() {
    try {
        const pkgPath = path.resolve(__dirname, '..', 'package.json');
        const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
        return pkg.version || 'unknown';
    } catch (_err) {
        return 'unknown';
    }
}

function writeLine(stream, text) {
    stream.write(text);
    if (!text.endsWith('\n')) stream.write('\n');
}

function collectAllNames(manifest) {
    const names = [];
    if (manifest && manifest.globalVerbs) {
        for (const verb of Object.keys(manifest.globalVerbs)) {
            names.push(verb);
        }
    }
    if (manifest && manifest.resources) {
        for (const resource of Object.keys(manifest.resources)) {
            names.push(resource);
        }
    }
    return names;
}

function suggestSimilar(unknown, candidates) {
    if (!unknown || typeof unknown !== 'string' || unknown.length === 0) return [];
    if (!Array.isArray(candidates) || candidates.length === 0) return [];
    const u = unknown.toLowerCase();
    const matches = [];
    const seen = new Set();

    for (const raw of candidates) {
        if (typeof raw !== 'string') continue;
        if (raw === unknown || seen.has(raw)) continue;
        const c = raw.toLowerCase();

        if (u.length >= 2 && c.length >= 2 && u.slice(0, 2) === c.slice(0, 2)) {
            matches.push(raw);
            seen.add(raw);
            continue;
        }
        if (u.length >= 3 && c.includes(u)) {
            matches.push(raw);
            seen.add(raw);
            continue;
        }
        if (c.length >= 3 && u.includes(c)) {
            matches.push(raw);
            seen.add(raw);
            continue;
        }
    }

    return matches.slice(0, 3);
}

async function dispatch(argv, options = {}) {
    if (!Array.isArray(argv)) {
        throw new Error('dispatch: argv must be an array');
    }

    const cliDir = options.cliDir || DEFAULT_CLI_DIR;
    const stdout = options.stdout || process.stdout;
    const stderr = options.stderr || process.stderr;
    const version = options.version || getDefaultVersion();

    if (argv.length === 0) {
        const manifest = discoverCommands(cliDir);
        writeLine(stdout, generateGeneralHelp(manifest, loadCommand));
        return 0;
    }

    const first = argv[0];

    if (first === '--help' || first === '-h') {
        const manifest = discoverCommands(cliDir);
        writeLine(stdout, generateGeneralHelp(manifest, loadCommand));
        return 0;
    }

    if (first === '--version' || first === '-v') {
        writeLine(stdout, `@restforgejs/platform v${version}`);
        return 0;
    }

    const manifest = discoverCommands(cliDir);

    let contractFile = null;
    let commandArgv = null;
    let commandLabel = null;

    if (
        manifest.resources &&
        manifest.resources[first] &&
        argv.length >= 2 &&
        manifest.resources[first][argv[1]]
    ) {
        contractFile = manifest.resources[first][argv[1]];
        commandArgv = argv.slice(2);
        commandLabel = `${first} ${argv[1]}`;
    } else if (manifest.globalVerbs && manifest.globalVerbs[first]) {
        contractFile = manifest.globalVerbs[first];
        commandArgv = argv.slice(1);
        commandLabel = first;
    } else {
        writeLine(stderr, `Error: Unknown command: ${first}`);
        const candidates = collectAllNames(manifest);
        const suggestions = suggestSimilar(first, candidates);
        if (suggestions.length > 0) {
            writeLine(stderr, '');
            writeLine(stderr, 'Did you mean:');
            for (const s of suggestions) writeLine(stderr, `  ${s}`);
        }
        writeLine(stderr, '');
        writeLine(stderr, "Run 'npx restforge --help' for available commands.");
        return 2;
    }

    let contract;
    try {
        contract = loadCommand(contractFile);
    } catch (loadErr) {
        writeLine(stderr, `Error: ${loadErr.message}`);
        return 2;
    }

    const parseResult = parseArgs(commandArgv, contract);

    if (parseResult.args && parseResult.args._help === true) {
        writeLine(stdout, generateCommandHelp(contract));
        return 0;
    }

    if (parseResult.errors && parseResult.errors.length > 0) {
        writeLine(stderr, `Invalid usage of ${commandLabel}:`);
        for (const e of parseResult.errors) {
            writeLine(stderr, `  - ${e}`);
        }
        writeLine(stderr, '');
        writeLine(stderr, generateCommandHelp(contract));
        return 2;
    }

    try {
        await contract.handler(parseResult.args);
        return 0;
    } catch (handlerErr) {
        // Handler dapat men-set property `silent: true` untuk men-suppress
        // pesan "Error: ..." (mis. payload validate/diff/sync yang sudah
        // print verdict lengkap dan tidak perlu redundant error message).
        // DEBUG mode tetap tampilkan stack untuk troubleshooting.
        if (!handlerErr || handlerErr.silent !== true) {
            const message = handlerErr && handlerErr.message ? handlerErr.message : String(handlerErr);
            writeLine(stderr, `Error: ${message}`);
        }
        if (process.env.DEBUG && handlerErr && handlerErr.stack) {
            writeLine(stderr, handlerErr.stack);
        }
        // Handler dapat men-set property `exitCode` pada Error untuk override
        // default exit code 1. Dipakai oleh validasi schema di `endpoint create`
        // yang membedakan drift (1) vs usage (2) vs connection (3).
        const customCode = handlerErr && Number.isInteger(handlerErr.exitCode)
            ? handlerErr.exitCode
            : null;
        if (customCode !== null && customCode >= 0 && customCode <= 255) {
            return customCode;
        }
        return 1;
    }
}

async function main() {
    const argv = process.argv.slice(2);
    const code = await dispatch(argv);
    process.exit(code);
}

module.exports = { main, dispatch, suggestSimilar };

if (require.main === module) {
    main().catch((err) => {
        const message = err && err.message ? err.message : String(err);
        process.stderr.write(`Fatal: ${message}\n`);
        if (process.env.DEBUG && err && err.stack) {
            process.stderr.write(err.stack + '\n');
        }
        process.exit(1);
    });
}
