"""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, File, Header, HTTPException, Request, UploadFile
from fastapi.responses import PlainTextResponse
from pydantic import BaseModel, Field

from . import (
    __version__,
    admin as admin_svc,
    auth,
    mail as mail_svc,
    photo as photo_svc,
    profile as profile_svc,
    query as query_svc,
    realname as realname_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,
    )


class VerifyIdIn(BaseModel):
    legal_name: str = Field(..., min_length=1, max_length=64)
    cert_id: str = Field(..., min_length=18, max_length=18)


class VerifyIdOut(BaseModel):
    status: str  # passed / mismatch / unknown_id / name_wrong / error
    verified: bool
    message: str


@app.post("/auth/verify-id", response_model=VerifyIdOut)
def verify_id(body: VerifyIdIn, user_id: str = Depends(current_user_id)) -> VerifyIdOut:
    """实名二要素核验（FEAT-031'）。调用方必须已登录。"""
    try:
        r = realname_svc.verify(user_id, body.legal_name, body.cert_id)
    except realname_svc.RealnameError as e:
        raise HTTPException(
            status_code=e.http_status, detail={"code": e.code, "message": str(e)}
        ) from e
    return VerifyIdOut(status=r.status, verified=r.verified, message=r.message)


@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
    verified: bool  # PRD-001 决策点 #5：实名标签
    avatar_url: str | None = None  # 签名 URL（1h 有效），无头像返 null


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

    hits: list[QueryHitOut] = []
    for h in r.results:
        try:
            a_url = photo_svc.avatar_url(h.user_id)
        except photo_svc.PhotoError:
            a_url = None  # OSS 挂了不拖整个 query
        hits.append(
            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,
                verified=h.verified,
                avatar_url=a_url,
            )
        )
    return QueryOut(results=hits, 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 = False


@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


@app.get("/envelope/order/{order_id}")
def envelope_order_get(
    order_id: str,
    user_id: str = Depends(current_user_id),
) -> dict[str, Any]:
    try:
        order = mail_svc.get_order(user_id, order_id)
    except mail_svc.MailError as e:
        raise HTTPException(
            status_code=e.http_status if hasattr(e, "http_status") else 400,
            detail={"code": e.code, "message": str(e)},
        ) from e
    if not order:
        raise HTTPException(status_code=404, detail={"code": "not_found", "message": "订单不存在"})
    return order


@app.post("/envelope/webhook/alipay")
async def envelope_alipay_webhook(request: Request) -> PlainTextResponse:
    """支付宝异步回调。返回纯文本 'success' 让支付宝停止重试。"""
    body = await request.body()
    try:
        mail_svc.handle_alipay_webhook(body)
    except mail_svc.MailError:
        # 支付宝收到非 'success' 响应会重试 —— 我们故意返回 fail 让它重试
        return PlainTextResponse("fail", status_code=200)
    return PlainTextResponse("success", status_code=200)


# ========== photo（FEAT-008 avatar + FEAT-009 album）==========


class PhotoOut(BaseModel):
    id: str
    purpose: str
    content_type: str
    size_bytes: int
    uploaded_at: int
    moderation_status: str
    url: str | None = None  # 签名 URL（上传时无，list 时填）


def _photo_to_out(p: photo_svc.Photo, url: str | None = None) -> PhotoOut:
    return PhotoOut(
        id=p.id,
        purpose=p.purpose,
        content_type=p.content_type,
        size_bytes=p.size_bytes,
        uploaded_at=p.uploaded_at,
        moderation_status=p.moderation_status,
        url=url,
    )


def _photo_error_to_http(e: photo_svc.PhotoError) -> HTTPException:
    return HTTPException(
        status_code=e.http_status,
        detail={"code": e.code, "message": str(e)},
    )


@app.post("/photo/avatar", response_model=PhotoOut)
async def photo_avatar_upload(
    file: UploadFile = File(...),
    user_id: str = Depends(current_user_id),
) -> PhotoOut:
    data = await file.read()
    try:
        p = photo_svc.upload(
            user_id=user_id,
            data=data,
            content_type=file.content_type or "application/octet-stream",
            purpose="avatar",
        )
    except photo_svc.PhotoError as e:
        raise _photo_error_to_http(e) from e
    return _photo_to_out(p)


@app.post("/photo/album", response_model=PhotoOut)
async def photo_album_add(
    file: UploadFile = File(...),
    user_id: str = Depends(current_user_id),
) -> PhotoOut:
    data = await file.read()
    try:
        p = photo_svc.upload(
            user_id=user_id,
            data=data,
            content_type=file.content_type or "application/octet-stream",
            purpose="album",
        )
    except photo_svc.PhotoError as e:
        raise _photo_error_to_http(e) from e
    return _photo_to_out(p)


@app.get("/photo/album", response_model=list[PhotoOut])
def photo_album_list(user_id: str = Depends(current_user_id)) -> list[PhotoOut]:
    photos = photo_svc.list_album(user_id)
    outs: list[PhotoOut] = []
    for p in photos:
        try:
            url = photo_svc.signed_url(p.id)
        except photo_svc.PhotoError:
            url = None
        outs.append(_photo_to_out(p, url=url))
    return outs


@app.delete("/photo/{photo_id}")
def photo_delete(
    photo_id: str,
    user_id: str = Depends(current_user_id),
) -> dict[str, str]:
    try:
        photo_svc.delete(user_id, photo_id)
    except photo_svc.PhotoError as e:
        raise _photo_error_to_http(e) from e
    return {"status": "deleted"}


@app.get("/photo/avatar/{target_user_id}")
def photo_avatar_of(
    target_user_id: str,
    _user_id: str = Depends(current_user_id),
) -> dict[str, str | None]:
    """查某用户当前头像的签名 URL（query 结果里也会 inline，这是单独接口）。"""
    try:
        url = photo_svc.avatar_url(target_user_id)
    except photo_svc.PhotoError as e:
        raise _photo_error_to_http(e) from e
    return {"url": url}


# ========== admin（FEAT-025/028/029，仅白名单 user 可调）==========


def _admin_error_to_http(e: admin_svc.AdminError) -> HTTPException:
    return HTTPException(status_code=e.http_status, detail={"code": e.code, "message": str(e)})


def current_admin_id(user_id: str = Depends(current_user_id)) -> str:
    """admin endpoints 公用：先验 session，再验白名单。"""
    try:
        admin_svc.require_admin(user_id)
    except admin_svc.AdminError as e:
        raise _admin_error_to_http(e) from e
    return user_id


class AdminBanIn(BaseModel):
    reason: str | None = None
    mode: str = "ban"  # ban | freeze


@app.post("/admin/user/{target_user_id}/ban")
def admin_ban_user(
    target_user_id: str,
    body: AdminBanIn,
    admin_id: str = Depends(current_admin_id),
) -> dict[str, Any]:
    try:
        if body.mode == "freeze":
            s = admin_svc.freeze_user(target_user_id, body.reason, admin_id)
        else:
            s = admin_svc.ban_user(target_user_id, body.reason, admin_id)
    except admin_svc.AdminError as e:
        raise _admin_error_to_http(e) from e
    return {"user_id": s.user_id, "status": s.status, "reason": s.status_reason}


@app.post("/admin/user/{target_user_id}/unban")
def admin_unban_user(
    target_user_id: str,
    admin_id: str = Depends(current_admin_id),
) -> dict[str, Any]:
    try:
        s = admin_svc.unban_user(target_user_id, admin_id)
    except admin_svc.AdminError as e:
        raise _admin_error_to_http(e) from e
    return {"user_id": s.user_id, "status": s.status}


@app.get("/admin/user/{target_user_id}")
def admin_get_user(
    target_user_id: str,
    admin_id: str = Depends(current_admin_id),
) -> dict[str, Any]:
    try:
        s = admin_svc.get_user_status(target_user_id)
    except admin_svc.AdminError as e:
        raise _admin_error_to_http(e) from e
    return {
        "user_id": s.user_id,
        "status": s.status,
        "reason": s.status_reason,
        "updated_at": s.status_updated_at,
    }


@app.get("/admin/users")
def admin_list_users(
    status: str = "banned",
    limit: int = 100,
    admin_id: str = Depends(current_admin_id),
) -> list[dict[str, Any]]:
    try:
        rows = admin_svc.list_users_by_status(status, limit)
    except admin_svc.AdminError as e:
        raise _admin_error_to_http(e) from e
    return [
        {
            "user_id": r.user_id,
            "status": r.status,
            "reason": r.status_reason,
            "updated_at": r.status_updated_at,
        }
        for r in rows
    ]


@app.get("/admin/reports/pending")
def admin_list_pending_reports(
    limit: int = 50,
    admin_id: str = Depends(current_admin_id),
) -> list[dict[str, Any]]:
    rows = admin_svc.list_pending_reports(limit)
    return [
        {
            "id": r.id,
            "message_id": r.message_id,
            "reporter_id": r.reporter_id,
            "reported_id": r.reported_id,
            "reason": r.reason,
            "created_at": r.created_at,
        }
        for r in rows
    ]


class AdminReviewIn(BaseModel):
    action: str  # 'pass' | 'reject' | 'confirm'
    ban_target: bool = False
    ban_reason: str | None = None


@app.post("/admin/reports/{report_id}/review")
def admin_review_report(
    report_id: str,
    body: AdminReviewIn,
    admin_id: str = Depends(current_admin_id),
) -> dict[str, Any]:
    try:
        if body.action == "pass":
            r = admin_svc.pass_report(report_id, admin_id)
        elif body.action == "reject":
            r = admin_svc.reject_report(report_id, admin_id)
        elif body.action == "confirm":
            r = admin_svc.confirm_report(
                report_id, admin_id, ban_target=body.ban_target, ban_reason=body.ban_reason
            )
        else:
            raise HTTPException(
                status_code=400,
                detail={"code": "invalid_action", "message": "action: pass|reject|confirm"},
            )
    except admin_svc.AdminError as e:
        raise _admin_error_to_http(e) from e
    return {
        "id": r.id,
        "review_status": r.review_status,
        "reviewed_by": r.reviewed_by,
        "reviewed_at": r.reviewed_at,
    }


@app.get("/admin/stats")
def admin_stats(admin_id: str = Depends(current_admin_id)) -> dict[str, Any]:
    return admin_svc.get_stats()


def run() -> None:
    import uvicorn

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