#!/usr/bin/env python3
"""
CW copy helpers backed by multimon-ng.
"""

from __future__ import annotations

from dataclasses import asdict, dataclass
from datetime import datetime
from pathlib import Path
import re
import shlex
from typing import Dict, Optional


_TEXT_RE = re.compile(
    r"^(?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}):\s*(?P<text>.*\S.*)$"
)
_STAT_RE = re.compile(r"([A-Z_]+):\s*([0-9]+)")
_EMBEDDED_TIMESTAMP_RE = re.compile(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}:?")
_SKIP_PREFIXES = (
    "multimon-ng ",
    "(C) ",
    "Available demodulators:",
    "Enabled demodulators:",
)
_CALLSIGN_RE = re.compile(r"\b(?:[A-Z]{1,2}|[KNW])[0-9][A-Z]{1,4}\b")


@dataclass(frozen=True)
class CWCopyConfig:
    """Configuration for a single RX-only CW copy session."""

    audio_device: str
    sample_rate: int = 22050
    duration_sec: Optional[int] = 20
    bandpass_low_hz: int = 500
    bandpass_high_hz: int = 900
    verbosity: int = 1
    auto_threshold: bool = True
    auto_timing: bool = True
    dit_ms: Optional[int] = None
    gap_ms: Optional[int] = None

    def to_dict(self) -> Dict:
        return asdict(self)


def build_cw_copy_log_path() -> Path:
    """Create a durable CW copy log path under the project log directory."""
    log_dir = Path.home() / "Projects" / "lbf-ham-radio" / "logs" / "cw-copy"
    log_dir.mkdir(parents=True, exist_ok=True)
    stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    return log_dir / f"cw_copy_{stamp}.jsonl"


def normalize_multimon_text(text: str) -> str:
    """Collapse decoder whitespace without fabricating new content."""
    return " ".join(text.split())


def score_copy_text(text: str) -> float:
    """Rank CW copy candidates without inventing content."""
    normalized = normalize_multimon_text(text)
    if not normalized:
        return 0.0

    upper = f" {normalized.upper()} "
    score = min(len(normalized), 48) / 48.0
    alnum_ratio = sum(ch.isalnum() or ch == " " for ch in normalized) / len(normalized)
    score += alnum_ratio

    for token in (" CQ ", " DE ", " TU ", " 73 ", " 5NN ", " TEST "):
        if token in upper:
            score += 0.35

    if _CALLSIGN_RE.search(upper):
        score += 0.5

    if "<ERR_" in upper or "<_" in upper:
        score -= 0.5

    return round(score, 3)


def parse_multimon_cw_line(line: str) -> Optional[Dict]:
    """Parse multimon-ng MORSE_CW output into structured events."""
    raw = line.strip()
    if not raw:
        return None

    if raw.startswith(_SKIP_PREFIXES):
        return None

    text_match = _TEXT_RE.match(raw)
    if text_match:
        text = normalize_multimon_text(text_match.group("text"))
        if text:
            return {
                "type": "copy",
                "timestamp": text_match.group("timestamp"),
                "text": text,
                "raw": raw,
        }
        return None

    embedded_timestamp = _EMBEDDED_TIMESTAMP_RE.search(raw)
    if embedded_timestamp and embedded_timestamp.start() > 0:
        raw = raw[: embedded_timestamp.start()].rstrip()
        if not raw:
            return None

    stats = {match.group(1).lower(): int(match.group(2)) for match in _STAT_RE.finditer(raw)}
    if stats:
        return {"type": "stats", "stats": stats, "raw": raw}

    return {"type": "raw", "raw": raw}


def build_multimon_cw_command(config: CWCopyConfig) -> str:
    """Build the shell pipeline that captures RX audio and decodes CW."""
    if config.sample_rate <= 0:
        raise ValueError("sample_rate must be positive")
    if config.duration_sec is not None and config.duration_sec <= 0:
        raise ValueError("duration_sec must be positive when provided")
    if config.bandpass_low_hz <= 0:
        raise ValueError("bandpass_low_hz must be positive")
    if config.bandpass_high_hz <= config.bandpass_low_hz:
        raise ValueError("bandpass_high_hz must be greater than bandpass_low_hz")

    arecord_cmd = [
        "arecord",
        "-D",
        config.audio_device,
        "-t",
        "raw",
        "-f",
        "S16_LE",
        "-r",
        str(config.sample_rate),
        "-c",
        "1",
    ]
    if config.duration_sec is not None:
        arecord_cmd.extend(["-d", str(config.duration_sec)])
    arecord_cmd.append("-")

    sox_cmd = [
        "sox",
        "-t",
        "raw",
        "-r",
        str(config.sample_rate),
        "-e",
        "signed-integer",
        "-b",
        "16",
        "-c",
        "1",
        "-",
        "-t",
        "raw",
        "-",
        "sinc",
        f"{config.bandpass_low_hz}-{config.bandpass_high_hz}",
        "gain",
        "-n",
    ]

    multimon_cmd = [
        "stdbuf",
        "-oL",
        "-eL",
        "multimon-ng",
        "-t",
        "raw",
        "-a",
        "MORSE_CW",
        "--timestamp",
        "-v",
        str(config.verbosity),
    ]
    if not config.auto_threshold:
        multimon_cmd.append("-x")
    if not config.auto_timing:
        multimon_cmd.append("-y")
    if config.dit_ms is not None:
        multimon_cmd.extend(["-d", str(config.dit_ms)])
    if config.gap_ms is not None:
        multimon_cmd.extend(["-g", str(config.gap_ms)])
    multimon_cmd.append("-")

    def quote_cmd(parts: list[str]) -> str:
        return " ".join(shlex.quote(part) for part in parts)

    return "set -o pipefail; " + " | ".join(
        [
            f"{quote_cmd(arecord_cmd)} 2>/dev/null",
            f"{quote_cmd(sox_cmd)} 2>/dev/null",
            quote_cmd(multimon_cmd),
        ]
    )
