#!/usr/bin/env python3
"""Small helper for Xbox raw-disc .xdvd.json sidecars.

This module is intentionally data-only: it loads user-captured SCSI response
metadata and returns byte streams for exact CDBs.  It does not claim that auth
completed; callers decide which captured drive state (locked/unlocked) to use.
"""
from __future__ import annotations

import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any

VERSION = "v100"


def _clean_hex(text: str) -> str:
    return "".join(ch for ch in str(text) if ch in "0123456789abcdefABCDEF").upper()


def hex_to_bytes(text: str) -> bytes:
    s = _clean_hex(text)
    if len(s) % 2:
        raise ValueError(f"odd-length hex string: {text!r}")
    return bytes.fromhex(s)


def bytes_to_hex(data: bytes | bytearray | list[int]) -> str:
    return " ".join(f"{int(b) & 0xFF:02X}" for b in data)


def cdb_key(data: bytes | bytearray | list[int] | str) -> str:
    if isinstance(data, str):
        return _clean_hex(data)
    return "".join(f"{int(b) & 0xFF:02X}" for b in data)


@dataclass
class SidecarResponse:
    name: str
    cdb_hex: str
    data: bytes
    source: str
    state: str | None = None


class XDVDSidecar:
    def __init__(self, path: str | Path):
        self.path = Path(path).resolve()
        with self.path.open("r", encoding="utf-8") as f:
            self.data: dict[str, Any] = json.load(f)
        self.base_dir = self.path.parent

    @property
    def image(self) -> dict[str, Any]:
        return dict(self.data.get("image") or {})

    @property
    def drive_states(self) -> dict[str, Any]:
        return dict(self.data.get("drive_states") or {})

    def image_file_path(self) -> Path | None:
        name = (self.image.get("file") or "").strip()
        if not name:
            return None
        p = Path(name)
        if not p.is_absolute():
            p = self.base_dir / p
        return p

    def summary(self) -> dict[str, Any]:
        image = self.image
        states = self.drive_states
        return {
            "path": str(self.path),
            "format": self.data.get("format"),
            "version": self.data.get("version"),
            "image_file": image.get("file"),
            "sector_size": image.get("sector_size"),
            "sector_count": image.get("sector_count"),
            "byte_count": image.get("byte_count"),
            "sha1": image.get("sha1"),
            "states": sorted(states.keys()),
            "responses": [r.get("name") for r in self.data.get("scsi_responses", []) if isinstance(r, dict)],
        }

    def _state_response(self, state: str, cdb: list[int]) -> SidecarResponse | None:
        state = (state or "locked").strip().lower()
        states = self.drive_states
        if state not in states:
            raise KeyError(f"xdvd state {state!r} not in sidecar; available={sorted(states)}")
        st = states[state] or {}
        key = cdb_key(cdb)
        # READ CAPACITY(10)
        if key.startswith(cdb_key(st.get("read_capacity_10_cdb_hex", ""))):
            return SidecarResponse(
                name=f"{state}_read_capacity_10",
                cdb_hex=st.get("read_capacity_10_cdb_hex", ""),
                data=hex_to_bytes(st.get("read_capacity_10_hex", "")),
                source=f"xdvd:{state}.read_capacity_10_hex",
                state=state,
            )
        # MODE SENSE(10), page 0x3E
        if key.startswith(cdb_key(st.get("mode_sense_3e_cdb_hex", ""))):
            return SidecarResponse(
                name=f"{state}_mode_sense_3e",
                cdb_hex=st.get("mode_sense_3e_cdb_hex", ""),
                data=hex_to_bytes(st.get("mode_sense_3e_hex", "")),
                source=f"xdvd:{state}.mode_sense_3e_hex",
                state=state,
            )
        return None

    def _external_response(self, cdb: list[int]) -> SidecarResponse | None:
        key = cdb_key(cdb)
        for raw in self.data.get("scsi_responses", []) or []:
            if not isinstance(raw, dict):
                continue
            raw_key = cdb_key(raw.get("cdb_hex", ""))
            # Use exact/leading match so a 10-byte CDB does not accidentally
            # match a 12-byte packet; sidecar CDBs are authoritative here.
            if key != raw_key:
                continue
            rel = raw.get("response_file")
            if not rel:
                continue
            p = Path(str(rel))
            if not p.is_absolute():
                p = self.base_dir / p
            data = p.read_bytes()
            expected = raw.get("data_len")
            if expected is not None and int(expected) != len(data):
                raise ValueError(f"{p} length {len(data)} != sidecar data_len {expected}")
            return SidecarResponse(
                name=str(raw.get("name") or p.name),
                cdb_hex=str(raw.get("cdb_hex") or ""),
                data=data,
                source=str(p),
                state=None,
            )
        return None

    def response_for_cdb(self, cdb: list[int], state: str = "locked") -> SidecarResponse | None:
        state_resp = self._state_response(state, cdb)
        if state_resp is not None:
            return state_resp
        return self._external_response(cdb)


def load_sidecar(path: str | Path | None) -> XDVDSidecar | None:
    if not path:
        return None
    return XDVDSidecar(path)
