#!/usr/bin/env python3
"""Build two original, low-level non-speech cues and rights/hash records.

Copyright 2026 NeuroForgeIO Pty Ltd.
Licensed under Apache License 2.0: https://www.apache.org/licenses/LICENSE-2.0
"""

from __future__ import annotations

import hashlib
import json
import math
import struct
import wave
from pathlib import Path


ROOT = Path(__file__).resolve().parent
SAMPLE_RATE = 48_000
PEAK_TARGET = 10 ** (-18 / 20)


def tone(frequency: float, seconds: float) -> list[float]:
    count = round(seconds * SAMPLE_RATE)
    fade = round(0.035 * SAMPLE_RATE)
    samples: list[float] = []
    for index in range(count):
        envelope = min(1.0, index / fade, (count - 1 - index) / fade)
        time = index / SAMPLE_RATE
        sample = envelope * (
            math.sin(2 * math.pi * frequency * time)
            + 0.12 * math.sin(4 * math.pi * frequency * time)
        )
        samples.append(sample)
    return samples


def silence(seconds: float) -> list[float]:
    return [0.0] * round(seconds * SAMPLE_RATE)


def write_wav(name: str, signal: list[float]) -> None:
    maximum = max(abs(sample) for sample in signal)
    scaled = [round(sample / maximum * PEAK_TARGET * 32767)
              for sample in signal]
    output = ROOT / "cues" / name
    output.parent.mkdir(exist_ok=True)
    with wave.open(str(output), "wb") as file:
        file.setnchannels(1)
        file.setsampwidth(2)
        file.setframerate(SAMPLE_RATE)
        file.writeframes(struct.pack(f"<{len(scaled)}h", *scaled))


def cue_metrics(path: Path) -> dict[str, float | int]:
    with wave.open(str(path), "rb") as file:
        assert file.getnchannels() == 1
        assert file.getsampwidth() == 2
        assert file.getframerate() == SAMPLE_RATE
        frames = file.getnframes()
        samples = struct.unpack(f"<{frames}h", file.readframes(frames))
    peak = max(abs(sample) for sample in samples)
    peak_dbfs = 20 * math.log10(peak / 32768)
    assert peak_dbfs <= -17.9, (path, peak_dbfs)
    assert any(sample for sample in samples)
    assert all(abs(sample) < 32767 for sample in samples)
    return {
        "sample_rate_hz": SAMPLE_RATE,
        "channels": 1,
        "pcm_bits_per_sample": 16,
        "duration_ms": round(frames / SAMPLE_RATE * 1000),
        "peak_dbfs": round(peak_dbfs, 2),
    }


def sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def make_manifest() -> None:
    code_hash = sha256(ROOT / "build_cues.py")
    assets = []
    for path in sorted(ROOT.rglob("*")):
        if (not path.is_file() or path.name == "asset-manifest.json"
                or "review-pending" in path.relative_to(ROOT).parts):
            continue
        relative = str(path.relative_to(ROOT))
        is_code = path.suffix == ".py"
        is_cue = path.suffix == ".wav"
        is_script = relative.startswith("transcripts/")
        record: dict[str, object] = {
            "item_code_or_locator": relative,
            "authority": "NeuroForgeIO Pty Ltd",
            "title": path.stem.replace("-", " ").title(),
            "version": "0.1.0-draft",
            "created_at": "2026-09-29",
            "sha256": sha256(path),
            "licence": "Apache-2.0" if is_code else "CC BY 4.0",
            "licence_url": ("https://www.apache.org/licenses/LICENSE-2.0"
                            if is_code else
                            "https://creativecommons.org/licenses/by/4.0/"),
            "attribution_text": "© NeuroForgeIO Pty Ltd 2026. Original SubjectNest work.",
            "change_notice": "Original release; reusers must indicate changes if adapted.",
            "source_url": None,
            "source_hash": None,
            "modified": False,
            "excluded_material_note": "No third-party music, sound sample, recording or voice included.",
            "review_status": "local educator, accessibility and human listening review pending",
            "reviewed_at": None,
        }
        if is_script:
            source = ROOT.parent / "ten-minute-play-australia" / "cards" / path.name.replace(".txt", ".md")
            assert source.exists(), source
            record.update({
                "media_type": "text/plain",
                "release_state": "voice-ready script only; no narrated recording",
                "related_activity": str(source.relative_to(ROOT.parent)),
                "source_hash": sha256(source),
                "week": (int(path.name[:2]) - 1) // 4 + 1,
            })
        if is_cue:
            record.update({
                "media_type": "audio/wav",
                "release_state": "original optional non-speech cue",
                "source_hash": code_hash,
                "text_alternative": "AUDIO_DESCRIPTIONS.md",
                "audio_metrics": cue_metrics(path),
            })
        assets.append(record)
    manifest = {
        "pack_id": "subjectnest-early-years-ten-minute-play-australia-audio-companion",
        "created_at": "2026-09-29",
        "status": "20 voice-ready scripts and 2 non-speech cues; no narrated speech",
        "licence_policy": {
            "original_resources": "CC BY 4.0",
            "build_code": "Apache-2.0",
            "third_party_voice_output": "none released",
        },
        "assets": assets,
    }
    (ROOT / "asset-manifest.json").write_text(
        json.dumps(manifest, indent=2, ensure_ascii=False) + "\n",
        encoding="utf-8",
    )


def main() -> None:
    write_wav("invitation.wav", silence(0.12) + tone(392.0, 0.18)
              + silence(0.08) + tone(523.25, 0.18) + silence(0.09))
    write_wav("pause.wav", silence(0.10) + tone(330.0, 0.26)
              + silence(0.09))
    make_manifest()


if __name__ == "__main__":
    main()
