from __future__ import annotations

import json
import io
import logging
import threading
import time
import wave
from dataclasses import dataclass

import httpx
from fastapi import HTTPException, status
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.core.config import get_settings
from app.models.ai_config import SystemConfig
from app.services.secret_service import SecretService
from app.services.security_state_service import SecurityStateService

logger = logging.getLogger(__name__)
MAX_UPLOAD_BYTES = 8 * 1024 * 1024
ALIYUN_MAX_AUDIO_BYTES = 2 * 1024 * 1024
_token_cache: dict[str, tuple[str, float]] = {}
_token_lock = threading.Lock()


@dataclass(frozen=True)
class VoiceInputConfig:
    enabled: bool
    max_duration_seconds: int
    app_key: str
    access_key_id: str
    access_key_secret: str
    endpoint: str


class VoiceInputService:
    @staticmethod
    def public_config(db: Session) -> dict:
        config = load_voice_config(db)
        return {"enabled": config.enabled, "maxDurationSeconds": config.max_duration_seconds}

    @staticmethod
    def transcribe(db: Session, user_id: int, audio: bytes, content_type: str | None) -> dict:
        config = load_voice_config(db)
        if not config.enabled:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="语音输入功能未开启")
        if not audio:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="录音内容为空")
        if len(audio) > MAX_UPLOAD_BYTES:
            raise HTTPException(status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, detail="录音文件过大")
        SecurityStateService.enforce_limit(
            f"voice:transcribe:{user_id}",
            limit=10,
            window_seconds=60,
            message="语音识别请求过于频繁，请稍后再试",
        )
        missing = [name for name, value in (("AppKey", config.app_key), ("AccessKey ID", config.access_key_id), ("AccessKey Secret", config.access_key_secret)) if not value]
        if missing:
            raise HTTPException(status_code=503, detail=f"语音识别配置不完整：{', '.join(missing)}")

        wav = _normalize_audio(audio, content_type)
        duration = _wav_duration(wav)
        if duration < 0.2:
            raise HTTPException(status_code=400, detail="录音时间太短，请重新录制")
        if duration > config.max_duration_seconds + 0.5 or duration > 60.5:
            raise HTTPException(status_code=400, detail=f"单条语音不能超过 {config.max_duration_seconds} 秒")
        if len(wav) > ALIYUN_MAX_AUDIO_BYTES:
            raise HTTPException(status_code=413, detail="转码后的录音文件过大")

        token = _create_aliyun_token(config)
        params = {
            "appkey": config.app_key,
            "format": "wav",
            "sample_rate": 16000,
            "enable_punctuation_prediction": "true",
            "enable_inverse_text_normalization": "true",
            "enable_voice_detection": "true",
        }
        try:
            response = httpx.post(
                config.endpoint,
                params=params,
                headers={"X-NLS-Token": token, "Content-Type": "application/octet-stream"},
                content=wav,
                timeout=httpx.Timeout(20.0, connect=5.0),
            )
            payload = response.json()
        except (httpx.HTTPError, ValueError) as exc:
            logger.warning("Aliyun NLS request failed: %s", exc.__class__.__name__)
            raise HTTPException(status_code=502, detail="语音识别服务暂不可用，请稍后重试") from exc
        if response.status_code != 200 or int(payload.get("status", 0)) != 20000000:
            logger.warning("Aliyun NLS rejected request: status=%s code=%s", response.status_code, payload.get("status"))
            raise HTTPException(status_code=502, detail=_safe_provider_message(payload))
        text = str(payload.get("result") or "").strip()
        if not text:
            raise HTTPException(status_code=422, detail="没有识别到有效语音，请重新录制")
        return {"text": text, "durationSeconds": round(duration, 1)}


def load_voice_config(db: Session) -> VoiceInputConfig:
    settings = get_settings()
    rows = db.scalars(select(SystemConfig)).all()
    values = {row.config_key: row.config_value for row in rows}
    enabled = _bool(values.get("voice_input_enabled"), settings.voice_input_enabled)
    duration = _int(values.get("voice_max_duration_seconds"), settings.voice_max_duration_seconds, 5, 60)
    access_key_id = str(values.get("aliyun_nls_access_key_id") or settings.aliyun_nls_access_key_id).strip()
    encrypted_secret = str(values.get("aliyun_nls_access_key_secret") or settings.aliyun_nls_access_key_secret).strip()
    return VoiceInputConfig(
        enabled=enabled,
        max_duration_seconds=duration,
        app_key=str(values.get("aliyun_nls_app_key") or settings.aliyun_nls_app_key).strip(),
        access_key_id=access_key_id,
        access_key_secret=SecretService.decrypt(encrypted_secret),
        endpoint=str(values.get("aliyun_nls_endpoint") or settings.aliyun_nls_endpoint).strip(),
    )


def _normalize_audio(audio: bytes, content_type: str | None) -> bytes:
    if audio[:4] != b"RIFF" or audio[8:12] != b"WAVE":
        raise HTTPException(status_code=422, detail="当前录音格式无法处理，请更换浏览器后重试")
    return audio


def _wav_duration(audio: bytes) -> float:
    try:
        with wave.open(io.BytesIO(audio), "rb") as wav_file:
            if wav_file.getnchannels() != 1 or wav_file.getsampwidth() != 2 or wav_file.getframerate() != 16000:
                raise HTTPException(status_code=422, detail="录音参数不正确，请重新录制")
            return wav_file.getnframes() / float(wav_file.getframerate())
    except wave.Error as exc:
        raise HTTPException(status_code=422, detail="录音文件已损坏，请重新录制") from exc


def _create_aliyun_token(config: VoiceInputConfig) -> str:
    cached = _token_cache.get(config.access_key_id)
    if cached and cached[1] - 300 > time.time():
        return cached[0]
    try:
        from aliyunsdkcore.client import AcsClient
        from aliyunsdkcore.request import CommonRequest

        client = AcsClient(config.access_key_id, config.access_key_secret, "cn-shanghai")
        request = CommonRequest()
        request.set_method("POST")
        request.set_domain("nls-meta.cn-shanghai.aliyuncs.com")
        request.set_version("2019-02-28")
        request.set_action_name("CreateToken")
        with _token_lock:
            cached = _token_cache.get(config.access_key_id)
            if cached and cached[1] - 300 > time.time():
                return cached[0]
            payload = json.loads(client.do_action_with_exception(request))
            token = str(payload["Token"]["Id"])
            expires_at = float(payload["Token"]["ExpireTime"])
            _token_cache[config.access_key_id] = (token, expires_at)
            return token
    except Exception as exc:
        logger.warning("Aliyun NLS token creation failed: %s", exc.__class__.__name__)
        raise HTTPException(status_code=502, detail="语音识别鉴权失败，请联系管理员检查阿里云权限") from exc


def _safe_provider_message(payload: dict) -> str:
    code = str(payload.get("status") or "")
    if code in {"40070001", "40070002", "40070004"}:
        return "没有识别到有效语音，请重新录制"
    if code in {"40000001", "40000002", "40020503"}:
        return "语音识别鉴权失败，请联系管理员检查阿里云权限"
    return "语音识别失败，请稍后重试"


def _bool(value: str | None, default: bool) -> bool:
    if value is None:
        return default
    return value.strip().lower() in {"1", "true", "yes", "on"}


def _int(value: str | None, default: int, minimum: int, maximum: int) -> int:
    try:
        parsed = int(value) if value is not None else default
    except (TypeError, ValueError):
        parsed = default
    return max(minimum, min(maximum, parsed))
