"""辞書改善の試行記録 (probe 語句 / A/B 行) を重複なしで累積する。

- probe: 語句リストを furigana lookup --batch で読み、 語句を ledger に登録して結果を出す
- ab:    dict-diff を実行し、 入力行を ledger に登録して diff を出す
- round: 1 ラウンドの成果 (修正数 / 回帰追加数 / commit) を記録
- import: 実行済みの語句 / 行ファイルを読み直さずに登録 (過去分の取り込み)
- stats: 累計 (重複除去後の語句数 / 行数) とラウンド一覧

重複判定は NFKC + 前後空白除去した文字列の sha1。 保存するのは hash だけで、
語句・コメント本文そのものは ledger に残さない。
"""
from __future__ import annotations

import argparse
import datetime as dt
import hashlib
import os
import sqlite3
import subprocess
import sys
import unicodedata
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
WS = ROOT.parent
DB = ROOT / "data" / "probe_ledger.sqlite"
DICT = Path(os.environ.get("FURIGANA_DICT_ROOT", WS / "furigana-dict"))
BIN = Path(os.environ.get("FURIGANA_BIN", WS / "furigana" / "target" / "release" / "furigana.exe"))
DIFF = ROOT / "rust-tools" / "target" / "release" / "dict-diff.exe"
SUBS = ("jukugo", "unihan", "kanji", "loanwords", "works")


def connect() -> sqlite3.Connection:
    DB.parent.mkdir(parents=True, exist_ok=True)
    con = sqlite3.connect(DB)
    con.executescript(
        """
        create table if not exists terms (h blob primary key, first_date text, first_round text);
        create table if not exists lines (h blob primary key, first_date text, first_round text);
        create table if not exists rounds (
            id integer primary key, date text, round text,
            terms_in integer, terms_new integer, lines_in integer, lines_new integer,
            ab_changed integer, fixed integer, cases_added integer, commit_sha text);
        """
    )
    return con


def key(s: str) -> bytes:
    return hashlib.sha1(unicodedata.normalize("NFKC", s).strip().encode("utf-8")).digest()


def register(con, table: str, items: list[str], round_name: str) -> tuple[int, int]:
    today = dt.date.today().isoformat()
    hs = {key(x) for x in items if x.strip()}
    before = con.execute(f"select count(*) from {table}").fetchone()[0]
    con.executemany(
        f"insert or ignore into {table} values (?, ?, ?)", ((h, today, round_name) for h in hs)
    )
    after = con.execute(f"select count(*) from {table}").fetchone()[0]
    return len(hs), after - before


def upsert_round(con, round_name: str, **cols) -> None:
    today = dt.date.today().isoformat()
    row = con.execute(
        "select id from rounds where date=? and round=?", (today, round_name)
    ).fetchone()
    if row is None:
        con.execute(
            "insert into rounds (date, round, terms_in, terms_new, lines_in, lines_new,"
            " ab_changed, fixed, cases_added) values (?, ?, 0, 0, 0, 0, 0, 0, 0)",
            (today, round_name),
        )
        row = con.execute(
            "select id from rounds where date=? and round=?", (today, round_name)
        ).fetchone()
    for c, v in cols.items():
        if c in ("commit_sha",):
            con.execute(f"update rounds set {c}=? where id=?", (v, row[0]))
        else:
            con.execute(f"update rounds set {c}=coalesce({c},0)+? where id=?", (v, row[0]))


def dict_args(root: Path, suffix: str = "") -> list[str]:
    a = [f"--rules-dir{suffix}", str(root / "rules")]
    for s in SUBS:
        a += [f"--core-dict-dir{suffix}", str(root / "core" / s)]
    return a


def cmd_probe(ns) -> None:
    items = [l.rstrip("\n") for l in open(ns.file, encoding="utf-8") if l.strip()]
    out = subprocess.run(
        [str(BIN), "lookup", "--mode", ns.mode, *dict_args(DICT), "--batch"],
        input="\n".join(items) + "\n", capture_output=True, text=True, encoding="utf-8", check=True,
    ).stdout.splitlines()
    for i, o in zip(items, out):
        print(f"{i}\t{o}")
    con = connect()
    n_in, n_new = register(con, "terms", items, ns.round)
    upsert_round(con, ns.round, terms_in=n_in, terms_new=n_new)
    con.commit()
    print(f"[ledger] probe {n_in} 語句 (新規 {n_new})", file=sys.stderr)


def cmd_ab(ns) -> None:
    base = Path(ns.base)
    res = subprocess.run(
        [str(DIFF), *dict_args(base, "-a"), *dict_args(DICT, "-b"), "--mode", ns.mode, *ns.inputs],
        capture_output=True, text=True, encoding="utf-8", check=True,
    )
    sys.stdout.write(res.stdout)
    changed = 0
    for l in res.stdout.splitlines():
        if l.startswith("Changed:"):
            changed = int(l.split()[1])
    lines: list[str] = []
    for f in ns.inputs:
        for l in open(f, encoding="utf-8"):
            lines.append(l.split("\t", 1)[-1])
    con = connect()
    n_in, n_new = register(con, "lines", lines, ns.round)
    upsert_round(con, ns.round, lines_new=n_new)
    # 同じラウンドで A/B を回し直すことがあるので、 入力行数と changed は最後の値で上書きする
    con.execute(
        "update rounds set lines_in=?, ab_changed=? where date=? and round=?",
        (n_in, changed, dt.date.today().isoformat(), ns.round),
    )
    con.commit()
    print(f"[ledger] A/B {n_in} 行 (新規 {n_new})、 変化 {changed}", file=sys.stderr)


def cmd_import(ns) -> None:
    """実行済みの probe 語句 / A/B 入力を、 読み直さずに ledger へ登録する (過去分の取り込み用)。"""
    con = connect()
    items: list[str] = []
    for f in ns.files:
        for l in open(f, encoding="utf-8"):
            items.append(l.split("\t", 1)[-1] if ns.kind == "lines" else l.rstrip("\n"))
    n_in, n_new = register(con, ns.kind, items, ns.round)
    if ns.kind == "terms":
        upsert_round(con, ns.round, terms_in=n_in, terms_new=n_new)
    else:
        upsert_round(con, ns.round, lines_in=n_in, lines_new=n_new)
    con.commit()
    print(f"[ledger] import {ns.kind} {n_in} (新規 {n_new})", file=sys.stderr)


def cmd_round(ns) -> None:
    con = connect()
    upsert_round(con, ns.round, fixed=ns.fixed, cases_added=ns.cases)
    if ns.commit:
        upsert_round(con, ns.round, commit_sha=ns.commit)
    con.commit()
    cmd_stats(ns)


def cmd_stats(_ns) -> None:
    con = connect()
    t = con.execute("select count(*) from terms").fetchone()[0]
    l = con.execute("select count(*) from lines").fetchone()[0]
    f, c, r = con.execute(
        "select coalesce(sum(fixed),0), coalesce(sum(cases_added),0), count(*) from rounds"
    ).fetchone()
    print(f"累計 (重複なし): probe 語句 {t:,} / A/B 行 {l:,} / 修正 {f} / 回帰追加 {c} / ラウンド {r}")
    for row in con.execute(
        "select date, round, terms_new, lines_new, ab_changed, fixed, cases_added, coalesce(commit_sha,'')"
        " from rounds order by id"
    ):
        print("  " + " | ".join(map(str, row)))


def main() -> None:
    for s in (sys.stdout, sys.stderr):
        s.reconfigure(encoding="utf-8")
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    sp = p.add_subparsers(dest="cmd", required=True)
    a = sp.add_parser("probe"); a.add_argument("round"); a.add_argument("file")
    a.add_argument("--mode", default="hiragana"); a.set_defaults(fn=cmd_probe)
    a = sp.add_parser("ab"); a.add_argument("round"); a.add_argument("--base", required=True,
        help="A 側 (変更前) の dict root。 例: git archive HEAD core rules を展開した dir")
    a.add_argument("inputs", nargs="+"); a.add_argument("--mode", default="hiragana"); a.set_defaults(fn=cmd_ab)
    a = sp.add_parser("round"); a.add_argument("round"); a.add_argument("--fixed", type=int, default=0)
    a.add_argument("--cases", type=int, default=0); a.add_argument("--commit"); a.set_defaults(fn=cmd_round)
    a = sp.add_parser("import"); a.add_argument("round"); a.add_argument("kind", choices=["terms", "lines"])
    a.add_argument("files", nargs="+"); a.set_defaults(fn=cmd_import)
    a = sp.add_parser("stats"); a.set_defaults(fn=cmd_stats)
    ns = p.parse_args()
    ns.fn(ns)


if __name__ == "__main__":
    main()
