from __future__ import annotations

from datetime import datetime
from typing import Optional

from app.features.personalized_reco.content_repository.types import ContentProfileDTO
from app.features.personalized_reco.scoring.defaults import get_default_config
from app.features.personalized_reco.scoring.types import ExternalTerms, Scene, ScoreBreakdown, ScoreConfig, ScoreResult
from app.features.personalized_reco.scoring.utils import as_finite_float, clamp, pick_one_hot_key
from app.features.user_profile_scoring.types import UserProfileV1_2


def _missing_fields(user_profile: UserProfileV1_2) -> list[str]:
    missing: list[str] = []
    if not user_profile.need:
        missing.append("need")
    if not user_profile.context:
        missing.append("context")
    if user_profile.emotion_score is None:
        missing.append("emotion")
    return missing


def _score_need(user_profile: UserProfileV1_2, content: ContentProfileDTO) -> float:
    key = pick_one_hot_key(user_profile.need)  # type: ignore[arg-type]
    if key is None:
        return 0.5
    raw = content.need_suitability.get(key, 0.5)
    return clamp(as_finite_float(raw, default=0.5), 0.0, 1.0)


def _score_context(user_profile: UserProfileV1_2, content: ContentProfileDTO) -> float:
    key = pick_one_hot_key(user_profile.context)  # type: ignore[arg-type]
    if key is None:
        return 0.5
    raw = content.context_suitability.get(key, 0.5)
    return clamp(as_finite_float(raw, default=0.5), 0.0, 1.0)


def _score_emotion(user_profile: UserProfileV1_2, content: ContentProfileDTO) -> float:
    # V1.2：用户情绪缺失 -> 0.8
    if user_profile.emotion_score is None:
        return 0.8

    # 文案 general（emotion_score=None）-> 0.8
    if content.emotion_score is None:
        return 0.8

    u = clamp(as_finite_float(user_profile.emotion_score, default=0.8), 0.0, 1.0)
    c = clamp(as_finite_float(content.emotion_score, default=0.8), 0.0, 1.0)
    return clamp(1.0 - abs(u - c), 0.0, 1.0)


def _user_stage_key(user_profile: UserProfileV1_2) -> str:
    # 约定：UserStageOneHot.unknown 必填；但这里仍做防御
    stage = user_profile.stage
    if getattr(stage, "expecting", 0) == 1:
        return "expecting"
    if getattr(stage, "parenting", 0) == 1:
        return "parenting"
    if getattr(stage, "unknown", 1) == 1:
        return "unknown"
    return "unknown"


def _score_stage(user_profile: UserProfileV1_2, content: ContentProfileDTO) -> float:
    # 对齐算法规则：
    # - general=1；命中=1；unknown对非unknown=0.7；其余=0
    if content.stage == "general":
        return 1.0

    u_stage = _user_stage_key(user_profile)
    if content.stage == u_stage:
        return 1.0

    if u_stage == "unknown" and content.stage != "unknown":
        return 0.7

    return 0.0


def _score_personal(alpha: float, personalization_power: float, s_need: float, s_context: float) -> float:
    power = clamp(as_finite_float(personalization_power, default=0.0), 0.0, 1.0)
    a = as_finite_float(alpha, default=0.0)
    return float(a) * float(power) * max(float(s_need), float(s_context))


def _penalty_uncertainty(beta: float, user_profile: UserProfileV1_2, content: ContentProfileDTO) -> float:
    b = as_finite_float(beta, default=0.0)
    power = clamp(as_finite_float(content.personalization_power, default=0.0), 0.0, 1.0)

    # V1 约定：conf_U 缺失时按 1.0（避免过惩罚）
    conf_u = clamp(as_finite_float(getattr(user_profile, "profile_confidence", 1.0), default=1.0), 0.0, 1.0)
    conf_c = clamp(as_finite_float(getattr(content, "review_confidence", 0.7), default=0.7), 0.0, 1.0)

    return float(b) * (1.0 - float(conf_u)) * (1.0 - float(conf_c)) * float(power)


def _widget_emotion_penalty(scene: Scene, content: ContentProfileDTO, config: ScoreConfig) -> float:
    if scene != "widget":
        return 0.0
    if content.emotion_score is None:
        return 0.0

    lo, hi = config.widget_emotion_soft_range
    lo_f = as_finite_float(lo, default=0.4)
    hi_f = as_finite_float(hi, default=0.8)
    width = hi_f - lo_f
    if width <= 0:
        return 0.0

    e = clamp(as_finite_float(content.emotion_score, default=0.6), 0.0, 1.0)
    if e < lo_f:
        d = lo_f - e
    elif e > hi_f:
        d = e - hi_f
    else:
        d = 0.0

    gamma = as_finite_float(config.widget_emotion_penalty_gamma, default=0.25)
    raw = float(gamma) * float(d) / float(width)
    return clamp(raw, 0.0, float(gamma))


def score_content(
    *,
    scene: Scene,
    user_profile: UserProfileV1_2,
    content_profile: ContentProfileDTO,
    config: Optional[ScoreConfig] = None,
    pass_filters: bool = True,
    external_terms: Optional[ExternalTerms] = None,
    now: Optional[datetime] = None,  # 预留：V1 不使用
) -> ScoreResult:
    """
    主入口：对单条内容 Cᵢ 进行软打分，返回 final_score 与 breakdown。

    说明（V1）：
    - `pass_filters` 来自 Hard Filter（本模块不做硬过滤）
    - `external_terms` 可注入 S_fresh / P_fatigue / P_repeat / P_risk（缺省按 0）
    - `now` 预留给未来的 freshness/时间衰减（V1 不实现）
    """

    cfg = config or get_default_config(scene)
    ext = external_terms or ExternalTerms()

    missing = _missing_fields(user_profile)

    s_need = _score_need(user_profile, content_profile)
    s_context = _score_context(user_profile, content_profile)
    s_emotion = _score_emotion(user_profile, content_profile)
    s_stage = _score_stage(user_profile, content_profile)

    w_need = as_finite_float(cfg.w_need, default=0.0)
    w_emotion = as_finite_float(cfg.w_emotion, default=0.0)
    w_stage = as_finite_float(cfg.w_stage, default=0.0)
    w_context = as_finite_float(cfg.w_context, default=0.0)

    s_core = float(w_need) * s_need + float(w_emotion) * s_emotion + float(w_stage) * s_stage + float(w_context) * s_context

    s_personal = _score_personal(cfg.alpha, content_profile.personalization_power, s_need, s_context)

    p_uncertainty = 0.0
    if cfg.enable_uncertainty_penalty:
        p_uncertainty = _penalty_uncertainty(cfg.beta, user_profile, content_profile)

    p_widget = _widget_emotion_penalty(scene, content_profile, cfg)

    s_fresh = as_finite_float(ext.S_fresh, default=0.0)
    p_fatigue = as_finite_float(ext.P_fatigue, default=0.0)
    p_repeat = as_finite_float(ext.P_repeat, default=0.0)
    p_risk_external = as_finite_float(ext.P_risk, default=0.0)

    # Widget 软降权并入 P_risk（但在 breakdown 中单独暴露，便于打点）
    p_risk = float(p_risk_external) + float(p_widget)

    raw_final = s_core + s_personal + float(s_fresh) - float(p_fatigue) - float(p_repeat) - float(p_risk) - float(p_uncertainty)
    final_score = float(raw_final) if pass_filters else 0.0

    breakdown = ScoreBreakdown(
        scene=scene,
        **{
            "pass": bool(pass_filters),
        },
        missing_fields=missing,
        S_need=float(s_need),
        S_context=float(s_context),
        S_stage=float(s_stage),
        S_emotion=float(s_emotion),
        S_core=float(s_core),
        S_personal=float(s_personal),
        S_fresh=float(s_fresh),
        P_fatigue=float(p_fatigue),
        P_repeat=float(p_repeat),
        P_risk=float(p_risk),
        P_uncertainty=float(p_uncertainty),
        P_widget_emotion_out_of_range=float(p_widget),
    )

    return ScoreResult(final_score=float(final_score), breakdown=breakdown)

