"""
Benefit calculation engine.

All benefits convert saved handle-time seconds into dollars via each
site's fully-loaded labour rate per working second. Reduction
percentages come from :data:`tokencalc.scenarios.BENEFIT_PARAMS` —
``realistic`` (pressure-tested) by default; pass ``params="claim"``
to reproduce the Genesys ROI-doc figures for side-by-side comparison.

Every figure scales by the scenario's year realization ramp.
"""

from __future__ import annotations

import pandas as pd

from .inputs import FeatureScope, SiteInput
from .meters import Confidence
from .rollout import NO_ROLLOUT, RolloutPlan
from .scenarios import BENEFIT_PARAMS, Scenario, get_scenario

MONTHS_PER_YEAR = 12


def _param(name: str, params: str) -> float:
    return BENEFIT_PARAMS[name][params]


def _scope_for(feature_scopes: list[FeatureScope] | FeatureScope,
               feature: str) -> FeatureScope | None:
    if isinstance(feature_scopes, FeatureScope):
        return feature_scopes if feature_scopes.feature == feature else None
    return next((s for s in feature_scopes if s.feature == feature), None)


def _df(rows: list[dict]) -> pd.DataFrame:
    return pd.DataFrame(
        rows, columns=["benefit_line", "scope", "annual_value", "confidence"]
    )


def calculate_voice_handle_time_benefit(
    sites: list[SiteInput],
    feature_scope: FeatureScope,
    scenario: str | Scenario,
    year: int,
    params: str = "realistic",
    rollout: RolloutPlan | None = None,
) -> pd.DataFrame:
    """AHT reduction from knowledge surfacing (Agent Copilot).

    Benefit = volume × eligibility × AHT × reduction% × labour rate.
    """
    sc = get_scenario(scenario) if isinstance(scenario, str) else scenario
    ro = rollout or NO_ROLLOUT
    reduction = _param("voice_aht_knowledge_reduction", params)
    realization = sc.realization(year)
    rows = []
    for s in sites:
        if not feature_scope.active(s.site_name, year):
            continue
        eligibility = (
            feature_scope.eligibility_pct
            if feature_scope.eligibility_pct is not None
            else sc.voice_knowledge_eligibility
        )
        seconds_saved = (
            s.voice_volume_monthly * MONTHS_PER_YEAR
            * eligibility * s.voice_aht_seconds * reduction * realization
        )
        rows.append(
            {
                "benefit_line": "Voice AHT (knowledge surfacing)",
                "scope": s.site_name,
                "annual_value": seconds_saved * s.agent_cost_per_second
                * ro.fraction_live(s.site_name, year),
                "confidence": Confidence.ESTIMATED.value,
            }
        )
    return _df(rows)


def calculate_acw_summarization_benefit(
    sites: list[SiteInput],
    feature_scope: FeatureScope,
    scenario: str | Scenario,
    year: int,
    params: str = "realistic",
    rollout: RolloutPlan | None = None,
) -> pd.DataFrame:
    """ACW eliminated by auto-summarization (Copilot / AI Summary)."""
    sc = get_scenario(scenario) if isinstance(scenario, str) else scenario
    ro = rollout or NO_ROLLOUT
    reduction = _param("voice_acw_reduction", params)
    realization = sc.realization(year)
    rows = []
    for s in sites:
        if not feature_scope.active(s.site_name, year):
            continue
        eligibility = (
            feature_scope.eligibility_pct
            if feature_scope.eligibility_pct is not None
            else sc.voice_summarization_eligibility
        )
        seconds_saved = (
            s.voice_volume_monthly * MONTHS_PER_YEAR
            * eligibility * s.voice_acw_seconds * reduction * realization
        )
        rows.append(
            {
                "benefit_line": "Voice ACW (summarization)",
                "scope": s.site_name,
                "annual_value": seconds_saved * s.agent_cost_per_second
                * ro.fraction_live(s.site_name, year),
                "confidence": Confidence.ESTIMATED.value,
            }
        )
    return _df(rows)


def calculate_email_ai_benefit(
    sites: list[SiteInput],
    feature_scope: FeatureScope,
    scenario: str | Scenario,
    year: int,
    params: str = "realistic",
    rollout: RolloutPlan | None = None,
) -> pd.DataFrame:
    """Email Auto-Respond — full displacement at the respond rate.

    (Email Auto-Suggest is not a separate benefit line: it is included
    in Agent Copilot, whose per-user meter carries the drafting help.)"""
    sc = get_scenario(scenario) if isinstance(scenario, str) else scenario
    ro = rollout or NO_ROLLOUT
    realization = sc.realization(year)
    rows = []
    for s in sites:
        if not feature_scope.active(s.site_name, year):
            continue
        respond_rate = (
            feature_scope.deflection_target
            if feature_scope.deflection_target is not None
            else sc.email_auto_respond_rate
        )
        annual_emails = s.email_volume_monthly * MONTHS_PER_YEAR
        respond_seconds = (
            annual_emails * respond_rate * s.email_aht_seconds * realization
        )
        rate = s.agent_cost_per_second
        rows.append(
            {
                "benefit_line": "Email Auto-Respond (displaced handling)",
                "scope": s.site_name,
                "annual_value": respond_seconds * rate
                * ro.fraction_live(s.site_name, year),
                "confidence": Confidence.UNKNOWN.value,  # meter rate unsourced
            }
        )
    return _df(rows)


def calculate_sta_benefit(
    sites: list[SiteInput],
    feature_scope: FeatureScope,
    scenario: str | Scenario,
    year: int,
    params: str = "realistic",
    rollout: RolloutPlan | None = None,
) -> pd.DataFrame:
    """STA reduces AHT *indirectly* via coaching — small reduction with
    a realistic ramp (default 1.5% vs the 4% claim)."""
    sc = get_scenario(scenario) if isinstance(scenario, str) else scenario
    ro = rollout or NO_ROLLOUT
    reduction = _param("sta_aht_reduction", params)
    realization = sc.realization(year)
    rows = []
    for s in sites:
        if not feature_scope.active(s.site_name, year):
            continue
        seconds_saved = (
            s.voice_volume_monthly * MONTHS_PER_YEAR
            * s.voice_aht_seconds * reduction * realization
        )
        rows.append(
            {
                "benefit_line": "STA coaching (AHT)",
                "scope": s.site_name,
                "annual_value": seconds_saved * s.agent_cost_per_second
                * ro.fraction_live(s.site_name, year),
                "confidence": Confidence.ESTIMATED.value,
            }
        )
    return _df(rows)


def calculate_va_deflection_benefit(
    sites: list[SiteInput],
    feature_scope: FeatureScope,
    scenario: str | Scenario,
    year: int,
    params: str = "realistic",
    rollout: RolloutPlan | None = None,
) -> pd.DataFrame:
    """Agent labour avoided on calls deflected to Voice Bot or Agentic VA.

    **Layered (sequential) deflection model** — Voice Bot runs first on
    the full call pool; Agentic VA handles a share of the *residual*
    (calls the bot did not deflect).  The two mechanisms are substitutes
    operating on the same call base, not independent additive benefits.

    Effective total deflection:
        bot_rate + (1 − bot_rate) × va_rate
        e.g. 35% + 65% × 15% = 44.75%  (not 50%)

    **Three realization haircuts** are applied to convert raw deflected
    volume into realizable labour savings:

    1. ``completion_rate``   — share of "deflected" calls that don't
       escalate to an agent mid-session (bot/VA fully handles the call).
    2. ``labour_realization`` — staffing flexibility factor; deflected
       volume doesn't reduce headcount 1:1 due to minimums, shrinkage,
       and occupancy ceilings.
    3. ``callback_discount`` — fraction of deflected calls that re-enter
       as repeat contacts (poorly-handled deflections drive callbacks).

    Combined realistic factor: 0.70 × 0.80 × (1 − 0.05) ≈ 0.53

    The ``params="claim"`` path sets all three factors to their
    ``claim`` values (1.0 / 1.0 / 0.0) to reproduce the original
    Genesys ROI-doc figures for side-by-side comparison.
    """
    sc = get_scenario(scenario) if isinstance(scenario, str) else scenario
    ro = rollout or NO_ROLLOUT
    realization = sc.realization(year)

    # Realization haircuts — read from BENEFIT_PARAMS so claim/realistic
    # paths are consistent with all other benefit lines.
    completion_rate = _param("va_completion_rate", params)
    labour_real = _param("va_labour_realization", params)
    callback_disc = _param("va_callback_discount", params)
    realization_factor = completion_rate * labour_real * (1.0 - callback_disc)

    rows = []
    for s in sites:
        if not feature_scope.active(s.site_name, year):
            continue

        if feature_scope.feature == "Voice Bot":
            # Bot operates on the full call pool.
            bot_rate = (
                feature_scope.deflection_target
                if feature_scope.deflection_target is not None
                else sc.voice_bot_deflection
            )
            deflected_calls = s.voice_volume_monthly * MONTHS_PER_YEAR * bot_rate

        else:  # Agentic Virtual Agent
            # VA operates on the residual after the bot has deflected its share.
            # If Voice Bot is not in scope (VA-only deployment), bot_rate = 0
            # and the VA works on the full pool — still correct.
            bot_rate = sc.voice_bot_deflection
            va_rate = (
                feature_scope.deflection_target
                if feature_scope.deflection_target is not None
                else sc.agentic_va_deflection
            )
            residual_calls = (
                s.voice_volume_monthly * MONTHS_PER_YEAR * (1.0 - bot_rate)
            )
            deflected_calls = residual_calls * va_rate

        seconds_saved = deflected_calls * s.voice_aht_seconds * realization
        rows.append(
            {
                "benefit_line": f"{feature_scope.feature} deflection (labour avoided)",
                "scope": s.site_name,
                "annual_value": (
                    seconds_saved
                    * s.agent_cost_per_second
                    * realization_factor
                    * ro.fraction_live(s.site_name, year)
                ),
                "confidence": Confidence.ESTIMATED.value,
            }
        )
    return _df(rows)


def calculate_supervisor_copilot_benefit(
    sites: list[SiteInput],
    feature_scope: FeatureScope,
    scenario: str | Scenario,
    year: int,
    params: str = "realistic",
    rollout: RolloutPlan | None = None,
) -> pd.DataFrame:
    """Supervisor time reclaimed (summaries, QA triage). ESTIMATED."""
    sc = get_scenario(scenario) if isinstance(scenario, str) else scenario
    ro = rollout or NO_ROLLOUT
    saving = _param("supervisor_copilot_time_saving", params)
    realization = sc.realization(year)
    rows = []
    for s in sites:
        if not feature_scope.active(s.site_name, year):
            continue
        rows.append(
            {
                "benefit_line": "Supervisor time (AI summaries/insights)",
                "scope": s.site_name,
                "annual_value": s.supervisors
                * s.fully_loaded_supervisor_cost_annual
                * saving * realization
                * ro.fraction_live(s.site_name, year),
                "confidence": Confidence.ESTIMATED.value,
            }
        )
    return _df(rows)


def calculate_predictive_routing_benefit(
    sites: list[SiteInput],
    feature_scope: FeatureScope,
    scenario: str | Scenario,
    year: int,
    params: str = "realistic",
    rollout: RolloutPlan | None = None,
) -> pd.DataFrame:
    """Predictive routing AHT effect. ESTIMATED; off unless scoped."""
    sc = get_scenario(scenario) if isinstance(scenario, str) else scenario
    ro = rollout or NO_ROLLOUT
    reduction = _param("predictive_routing_aht_reduction", params)
    realization = sc.realization(year)
    rows = []
    for s in sites:
        if not feature_scope.active(s.site_name, year):
            continue
        seconds_saved = (
            s.voice_volume_monthly * MONTHS_PER_YEAR
            * s.voice_aht_seconds * reduction * realization
        )
        rows.append(
            {
                "benefit_line": "Predictive routing (AHT)",
                "scope": s.site_name,
                "annual_value": seconds_saved * s.agent_cost_per_second
                * ro.fraction_live(s.site_name, year),
                "confidence": Confidence.ESTIMATED.value,
            }
        )
    return _df(rows)


#: Which calculator handles which feature scope.
#: Agent Copilot and STA exist in named/concurrent variants — both map
#: to the same benefit calculators.
#: Voice Bot and Agentic VA both route to calculate_va_deflection_benefit,
#: which implements the layered sequential model — VA operates on the
#: residual after the bot has deflected its share.
_BENEFIT_DISPATCH = {
    "Agent Copilot [named]": (
        calculate_voice_handle_time_benefit,
        calculate_acw_summarization_benefit,
    ),
    "Agent Copilot [concurrent]": (
        calculate_voice_handle_time_benefit,
        calculate_acw_summarization_benefit,
    ),
    "AI Summary & Insights": (),  # benefit carried by Copilot where present
    "Speech & Text Analytics [named]": (calculate_sta_benefit,),
    "Speech & Text Analytics [concurrent]": (calculate_sta_benefit,),
    "Voice Bot": (calculate_va_deflection_benefit,),
    "Agentic Virtual Agent": (calculate_va_deflection_benefit,),
    "Predictive Routing": (calculate_predictive_routing_benefit,),
}

_COPILOT_FEATURES = {"Agent Copilot [named]", "Agent Copilot [concurrent]"}


def calculate_total_benefit(
    sites: list[SiteInput],
    feature_scopes: list[FeatureScope],
    scenario: str | Scenario,
    year: int,
    params: str = "realistic",
    include_supervisor_benefit: bool = True,
    rollout: RolloutPlan | None = None,
) -> pd.DataFrame:
    """All benefit lines for one scenario-year, aggregated per line.

    Returns DataFrame: benefit_line, scope, annual_value, confidence.

    Voice Bot and Agentic VA deflection benefits use the layered
    sequential model: the bot deflects from the full call pool; the VA
    deflects from the residual.  The two features are NOT additive on
    the same base — see :func:`calculate_va_deflection_benefit`.
    """
    sc = get_scenario(scenario) if isinstance(scenario, str) else scenario
    frames: list[pd.DataFrame] = []
    # Find whichever Copilot variant is in scope (named or concurrent).
    copilot_scope = next(
        (s for s in feature_scopes if s.feature in _COPILOT_FEATURES), None
    )

    for scope in feature_scopes:
        for fn in _BENEFIT_DISPATCH.get(scope.feature, ()):  # type: ignore[arg-type]
            frames.append(fn(sites, scope, sc, year, params=params, rollout=rollout))

    if include_supervisor_benefit and copilot_scope is not None:
        frames.append(
            calculate_supervisor_copilot_benefit(
                sites, copilot_scope, sc, year, params=params, rollout=rollout
            )
        )

    frames = [f for f in frames if not f.empty]
    if not frames:
        return _df([])
    detail = pd.concat(frames, ignore_index=True)

    grouped = (
        detail.groupby("benefit_line", sort=False)
        .agg(
            scope=("scope", lambda v: ", ".join(sorted(set(v)))),
            annual_value=("annual_value", "sum"),
            confidence=("confidence", "first"),
        )
        .reset_index()
    )
    return grouped[["benefit_line", "scope", "annual_value", "confidence"]]
