"""FastAPI entrypoint.

0.1 路由：
    GET  /health
    POST /auth/register     {phone}
    POST /auth/verify       {request_id, code}
    POST /auth/logout       (Bearer session)
"""

from __future__ import annotations

from contextlib import asynccontextmanager

from fastapi import Depends, FastAPI, Header, HTTPException
from pydantic import BaseModel, Field

from . import (
    __version__,
    auth,
    exposure as exposure_svc,
    profile as profile_svc,
    query as query_svc,
)
from .db import init_db


@asynccontextmanager
async def lifespan(app: FastAPI):
    init_db()
    yield


app = FastAPI(
    title="xiangqin",
    version=__version__,
    description="相亲平台 — 付费置顶曝光",
    lifespan=lifespan,
)


# ========== 基础 ==========


@app.get("/health")
def health() -> dict[str, str]:
    return {"status": "ok", "version": __version__}


# ========== auth ==========


class RegisterIn(BaseModel):
    phone: str = Field(..., min_length=11, max_length=11)


class RegisterOut(BaseModel):
    request_id: str
    masked_phone: str
    expires_in_s: int
    mock_code_hint: str = "0.1 期 mock：验证码恒为 123456"


class VerifyIn(BaseModel):
    request_id: str
    code: str = Field(..., min_length=4, max_length=8)


class VerifyOut(BaseModel):
    session_token: str
    user_id: str
    is_new_user: bool


def _auth_error_to_http(e: auth.AuthError) -> HTTPException:
    return HTTPException(
        status_code=e.http_status,
        detail={"code": e.code, "message": str(e)},
    )


def current_user_id(authorization: str | None = Header(default=None)) -> str:
    if not authorization or not authorization.lower().startswith("bearer "):
        raise HTTPException(
            status_code=401,
            detail={"code": "unauthenticated", "message": "缺少 Authorization: Bearer 头"},
        )
    token = authorization.split(" ", 1)[1].strip()
    try:
        return auth.resolve_session(token)
    except auth.AuthError as e:
        raise _auth_error_to_http(e) from e


@app.post("/auth/register", response_model=RegisterOut)
def register(body: RegisterIn) -> RegisterOut:
    try:
        r = auth.register(body.phone)
    except auth.AuthError as e:
        raise _auth_error_to_http(e) from e
    return RegisterOut(
        request_id=r.request_id,
        masked_phone=r.masked_phone,
        expires_in_s=r.expires_in_s,
    )


@app.post("/auth/verify", response_model=VerifyOut)
def verify(body: VerifyIn) -> VerifyOut:
    try:
        r = auth.verify(body.request_id, body.code)
    except auth.AuthError as e:
        raise _auth_error_to_http(e) from e
    return VerifyOut(
        session_token=r.session_token,
        user_id=r.user_id,
        is_new_user=r.is_new_user,
    )


@app.post("/auth/logout")
def logout(authorization: str | None = Header(default=None)) -> dict[str, str]:
    if authorization and authorization.lower().startswith("bearer "):
        token = authorization.split(" ", 1)[1].strip()
        auth.logout(token)
    return {"status": "ok"}


@app.get("/auth/me")
def me(user_id: str = Depends(current_user_id)) -> dict[str, str]:
    return {"user_id": user_id}


# ========== profile ==========


class ProfileOut(BaseModel):
    user_id: str
    gender: str | None
    age: int | None
    city: str | None
    tags: list[str]
    bio: str | None
    updated_at: int


class ProfileUpdateIn(BaseModel):
    gender: str | None = None
    age: int | None = None
    city: str | None = None
    tags: list[str] | None = None
    bio: str | None = None


def _profile_to_out(p: profile_svc.Profile) -> ProfileOut:
    return ProfileOut(
        user_id=p.user_id,
        gender=p.gender,
        age=p.age,
        city=p.city,
        tags=p.tags,
        bio=p.bio,
        updated_at=p.updated_at,
    )


@app.get("/profile/me", response_model=ProfileOut)
def profile_get(user_id: str = Depends(current_user_id)) -> ProfileOut:
    try:
        p = profile_svc.get(user_id)
    except profile_svc.ProfileError as e:
        raise HTTPException(
            status_code=404, detail={"code": e.code, "message": str(e)}
        ) from e
    return _profile_to_out(p)


@app.put("/profile/me", response_model=ProfileOut)
def profile_update(
    body: ProfileUpdateIn,
    user_id: str = Depends(current_user_id),
) -> ProfileOut:
    payload = body.model_dump(exclude_unset=True)
    try:
        p = profile_svc.update(user_id, payload)
    except profile_svc.ProfileError as e:
        raise HTTPException(
            status_code=400, detail={"code": e.code, "message": str(e)}
        ) from e
    return _profile_to_out(p)


# ========== query ==========


class QueryIn(BaseModel):
    where: str
    limit: int = 50


class QueryHitOut(BaseModel):
    user_id: str
    gender: str | None
    age: int | None
    city: str | None
    tags: list[str]
    bio: str | None
    is_promoted: bool


class QueryOut(BaseModel):
    results: list[QueryHitOut]
    total_hits: int
    promoted_hits: int
    exposure_consumed: int


@app.post("/query", response_model=QueryOut)
def query(
    body: QueryIn,
    user_id: str = Depends(current_user_id),
) -> QueryOut:
    try:
        r = query_svc.run(user_id, body.where, body.limit)
    except query_svc.QueryError as e:
        raise HTTPException(
            status_code=400, detail={"code": e.code, "message": str(e)}
        ) from e
    return QueryOut(
        results=[
            QueryHitOut(
                user_id=h.user_id,
                gender=h.gender,
                age=h.age,
                city=h.city,
                tags=h.tags,
                bio=h.bio,
                is_promoted=h.is_promoted,
            )
            for h in r.results
        ],
        total_hits=r.total_hits,
        promoted_hits=r.promoted_hits,
        exposure_consumed=r.exposure_consumed,
    )


# ========== exposure ==========


class BuyIn(BaseModel):
    count: int = Field(..., ge=1, le=10_000)
    mock: bool = True  # 0.1 期强制 true


class OrderOut(BaseModel):
    id: str
    user_id: str
    count: int
    amount_cents: int
    mock: bool
    paid: bool
    created_at: int


class BalanceOut(BaseModel):
    remaining: int
    total_purchased: int
    last_consumed_at: int | None


@app.post("/exposure/orders", response_model=OrderOut)
def exposure_buy(
    body: BuyIn,
    user_id: str = Depends(current_user_id),
) -> OrderOut:
    try:
        o = exposure_svc.buy(user_id, body.count, body.mock)
    except exposure_svc.ExposureError as e:
        raise HTTPException(
            status_code=400, detail={"code": e.code, "message": str(e)}
        ) from e
    return OrderOut(**o.__dict__)


@app.get("/exposure/balance", response_model=BalanceOut)
def exposure_balance(user_id: str = Depends(current_user_id)) -> BalanceOut:
    b = exposure_svc.get_balance(user_id)
    return BalanceOut(
        remaining=b.remaining,
        total_purchased=b.total_purchased,
        last_consumed_at=b.last_consumed_at,
    )


@app.get("/exposure/history", response_model=list[OrderOut])
def exposure_history(
    limit: int = 50,
    user_id: str = Depends(current_user_id),
) -> list[OrderOut]:
    try:
        orders = exposure_svc.list_orders(user_id, limit)
    except exposure_svc.ExposureError as e:
        raise HTTPException(
            status_code=400, detail={"code": e.code, "message": str(e)}
        ) from e
    return [OrderOut(**o.__dict__) for o in orders]


def run() -> None:
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8000)
