"""风控监控：统计、风险事件列表/详情及忽略、封禁、解封。"""
from __future__ import annotations

from typing import Annotated, Literal

from fastapi import APIRouter, Depends, HTTPException, Query, Request, status

from app.admin.audit import write_audit
from app.admin.deps import (
    AdminDb,
    get_client_ip,
    require_page,
    require_role,
)
from app.admin.repositories import risk_monitor as repo
from app.admin.schemas.risk_monitor import (
    RestrictionRevokeResponse,
    RiskActionRequest,
    RiskActionResponse,
    RiskDetailPage,
    RiskIncidentPage,
    RiskMonitorSummary,
    RiskResetResponse,
    RiskRuleConfig,
)
from app.core.config_schema import (
    RISK_COMPARE_DAILY_THRESHOLD_KEY,
    RISK_ONECLICK_DAILY_THRESHOLD_KEY,
    RISK_SMS_HOURLY_THRESHOLD_KEY,
)
from app.models.risk import RiskIncident, SubjectRestriction
from app.repositories import app_config
from app.repositories import risk as risk_repo

router = APIRouter(
    prefix="/admin/api/risk-monitor",
    tags=["admin-risk-monitor"],
    dependencies=[Depends(require_page("risk-monitor"))],
)

RiskKind = Literal["sms", "oneclick", "compare"]


def _rule_config(db) -> RiskRuleConfig:
    return RiskRuleConfig(
        sms_hourly_threshold=risk_repo.get_rule_threshold(
            db, risk_repo.RULE_SMS_HOURLY
        ),
        oneclick_daily_threshold=risk_repo.get_rule_threshold(
            db, risk_repo.RULE_ONECLICK_DAILY
        ),
        compare_daily_threshold=risk_repo.get_rule_threshold(
            db, risk_repo.RULE_COMPARE_DAILY
        ),
    )


def _incident_or_404(db, incident_id: int, kind: RiskKind | None = None) -> RiskIncident:
    incident = db.get(RiskIncident, incident_id)
    if incident is None:
        raise HTTPException(status_code=404, detail="风险事件不存在")
    if kind and incident.rule_code != repo.KIND_TO_RULE[kind]:
        raise HTTPException(status_code=404, detail="风险事件类型不匹配")
    return incident


@router.get("/summary", response_model=RiskMonitorSummary, summary="风控监控顶部卡片")
def get_summary(db: AdminDb) -> RiskMonitorSummary:
    return RiskMonitorSummary(**repo.summary(db))


@router.get("/rules", response_model=RiskRuleConfig, summary="读取风控规则阈值")
def get_rules(db: AdminDb) -> RiskRuleConfig:
    return _rule_config(db)


@router.patch("/rules", response_model=RiskRuleConfig, summary="修改风控规则并即时重算")
def update_rules(
    body: RiskRuleConfig,
    request: Request,
    admin: Annotated[object, Depends(require_role("operator", "tech"))],
    db: AdminDb,
) -> RiskRuleConfig:
    before = _rule_config(db).model_dump()
    after = body.model_dump()
    values = (
        (RISK_SMS_HOURLY_THRESHOLD_KEY, body.sms_hourly_threshold),
        (RISK_ONECLICK_DAILY_THRESHOLD_KEY, body.oneclick_daily_threshold),
        (RISK_COMPARE_DAILY_THRESHOLD_KEY, body.compare_daily_threshold),
    )
    for key, value in values:
        app_config.set_value(
            db, key, value, admin_id=admin.id, commit=False
        )

    now = risk_repo.utcnow()
    risk_repo.reconcile_behavior_rule(
        db, rule_code=risk_repo.RULE_SMS_HOURLY, at=now, commit=False
    )
    risk_repo.reconcile_behavior_rule(
        db, rule_code=risk_repo.RULE_ONECLICK_DAILY, at=now, commit=False
    )
    risk_repo.reconcile_compare_rule(db, at=now, commit=False)
    write_audit(
        db,
        admin,
        action="risk.rules.update",
        target_type="risk_rule",
        target_id="all",
        detail={"before": before, "after": after},
        ip=get_client_ip(request),
        commit=False,
    )
    db.commit()
    return _rule_config(db)


@router.post(
    "/reset",
    response_model=RiskResetResponse,
    summary="重置三类待处理报警并从零重新累计",
)
def reset_alerts(
    request: Request,
    admin: Annotated[object, Depends(require_role("operator", "tech"))],
    db: AdminDb,
) -> RiskResetResponse:
    reset_at = risk_repo.utcnow()
    counts = repo.reset_open_alerts(db, admin_id=admin.id, reset_at=reset_at)
    total = sum(counts.values())
    write_audit(
        db,
        admin,
        action="risk.alerts.reset",
        target_type="risk_monitor",
        target_id="all",
        detail={
            "reset_at": reset_at.isoformat(),
            "reset_incident_count": total,
            "reset_counts": counts,
            "restrictions_changed": False,
        },
        ip=get_client_ip(request),
        commit=False,
    )
    db.commit()
    return RiskResetResponse(
        reset_at=reset_at,
        reset_incident_count=total,
        reset_counts=counts,
    )


@router.get(
    "/incidents/{kind}",
    response_model=RiskIncidentPage,
    summary="按短信/一键登录/比价读取风险事件",
)
def get_incidents(
    kind: RiskKind,
    db: AdminDb,
    incident_status: Annotated[
        Literal["open", "ignored", "blocked", "resolved", "all"], Query(alias="status")
    ] = "open",
    limit: Annotated[int, Query(ge=1, le=100)] = 20,
    cursor: Annotated[int, Query(ge=0)] = 0,
) -> RiskIncidentPage:
    if kind == "compare":
        repo.sync_today_compare_incidents(db)
    items, next_cursor, total = repo.list_incidents(
        db, kind=kind, status=incident_status, limit=limit, cursor=cursor
    )
    return RiskIncidentPage(items=items, next_cursor=next_cursor, total=total)


@router.get(
    "/incidents/{kind}/{incident_id}/details",
    response_model=RiskDetailPage,
    summary="风险事件当期事实明细",
)
def get_incident_details(
    kind: RiskKind,
    incident_id: int,
    db: AdminDb,
    limit: Annotated[int, Query(ge=1, le=100)] = 20,
    cursor: Annotated[int, Query(ge=0)] = 0,
) -> RiskDetailPage:
    incident = _incident_or_404(db, incident_id, kind)
    return RiskDetailPage(
        **repo.incident_details(
            db, incident=incident, kind=kind, limit=limit, cursor=cursor
        )
    )


@router.post(
    "/incidents/{incident_id}/ignore",
    response_model=RiskActionResponse,
    summary="忽略风险事件",
)
def ignore_incident(
    incident_id: int,
    body: RiskActionRequest,
    request: Request,
    admin: Annotated[object, Depends(require_role("operator", "tech"))],
    db: AdminDb,
) -> RiskActionResponse:
    incident = _incident_or_404(db, incident_id)
    if incident.status != "open":
        raise HTTPException(status_code=409, detail="该风险事件已处理")
    incident.status = "ignored"
    incident.action_reason = body.reason.strip() or "管理员确认忽略"
    incident.handled_by = admin.id
    incident.handled_at = risk_repo.utcnow()
    write_audit(
        db,
        admin,
        action="risk.incident.ignore",
        target_type="risk_incident",
        target_id=incident.id,
        detail={
            "rule_code": incident.rule_code,
            "subject_type": incident.subject_type,
            "subject_id": incident.subject_id,
            "reason": incident.action_reason,
        },
        ip=get_client_ip(request),
        commit=False,
    )
    db.commit()
    return RiskActionResponse(incident_id=incident.id, status=incident.status)


@router.post(
    "/incidents/{incident_id}/block",
    response_model=RiskActionResponse,
    summary="封禁风险主体",
)
def block_incident(
    incident_id: int,
    body: RiskActionRequest,
    request: Request,
    admin: Annotated[object, Depends(require_role("operator", "tech"))],
    db: AdminDb,
) -> RiskActionResponse:
    incident = _incident_or_404(db, incident_id)
    if incident.status not in ("open", "blocked"):
        raise HTTPException(status_code=409, detail="该风险事件已处理")
    if incident.subject_type == "device":
        if incident.subject_id.startswith("legacy-ip:"):
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="旧客户端未上报设备ID，不能执行设备封禁",
            )
        scope = risk_repo.SCOPE_AUTH_DEVICE
    elif incident.subject_type == "user":
        scope = risk_repo.SCOPE_ECONOMIC_ACCOUNT
    else:  # pragma: no cover - 当前三条规则只会生成 device/user
        raise HTTPException(status_code=400, detail="该主体类型暂不支持封禁")

    reason = body.reason.strip() or "风控规则命中"
    restriction = risk_repo.activate_restriction(
        db,
        subject_type=incident.subject_type,
        subject_id=incident.subject_id,
        scope=scope,
        reason=reason,
        incident_id=incident.id,
        admin_id=admin.id,
    )
    incident.status = "blocked"
    incident.action_reason = reason
    incident.handled_by = admin.id
    incident.handled_at = risk_repo.utcnow()
    write_audit(
        db,
        admin,
        action="risk.subject.block",
        target_type=incident.subject_type,
        target_id=incident.subject_id,
        detail={
            "incident_id": incident.id,
            "rule_code": incident.rule_code,
            "scope": scope,
            "reason": reason,
        },
        ip=get_client_ip(request),
        commit=False,
    )
    db.commit()
    return RiskActionResponse(
        incident_id=incident.id,
        status=incident.status,
        restriction_id=restriction.id,
    )


@router.post(
    "/restrictions/{restriction_id}/revoke",
    response_model=RestrictionRevokeResponse,
    summary="解除主体限制",
)
def revoke_restriction(
    restriction_id: int,
    request: Request,
    admin: Annotated[object, Depends(require_role("operator", "tech"))],
    db: AdminDb,
) -> RestrictionRevokeResponse:
    restriction = db.get(SubjectRestriction, restriction_id)
    if restriction is None:
        raise HTTPException(status_code=404, detail="限制记录不存在")
    if restriction.active:
        risk_repo.revoke_restriction(db, restriction=restriction, admin_id=admin.id)
        if restriction.incident_id:
            incident = db.get(RiskIncident, restriction.incident_id)
            if incident and incident.status == "blocked":
                incident.status = "resolved"
                incident.handled_by = admin.id
                incident.handled_at = risk_repo.utcnow()
        write_audit(
            db,
            admin,
            action="risk.subject.unblock",
            target_type=restriction.subject_type,
            target_id=restriction.subject_id,
            detail={"restriction_id": restriction.id, "scope": restriction.scope},
            ip=get_client_ip(request),
            commit=False,
        )
        db.commit()
    return RestrictionRevokeResponse(restriction_id=restriction.id)
