#!/usr/bin/env python3
"""
Query / append the research ledger (what was tested historically).

Examples::

    cd /Users/robzingale/trading_bot && .venv/bin/python \\
      RenTech/strategy_stack/research_ledger_cli.py list --status fail

    .venv/bin/python RenTech/strategy_stack/research_ledger_cli.py list --family equity_intraday

    .venv/bin/python RenTech/strategy_stack/research_ledger_cli.py summary

    # Explicit flush trigger (any chat): user says LEDGERIT
    .venv/bin/python RenTech/strategy_stack/research_ledger_cli.py append \\
      --id my_new_test --token my_token --name "My idea" --family equity_trend \\
      --status fail --book research --why "OOS broke" --caveat exit_day \\
      --runner "run_foo.py" --source "this session"
"""

from __future__ import annotations

import argparse
import sys
from pathlib import Path

import pandas as pd

_REPO = Path(__file__).resolve().parents[2]
LEDGER = Path(__file__).resolve().parent / "research_ledger.csv"
TRADE_STATS = Path(__file__).resolve().parent / "research_ledger_trade_stats.csv"
COLS = [
    "id",
    "token",
    "name",
    "family",
    "status",
    "book",
    "window_start",
    "window_end",
    "capital_usd",
    "total_return_pct",
    "cagr_pct",
    "sharpe",
    "max_dd_pct",
    "vol_ann_pct",
    "alpha_ann_pct",
    "beta",
    "rho_spy",
    "metric_caveat",
    "runner_or_artifact",
    "why_works_or_fails",
    "verified_date",
    "source",
]


def _load() -> pd.DataFrame:
    if not LEDGER.exists():
        raise SystemExit(f"Missing ledger: {LEDGER}")
    return pd.read_csv(LEDGER, dtype=str).fillna("")


def cmd_list(args: argparse.Namespace) -> None:
    df = _load()
    if args.status:
        df = df[df["status"] == args.status]
    if args.family:
        df = df[df["family"].str.contains(args.family, case=False, na=False)]
    if args.token:
        df = df[df["token"].str.contains(args.token, case=False, na=False)]
    if args.book:
        df = df[df["book"].str.contains(args.book, case=False, na=False)]
    cols = ["id", "token", "status", "cagr_pct", "sharpe", "max_dd_pct", "why_works_or_fails"]
    cols = [c for c in cols if c in df.columns]
    print(df[cols].to_string(index=False))
    print(f"\n{len(df)} rows  ({LEDGER})")


def cmd_summary(_: argparse.Namespace) -> None:
    df = _load()
    print("=== Research ledger summary ===")
    print(f"File: {LEDGER}")
    print(f"Rows: {len(df)}")
    print("\nBy status:")
    print(df["status"].value_counts().to_string())
    print("\nBy family (top 20):")
    print(df["family"].value_counts().head(20).to_string())
    print("\nDefault book tokens:")
    toks = df.loc[df["status"] == "default_book", "token"].tolist()
    print("  " + ", ".join(toks))

    # Compact catalog pass rates
    print("\nCatalog pass-rates (promising / total):")
    for label, mask in [
        ("D000-D099 2021-24", df["id"].str.startswith("dtheta_2124_")),
        ("Lit v4 2016-22", df["id"].str.startswith("litv4_1622_")),
        ("Lit OOS 2022-24", df["id"].str.startswith("lit_oos_2224_")),
        ("Lit rank 2016-26", df["id"].str.startswith("litrank_1626_")),
        ("QS rank50", df["id"].str.startswith("qs_s")),
        ("French discovery", df["id"].str.startswith("french_disc_")),
        ("Trade-log (tl_*)", df["id"].str.startswith("tl_")),
    ]:
        sub = df.loc[mask]
        if not len(sub):
            continue
        n_prom = int((sub["status"] == "promising").sum())
        n_fail = int((sub["status"] == "fail").sum())
        print(f"  {label}: {n_prom}/{len(sub)} promising · {n_fail} fail")

    print("\nFails (sample, exclude bulk catalog zeros) — first 25 non-catalog:")
    fails = df.loc[df["status"] == "fail"]
    bulk = fails["id"].str.startswith(("dtheta_2124_", "litv4_", "lit_oos_", "litrank_", "french_lowdd_", "french_voltgt_", "litbase_", "litv3_"))
    sample = fails.loc[~bulk, ["token", "why_works_or_fails"]].head(25)
    for _, row in sample.iterrows():
        print(f"  - {row['token']}: {str(row['why_works_or_fails'])[:90]}")


def cmd_trades(args: argparse.Namespace) -> None:
    """Show companion trade-level stats (from research_ledger_trade_stats.csv)."""
    if not TRADE_STATS.exists():
        raise SystemExit(f"Missing {TRADE_STATS} — run trade-level mine first")
    st = pd.read_csv(TRADE_STATS)
    if args.query:
        q = args.query.lower()
        mask = (
            st["ledger_id"].astype(str).str.lower().str.contains(q, na=False)
            | st.get("token", pd.Series("", index=st.index)).astype(str).str.lower().str.contains(q, na=False)
            | st.get("family", pd.Series("", index=st.index)).astype(str).str.lower().str.contains(q, na=False)
            | st.get("artifact", pd.Series("", index=st.index)).astype(str).str.lower().str.contains(q, na=False)
        )
        st = st.loc[mask]
    if args.status:
        st = st[st["status"] == args.status]
    st = st.sort_values("sum_pnl_usd", ascending=args.worst, na_position="last")
    cols = [
        c
        for c in [
            "ledger_id",
            "token",
            "status",
            "n_trades",
            "sum_pnl_usd",
            "win_rate",
            "mean_pnl_usd",
            "max_win_usd",
            "max_loss_usd",
            "exit_day_sharpe_approx",
            "exit_day_max_dd_frac",
        ]
        if c in st.columns
    ]
    print(st[cols].head(args.limit).to_string(index=False))
    print(f"\n{len(st)} trade-stat rows  ({TRADE_STATS})")


def cmd_search(args: argparse.Namespace) -> None:
    df = _load()
    q = args.query.lower()
    mask = (
        df["id"].str.lower().str.contains(q, na=False)
        | df["token"].str.lower().str.contains(q, na=False)
        | df["name"].str.lower().str.contains(q, na=False)
        | df["why_works_or_fails"].str.lower().str.contains(q, na=False)
        | df["family"].str.lower().str.contains(q, na=False)
    )
    hit = df.loc[mask]
    cols = ["id", "token", "status", "sharpe", "cagr_pct", "max_dd_pct", "why_works_or_fails"]
    print(hit[cols].to_string(index=False))
    print(f"\n{len(hit)} hits for {args.query!r}")


def cmd_append(args: argparse.Namespace) -> None:
    df = _load()
    if (df["id"] == args.id).any():
        if not args.overwrite:
            raise SystemExit(f"id={args.id!r} already exists; pass --overwrite to replace")
        df = df[df["id"] != args.id]

    row = {c: "" for c in COLS}
    row.update(
        {
            "id": args.id,
            "token": args.token,
            "name": args.name,
            "family": args.family,
            "status": args.status,
            "book": args.book,
            "window_start": args.start or "",
            "window_end": args.end or "",
            "capital_usd": args.capital or "",
            "total_return_pct": args.return_pct or "",
            "cagr_pct": args.cagr or "",
            "sharpe": args.sharpe or "",
            "max_dd_pct": args.max_dd or "",
            "vol_ann_pct": args.vol or "",
            "alpha_ann_pct": args.alpha or "",
            "beta": args.beta or "",
            "rho_spy": args.rho_spy or "",
            "metric_caveat": args.caveat or "",
            "runner_or_artifact": args.runner or "",
            "why_works_or_fails": args.why or "",
            "verified_date": args.verified or pd.Timestamp.today().strftime("%Y-%m-%d"),
            "source": args.source or "",
        }
    )
    df = pd.concat([df, pd.DataFrame([row])], ignore_index=True)
    df.to_csv(LEDGER, index=False)
    print(f"Appended/updated id={args.id} → {LEDGER}  (n={len(df)})")
    print("Also append a short note to RESEARCH_LEDGER.md Session notes.")


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    sub = ap.add_subparsers(dest="cmd", required=True)

    p_list = sub.add_parser("list", help="Filter and print ledger rows")
    p_list.add_argument("--status", default="")
    p_list.add_argument("--family", default="")
    p_list.add_argument("--token", default="")
    p_list.add_argument("--book", default="")
    p_list.set_defaults(func=cmd_list)

    p_sum = sub.add_parser("summary", help="Status / family counts + fail list")
    p_sum.set_defaults(func=cmd_summary)

    p_search = sub.add_parser("search", help="Substring search across id/token/name/why/family")
    p_search.add_argument("query")
    p_search.set_defaults(func=cmd_search)

    p_tr = sub.add_parser("trades", help="List companion trade-log aggregates (tl_* stats)")
    p_tr.add_argument("query", nargs="?", default="", help="Optional substring filter")
    p_tr.add_argument("--status", default="")
    p_tr.add_argument("--worst", action="store_true", help="Sort ascending by sum PnL")
    p_tr.add_argument("--limit", type=int, default=40)
    p_tr.set_defaults(func=cmd_trades)

    p_app = sub.add_parser("append", help="Append or overwrite one row")
    p_app.add_argument("--id", required=True)
    p_app.add_argument("--token", required=True)
    p_app.add_argument("--name", required=True)
    p_app.add_argument("--family", required=True)
    p_app.add_argument(
        "--status",
        required=True,
        choices=(
            "default_book",
            "optional",
            "promising",
            "weak",
            "fail",
            "deprecated",
            "research_only",
            "ideation_only",
        ),
    )
    p_app.add_argument("--book", default="research")
    p_app.add_argument("--start", default="")
    p_app.add_argument("--end", default="")
    p_app.add_argument("--capital", default="")
    p_app.add_argument("--return-pct", default="")
    p_app.add_argument("--cagr", default="")
    p_app.add_argument("--sharpe", default="")
    p_app.add_argument("--max-dd", default="")
    p_app.add_argument("--vol", default="")
    p_app.add_argument("--alpha", default="")
    p_app.add_argument("--beta", default="")
    p_app.add_argument("--rho-spy", default="")
    p_app.add_argument("--caveat", default="")
    p_app.add_argument("--runner", default="")
    p_app.add_argument("--why", default="")
    p_app.add_argument("--verified", default="")
    p_app.add_argument("--source", default="")
    p_app.add_argument("--overwrite", action="store_true")
    p_app.set_defaults(func=cmd_append)

    args = ap.parse_args()
    args.func(args)


if __name__ == "__main__":
    if str(_REPO) not in sys.path:
        sys.path.insert(0, str(_REPO))
    main()
