"""Versioned three-day policy with a latest-day creative stop supplement."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Iterable

import numpy as np
import pandas as pd

from .fission_multiplier import FissionMultiplierParameters
from .metrics import (
    ENTITY_GZH,
    ENTITY_SELF,
    ENTITY_SELF_AD,
    GZH_CHANNEL,
    SELF_CHANNEL,
    compute_roi_summary,
)


POLICY_VERSION = "roi_policy_v15"
POLICY_RUN_SUFFIX = "p15"

STOP_RULE_THREE_DAY = "三日持续低ROI"
STOP_RULE_ONE_DAY = "单日绝对低ROI且P10"


@dataclass(frozen=True)
class RuleConfig:
    self_stop_min_age: int = 4
    self_up_min_age: int = 3
    self_min_daily_uv: float = 200
    partner_min_daily_uv: float = 200
    observe_min_latest_uv: float = 200
    one_day_min_uv: float = 200
    one_day_hard_stop_roi: float = 0.20
    one_day_stop_quantile: float = 0.10
    self_stop_cost_soft_lower_ratio: float = 0.045
    self_stop_cost_target_ratio: float = 0.05
    self_stop_cost_soft_upper_ratio: float = 0.055
    self_stop_cost_hard_cap_ratio: float = 0.06
    self_stop_three_day_share: float = 0.60
    self_stop_one_day_share: float = 0.40
    stop_quantile: float = 0.20
    up_quantile: float = 0.80


def _daily_sample_mask(
    summary: pd.DataFrame,
    entity_type: str,
    dt: str,
    config: RuleConfig,
) -> pd.Series:
    min_uv = (
        config.partner_min_daily_uv
        if entity_type == ENTITY_GZH
        else config.self_min_daily_uv
    )
    return (
        summary["entity_type"].eq(entity_type)
        & pd.to_numeric(summary[f"首层UV_{dt}"], errors="coerce").gt(min_uv)
        & pd.to_numeric(summary[f"成本_{dt}"], errors="coerce").gt(0)
        & np.isfinite(pd.to_numeric(summary[f"ROI_{dt}"], errors="coerce"))
    )


def entity_eligibility_mask(
    summary: pd.DataFrame,
    entity_type: str,
    config: RuleConfig,
    expected_dates: list[str],
) -> pd.Series:
    eligible = summary["entity_type"].eq(entity_type)
    for dt in expected_dates:
        eligible &= _daily_sample_mask(summary, entity_type, dt, config)
    return eligible


def threshold_eligibility_mask(
    summary: pd.DataFrame,
    config: RuleConfig,
    expected_dates: list[str],
) -> pd.Series:
    """Return formal creative-level samples for channel-independent lines."""

    return entity_eligibility_mask(
        summary, ENTITY_SELF, config, expected_dates
    ) | entity_eligibility_mask(summary, ENTITY_GZH, config, expected_dates)


def report_formal_mask(
    summary: pd.DataFrame,
    config: RuleConfig,
    expected_dates: list[str],
) -> pd.Series:
    return threshold_eligibility_mask(
        summary, config, expected_dates
    ) | entity_eligibility_mask(summary, ENTITY_SELF_AD, config, expected_dates)


def observation_mask(
    summary: pd.DataFrame,
    config: RuleConfig,
    expected_dates: list[str],
) -> pd.Series:
    latest = expected_dates[-1]
    formal = report_formal_mask(summary, config, expected_dates)
    return (
        summary["entity_type"].isin([ENTITY_SELF, ENTITY_SELF_AD, ENTITY_GZH])
        & ~formal
        & pd.to_numeric(summary[f"首层UV_{latest}"], errors="coerce").gt(
            config.observe_min_latest_uv
        )
    )


def one_day_supplement_mask(
    summary: pd.DataFrame,
    config: RuleConfig,
    expected_dates: list[str],
) -> pd.Series:
    latest = expected_dates[-1]
    latest_roi = pd.to_numeric(summary[f"ROI_{latest}"], errors="coerce")
    formal_creative = entity_eligibility_mask(
        summary, ENTITY_SELF, config, expected_dates
    )
    return (
        summary["entity_type"].eq(ENTITY_SELF)
        & ~formal_creative
        & pd.to_numeric(summary[f"首层UV_{latest}"], errors="coerce").gt(
            config.one_day_min_uv
        )
        & pd.to_numeric(summary[f"成本_{latest}"], errors="coerce").gt(0)
        & np.isfinite(latest_roi)
    )


def compute_channel_thresholds(
    summary: pd.DataFrame,
    expected_dates: list[str],
    config: RuleConfig,
) -> pd.DataFrame:
    self_eligible = entity_eligibility_mask(
        summary, ENTITY_SELF, config, expected_dates
    )
    gzh_eligible = entity_eligibility_mask(
        summary, ENTITY_GZH, config, expected_dates
    )
    self_sample = pd.to_numeric(
        summary.loc[self_eligible, "ROI"], errors="coerce"
    )
    self_sample = self_sample[np.isfinite(self_sample)]
    gzh_sample = pd.to_numeric(
        summary.loc[gzh_eligible, "ROI"], errors="coerce"
    )
    gzh_sample = gzh_sample[np.isfinite(gzh_sample)]
    if self_sample.empty and gzh_sample.empty:
        raise ValueError("最近三日没有满足每日UV和成本门槛的渠道独立阈值样本")

    t_up = (
        float(self_sample.quantile(config.up_quantile))
        if not self_sample.empty
        else np.nan
    )
    gzh_stop = (
        float(gzh_sample.quantile(config.stop_quantile))
        if not gzh_sample.empty
        else np.nan
    )
    latest = expected_dates[-1]
    return pd.DataFrame(
        [
            {
                "统计窗口": f"{expected_dates[0]} 至 {latest}",
                "entity_type": ENTITY_SELF,
                "渠道": SELF_CHANNEL,
                "t_stop": np.nan,
                "t_up": t_up,
                "t_one_day_stop": np.nan,
                "关停线分位点": np.nan,
                "单日实际关停线分位点": np.nan,
                "阈值样本数": int(len(self_sample)),
                "扩量样本数": int(len(self_sample)),
                "单日候选池样本数": 0,
                "单日硬关停ROI": config.one_day_hard_stop_roi,
                "单日P10线": np.nan,
                "单日合并资格线": np.nan,
                "单日最低UV": config.one_day_min_uv,
                "关停线口径": "T-1实际成本5%软预算动态线",
                "扩量线口径": (
                    f"合格小程序创意实体等权P{int(config.up_quantile * 100)}"
                ),
                "广告级是否入池": "否_仅复用小程序三日动态关停线",
            },
            {
                "统计窗口": f"{expected_dates[0]} 至 {latest}",
                "entity_type": ENTITY_GZH,
                "渠道": GZH_CHANNEL,
                "t_stop": gzh_stop,
                "t_up": np.nan,
                "t_one_day_stop": np.nan,
                "关停线分位点": (
                    config.stop_quantile if np.isfinite(gzh_stop) else np.nan
                ),
                "单日实际关停线分位点": np.nan,
                "阈值样本数": int(len(gzh_sample)),
                "扩量样本数": 0,
                "单日候选池样本数": 0,
                "单日硬关停ROI": np.nan,
                "单日P10线": np.nan,
                "单日合并资格线": np.nan,
                "单日最低UV": np.nan,
                "关停线口径": (
                    f"公众号合格实体等权P{int(config.stop_quantile * 100)}"
                ),
                "扩量线口径": "不适用",
                "广告级是否入池": "不适用",
            },
        ]
    )


def _sample_percentile(values: pd.Series, target: float) -> float:
    clean = pd.to_numeric(values, errors="coerce")
    clean = clean[np.isfinite(clean)]
    if clean.empty or not np.isfinite(target):
        return np.nan
    return float((clean <= target).mean())


def _ordered_candidate_indices(
    result: pd.DataFrame,
    mask: pd.Series,
    roi_column: str,
    cost_column: str,
) -> list[object]:
    roi = pd.to_numeric(result[roi_column], errors="coerce")
    cost = pd.to_numeric(result[cost_column], errors="coerce")
    eligible = mask & np.isfinite(roi) & cost.gt(0)
    return result.loc[eligible].assign(_decision_roi=roi.loc[eligible]).sort_values(
        "_decision_roi",
        ascending=True,
        kind="stable",
    ).index.tolist()


def _select_prefix_within_budget(
    result: pd.DataFrame,
    ordered: list[object],
    cost_column: str,
    budget: float,
) -> list[object]:
    selected: list[object] = []
    cost = 0.0
    for index in ordered:
        candidate_cost = float(result.at[index, cost_column])
        if cost + candidate_cost > budget + 1e-9:
            break
        selected.append(index)
        cost += candidate_cost
    return selected


def _selection_line(
    result: pd.DataFrame,
    ordered: list[object],
    selected: set[object],
    roi_column: str,
) -> tuple[float, float]:
    selected_indices = [index for index in ordered if index in selected]
    if not selected_indices:
        return np.nan, np.nan
    line = float(
        pd.to_numeric(
            result.loc[selected_indices, roi_column], errors="coerce"
        ).max()
    )
    sample = pd.to_numeric(result.loc[ordered, roi_column], errors="coerce")
    return line, _sample_percentile(sample, line)


def _allocate_self_stop_budget(
    result: pd.DataFrame,
    config: RuleConfig,
    expected_dates: list[str],
    formal_creative: pd.Series,
    one_day_supplement: pd.Series,
) -> tuple[pd.Series, pd.Series, dict[str, float | str]]:
    latest = expected_dates[-1]
    cost_column = f"成本_{latest}"
    latest_roi_column = f"ROI_{latest}"
    latest_cost = pd.to_numeric(result[cost_column], errors="coerce").fillna(0.0)
    latest_roi = pd.to_numeric(result[latest_roi_column], errors="coerce")
    age = pd.to_numeric(result.get("广告age"), errors="coerce").fillna(0)
    actionable_age = age.ge(config.self_stop_min_age)

    three_day_mask = formal_creative & actionable_age
    one_day_pool = one_day_supplement & actionable_age
    one_day_sample = latest_roi.loc[one_day_pool]
    one_day_sample = one_day_sample[np.isfinite(one_day_sample)]
    one_day_p10_line = (
        float(one_day_sample.quantile(config.one_day_stop_quantile))
        if not one_day_sample.empty
        else np.nan
    )
    one_day_eligibility_line = (
        min(config.one_day_hard_stop_roi, one_day_p10_line)
        if np.isfinite(one_day_p10_line)
        else np.nan
    )
    one_day_candidate = (
        one_day_pool
        & latest_roi.le(config.one_day_hard_stop_roi)
        & latest_roi.le(one_day_p10_line)
    )
    result["单日ROI排名百分位"] = np.nan
    if not one_day_sample.empty:
        result.loc[one_day_pool, "单日ROI排名百分位"] = latest_roi.loc[
            one_day_pool
        ].apply(lambda value: _sample_percentile(one_day_sample, float(value)))
    result["单日P10线"] = one_day_p10_line
    result["单日合并资格线"] = one_day_eligibility_line
    masks = {
        STOP_RULE_THREE_DAY: three_day_mask,
        STOP_RULE_ONE_DAY: one_day_candidate,
    }
    roi_columns = {
        STOP_RULE_THREE_DAY: "ROI",
        STOP_RULE_ONE_DAY: latest_roi_column,
    }
    shares = {
        STOP_RULE_THREE_DAY: config.self_stop_three_day_share,
        STOP_RULE_ONE_DAY: config.self_stop_one_day_share,
    }
    all_self = result["entity_type"].eq(ENTITY_SELF)
    total_cost = float(latest_cost.loc[all_self & latest_cost.gt(0)].sum())
    target_cost = total_cost * config.self_stop_cost_target_ratio
    soft_lower_cost = total_cost * config.self_stop_cost_soft_lower_ratio
    soft_upper_cost = total_cost * config.self_stop_cost_soft_upper_ratio
    hard_cap_cost = total_cost * config.self_stop_cost_hard_cap_ratio

    ordered = {
        rule: _ordered_candidate_indices(
            result,
            mask,
            roi_columns[rule],
            cost_column,
        )
        for rule, mask in masks.items()
    }
    selected: set[object] = set()
    for rule in (STOP_RULE_THREE_DAY, STOP_RULE_ONE_DAY):
        selected.update(
            _select_prefix_within_budget(
                result,
                ordered[rule],
                cost_column,
                target_cost * shares[rule],
            )
        )

    selected_cost = float(latest_cost.loc[list(selected)].sum()) if selected else 0.0
    for rule in (STOP_RULE_ONE_DAY, STOP_RULE_THREE_DAY):
        for index in ordered[rule]:
            if index in selected:
                continue
            candidate_cost = float(latest_cost.loc[index])
            proposed = selected_cost + candidate_cost
            if proposed <= target_cost + 1e-9:
                selected.add(index)
                selected_cost = proposed
                continue
            if (
                proposed <= soft_upper_cost + 1e-9
                and abs(proposed - target_cost) < abs(selected_cost - target_cost)
            ):
                selected.add(index)
                selected_cost = proposed
            break

    # The normal selection stays within the 5.5% soft upper bound. If discrete
    # creative costs still leave the result below the 4.5% soft lower bound,
    # allow the next uninterrupted ROI prefix to approach 5%, but never exceed
    # the 6% absolute cap.
    if selected_cost < soft_lower_cost:
        for rule in (STOP_RULE_ONE_DAY, STOP_RULE_THREE_DAY):
            for index in ordered[rule]:
                if index in selected:
                    continue
                candidate_cost = float(latest_cost.loc[index])
                proposed = selected_cost + candidate_cost
                if (
                    proposed <= hard_cap_cost + 1e-9
                    and abs(proposed - target_cost)
                    < abs(selected_cost - target_cost)
                ):
                    selected.add(index)
                    selected_cost = proposed
                break

    if selected_cost > hard_cap_cost + 1e-9:
        raise ValueError("小程序关停候选成本超过绝对上限")

    selected_mask = pd.Series(result.index.isin(selected), index=result.index)
    candidate_rule = pd.Series("", index=result.index, dtype="object")
    for rule, mask in masks.items():
        candidate_rule.loc[mask] = rule

    lines: dict[str, float] = {}
    for rule in ordered:
        line, percentile = _selection_line(
            result,
            ordered[rule],
            selected,
            roi_columns[rule],
        )
        lines[f"{rule}_line"] = line
        lines[f"{rule}_percentile"] = percentile
        selected_indices = [index for index in ordered[rule] if index in selected]
        lines[f"{rule}_cost"] = (
            float(latest_cost.loc[selected_indices].sum())
            if selected_indices
            else 0.0
        )

    stats = {
        "小程序昨日总成本": total_cost,
        "软下限关停成本": soft_lower_cost,
        "目标关停成本": target_cost,
        "软上限关停成本": soft_upper_cost,
        "绝对上限关停成本": hard_cap_cost,
        "实际关停成本": selected_cost,
        "实际关停成本占比": selected_cost / total_cost if total_cost > 0 else 0.0,
        "关停成本预算状态": (
            "正常范围"
            if soft_lower_cost <= selected_cost <= soft_upper_cost
            else "低于软下限"
            if selected_cost < soft_lower_cost
            else "高于软上限"
        ),
        "三日基础预算成本": target_cost * config.self_stop_three_day_share,
        "单日基础预算成本": target_cost * config.self_stop_one_day_share,
        "三日候选数": len(ordered[STOP_RULE_THREE_DAY]),
        "单日候选池样本数": int(one_day_pool.sum()),
        "单日合并候选数": len(ordered[STOP_RULE_ONE_DAY]),
        "单日P10线": one_day_p10_line,
        "单日合并资格线": one_day_eligibility_line,
        "三日实际关停成本": lines[f"{STOP_RULE_THREE_DAY}_cost"],
        "单日实际关停成本": lines[f"{STOP_RULE_ONE_DAY}_cost"],
        "三日关停线": lines[f"{STOP_RULE_THREE_DAY}_line"],
        "三日关停线分位点": lines[f"{STOP_RULE_THREE_DAY}_percentile"],
        "单日实际关停线": lines[f"{STOP_RULE_ONE_DAY}_line"],
        "单日实际关停线分位点": lines[f"{STOP_RULE_ONE_DAY}_percentile"],
    }
    return selected_mask, candidate_rule, stats


def apply_actions(
    summary: pd.DataFrame,
    thresholds: pd.DataFrame,
    config: RuleConfig,
    expected_dates: list[str],
) -> tuple[pd.DataFrame, pd.DataFrame]:
    result = summary.copy()
    result["动作"] = ""
    result["动作原因"] = ""
    thresholds = thresholds.copy()
    threshold_by_type = thresholds.set_index("entity_type")
    self_threshold = threshold_by_type.loc[ENTITY_SELF]
    gzh_threshold = threshold_by_type.loc[ENTITY_GZH]
    result["t_stop"] = np.nan
    result.loc[result["entity_type"].eq(ENTITY_GZH), "t_stop"] = float(
        gzh_threshold["t_stop"]
    )
    result["t_up"] = np.nan
    result.loc[
        result["entity_type"].isin([ENTITY_SELF, ENTITY_SELF_AD]), "t_up"
    ] = float(self_threshold["t_up"])
    result["t_one_day_stop"] = np.nan
    result["关停线分位点"] = np.nan

    threshold_eligible = threshold_eligibility_mask(result, config, expected_dates)
    ad_eligible = entity_eligibility_mask(
        result, ENTITY_SELF_AD, config, expected_dates
    )
    observe_only = observation_mask(result, config, expected_dates)
    one_day_supplement = one_day_supplement_mask(
        result, config, expected_dates
    )
    creative_eligible = entity_eligibility_mask(
        result, ENTITY_SELF, config, expected_dates
    )
    gzh_eligible = entity_eligibility_mask(
        result, ENTITY_GZH, config, expected_dates
    )
    self_sample_roi = result.loc[creative_eligible, "ROI"]
    gzh_sample_roi = result.loc[gzh_eligible, "ROI"]
    creative_sample_roi = result.loc[creative_eligible, "ROI"]
    result["整体三日ROI排名百分位"] = np.nan
    for mask, sample in (
        (result["entity_type"].isin([ENTITY_SELF, ENTITY_SELF_AD]), self_sample_roi),
        (result["entity_type"].eq(ENTITY_GZH), gzh_sample_roi),
    ):
        result.loc[mask, "整体三日ROI排名百分位"] = result.loc[mask, "ROI"].apply(
            lambda value: _sample_percentile(sample, float(value))
            if pd.notna(value)
            else np.nan
        )
    result["是否低于三日关停线"] = False
    result["创意三日ROI排名百分位"] = result["ROI"].apply(
        lambda value: _sample_percentile(creative_sample_roi, float(value))
        if pd.notna(value)
        else np.nan
    )
    result["是否位于创意三日ROI前20%"] = False
    if np.isfinite(float(self_threshold["t_up"])):
        result.loc[creative_eligible, "是否位于创意三日ROI前20%"] = (
            pd.to_numeric(result.loc[creative_eligible, "ROI"], errors="coerce")
            >= result.loc[creative_eligible, "t_up"]
        )

    (
        selected_stop,
        candidate_rule,
        budget_stats,
    ) = _allocate_self_stop_budget(
        result,
        config,
        expected_dates,
        creative_eligible,
        one_day_supplement,
    )
    self_row = thresholds["entity_type"].eq(ENTITY_SELF)
    threshold_updates = {
        **budget_stats,
        "t_stop": budget_stats["三日关停线"],
        "t_one_day_stop": budget_stats["单日实际关停线"],
        "关停线分位点": budget_stats["三日关停线分位点"],
        "单日实际关停线分位点": budget_stats["单日实际关停线分位点"],
    }
    for column, value in threshold_updates.items():
        thresholds.loc[self_row, column] = value

    latest = expected_dates[-1]
    result["昨日成本"] = np.where(
        result["entity_type"].eq(ENTITY_SELF),
        pd.to_numeric(result[f"成本_{latest}"], errors="coerce"),
        np.nan,
    )
    result["关停规则"] = candidate_rule
    result["关停预算选择状态"] = ""
    result.loc[candidate_rule.ne(""), "关停预算选择状态"] = "预算未选中"
    result.loc[selected_stop, "关停预算选择状态"] = "预算已选中"
    result["小程序昨日总成本"] = budget_stats["小程序昨日总成本"]
    result["小程序实际关停成本占比"] = budget_stats["实际关停成本占比"]
    formal_line = budget_stats["三日关停线"]
    formal_percentile = budget_stats["三日关停线分位点"]
    self_or_ad = result["entity_type"].isin([ENTITY_SELF, ENTITY_SELF_AD])
    result.loc[self_or_ad, "t_stop"] = formal_line
    result.loc[self_or_ad, "关停线分位点"] = formal_percentile
    one_day_rows = candidate_rule.eq(STOP_RULE_ONE_DAY)
    result.loc[one_day_rows, "t_stop"] = budget_stats["单日实际关停线"]
    result.loc[one_day_rows, "t_one_day_stop"] = budget_stats["单日实际关停线"]
    result.loc[one_day_rows, "关停线分位点"] = budget_stats[
        "单日实际关停线分位点"
    ]
    result.loc[selected_stop & creative_eligible, "是否低于三日关停线"] = True
    result.loc[gzh_eligible, "关停线分位点"] = float(
        gzh_threshold["关停线分位点"]
    )
    result.loc[gzh_eligible, "是否低于三日关停线"] = (
        pd.to_numeric(result.loc[gzh_eligible, "ROI"], errors="coerce")
        <= float(gzh_threshold["t_stop"])
    )

    for index, row in result.iterrows():
        entity_type = str(row["entity_type"])
        if bool(one_day_supplement.loc[index]):
            latest_uv = float(row[f"首层UV_{latest}"])
            latest_roi = float(row[f"ROI_{latest}"])
            three_day_roi = float(row["ROI"])
            raw_age = row.get("广告age")
            age = int(raw_age) if pd.notna(raw_age) else 0
            decision_context = (
                "未满足连续三天每天首层UV>200、成本>0且ROI有效，"
                "启用单日补充规则；"
            )
            three_day_context = (
                f"；三日预测总效率ROI={three_day_roi:.2f}仅作参考，"
                "不参与本次单日判断"
            )
            one_day_p10_line = float(budget_stats["单日P10线"])
            meets_absolute = latest_roi <= config.one_day_hard_stop_roi
            meets_p10 = (
                np.isfinite(one_day_p10_line)
                and latest_roi <= one_day_p10_line
            )
            qualifies = meets_absolute and meets_p10
            stop_reason = (
                f"最新日首层UV={latest_uv:.0f}>{config.one_day_min_uv:g}，"
                f"最新日预测总效率ROI={latest_roi:.2f}同时满足ROI≤"
                f"{config.one_day_hard_stop_roi:.2f}和单日候选池P10线"
                f"{one_day_p10_line:.2f}"
            )

            if qualifies and age < config.self_stop_min_age:
                result.at[index, "动作"] = "观察"
                result.at[index, "动作原因"] = (
                    f"{decision_context}{stop_reason}{three_day_context}；"
                    f"广告age={age}≤{config.self_stop_min_age - 1}天，暂不关停"
                )
            elif bool(selected_stop.loc[index]):
                result.at[index, "动作"] = "关停"
                result.at[index, "动作原因"] = (
                    f"{decision_context}{stop_reason}{three_day_context}；"
                    f"广告age={age}>{config.self_stop_min_age - 1}天，"
                    f"昨日成本={float(row[f'成本_{latest}']):.2f}元，"
                    f"按5%成本软预算选中（{candidate_rule.loc[index]}）；"
                    "建议审批后暂停动态创意"
                )
            elif qualifies:
                result.at[index, "动作"] = "观察"
                result.at[index, "动作原因"] = (
                    f"{decision_context}{stop_reason}{three_day_context}；"
                    f"昨日成本={float(row[f'成本_{latest}']):.2f}元，"
                    "满足低质候选条件但未被5%成本软预算选中；建议观察"
                )
            elif not meets_absolute:
                result.at[index, "动作"] = "观察"
                result.at[index, "动作原因"] = (
                    f"{decision_context}最新日首层UV={latest_uv:.0f}>"
                    f"{config.one_day_min_uv:g}，最新日预测总效率ROI="
                    f"{latest_roi:.2f}高于绝对线{config.one_day_hard_stop_roi:.2f}，"
                    f"不满足单日两个条件{three_day_context}；建议继续观察"
                )
            elif not meets_p10:
                result.at[index, "动作"] = "观察"
                result.at[index, "动作原因"] = (
                    f"{decision_context}最新日预测总效率ROI={latest_roi:.2f}≤"
                    f"{config.one_day_hard_stop_roi:.2f}，但高于单日候选池P10线"
                    f"{one_day_p10_line:.2f}、未进入后10%{three_day_context}；"
                    "建议继续观察"
                )
            else:
                result.at[index, "动作"] = "观察"
                result.at[index, "动作原因"] = (
                    f"{decision_context}单日候选池不足，无法计算P10线"
                    f"{three_day_context}；建议继续观察"
                )
            continue
        if bool(observe_only.loc[index]):
            result.at[index, "动作"] = "观察"
            result.at[index, "动作原因"] = (
                f"最新日首层UV>{config.observe_min_latest_uv:g}，但未满足连续三天"
                "每天首层UV>200、成本>0且ROI有效；仅置底展示，不进入正式阈值和自动执行"
            )
            continue
        if bool(ad_eligible.loc[index]):
            if np.isfinite(float(row["t_stop"])) and float(row["ROI"]) <= float(
                row["t_stop"]
            ):
                raw_age = row.get("广告age")
                age = int(raw_age) if pd.notna(raw_age) else 0
                if age >= config.self_stop_min_age:
                    result.at[index, "动作"] = "关停"
                    result.at[index, "动作原因"] = (
                        "广告级三日加权平均效率ROI≤小程序三日动态关停线，"
                        f"广告age>{config.self_stop_min_age - 1}天；审批后暂停整个广告"
                    )
                else:
                    result.at[index, "动作"] = "观察"
                    result.at[index, "动作原因"] = (
                        "广告级三日加权平均效率ROI≤小程序三日动态关停线，但广告age≤"
                        f"{config.self_stop_min_age - 1}天"
                    )
            continue
        if not bool(threshold_eligible.loc[index]):
            continue
        if entity_type == ENTITY_SELF:
            raw_age = row.get("广告age")
            age = int(raw_age) if pd.notna(raw_age) else 0
            if bool(selected_stop.loc[index]):
                result.at[index, "动作"] = "关停"
                result.at[index, "动作原因"] = (
                    "三日加权平均预测总效率ROI按从低到高排序，"
                    f"昨日成本={float(row[f'成本_{latest}']):.2f}元，"
                    "按5%成本软预算的60%基础额度或结余额度选中；"
                    f"广告age>{config.self_stop_min_age - 1}天；审批后仅暂停动态创意"
                )
            elif (
                np.isfinite(float(row["t_stop"]))
                and float(row["ROI"]) <= float(row["t_stop"])
                and age < config.self_stop_min_age
            ):
                result.at[index, "动作"] = "观察"
                result.at[index, "动作原因"] = (
                    "三日加权平均预测总效率ROI低于小程序动态关停线，但广告age≤"
                    f"{config.self_stop_min_age - 1}天"
                )
            elif bool(row["是否位于创意三日ROI前20%"]):
                if age >= config.self_up_min_age:
                    result.at[index, "动作"] = "扩量"
                    result.at[index, "动作原因"] = (
                        f"三日加权平均效率ROI≥合格创意实体等权P80，"
                        f"广告age≥{config.self_up_min_age}天；审批后提高广告永久基础出价"
                    )
                else:
                    result.at[index, "动作"] = "观察"
                    result.at[index, "动作原因"] = (
                        f"三日加权平均效率ROI≥合格创意实体等权P80，但广告age<"
                        f"{config.self_up_min_age}天"
                    )
        elif (
            entity_type == ENTITY_GZH
            and np.isfinite(float(row["t_stop"]))
            and float(row["ROI"]) <= float(row["t_stop"])
        ):
            result.at[index, "动作"] = "关停"
            result.at[index, "动作原因"] = (
                "三日加权平均效率ROI≤公众号独立实体等权关停线；"
                "公众号当前仅通知参考"
            )
    return result, thresholds


def evaluate_rules(
    raw_daily: pd.DataFrame,
    expected_dates: Iterable[str],
    ad_age: pd.DataFrame | None = None,
    config: RuleConfig = RuleConfig(),
    *,
    fission_parameters: FissionMultiplierParameters,
) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
    summary, dates = compute_roi_summary(
        raw_daily,
        expected_dates,
        ad_age,
        fission_parameters=fission_parameters,
    )
    thresholds = compute_channel_thresholds(summary, dates, config)
    evaluated, thresholds = apply_actions(summary, thresholds, config, dates)
    formal_threshold = threshold_eligibility_mask(evaluated, config, dates)
    formal_ad = entity_eligibility_mask(evaluated, ENTITY_SELF_AD, config, dates)
    one_day_supplement = one_day_supplement_mask(evaluated, config, dates)
    observe_only = observation_mask(evaluated, config, dates) & ~one_day_supplement
    evaluated["阈值样本状态"] = "未达到三日正式样本门槛"
    evaluated.loc[formal_threshold, "阈值样本状态"] = (
        "进入三日渠道独立阈值样本池"
    )
    evaluated.loc[formal_ad, "阈值样本状态"] = "广告级三日合格_不进入阈值样本池"
    evaluated.loc[one_day_supplement, "阈值样本状态"] = (
        "单日补充决策_昨日UV>200"
    )
    evaluated.loc[observe_only, "阈值样本状态"] = "补充观察_昨日UV>200"
    candidates = evaluated[evaluated["动作"].ne("")].copy()
    return candidates, thresholds, evaluated
