from __future__ import annotations

import argparse
import json
import os
from pathlib import Path
import shutil
import sqlite3
import sys
import tempfile
from typing import Any


ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))

if hasattr(sys.stdout, "reconfigure"):
    sys.stdout.reconfigure(encoding="utf-8")

from services.ai_voice_runtime_service.audiosocket import pcm16le_to_wav_bytes, resample_pcm16le
from services.ai_voice_runtime_service.providers.tts import build_tts_provider


def _default_provider_name() -> str:
    return os.getenv("AI_VOICE_TTS_PROVIDER", "yandex").strip() or "yandex"


def _parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Bulk-materialize IVR prompt WAV files from a flow JSON document or IVR SQLite DB.",
    )
    source = parser.add_mutually_exclusive_group(required=True)
    source.add_argument("--flow-json-file", help="Path to a flow JSON document.")
    source.add_argument("--sqlite-db", help="Path to SQLite DB containing ivr_flows.")
    parser.add_argument("--flow-id", default=None, help="Optional specific flow_id when reading from SQLite.")
    parser.add_argument("--output-dir", required=True, help="Directory where <prompt_audio_key>.wav files will be written.")
    parser.add_argument("--preview-dir", default=None, help="Optional directory to mirror generated WAV files into.")
    parser.add_argument(
        "--provider",
        default=_default_provider_name(),
        help="TTS provider name. Defaults to AI_VOICE_TTS_PROVIDER or yandex.",
    )
    parser.add_argument("--sample-rate", type=int, default=8000, help="Target WAV sample rate. Defaults to 8000.")
    parser.add_argument("--default-language", default="ru", help="Fallback language for ambiguous prompts. Defaults to ru.")
    parser.add_argument(
        "--language-override",
        action="append",
        default=[],
        help="Explicit language override in prompt_audio_key=language form. May be repeated.",
    )
    parser.add_argument("--dry-run", action="store_true", help="Print planned prompt files without generating audio.")
    return parser.parse_args()


def _parse_language_overrides(raw_overrides: list[str]) -> dict[str, str]:
    overrides: dict[str, str] = {}
    for raw in raw_overrides:
        item = str(raw or "").strip()
        if not item:
            continue
        key, sep, value = item.partition("=")
        if not sep or not key.strip() or not value.strip():
            raise SystemExit(f"Invalid --language-override value: {raw!r}")
        overrides[key.strip()] = value.strip()
    return overrides


def _load_flow_document(args: argparse.Namespace) -> dict[str, Any]:
    if args.flow_json_file:
        return json.loads(Path(args.flow_json_file).read_text(encoding="utf-8"))

    conn = sqlite3.connect(str(args.sqlite_db))
    try:
        if str(args.flow_id or "").strip():
            row = conn.execute(
                "select flow_json from ivr_flows where flow_id = ? order by id desc limit 1",
                (args.flow_id.strip(),),
            ).fetchone()
        else:
            row = conn.execute(
                "select flow_json from ivr_flows where is_active = 1 order by id desc limit 1"
            ).fetchone()
    finally:
        conn.close()
    if row is None or not str(row[0] or "").strip():
        raise SystemExit("Unable to load IVR flow JSON")
    return json.loads(row[0])


def _node_identity(node: dict[str, Any]) -> str:
    return str(node.get("node_id") or node.get("id") or "").strip()


def _infer_language(node: dict[str, Any], *, prompt_key: str, default_language: str) -> str:
    candidates = [
        prompt_key,
        _node_identity(node),
        str(node.get("resolved_queue_code") or "").strip(),
        str(node.get("outcome_code") or "").strip(),
    ]
    for candidate in candidates:
        lowered = candidate.lower()
        if any(token in lowered for token in ("_kz", "-kz", "_kk", "-kk", "kz_", "kk_")):
            return "kz"
        if any(token in lowered for token in ("_ru", "-ru", "ru_")):
            return "ru"
    text = str(node.get("prompt_text") or "").strip().lower()
    has_kazakh_letters = any(char in text for char in "әіңғүұқөһ")
    has_russian_wording = any(token in text for token in ("здравствуйте", "добро пожаловать", "русского", "службы поддержки", "отдела продаж"))
    if has_kazakh_letters and not has_russian_wording:
        return "kz"
    return str(default_language or "ru").strip() or "ru"


def _collect_prompts(
    flow_document: dict[str, Any],
    *,
    default_language: str,
    language_overrides: dict[str, str],
) -> list[dict[str, str]]:
    prompts: list[dict[str, str]] = []
    seen: dict[str, str] = {}

    def _append_prompt(*, node: dict[str, Any], prompt_key: str, prompt_text: str, language: str | None = None) -> None:
        normalized_key = str(prompt_key or "").strip()
        normalized_text = str(prompt_text or "").strip()
        if not normalized_key or not normalized_text:
            return
        previous_text = seen.get(normalized_key)
        if previous_text is not None and previous_text != normalized_text:
            raise SystemExit(f"Prompt key {normalized_key!r} is reused with different texts")
        seen[normalized_key] = normalized_text
        prompts.append(
            {
                "prompt_audio_key": normalized_key,
                "prompt_text": normalized_text,
                "language": language_overrides.get(
                    normalized_key,
                    str(language or "").strip()
                    or _infer_language(node, prompt_key=normalized_key, default_language=default_language),
                ),
                "node_id": _node_identity(node),
            }
        )

    for raw_node in flow_document.get("nodes", []):
        node = raw_node if isinstance(raw_node, dict) else None
        if node is None:
            continue
        prompt_sequence = node.get("prompt_sequence")
        if isinstance(prompt_sequence, list):
            for prompt in prompt_sequence:
                if not isinstance(prompt, dict):
                    continue
                _append_prompt(
                    node=node,
                    prompt_key=str(prompt.get("prompt_audio_key") or ""),
                    prompt_text=str(prompt.get("prompt_text") or ""),
                    language=str(prompt.get("language") or "").strip() or None,
                )
        _append_prompt(
            node=node,
            prompt_key=str(node.get("prompt_audio_key") or ""),
            prompt_text=str(node.get("prompt_text") or ""),
        )
    return prompts


def _write_atomic(target: Path, payload: bytes) -> None:
    target.parent.mkdir(parents=True, exist_ok=True)
    temp_path: str | None = None
    try:
        with tempfile.NamedTemporaryFile(dir=target.parent, delete=False, suffix=".tmp") as handle:
            handle.write(payload)
            temp_path = handle.name
        Path(temp_path).replace(target)
        target.chmod(0o644)
    finally:
        if temp_path:
            try:
                Path(temp_path).unlink(missing_ok=True)
            except OSError:
                pass


def _materialize_prompt(
    *,
    provider_name: str,
    prompt_text: str,
    language: str,
    sample_rate: int,
    output_path: Path,
) -> None:
    provider = build_tts_provider(provider_name)
    synthesis = provider.synthesize(prompt_text, language=language)
    pcm_bytes = resample_pcm16le(
        synthesis.audio_bytes,
        input_rate_hz=synthesis.sample_rate_hz,
        output_rate_hz=max(sample_rate, 1),
    )
    wav_bytes = pcm16le_to_wav_bytes(
        pcm_bytes,
        sample_rate_hz=max(sample_rate, 1),
    )
    _write_atomic(output_path, wav_bytes)


def main() -> int:
    args = _parse_args()
    flow_document = _load_flow_document(args)
    language_overrides = _parse_language_overrides(args.language_override)
    prompts = _collect_prompts(
        flow_document,
        default_language=args.default_language,
        language_overrides=language_overrides,
    )

    output_dir = Path(args.output_dir).expanduser()
    preview_dir = Path(args.preview_dir).expanduser() if args.preview_dir else None
    output_dir.mkdir(parents=True, exist_ok=True)
    if preview_dir is not None:
        preview_dir.mkdir(parents=True, exist_ok=True)

    generated: list[dict[str, str]] = []
    for prompt in prompts:
        output_path = output_dir / f"{prompt['prompt_audio_key']}.wav"
        if not args.dry_run:
            _materialize_prompt(
                provider_name=args.provider,
                prompt_text=prompt["prompt_text"],
                language=prompt["language"],
                sample_rate=args.sample_rate,
                output_path=output_path,
            )
            if preview_dir is not None:
                preview_path = preview_dir / output_path.name
                shutil.copyfile(output_path, preview_path)
                preview_path.chmod(0o644)
        generated.append(
            {
                **prompt,
                "output_path": str(output_path),
            }
        )

    print(json.dumps({"provider": args.provider, "count": len(generated), "prompts": generated}, ensure_ascii=False, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
