/**
 * Self-Reflector Phase C — *실행 기반* 검증.
 *
 * Phase A/B는 LLM 텍스트 분석에 의존하므로 "코드가 실제로 컴파일되는가?"
 * 같은 질문엔 한계가 있다. Phase C는 정답: 그냥 *실행해 본다*.
 *
 * 동작:
 *   1. action-tag executor가 반환한 report를 받아 `✅ Created: <path>` /
 *      `✅ Edited: <path>` 항목에서 경로를 추출
 *   2. 파일 확장자별 toolchain 선택:
 *        .py            → `python3 -m py_compile <path>` (`python` on Windows)
 *        .js / .mjs / .cjs → `node --check <path>`
 *        .ts / .tsx     → 프로젝트 단위 `tsc --noEmit` (단일 파일 체크는 의존성 때문에 실패율 높음)
 *        .json          → `JSON.parse` (node)
 *   3. exitCode 0이면 ✅, 아니면 ❌ + 첫 줄 에러 메시지 캡쳐
 *   4. 추가 report 항목으로 결과 반환
 *
 * 안전장치:
 *   - timeout 10초 (절대 멈춰선 안 됨)
 *   - 도구 미설치 / spawn 실패 → 경고 한 줄로 마무리 (블로킹 X)
 *   - 워크스페이스 외부 경로 무시
 *   - 사용자가 `executionVerification=false`면 통째로 skip — 호출자가 가드
 */
import { spawn, spawnSync } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
import { logError, logInfo } from '../../utils';

export type ExecCheckResult = {
    relPath: string;
    ok: boolean;
    /** 도구 없음 / 환경 문제 등 *체크 자체*가 실패 — ok=false로 분류하되 errorLine은 도구 부재 메시지. */
    toolMissing?: boolean;
    /** 실패 시 첫 줄 에러 메시지. ok=true면 비어 있음. */
    errorLine?: string;
    /** 사용한 도구 명령(디버그용). */
    tool: string;
};

/** 한 명령을 spawn, 표준 출력+에러 캡쳐, timeout 후 강제 종료. */
function _runCheck(cmd: string, args: string[], cwd: string, timeoutMs = 10000): Promise<{ code: number; out: string; err: string; spawnFailed?: boolean }> {
    return new Promise((resolve) => {
        let out = '', err = '';
        let settled = false;
        let proc: ReturnType<typeof spawn> | undefined;
        try {
            proc = spawn(cmd, args, { cwd, shell: false, windowsHide: true });
        } catch (e: any) {
            resolve({ code: -1, out: '', err: e?.message ?? String(e), spawnFailed: true });
            return;
        }
        const timer = setTimeout(() => {
            if (settled) return;
            try { proc?.kill('SIGKILL'); } catch { /* noop */ }
            settled = true;
            resolve({ code: -2, out, err: err + '\n[timeout after ' + timeoutMs + 'ms]' });
        }, timeoutMs);
        proc.stdout?.on('data', (b) => { out += b.toString(); });
        proc.stderr?.on('data', (b) => { err += b.toString(); });
        proc.on('error', (e: any) => {
            if (settled) return;
            settled = true;
            clearTimeout(timer);
            resolve({ code: -1, out, err: e?.message ?? String(e), spawnFailed: true });
        });
        proc.on('close', (code) => {
            if (settled) return;
            settled = true;
            clearTimeout(timer);
            resolve({ code: code ?? 0, out, err });
        });
    });
}

function _firstNonEmptyLine(s: string): string {
    return (s || '').split(/\r?\n/).map((x) => x.trim()).find((x) => x.length > 0) ?? '';
}

/**
 * Resolve the Python executable name once and cache it.
 *
 * macOS 12.3+ removed the bare `/usr/bin/python`; it ships `python3` only.
 * Windows installers, conversely, expose `python` (and often no `python3`).
 * So probe the platform-preferred name first, fall back to the other.
 *
 * Returns the platform-preferred default when neither is found so the caller
 * still produces the usual "미설치" warning via the spawn-failure path.
 */
let _pythonCmdCache: string | undefined;
function _resolvePythonCmd(): string {
    if (_pythonCmdCache !== undefined) return _pythonCmdCache;
    const candidates = process.platform === 'win32'
        ? ['python', 'python3']
        : ['python3', 'python'];
    for (const cmd of candidates) {
        try {
            const r = spawnSync(cmd, ['--version'], { stdio: 'ignore', windowsHide: true });
            if (!r.error && (r.status === 0 || r.status === null)) {
                _pythonCmdCache = cmd;
                return cmd;
            }
        } catch { /* try next candidate */ }
    }
    _pythonCmdCache = candidates[0];
    return _pythonCmdCache;
}

/** 확장자별 검사 명령 결정. 지원 안 하는 확장자면 null 반환 (skip). */
function _pickTool(absPath: string, projectRoot: string): { cmd: string; args: string[]; cwd: string; label: string } | null {
    const ext = path.extname(absPath).toLowerCase();
    if (ext === '.py') {
        return { cmd: _resolvePythonCmd(), args: ['-m', 'py_compile', absPath], cwd: projectRoot, label: 'py_compile' };
    }
    if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
        return { cmd: 'node', args: ['--check', absPath], cwd: projectRoot, label: 'node --check' };
    }
    if (ext === '.json') {
        // node -e "JSON.parse(fs.readFileSync(...))"
        return {
            cmd: 'node',
            args: ['-e', `JSON.parse(require('fs').readFileSync(${JSON.stringify(absPath)},'utf8'))`],
            cwd: projectRoot,
            label: 'node JSON.parse',
        };
    }
    if (ext === '.ts' || ext === '.tsx') {
        // 단일 파일 tsc는 의존성 때문에 false-positive가 많아 *프로젝트 단위* noEmit으로 돌린다.
        // 비용은 더 크지만 실제 사용자 환경에서 의미 있는 결과를 낸다.
        const tsconfig = path.join(projectRoot, 'tsconfig.json');
        if (!fs.existsSync(tsconfig)) return null;
        // `npx` is a `.cmd` shim on Windows and won't spawn under shell:false,
        // so invoke the TypeScript compiler's plain-JS bin script via `node`
        // directly — identical behavior on macOS, Linux and Windows. If the
        // project has no local typescript install there is nothing to run.
        const tscBin = path.join(projectRoot, 'node_modules', 'typescript', 'bin', 'tsc');
        if (!fs.existsSync(tscBin)) return null;
        return {
            cmd: 'node', args: [tscBin, '--noEmit', '-p', tsconfig],
            cwd: projectRoot, label: 'tsc --noEmit',
        };
    }
    return null;
}

/**
 * report 한 줄에서 `✅ Created: foo.py` / `✅ Edited: foo.py` 형태의 경로 추출.
 * 이 두 케이스만 의미 있음 (Listed/Reveal 등은 syntax 체크 대상 아님).
 */
function _extractPathFromReportLine(line: string): string | null {
    const m = line.match(/^\s*[✅⚠️]\s*(?:Created|Edited|Updated)\s*:\s*(.+)$/);
    return m ? m[1].trim() : null;
}

/**
 * report 내 모든 file action에 대해 syntax 체크 실행. 추가 report 라인들을 반환.
 * 호출자가 기존 actionReport에 concat 해서 사용자에게 보여주기만 하면 됨.
 *
 * @param report      executeActionTags가 반환한 원본 report
 * @param projectRoot 현재 워크스페이스 루트 (cwd로 사용)
 * @returns 추가 report 라인 (없으면 빈 배열 — 검증 대상 파일 없음)
 */
export async function verifyCreatedFiles(report: string[], projectRoot: string): Promise<string[]> {
    const candidates: string[] = [];
    for (const line of report) {
        const rel = _extractPathFromReportLine(line);
        if (!rel) continue;
        const abs = path.isAbsolute(rel) ? rel : path.join(projectRoot, rel);
        // 워크스페이스 외부 / 존재하지 않는 파일 skip.
        if (!abs.startsWith(projectRoot)) continue;
        if (!fs.existsSync(abs)) continue;
        candidates.push(abs);
    }
    if (candidates.length === 0) return [];

    // TypeScript 프로젝트 단위 체크는 *한 번만* 돌리면 됨 (모든 .ts 파일 커버).
    // 그래서 ts 파일이 여럿이어도 tsc는 한 번만 호출.
    const ranTsForProject = new Set<string>(); // projectRoot 단위로
    const out: string[] = [];

    for (const abs of candidates) {
        const tool = _pickTool(abs, projectRoot);
        const rel = path.relative(projectRoot, abs);
        if (!tool) continue;
        // ts 프로젝트 체크 중복 회피.
        if (tool.label === 'tsc --noEmit') {
            if (ranTsForProject.has(tool.cwd)) continue;
            ranTsForProject.add(tool.cwd);
        }
        const t0 = Date.now();
        const res = await _runCheck(tool.cmd, tool.args, tool.cwd);
        const dur = ((Date.now() - t0) / 1000).toFixed(1);
        if (res.spawnFailed) {
            // 도구 미설치 — warning 한 줄로 마무리, 차단하지 않음.
            out.push(`⚠️ ${tool.label} 미설치 — ${rel} 검증 skip`);
            logInfo('selfReflector.C: tool missing.', { tool: tool.label, path: rel });
            continue;
        }
        if (res.code === 0) {
            out.push(`🔬 ${tool.label} OK: ${rel} (${dur}s)`);
        } else {
            const errLine = _firstNonEmptyLine(res.err || res.out) || `exit ${res.code}`;
            out.push(`❌ ${tool.label} FAIL: ${rel} — ${errLine}`);
            logError('selfReflector.C: syntax check failed.', { path: rel, tool: tool.label, code: res.code, err: errLine });
        }
    }
    return out;
}
