#!/usr/bin/env python3
"""Bank batch-239: Rule 197 visual/colour property lexical expansion."""

from __future__ import annotations

import datetime as dt
import json
import re
import sqlite3

from process_ledger import ensure_schema, record_event

DB = "/mnt/donto-data/donto-resources/research/translation-training/kuku-yalanji-runpod-2026-06-30/synthetic/claude-synthetic-v1-2026-07-02/synthetic.db"
LABEL = "batch-239"
NOW = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
PROCESS_LOG = "process_logs/batch-239-visual-colour-property-lexical-expansion-after-rule196-process.md"

TARGET_HEADS = {
    "ngumbu": "black/dark",
    "bingaji": "light-coloured",
    "ngala-ngala": "red",
    "dayirr": "clean/clear",
}
SUBJECT_NOUNS = {
    "kulji": "stone",
    "juku": "tree",
    "ngurrban": "cloud",
    "mungka": "hair",
    "kambi": "clothing",
    "nganka": "flower",
    "karrambil": "king parrot",
    "bana": "water",
    "bubu": "ground",
    "bayan": "house",
}
SUPPORT = {"yala", "yinya", "yaluy", "yinyay"}
DENY_KUKU = {
    "waki", "kambar", "makirr", "makarra", "murbuy", "wuba", "mula-mula", "marun-marun", "jurrbil",
    "warral", "wanka", "jarrmba", "jinbal", "jibarr", "kunji", "dalku", "kubarr", "jarrakulba",
    "balbal", "ngungul-ngungul", "blue-bala", "brown-bala", "karrkay", "kulngkul", "kari",
    "baja", "jarra", "bungan", "bungal", "manya", "manda",
}
DENY_ENGLISH = (
    "green", "blue", "brown", "purple", "yellow", "white", "ochre", "clay", "paint", "shine",
    "shining", "glow", "glowing", "burn", "burnt", "smoke", "storm", "rain", "weather", "country",
    "own", "belong", "feather", "fly", "eat", "food", "tree-use", "timber", "cut", "make", "become",
    "very", "more", "less", "not", "without",
)


def js(values: list[str]) -> str:
    return json.dumps(values, ensure_ascii=False)


def row(seq: int, english: str, kuku: str, analysis: str, frame: str, words: list[str], evidence: str) -> tuple:
    return (seq, english, kuku, analysis, frame, 3, js(words), js([]), evidence, "0.86")


PAIRS = [
    ("stone", "Kulji", "kulji", "ngumbu", "black", "Rule 103 colour table: Kulji ngumbu."),
    ("tree", "Juku", "juku", "ngumbu", "black", "Dictionary juku tree/wood; colour adjective ngumbu."),
    ("cloud", "Ngurrban", "ngurrban", "ngumbu", "dark", "Dictionary ngurrban cloud; no weather-event predicate."),
    ("hair", "Mungka", "mungka", "bingaji", "light-coloured", "Rule 103 colour table: Mungka bingaji."),
    ("clothing", "Kambi", "kambi", "bingaji", "light-coloured", "Dictionary kambi clothing sense; no flying-fox reading."),
    ("flower", "Nganka", "nganka", "ngala-ngala", "red", "Rule 103 colour table: Nganka ngala-ngala."),
    ("king parrot", "Karrambil", "karrambil", "ngala-ngala", "red", "Dictionary karrambil king parrot; no bird-event semantics."),
    ("water", "Bana", "bana", "dayirr", "clear", "Rule 103 property table: Bana dayirr."),
    ("ground", "Bubu", "bubu", "dayirr", "clean", "Dictionary bubu ground/earth; country readings closed."),
    ("house", "Bayan", "bayan", "dayirr", "clean", "Rule 112/154 house-clean frames retained without causative."),
]

ROWS = []
for i, (eng_n, kuku_n_cap, kuku_n, adj, eng_adj, evidence) in enumerate(PAIRS, start=1):
    ROWS.append(row(i, f"This {eng_n} is {eng_adj}.", f"Yala {kuku_n} {adj}.", f"yala {kuku_n} {adj} | this {eng_n} {eng_adj}", f"r197_this_np_visual_{kuku_n}_{adj.replace('-', '_')}", ["yala", kuku_n, adj], evidence))
for i, (eng_n, kuku_n_cap, kuku_n, adj, eng_adj, evidence) in enumerate(PAIRS, start=11):
    ROWS.append(row(i, f"The {eng_adj} {eng_n} is here.", f"{kuku_n_cap} {adj} yaluy.", f"{kuku_n} {adj} yaluy | {eng_n} {eng_adj} here", f"r197_np_visual_here_{kuku_n}_{adj.replace('-', '_')}", [kuku_n, adj, "yaluy"], evidence))
for i, (eng_n, _kuku_n_cap, kuku_n, adj, eng_adj, evidence) in enumerate(PAIRS, start=21):
    ROWS.append(row(i, f"That {eng_adj} {eng_n} is there.", f"Yinya {kuku_n} {adj} yinyay.", f"yinya {kuku_n} {adj} yinyay | that {eng_n} {eng_adj} there", f"r197_that_np_visual_there_{kuku_n}_{adj.replace('-', '_')}", ["yinya", kuku_n, adj, "yinyay"], evidence))
for i, (eng_n, kuku_n_cap, kuku_n, adj, eng_adj, evidence) in enumerate(PAIRS, start=31):
    ROWS.append(row(i, f"The {eng_adj} {eng_n} is there.", f"{kuku_n_cap} {adj} yinyay.", f"{kuku_n} {adj} yinyay | {eng_n} {eng_adj} there", f"r197_np_visual_there_{kuku_n}_{adj.replace('-', '_')}", [kuku_n, adj, "yinyay"], evidence))
for i, (eng_n, _kuku_n_cap, kuku_n, adj, eng_adj, evidence) in enumerate(PAIRS, start=41):
    ROWS.append(row(i, f"This {eng_adj} {eng_n} is here.", f"Yala {kuku_n} {adj} yaluy.", f"yala {kuku_n} {adj} yaluy | this {eng_n} {eng_adj} here", f"r197_this_np_visual_here_{kuku_n}_{adj.replace('-', '_')}", ["yala", kuku_n, adj, "yaluy"], evidence))


def has_token(low: str, token: str) -> bool:
    return re.search(rf"(?<![a-z-]){re.escape(token)}(?![a-z-])", low) is not None


def has_forbidden_target_morphology(low: str, head: str) -> bool:
    suffixes = "ngka|ngku|angka|nga|ngu|ba|mba|mbu|muny|darr|burr|bu|ka|nka|nku|ji|jirr|ku|mundu|amu|mu|iji|nda|ndu|anda|undu|unji|nji|kari|baka|baja|bungal|bungan|mal|man"
    return re.search(rf"(?<![a-z-]){re.escape(head)}-(?:{suffixes})\b", low) is not None


def english_has_denied(english_low: str, denied: str) -> bool:
    return denied in english_low if " " in denied else re.search(rf"\b{re.escape(denied)}\b", english_low) is not None


def scan_static() -> list[str]:
    issues: list[str] = []
    if len(ROWS) != 50:
        issues.append(f"{LABEL}: expected 50 rows, found {len(ROWS)}")
    seen_ku: set[str] = set()
    for expected, row_data in enumerate(ROWS, start=1):
        seq, english, kuku, analysis, frame, _tier, words_used, loans_used, evidence, confidence = row_data
        label = f"{LABEL}.{seq:02d}"
        low = kuku.lower()
        english_low = english.lower()
        words = json.loads(words_used)
        if seq != expected:
            issues.append(f"{label}: sequence mismatch; expected {expected}")
        if not english[:1].isupper() or english[-1:] not in ".!?":
            issues.append(f"{label}: English punctuation/capitalization failure")
        if " | " not in analysis:
            issues.append(f"{label}: analysis missing gloss separator")
        if not frame.startswith("r197_"):
            issues.append(f"{label}: Rule 197 frame must start with r197_")
        if low in seen_ku:
            issues.append(f"{label}: duplicate Kuku surface within batch")
        seen_ku.add(low)
        if json.loads(loans_used):
            issues.append(f"{label}: no loans are admitted in Rule 197")
        for denied in DENY_KUKU:
            if has_token(low, denied):
                issues.append(f"{label}: denied carry-forward token {denied!r}")
        targets = [head for head in TARGET_HEADS if has_token(low, head)]
        if len(targets) != 1:
            issues.append(f"{label}: expected exactly one Rule 197 visual target head, found {targets}")
        for head in targets:
            if head not in words:
                issues.append(f"{label}: target head {head} missing from words_used")
            if has_forbidden_target_morphology(low, head):
                issues.append(f"{label}: target head {head} has forbidden morphology")
        subjects = [head for head in SUBJECT_NOUNS if has_token(low, head)]
        if len(subjects) != 1:
            issues.append(f"{label}: expected exactly one subject noun, found {subjects}")
        for support in SUPPORT:
            if has_token(low, support) and support not in words:
                issues.append(f"{label}: support {support} missing from words_used")
        if 1 <= seq <= 10 and not low.startswith("yala "):
            issues.append(f"{label}: rows 1-10 require yala NP ADJ")
        if 11 <= seq <= 20 and (not low.endswith(" yaluy.") or low.startswith(("yala ", "yinya "))):
            issues.append(f"{label}: rows 11-20 require bare NP ADJ yaluy")
        if 21 <= seq <= 30 and (not low.startswith("yinya ") or not low.endswith(" yinyay.")):
            issues.append(f"{label}: rows 21-30 require yinya NP ADJ yinyay")
        if 31 <= seq <= 40 and (not low.endswith(" yinyay.") or low.startswith(("yala ", "yinya "))):
            issues.append(f"{label}: rows 31-40 require bare NP ADJ yinyay")
        if 41 <= seq <= 50 and (not low.startswith("yala ") or not low.endswith(" yaluy.")):
            issues.append(f"{label}: rows 41-50 require yala NP ADJ yaluy")
        if any(english_has_denied(english_low, bad) for bad in DENY_ENGLISH):
            issues.append(f"{label}: denied colour/polysemy/event field in English")
        if not evidence or not confidence:
            issues.append(f"{label}: missing evidence/confidence")
    return issues


def main() -> int:
    issues = scan_static()
    if issues:
        print("pre-bank scan failed:")
        for issue in issues:
            print(f"  - {issue}")
        return 1

    db = sqlite3.connect(DB)
    db.execute("PRAGMA foreign_keys = ON")
    db.execute("BEGIN")
    ensure_schema(db)
    existing = db.execute("SELECT id FROM batches WHERE label=?", (LABEL,)).fetchone()
    if existing:
        batch_id = existing[0]
        accepted = db.execute("SELECT COUNT(*) FROM sentences WHERE batch_id=? AND status IN ('accepted','revised')", (batch_id,)).fetchone()[0]
        if accepted:
            raise SystemExit(f"{LABEL} already has accepted/revised rows; refusing to overwrite")
        db.execute("DELETE FROM reviews WHERE sentence_id IN (SELECT id FROM sentences WHERE batch_id=?)", (batch_id,))
        db.execute("DELETE FROM sentences WHERE batch_id=?", (batch_id,))
        db.execute("UPDATE batches SET notes=?, created_at=? WHERE id=?", ("rebanked under_review", NOW, batch_id))
    else:
        cur = db.execute(
            "INSERT INTO batches(label, cheatsheet_version, guide_version, notes, created_at) VALUES(?,?,?,?,?)",
            (LABEL, "Rule 197 draft: visual/colour property lexical expansion", "operator-guide-2026-07-08", "under_review", NOW),
        )
        batch_id = cur.lastrowid

    dupes: list[str] = []
    for seq, _english, kuku, *_rest in ROWS:
        hit = db.execute(
            """
            SELECT b.label, s.seq
            FROM sentences s JOIN batches b ON b.id=s.batch_id
            WHERE lower(trim(s.kuku))=lower(trim(?))
              AND s.status!='rejected'
              AND s.batch_id!=?
            """,
            (kuku, batch_id),
        ).fetchone()
        if hit:
            dupes.append(f"{LABEL}.{seq:02d} duplicates {hit[0]}.{hit[1]:02d}: {kuku}")
    if dupes:
        db.rollback()
        print("cross-corpus duplicate check failed:")
        for dupe in dupes:
            print(f"  - {dupe}")
        return 1

    record_event(db, batch_label=LABEL, phase="source", event_type="source-pass", actor="claude-main",
                 detail="Rule 197 source pass selected four visual-property heads: ngumbu black/dark, bingaji light-coloured, ngala-ngala red, and dayirr clean/clear. Patz grammar confirms the colour inventory ngumbu, bingaji, and ngalangala; dictionary/readback confirms ngumbu, bingaji, ngala-ngala, dayirr, and all subject nouns. Waki, kambar, makirr, murbuy, wuba, mula-mula, marun-marun, jurrbil, green/blue/brown/purple/yellow terms, and colour-noun/verb calques stayed closed.",
                 command="operator handoff readback; Patz grammar colour/property lines; dictionary snippets; sqlite exact duplicate preflight",
                 artifact_path=PROCESS_LOG, created_at=NOW)
    record_event(db, batch_label=LABEL, phase="scope", event_type="lexical-scope", actor="claude-main",
                 detail="Rule 197 is a lexical-expansion pass in proven property/deictic frames. It uses yala NP ADJ, bare NP ADJ yaluy, yinya NP ADJ yinyay, bare NP ADJ yinyay, and yala NP ADJ yaluy. It excludes case, degree, comparison, causative, inchoative, plural, target compounds, productive reduplication, privative, material-source, plant-use, body-possession, weather-event, bird-event, and land/country ownership rows.",
                 artifact_path=PROCESS_LOG, created_at=NOW)
    record_event(db, batch_label=LABEL, phase="preflight", event_type="duplicate-preflight", actor="claude-main",
                 detail="Exact duplicate preflight rejected draft Kambi dayirr yinyay because it duplicated Batch 146 row 9; final plan replaced that there-frame with Bubu dayirr yinyay. Final plan had 50 Kuku surfaces, 50 unique within-batch surfaces, and no exact accepted/revised/under_review cross-corpus duplicates.",
                 command="sqlite exact lower(trim(kuku)) duplicate scan before banking", artifact_path="batch_scripts/bank_239.py", created_at=NOW)
    record_event(db, batch_label=LABEL, phase="preflight", event_type="static-guard-pass", actor="claude-main",
                 detail="Static guard passed: exactly one retained visual target and one subject noun per row, no loans, no denied colour/polysemy/event tokens, no target morphology, correct frame distribution, and no denied event/polysemy English fields.",
                 command="python3 batch_scripts/bank_239.py pre-bank scan", artifact_path="batch_scripts/bank_239.py", created_at=NOW)

    db.executemany(
        """
        INSERT INTO sentences(
          batch_id, seq, english, kuku, analysis, frame, tier, words_used,
          loans_used, evidence, status, confidence, author, created_at, updated_at
        )
        VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
        """,
        [(batch_id, seq, english, kuku, analysis, frame, tier, words_used, loans_used, evidence, "under_review", confidence, "claude", NOW, NOW)
         for seq, english, kuku, analysis, frame, tier, words_used, loans_used, evidence, confidence in ROWS],
    )
    db.commit()

    count = db.execute("SELECT COUNT(*) FROM sentences WHERE batch_id=?", (batch_id,)).fetchone()[0]
    under_review = db.execute("SELECT COUNT(*) FROM sentences WHERE batch_id=? AND status='under_review'", (batch_id,)).fetchone()[0]
    verified = db.execute("SELECT COUNT(*) FROM sentences WHERE status IN ('accepted','revised')").fetchone()[0]
    print(f"banked {LABEL}: rows={count}, under_review={under_review}, verified_before_close={verified}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
