#!/usr/bin/env python3
"""GDR-8050L firmware window-constant probe.

This helper is deliberately conservative: it looks for the currently configured
Redump/XGD1 unlocked media-window delta in the GDR-8050L firmware image and
links that finding to the existing READ CDB CPU sweep.  A constant occurrence is
not treated as a full firmware-derived transfer proof; it is an intermediate
breadcrumb for the next MMIO/SRAM execution step.
"""
from __future__ import annotations

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

from hlds_gdr_firmware_lba_cpu_smoke import resolve_firmware, load_json, write_json, parse_int

VERSION = "v234"
ROM_BASE_DEFAULT = 0x90000000


def _read_json_optional(path: str | None) -> dict[str, Any]:
    if not path:
        return {}
    p = Path(path)
    if not p.exists():
        return {}
    with p.open("r", encoding="utf-8") as f:
        return json.load(f)


def _windows_basename(path_text: str) -> str:
    return path_text.replace("\\", "/").rsplit("/", 1)[-1]


def _candidate_file(path_text: str | None, config_dir: Path) -> Path | None:
    if not path_text:
        return None
    text = str(path_text)
    p = Path(text)
    if p.exists():
        return p
    q = config_dir / text.replace("\\", os.sep).replace("/", os.sep)
    if q.exists():
        return q
    r = Path("/mnt/data") / _windows_basename(text)
    if r.exists():
        return r
    return None


def infer_media_profile(config: dict[str, Any], config_path: Path, explicit: str | None, jsonl: str | None) -> Path | None:
    if explicit:
        p = _candidate_file(explicit, config_path.parent)
        if p:
            return p
    # Existing live rows carry media_profile_path once the bridge has loaded a profile.
    if jsonl and Path(jsonl).exists():
        try:
            with Path(jsonl).open("r", encoding="utf-8") as f:
                for line in f:
                    if not line.strip():
                        continue
                    row = json.loads(line)
                    mp = row.get("media_profile_path")
                    p = _candidate_file(mp, config_path.parent)
                    if p:
                        return p
        except Exception:
            pass
    media = config.get("media") if isinstance(config.get("media"), dict) else {}
    iso = str(media.get("iso_path") or "")
    if iso:
        # Adjacent .media.json is the v227/v228/v229 convention.
        base = _windows_basename(iso)
        if base.lower().endswith(".iso"):
            local = Path("/mnt/data") / (base[:-4] + ".media.json")
            if local.exists():
                return local
            ws = config_path.parent / (base[:-4] + ".media.json")
            if ws.exists():
                return ws
    return None


def infer_expected_delta(args: argparse.Namespace, config: dict[str, Any], config_path: Path) -> tuple[int | None, str, dict[str, Any]]:
    if args.expected_delta is not None:
        return int(parse_int(args.expected_delta)), "cli_expected_delta", {}
    lba_map = _read_json_optional(args.lba_map_json)
    for key in ("configured_media_lba_delta", "candidate_delta_from_unlocked_bridge_rows"):
        if lba_map.get(key) is not None:
            return int(parse_int(lba_map.get(key))), f"lba_map_json.{key}", lba_map
    mp_path = infer_media_profile(config, config_path, args.media_profile, args.jsonl)
    if mp_path and mp_path.exists():
        mp = load_json(mp_path)
        # Prefer explicit layout field names used by the corrected Redump profile.
        for path in [
            ("layout", "game_output_start_lba"),
            ("redump_layout", "game_output_start_lba"),
            ("xgd1", "game_output_start_lba"),
        ]:
            cur: Any = mp
            for part in path:
                cur = cur.get(part) if isinstance(cur, dict) else None
            if cur is not None:
                return int(parse_int(cur)), "media_profile." + ".".join(path), {"media_profile_path": str(mp_path)}
        for key in ("game_output_start_lba", "configured_media_lba_delta", "media_lba_delta"):
            if mp.get(key) is not None:
                return int(parse_int(mp.get(key))), f"media_profile.{key}", {"media_profile_path": str(mp_path)}
    media = config.get("media") if isinstance(config.get("media"), dict) else {}
    fw = config.get("firmware_window_proof") if isinstance(config.get("firmware_window_proof"), dict) else {}
    # This is legacy fallback only; v229 should normally use lba-map or media profile.
    for key, src in [("redump_game_output_start_lba", "config.media.redump_game_output_start_lba"), ("gdr_media_lba_delta", "config.media.gdr_media_lba_delta"), ("legacy_contiguous_unlocked_game_media_lba_delta", "config.firmware_window_proof.legacy_contiguous_unlocked_game_media_lba_delta")]:
        obj = media if key in media else fw
        if isinstance(obj, dict) and obj.get(key) is not None:
            return int(parse_int(obj.get(key))), src, {}
    return None, "unresolved", {}


def find_patterns(blob: bytes, value: int, rom_base: int) -> list[dict[str, Any]]:
    encodings: list[tuple[str, bytes]] = []
    if 0 <= value <= 0xFFFFFFFF:
        encodings += [
            ("u32le", value.to_bytes(4, "little")),
            ("u32be", value.to_bytes(4, "big")),
        ]
    if 0 <= value <= 0xFFFFFF:
        encodings += [
            ("u24le", value.to_bytes(3, "little")),
            ("u24be", value.to_bytes(3, "big")),
        ]
    if 0 <= value <= 0xFFFF:
        encodings += [
            ("u16le", value.to_bytes(2, "little")),
            ("u16be", value.to_bytes(2, "big")),
        ]
    out: list[dict[str, Any]] = []
    for enc, pat in encodings:
        pos = 0
        while True:
            off = blob.find(pat, pos)
            if off < 0:
                break
            lo = max(0, off - 16)
            hi = min(len(blob), off + len(pat) + 16)
            out.append({
                "encoding": enc,
                "offset": off,
                "rom_address": f"0x{rom_base + off:08X}",
                "pattern_hex": pat.hex(" ").upper(),
                "context_offset": lo,
                "context_rom_address": f"0x{rom_base + lo:08X}",
                "context_hex": blob[lo:hi].hex(" ").upper(),
            })
            pos = off + 1
    return out


def summarize_matches(matches: list[dict[str, Any]]) -> dict[str, Any]:
    by_encoding: dict[str, int] = {}
    for m in matches:
        by_encoding[m["encoding"]] = by_encoding.get(m["encoding"], 0) + 1
    # Prefer exact 32-bit little-endian, then 24-bit little-endian, because MN103S is little-endian.
    strongest = None
    for enc in ("u32le", "u24le", "u32be", "u24be", "u16le", "u16be"):
        cand = [m for m in matches if m["encoding"] == enc]
        if cand:
            strongest = cand[0]
            break
    return {"counts_by_encoding": by_encoding, "strongest_candidate": strongest}


def write_md(path: Path, report: dict[str, Any]) -> None:
    lines = [
        f"# GDR-8050L {VERSION} firmware window constant probe",
        "",
        f"- ok: `{str(report.get('ok')).lower()}`",
        f"- expected_delta: `{report.get('expected_delta')}` / `{report.get('expected_delta_hex')}`",
        f"- expected_delta_source: `{report.get('expected_delta_source')}`",
        f"- firmware_delta_constant_present: `{str(report.get('firmware_delta_constant_present')).lower()}`",
        f"- unique_match_count: `{report.get('unique_match_count')}`",
        f"- firmware_derived_host_lba: `{str(report.get('firmware_derived_host_lba')).lower()}`",
        f"- firmware_derived_delta: `{str(report.get('firmware_derived_delta')).lower()}`",
        "",
        "## Interpretation",
        "",
        str(report.get("interpretation") or ""),
        "",
        "## Match summary",
        "",
        f"- counts_by_encoding: `{report.get('counts_by_encoding')}`",
    ]
    sc = report.get("strongest_candidate") or {}
    if sc:
        lines += [
            f"- strongest_candidate: `{sc.get('rom_address')}` `{sc.get('encoding')}` pattern `{sc.get('pattern_hex')}`",
            f"- context: `{sc.get('context_rom_address')}` `{sc.get('context_hex')}`",
        ]
    lines += ["", "## Matches", "", "| encoding | ROM address | offset | pattern | context |", "|---|---:|---:|---|---|"]
    for m in report.get("matches", [])[:64]:
        lines.append(f"| `{m.get('encoding')}` | `{m.get('rom_address')}` | `{m.get('offset')}` | `{m.get('pattern_hex')}` | `{m.get('context_hex')}` |")
    if len(report.get("matches", [])) > 64:
        lines.append(f"| ... | ... | ... | ... | truncated {len(report.get('matches', [])) - 64} more |")
    lines += ["", "## Inputs", "", f"- firmware: `{report.get('firmware')}`", f"- config: `{report.get('config')}`"]
    if report.get("read_cdb_sweep_json"):
        lines.append(f"- read_cdb_sweep_json: `{report.get('read_cdb_sweep_json')}`")
    path.write_text("\n".join(lines) + "\n", encoding="utf-8")


def main(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(description=f"GDR-8050L {VERSION} firmware window constant probe")
    ap.add_argument("--config", default="gdr8050l_auth_v234_stateful_relock_keep_loaded_iscsi_config.json")
    ap.add_argument("--firmware")
    ap.add_argument("--jsonl")
    ap.add_argument("--lba-map-json")
    ap.add_argument("--media-profile")
    ap.add_argument("--read-cdb-sweep-json")
    ap.add_argument("--expected-delta")
    ap.add_argument("--rom-base", default="0x90000000")
    ap.add_argument("--json", default="gdr8050l_auth_v234_firmware_window_constant_probe_report.json")
    ap.add_argument("--md", default="gdr8050l_auth_v234_firmware_window_constant_probe_report.md")
    args = ap.parse_args(argv)

    config_path = Path(args.config).resolve()
    config = load_json(config_path) if config_path.exists() else {}
    firmware = resolve_firmware(config, config_path, args.firmware)
    rom_base = int(parse_int(args.rom_base, ROM_BASE_DEFAULT))
    expected_delta, expected_source, source_obj = infer_expected_delta(args, config, config_path)
    sweep = _read_json_optional(args.read_cdb_sweep_json)

    report: dict[str, Any] = {
        "version": VERSION,
        "config": str(config_path),
        "firmware": str(firmware or ""),
        "jsonl": str(args.jsonl or ""),
        "lba_map_json": str(args.lba_map_json or ""),
        "read_cdb_sweep_json": str(args.read_cdb_sweep_json or ""),
        "expected_delta": expected_delta,
        "expected_delta_hex": None if expected_delta is None else f"0x{expected_delta:08X}",
        "expected_delta_source": expected_source,
        "expected_delta_context": source_obj,
        "firmware_derived_host_lba": bool(sweep.get("firmware_derived_host_lba")) if sweep else None,
        "firmware_derived_delta": False,
        "interpretation": "This is an intermediate firmware-data constant probe. It can show that the configured Redump unlocked-window delta is present in the ROM image, but it does not by itself prove that the live READ transfer path uses that constant. The next proof step is an execution trace from READ CDB ingestion through the SRAM/MMIO/media-window decision that consumes this value.",
    }

    if expected_delta is None:
        report.update({"ok": False, "error": "could not infer expected delta", "matches": [], "firmware_delta_constant_present": False})
    elif not firmware or not Path(str(firmware)).exists():
        report.update({"ok": False, "error": f"firmware image not found: {firmware}", "matches": [], "firmware_delta_constant_present": False})
    else:
        blob = Path(str(firmware)).read_bytes()
        matches = find_patterns(blob, int(expected_delta), rom_base)
        summary = summarize_matches(matches)
        report.update({
            "firmware_size": len(blob),
            "matches": matches,
            "match_count": len(matches),
            "unique_match_offsets": sorted({m["offset"] for m in matches}),
            "unique_match_count": len({m["offset"] for m in matches}),
            "counts_by_encoding": summary["counts_by_encoding"],
            "strongest_candidate": summary["strongest_candidate"],
            "firmware_delta_constant_present": bool(matches),
            "ok": bool(matches),
        })

    write_json(Path(args.json), report)
    write_md(Path(args.md), report)
    sc = report.get("strongest_candidate") or {}
    print(f"[FIRMWARE-WINDOW-CONSTANT] expected_delta={report.get('expected_delta_hex')} source={report.get('expected_delta_source')} matches={report.get('match_count', 0)} unique_offsets={report.get('unique_match_count', 0)} strongest={sc.get('rom_address', '')} firmware_derived_delta=false ok={str(report.get('ok')).lower()}")
    return 0 if report.get("ok") else 1


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