from __future__ import annotations

import argparse
import json
from collections import Counter, defaultdict
from collections.abc import Mapping
from pathlib import Path
from typing import Any

from .formatting import h16
from .lcd_text import analyze_lcd_text
from .rom import Rom
from .serial_semantics import OBSERVED_TX_REPORT_OVERLAY
from .table_xrefs import analyze_table_xrefs


JsonObject = dict[str, Any]

DEFAULT_INPUT = Path("build/rom_decompiled.json")
DEFAULT_ROM = Path("ROM/M27C512@DIP28_1.BIN")
CHECKSUM_SEED = 0x5A
DISPLAY_TERMS = (
    "CONNECT",
    "COMM LINK",
    "COMPLETED",
    "CAM",
    "BARS",
    "BLACK",
    "IRIS",
    "GAIN",
    "SHUTTER",
    "CALL",
    "POWER",
    "AUTO",
    "DIAG",
    "DXC",
)

SPECIAL_SELECTORS: tuple[JsonObject, ...] = (
    {
        "selector": 0x000,
        "name": "connection_or_heartbeat_root_candidate",
        "reason": "idle report selector and CONNECT OK emulator condition both center on selector zero",
        "seed_values": [0x0080, 0x8080],
    },
    {
        "selector": 0x003,
        "name": "default_enabled_bit_candidate",
        "reason": "ROM default table writes E000/E800 selector 0x003 to 0x8000",
        "seed_values": [0x8000],
    },
    {
        "selector": 0x040,
        "name": "default_all_ones_or_status_block_candidate",
        "reason": "ROM default table writes E000/E800 selector 0x040 to 0xFFFF and bench tests repeatedly touched the 0x40 family",
        "seed_values": [0xFFFF, 0x4030],
    },
    {
        "selector": 0x0F6,
        "name": "active_status_bridge_candidate",
        "reason": "loc_48FA tests E1EC bit13 and can enqueue report selector 0x00F6",
        "seed_values": [0x2000],
    },
    {
        "selector": 0x006C,
        "name": "command5_be70_candidate",
        "reason": "continuation command 5 calls BE70 for selector 0x006C",
        "seed_values": [],
    },
    {
        "selector": 0x006D,
        "name": "command5_be70_candidate",
        "reason": "continuation command 5 calls BE70 for selector 0x006D",
        "seed_values": [],
    },
    {
        "selector": 0x006E,
        "name": "command5_be70_candidate",
        "reason": "continuation command 5 calls BE70 for selector 0x006E",
        "seed_values": [],
    },
    {
        "selector": 0x006B,
        "name": "connection_latch_clear_candidate",
        "reason": "when F731.7 is set, command 5 on this selector clears F731.7/F790.7",
        "seed_values": [],
    },
    {
        "selector": 0x0096,
        "name": "connection_latch_clear_candidate",
        "reason": "when F731.7 is set, command 5 on this selector clears F731.7/F790.7",
        "seed_values": [],
    },
    {
        "selector": 0x0097,
        "name": "connection_latch_clear_candidate",
        "reason": "when F731.7 is set, command 5 on this selector clears F731.7/F790.7",
        "seed_values": [],
    },
    {
        "selector": 0x00C6,
        "name": "connection_latch_clear_candidate",
        "reason": "when F731.7 is set, command 5 on this selector clears F731.7/F790.7",
        "seed_values": [],
    },
    {
        "selector": 0x00F8,
        "name": "connection_latch_clear_candidate",
        "reason": "when F731.7 is set, command 5 on this selector clears F731.7/F790.7",
        "seed_values": [],
    },
)


def load_seed_hints_input(path: Path) -> JsonObject:
    with path.open("r", encoding="utf-8") as handle:
        payload = json.load(handle)
    if not isinstance(payload, dict) or "instructions" not in payload:
        raise ValueError(f"{path} does not look like h8536_decompiler JSON output")
    return payload


def analyze_ccu_seed_hints(payload: Mapping[str, Any], *, rom_path: Path | None = DEFAULT_ROM) -> JsonObject:
    table_analysis = analyze_table_xrefs(payload)
    selector_hints = _selector_hints_from_tables(table_analysis)
    _merge_special_selectors(selector_hints)
    _merge_observed_reports(selector_hints)

    dispatch = _dispatch_table_summary(payload, rom_path)
    for entry in dispatch.get("interesting_entries", []):
        selector = int(entry["selector"])
        hint = selector_hints.setdefault(selector, _new_selector_hint(selector))
        hint["score"] += 2
        hint["reasons"].append(f"selector dispatches to {entry['target_label_or_hex']}")
        hint["dispatch_target"] = entry

    display = _display_hint_summary(payload, rom_path)
    seed_plan = _seed_plan(selector_hints)

    candidates = sorted(
        selector_hints.values(),
        key=lambda item: (-int(item["score"]), int(item["selector"])),
    )
    return {
        "kind": "ccu_seed_hints",
        "summary": {
            "candidate_count": len(candidates),
            "core_model": (
                "The RCP likely waits for the CCU to seed mirrored state tables, then uses those "
                "selector values to update LCD text, panel lamps, and report state changes."
            ),
            "confidence": "medium",
        },
        "table_model": _table_model_summary(table_analysis),
        "selector_candidates": candidates[:80],
        "display_text_hints": display,
        "dispatch_table": dispatch,
        "seed_plan": seed_plan,
        "bench_implications": [
            "Do not wait for non-heartbeat reports as the only activation source; the CCU may be expected to push initial table state first.",
            "Use command 0 writes for initial seeding, then command 1 readbacks for verification. Treat command 4/5/6 as continuation-only until a live report proves otherwise.",
            "Selector zero remains the highest-value activation candidate because the emulator reaches CONNECT OK when E000[0]=0x8080 and the selector-zero processing queue runs.",
            "E1EC/selector 0x00F6 is a strong follow-up candidate because loc_48FA tests bit13 there and can enqueue report 0x00F6.",
            "LCD text terms such as CAM/BARS/BLACK/COMM LINK appear in ROM records, but they are not direct serial payload strings; they point to selector-driven display builders.",
        ],
        "caveats": [
            "Selector names are candidates, not confirmed protocol labels.",
            "Static table xrefs prove that firmware reads/writes a selector; they do not prove the external CCU must seed it on boot.",
            "Generated frames are syntactically valid six-byte host frames; bench safety still depends on timing and current RCP state.",
        ],
    }


def format_text_report(analysis: Mapping[str, Any]) -> str:
    summary = analysis["summary"]
    lines = [
        "H8/536 CCU Seed Hint Report",
        "",
        f"Summary: {summary['core_model']}",
        f"Confidence: {summary['confidence']}",
        "",
        "Table Model:",
    ]
    for table in analysis.get("table_model", []):
        lines.append(
            f"- {table['name']}: {table['logical_range_hex']}; accesses={table['access_count']} "
            f"static selectors={', '.join(table.get('static_selectors_hex', [])[:12]) or 'none'}"
        )

    lines.extend(["", "Highest-Value Selector Candidates:"])
    for hint in analysis.get("selector_candidates", [])[:24]:
        lines.append(
            f"- {hint['selector_hex']} {hint['name']}: score={hint['score']} "
            f"tables={', '.join(hint.get('tables', [])) or 'none'}"
        )
        for reason in hint.get("reasons", [])[:4]:
            lines.append(f"  - {reason}")
        frames = hint.get("seed_frames", [])
        if frames:
            frame_text = "; ".join(f"{frame['value_hex']} -> {frame['cmd0_frame']}" for frame in frames[:3])
            lines.append(f"  seed frames: {frame_text}")
        read_frame = hint.get("cmd1_read_frame")
        if read_frame:
            lines.append(f"  readback frame: {read_frame}")

    display = analysis.get("display_text_hints", {})
    lines.extend(["", "Display Text Hints:"])
    for hit in display.get("term_hits", [])[:16]:
        samples = ", ".join(
            f"{sample['address_hex']} {sample['text']!r}"
            for sample in hit.get("samples", [])[:3]
        )
        lines.append(f"- {hit['term']}: {hit['hit_count']} hit(s){f' - {samples}' if samples else ''}")

    dispatch = analysis.get("dispatch_table", {})
    lines.extend(["", "Selector Dispatch Hints:"])
    lines.append(
        f"- table {dispatch.get('table_base_hex', 'unknown')}: "
        f"{dispatch.get('interesting_count', 0)} non-default/interesting entries"
    )
    for entry in dispatch.get("interesting_entries", [])[:16]:
        lines.append(
            f"  - selector {entry['selector_hex']} -> {entry['target_label_or_hex']} "
            f"(dispatch index {entry['dispatch_index_hex']})"
        )

    seed_plan = analysis.get("seed_plan", {})
    lines.extend(["", "Candidate Fake-CCU Seed Plan:"])
    for step in seed_plan.get("steps", []):
        lines.append(f"- {step['name']}: {step['frame']}")
        lines.append(f"  {step['why']}")

    lines.extend(["", "Bench Implications:"])
    for item in analysis.get("bench_implications", []):
        lines.append(f"- {item}")

    lines.extend(["", "Caveats:"])
    for item in analysis.get("caveats", []):
        lines.append(f"- {item}")
    return "\n".join(lines).rstrip() + "\n"


def write_ccu_seed_hints(
    input_path: Path,
    output_path: Path,
    *,
    rom_path: Path | None = DEFAULT_ROM,
    as_json: bool = False,
) -> JsonObject:
    analysis = analyze_ccu_seed_hints(load_seed_hints_input(input_path), rom_path=rom_path)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    if as_json:
        output_path.write_text(json.dumps(analysis, indent=2, sort_keys=True) + "\n", encoding="utf-8")
    else:
        output_path.write_text(format_text_report(analysis), encoding="utf-8")
    return analysis


def main(argv: list[str] | None = None, stdout: Any | None = None) -> int:
    parser = argparse.ArgumentParser(description="Mine ROM hints for CCU-to-RCP state seeding candidates.")
    parser.add_argument("input", nargs="?", type=Path, default=DEFAULT_INPUT)
    parser.add_argument("--rom", type=Path, default=DEFAULT_ROM, help="ROM binary used for LCD text and dispatch-table mining")
    parser.add_argument("--json", action="store_true", help="emit structured JSON instead of readable text")
    parser.add_argument("--out", type=Path, default=None, help="write report to this path")
    args = parser.parse_args(argv)

    stream = stdout
    if stream is None:
        import sys

        stream = sys.stdout

    rom_path = args.rom if args.rom and args.rom.exists() else None
    analysis = analyze_ccu_seed_hints(load_seed_hints_input(args.input), rom_path=rom_path)
    rendered = json.dumps(analysis, indent=2, sort_keys=True) + "\n" if args.json else format_text_report(analysis)
    if args.out:
        args.out.parent.mkdir(parents=True, exist_ok=True)
        args.out.write_text(rendered, encoding="utf-8")
        print(f"wrote {args.out}", file=stream)
    else:
        print(rendered, end="", file=stream)
    return 0


def encode_host_frame(command: int, selector: int, value: int = 0) -> list[int]:
    byte1, byte2 = selector_bytes(selector)
    frame = [command & 0x07, byte1, byte2, (value >> 8) & 0xFF, value & 0xFF]
    frame.append(checksum(frame))
    return frame


def frame_hex(frame: list[int]) -> str:
    return " ".join(f"{byte & 0xFF:02X}" for byte in frame)


def selector_bytes(selector: int) -> tuple[int, int]:
    selector &= 0x01FF
    if selector <= 0x007F:
        return 0x00, selector
    if selector <= 0x017F:
        return 0x01, selector - 0x0080
    return 0x02, selector - 0x0180


def checksum(frame_without_checksum: list[int]) -> int:
    value = CHECKSUM_SEED
    for byte in frame_without_checksum[:5]:
        value ^= byte & 0xFF
    return value & 0xFF


def _selector_hints_from_tables(table_analysis: Mapping[str, Any]) -> dict[int, JsonObject]:
    hints: dict[int, JsonObject] = {}
    for table in table_analysis.get("tables", []):
        if not isinstance(table, Mapping):
            continue
        table_name = str(table.get("name", "unknown_table"))
        element = str(table.get("element_candidate", ""))
        for access in table.get("accesses", []):
            if not isinstance(access, Mapping) or not isinstance(access.get("offset"), int):
                continue
            selector = _selector_from_offset(int(access["offset"]), element)
            if selector is None:
                continue
            hint = hints.setdefault(selector, _new_selector_hint(selector))
            hint["score"] += _access_score(access, table_name)
            if table_name not in hint["tables"]:
                hint["tables"].append(table_name)
            hint["accesses"].append(
                {
                    "address_hex": access.get("instruction_address_hex"),
                    "function": access.get("function_label"),
                    "table": table_name,
                    "access": access.get("access"),
                    "instruction": access.get("instruction"),
                }
            )
            reason = _access_reason(access, table_name)
            if reason not in hint["reasons"]:
                hint["reasons"].append(reason)
    return hints


def _merge_special_selectors(hints: dict[int, JsonObject]) -> None:
    for item in SPECIAL_SELECTORS:
        selector = int(item["selector"])
        hint = hints.setdefault(selector, _new_selector_hint(selector))
        hint["score"] += 5
        hint["name"] = str(item["name"])
        hint["reasons"].append(str(item["reason"]))
        for value in item.get("seed_values", []):
            _add_seed_value(hint, int(value))


def _merge_observed_reports(hints: dict[int, JsonObject]) -> None:
    for report in OBSERVED_TX_REPORT_OVERLAY:
        selector = int(report["logical_index"])
        hint = hints.setdefault(selector, _new_selector_hint(selector))
        hint["score"] += 3
        hint["name"] = str(report["name_candidate"])
        frames = ", ".join(str(frame) for frame in report.get("observed_frames_hex", []))
        hint["reasons"].append(f"observed RCP autonomous report frame(s): {frames}")


def _seed_plan(hints: Mapping[int, JsonObject]) -> JsonObject:
    planned = [
        (0x000, 0x8080, "selector zero active/connect candidate from emulator state search"),
        (0x003, 0x8000, "ROM default state also sets selector 0x003 high bit"),
        (0x040, 0xFFFF, "ROM default all-ones/status candidate touched by bench 0x40 family"),
        (0x0F6, 0x2000, "sets E1EC bit13 candidate used by loc_48FA report bridge"),
    ]
    steps: list[JsonObject] = []
    for selector, value, why in planned:
        frame = frame_hex(encode_host_frame(0x00, selector, value))
        readback = frame_hex(encode_host_frame(0x01, selector, 0))
        hint = hints.get(selector)
        if hint is not None:
            _add_seed_value(hint, value)
        steps.append(
            {
                "selector": selector,
                "selector_hex": f"0x{selector:03X}",
                "value": value,
                "value_hex": f"0x{value:04X}",
                "name": f"cmd0 seed selector 0x{selector:03X} = 0x{value:04X}",
                "frame": frame,
                "readback_frame": readback,
                "why": why,
            }
        )
    return {
        "model": "candidate initial CCU state push using command 0 writes, verified with command 1 reads",
        "steps": steps,
    }


def _table_model_summary(table_analysis: Mapping[str, Any]) -> list[JsonObject]:
    rows: list[JsonObject] = []
    for table in table_analysis.get("tables", []):
        if not isinstance(table, Mapping):
            continue
        element = str(table.get("element_candidate", ""))
        selectors = []
        for offset in table.get("static_offsets", []):
            if isinstance(offset, int):
                selector = _selector_from_offset(offset, element)
                if selector is not None:
                    selectors.append(selector)
        rows.append(
            {
                "name": table.get("name"),
                "logical_range_hex": f"{table.get('logical_base_address_hex')}-{table.get('logical_range_end_hex')}",
                "access_count": table.get("access_count", 0),
                "static_selectors": sorted(set(selectors)),
                "static_selectors_hex": [f"0x{selector:03X}" for selector in sorted(set(selectors))],
            }
        )
    return rows


def _display_hint_summary(payload: Mapping[str, Any], rom_path: Path | None) -> JsonObject:
    del payload
    if rom_path is None or not rom_path.exists():
        return {"term_hits": [], "note": "ROM binary was not available for LCD text mining."}
    rom = Rom(rom_path.read_bytes())
    text = analyze_lcd_text(rom, None, search_terms=DISPLAY_TERMS, max_candidates=360)
    term_hits = []
    for search in text.get("searches", []):
        if not isinstance(search, Mapping):
            continue
        samples = []
        for address in search.get("literal_hits", [])[:4]:
            if isinstance(address, int):
                samples.append({"address_hex": h16(address), "text": f"literal {search.get('term')}"})
        for hit in search.get("candidate_hits", [])[:6]:
            if not isinstance(hit, Mapping):
                continue
            samples.append(
                {
                    "address_hex": h16(int(hit.get("address", 0))),
                    "text": str(hit.get("trimmed") or hit.get("text") or ""),
                }
            )
        hit_count = len(search.get("literal_hits", []) or []) + len(search.get("candidate_hits", []) or [])
        term_hits.append({"term": search.get("term"), "hit_count": hit_count, "samples": samples[:8]})
    return {
        "term_hits": term_hits,
        "regions": text.get("regions", [])[:8],
        "note": "Text hits are ROM display resources, not literal serial payloads.",
    }


def _dispatch_table_summary(payload: Mapping[str, Any], rom_path: Path | None) -> JsonObject:
    table_base = 0x28A6
    entries = _indirect_entries(payload, table_base)
    if not entries and rom_path is not None and rom_path.exists():
        entries = _raw_dispatch_entries(rom_path, table_base, 128)
    if not entries:
        return {"table_base": table_base, "table_base_hex": h16(table_base), "interesting_count": 0, "interesting_entries": []}

    target_counts = Counter(int(entry["target"]) for entry in entries if isinstance(entry.get("target"), int))
    default_target, _ = target_counts.most_common(1)[0]
    interesting: list[JsonObject] = []
    for entry in entries:
        index = int(entry["index"])
        selector = _selector_from_dispatch_index(index)
        if selector is None:
            continue
        target = int(entry["target"])
        label = entry.get("target_label") or h16(target)
        decoded = bool(entry.get("decoded_code", True))
        if target == default_target and decoded:
            continue
        interesting.append(
            {
                "selector": selector,
                "selector_hex": f"0x{selector:03X}",
                "dispatch_index": index,
                "dispatch_index_hex": f"0x{index:03X}",
                "entry_address_hex": h16(int(entry.get("entry_address", table_base + index * 2))),
                "target": target,
                "target_hex": h16(target),
                "target_label_or_hex": str(label),
                "decoded_code": decoded,
            }
        )
    return {
        "table_base": table_base,
        "table_base_hex": h16(table_base),
        "entry_count": len(entries),
        "default_target_hex": h16(default_target),
        "interesting_count": len(interesting),
        "interesting_entries": interesting[:80],
    }


def _indirect_entries(payload: Mapping[str, Any], table_base: int) -> list[JsonObject]:
    indirect = payload.get("indirect_flow")
    if not isinstance(indirect, Mapping):
        return []
    for site in indirect.get("sites", []):
        if not isinstance(site, Mapping):
            continue
        table = site.get("table")
        if isinstance(table, Mapping) and int(table.get("base", -1)) == table_base:
            entries = table.get("entries", [])
            if isinstance(entries, list):
                return [dict(entry) for entry in entries if isinstance(entry, Mapping)]
    return []


def _raw_dispatch_entries(rom_path: Path, table_base: int, count: int) -> list[JsonObject]:
    rom = Rom(rom_path.read_bytes())
    entries = []
    for index in range(count):
        address = table_base + index * 2
        if not rom.contains(address, 2):
            break
        target = rom.u16(address)
        entries.append({"index": index, "entry_address": address, "target": target, "target_label": None, "decoded_code": True})
    return entries


def _selector_from_dispatch_index(index: int) -> int | None:
    if 0 <= index <= 0x007F:
        return index
    if 0x0100 <= index <= 0x01FF:
        return index - 0x0080
    if 0x0200 <= index <= 0x027F:
        return index - 0x0080
    return None


def _selector_from_offset(offset: int, element: str) -> int | None:
    if element == "word_value":
        if offset % 2:
            return None
        return (offset // 2) & 0x01FF
    return offset & 0x01FF


def _new_selector_hint(selector: int) -> JsonObject:
    return {
        "selector": selector,
        "selector_hex": f"0x{selector:03X}",
        "name": "state_selector_candidate",
        "score": 0,
        "tables": [],
        "reasons": [],
        "accesses": [],
        "seed_frames": [],
        "cmd1_read_frame": frame_hex(encode_host_frame(0x01, selector, 0)),
    }


def _add_seed_value(hint: JsonObject, value: int) -> None:
    frames = hint.setdefault("seed_frames", [])
    value_hex = f"0x{value & 0xFFFF:04X}"
    if any(frame.get("value_hex") == value_hex for frame in frames):
        return
    frames.append(
        {
            "value": value & 0xFFFF,
            "value_hex": value_hex,
            "cmd0_frame": frame_hex(encode_host_frame(0x00, int(hint["selector"]), value)),
        }
    )


def _access_score(access: Mapping[str, Any], table_name: str) -> int:
    score = 1
    if access.get("access") == "read":
        score += 1
    if access.get("access") == "write":
        score += 1
    if "primary" in table_name or "current" in table_name:
        score += 1
    return score


def _access_reason(access: Mapping[str, Any], table_name: str) -> str:
    function = access.get("function_label") or "<no function>"
    instruction = access.get("instruction") or ""
    return f"{table_name} {access.get('access')} in {function}: {instruction}"


__all__ = [
    "analyze_ccu_seed_hints",
    "checksum",
    "encode_host_frame",
    "format_text_report",
    "frame_hex",
    "load_seed_hints_input",
    "main",
    "selector_bytes",
    "write_ccu_seed_hints",
]
