from __future__ import annotations

import logging
from datetime import datetime
from typing import Any, Optional

from app.features.personalized_reco.observability.types import MissingFields, RecoMeta, Scene
from app.features.personalized_reco.observability.utils import compute_empty_reason, compute_missing_fields

logger = logging.getLogger(__name__)


def _non_negative_int(value: Any, *, default: int = 0) -> int:
    try:
        n = int(value)
    except Exception:
        return int(default)
    return max(0, int(n))


class RecoMetaBuilder:
    """
    在推荐 pipeline 中逐阶段填充 RecoMeta，避免“散落字段/散落日志”。

    说明（V1）：
    - set 调用允许任意顺序；build 时会做防御式兜底与单调性修正
    - 单调性约束：raw >= after_hard_filter >= after_dedup >= after_freqcap >= served_k
    """

    def __init__(self, *, scene: Scene, user_profile: object, k: int, now: Optional[datetime] = None) -> None:
        self.scene: Scene = scene
        self.user_profile = user_profile
        self.k = _non_negative_int(k, default=0)
        self.now = now

        self._raw: Optional[int] = None
        self._after_hard: Optional[int] = None
        self._after_dedup: Optional[int] = None
        self._after_freqcap: Optional[int] = None
        self._served_k: Optional[int] = None
        self._fallback_level_final: Optional[int] = None

        self._risk_filtered_count_by_flag: dict[str, int] = {}
        self._freqcap_filtered_counts: dict[str, int] = {}
        self._config_snapshot: dict[str, Any] = {}

    def set_candidate_pool_size_raw(self, n: Any) -> "RecoMetaBuilder":
        self._raw = _non_negative_int(n)
        return self

    def set_after_hard_filter(self, n: Any, *, risk_filtered_count_by_flag: Optional[dict[str, Any]] = None) -> "RecoMetaBuilder":
        self._after_hard = _non_negative_int(n)
        if risk_filtered_count_by_flag:
            self._risk_filtered_count_by_flag = {str(k): _non_negative_int(v) for k, v in risk_filtered_count_by_flag.items()}
        return self

    def set_after_dedup(self, n: Any) -> "RecoMetaBuilder":
        self._after_dedup = _non_negative_int(n)
        return self

    def set_after_freqcap(self, n: Any, *, freqcap_filtered_counts: Optional[dict[str, Any]] = None) -> "RecoMetaBuilder":
        self._after_freqcap = _non_negative_int(n)
        if freqcap_filtered_counts:
            self._freqcap_filtered_counts = {str(k): _non_negative_int(v) for k, v in freqcap_filtered_counts.items()}
        return self

    def set_fallback_level_final(self, level: Any, *, reason: Optional[str] = None) -> "RecoMetaBuilder":
        # reason 预留，V1 先不入 meta（可放入 config_snapshot 或后续字段）
        self._fallback_level_final = _non_negative_int(level, default=0)
        if reason:
            self._config_snapshot.setdefault("fallback_trigger_reason", str(reason))
        return self

    def set_served_k(self, n: Any) -> "RecoMetaBuilder":
        self._served_k = _non_negative_int(n)
        return self

    def set_config_snapshot(self, snapshot: dict[str, Any]) -> "RecoMetaBuilder":
        self._config_snapshot = dict(snapshot or {})
        return self

    def build(self) -> RecoMeta:
        missing: MissingFields = compute_missing_fields(self.user_profile)
        conf_u = getattr(self.user_profile, "profile_confidence", 1.0)
        try:
            conf_u_f = float(conf_u)
        except Exception:
            conf_u_f = 1.0
        if conf_u_f != conf_u_f:
            conf_u_f = 1.0

        raw = self._raw if self._raw is not None else 0
        after_hard = self._after_hard if self._after_hard is not None else raw
        after_dedup = self._after_dedup if self._after_dedup is not None else after_hard
        after_freqcap = self._after_freqcap if self._after_freqcap is not None else after_dedup
        served_k = self._served_k if self._served_k is not None else 0

        # 防御式单调性修正（以最保守值输出）
        if after_hard > raw:
            logger.debug("after_hard_filter(%s) > raw(%s)，已修正为 raw", after_hard, raw)
            after_hard = raw
        if after_dedup > after_hard:
            logger.debug("after_dedup(%s) > after_hard_filter(%s)，已修正为 after_hard_filter", after_dedup, after_hard)
            after_dedup = after_hard
        if after_freqcap > after_dedup:
            logger.debug("after_freqcap(%s) > after_dedup(%s)，已修正为 after_dedup", after_freqcap, after_dedup)
            after_freqcap = after_dedup
        if served_k > after_freqcap:
            logger.debug("served_k(%s) > after_freqcap(%s)，已修正为 after_freqcap", served_k, after_freqcap)
            served_k = after_freqcap

        fallback_level_final = self._fallback_level_final if self._fallback_level_final is not None else 0

        empty_reason = compute_empty_reason(
            served_k=served_k,
            candidate_pool_size_raw=raw,
            candidate_pool_size_after_hard_filter=after_hard,
            candidate_pool_size_after_freqcap=after_freqcap,
        )

        return RecoMeta(
            scene=self.scene,
            candidate_pool_size_raw=int(raw),
            candidate_pool_size_after_hard_filter=int(after_hard),
            candidate_pool_size_after_dedup=int(after_dedup),
            candidate_pool_size_after_freqcap=int(after_freqcap),
            fallback_level_final=int(fallback_level_final),
            served_k=int(served_k),
            empty_reason=empty_reason,
            conf_U=float(conf_u_f),
            missing_fields=missing,
            risk_filtered_count_by_flag=dict(self._risk_filtered_count_by_flag),
            freqcap_filtered_counts=dict(self._freqcap_filtered_counts),
            config_snapshot=dict(self._config_snapshot),
        )

