"""
Pipeline Results Analysis
=====================================================
Analyses the CSV outputs produced by carmignac_repair.py:
  - carmignac_scores.csv      (post-surgery score history)
  - carmignac_mapping.csv     (reg_id mapping history)
  - carmignac_surgery_log.csv (surgery operations)

Produces a self-contained HTML report with interactive charts.

Usage:
    python carmignac_analysis.py
    python carmignac_analysis.py --scores path/to/scores.csv \
                                  --mapping path/to/mapping.csv \
                                  --surgery path/to/surgery_log.csv \
                                  --out report.html
"""

import argparse
import os
import sys
import numpy as np
import pandas as pd

from helpers import build_html_repair

# ─────────────────────────────────────────────────────────────
# 1. LOAD & VALIDATE
# ─────────────────────────────────────────────────────────────


def load_outputs(
    scores_path, mapping_path, surgery_path, err_isin_path=None, err_agg_path=None
):
    scores = pd.read_csv(scores_path, parse_dates=["date"])
    mapping = pd.read_csv(mapping_path, parse_dates=["date"])
    surgery = pd.read_csv(surgery_path, parse_dates=["date"])

    # Normalise dtypes
    scores["reg_id"] = scores["reg_id"].astype(str)
    mapping["reg_orig"] = mapping["reg_orig"].astype(str)
    mapping["reg_used"] = mapping["reg_used"].astype(str)
    mapping["changed"] = mapping["changed"].astype(bool)
    surgery["reg_orig"] = surgery["reg_orig"].astype(str)
    surgery["reg_from"] = surgery["reg_from"].astype(str)
    surgery["reg_to"] = surgery["reg_to"].astype(str)
    if "lookback_months" not in surgery.columns:
        surgery["lookback_months"] = 1  # backwards compat

    # Error account (optional)
    err_isin = None
    err_agg = None
    if err_isin_path and os.path.exists(err_isin_path):
        err_isin = pd.read_csv(err_isin_path, parse_dates=["date"])
        err_isin["isin"] = err_isin["isin"].astype(str)
    if err_agg_path and os.path.exists(err_agg_path):
        err_agg = pd.read_csv(err_agg_path, parse_dates=["date"])

    return scores, mapping, surgery, err_isin, err_agg


# ─────────────────────────────────────────────────────────────
# LOAD ERROR ACCOUNT (optional)
# ─────────────────────────────────────────────────────────────


def load_error_account(isin_path, agg_path):
    """
    Loads the error account CSVs produced by carmignac_diagnostics.py.
    Returns (df_err_isin, df_err_agg) or (None, None) if files not found.
    """
    if not isin_path or not agg_path:
        return None, None
    try:
        ei = pd.read_csv(isin_path, parse_dates=["date"])
        ea = pd.read_csv(agg_path, parse_dates=["date"])
        ei["isin"] = ei["isin"].astype(str)
        print(
            f"[Load] error account (ISIN) : {len(ei)} rows, "
            f"{ei['isin'].nunique()} ISINs"
        )
        print(f"[Load] error account (agg)  : {len(ea)} rows")
        return ei, ea
    except Exception as e:
        print(f"[WARN] Could not load error account: {e}")
        return None, None


# ─────────────────────────────────────────────────────────────
# 2. COMPUTE ANALYTICS
# ─────────────────────────────────────────────────────────────


def compute_analytics(scores, mapping, surgery):
    dates = sorted(scores["date"].unique())

    # ── 2.1  Sum of scores per date (post-surgery) ──────────────
    sum_post = scores.groupby("date")["score"].sum().reindex(dates).rename("sum_post")

    # ── 2.2  Reconstruct pre-surgery (counterfactual) ───────────
    # Without surgery, every reg_id that had a hard break would score 0
    # from that date backwards.  We propagate the surgery "gain" as a
    # cumulative deficit going back in time.
    gain_by_date = surgery.groupby("date")["gain_vs_no_surgery"].sum()
    # cumulative deficit = sum of gains for all surgeries at or after date t
    cumulative_deficit = pd.Series(0.0, index=dates)
    for d in dates:
        cumulative_deficit[d] = gain_by_date[gain_by_date.index >= d].sum()
    sum_pre = (sum_post - cumulative_deficit).clip(lower=0).rename("sum_pre")

    timeline = pd.DataFrame({"sum_post": sum_post, "sum_pre": sum_pre})
    timeline.index = pd.to_datetime(timeline.index)
    timeline["recovery_pct"] = np.where(
        sum_pre < sum_post,
        (sum_post - sum_pre) / sum_post.clip(lower=1e-9) * 100,
        0.0,
    )

    # ── 2.3  Per-date surgery stats ─────────────────────────────
    surgery_stats = (
        surgery.groupby("date")
        .agg(
            n_surgeries=("reg_orig", "count"),
            total_gain=("gain_vs_no_surgery", "sum"),
            avg_gain=("gain_vs_no_surgery", "mean"),
            avg_jaccard=("jaccard_composite", "mean"),
            avg_score_before=("score_before", "mean"),
            avg_score_after=("score_after", "mean"),
        )
        .reindex(dates, fill_value=0)
    )

    # ── 2.4  Score distribution over time ───────────────────────
    # Wide format: rows=dates, cols=reg_ids
    pivot = scores.pivot_table(
        index="date", columns="reg_id", values="score", aggfunc="last"
    )
    pivot = pivot.reindex(dates)
    pivot.index = pd.to_datetime(pivot.index)

    # ── 2.5  Mapping churn ──────────────────────────────────────
    # For each date, how many reg_ids are remapped (not using their original code)?
    churn = (
        mapping.groupby("date")["changed"]
        .sum()
        .reindex(dates, fill_value=0)
        .rename("n_remapped")
    )

    # ── 2.6  Score entropy (distribution spread) ────────────────
    def entropy(row):
        p = row.dropna()
        p = p[p > 0]
        if len(p) == 0:
            return np.nan
        p = p / p.sum()
        return -(p * np.log(p)).sum()

    timeline["entropy"] = pivot.apply(entropy, axis=1).values

    # ── 2.7  Individual score trajectories ──────────────────────
    # Identify which reg_ids were ever remapped
    ever_remapped = set(mapping.loc[mapping["changed"], "reg_orig"].unique())

    # ── 2.8  Surgery detail table ───────────────────────────────
    surgery_detail = surgery.copy()
    surgery_detail["gain_pct_of_score"] = (
        surgery_detail["gain_vs_no_surgery"]
        / surgery_detail["score_before"].clip(lower=1e-9)
        * 100
    ).round(2)

    return {
        "timeline": timeline,
        "surgery_stats": surgery_stats,
        "pivot": pivot,
        "churn": churn,
        "ever_remapped": ever_remapped,
        "surgery_detail": surgery_detail,
        "dates": [d.strftime("%Y-%m-%d") for d in dates],
    }


# ─────────────────────────────────────────────────────────────
# 3. PRINT CONSOLE SUMMARY
# ─────────────────────────────────────────────────────────────


def print_summary(analytics, surgery):
    tl = analytics["timeline"]
    ss = analytics["surgery_stats"]

    print("\n" + "=" * 65)
    print("  CARMIGNAC PIPELINE — RESULTS SUMMARY")
    print("=" * 65)

    print(f"\n  Date range   : {tl.index.min().date()} → {tl.index.max().date()}")
    print(f"  Total months : {len(tl)}")
    print(f"  Reg IDs      : {analytics['pivot'].shape[1]}")

    print("\n  ── Score (Σ) ──────────────────────────────────────────")
    print(f"  At t_ref (latest)  : {tl['sum_post'].iloc[-1]:.6f}")
    print(f"  At t_min (earliest): {tl['sum_post'].iloc[0]:.6f}")
    print(
        f"  Min (post-surgery) : {tl['sum_post'].min():.6f}  "
        f"({tl['sum_post'].idxmin().date()})"
    )
    print(
        f"  Min (pre-surgery)  : {tl['sum_pre'].min():.6f}  "
        f"({tl['sum_pre'].idxmin().date()})"
    )
    print(f"  Max recovery (pct) : {tl['recovery_pct'].max():.2f}%")

    print("\n  ── Surgeries ─────────────────────────────────────────")
    if len(surgery) == 0:
        print("  No surgeries performed.")
    else:
        print(f"  Total operations   : {len(surgery)}")
        print(f"  Total score gained : {surgery['gain_vs_no_surgery'].sum():.6f}")
        print(f"  Avg Jaccard        : {surgery['jaccard_composite'].mean():.4f}")
        print(f"  Avg gain / surgery : {surgery['gain_vs_no_surgery'].mean():.6f}")
        print()
        print(
            f"  {'Date':12s} {'Reg orig':12s} {'From':15s} {'To':15s} "
            f"{'Jaccard':>8s} {'Gain':>10s}"
        )
        print("  " + "-" * 78)
        for _, row in surgery.sort_values("date").iterrows():
            print(
                f"  {str(row['date'].date()):12s} {row['reg_orig']:12s} "
                f"{row['reg_from']:15s} {row['reg_to']:15s} "
                f"{row['jaccard_composite']:8.4f} {row['gain_vs_no_surgery']:10.6f}"
            )

    print("\n  ── Mapping churn ─────────────────────────────────────")
    ch = analytics["churn"]
    print(
        f"  Max remapped at one date  : {int(ch.max())}  ({ch.idxmax().date() if ch.max() > 0 else 'N/A'})"
    )
    print(f"  Reg IDs ever remapped     : {len(analytics['ever_remapped'])}")

    print("\n  ── Score entropy (distribution spread) ───────────────")
    ent = analytics["timeline"]["entropy"]
    print(f"  Mean entropy : {ent.mean():.4f}")
    print(f"  Std  entropy : {ent.std():.4f}")
    print()


# ─────────────────────────────────────────────────────────────
# MAIN
# ─────────────────────────────────────────────────────────────


def main():
    parser = argparse.ArgumentParser(description="Carmignac pipeline results analyser")
    parser.add_argument("--scores", default="repair_results/carmignac_scores.csv")
    parser.add_argument("--mapping", default="repair_results/carmignac_mapping.csv")
    parser.add_argument("--surgery", default="repair_results/carmignac_surgery_log.csv")
    parser.add_argument("--out", default="repair_results/carmignac_report.html")
    parser.add_argument(
        "--error-account-isin",
        default=None,
        dest="error_isin",
        help="Path to carmignac_error_account.csv (optional)",
    )
    parser.add_argument(
        "--error-account-agg",
        default=None,
        dest="error_agg",
        help="Path to carmignac_error_account_agg.csv (optional)",
    )
    args = parser.parse_args()

    # Resolve paths relative to this script's directory if files not found
    base = os.path.dirname(os.path.abspath(__file__))

    def resolve(p, required=True):
        if p is None:
            return None
        if os.path.exists(p):
            return p
        alt = os.path.join(base, p)
        if os.path.exists(alt):
            return alt
        if required:
            sys.exit(f"[ERROR] File not found: {p}")
        print(f"[WARN] Optional file not found: {p}")
        return None

    scores_path = resolve(args.scores)
    mapping_path = resolve(args.mapping)
    surgery_path = resolve(args.surgery)
    error_isin_path = resolve(args.error_isin, required=False)
    error_agg_path = resolve(args.error_agg, required=False)

    print(f"[Load] scores  : {scores_path}")
    print(f"[Load] mapping : {mapping_path}")
    print(f"[Load] surgery : {surgery_path}")

    scores, mapping, surgery, df_err_isin, df_err_agg = load_outputs(
        scores_path,
        mapping_path,
        surgery_path,
        err_isin_path=error_isin_path,
        err_agg_path=error_agg_path,
    )
    analytics = compute_analytics(scores, mapping, surgery)

    print_summary(analytics, surgery)

    html = build_html_repair(
        analytics,
        surgery,
        scores,
        mapping,
        df_err_isin=df_err_isin,
        df_err_agg=df_err_agg,
    )

    out_path = "../" + args.out
    os.makedirs(os.path.dirname(out_path), exist_ok=True)
    with open(out_path, "w", encoding="utf-8") as f:
        f.write(html)
    print(f"\n[Report] Written to → {out_path}")


if __name__ == "__main__":
    main()
