"""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

import json
import os
import time
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any

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

from . import (
    __version__,
    auth,
    mail as mail_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,
)


# ========== 性能监控 middleware ==========

_ACCESS_LOG_PATH = Path(os.environ.get("XIANGQIN_ACCESS_LOG", "/opt/xiangqin/logs/access.jsonl"))


@app.middleware("http")
async def access_log_middleware(request: Request, call_next):
    """每个请求写一行 JSON 到 access.jsonl，供每日聚合算 p50/p95/error_rate。"""
    t0 = time.monotonic()
    response = await call_next(request)
    latency_ms = int((time.monotonic() - t0) * 1000)
    try:
        _ACCESS_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
        with _ACCESS_LOG_PATH.open("a") as f:
            f.write(
                json.dumps(
                    {
                        "ts": int(time.time()),
                        "method": request.method,
                        "path": request.url.path,
                        "status": response.status_code,
                        "latency_ms": latency_ms,
                    },
                    ensure_ascii=False,
                )
                + "\n"
            )
    except Exception:
        pass
    return response


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


@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
    height: int | None
    education: str | None
    agent_gateway_url: str | None
    agent_hooks_token: str | None  # masked
    notify_on_new_mail: bool
    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
    height: int | None = None
    education: str | None = None
    agent_gateway_url: str | None = None
    agent_hooks_token: str | None = None
    notify_on_new_mail: bool | 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,
        height=p.height,
        education=p.education,
        agent_gateway_url=p.agent_gateway_url,
        agent_hooks_token=p.agent_hooks_token_masked,
        notify_on_new_mail=p.notify_on_new_mail,
        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
    height: int | None
    education: str | None


class QueryOut(BaseModel):
    results: list[QueryHitOut]
    total_hits: 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,
                height=h.height,
                education=h.education,
            )
            for h in r.results
        ],
        total_hits=r.total_hits,
    )


# ========== mail / 虚拟信封（M4）==========


class DmSendIn(BaseModel):
    to_user_id: str
    body: str


class MessageOut(BaseModel):
    id: str
    from_user_id: str
    to_user_id: str
    body: str
    sent_at: int
    delivery_status: str
    delivery_failure_reason: str | None
    notify_attempted_at: int | None
    notify_status: str | None
    notify_error: str | None
    read_at: int | None
    replied_at: int | None
    reply_to_message_id: str | None
    deleted_by_recipient_at: int | None
    envelope_source: str
    expires_at: int


def _msg_to_out(m: mail_svc.MessageRow) -> MessageOut:
    return MessageOut(
        id=m.id,
        from_user_id=m.from_user_id,
        to_user_id=m.to_user_id,
        body=m.body,
        sent_at=m.sent_at,
        delivery_status=m.delivery_status,
        delivery_failure_reason=m.delivery_failure_reason,
        notify_attempted_at=m.notify_attempted_at,
        notify_status=m.notify_status,
        notify_error=m.notify_error,
        read_at=m.read_at,
        replied_at=m.replied_at,
        reply_to_message_id=m.reply_to_message_id,
        deleted_by_recipient_at=m.deleted_by_recipient_at,
        envelope_source=m.envelope_source,
        expires_at=m.expires_at,
    )


@app.post("/dm/send", response_model=MessageOut)
def dm_send(body: DmSendIn, user_id: str = Depends(current_user_id)) -> MessageOut:
    try:
        msg = mail_svc.send(user_id, body.to_user_id, body.body)
    except mail_svc.MailError as e:
        raise HTTPException(status_code=400, detail={"code": e.code, "message": str(e)}) from e
    return _msg_to_out(msg)


@app.get("/dm/sent", response_model=list[MessageOut])
def dm_sent_list(
    user_id: str = Depends(current_user_id),
    limit: int = 50,
) -> list[MessageOut]:
    return [_msg_to_out(m) for m in mail_svc.list_sent(user_id, limit)]


@app.get("/dm/message/{message_id}", response_model=MessageOut)
def dm_message_get(
    message_id: str,
    user_id: str = Depends(current_user_id),
) -> MessageOut:
    try:
        msg = mail_svc.get_message(user_id, message_id, as_role="any")
    except mail_svc.MailError as e:
        code = 404 if e.code == "message_not_found" else 403
        raise HTTPException(status_code=code, detail={"code": e.code, "message": str(e)}) from e
    return _msg_to_out(msg)


@app.get("/inbox", response_model=list[MessageOut])
def inbox_list(
    user_id: str = Depends(current_user_id),
    limit: int = 50,
) -> list[MessageOut]:
    return [_msg_to_out(m) for m in mail_svc.list_inbox(user_id, limit)]


@app.post("/inbox/{message_id}/read", response_model=MessageOut)
def inbox_read(
    message_id: str,
    user_id: str = Depends(current_user_id),
) -> MessageOut:
    try:
        msg = mail_svc.read_message(user_id, message_id)
    except mail_svc.MailError as e:
        code = 404 if e.code == "message_not_found" else 403
        raise HTTPException(status_code=code, detail={"code": e.code, "message": str(e)}) from e
    return _msg_to_out(msg)


@app.delete("/inbox/{message_id}")
def inbox_delete(
    message_id: str,
    user_id: str = Depends(current_user_id),
) -> dict[str, str]:
    try:
        mail_svc.delete_message_by_recipient(user_id, message_id)
    except mail_svc.MailError as e:
        code = 404 if e.code == "message_not_found" else 403
        raise HTTPException(status_code=code, detail={"code": e.code, "message": str(e)}) from e
    return {"status": "deleted"}


class ReplyIn(BaseModel):
    body: str


@app.post("/inbox/{message_id}/reply", response_model=MessageOut)
def inbox_reply(
    message_id: str,
    body: ReplyIn,
    user_id: str = Depends(current_user_id),
) -> MessageOut:
    try:
        msg = mail_svc.reply(user_id, message_id, body.body)
    except mail_svc.MailError as e:
        code = 404 if e.code == "message_not_found" else 400
        raise HTTPException(status_code=code, detail={"code": e.code, "message": str(e)}) from e
    return _msg_to_out(msg)


class ReportIn(BaseModel):
    reason: str | None = None


@app.post("/inbox/{message_id}/report")
def inbox_report(
    message_id: str,
    body: ReportIn,
    user_id: str = Depends(current_user_id),
) -> dict[str, Any]:
    try:
        return mail_svc.report(user_id, message_id, body.reason)
    except mail_svc.MailError as e:
        code = 404 if e.code == "message_not_found" else 403
        raise HTTPException(status_code=code, detail={"code": e.code, "message": str(e)}) from e


class BlockIn(BaseModel):
    user_id: str


@app.post("/block")
def block_user(
    body: BlockIn,
    user_id: str = Depends(current_user_id),
) -> dict[str, str]:
    try:
        mail_svc.block(user_id, body.user_id)
    except mail_svc.MailError as e:
        raise HTTPException(status_code=400, detail={"code": e.code, "message": str(e)}) from e
    return {"status": "blocked"}


@app.delete("/block/{blocked_id}")
def unblock_user(
    blocked_id: str,
    user_id: str = Depends(current_user_id),
) -> dict[str, str]:
    mail_svc.unblock(user_id, blocked_id)
    return {"status": "unblocked"}


@app.get("/blocklist")
def blocklist(user_id: str = Depends(current_user_id)) -> list[dict[str, Any]]:
    return mail_svc.blocklist(user_id)


class EnvelopeStateOut(BaseModel):
    free_remaining_today: int
    paid_remaining: int
    total_purchased: int
    free_per_day: int = mail_svc.FREE_ENVELOPES_PER_DAY
    price_cents: int = mail_svc.ENVELOPE_PRICE_CENTS


@app.get("/envelope", response_model=EnvelopeStateOut)
def envelope_state(user_id: str = Depends(current_user_id)) -> EnvelopeStateOut:
    s = mail_svc.get_envelope_state(user_id)
    return EnvelopeStateOut(
        free_remaining_today=s.free_remaining_today,
        paid_remaining=s.paid_remaining,
        total_purchased=s.total_purchased,
    )


class EnvelopeBuyIn(BaseModel):
    count: int
    mock: bool = True


@app.post("/envelope/buy")
def envelope_buy(
    body: EnvelopeBuyIn,
    user_id: str = Depends(current_user_id),
) -> dict[str, Any]:
    try:
        return mail_svc.buy_envelopes(user_id, body.count, mock=body.mock)
    except mail_svc.MailError as e:
        raise HTTPException(status_code=400, detail={"code": e.code, "message": str(e)}) from e


def run() -> None:
    import uvicorn

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