"""风控监控读模型：聚合通用行为流水、风险事件与比价记录。"""
from __future__ import annotations

from datetime import UTC, datetime, timedelta

from sqlalchemy import desc, func, select
from sqlalchemy.orm import Session

from app.core.config import settings
from app.models.comparison import ComparisonRecord
from app.models.device import DeviceLiveness
from app.models.risk import BehaviorEvent, RiskIncident
from app.models.user import User
from app.repositories import risk as risk_repo

KIND_TO_RULE = {
    "sms": risk_repo.RULE_SMS_HOURLY,
    "oneclick": risk_repo.RULE_ONECLICK_DAILY,
    "compare": risk_repo.RULE_COMPARE_DAILY,
}
RULE_TO_KIND = {rule: kind for kind, rule in KIND_TO_RULE.items()}


def _day_bounds(now: datetime | None = None) -> tuple[str, datetime, datetime]:
    current = (now or datetime.now(UTC)).astimezone(risk_repo.CN_TZ)
    start = current.replace(hour=0, minute=0, second=0, microsecond=0)
    end = start + timedelta(days=1)
    return (
        start.strftime("%Y-%m-%d"),
        start.astimezone(UTC),
        end.astimezone(UTC),
    )


def _compare_day_bounds(now: datetime | None = None) -> tuple[datetime, datetime]:
    current = (now or datetime.now(UTC)).astimezone(risk_repo.CN_TZ).replace(
        tzinfo=None
    )
    start = current.replace(hour=0, minute=0, second=0, microsecond=0)
    return start, start + timedelta(days=1)


def sync_today_compare_incidents(db: Session, now: datetime | None = None) -> None:
    risk_repo.reconcile_compare_rule(db, at=now)


def reset_open_alerts(
    db: Session, *, admin_id: int, reset_at: datetime
) -> dict[str, int]:
    """将三类待处理报警归零，并从 reset_at 开始重新累计。"""
    rule_codes = tuple(KIND_TO_RULE.values())
    incidents = list(
        db.scalars(
            select(RiskIncident).where(
                RiskIncident.rule_code.in_(rule_codes),
                RiskIncident.status == "open",
            )
        ).all()
    )
    counts = {kind: 0 for kind in KIND_TO_RULE}
    for incident in incidents:
        incident.status = "resolved"
        incident.action_reason = risk_repo.MANUAL_RESET_REASON
        incident.handled_by = admin_id
        incident.handled_at = reset_at
        counts[RULE_TO_KIND[incident.rule_code]] += 1
    risk_repo.reset_rule_baselines(
        db,
        rule_codes=rule_codes,
        reset_at=reset_at,
        admin_id=admin_id,
    )
    return counts


def summary(db: Session, now: datetime | None = None) -> dict:
    date_key, start, end = _day_bounds(now)
    compare_start, compare_end = _compare_day_bounds(now)
    current = now or datetime.now(UTC)
    risk_repo.reconcile_behavior_rule(
        db, rule_code=risk_repo.RULE_SMS_HOURLY, at=current, commit=False
    )
    risk_repo.reconcile_behavior_rule(
        db, rule_code=risk_repo.RULE_ONECLICK_DAILY, at=current, commit=False
    )
    risk_repo.reconcile_compare_rule(db, at=current, commit=False)
    db.commit()
    sms_threshold = risk_repo.get_rule_threshold(db, risk_repo.RULE_SMS_HOURLY)
    oneclick_threshold = risk_repo.get_rule_threshold(
        db, risk_repo.RULE_ONECLICK_DAILY
    )
    compare_threshold = risk_repo.get_rule_threshold(db, risk_repo.RULE_COMPARE_DAILY)

    def _behavior_total(event_type: str, outcomes: tuple[str, ...]) -> int:
        return int(
            db.scalar(
                select(func.count(BehaviorEvent.id)).where(
                    BehaviorEvent.event_type == event_type,
                    BehaviorEvent.outcome.in_(outcomes),
                    BehaviorEvent.occurred_at >= start,
                    BehaviorEvent.occurred_at < end,
                )
            )
            or 0
        )

    compare_total = int(
        db.scalar(
            select(func.count(ComparisonRecord.id)).where(
                ComparisonRecord.created_at >= compare_start,
                ComparisonRecord.created_at < compare_end,
            )
        )
        or 0
    )

    def _open_count(rule_code: str) -> int:
        return int(
            db.scalar(
                select(func.count(RiskIncident.id)).where(
                    RiskIncident.rule_code == rule_code,
                    RiskIncident.status == "open",
                    RiskIncident.window_key.like(f"{date_key}%"),
                )
            )
            or 0
        )

    return {
        "date": date_key,
        "updated_at": now or datetime.now(UTC),
        "cards": [
            {
                "kind": "sms",
                "alert_subject_count": _open_count(risk_repo.RULE_SMS_HOURLY),
                "today_total": _behavior_total(risk_repo.EVENT_SMS_SEND, ("success",)),
                "threshold": sms_threshold,
                "rule_text": f"单设备 1 小时 ≥ {sms_threshold} 次",
            },
            {
                "kind": "oneclick",
                "alert_subject_count": _open_count(risk_repo.RULE_ONECLICK_DAILY),
                "today_total": _behavior_total(
                    risk_repo.EVENT_ONECLICK_LOGIN, ("success", "failed")
                ),
                "threshold": oneclick_threshold,
                "rule_text": f"单设备当日 ≥ {oneclick_threshold} 次",
            },
            {
                "kind": "compare",
                "alert_subject_count": _open_count(risk_repo.RULE_COMPARE_DAILY),
                "today_total": compare_total,
                "threshold": compare_threshold,
                "rule_text": f"单账户当日 ≥ {compare_threshold} 次",
            },
        ],
    }


def _latest_device_model(db: Session, subject_id: str) -> str | None:
    return db.execute(
        select(BehaviorEvent.device_model)
        .where(
            BehaviorEvent.subject_type == "device",
            BehaviorEvent.subject_id == subject_id,
            BehaviorEvent.device_model.is_not(None),
            BehaviorEvent.device_model != "",
        )
        .order_by(BehaviorEvent.occurred_at.desc(), BehaviorEvent.id.desc())
        .limit(1)
    ).scalar_one_or_none()


def _first_used_at(db: Session, subject_id: str) -> datetime | None:
    behavior_first = db.scalar(
        select(func.min(BehaviorEvent.occurred_at)).where(
            BehaviorEvent.subject_type == "device",
            BehaviorEvent.subject_id == subject_id,
        )
    )
    registered_first = db.scalar(
        select(func.min(DeviceLiveness.created_at)).where(
            DeviceLiveness.device_id == subject_id
        )
    )
    observed = [value for value in (behavior_first, registered_first) if value is not None]
    if not observed:
        return None

    def _utc(value: datetime) -> datetime:
        return value.replace(tzinfo=UTC) if value.tzinfo is None else value.astimezone(UTC)

    return min(observed, key=_utc)


def _common_device(db: Session, user_id: int, start: datetime, end: datetime) -> str | None:
    return db.execute(
        select(ComparisonRecord.device_id)
        .where(
            ComparisonRecord.user_id == user_id,
            ComparisonRecord.device_id.is_not(None),
            ComparisonRecord.created_at >= start,
            ComparisonRecord.created_at < end,
        )
        .group_by(ComparisonRecord.device_id)
        .order_by(func.count(ComparisonRecord.id).desc(), ComparisonRecord.device_id.asc())
        .limit(1)
    ).scalar_one_or_none()


def list_incidents(
    db: Session,
    *,
    kind: str,
    status: str = "open",
    limit: int = 20,
    cursor: int = 0,
) -> tuple[list[dict], int | None, int]:
    rule_code = KIND_TO_RULE[kind]
    stmt = select(RiskIncident).where(RiskIncident.rule_code == rule_code)
    count_stmt = select(func.count(RiskIncident.id)).where(
        RiskIncident.rule_code == rule_code
    )
    if status != "all":
        stmt = stmt.where(RiskIncident.status == status)
        count_stmt = count_stmt.where(RiskIncident.status == status)
    total = int(db.scalar(count_stmt) or 0)
    incidents = list(
        db.scalars(
            stmt.order_by(desc(RiskIncident.triggered_at), desc(RiskIncident.id))
            .offset(cursor)
            .limit(limit)
        ).all()
    )

    user_ids = [int(i.subject_id) for i in incidents if i.subject_type == "user"]
    users = {
        u.id: u
        for u in db.scalars(select(User).where(User.id.in_(user_ids))).all()
    } if user_ids else {}

    items: list[dict] = []
    for incident in incidents:
        item = {
            "incident_id": incident.id,
            "kind": kind,
            "subject_type": incident.subject_type,
            "subject_id": incident.subject_id,
            "window_start": incident.window_start,
            "window_end": incident.window_end,
            "first_event_at": incident.first_event_at,
            "triggered_at": incident.triggered_at,
            "last_event_at": incident.last_event_at,
            "event_count": incident.event_count,
            "status": incident.status,
        }
        if incident.subject_type == "device":
            restriction = risk_repo.get_active_restriction(
                db,
                subject_type="device",
                subject_id=incident.subject_id,
                scope=risk_repo.SCOPE_AUTH_DEVICE,
            )
            item["device_model"] = _latest_device_model(db, incident.subject_id)
            item["first_used_at"] = _first_used_at(db, incident.subject_id)
            item["restricted"] = restriction is not None
            item["restriction_id"] = restriction.id if restriction else None
        else:
            uid = int(incident.subject_id)
            user = users.get(uid)
            restriction = risk_repo.get_active_restriction(
                db,
                subject_type="user",
                subject_id=incident.subject_id,
                scope=risk_repo.SCOPE_ECONOMIC_ACCOUNT,
            )
            item.update(
                {
                    "user_id": uid,
                    "phone": user.phone if user else None,
                    "registered_at": user.created_at if user else None,
                    "common_device_id": _common_device(
                        db, uid, incident.window_start, incident.window_end
                    ),
                    "restricted": restriction is not None,
                    "restriction_id": restriction.id if restriction else None,
                }
            )
        items.append(item)
    next_cursor = cursor + len(items) if cursor + len(items) < total else None
    return items, next_cursor, total


def _page_events(
    db: Session,
    incident: RiskIncident,
    *,
    event_type: str,
    outcomes: tuple[str, ...] | None,
    limit: int,
    cursor: int,
) -> tuple[list[BehaviorEvent], int]:
    filters = [
        BehaviorEvent.event_type == event_type,
        BehaviorEvent.subject_type == incident.subject_type,
        BehaviorEvent.subject_id == incident.subject_id,
        BehaviorEvent.occurred_at >= incident.window_start,
        BehaviorEvent.occurred_at < incident.window_end,
    ]
    if outcomes:
        filters.append(BehaviorEvent.outcome.in_(outcomes))
    total = int(db.scalar(select(func.count(BehaviorEvent.id)).where(*filters)) or 0)
    rows = list(
        db.scalars(
            select(BehaviorEvent)
            .where(*filters)
            .order_by(BehaviorEvent.occurred_at.desc(), BehaviorEvent.id.desc())
            .offset(cursor)
            .limit(limit)
        ).all()
    )
    return rows, total


def _user_map(db: Session, user_ids: set[int]) -> dict[int, User]:
    if not user_ids:
        return {}
    return {u.id: u for u in db.scalars(select(User).where(User.id.in_(user_ids))).all()}


def _distinct_event_users(
    db: Session,
    incident: RiskIncident,
    *,
    event_type: str,
    outcomes: tuple[str, ...],
) -> int:
    return int(
        db.scalar(
            select(func.count(func.distinct(BehaviorEvent.user_id))).where(
                BehaviorEvent.event_type == event_type,
                BehaviorEvent.subject_type == incident.subject_type,
                BehaviorEvent.subject_id == incident.subject_id,
                BehaviorEvent.outcome.in_(outcomes),
                BehaviorEvent.user_id.is_not(None),
                BehaviorEvent.occurred_at >= incident.window_start,
                BehaviorEvent.occurred_at < incident.window_end,
            )
        )
        or 0
    )


def _sms_distinct_accounts(db: Session, incident: RiskIncident) -> int:
    """统计整个告警窗口内，确实完成本设备短信验证的不同账号数。"""
    send_rows = db.execute(
        select(BehaviorEvent.phone, BehaviorEvent.occurred_at).where(
            BehaviorEvent.event_type == risk_repo.EVENT_SMS_SEND,
            BehaviorEvent.subject_type == incident.subject_type,
            BehaviorEvent.subject_id == incident.subject_id,
            BehaviorEvent.outcome == "success",
            BehaviorEvent.phone.is_not(None),
            BehaviorEvent.occurred_at >= incident.window_start,
            BehaviorEvent.occurred_at < incident.window_end,
        )
    ).all()
    phones = {phone for phone, _ in send_rows}
    if not phones:
        return 0
    login_rows = db.execute(
        select(
            BehaviorEvent.user_id,
            BehaviorEvent.phone,
            BehaviorEvent.occurred_at,
        )
        .where(
            BehaviorEvent.event_type == risk_repo.EVENT_SMS_LOGIN,
            BehaviorEvent.subject_id == incident.subject_id,
            BehaviorEvent.phone.in_(phones),
            BehaviorEvent.user_id.is_not(None),
            BehaviorEvent.outcome == "success",
            BehaviorEvent.occurred_at >= incident.window_start,
            BehaviorEvent.occurred_at
            < incident.window_end + timedelta(seconds=settings.SMS_CODE_TTL_SEC),
        )
        .order_by(BehaviorEvent.occurred_at.asc(), BehaviorEvent.id.asc())
    ).all()
    matched_users = {
        user_id
        for user_id, phone, login_at in login_rows
        if any(
            sent_phone == phone
            and sent_at <= login_at
            and login_at
            <= sent_at + timedelta(seconds=settings.SMS_CODE_TTL_SEC)
            for sent_phone, sent_at in send_rows
        )
    }
    return len(matched_users)


def incident_details(
    db: Session,
    *,
    incident: RiskIncident,
    kind: str,
    limit: int,
    cursor: int,
) -> dict:
    if kind == "sms":
        rows, total = _page_events(
            db,
            incident,
            event_type=risk_repo.EVENT_SMS_SEND,
            outcomes=("success",),
            limit=limit,
            cursor=cursor,
        )
        phone_values = {r.phone for r in rows if r.phone}
        login_rows = list(
            db.scalars(
                select(BehaviorEvent)
                .where(
                    BehaviorEvent.event_type == risk_repo.EVENT_SMS_LOGIN,
                    BehaviorEvent.subject_id == incident.subject_id,
                    BehaviorEvent.phone.in_(phone_values),
                    BehaviorEvent.outcome == "success",
                    BehaviorEvent.occurred_at >= incident.window_start,
                    BehaviorEvent.occurred_at
                    < incident.window_end + timedelta(seconds=settings.SMS_CODE_TTL_SEC),
                )
                .order_by(BehaviorEvent.occurred_at.asc(), BehaviorEvent.id.asc())
            ).all()
        ) if phone_values else []
        users = _user_map(db, {r.user_id for r in login_rows if r.user_id is not None})
        items = []
        for row in rows:
            match = next(
                (
                    login
                    for login in login_rows
                    if login.phone == row.phone
                    and login.occurred_at >= row.occurred_at
                    and login.occurred_at
                    <= row.occurred_at + timedelta(seconds=settings.SMS_CODE_TTL_SEC)
                ),
                None,
            )
            user = users.get(match.user_id) if match and match.user_id else None
            items.append(
                {
                    "id": row.id,
                    "occurred_at": row.occurred_at,
                    "outcome": row.outcome,
                    "phone": row.phone,
                    "user_id": user.id if user else None,
                    "username": user.username if user else None,
                    "account_registered_at": user.created_at if user else None,
                    "verified": match is not None,
                }
            )
        distinct_accounts = _sms_distinct_accounts(db, incident)
    elif kind == "oneclick":
        rows, total = _page_events(
            db,
            incident,
            event_type=risk_repo.EVENT_ONECLICK_LOGIN,
            outcomes=("success", "failed"),
            limit=limit,
            cursor=cursor,
        )
        users = _user_map(db, {r.user_id for r in rows if r.user_id is not None})
        items = []
        for row in rows:
            user = users.get(row.user_id) if row.user_id else None
            items.append(
                {
                    "id": row.id,
                    "occurred_at": row.occurred_at,
                    "outcome": row.outcome,
                    "phone": row.phone,
                    "user_id": row.user_id,
                    "username": user.username if user else None,
                    "account_registered_at": user.created_at if user else None,
                    "reason": row.reason,
                }
            )
        distinct_accounts = _distinct_event_users(
            db,
            incident,
            event_type=risk_repo.EVENT_ONECLICK_LOGIN,
            outcomes=("success", "failed"),
        )
    else:
        user_id = int(incident.subject_id)
        filters = (
            ComparisonRecord.user_id == user_id,
            ComparisonRecord.created_at >= incident.window_start,
            ComparisonRecord.created_at < incident.window_end,
        )
        total = int(db.scalar(select(func.count(ComparisonRecord.id)).where(*filters)) or 0)
        rows = list(
            db.scalars(
                select(ComparisonRecord)
                .where(*filters)
                .order_by(ComparisonRecord.created_at.desc(), ComparisonRecord.id.desc())
                .offset(cursor)
                .limit(limit)
            ).all()
        )
        items = []
        for row in rows:
            prices = {
                str(result.get("platform_name") or result.get("platform_id") or "未知平台"): (
                    float(result["price"]) if result.get("price") is not None else None
                )
                for result in (row.comparison_results or [])
            }
            items.append(
                {
                    "id": row.id,
                    "occurred_at": row.created_at,
                    "outcome": row.status,
                    "store_name": row.store_name,
                    "product_names": row.product_names,
                    "prices": prices,
                    "saved_amount_yuan": (
                        row.saved_amount_cents / 100
                        if row.saved_amount_cents is not None
                        else None
                    ),
                    "status": row.status,
                }
            )
        distinct_accounts = None
    next_cursor = cursor + len(items) if cursor + len(items) < total else None
    return {
        "kind": kind,
        "incident_id": incident.id,
        "event_count": incident.event_count,
        "distinct_accounts": distinct_accounts,
        "items": items,
        "next_cursor": next_cursor,
        "total": total,
    }
