from __future__ import annotations

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

from src.tools.trader_desk_shadow_runner import RUNTIME_DIR, STATE_PATH, DECISIONS, OUTCOMES

DEFAULT_OUTPUT = RUNTIME_DIR / "scorecard_latest.json"
DEFAULT_FINANCE_OUTPUT = Path("/home/agent/jarvis_runtime/finance-system/crypto_trader/trader_desk_scorecard_latest.json")


def D(value: Any, default: str = "0") -> Decimal:
    try:
        return Decimal(str(value))
    except Exception:
        return Decimal(default)


def q(value: Decimal, places: str = "0.01") -> str:
    return str(value.quantize(Decimal(places), rounding=ROUND_HALF_UP))


def _now() -> str:
    return datetime.now(timezone.utc).isoformat()


def _json_default(value: Any) -> Any:
    if isinstance(value, Decimal):
        return str(value)
    return str(value)


def read_jsonl(path: Path, *, limit: int | None = None) -> list[dict[str, Any]]:
    if not path.exists():
        return []
    rows: list[dict[str, Any]] = []
    lines = path.read_text(encoding="utf-8").splitlines()
    if limit is not None and limit > 0:
        lines = lines[-limit:]
    for line in lines:
        if not line.strip():
            continue
        try:
            row = json.loads(line)
        except json.JSONDecodeError:
            continue
        if isinstance(row, dict):
            rows.append(row)
    return rows


def read_state(path: Path) -> dict[str, Any]:
    if not path.exists():
        return {}
    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
        return payload if isinstance(payload, dict) else {}
    except Exception:
        return {}


def write_json(path: Path, payload: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(json.dumps(payload, indent=2, sort_keys=True, default=_json_default), encoding="utf-8")
    tmp.replace(path)


def score_bucket(score: Any) -> str:
    value = D(score)
    if value >= Decimal("85"):
        return "85_plus"
    if value >= Decimal("80"):
        return "80_85"
    if value >= Decimal("75"):
        return "75_80"
    if value >= Decimal("70"):
        return "70_75"
    return "below_70"


def _empty_group() -> dict[str, Any]:
    return {
        "decisions": 0,
        "tiny_live_candidates": 0,
        "watch_shadow": 0,
        "blocked": 0,
        "closed": 0,
        "wins": 0,
        "losses": 0,
        "scratch": 0,
        "realized_r": Decimal("0"),
    }


def _add_decision(group: dict[str, Any], row: dict[str, Any]) -> None:
    group["decisions"] += 1
    action = str(row.get("action") or "")
    if action == "tiny_live_candidate":
        group["tiny_live_candidates"] += 1
    elif action == "watch_shadow":
        group["watch_shadow"] += 1
    elif action == "no_trade":
        group["blocked"] += 1


def _add_outcome(group: dict[str, Any], row: dict[str, Any]) -> None:
    if row.get("event") != "shadow_close":
        return
    group["closed"] += 1
    bucket = str(row.get("bucket") or "")
    if bucket == "win":
        group["wins"] += 1
    elif bucket == "loss":
        group["losses"] += 1
    else:
        group["scratch"] += 1
    group["realized_r"] += D(row.get("r_multiple"))


def _finalize_group(group: dict[str, Any]) -> dict[str, Any]:
    closed = int(group["closed"])
    wins = int(group["wins"])
    decisions = int(group["decisions"])
    realized_r = D(group["realized_r"])
    return {
        "decisions": decisions,
        "tiny_live_candidates": int(group["tiny_live_candidates"]),
        "watch_shadow": int(group["watch_shadow"]),
        "blocked": int(group["blocked"]),
        "closed": closed,
        "wins": wins,
        "losses": int(group["losses"]),
        "scratch": int(group["scratch"]),
        "win_rate_pct": q((Decimal(wins) / Decimal(closed) * Decimal("100")) if closed else Decimal("0")),
        "realized_r": q(realized_r, "0.0001"),
        "avg_r": q((realized_r / Decimal(closed)) if closed else Decimal("0"), "0.0001"),
        "closure_rate_pct": q((Decimal(closed) / Decimal(decisions) * Decimal("100")) if decisions else Decimal("0")),
    }


def build_scorecard(
    *,
    decisions_path: Path = DECISIONS,
    outcomes_path: Path = OUTCOMES,
    state_path: Path = STATE_PATH,
    limit: int | None = None,
) -> dict[str, Any]:
    decisions = [row for row in read_jsonl(decisions_path, limit=limit) if row.get("event") == "desk_decision"]
    outcomes = read_jsonl(outcomes_path, limit=limit)
    closed_outcomes = [row for row in outcomes if row.get("event") == "shadow_close"]
    state = read_state(state_path)

    overall = _empty_group()
    by_coin: dict[str, dict[str, Any]] = defaultdict(_empty_group)
    by_action: dict[str, dict[str, Any]] = defaultdict(_empty_group)
    by_setup: dict[str, dict[str, Any]] = defaultdict(_empty_group)
    by_score_bucket: dict[str, dict[str, Any]] = defaultdict(_empty_group)

    for row in decisions:
        coin = str(row.get("coin") or "UNKNOWN").upper()
        action = str(row.get("action") or "unknown")
        setup = str(row.get("setup") or "unknown")
        bucket = score_bucket(row.get("score"))
        for group in (overall, by_coin[coin], by_action[action], by_setup[setup], by_score_bucket[bucket]):
            _add_decision(group, row)

    for row in closed_outcomes:
        coin = str(row.get("coin") or "UNKNOWN").upper()
        side = str(row.get("side") or "unknown")
        # Historical outcome rows may not contain action/setup; coin and side remain reliable.
        for group in (overall, by_coin[coin], by_action[f"outcome_side_{side}"]):
            _add_outcome(group, row)

    positions_raw = state.get("positions")
    open_positions: dict[str, Any] = positions_raw if isinstance(positions_raw, dict) else {}
    return {
        "schema_version": "trader_desk_shadow_scorecard.v1",
        "generated_at": _now(),
        "source": {
            "decisions_path": str(decisions_path),
            "outcomes_path": str(outcomes_path),
            "state_path": str(state_path),
            "limit": limit,
        },
        "safety_boundary": {
            "paper_only": True,
            "live_order_allowed": False,
            "mainnet_signed_action": False,
            "dashboard_executes_orders": False,
        },
        "overall": _finalize_group(overall),
        "open_shadow_positions": len(open_positions),
        "open_positions": [
            {
                "coin": coin,
                "side": pos.get("side"),
                "entry": pos.get("entry"),
                "stop_loss": pos.get("stop_loss"),
                "take_profit": pos.get("take_profit") or {},
                "score": pos.get("score"),
                "action": pos.get("action"),
                "setup": pos.get("setup"),
                "opened_at": pos.get("opened_at"),
            }
            for coin, pos in sorted(open_positions.items())
            if isinstance(pos, dict)
        ],
        "by_coin": {key: _finalize_group(value) for key, value in sorted(by_coin.items())},
        "by_action": {key: _finalize_group(value) for key, value in sorted(by_action.items())},
        "by_setup": {key: _finalize_group(value) for key, value in sorted(by_setup.items())},
        "by_score_bucket": {key: _finalize_group(value) for key, value in sorted(by_score_bucket.items())},
        "live_order_allowed": False,
        "mainnet_signed_action": False,
        "paper_only": True,
    }


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Build JARVIS Trader Desk shadow scorecard from decision/outcome journals.")
    parser.add_argument("--runtime-dir", default=str(RUNTIME_DIR))
    parser.add_argument("--output", default=str(DEFAULT_OUTPUT))
    parser.add_argument("--finance-output", default=str(DEFAULT_FINANCE_OUTPUT))
    parser.add_argument("--no-export-finance", action="store_true")
    parser.add_argument("--limit", type=int, default=0, help="Only read the latest N journal rows per file; 0 means all rows.")
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args(argv)

    runtime_dir = Path(args.runtime_dir).expanduser()
    limit = args.limit if args.limit and args.limit > 0 else None
    scorecard = build_scorecard(
        decisions_path=runtime_dir / "decision_journal.jsonl",
        outcomes_path=runtime_dir / "outcome_journal.jsonl",
        state_path=runtime_dir / "state.json",
        limit=limit,
    )
    output = Path(args.output).expanduser()
    if not output.is_absolute() and args.runtime_dir != str(RUNTIME_DIR):
        output = runtime_dir / output
    write_json(output, scorecard)
    finance_output: str | None = None
    if not args.no_export_finance:
        finance_target = Path(args.finance_output).expanduser()
        write_json(finance_target, scorecard)
        finance_output = str(finance_target)
    result = {
        "status": "ok",
        "output": str(output),
        "finance_output": finance_output,
        "overall": scorecard["overall"],
        "open_shadow_positions": scorecard["open_shadow_positions"],
        "paper_only": True,
        "live_order_allowed": False,
        "mainnet_signed_action": False,
    }
    if args.json:
        print(json.dumps(result, indent=2, sort_keys=True, default=_json_default))
    else:
        overall = result["overall"]
        print(
            "JARVIS Trader Desk Shadow Scorecard: "
            f"decisions={overall['decisions']}, closed={overall['closed']}, "
            f"win_rate={overall['win_rate_pct']}%, avg_r={overall['avg_r']}, "
            "paper_only=true, live=false, signed=false"
        )
    return 0


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