#!/usr/bin/env node
// cli/src/index.ts
import { loadMeta, loadYear, resolveYear, filterPositions } from "./index-loader.js";
import { recommend } from "./recommend.js";
import { matchPositions } from "./match.js";
import { loadMemory, setPrefs, addWatched, logEvent, clearMemory, memoryPath } from "./memory.js";
import { resolveProvince, PROVINCES, EXAM_TYPES } from "./codes.js";
import { isTty, formatPositions, formatRecommend } from "./format.js";
import { loadRatio, buildRatioMap, lookupRatio, availableRatioYears } from "./ratio-loader.js";
import { loadScore, scoreYears, cutoffStats, provinceToSlug, loadScoreIndex, latestYearWith } from "./score-loader.js";
const VERSION = "0.9.1";
const HELP = `
kaogong-pro v${VERSION}
用 Claude Code 规划你的考公选岗。

Usage: kaogong-pro <verb> [flags]

Core:
  search      搜索岗位
                --exam <考试>  guokao/beijing/shengkao  默认全部
                --keyword <text>  职位名称/部门关键词
                --education <学历>  本科/硕士/博士
                --major <专业>  你的专业
                --province <省份>  意向工作地点 (名称/拼音/代码)
                --political <政治面貌>  中共党员/不限
                --dept <部门>  部门名称关键词
                --program <专项>  退役士兵/基层项目  专项定向岗筛选
                --year <年份>  默认最新;历史2020-2026可查
                --limit <N>  返回前 N 条 (默认 50)
                (--keyword 现已搜索备注栏；退役/基层项目限制多写在备注里)

  detail      岗位详情
                --id <职位代码>
                --year <年份>

  recommend   冲/稳/保推荐
                --score <分数>  模考成绩 (行测+申论总分)
                --education <学历>
                --major <专业>
                --province <省份>
                --political <政治面貌>
                --exam-category <考试类别>  综合/行政执法
                --program <专项>  退役士兵/基层项目
                --year <年份>

  compare     对比岗位
                --ids <id1,id2,...>
                --year <年份>

  hot         热门岗位 (有报录比数据时按竞争比, 否则按部门 headcount)
                --year <年份>  --top <N>  [--by ratio|applicants|dept]

  cold        冷门岗位 (招录多、限制严)
                --year <年份>  --top <N>

  match       智能匹配 (基于你的 profile)
                --education/--major/--province/--political/--grassroots
                --keywords <k1,k2>  兴趣关键词
                --program <专项>  退役士兵/基层项目

Data:
  cutoff      进面分数线
                --province <省>  省考逐岗进面分参考(26省) + 分档合格线
                --year <年份>  --keyword <职位/部门>  --id <代码>  --limit <N>
                (不带--province 列出可用省份；国考逐岗进面分尚未入库)
  ratio       报录比 / 竞争比
                --province <省>  省考逐岗报名人数/竞争比参考
                --year <年份>  [--top <N>]  [--keyword]  [--id]  [--by ratio|applicants]
  stats       统计概览
                --year <年份>

State:
  memory list       列出记忆
  memory set        设置偏好 --score 140 --education 本科 --major 计算机
  memory watch      关注岗位 --id <id> --year <year> [--note <text>]
  memory clear      清除记忆

Meta:
  help        帮助
  version     版本
  selftest    自检
  provinces   列出 31 省份
  exams       列出支持的考试类型
`.trim();
function resolveExamFlag(flag) {
    if (!flag)
        return undefined;
    if (flag in EXAM_TYPES)
        return flag;
    return undefined;
}
const PROGRAM_TAGS = ["退役士兵", "基层项目"];
function resolveProgramFlag(flag) {
    if (!flag)
        return undefined;
    return PROGRAM_TAGS.includes(flag) ? flag : undefined;
}
function parseFlags(args) {
    const positional = [];
    const flags = {};
    for (let i = 0; i < args.length; i++) {
        const a = args[i];
        if (a.startsWith("--")) {
            const key = a.slice(2);
            const next = args[i + 1];
            if (next && !next.startsWith("--")) {
                flags[key] = next;
                i++;
            }
            else {
                flags[key] = "true";
            }
        }
        else {
            positional.push(a);
        }
    }
    return { positional, flags };
}
function printJson(value) {
    console.log(JSON.stringify(value, null, 2));
}
function countBy(arr, key) {
    const counts = {};
    for (const item of arr) {
        const val = String(item[key] ?? "unknown");
        counts[val] = (counts[val] ?? 0) + 1;
    }
    return counts;
}
function resolveProvinceFlag(flag) {
    if (!flag)
        return undefined;
    const id = resolveProvince(flag);
    return id ? PROVINCES[id]?.name : flag;
}
const VERBS = {
    help() { console.log(HELP); },
    version() { console.log(VERSION); },
    provinces() {
        printJson(Object.entries(PROVINCES).map(([id, p]) => ({ id, ...p })));
    },
    exams() {
        printJson(Object.entries(EXAM_TYPES).map(([id, name]) => ({ id, name })));
    },
    search(flags) {
        const year = resolveYear(flags.year);
        let results = filterPositions(loadYear(year), {
            year,
            exam: resolveExamFlag(flags.exam),
            education: flags.education,
            major: flags.major,
            political: flags.political,
            province: resolveProvinceFlag(flags.province),
            dept: flags.dept,
            grassroots: flags.grassroots,
            exam_category: flags["exam-category"],
            program: resolveProgramFlag(flags.program),
        });
        if (flags.keyword) {
            // 大小写不敏感（两侧都转小写——中文无影响，修复了英文关键词如 MPA 漏匹配的 bug）。
            // 同时纳入 remarks：退役专项/服务基层项目等"限…报考"限制几乎只写在备注里，
            // 不搜 remarks 会漏掉绝大多数专项岗。
            const kw = flags.keyword.toLowerCase();
            const has = (s) => s.toLowerCase().includes(kw);
            results = results.filter((p) => has(p.dept_name) || has(p.bureau) ||
                has(p.position_name) || has(p.position_desc) || has(p.remarks));
        }
        const limit = Number(flags.limit ?? 50);
        const sliced = results.slice(0, limit);
        if (isTty() && flags.format !== "json") {
            console.log(`共 ${results.length} 个岗位 (显示前 ${sliced.length})\n`);
            console.log(formatPositions(sliced));
        }
        else {
            printJson({ total: results.length, positions: sliced });
        }
    },
    detail(flags) {
        const year = resolveYear(flags.year);
        const id = flags.id;
        if (!id) {
            console.error("--id required");
            process.exitCode = 1;
            return;
        }
        const exam = resolveExamFlag(flags.exam);
        const found = loadYear(year).find((p) => p.id === id && p.year === year && (!exam || p.exam === exam));
        if (!found) {
            printJson({ ok: false, error: `position ${id} not found in ${year}` });
            process.exitCode = 1;
            return;
        }
        printJson(found);
    },
    recommend(flags) {
        const score = Number(flags.score);
        if (!score) {
            console.error("--score required");
            process.exitCode = 1;
            return;
        }
        const result = recommend({
            score,
            exam: resolveExamFlag(flags.exam),
            education: flags.education,
            major: flags.major,
            province: resolveProvinceFlag(flags.province),
            political: flags.political,
            grassroots: flags.grassroots,
            exam_category: flags["exam-category"],
            program: resolveProgramFlag(flags.program),
            year: flags.year ? Number(flags.year) : undefined,
        });
        logEvent("recommend", `score=${score}`);
        if (isTty() && flags.format !== "json") {
            console.log(formatRecommend({ score, evaluated: result.evaluated, skipped: result.buckets.skipped }, result.buckets));
        }
        else {
            printJson(result);
        }
    },
    compare(flags) {
        const year = resolveYear(flags.year);
        const ids = (flags.ids ?? "").split(",").filter(Boolean);
        if (ids.length < 2) {
            console.error("--ids requires at least 2 comma-separated IDs");
            process.exitCode = 1;
            return;
        }
        const yp = loadYear(year);
        const found = ids.map((id) => yp.find((p) => p.id === id && p.year === year)).filter(Boolean);
        printJson(found);
    },
    hot(flags) {
        const year = resolveYear(flags.year);
        const top = Number(flags.top ?? 20);
        const by = flags.by ?? "ratio"; // ratio | applicants | dept
        const ratioFile = loadRatio(year);
        // Explicit dept mode or no ratio data → fallback to dept headcount sort
        if (by === "dept" || !ratioFile) {
            const yearPositions = loadYear(year);
            const deptCounts = new Map();
            for (const p of yearPositions)
                deptCounts.set(p.dept_name, (deptCounts.get(p.dept_name) ?? 0) + 1);
            const sorted = [...deptCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, top);
            printJson({
                mode: "dept",
                ratio_data_available: Boolean(ratioFile),
                year,
                results: sorted.map(([dept, count]) => ({ dept, positions: count })),
            });
            return;
        }
        const list = by === "applicants"
            ? ratioFile.top_positions_by_applicants
            : ratioFile.top_positions_by_ratio;
        printJson({
            mode: by,
            year,
            snapshot_at: ratioFile.snapshot_at,
            national_avg_ratio: ratioFile.national.avg_ratio,
            results: list.slice(0, top),
        });
    },
    cold(flags) {
        const year = resolveYear(flags.year);
        const top = Number(flags.top ?? 20);
        // Cold = 招录多 + 限制严, then de-prioritize positions known to be hot.
        const ratioMap = buildRatioMap(year);
        const hotKeys = new Set(ratioMap.keys());
        const candidates = filterPositions(loadYear(year), { year })
            .filter((p) => p.headcount >= 3 && p.major !== "不限" && p.political !== "不限")
            .filter((p) => {
            // Drop positions that appear in known-hot list.
            const key = `${p.year}|${p.id}|${p.dept_name}|${p.position_name}`;
            return !hotKeys.has(key);
        })
            .sort((a, b) => b.headcount - a.headcount)
            .slice(0, top);
        printJson({
            year,
            ratio_data_available: ratioMap.size > 0,
            results: candidates,
        });
    },
    ratio(flags) {
        // 省考报名人数/竞争比参考库：ratio --province <省> [--year] [--keyword] [--id] [--top N]
        if (flags.province) {
            const slug = provinceToSlug(flags.province);
            if (!slug) {
                printJson({ ok: false, error: `无法识别省份: ${flags.province}` });
                process.exitCode = 1;
                return;
            }
            const sf = loadScore(slug, flags.year ? Number(flags.year) : latestYearWith(slug, "applicants"));
            if (!sf || sf.applicants.length === 0) {
                printJson({ ok: false, error: `${flags.province} 暂无报名人数/竞争比参考数据（可能为登录门控省）`, available_years: scoreYears(slug) });
                process.exitCode = 1;
                return;
            }
            const kw = flags.keyword;
            let hits = sf.applicants;
            if (flags.id)
                hits = hits.filter((a) => a.code === flags.id);
            else if (kw)
                hits = hits.filter((a) => a.dept.includes(kw) || a.name.includes(kw) || a.loc.includes(kw));
            const parseRatio = (r) => { const m = (r || "").match(/[\d.]+/); return m ? Number(m[0]) : 0; };
            const top = Number(flags.top ?? 20);
            const sorted = (flags.id || kw ? hits : [...hits]).sort((a, b) => (b.applicants ?? parseRatio(b.ratio)) - (a.applicants ?? parseRatio(a.ratio)));
            printJson({
                province: sf.province, year: sf.year, available_years: scoreYears(slug),
                total_positions_with_data: sf.applicants.length,
                note: "报名/竞争比为历史参考；部分省为第三方快照或竞争比范围",
                results: sorted.slice(0, top),
                matched: hits.length,
            });
            return;
        }
        const year = resolveYear(flags.year);
        const file = loadRatio(year);
        if (!file) {
            printJson({
                ok: false,
                error: `ratio data for ${year} not available`,
                available_years: availableRatioYears(),
            });
            process.exitCode = 1;
            return;
        }
        // --id lookup — composite key needs dept_name + position_name, so we resolve via index
        if (flags.id) {
            const map = buildRatioMap(year);
            const positions = loadYear(year).filter((p) => p.id === flags.id && p.year === year);
            const matches = positions
                .map((p) => ({
                position: p,
                ratio: lookupRatio(map, year, p.id, p.dept_name, p.position_name),
            }))
                .filter((m) => m.ratio);
            printJson({
                year,
                id: flags.id,
                matches,
                note: matches.length === 0 ? "no public ratio data for this position (only top-10 are published)" : undefined,
            });
            return;
        }
        const top = Number(flags.top ?? 10);
        const by = flags.by ?? "ratio";
        const list = by === "applicants"
            ? file.top_positions_by_applicants
            : file.top_positions_by_ratio;
        printJson({
            year: file.year,
            snapshot_at: file.snapshot_at,
            snapshot_kind: file.snapshot_kind,
            national: file.national,
            top_by: by,
            top: list.slice(0, top),
            notes: file.notes,
            sources: file.sources,
        });
    },
    stats(flags) {
        const year = resolveYear(flags.year);
        const exam = resolveExamFlag(flags.exam);
        const yearPositions = loadYear(year).filter((p) => p.year === year && (!exam || p.exam === exam));
        const totalHeadcount = yearPositions.reduce((s, p) => s + p.headcount, 0);
        printJson({
            year,
            exam: exam ?? "all",
            total_positions: yearPositions.length,
            total_headcount: totalHeadcount,
            by_exam: countBy(yearPositions, "exam"),
            by_inst_type: countBy(yearPositions, "inst_type"),
            by_education: countBy(yearPositions, "education"),
            by_exam_category: countBy(yearPositions, "exam_category"),
        });
    },
    match(flags) {
        const results = matchPositions({
            education: flags.education,
            major: flags.major,
            political: flags.political,
            province: resolveProvinceFlag(flags.province),
            grassroots: flags.grassroots,
            keywords: flags.keywords?.split(","),
            program: resolveProgramFlag(flags.program),
            year: flags.year ? Number(flags.year) : undefined,
        });
        const limit = Number(flags.limit ?? 30);
        printJson({ total: results.length, positions: results.slice(0, limit) });
    },
    cutoff(flags) {
        // 省考进面分参考库：cutoff --province <省> [--year] [--keyword <职位/部门>] [--id <代码>]
        if (flags.province) {
            const slug = provinceToSlug(flags.province);
            if (!slug) {
                printJson({ ok: false, error: `无法识别省份: ${flags.province}` });
                process.exitCode = 1;
                return;
            }
            const year = flags.year ? Number(flags.year) : latestYearWith(slug, "cutoffs");
            const sf = loadScore(slug, year);
            if (!sf || sf.cutoffs.length === 0) {
                printJson({ ok: false, error: `${flags.province} 暂无逐岗进面分参考数据（可能为登录门控省）`, available_years: scoreYears(slug), tier_lines: sf?.tier_lines || undefined });
                process.exitCode = 1;
                return;
            }
            const kw = flags.keyword;
            let hits = sf.cutoffs;
            if (flags.id)
                hits = hits.filter((c) => c.code === flags.id);
            else if (kw)
                hits = hits.filter((c) => c.dept.includes(kw) || c.name.includes(kw) || c.loc.includes(kw));
            const limit = Number(flags.limit ?? (flags.id || kw ? 50 : 20));
            printJson({
                province: sf.province, year: sf.year, available_years: scoreYears(slug),
                scoring_note: sf.scoring_note || undefined,
                tier_lines: sf.tier_lines || undefined,
                cutoff_stats: cutoffStats(sf.cutoffs),
                note: "进面分为历史参考(该年最后进面者分数)，各省计分制不同；详见 tier_lines 分档合格线",
                results: hits.slice(0, limit),
                matched: hits.length,
            });
            return;
        }
        // 无 --province：列出已有进面分参考数据的省份(供发现)
        const idxList = loadScoreIndex().filter((e) => e.cutoffs > 0 || e.has_tier);
        const byProv = new Map();
        for (const e of idxList) {
            const v = byProv.get(e.province) ?? { years: [], cutoffs: 0 };
            v.years.push(e.year);
            v.cutoffs += e.cutoffs;
            byProv.set(e.province, v);
        }
        printJson({
            note: "国考逐岗进面分尚未入库；省考进面分参考用 cutoff --province <省> [--year] [--keyword]",
            省考进面分可用省份: [...byProv.entries()].map(([province, v]) => ({ province, years: v.years.sort(), 进面分行数: v.cutoffs })),
        });
    },
    "memory"(flags, positional) {
        const sub = positional[0];
        if (sub === "list" || !sub) {
            printJson(loadMemory());
        }
        else if (sub === "set") {
            printJson(setPrefs(flags));
        }
        else if (sub === "watch") {
            if (!flags.id || !flags.year) {
                console.error("--id and --year required");
                process.exitCode = 1;
                return;
            }
            const label = flags.label ?? flags.id;
            printJson(addWatched(flags.id, Number(flags.year), label, flags.note));
        }
        else if (sub === "clear") {
            printJson(clearMemory());
        }
        else {
            console.error(`unknown memory subcommand: ${sub}`);
            process.exitCode = 1;
        }
    },
    selftest() {
        const meta = loadMeta();
        const ly = Math.max(...meta.years);
        console.log(`✓ index loaded: ${meta.total_positions} positions, years ${meta.years.join(",")}`);
        console.log(`✓ latest year ${ly}: ${loadYear(ly).length} positions (lazy shard)`);
        console.log(`✓ memory path: ${memoryPath()}`);
        const ratioYears = availableRatioYears();
        if (ratioYears.length > 0) {
            console.log(`✓ ratio data: years ${ratioYears.join(",")}`);
        }
        else {
            console.log(`  (no ratio data found)`);
        }
    },
};
async function main() {
    const args = process.argv.slice(2);
    if (args.length === 0) {
        VERBS.help({}, []);
        return;
    }
    const verb = args[0];
    const { positional, flags } = parseFlags(args.slice(1));
    const fn = VERBS[verb];
    if (!fn) {
        console.error(`unknown verb: ${verb}. Run 'kaogong-pro help' for usage.`);
        process.exitCode = 1;
        return;
    }
    try {
        await fn(flags, positional);
    }
    catch (e) {
        printJson({ ok: false, error: e.message });
        process.exitCode = 1;
    }
}
main();
