"""
NTT DATA — CTM Token Calculator (Streamlit).

Run from the ctm-token-calculator root::

    streamlit run app/streamlit_app.py

Thin presentation layer over ``tokencalc`` — all math lives in the
library, shared with the JupyterLab notebook.
"""

from __future__ import annotations

import dataclasses
import io
import json
import sys
from pathlib import Path

# Import tokencalc from the project root without install
_ROOT = Path(__file__).resolve().parent.parent
if str(_ROOT) not in sys.path:
    sys.path.insert(0, str(_ROOT))

import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st

import tokencalc.scenarios as tc_scenarios
from tokencalc import (
    CONTRACTED_NAMED_USERS,
    CTM_DEFAULT_FEATURE_SCOPES,
    CTM_DEFAULT_SITES,
    CTM_DEFAULT_TAKEOUTS,
    DEFAULT_METERS,
    DEFAULT_PRICING,
    Confidence,
    CostTakeout,
    FeatureScope,
    SiteInput,
    build_business_case,
    calculate_total_benefit,
    calculate_total_cost,
    export_excel,
    get_scenario,
    meters_dataframe,
    scenario_state_from_json,
    scenario_state_to_json,
    sites_dataframe,
)

st.set_page_config(page_title="NTT DATA — CTM Token Calculator",
                   page_icon="🧮", layout="wide")

YEARS = (1, 2, 3)
FEATURES = list(DEFAULT_METERS)
_DEFAULT_REALISTIC = {
    k: v["realistic"] for k, v in tc_scenarios.BENEFIT_PARAMS.items()
}


# ── State ────────────────────────────────────────────────────────────

def _init_state(force: bool = False) -> None:
    if force or "sites" not in st.session_state:
        st.session_state.sites = list(CTM_DEFAULT_SITES)
        st.session_state.takeouts = list(CTM_DEFAULT_TAKEOUTS)
        st.session_state.scopes = [
            dataclasses.replace(s) for s in CTM_DEFAULT_FEATURE_SCOPES
        ]
        st.session_state.meters = dict(DEFAULT_METERS)
        st.session_state.pricing = dict(DEFAULT_PRICING)
        st.session_state.use_contracted = False
        st.session_state.implementation_cost = 0.0
        for k, v in _DEFAULT_REALISTIC.items():  # reset benefit sliders
            tc_scenarios.BENEFIT_PARAMS[k]["realistic"] = v


_init_state()


def _state_key() -> str:
    """Stable serialization of inputs for st.cache_data keys."""
    return scenario_state_to_json(
        st.session_state.sites, st.session_state.takeouts, st.session_state.scopes
    ) + json.dumps(
        {
            "params": {k: v["realistic"] for k, v in tc_scenarios.BENEFIT_PARAMS.items()},
            "contracted": st.session_state.use_contracted,
            "impl": st.session_state.implementation_cost,
            "meters": {f: m.tokens_per_unit for f, m in st.session_state.meters.items()},
            "pricing": {
                r: (p.list_rate_per_token, p.contracted_rate_per_token)
                for r, p in st.session_state.pricing.items()
            },
        }
    )


@st.cache_data(show_spinner=False)
def _cached_case(state_key: str, scenario: str) -> dict:
    return build_business_case(
        st.session_state.sites, st.session_state.scopes,
        st.session_state.meters, st.session_state.pricing,
        st.session_state.takeouts, scenario,
        implementation_cost=st.session_state.implementation_cost,
        use_contracted=st.session_state.use_contracted,
    )


def _case(scenario: str) -> dict:
    return _cached_case(_state_key(), scenario)


# ── Sidebar ──────────────────────────────────────────────────────────

st.sidebar.title("NTT DATA — CTM Token Calculator")
page = st.sidebar.radio("Page", [
    "1. Inputs", "2. Token Meters", "3. Cost Model", "4. Benefit Model",
    "5. Business Case", "6. Sensitivity Analysis", "7. Export",
])
st.sidebar.divider()
scenario_name = st.sidebar.radio(
    "Scenario", ["floor", "realistic", "stretch"], index=1, horizontal=True
)
year = st.sidebar.radio("Year", YEARS, horizontal=True)
if st.sidebar.button("Reset to CTM defaults"):
    _init_state(force=True)
    st.cache_data.clear()
    st.rerun()
st.sidebar.caption(
    "⚠️ Planning tool — published list rates unless overridden; "
    "not contractual pricing."
)

sites: list[SiteInput] = st.session_state.sites
scopes: list[FeatureScope] = st.session_state.scopes
meters = st.session_state.meters
pricing = st.session_state.pricing
scenario = get_scenario(scenario_name)


def _users_warning() -> None:
    total = sum(s.named_users for s in sites)
    if total != CONTRACTED_NAMED_USERS:
        st.warning(
            f"Named users across sites = {total:,} ≠ contracted licence "
            f"count {CONTRACTED_NAMED_USERS:,}."
        )


# ── Page 1: Inputs ───────────────────────────────────────────────────

if page == "1. Inputs":
    st.header("Inputs")
    st.caption("Site data outside NAM is **estimated — confirm with CTM data**.")
    _users_warning()

    df = sites_dataframe(sites)
    edited = st.data_editor(df, num_rows="dynamic", key="sites_editor")
    if st.button("Apply site changes"):
        try:
            st.session_state.sites = [
                SiteInput(
                    **{
                        **row,
                        "languages": [
                            x.strip() for x in str(row["languages"]).split(",") if x.strip()
                        ],
                    }
                )
                for row in edited.to_dict("records")
            ]
            st.cache_data.clear()
            st.success("Sites updated.")
            st.rerun()
        except (ValueError, TypeError) as e:
            st.error(f"Validation failed: {e}")

    st.subheader("Cost takeouts")
    tdf = pd.DataFrame(
        [
            {"name": t.name, "annual_cost": t.annual_cost,
             "start_year": t.start_year, "confidence": t.confidence.value,
             "notes": t.notes}
            for t in st.session_state.takeouts
        ]
    )
    tedit = st.data_editor(
        tdf, num_rows="dynamic", key="takeouts_editor",
        column_config={
            "confidence": st.column_config.SelectboxColumn(
                options=[c.value for c in Confidence]
            )
        },
    )
    if st.button("Apply takeout changes"):
        try:
            st.session_state.takeouts = [
                CostTakeout(
                    name=r["name"], annual_cost=float(r["annual_cost"] or 0),
                    start_year=int(r["start_year"] or 1),
                    confidence=Confidence(r["confidence"]), notes=r["notes"] or "",
                )
                for r in tedit.to_dict("records")
            ]
            st.cache_data.clear()
            st.success("Takeouts updated.")
            st.rerun()
        except (ValueError, TypeError) as e:
            st.error(f"Validation failed: {e}")

    st.subheader("Save / load scenario")
    col1, col2 = st.columns(2)
    with col1:
        st.download_button(
            "Download scenario JSON",
            scenario_state_to_json(sites, st.session_state.takeouts, scopes),
            file_name="ctm_scenario.json", mime="application/json",
        )
    with col2:
        up = st.file_uploader("Load scenario JSON", type="json")
        if up is not None and st.button("Load"):
            s, t, sc = scenario_state_from_json(up.read().decode())
            st.session_state.sites, st.session_state.takeouts = s, t
            st.session_state.scopes = sc
            st.cache_data.clear()
            st.success("Scenario loaded.")
            st.rerun()

# ── Page 2: Token Meters ─────────────────────────────────────────────

elif page == "2. Token Meters":
    st.header("Token Meters")
    st.dataframe(meters_dataframe(meters), width="stretch", hide_index=True)

    st.subheader("Override a meter rate")
    feature = st.selectbox("Feature", FEATURES)
    m = meters[feature]
    override = st.toggle("Override default", key=f"ovr_{feature}")
    if override:
        new_rate = st.number_input(
            "tokens per unit (per user/month for per-user meters)",
            value=float(m.tokens_per_unit), min_value=0.0, step=0.005,
            format="%.4f",
        )
        if st.button("Apply override"):
            meters[feature] = dataclasses.replace(
                m,
                tokens_per_unit=new_rate,
                units_per_token=(1 / new_rate if new_rate and m.units_per_token else 0.0),
                confidence=Confidence.ESTIMATED,
                notes=m.notes + " [rate overridden by user]",
            )
            st.cache_data.clear()
            st.success(f"{feature} now {new_rate} tokens/unit (flagged estimated).")

    st.subheader("Token pricing per region")
    st.session_state.use_contracted = st.toggle(
        "Apply contracted rate (if known) instead of list rate",
        value=st.session_state.use_contracted,
    )
    for region, p in pricing.items():
        c1, c2 = st.columns(2)
        with c1:
            lr = st.number_input(
                f"{region} — list $/token", value=float(p.list_rate_per_token),
                min_value=0.0, key=f"list_{region}",
            )
        with c2:
            cr = st.number_input(
                f"{region} — contracted $/token (0 = unknown)",
                value=float(p.contracted_rate_per_token or 0.0),
                min_value=0.0, key=f"con_{region}",
            )
        pricing[region] = dataclasses.replace(
            p, list_rate_per_token=lr,
            contracted_rate_per_token=cr or None,
        )

# ── Page 3: Cost Model ───────────────────────────────────────────────

elif page == "3. Cost Model":
    st.header("Cost Model")
    _users_warning()

    st.subheader("Feature enablement & phasing")
    st.caption("Phase = model year the feature switches on at that site; 0 = off.")
    site_names = [s.site_name for s in sites]
    matrix = pd.DataFrame(0, index=site_names, columns=FEATURES, dtype=int)
    for sc in scopes:
        for sn in sc.enabled_sites:
            if sn in matrix.index:
                matrix.loc[sn, sc.feature] = sc.phase
    edited_matrix = st.data_editor(matrix, key="phasing_matrix")
    if st.button("Apply phasing"):
        new_scopes: list[FeatureScope] = []
        for feature in FEATURES:
            for phase in (1, 2, 3):
                enabled = [sn for sn in site_names
                           if int(edited_matrix.loc[sn, feature]) == phase]
                if enabled:
                    template = next(
                        (s for s in scopes if s.feature == feature), None
                    )
                    new_scopes.append(
                        FeatureScope(
                            feature, enabled, phase=phase,
                            adoption_curve=(
                                template.adoption_curve if template else {}
                            ),
                            deflection_target=(
                                template.deflection_target if template else None
                            ),
                            eligibility_pct=(
                                template.eligibility_pct if template else None
                            ),
                        )
                    )
        st.session_state.scopes = new_scopes
        st.cache_data.clear()
        st.success("Phasing updated.")
        st.rerun()

    frames = []
    for y in YEARS:
        d = calculate_total_cost(
            sites, scopes, meters, pricing, scenario, y,
            use_contracted=st.session_state.use_contracted,
        )
        d["year"] = f"Y{y}"
        frames.append(d)
    cost_3y = pd.concat(frames, ignore_index=True)

    this_year = frames[year - 1]
    total = this_year["annual_cost"].sum()
    unknown = this_year[this_year["confidence"] == "unknown"]["annual_cost"].sum()
    c1, c2 = st.columns(2)
    c1.metric(f"Year {year} total cost ({scenario_name})", f"${total:,.0f}")
    c2.metric("of which 🔴 unknown-rate features", f"${unknown:,.0f}",
              help="Range driven by unsourced meter rates — total could move "
                   "materially once these are confirmed.")

    st.plotly_chart(
        px.bar(cost_3y, x="year", y="annual_cost", color="cost_line",
               title=f"Cost breakdown by feature — {scenario_name}",
               labels={"annual_cost": "$/yr"}),
        width="stretch", key="cost_stack",
    )
    icon_map = {c.value: c.icon for c in Confidence}
    show = this_year.copy()
    show["confidence"] = show["confidence"].map(
        lambda v: f"{icon_map.get(v, '')} {v}"
    )
    st.dataframe(show.sort_values("annual_cost", ascending=False),
                 width="stretch", hide_index=True)

# ── Page 4: Benefit Model ────────────────────────────────────────────

elif page == "4. Benefit Model":
    st.header("Benefit Model")
    st.caption("Sliders adjust the pressure-tested (realistic) parameters; "
               "the Genesys-claim figures stay fixed for comparison.")

    cols = st.columns(3)
    for i, (key, vals) in enumerate(tc_scenarios.BENEFIT_PARAMS.items()):
        with cols[i % 3]:
            tc_scenarios.BENEFIT_PARAMS[key]["realistic"] = st.slider(
                key.replace("_", " "),
                0.0, max(1.0, vals["claim"]),
                value=float(vals["realistic"]), step=0.005, format="%.3f",
                key=f"bp_{key}",
            )

    frames = []
    for y in YEARS:
        d = calculate_total_benefit(sites, scopes, scenario, y, params="realistic")
        d["year"] = f"Y{y}"
        frames.append(d)
    ben_3y = pd.concat(frames, ignore_index=True)

    st.metric(f"Year {year} total benefit ({scenario_name})",
              f"${frames[year - 1]['annual_value'].sum():,.0f}")
    st.plotly_chart(
        px.bar(ben_3y, x="year", y="annual_value", color="benefit_line",
               title=f"Benefit breakdown by source — {scenario_name}",
               labels={"annual_value": "$/yr"}),
        width="stretch", key="benefit_stack",
    )

    claim = calculate_total_benefit(sites, scopes, scenario, year, params="claim")
    realistic = frames[year - 1]
    comp = pd.merge(
        claim[["benefit_line", "annual_value"]].rename(
            columns={"annual_value": "Genesys claim"}),
        realistic[["benefit_line", "annual_value"]].rename(
            columns={"annual_value": "Pressure-tested"}),
        on="benefit_line", how="outer",
    ).fillna(0)
    fig = go.Figure([
        go.Bar(name="Genesys claim", x=comp.benefit_line, y=comp["Genesys claim"]),
        go.Bar(name="Pressure-tested realistic", x=comp.benefit_line,
               y=comp["Pressure-tested"]),
    ])
    fig.update_layout(barmode="group", yaxis_tickformat="$,.0f",
                      title=f"Genesys claim vs pressure-tested — Year {year}")
    st.plotly_chart(fig, width="stretch", key="claim_vs_real")

# ── Page 5: Business Case ────────────────────────────────────────────

elif page == "5. Business Case":
    st.header("Business Case")
    st.session_state.implementation_cost = st.number_input(
        "One-off implementation cost (amortized over 3 years)",
        value=float(st.session_state.implementation_cost), min_value=0.0,
        step=50_000.0,
    )
    case = _case(scenario_name)

    pb = case["payback_period_years"]
    c1, c2, c3 = st.columns(3)
    c1.metric("NPV @ 8%", f"${case['npv']:,.0f}")
    c2.metric("Payback", f"{pb:.2f} yrs" if pb is not None else "never")
    c3.metric("3-Year ROI", f"{case['roi_3yr']:.0%}" if case["roi_3yr"] else "n/a")

    pnl = pd.concat(
        [
            case["cost_by_year"].drop(columns="confidence"),
            case["takeouts_by_year"].drop(columns="confidence"),
            case["benefit_by_year"].drop(columns="confidence"),
            case["net_by_year"],
        ],
        ignore_index=True,
    )
    pnl["3-yr Total"] = pnl[["Y1", "Y2", "Y3"]].sum(axis=1)
    st.dataframe(
        pnl, width="stretch", hide_index=True,
        column_config={
            c: st.column_config.NumberColumn(c, format="$%,.0f")
            for c in ("Y1", "Y2", "Y3", "3-yr Total")
        },
    )

    fig = go.Figure()
    for name in ("floor", "realistic", "stretch"):
        c = _case(name)
        fig.add_scatter(
            x=c["cumulative_net"].year, y=c["cumulative_net"].cumulative_net,
            mode="lines+markers", name=name.capitalize(),
        )
    fig.update_layout(title="Cumulative net cash flow by scenario",
                      xaxis_title="Year", yaxis_tickformat="$,.0f")
    st.plotly_chart(fig, width="stretch", key="cum_net")

# ── Page 6: Sensitivity ──────────────────────────────────────────────

elif page == "6. Sensitivity Analysis":
    st.header("Sensitivity Analysis")
    base_npv = _case(scenario_name)["npv"]
    st.caption(f"Base 3-yr NPV ({scenario_name}): ${base_npv:,.0f}")

    def _npv_with(**overrides) -> float:
        sc = dataclasses.replace(scenario, **overrides)
        return build_business_case(
            sites, scopes, meters, pricing, st.session_state.takeouts, sc,
            implementation_cost=st.session_state.implementation_cost,
            use_contracted=st.session_state.use_contracted,
        )["npv"]

    drivers = [
        "voice_bot_deflection", "voice_bot_avg_minutes", "agentic_va_deflection",
        "voice_summarization_eligibility", "voice_knowledge_eligibility",
        "email_auto_respond_rate", "email_auto_suggest_acceptance",
    ]
    rows = []
    for d in drivers:
        base_v = getattr(scenario, d)
        lo = base_v * 0.75 if d == "voice_bot_avg_minutes" else min(base_v * 0.75, 1.0)
        hi = base_v * 1.25 if d == "voice_bot_avg_minutes" else min(base_v * 1.25, 1.0)
        rows.append({"driver": d,
                     "low": _npv_with(**{d: lo}) - base_npv,
                     "high": _npv_with(**{d: hi}) - base_npv})
    torn = pd.DataFrame(rows)
    torn["swing"] = (torn.high - torn.low).abs()
    torn = torn.sort_values("swing")
    fig = go.Figure([
        go.Bar(y=torn.driver, x=torn.low, orientation="h", name="-25%"),
        go.Bar(y=torn.driver, x=torn.high, orientation="h", name="+25%"),
    ])
    fig.update_layout(barmode="overlay", title="Tornado — NPV impact of ±25%",
                      xaxis_tickformat="$,.0f")
    st.plotly_chart(fig, width="stretch", key="tornado")

    st.subheader("Two-variable heatmap")
    xs = np.linspace(0.0, 0.50, 6)   # Email Auto-Respond rate
    ys = np.linspace(0.0, 0.25, 6)   # Agentic VA deflection
    z = [[_npv_with(email_auto_respond_rate=float(x),
                    agentic_va_deflection=float(yv)) for x in xs] for yv in ys]
    fig = go.Figure(go.Heatmap(
        x=[f"{x:.0%}" for x in xs], y=[f"{yv:.0%}" for yv in ys], z=z,
        colorbar={"title": "3-yr NPV"},
    ))
    fig.update_layout(title="NPV: Email Auto-Respond rate × Agentic VA deflection",
                      xaxis_title="Email Auto-Respond rate",
                      yaxis_title="Agentic VA deflection")
    st.plotly_chart(fig, width="stretch", key="heatmap")

    st.subheader("Break-even finder")
    rates = np.linspace(0.0, 0.50, 26)
    npvs = [_npv_with(email_auto_respond_rate=float(r)) for r in rates]
    breakeven = next((r for r, v in zip(rates, npvs) if v >= 0), None)
    if npvs[0] >= 0:
        st.success(f"Case is NPV-positive even at 0% Auto-Respond "
                   f"(${npvs[0]:,.0f}).")
    elif breakeven is not None:
        st.info(f"Break-even at ~{breakeven:.0%} email Auto-Respond rate.")
    else:
        st.error("No break-even within 0–50% Auto-Respond.")
    st.plotly_chart(
        px.line(x=rates, y=npvs,
                labels={"x": "Email Auto-Respond rate", "y": "3-yr NPV ($)"}),
        width="stretch", key="breakeven",
    )

# ── Page 7: Export ───────────────────────────────────────────────────

elif page == "7. Export":
    st.header("Export")
    case = _case(scenario_name)
    cost_frames, ben_frames = [], []
    for y in YEARS:
        d = calculate_total_cost(sites, scopes, meters, pricing, scenario, y,
                                 use_contracted=st.session_state.use_contracted)
        d["year"] = f"Y{y}"
        cost_frames.append(d)
        b = calculate_total_benefit(sites, scopes, scenario, y)
        b["year"] = f"Y{y}"
        ben_frames.append(b)

    comparison = pd.DataFrame([
        {"scenario": n, "NPV": _case(n)["npv"],
         "payback_years": _case(n)["payback_period_years"],
         "roi_3yr": _case(n)["roi_3yr"]}
        for n in ("floor", "realistic", "stretch")
    ])

    pnl = pd.concat(
        [case["cost_by_year"].drop(columns="confidence"),
         case["takeouts_by_year"].drop(columns="confidence"),
         case["benefit_by_year"].drop(columns="confidence"),
         case["net_by_year"]],
        ignore_index=True,
    )

    buf = io.BytesIO()
    with pd.ExcelWriter(buf, engine="openpyxl") as writer:
        sites_dataframe(sites).to_excel(writer, sheet_name="Inputs", index=False)
        meters_dataframe(meters).to_excel(writer, sheet_name="Meters", index=False)
        pd.concat(cost_frames).to_excel(writer, sheet_name="Cost detail", index=False)
        pd.concat(ben_frames).to_excel(writer, sheet_name="Benefit detail", index=False)
        pnl.to_excel(writer, sheet_name="Business case", index=False)
        comparison.to_excel(writer, sheet_name="Scenario comparison", index=False)
    st.download_button(
        "⬇️ Download Excel workbook",
        buf.getvalue(),
        file_name=f"ctm_token_calculator_{scenario_name}.xlsx",
        mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    )
    st.download_button(
        "⬇️ Download scenario JSON",
        scenario_state_to_json(sites, st.session_state.takeouts, scopes),
        file_name="ctm_scenario.json", mime="application/json",
    )
    st.dataframe(comparison, width="stretch", hide_index=True)
