from __future__ import annotations

import argparse
import json
from collections import Counter, defaultdict, deque
from datetime import datetime, timezone
from decimal import Decimal
from pathlib import Path
from typing import Any

STRATEGY_ID = "candidate_swing_trend_retest_research"


def _d(value: Any, default: str = "0") -> Decimal:
    try:
        if value is None:
            return Decimal(default)
        return Decimal(str(value))
    except Exception:
        return Decimal(default)


def _read_jsonl(path: Path) -> list[dict[str, Any]]:
    if not path.exists():
        return []
    rows: list[dict[str, Any]] = []
    for line in path.read_text(encoding="utf-8", errors="ignore").splitlines():
        try:
            row = json.loads(line)
        except json.JSONDecodeError:
            continue
        if isinstance(row, dict):
            rows.append(row)
    return rows


def _parse_ts(value: Any) -> datetime | None:
    try:
        parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
        return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
    except Exception:
        return None


def _bucket_score(value: Decimal | None) -> str:
    if value is None:
        return "unknown"
    if value >= Decimal("80"):
        return ">=80"
    if value >= Decimal("72"):
        return "72-79"
    if value >= Decimal("65"):
        return "65-71"
    return "<65"


def _row_pnl(row: dict[str, Any]) -> Decimal:
    if "pnl" in row:
        return _d(row.get("pnl"))
    return _d(row.get("net_pnl_usd", row.get("realized_pnl_usd")))


def _summarize_pnls(rows: list[dict[str, Any]]) -> dict[str, Any]:
    pnls = [_row_pnl(row) for row in rows]
    wins = [p for p in pnls if p > 0]
    losses = [p for p in pnls if p < 0]
    gross_win = sum(wins, Decimal("0"))
    gross_loss = abs(sum(losses, Decimal("0")))
    return {
        "closed": len(rows),
        "wins": len(wins),
        "losses": len(losses),
        "win_rate_pct": str((Decimal(len(wins)) / Decimal(len(rows)) * Decimal("100")).quantize(Decimal("0.01")) if rows else Decimal("0.00")),
        "net_pnl_usd": str(sum(pnls, Decimal("0")).quantize(Decimal("0.0001"))),
        "avg_win_usd": str((gross_win / Decimal(len(wins))).quantize(Decimal("0.0001")) if wins else Decimal("0.0000")),
        "avg_loss_usd": str((gross_loss / Decimal(len(losses))).quantize(Decimal("0.0001")) if losses else Decimal("0.0000")),
        "profit_factor": str((gross_win / gross_loss).quantize(Decimal("0.0001")) if gross_loss > 0 else Decimal("999.0000") if gross_win > 0 else Decimal("0.0000")),
    }


def _entry_context_map(signals: list[dict[str, Any]]) -> dict[tuple[str, str], dict[str, Any]]:
    out: dict[tuple[str, str], dict[str, Any]] = {}
    latest_by_coin: dict[str, dict[str, Any]] = {}
    for sig in signals:
        coin = str(sig.get("coin") or "").upper()
        if not coin:
            continue
        latest_by_coin[coin] = sig
        decision = str(sig.get("final_decision") or "")
        if decision.startswith("paper_opened"):
            ts = str(sig.get("timestamp") or "")[:19]
            out[(coin, ts)] = sig
    # Fallback: use closest latest signal before entry by coin if exact timestamp missing.
    for coin, sig in latest_by_coin.items():
        out.setdefault((coin, "latest"), sig)
    return out


def analyze_swing_retest(*, runtime_dir: Path = Path("runtime/experiments") / STRATEGY_ID) -> dict[str, Any]:
    trades = _read_jsonl(runtime_dir / "trade_journal.jsonl")
    signals = _read_jsonl(runtime_dir / "signal_journal.jsonl")
    ctx_map = _entry_context_map(signals)
    open_by_coin: dict[str, deque[dict[str, Any]]] = defaultdict(deque)
    closed_pairs: list[dict[str, Any]] = []
    for row in trades:
        coin = str(row.get("coin") or "").upper()
        if not coin:
            continue
        if row.get("event") == "entry":
            open_by_coin[coin].append(row)
        elif row.get("event") == "exit":
            entry = open_by_coin[coin].popleft() if open_by_coin[coin] else {}
            entry_ts = str(entry.get("timestamp") or "")[:19]
            signal = ctx_map.get((coin, entry_ts)) or ctx_map.get((coin, "latest"), {})
            conf_obj = signal.get("confluence_gate")
            conf = conf_obj if isinstance(conf_obj, dict) else {}
            tv_obj = signal.get("tradingview_context")
            tv = tv_obj if isinstance(tv_obj, dict) else {}
            pnl = _d(row.get("net_pnl_usd", row.get("realized_pnl_usd")))
            conf_score_raw = conf.get("score")
            closed_pairs.append({
                "coin": coin,
                "pnl": pnl,
                "win": pnl > 0,
                "exit_reason": str(row.get("exit_reason") or "unknown"),
                "candles_held": int(_d(row.get("candles_held"))),
                "mfe_pct": _d(row.get("mfe_pct")),
                "mae_pct": _d(row.get("mae_pct")),
                "confluence_score": _d(conf_score_raw) if conf_score_raw is not None else None,
                "confluence_allowed": conf.get("allowed"),
                "tv_available": bool(tv.get("available")),
                "tv_bias": tv.get("bias"),
                "tv_alignment": tv.get("trend_alignment"),
            })
    by_coin: dict[str, list[dict[str, Any]]] = defaultdict(list)
    by_exit: dict[str, list[dict[str, Any]]] = defaultdict(list)
    by_conf_bucket: dict[str, list[dict[str, Any]]] = defaultdict(list)
    by_tv: dict[str, list[dict[str, Any]]] = defaultdict(list)
    holding_counter: Counter[str] = Counter()
    for row in closed_pairs:
        by_coin[row["coin"]].append(row)
        by_exit[row["exit_reason"]].append(row)
        by_conf_bucket[_bucket_score(row["confluence_score"])].append(row)
        tv_key = f"bias={row['tv_bias'] or 'unknown'}|align={row['tv_alignment'] or 'unknown'}|available={row['tv_available']}"
        by_tv[tv_key].append(row)
        held = row["candles_held"]
        if held >= 96:
            holding_counter["max_hold_96"] += 1
        elif held <= 4:
            holding_counter["fast_exit_<=4"] += 1
        else:
            holding_counter["mid_hold"] += 1
    losses = [row for row in closed_pairs if not row["win"]]
    loss_patterns = Counter()
    for row in losses:
        if row["exit_reason"] == "time_exit" and row["mfe_pct"] < Decimal("0.30") and row["mae_pct"] <= Decimal("-0.80"):
            loss_patterns["entry_failed_no_followthrough_time_exit"] += 1
        if row["exit_reason"] == "stop_loss":
            loss_patterns["stop_loss_hit"] += 1
        if row["mae_pct"] <= Decimal("-1.50"):
            loss_patterns["large_adverse_excursion"] += 1
        if row["mfe_pct"] >= Decimal("0.80") and row["pnl"] < 0:
            loss_patterns["had_profit_then_reversed"] += 1
    return {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "strategy_id": STRATEGY_ID,
        "research_only": True,
        "live_order_allowed": False,
        "mainnet_signed_action": False,
        "overall": _summarize_pnls(closed_pairs),
        "by_coin": {coin: _summarize_pnls(rows) for coin, rows in sorted(by_coin.items())},
        "by_exit_reason": {key: _summarize_pnls(rows) for key, rows in sorted(by_exit.items())},
        "by_confluence_score_bucket": {key: _summarize_pnls(rows) for key, rows in sorted(by_conf_bucket.items())},
        "by_tradingview_context": {key: _summarize_pnls(rows) for key, rows in sorted(by_tv.items())},
        "holding_distribution": dict(holding_counter),
        "loss_patterns": loss_patterns.most_common(),
        "open_positions_remaining": {coin: len(rows) for coin, rows in open_by_coin.items() if rows},
    }


def format_markdown(payload: dict[str, Any]) -> str:
    lines = [
        "# Swing Retest Analysis",
        "",
        f"Generated: {payload['timestamp']}",
        f"Overall: {payload['overall']}",
        "",
        "## By Coin",
    ]
    for coin, row in payload["by_coin"].items():
        lines.append(f"- {coin}: closed={row['closed']}, winrate={row['win_rate_pct']}%, PF={row['profit_factor']}, PnL={row['net_pnl_usd']} USDC, avg_win={row['avg_win_usd']}, avg_loss={row['avg_loss_usd']}")
    lines.extend(["", "## By Exit Reason"])
    for reason, row in payload["by_exit_reason"].items():
        lines.append(f"- {reason}: closed={row['closed']}, winrate={row['win_rate_pct']}%, PF={row['profit_factor']}, PnL={row['net_pnl_usd']} USDC")
    lines.extend(["", "## By Confluence Score Bucket"])
    for bucket, row in payload["by_confluence_score_bucket"].items():
        lines.append(f"- {bucket}: closed={row['closed']}, winrate={row['win_rate_pct']}%, PF={row['profit_factor']}, PnL={row['net_pnl_usd']} USDC")
    lines.extend(["", "## TradingView Context"])
    for key, row in payload["by_tradingview_context"].items():
        lines.append(f"- {key}: closed={row['closed']}, winrate={row['win_rate_pct']}%, PF={row['profit_factor']}, PnL={row['net_pnl_usd']} USDC")
    lines.extend(["", f"Holding distribution: {payload['holding_distribution']}", f"Loss patterns: {payload['loss_patterns']}", "", "Research-only; no live authorization."])
    return "\n".join(lines) + "\n"


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Analyze Swing Retest paper outcomes by coin, context, holding and exit pattern.")
    parser.add_argument("--runtime-dir", default=f"runtime/experiments/{STRATEGY_ID}")
    parser.add_argument("--output", default="runtime/reports/swing_retest_analysis_latest.json")
    parser.add_argument("--markdown-output", default="runtime/reports/swing_retest_analysis_latest.md")
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args(argv)
    payload = analyze_swing_retest(runtime_dir=Path(args.runtime_dir))
    output = Path(args.output)
    md = Path(args.markdown_output)
    output.parent.mkdir(parents=True, exist_ok=True)
    md.parent.mkdir(parents=True, exist_ok=True)
    output.write_text(json.dumps(payload, indent=2, sort_keys=True, default=str), encoding="utf-8")
    md.write_text(format_markdown(payload), encoding="utf-8")
    print(json.dumps(payload, indent=2, sort_keys=True, default=str) if args.json else format_markdown(payload))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
