"""
Finance — Business KPIs: LTV, CAC, Churn, MRR/ARR, Payback, Cohort metrics, and more.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

import numpy as np
import pandas as pd

from bizanalyticsai.core.dataframe import BizFrame


# ── Pandas frequency alias compat (pandas ≥ 2.2 dropped 'M'/'Q'/'Y') ─────────
_FREQ_MAP = {"M": "ME", "Q": "QE", "Y": "YE", "A": "YE",
             "BM": "BME", "MS": "MS", "QS": "QS", "AS": "YS", "BMS": "BMS"}

def _resample_freq(period: str) -> str:
    """Convert legacy period alias to pandas ≥ 2.2 resample-compatible alias."""
    return _FREQ_MAP.get(period.upper(), period)


# ── KPI Dataclasses ───────────────────────────────────────────────────────────

@dataclass
class ChurnMetrics:
    monthly_rate:    float    # %
    annual_rate:     float    # %
    ltv:             float    # absolute currency
    avg_lifetime_months: float
    retention_12m:   float    # % of customers retained at 12 months
    at_risk_count:   int
    at_risk_pct:     float

    def summary(self) -> str:
        return (
            f"Churn: {self.monthly_rate:.2f}%/mo | Annual: {self.annual_rate:.1f}% | "
            f"LTV: {self.ltv:,.2f} | Lifetime: {self.avg_lifetime_months:.1f}mo | "
            f"Retention 12m: {self.retention_12m:.1f}%"
        )


@dataclass
class FinancialKPIs:
    mrr:            float
    arr:            float
    gross_margin:   float    # %
    net_margin:     float    # %
    cac:            float
    ltv:            float
    ltv_cac_ratio:  float
    payback_months: float
    roi:            float    # %
    burn_rate:      float | None = None
    runway_months:  float | None = None
    extra:          dict = field(default_factory=dict)

    def summary(self) -> str:
        lines = [
            f"MRR: R${self.mrr:,.0f}  |  ARR: R${self.arr:,.0f}",
            f"Gross Margin: {self.gross_margin:.1f}%  |  Net Margin: {self.net_margin:.1f}%",
            f"CAC: R${self.cac:,.0f}  |  LTV: R${self.ltv:,.0f}  |  LTV/CAC: {self.ltv_cac_ratio:.2f}x",
            f"Payback: {self.payback_months:.1f} months  |  ROI: {self.roi:.1f}%",
        ]
        if self.burn_rate:
            lines.append(f"Burn Rate: R${self.burn_rate:,.0f}/mo  |  Runway: {self.runway_months:.1f}mo")
        return "\n".join(lines)

    def is_investable(self) -> bool:
        """Returns True if key SaaS investor thresholds are met."""
        return (
            self.ltv_cac_ratio >= 3.0
            and self.gross_margin >= 60
            and self.payback_months <= 24
        )


# ── Core functions ─────────────────────────────────────────────────────────────

def calc_ltv(arpu: float, monthly_churn_pct: float) -> float:
    """LTV = ARPU / monthly_churn_rate"""
    if monthly_churn_pct <= 0:
        return float("inf")
    return round(arpu / (monthly_churn_pct / 100), 2)


def calc_ltv_cac(ltv: float, cac: float) -> float:
    """LTV/CAC ratio — should be >= 3 for sustainable businesses."""
    if cac == 0:
        return float("inf")
    return round(ltv / cac, 2)


def calc_payback(cac: float, arpu: float, gross_margin_pct: float) -> float:
    """Months to recover CAC. payback = CAC / (ARPU * gross_margin)"""
    effective = arpu * (gross_margin_pct / 100)
    if effective <= 0:
        return float("inf")
    return round(cac / effective, 1)


def calc_mrr(df: pd.DataFrame, revenue_col: str, date_col: str, period: str = "M") -> BizFrame:
    """
    Compute Monthly Recurring Revenue from a transaction DataFrame.

    Args:
        df:          DataFrame with one revenue row per transaction.
        revenue_col: Column with revenue amounts.
        date_col:    Column with transaction dates.
        period:      Pandas period alias ('M' = month, 'Q' = quarter, 'Y' = year).

    Returns:
        BizFrame with period, mrr, cumulative_mrr, growth_pct columns.
    """
    out = df.copy()
    out[date_col] = pd.to_datetime(out[date_col])
    grouped = out.set_index(date_col)[revenue_col].resample(_resample_freq(period)).sum().reset_index()
    grouped.columns = ["period", "mrr"]
    grouped["cumulative_mrr"] = grouped["mrr"].cumsum()
    grouped["growth_pct"]     = grouped["mrr"].pct_change() * 100
    return BizFrame(grouped)


def calc_arr(mrr: float) -> float:
    return round(mrr * 12, 2)


def calc_gross_margin(revenue: float, cogs: float) -> float:
    if revenue == 0:
        return 0.0
    return round((revenue - cogs) / revenue * 100, 2)


def calc_burn_rate(df: pd.DataFrame, cost_col: str, date_col: str) -> float:
    """Average monthly burn rate from a cost DataFrame."""
    out = df.copy()
    out[date_col] = pd.to_datetime(out[date_col])
    monthly = out.set_index(date_col)[cost_col].resample("ME").sum()
    return round(float(monthly.mean()), 2)


def calc_runway(cash_balance: float, monthly_burn: float) -> float:
    """Months of runway given current cash and burn rate."""
    if monthly_burn <= 0:
        return float("inf")
    return round(cash_balance / monthly_burn, 1)


def kpis(
    df: pd.DataFrame,
    revenue_col: str,
    cost_col: str,
    *,
    customer_col: str | None = None,
    date_col: str | None = None,
    cac: float = 0,
    monthly_churn_pct: float = 5,
    cash_balance: float | None = None,
) -> FinancialKPIs:
    """
    Compute a full set of financial KPIs from a DataFrame.

    Args:
        df:              Transaction/revenue DataFrame.
        revenue_col:     Revenue column name.
        cost_col:        Cost/COGS column name.
        customer_col:    Customer ID column for unique customer count.
        date_col:        Date column for MRR computation.
        cac:             Customer acquisition cost (if known).
        monthly_churn_pct: Monthly churn rate %.
        cash_balance:    Current cash for runway calculation.

    Returns:
        FinancialKPIs dataclass.
    """
    total_rev  = float(df[revenue_col].sum())
    total_cost = float(df[cost_col].sum())
    net        = total_rev - total_cost
    n_months   = 1

    if date_col:
        df2 = df.copy()
        df2[date_col] = pd.to_datetime(df2[date_col])
        months = df2[date_col].dt.to_period("M").nunique()
        n_months = max(months, 1)

    mrr          = total_rev / n_months
    arr          = calc_arr(mrr)
    gross_margin = calc_gross_margin(total_rev, total_cost)
    net_margin   = round(net / max(total_rev, 1) * 100, 2)
    avg_rev      = mrr

    n_customers = df[customer_col].nunique() if customer_col else 1
    avg_arpu    = mrr / max(n_customers, 1)
    ltv         = calc_ltv(avg_arpu, monthly_churn_pct)
    ltv_cac     = calc_ltv_cac(ltv, cac) if cac else 0.0
    payback     = calc_payback(cac, avg_arpu, gross_margin) if cac else 0.0
    roi         = round(net / max(abs(total_cost), 1) * 100, 2)
    burn        = total_cost / n_months
    runway      = calc_runway(cash_balance, burn) if cash_balance else None

    return FinancialKPIs(
        mrr=round(mrr, 2),
        arr=round(arr, 2),
        gross_margin=gross_margin,
        net_margin=net_margin,
        cac=cac,
        ltv=ltv,
        ltv_cac_ratio=ltv_cac,
        payback_months=payback,
        roi=roi,
        burn_rate=round(burn, 2),
        runway_months=runway,
        extra={"n_customers": n_customers, "avg_arpu": round(avg_arpu, 2)},
    )


def churn(
    df: pd.DataFrame,
    customer_col: str,
    date_col: str,
    *,
    arpu: float | None = None,
    revenue_col: str | None = None,
    period: str = "M",
) -> ChurnMetrics:
    """
    Compute churn metrics from a subscriber/transaction DataFrame.

    The DataFrame must have one row per customer per active period.
    Churn is detected when a customer present in period N is absent in period N+1.

    Args:
        df:           DataFrame with customer_col and date_col.
        customer_col: Customer/subscriber ID column.
        date_col:     Date column (transaction date or subscription period).
        arpu:         Average revenue per user per month (for LTV).
        revenue_col:  If arpu is None, compute from revenue_col.
        period:       Grouping period ('M', 'Q', 'Y').

    Returns:
        ChurnMetrics dataclass.
    """
    out = df.copy()
    out[date_col] = pd.to_datetime(out[date_col])
    out["_period"] = out[date_col].dt.to_period(period)

    periods = sorted(out["_period"].unique())
    if len(periods) < 2:
        raise ValueError("Need at least 2 periods to compute churn.")

    churned_total   = 0
    active_total    = 0

    for i in range(1, len(periods)):
        prev = set(out[out["_period"] == periods[i - 1]][customer_col])
        curr = set(out[out["_period"] == periods[i]][customer_col])
        churned_total += len(prev - curr)
        active_total  += len(prev)

    monthly_rate = (churned_total / max(active_total, 1)) * 100
    annual_rate  = (1 - (1 - monthly_rate / 100) ** 12) * 100

    if arpu is None and revenue_col:
        arpu = float(out.groupby(customer_col)[revenue_col].mean().mean())
    arpu = arpu or 0.0

    ltv          = calc_ltv(arpu, monthly_rate) if monthly_rate > 0 else float("inf")
    avg_lifetime = 1 / (monthly_rate / 100) if monthly_rate > 0 else float("inf")
    retention_12 = (1 - monthly_rate / 100) ** 12 * 100

    last_period   = periods[-1]
    prev_period   = periods[-2]
    prev_set      = set(out[out["_period"] == prev_period][customer_col])
    curr_set      = set(out[out["_period"] == last_period][customer_col])
    at_risk_count = len(prev_set - curr_set)
    at_risk_pct   = at_risk_count / max(len(prev_set), 1) * 100

    return ChurnMetrics(
        monthly_rate=round(monthly_rate, 3),
        annual_rate=round(annual_rate, 2),
        ltv=round(ltv, 2),
        avg_lifetime_months=round(avg_lifetime, 1),
        retention_12m=round(retention_12, 1),
        at_risk_count=at_risk_count,
        at_risk_pct=round(at_risk_pct, 2),
    )


def break_even(
    fixed_costs: float,
    unit_price: float,
    unit_cost: float,
) -> dict[str, Any]:
    """
    Break-even analysis.

    Returns units, revenue, margin %, contribution margin.
    """
    contribution = unit_price - unit_cost
    if contribution <= 0:
        return {"units": None, "revenue": None, "margin_pct": 0.0,
                "contribution": contribution, "viable": False}
    units   = np.ceil(fixed_costs / contribution)
    revenue = units * unit_price
    margin  = contribution / unit_price * 100
    return {
        "units":        int(units),
        "revenue":      round(revenue, 2),
        "margin_pct":   round(margin, 2),
        "contribution": round(contribution, 2),
        "viable":       True,
    }


def roi(investment: float, net_profit: float) -> float:
    """Return on Investment %."""
    if investment == 0:
        return 0.0
    return round(net_profit / investment * 100, 2)
