from __future__ import annotations

import argparse
import json
import re
from pathlib import Path
from typing import Any

from .formatting import h16, label_for


JsonObject = dict[str, Any]

KEY_STATE_ADDRESSES: tuple[int, ...] = (
    0xF9B0,
    0xF9B4,
    0xF9B5,
    0xF9B9,
    0xF9C0,
    0xF9C1,
    0xF9C3,
    0xF9C4,
    0xF9C5,
    0xF9C6,
    0xF9C8,
    0xFAA2,
    0xFAA3,
    0xFAA5,
)

DEFAULT_INPUT = Path("build/rom_decompiled.json")
CAPTURE_OVERLAY_CAVEAT = (
    "Observed report indexes 0x0007 and 0x0015 are capture overlays/runtime queue "
    "entries; this analyzer does not treat them as statically proven ROM constants."
)


def load_serial_gate_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_serial_gate(payload: dict[str, Any]) -> JsonObject:
    instructions = _instruction_sequence(payload.get("instructions"))
    labels = _collect_labels(payload, instructions)
    by_address = {int(ins["address"]): ins for ins in instructions if "address" in ins}

    evidence = {
        "scheduler_gate_loc_3FD3": _scheduler_gate(by_address),
        "queue_send_gate_loc_BAF2": _queue_send_gate(by_address),
        "resend_gate_path": _resend_gate_path(by_address),
        "rx_session_maintenance": _rx_session_maintenance(by_address),
        "idle_heartbeat_gate_loc_4046": _idle_heartbeat_gate(payload, by_address),
        "timer_tick_evidence": _timer_tick_evidence(payload, by_address),
    }
    access_summary = _state_access_summary(instructions, labels)

    return {
        "kind": "serial_gate",
        "summary": {
            "state_machine_candidate": "autonomous serial TX/report queue gate",
            "confidence": _confidence(evidence),
            "basis": "address-driven static evidence from decompiler JSON",
        },
        "state_addresses": [
            {"address": address, "address_hex": h16(address), "symbol": f"ram_{address:04X}"}
            for address in KEY_STATE_ADDRESSES
        ],
        "evidence": evidence,
        "state_accesses": access_summary,
        "caveats": [
            CAPTURE_OVERLAY_CAVEAT,
            "Queue entries near F870 are reached through RAM-indexed addressing; static JSON proves the access pattern, not the runtime queue contents.",
            "Branch predicates are summarized from local instruction order and targets; this is not an emulator trace.",
        ],
    }


def format_text_report(analysis: dict[str, Any]) -> str:
    lines = [
        "H8/536 Serial Gate/Queue State-Machine Reconstruction",
        "",
        f"Summary: {analysis['summary']['state_machine_candidate']}",
        f"Confidence: {analysis['summary']['confidence']}",
        "",
        "Evidence:",
    ]

    for key, section in analysis.get("evidence", {}).items():
        title = str(section.get("title", key)).rstrip(".")
        status = "present" if section.get("present") else "missing"
        lines.append(f"- {title}: {status}")
        summary = section.get("summary")
        if summary:
            lines.append(f"  {summary}")
        for item in section.get("items", []):
            lines.append(f"  - {item['address_hex']}: {item['text']}")
        roles = section.get("candidate_timer_roles", [])
        if roles:
            lines.append("  Candidate timer roles:")
            for role in roles:
                lines.append(f"  - {role['address_hex']}: {role['role']}")
        timer = section.get("timer")
        if isinstance(timer, dict):
            source = timer.get("source")
            handler = timer.get("handler_address_hex")
            ocra = timer.get("ocra_value_hex")
            period = timer.get("observed_period_ms_candidate")
            timer_bits = [str(part) for part in (source, handler, f"OCRA={ocra}" if ocra else "", f"observed period ~= {period}ms" if period else "") if part]
            if timer_bits:
                lines.append(f"  Timer: {', '.join(timer_bits)}")

    lines.extend(["", "State address readers/writers:"])
    for entry in analysis.get("state_accesses", []):
        lines.append(
            f"- {entry['address_hex']}: reads={entry['read_count']} "
            f"writes={entry['write_count']} read/write={entry['read_write_count']}"
        )
        samples = entry.get("sample_accesses", [])
        if samples:
            sample_text = "; ".join(f"{sample['address_hex']} {sample['access']} {sample['text']}" for sample in samples)
            lines.append(f"  {sample_text}")

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


def write_serial_gate_report(input_path: Path, output_path: Path, *, as_json: bool = False) -> JsonObject:
    analysis = analyze_serial_gate(load_serial_gate_input(input_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="Summarize H8/536 autonomous serial TX/report gates and queue state.",
    )
    parser.add_argument(
        "input",
        nargs="?",
        type=Path,
        default=DEFAULT_INPUT,
        help="structured JSON emitted by h8536_decompiler.py",
    )
    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

    analysis = analyze_serial_gate(load_serial_gate_input(args.input))
    if args.json:
        rendered = json.dumps(analysis, indent=2, sort_keys=True) + "\n"
    else:
        rendered = 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 _scheduler_gate(by_address: dict[int, JsonObject]) -> JsonObject:
    addresses = [0x3FD3, 0x3FD7, 0x3FD9, 0x3FDD, 0x3FDF, 0x3FE3, 0x3FE5, 0x3FE9, 0x3FEB]
    items = _items(by_address, addresses)
    return {
        "title": "loc_3FD3 gate into loc_BAF2",
        "present": _has_all(by_address, (0x3FD3, 0x3FD9, 0x3FDF, 0x3FE5, 0x3FEB)),
        "summary": (
            "Requires FAA2 == 0, allows the FAA5.bit7 path only when F9C3 == 0, "
            "then requires F9C0 == 0 before BSR loc_BAF2."
        ),
        "items": items,
        "required_addresses_hex": [h16(address) for address in addresses],
    }


def _queue_send_gate(by_address: dict[int, JsonObject]) -> JsonObject:
    addresses = [
        0xBAF2,
        0xBAF8,
        0xBAFC,
        0xBAFE,
        0xBB00,
        0xBB08,
        0xBB1C,
        0xBB20,
        0xBB2B,
        0xBB39,
        0xBB3F,
        0xBB43,
        0xBB46,
        0xBB4C,
        0xBB51,
    ]
    return {
        "title": "loc_BAF2 queue send gate",
        "present": _has_all(by_address, (0xBAF2, 0xBAF8, 0xBB08, 0xBB1C, 0xBB39, 0xBB43)),
        "summary": (
            "F9B5 is compared against F9B0; inequality enters the send path, reads a queued "
            "word via the F9B5-derived index around F870, stages F850-F854, and calls BA26 at BB43."
        ),
        "items": _items(by_address, addresses),
        "queue_table_candidate": {
            "base_address_hex": h16(0xF870),
            "index_address_hex": h16(0xF9B5),
            "evidence_address_hex": h16(0xBB08),
            "addressing_text": _text(by_address, 0xBB08),
        },
        "staging_addresses_hex": [h16(address) for address in range(0xF850, 0xF855)],
        "send_subroutine_hex": h16(0xBA26),
        "send_call_address_hex": h16(0xBB43),
    }


def _resend_gate_path(by_address: dict[int, JsonObject]) -> JsonObject:
    addresses = [0xBE9E, 0xBEA5, 0xBEA9, 0xBEAF, 0xBEB5, 0xBEBB, 0xBEC5, 0xBECB, 0xBED1, 0xBED5]
    return {
        "title": "resend gate/path",
        "present": _has_all(by_address, (0xBE9E, 0xBEA5, 0xBEB5, 0xBEBB, 0xBECB, 0xBED5)),
        "summary": (
            "BE9E masks FAA5 with FAA3, waits for F9C6/F9C8 timeout gates, then if FAA3.bit7 "
            "remains set clears F9C3 and calls BA26 from BED5."
        ),
        "items": _items(by_address, addresses),
        "resend_call_address_hex": h16(0xBED5),
        "send_subroutine_hex": h16(0xBA26),
    }


def _rx_session_maintenance(by_address: dict[int, JsonObject]) -> JsonObject:
    addresses = [
        0x3FEF,
        0x3FF5,
        0x3FF9,
        0x3FFD,
        0x4007,
        0xBBCB,
        0xBC0F,
        0xBC15,
        0xBC33,
        0xBC5C,
        0xBC63,
        0xBCD0,
        0xBCFD,
        0xBD04,
        0xBD6D,
        0xBD71,
        0xBD75,
        0xBD79,
        0xBDC8,
        0xBDCC,
        0xBDD0,
        0xBDD4,
        0xBDF3,
        0xBDF7,
        0xBDFB,
        0xBDFF,
    ]
    return {
        "title": "RX/session maintenance",
        "present": _has_all(by_address, (0x3FEF, 0x3FF5, 0xBBCB, 0xBC15, 0xBD6D, 0xBD79)),
        "summary": (
            "F9C5 timeout maintenance clears F9B5/F9B0 and FAA5.bit7; RX command processing "
            "uses FAA2 as an in-session latch and paths advance F9B5/F9B0 or clear FAA3/FAA2."
        ),
        "items": _items(by_address, addresses),
    }


def _idle_heartbeat_gate(payload: dict[str, Any], by_address: dict[int, JsonObject]) -> JsonObject:
    vector = _vector_entry(payload, 0x006A, "frt2_ocia")
    handler = _int_field(vector, "target") if vector else None
    if handler is None:
        handler = 0xBF23
    addresses = [
        0x4046,
        0x404A,
        0x404C,
        0x4050,
        0x4052,
        0x4056,
        0x4058,
        0x4059,
        0x405F,
        0x4063,
        0x4067,
        0x406C,
        0x4070,
        0x40E0,
        0xBA31,
        handler,
        0xBF27,
        0xBF2D,
    ]
    present = _has_all(by_address, (0x4046, 0x4050, 0x4067, 0x40E0, 0xBA31, handler, 0xBF2D))
    return {
        "title": "loc_4046 idle heartbeat/report gate",
        "present": present,
        "summary": (
            "F9C4 gates the idle/default report enqueue. Reset/init loads H'14, each BA26 send "
            "reloads H'07, and the FRT2 OCIA handler decrements it; when it reaches zero loc_4046 "
            "can enqueue H'0000 if the queue is empty and the FAA5/F9C3 RX gate permits it. With "
            "FRT2 OCRA H'7A12 and CKS=phi/32, a phi near 10 MHz gives about 0.7s for H'07, matching "
            "the observed heartbeat cadence."
        ),
        "items": _items(by_address, addresses),
        "gate_address_hex": h16(0x4046),
        "queue_write_address_hex": h16(0x4067),
        "initial_reload_address_hex": h16(0x40E0),
        "post_tx_reload_address_hex": h16(0xBA31),
        "tick_handler_address_hex": h16(handler),
        "decrement_address_hex": h16(0xBF2D),
        "initial_reload_value_hex": "H'14",
        "post_tx_reload_value_hex": "H'07",
        "timer": {
            "source": "FRT2 OCIA",
            "vector_address_hex": h16(0x006A),
            "handler_address_hex": h16(handler),
            "vector_target_label": str(vector.get("target_label", "")) if vector else "",
            "tcr_address_hex": h16(0xFEA0),
            "tcsr_address_hex": h16(0xFEA1),
            "ocra_address_hex": h16(0xFEA4),
            "ocra_value_hex": "H'7A12",
            "clock_select": "CKS1=1 CKS0=0 => phi/32",
            "observed_period_ms_candidate": 700,
            "manual_reference": "Manual/0900766b802125d0.md:12038 FRT CKS1/CKS0 clock select",
        },
        "candidate_timer_roles": [
            {
                "address": 0xF9C4,
                "address_hex": h16(0xF9C4),
                "role": "candidate idle heartbeat/report gate countdown",
                "evidence_address_hex": h16(0xBF2D),
            }
        ],
        "required_addresses_hex": [h16(address) for address in (0x4046, 0x4050, 0x4067, 0x40E0, 0xBA31, handler, 0xBF2D)],
    }


def _timer_tick_evidence(payload: dict[str, Any], by_address: dict[int, JsonObject]) -> JsonObject:
    vector = _vector_entry(payload, 0x0062, "frt1_ocia")
    handler = _int_field(vector, "target") if vector else None
    if handler is None:
        handler = 0xBEEA
    addresses = [handler, 0xBEEE, 0xBEF4, 0xBEF8, 0xBEFE, 0xBF02, 0xBF08]
    has_vector = vector is not None and _int_field(vector, "target") == handler
    has_handler_clear = _instruction_mentions(by_address.get(handler), ("FRT1_TCSR", "#5"))
    decrement_addresses = (0xBEF4, 0xBEFE, 0xBF08)
    has_decrements = all(
        address in by_address and _access_kind(by_address[address], state_address) == "read_write"
        for address, state_address in zip(decrement_addresses, (0xF9C0, 0xF9C1, 0xF9C6))
    )
    return {
        "title": "FRT1 OCIA periodic tick countdowns",
        "present": bool((has_vector or has_handler_clear) and has_decrements),
        "summary": (
            "Static evidence links vector H'0062 to the FRT1 OCIA handler at H'BEEA; the handler "
            "clears FRT1_TCSR.OCFA and conditionally decrements H'F9C0, H'F9C1, and H'F9C6."
        ),
        "vector_address_hex": h16(0x0062),
        "handler_address_hex": h16(handler),
        "vector_target_label": str(vector.get("target_label", "")) if vector else "",
        "items": _items(by_address, addresses),
        "candidate_timer_roles": [
            {
                "address": 0xF9C0,
                "address_hex": h16(0xF9C0),
                "role": "candidate post-TX/report delay countdown",
                "evidence_address_hex": h16(0xBEF4),
            },
            {
                "address": 0xF9C1,
                "address_hex": h16(0xF9C1),
                "role": "candidate secondary delay countdown",
                "evidence_address_hex": h16(0xBEFE),
            },
            {
                "address": 0xF9C6,
                "address_hex": h16(0xF9C6),
                "role": "candidate periodic report/heartbeat countdown",
                "evidence_address_hex": h16(0xBF08),
            },
        ],
    }


def _state_access_summary(instructions: list[JsonObject], labels: dict[int, str]) -> list[JsonObject]:
    result: list[JsonObject] = []
    for state_address in KEY_STATE_ADDRESSES:
        accesses = []
        for ins in instructions:
            if state_address not in _reference_addresses(ins):
                continue
            access = _access_kind(ins, state_address)
            accesses.append(
                {
                    "address": int(ins["address"]),
                    "address_hex": h16(int(ins["address"])),
                    "function": _function_label_for_address(int(ins["address"]), labels),
                    "access": access,
                    "text": str(ins.get("text", "")),
                }
            )
        result.append(
            {
                "address": state_address,
                "address_hex": h16(state_address),
                "read_count": sum(1 for access in accesses if access["access"] == "read"),
                "write_count": sum(1 for access in accesses if access["access"] == "write"),
                "read_write_count": sum(1 for access in accesses if access["access"] == "read_write"),
                "accesses": accesses,
                "sample_accesses": accesses[:6],
            }
        )
    return result


def _instruction_sequence(raw: Any) -> list[JsonObject]:
    if not isinstance(raw, list):
        return []
    return sorted(
        [item for item in raw if isinstance(item, dict) and isinstance(item.get("address"), int)],
        key=lambda item: int(item["address"]),
    )


def _collect_labels(payload: dict[str, Any], instructions: list[JsonObject]) -> dict[int, str]:
    labels: dict[int, str] = {}
    nodes = payload.get("call_graph", {}).get("nodes", []) if isinstance(payload.get("call_graph"), dict) else []
    if isinstance(nodes, list):
        for node in nodes:
            if isinstance(node, dict) and isinstance(node.get("start"), int) and node.get("label"):
                labels[int(node["start"])] = str(node["label"])
    return labels


def _items(by_address: dict[int, JsonObject], addresses: list[int]) -> list[JsonObject]:
    return [
        {
            "address": address,
            "address_hex": h16(address),
            "text": _text(by_address, address),
            "present": address in by_address,
            "targets_hex": [h16(target) for target in by_address.get(address, {}).get("targets", []) if isinstance(target, int)],
        }
        for address in addresses
    ]


def _has_all(by_address: dict[int, JsonObject], addresses: tuple[int, ...]) -> bool:
    return all(address in by_address for address in addresses)


def _vector_entry(payload: dict[str, Any], address: int, name: str) -> JsonObject | None:
    vectors = payload.get("vectors", [])
    if not isinstance(vectors, list):
        return None
    for vector in vectors:
        if not isinstance(vector, dict):
            continue
        if _int_field(vector, "address") == address or str(vector.get("name", "")).lower() == name:
            return vector
    return None


def _int_field(payload: JsonObject | None, key: str, default: int | None = None) -> int | None:
    if not isinstance(payload, dict):
        return default
    value = payload.get(key)
    return value if isinstance(value, int) else default


def _instruction_mentions(ins: JsonObject | None, fragments: tuple[str, ...]) -> bool:
    if not isinstance(ins, dict):
        return False
    text = f"{ins.get('text', '')} {ins.get('operands', '')} {ins.get('comment', '')}".upper()
    return all(fragment.upper() in text for fragment in fragments)


def _text(by_address: dict[int, JsonObject], address: int) -> str:
    return str(by_address.get(address, {}).get("text", "<missing>"))


def _reference_addresses(ins: JsonObject) -> set[int]:
    addresses: set[int] = set()
    refs = ins.get("references", [])
    if isinstance(refs, list):
        for ref in refs:
            if isinstance(ref, dict) and isinstance(ref.get("address"), int):
                addresses.add(int(ref["address"]))
    text = str(ins.get("text", ""))
    for match in re.finditer(r"@H'([0-9A-Fa-f]{4})", text):
        addresses.add(int(match.group(1), 16))
    return addresses


def _access_kind(ins: JsonObject, address: int) -> str:
    mnemonic = str(ins.get("mnemonic", "")).upper()
    operands = str(ins.get("operands", ""))
    target = f"@H'{address:04X}"
    upper_operands = operands.upper()

    if mnemonic.startswith(("TST", "CMP", "BTST")):
        return "read"
    if mnemonic.startswith("CLR"):
        return "write"
    if mnemonic.startswith(("BSET", "BCLR", "ADD", "SUB", "INC", "DEC")):
        return "read_write"
    if mnemonic.startswith("MOV") and "," in upper_operands:
        _src, dest = [part.strip() for part in upper_operands.rsplit(",", 1)]
        return "write" if target in dest else "read"
    if mnemonic.startswith(("AND", "OR", "XOR")) and "," in upper_operands:
        _src, dest = [part.strip() for part in upper_operands.rsplit(",", 1)]
        return "read_write" if target in dest else "read"
    return "read"


def _function_label_for_address(address: int, labels: dict[int, str]) -> str:
    starts = [start for start in labels if start <= address]
    if not starts:
        return label_for(address)
    return labels[max(starts)]


def _confidence(evidence: dict[str, JsonObject]) -> str:
    present_count = sum(1 for section in evidence.values() if section.get("present"))
    if present_count == len(evidence):
        return "high"
    if present_count >= 2:
        return "medium"
    return "low"


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