import asyncio
import json
import logging
import random
import time
from datetime import datetime
from typing import Any, Tuple

from app.config import settings
from app.utils.redis import get_redis

logger = logging.getLogger("videogen")

# In-memory fallback for local development when Redis is disabled.
_sms_code_store: dict[str, tuple[str, float]] = {}
_sms_send_interval_store: dict[str, float] = {}
_sms_daily_count_store: dict[str, tuple[str, int]] = {}


def _generate_code(length: int | None = None) -> str:
    code_length = int(length or settings.SMS_CODE_LENGTH or 4)
    code_length = max(4, min(code_length, 8))
    return "".join(random.choices("0123456789", k=code_length))


def _normalize_scene(scene: str | None) -> str:
    value = (scene or "").strip().lower()
    if value not in {"register", "login", "set_password"}:
        value = "login"
    return value


def _code_key(phone: str, scene: str | None) -> str:
    return f"sms_code:{_normalize_scene(scene)}:{phone}"


def _interval_key(phone: str, scene: str | None) -> str:
    return f"sms_interval:{_normalize_scene(scene)}:{phone}"


def _daily_key(phone: str, scene: str | None) -> str:
    today = datetime.now().strftime("%Y%m%d")
    return f"sms_daily:{_normalize_scene(scene)}:{phone}:{today}"


def _today() -> str:
    return datetime.now().strftime("%Y%m%d")


async def _check_send_limit(phone: str, scene: str) -> None:
    """发送频控：同场景同手机号间隔限制 + 每日次数限制。"""
    redis = get_redis()
    interval_seconds = int(settings.SMS_SEND_INTERVAL_SECONDS or 60)
    daily_limit = int(settings.SMS_DAILY_LIMIT or 20)

    if redis:
        interval_key = _interval_key(phone, scene)
        if await redis.get(interval_key):
            raise ValueError(f"短信发送过于频繁，请{interval_seconds}秒后再试")

        daily_key = _daily_key(phone, scene)
        count = await redis.incr(daily_key)
        if count == 1:
            await redis.expire(daily_key, 24 * 60 * 60)
        if count > daily_limit:
            raise ValueError("今日短信发送次数已达上限，请明天再试")

        await redis.setex(interval_key, interval_seconds, "1")
        return

    now_ts = time.time()
    interval_key = _interval_key(phone, scene)
    last_send_at = _sms_send_interval_store.get(interval_key)
    if last_send_at and now_ts - last_send_at < interval_seconds:
        raise ValueError(f"短信发送过于频繁，请{interval_seconds}秒后再试")
    _sms_send_interval_store[interval_key] = now_ts

    daily_key = f"{_normalize_scene(scene)}:{phone}"
    current_day = _today()
    stored_day, count = _sms_daily_count_store.get(daily_key, (current_day, 0))
    if stored_day != current_day:
        stored_day, count = current_day, 0
    count += 1
    _sms_daily_count_store[daily_key] = (stored_day, count)
    if count > daily_limit:
        raise ValueError("今日短信发送次数已达上限，请明天再试")


def _send_volc_sms_sync(phone: str, code: str) -> dict[str, Any]:
    from volcengine.sms.SmsService import SmsService

    if not settings.VOLC_SMS_ACCESS_KEY_ID or not settings.VOLC_SMS_SECRET_ACCESS_KEY:
        raise RuntimeError("火山短信 AK/SK 未配置")
    if not settings.VOLC_SMS_ACCOUNT:
        raise RuntimeError("火山短信消息组ID VOLC_SMS_ACCOUNT 未配置")
    if not settings.VOLC_SMS_TEMPLATE_ID:
        raise RuntimeError("火山短信模板ID VOLC_SMS_TEMPLATE_ID 未配置")
    if not settings.VOLC_SMS_SIGN:
        raise RuntimeError("火山短信签名 VOLC_SMS_SIGN 未配置")

    sms_service = SmsService()
    sms_service.set_ak(settings.VOLC_SMS_ACCESS_KEY_ID)
    sms_service.set_sk(settings.VOLC_SMS_SECRET_ACCESS_KEY)

    body = {
        "SmsAccount": settings.VOLC_SMS_ACCOUNT,
        "Sign": settings.VOLC_SMS_SIGN,
        "TemplateID": settings.VOLC_SMS_TEMPLATE_ID,
        "TemplateParam": json.dumps({"xxxx": str(code)}, ensure_ascii=False, separators=(",", ":")),
        "Tag": f"{phone}:{int(time.time())}",
        "PhoneNumbers": phone,
    }
    raw_resp = sms_service.send_sms(json.dumps(body, ensure_ascii=False, separators=(",", ":")))

    if isinstance(raw_resp, str):
        try:
            resp: dict[str, Any] = json.loads(raw_resp)
        except json.JSONDecodeError:
            resp = {"raw": raw_resp}
    elif isinstance(raw_resp, dict):
        resp = raw_resp
    else:
        resp = {"raw": raw_resp}

    error = (resp.get("ResponseMetadata") or {}).get("Error") if isinstance(resp, dict) else None
    if error:
        raise RuntimeError(f"火山短信发送失败：{error.get('Code')} {error.get('Message')}")

    return resp


async def send_sms(phone: str, code: str, scene: str = "login") -> bool:
    """发送短信验证码。

    SMS_MOCK=true 时只写日志，方便本地调试；否则使用火山引擎短信 SDK。
    火山 SDK 为同步调用，这里放到线程中执行，避免阻塞 FastAPI event loop。
    """
    scene = _normalize_scene(scene)

    try:
        logger.info("[SMS] scene=%s, To=%s, Code=%s", scene, phone, code)
        await asyncio.to_thread(_send_volc_sms_sync, phone, code)
        return True
    except Exception:
        logger.exception("SMS send failed. scene=%s phone=%s", scene, phone)
        return False


async def store_sms_code(phone: str, code: str, scene: str = "login", ttl: int | None = None) -> None:
    ttl_seconds = int(ttl or settings.SMS_CODE_TTL_SECONDS or 300)
    key = _code_key(phone, scene)
    redis = get_redis()
    if redis:
        await redis.setex(key, ttl_seconds, code)
    else:
        _sms_code_store[key] = (code, time.time() + ttl_seconds)


async def generate_and_send_sms(phone: str, scene: str = "login") -> Tuple[bool, str]:
    scene = _normalize_scene(scene)
    await _check_send_limit(phone, scene)
    code = _generate_code()
    ok = await send_sms(phone, code, scene)
    if ok:
        await store_sms_code(phone, code, scene)
    return ok, code


async def verify_sms_code(phone: str, code: str, scene: str = "login") -> bool:
    key = _code_key(phone, scene)
    redis = get_redis()
    if redis:
        stored = await redis.get(key)
        if isinstance(stored, bytes):
            stored = stored.decode()
        if stored is not None and stored == code:
            await redis.delete(key)
            return True
        return False

    entry = _sms_code_store.pop(key, None)
    if entry:
        stored_code, expires = entry
        if time.time() < expires and stored_code == code:
            return True
    return False
