"""D7 fixture-audio contract: known-content WAV into a labelled injection path.

This module does not inject audio. It pins the corpus fixture the phone-mic
journey must hear, and the receipt label that keeps a platform microphone
path honest versus an in-app fake. Adding a virtual audio device or a
capture-source seam is a stop-and-ask, not a silent fallback.
"""

from __future__ import annotations

import hashlib
import json
import wave
from dataclasses import dataclass
from pathlib import Path
from typing import Mapping

from . import session_evidence as se

INJECTION_PLATFORM_MIC = "platform_mic"
INJECTION_IN_APP_FAKE = "in_app_fake"
ALLOWED_INJECTION_KINDS = frozenset({INJECTION_PLATFORM_MIC, INJECTION_IN_APP_FAKE})

CONTENT_EVIDENCE_CAPTURED_PCM = "captured_pcm"
CONTENT_EVIDENCE_TRANSCRIPT_PHRASE = "transcript_phrase"
ALLOWED_CONTENT_EVIDENCE = frozenset({CONTENT_EVIDENCE_CAPTURED_PCM, CONTENT_EVIDENCE_TRANSCRIPT_PHRASE})

FIXTURE_RELATIVE = Path("backend/testing/release_fixtures/transcription-release-probe.wav")
MANIFEST_RELATIVE = Path("backend/testing/release_fixtures/transcription-release-probe.json")

PHONE_MIC_SAMPLE_RATE = 16000
PHONE_MIC_CHANNELS = 1
PHONE_MIC_SAMPLE_WIDTH = 2


class FixtureAudioError(se.EvidenceError):
    """The D7 fixture is missing, the wrong bytes, or the wrong codec."""


@dataclass(frozen=True)
class KnownAudioFixture:
    path: Path
    sha256: str
    sample_rate: int
    channels: int
    sample_width: int
    frames: int
    duration_s: float
    expected_transcript: str
    language: str


def load_known_audio_fixture(repo_root: Path) -> KnownAudioFixture:
    """Load the LibriSpeech release-probe WAV the program already ships."""

    root = Path(repo_root)
    wav_path = root / FIXTURE_RELATIVE
    manifest_path = root / MANIFEST_RELATIVE
    if not wav_path.is_file() or not manifest_path.is_file():
        raise FixtureAudioError("D7 requires the transcription-release-probe WAV and manifest")
    payload = json.loads(manifest_path.read_text(encoding="utf-8"))
    digest = hashlib.sha256(wav_path.read_bytes()).hexdigest()
    expected = str(payload.get("sha256") or "")
    if digest != expected:
        raise FixtureAudioError(f"fixture sha256 {digest} does not match manifest {expected}")
    with wave.open(str(wav_path), "rb") as handle:
        channels = handle.getnchannels()
        sample_width = handle.getsampwidth()
        sample_rate = handle.getframerate()
        frames = handle.getnframes()
        comptype = handle.getcomptype()
    if comptype != "NONE":
        raise FixtureAudioError(f"fixture must be uncompressed PCM, got {comptype!r}")
    if sample_rate != PHONE_MIC_SAMPLE_RATE or channels != PHONE_MIC_CHANNELS or sample_width != PHONE_MIC_SAMPLE_WIDTH:
        raise FixtureAudioError(
            "fixture is not unmodified phone-mic PCM16 16 kHz mono "
            f"(rate={sample_rate} channels={channels} width={sample_width})"
        )
    expected_transcript = str(payload.get("expected_transcript") or "").strip()
    if not expected_transcript:
        raise FixtureAudioError("fixture manifest is missing expected_transcript")
    return KnownAudioFixture(
        path=wav_path,
        sha256=digest,
        sample_rate=sample_rate,
        channels=channels,
        sample_width=sample_width,
        frames=frames,
        duration_s=frames / float(sample_rate),
        expected_transcript=expected_transcript,
        language=str(payload.get("language") or "en"),
    )


def refuse_platform_mic_without_host_audio(audio_disabled: bool) -> None:
    """V3 product boots use -no-audio; that cannot be labelled platform_mic."""

    if audio_disabled:
        raise FixtureAudioError(
            "platform_mic injection requires host audio on the emulator/simulator; "
            "-no-audio is a fake-silent path and must not be labelled a microphone"
        )


def validate_injection_receipt(document: Mapping[str, object]) -> list[str]:
    """A D7 receipt must name the injection kind so a fake cannot pose as a mic."""

    errors: list[str] = []
    live = document.get("fixture_audio")
    if not isinstance(live, Mapping):
        return ["fixture_audio: required object"]
    kind = live.get("injection_kind")
    if kind not in ALLOWED_INJECTION_KINDS:
        errors.append(f"fixture_audio.injection_kind must be one of {sorted(ALLOWED_INJECTION_KINDS)}, got {kind!r}")
    digest = live.get("fixture_sha256")
    if not isinstance(digest, str) or not se.SHA256_RE.fullmatch(digest):
        errors.append("fixture_audio.fixture_sha256 must be 64-char hex")
    heard = live.get("heard")
    if heard is not True:
        errors.append("fixture_audio.heard must be true: a played file is not acceptance")
    evidence = live.get("content_evidence")
    if evidence not in ALLOWED_CONTENT_EVIDENCE:
        errors.append(
            "fixture_audio.content_evidence must be captured_pcm or transcript_phrase "
            f"(fixture content, not that a file played), got {evidence!r}"
        )
    if kind == INJECTION_IN_APP_FAKE and live.get("platform_path_proven") is True:
        errors.append("in_app_fake must not claim platform_path_proven")
    if kind == INJECTION_PLATFORM_MIC and live.get("platform_path_proven") is not True:
        errors.append("platform_mic receipts must set platform_path_proven true")
    return errors