"""Tone generator — short alert tones played before/after speech.

Generates WAV files programmatically using numpy. Tones are written once
at startup and reused for every queued playback.
"""

import sys
import wave
from pathlib import Path

import numpy as np

SAMPLE_RATE = 48000


def _sine_segment(freq_hz: float, duration_ms: float, fade_ms: float = 1.0) -> np.ndarray:
    """Generate a sine wave segment with fade-in/fade-out to avoid clicks."""
    n_samples = int(SAMPLE_RATE * duration_ms / 1000)
    t = np.arange(n_samples, dtype=np.float32) / SAMPLE_RATE
    tone = np.sin(2 * np.pi * freq_hz * t)

    # Apply fade envelope
    fade_samples = int(SAMPLE_RATE * fade_ms / 1000)
    if fade_samples > 0 and fade_samples * 2 < n_samples:
        tone[:fade_samples] *= np.linspace(0, 1, fade_samples)
        tone[-fade_samples:] *= np.linspace(1, 0, fade_samples)

    return tone


def _silence(duration_ms: float) -> np.ndarray:
    return np.zeros(int(SAMPLE_RATE * duration_ms / 1000), dtype=np.float32)


def _build_chirp() -> np.ndarray:
    """Nextel TPT (Talk Permit Tone): 1800 Hz, 24/24/24/24/48 ms pattern."""
    segments = [
        _sine_segment(1800, 24),
        _silence(24),
        _sine_segment(1800, 24),
        _silence(24),
        _sine_segment(1800, 48),
    ]
    return np.concatenate(segments)


def _build_apollo() -> np.ndarray:
    """Quindar intro tone: 2525 Hz, 250 ms with 5 ms fade in/out."""
    return _sine_segment(2525, 250, fade_ms=5.0)


def _build_roger() -> np.ndarray:
    """Classic descending two-tone roger beep: 1400 Hz -> 1000 Hz."""
    segments = [
        _sine_segment(1400, 40),
        _sine_segment(1000, 60),
    ]
    return np.concatenate(segments)


def _build_quindar_out() -> np.ndarray:
    """Quindar unkey tone: 2475 Hz, 250 ms with 5 ms fade in/out."""
    return _sine_segment(2475, 250, fade_ms=5.0)


def _build_standby() -> np.ndarray:
    """Quick ascending blip — "more coming, standby"."""
    segments = [
        _sine_segment(1000, 30),
        _sine_segment(1400, 30),
    ]
    return np.concatenate(segments)


def _build_scratch() -> np.ndarray:
    """Vinyl record scratch — descending sweep with noise, ~120ms."""
    duration_ms = 120
    n_samples = int(SAMPLE_RATE * duration_ms / 1000)
    t = np.arange(n_samples, dtype=np.float32) / SAMPLE_RATE

    # Descending frequency sweep: 2000 Hz -> 300 Hz
    f0, f1 = 2000.0, 300.0
    phase = 2 * np.pi * (f0 * t + (f1 - f0) / (2 * duration_ms / 1000) * t**2)
    sweep = np.sin(phase)

    # Mix in noise (pink-ish via low-pass filtered white noise)
    rng = np.random.default_rng(42)
    noise = rng.standard_normal(n_samples).astype(np.float32)
    # Simple low-pass: cumulative average over 8-sample windows
    kernel = np.ones(8, dtype=np.float32) / 8
    noise = np.convolve(noise, kernel, mode="same")

    # Mix: 60% sweep + 40% noise, with amplitude envelope (fade out)
    envelope = np.linspace(1.0, 0.0, n_samples, dtype=np.float32) ** 0.5
    mixed = (0.6 * sweep + 0.4 * noise) * envelope

    # Click at start (needle hit) — 2ms burst
    click_samples = int(SAMPLE_RATE * 0.002)
    mixed[:click_samples] += 0.5 * rng.standard_normal(click_samples).astype(np.float32)

    return mixed


def _build_reverse_roger() -> np.ndarray:
    """Ascending two-tone — inverse of roger beep: 1000 Hz -> 1400 Hz."""
    segments = [
        _sine_segment(1000, 40),
        _sine_segment(1400, 60),
    ]
    return np.concatenate(segments)


def _write_tone(samples: np.ndarray, path: Path) -> Path:
    """Write float32 samples to 16-bit PCM WAV."""
    peak = max(abs(samples.max()), abs(samples.min()), 1e-8)
    pcm = (samples / peak * 32767 * 0.7).astype(np.int16)  # -3 dB headroom

    with wave.open(str(path), "wb") as wf:
        wf.setnchannels(1)
        wf.setsampwidth(2)
        wf.setframerate(SAMPLE_RATE)
        wf.writeframes(pcm.tobytes())

    return path


_TONE_BUILDERS = {
    "chirp": _build_chirp,
    "apollo": _build_apollo,
    "roger": _build_roger,
    "quindar-out": _build_quindar_out,
    "standby": _build_standby,
    "scratch": _build_scratch,
    "reverse-roger": _build_reverse_roger,
}


def generate_tones(output_dir: Path) -> dict[str, Path]:
    """Generate all tone WAV files. Returns {name: path} mapping."""
    output_dir.mkdir(parents=True, exist_ok=True)
    tones = {}

    for name, builder in _TONE_BUILDERS.items():
        path = output_dir / f"_tone-{name}.wav"
        if not path.exists():
            _write_tone(builder(), path)
            print(f"  Generated tone: {path.name}", file=sys.stderr)
        tones[name] = path

    return tones


def resolve_tone(
    setting: str, tone_paths: dict[str, Path], label: str, default: str | None = None
) -> Path | None:
    """Resolve a tone setting to a WAV file path or None.

    Args:
        setting: Built-in tone name, "none", or a file path.
        tone_paths: Mapping from generate_tones().
        label: Setting name for warning messages (e.g. "entry_tone").
        default: Fallback tone name if setting not found. None = no fallback.
    """
    if setting == "none":
        return None

    if setting in tone_paths:
        return tone_paths[setting]

    # Treat as a custom WAV path
    custom = Path(setting)
    if custom.exists():
        return custom

    fallback = tone_paths.get(default) if default else None
    if fallback:
        print(
            f"  Warning: {label} '{setting}' not found, falling back to {default}",
            file=sys.stderr,
        )
    else:
        print(f"  Warning: {label} '{setting}' not found, disabled", file=sys.stderr)
    return fallback
