from __future__ import annotations

from datetime import datetime, timezone
from typing import Any, Literal, Optional

import httpx
from fastapi import APIRouter, Depends, Header, HTTPException, Query
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.api.limits import rate_limit_push_by_ip
from app.core.config import get_settings
from app.db.models.push_preference import PushPreference
from app.db.models.push_token import PushToken
from app.db.session import get_db
from app.features.user_profile_scoring.types import UserProfileV1_2


router = APIRouter(
    prefix="/v1/push",
    tags=["push"],
    dependencies=[Depends(rate_limit_push_by_ip)],
)


PushEnv = Literal["dev", "prod"]
PushPlatform = Literal["ios", "android"]


class PushDeviceMeta(BaseModel):
    model: Optional[str] = None
    os_version: Optional[str] = None
    app_version: Optional[str] = None
    locale: Optional[str] = None
    timezone: Optional[str] = None


class PushRegisterRequest(BaseModel):
    client_user_id: str = Field(min_length=8, max_length=64)
    platform: PushPlatform
    push_token: str = Field(min_length=8, max_length=255)
    app_id: str = Field(min_length=1, max_length=255)
    env: PushEnv
    device_meta: Optional[PushDeviceMeta] = None


class PushPreferencesRequest(BaseModel):
    client_user_id: str = Field(min_length=8, max_length=64)
    enabled: bool
    times_per_day: int = Field(ge=0, le=5)
    timezone: Optional[str] = None
    locale: Optional[str] = None
    # 可选：用户画像（用于 Push 场景推荐文案生成）
    user_profile: Optional[UserProfileV1_2] = None


class PushPreferencesResponse(BaseModel):
    client_user_id: str
    enabled: bool
    times_per_day: int
    timezone: Optional[str] = None
    locale: Optional[str] = None
    updated_at: Optional[str] = None


class PushTestRequest(BaseModel):
    client_user_id: str = Field(min_length=8, max_length=64)
    title: Optional[str] = None
    body: Optional[str] = None


def _ensure_utc(dt: datetime) -> datetime:
    if dt.tzinfo is None:
        return dt.replace(tzinfo=timezone.utc)
    return dt.astimezone(timezone.utc)


async def _pick_active_token(db: AsyncSession, *, client_user_id: str) -> Optional[PushToken]:
    # 取最近一次上报的 active token
    q = (
        select(PushToken)
        .where(PushToken.client_user_id == client_user_id, PushToken.is_active == True)  # noqa: E712
        .order_by(PushToken.last_seen_at.desc())
        .limit(1)
    )
    row = await db.execute(q)
    return row.scalar_one_or_none()


async def _send_expo_push(*, to: str, title: str, body: str, data: Optional[dict[str, Any]] = None) -> dict[str, Any]:
    """
    发送 Expo Push。

    说明：
    - V1：最小可用实现，满足 test 与后续定时任务调用
    - 失败处理与 token 停用在后续任务逻辑中完善
    """

    settings = get_settings()
    url = "https://exp.host/--/api/v2/push/send"

    headers: dict[str, str] = {
        "Content-Type": "application/json",
    }
    if settings.expo_access_token:
        headers["Authorization"] = f"Bearer {settings.expo_access_token}"

    payload: dict[str, Any] = {"to": to, "title": title, "body": body}
    if data:
        payload["data"] = data

    async with httpx.AsyncClient(timeout=10.0) as client:
        res = await client.post(url, headers=headers, json=payload)
        if res.status_code >= 400:
            raise HTTPException(status_code=502, detail=f"expo_push_failed:{res.status_code}")
        return res.json()


@router.post("/register")
async def register(req: PushRegisterRequest, db: AsyncSession = Depends(get_db)) -> dict[str, str]:
    """
    注册/更新 Push Token（幂等）。
    """

    now = datetime.now(timezone.utc)

    # 以 env+app_id+push_token 唯一：存在则更新归属与 last_seen
    q = select(PushToken).where(
        PushToken.env == req.env,
        PushToken.app_id == req.app_id,
        PushToken.push_token == req.push_token,
    )
    row = await db.execute(q)
    token = row.scalar_one_or_none()
    if token is None:
        token = PushToken(
            client_user_id=req.client_user_id,
            platform=req.platform,
            push_token=req.push_token,
            app_id=req.app_id,
            env=req.env,
            is_active=True,
            last_seen_at=_ensure_utc(now),
        )
        db.add(token)
    else:
        token.client_user_id = req.client_user_id
        token.platform = req.platform
        token.is_active = True
        token.last_seen_at = _ensure_utc(now)

    await db.commit()
    return {"status": "ok"}


@router.put("/preferences", response_model=PushPreferencesResponse)
async def put_preferences(req: PushPreferencesRequest, db: AsyncSession = Depends(get_db)) -> PushPreferencesResponse:
    """
    设置每日提醒偏好（幂等）。
    """

    # 规范化：enabled=false 或 times=0 均视为关闭
    times = int(req.times_per_day)
    enabled = bool(req.enabled) and times > 0

    q = select(PushPreference).where(PushPreference.client_user_id == req.client_user_id)
    row = await db.execute(q)
    pref = row.scalar_one_or_none()

    if pref is None:
        pref = PushPreference(
            client_user_id=req.client_user_id,
            enabled=enabled,
            times_per_day=times,
            timezone=req.timezone,
            locale=req.locale,
            # 注意：Pydantic 会把 ISO8601 字符串解析成 datetime；
            # SQLAlchemy JSON 列默认使用 json.dumps，无法序列化 datetime。
            # 这里用 mode="json" 保证写库内容都是可 JSON 序列化的基础类型（datetime → ISO 字符串）。
            user_profile_json=req.user_profile.model_dump(mode="json") if req.user_profile else None,
        )
        db.add(pref)
    else:
        pref.enabled = enabled
        pref.times_per_day = times
        pref.timezone = req.timezone
        pref.locale = req.locale
        if req.user_profile is not None:
            pref.user_profile_json = req.user_profile.model_dump(mode="json")

    await db.commit()

    # 返回更新时间（从 ORM 读取到的可能不包含 server_onupdate，这里用 now 兜底）
    updated_at = getattr(pref, "updated_at", None)
    updated_at_iso = updated_at.isoformat() if isinstance(updated_at, datetime) else None

    return PushPreferencesResponse(
        client_user_id=req.client_user_id,
        enabled=enabled,
        times_per_day=times,
        timezone=req.timezone,
        locale=req.locale,
        updated_at=updated_at_iso,
    )


@router.get("/preferences", response_model=PushPreferencesResponse)
async def get_preferences(
    client_user_id: str = Query(min_length=8, max_length=64),
    db: AsyncSession = Depends(get_db),
) -> PushPreferencesResponse:
    q = select(PushPreference).where(PushPreference.client_user_id == client_user_id)
    row = await db.execute(q)
    pref = row.scalar_one_or_none()
    if pref is None:
        return PushPreferencesResponse(client_user_id=client_user_id, enabled=False, times_per_day=0)
    updated_at_iso = pref.updated_at.isoformat() if pref.updated_at else None
    return PushPreferencesResponse(
        client_user_id=client_user_id,
        enabled=bool(pref.enabled) and int(pref.times_per_day) > 0,
        times_per_day=int(pref.times_per_day),
        timezone=pref.timezone,
        locale=pref.locale,
        updated_at=updated_at_iso,
    )


@router.post("/test")
async def test_push(
    req: PushTestRequest,
    db: AsyncSession = Depends(get_db),
    accept_language: Optional[str] = Header(default=None, alias="Accept-Language"),
) -> dict[str, Any]:
    """
    立即测试推送（仅用于 dev 联调）。
    """

    settings = get_settings()
    if settings.app_env != "dev":
        raise HTTPException(status_code=403, detail="test_only_in_dev")

    token = await _pick_active_token(db, client_user_id=req.client_user_id)
    if token is None:
        raise HTTPException(status_code=404, detail="no_active_token")

    # 如果没有用户画像，也不影响 test 推送；文案按请求/默认文案发送。
    q = select(PushPreference).where(PushPreference.client_user_id == req.client_user_id)
    row = await db.execute(q)
    pref = row.scalar_one_or_none()

    if pref and pref.user_profile_json:
        _ = UserProfileV1_2.model_validate(pref.user_profile_json)

    # V1：先发固定测试文案；后续在定时任务中替换为推荐模块的 push 场景模板
    title = req.title or "Hey Mama"
    body = req.body or "这是一条测试推送（dev）。"

    expo_res = await _send_expo_push(to=token.push_token, title=title, body=body, data={"client_user_id": req.client_user_id})
    _ = accept_language
    return {"status": "ok", "expo": expo_res}

