from __future__ import annotations

from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
import json
import logging
import random
from re import fullmatch

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.redis_client import get_sync_redis_client
from app.services.secret_service import SecretService

logger = logging.getLogger(__name__)


@dataclass
class SmsCodeRecord:
    code: str
    expires_at: datetime


@dataclass(frozen=True)
class SmsProviderConfig:
    mock_sms_enabled: bool
    mock_sms_code: str
    sms_code_expire_minutes: int
    aliyun_sms_access_key_id: str
    aliyun_sms_access_key_secret: str
    aliyun_sms_sign_name: str
    aliyun_sms_template_code: str
    aliyun_sms_template_param_key: str
    aliyun_sms_endpoint: str


class SmsCodeService:
    _codes: dict[str, SmsCodeRecord] = {}

    @classmethod
    def send_code(cls, db: Session, phone: str) -> None:
        cls._validate_phone(phone)
        config = _load_sms_config(db)
        code = config.mock_sms_code if config.mock_sms_enabled else _generate_sms_code()
        if not config.mock_sms_enabled:
            _send_aliyun_sms(phone, code, config)
        ttl_seconds = config.sms_code_expire_minutes * 60
        if _set_redis_code(phone, code, ttl_seconds):
            cls._codes.pop(phone, None)
            return
        cls._codes[phone] = SmsCodeRecord(code=code, expires_at=datetime.now(UTC) + timedelta(seconds=ttl_seconds))

    @classmethod
    def verify_code(cls, db: Session, phone: str, code: str) -> None:
        cls._validate_phone(phone)
        config = _load_sms_config(db)
        redis_available, redis_code = _get_redis_code(phone)
        if redis_available and redis_code is not None:
            if redis_code != code:
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="验证码错误")
            _delete_redis_code(phone)
            cls._codes.pop(phone, None)
            return

        record = cls._codes.get(phone)
        if config.mock_sms_enabled and record is None and code == config.mock_sms_code:
            return
        if record is None:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="验证码不存在或已过期")
        if record.expires_at < datetime.now(UTC):
            cls._codes.pop(phone, None)
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="验证码已过期")
        if record.code != code:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="验证码错误")
        cls._codes.pop(phone, None)

    @staticmethod
    def _validate_phone(phone: str) -> None:
        if fullmatch(r"1[3-9]\d{9}", phone) is None:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="手机号格式不正确")


def _load_sms_config(db: Session) -> SmsProviderConfig:
    settings = get_settings()
    rows = db.scalars(select(SystemConfig)).all()
    values = {row.config_key: row.config_value for row in rows}
    return SmsProviderConfig(
        mock_sms_enabled=_config_bool(values, "mock_sms_enabled", settings.mock_sms_enabled),
        mock_sms_code=_config_text(values, "mock_sms_code", settings.mock_sms_code),
        sms_code_expire_minutes=_config_int(
            values,
            "sms_code_expire_minutes",
            settings.sms_code_expire_minutes,
            minimum=1,
            maximum=60,
        ),
        aliyun_sms_access_key_id=_config_text(
            values,
            "aliyun_sms_access_key_id",
            settings.aliyun_sms_access_key_id,
        ),
        aliyun_sms_access_key_secret=SecretService.decrypt(
            _config_text(values, "aliyun_sms_access_key_secret", settings.aliyun_sms_access_key_secret)
        ),
        aliyun_sms_sign_name=_config_text(values, "aliyun_sms_sign_name", settings.aliyun_sms_sign_name),
        aliyun_sms_template_code=_config_text(
            values,
            "aliyun_sms_template_code",
            settings.aliyun_sms_template_code,
        ),
        aliyun_sms_template_param_key=_config_text(
            values,
            "aliyun_sms_template_param_key",
            settings.aliyun_sms_template_param_key,
        )
        or "code",
        aliyun_sms_endpoint=_config_text(values, "aliyun_sms_endpoint", settings.aliyun_sms_endpoint)
        or "dysmsapi.aliyuncs.com",
    )


def _send_aliyun_sms(phone: str, code: str, config: SmsProviderConfig) -> None:
    missing = [
        name
        for name, value in (
            ("AccessKey ID", config.aliyun_sms_access_key_id),
            ("AccessKey Secret", config.aliyun_sms_access_key_secret),
            ("短信签名", config.aliyun_sms_sign_name),
            ("模板 Code", config.aliyun_sms_template_code),
        )
        if not value
    ]
    if missing:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"阿里云短信配置不完整：请补充{','.join(missing)}",
        )

    try:
        from alibabacloud_dysmsapi20170525.client import Client as DysmsapiClient
        from alibabacloud_dysmsapi20170525 import models as dysms_models
        from alibabacloud_tea_openapi import models as open_api_models
        from alibabacloud_tea_util import models as util_models
    except ImportError as exc:
        logger.exception("Aliyun SMS SDK import failed")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="阿里云短信 SDK 未安装，请联系管理员检查部署依赖",
        ) from exc

    client_config = open_api_models.Config(
        access_key_id=config.aliyun_sms_access_key_id,
        access_key_secret=config.aliyun_sms_access_key_secret,
    )
    client_config.endpoint = config.aliyun_sms_endpoint
    request = dysms_models.SendSmsRequest(
        phone_numbers=phone,
        sign_name=config.aliyun_sms_sign_name,
        template_code=config.aliyun_sms_template_code,
        template_param=json.dumps({config.aliyun_sms_template_param_key: code}, ensure_ascii=False),
    )
    try:
        response = DysmsapiClient(client_config).send_sms_with_options(request, util_models.RuntimeOptions())
    except Exception as exc:
        logger.warning("Aliyun SMS send failed for %s: %s", _mask_phone(phone), exc.__class__.__name__)
        raise HTTPException(
            status_code=status.HTTP_502_BAD_GATEWAY,
            detail="短信发送失败，请稍后再试",
        ) from exc

    body = getattr(response, "body", None)
    response_code = getattr(body, "code", None)
    if response_code not in (None, "OK"):
        logger.warning("Aliyun SMS rejected for %s: %s", _mask_phone(phone), response_code)
        raise HTTPException(
            status_code=status.HTTP_502_BAD_GATEWAY,
            detail="短信发送失败，请稍后再试",
        )


def _generate_sms_code() -> str:
    return f"{random.SystemRandom().randint(0, 999999):06d}"


def _sms_code_key(phone: str) -> str:
    return f"auth:sms_code:{phone}"


def _set_redis_code(phone: str, code: str, ttl_seconds: int) -> bool:
    client = get_sync_redis_client()
    if client is None:
        return False
    try:
        return bool(client.set(_sms_code_key(phone), code, ex=ttl_seconds))
    except Exception:
        logger.warning("Redis SMS code write failed for %s", _mask_phone(phone), exc_info=True)
        return False


def _get_redis_code(phone: str) -> tuple[bool, str | None]:
    client = get_sync_redis_client()
    if client is None:
        return False, None
    try:
        value = client.get(_sms_code_key(phone))
    except Exception:
        logger.warning("Redis SMS code read failed for %s", _mask_phone(phone), exc_info=True)
        return False, None
    return True, value if isinstance(value, str) else None


def _delete_redis_code(phone: str) -> None:
    client = get_sync_redis_client()
    if client is None:
        return
    try:
        client.delete(_sms_code_key(phone))
    except Exception:
        logger.warning("Redis SMS code delete failed for %s", _mask_phone(phone), exc_info=True)


def _config_text(values: dict[str, str], key: str, default: str) -> str:
    value = values.get(key)
    return value.strip() if value is not None else default


def _config_bool(values: dict[str, str], key: str, default: bool) -> bool:
    value = values.get(key)
    if value is None:
        return default
    return value.strip().lower() in {"1", "true", "yes", "on"}


def _config_int(values: dict[str, str], key: str, default: int, *, minimum: int, maximum: int) -> int:
    value = values.get(key)
    if value is not None:
        try:
            return max(minimum, min(maximum, int(float(value.strip()))))
        except ValueError:
            pass
    return default


def _mask_phone(phone: str) -> str:
    if len(phone) != 11:
        return "***"
    return f"{phone[:3]}****{phone[-4:]}"
