"""Write all output artifacts for a backtest run."""
from __future__ import annotations

import csv
import datetime as dt
import json
from collections import defaultdict
from pathlib import Path
from typing import Any

import pyarrow as pa
import pyarrow.parquet as pq

from libs.backtest.domain import (
    BacktestConfig,
    DailyPortfolioState,
    ExperimentManifest,
    ExperimentResult,
    FilledTrade,
    MetricsBundle,
    OpenPosition,
)
from libs.common.logging import get_logger

logger = get_logger(__name__)


def create_run_directory(output_root: str | Path, run_id: str) -> Path:
    """Create the full run directory tree. Returns the run root path."""
    run_dir = Path(output_root) / run_id
    for subdir in ["logs", "metrics", "artifacts", "plots", "notes"]:
        (run_dir / subdir).mkdir(parents=True, exist_ok=True)
    logger.info("run_directory_created", path=str(run_dir))
    return run_dir


def write_metadata(
    run_dir: Path,
    run_id: str,
    started_at: dt.datetime,
    finished_at: dt.datetime,
    git_hash: str,
    total_trading_days: int,
    total_candidates_seen: int,
    total_orders_rejected: int,
    split_name: str | None = None,
) -> Path:
    """Write metadata.json."""
    meta = {
        "run_id": run_id,
        "started_at": started_at.isoformat(),
        "finished_at": finished_at.isoformat(),
        "elapsed_seconds": (finished_at - started_at).total_seconds(),
        "git_commit_hash": git_hash,
        "total_trading_days": total_trading_days,
        "total_candidates_seen": total_candidates_seen,
        "total_orders_rejected": total_orders_rejected,
    }
    if split_name is not None:
        meta["split_name"] = split_name
    out = run_dir / "metadata.json"
    out.write_text(json.dumps(meta, indent=2))
    return out


def write_metrics_summary(run_dir: Path, metrics: MetricsBundle) -> Path:
    """Write metrics/metrics_summary.json."""
    out = run_dir / "metrics" / "metrics_summary.json"
    out.write_text(metrics.model_dump_json(indent=2))
    return out


def write_trade_blotter(
    run_dir: Path,
    trades: list[FilledTrade],
) -> Path | None:
    """Write artifacts/trade_blotter.parquet."""
    if not trades:
        return None
    rows = [
        {
            "trade_id": t.trade_id,
            "position_id": t.position_id,
            "event_id": t.event_id,
            "symbol": t.symbol,
            "event_date": t.event_date.isoformat() if t.event_date else None,
            "timing_class": t.timing_class,
            "engine_id": t.engine_id,
            "entry_timing_policy": t.entry_timing_policy,
            "shadow_only": t.shadow_only,
            "entry_date": t.entry_date.isoformat(),
            "exit_date": t.exit_date.isoformat(),
            "entry_price": t.entry_price,
            "exit_price": t.exit_price,
            "exit_reason": t.exit_reason.value,
            "shares": t.shares,
            "commission": t.commission,
            "slippage_bps": t.slippage_bps,
            "gross_pnl": t.gross_pnl,
            "net_pnl": t.net_pnl,
            "pnl_pct": t.pnl_pct,
            "r_multiple": t.r_multiple,
            "holding_days": t.holding_days,
        }
        for t in trades
    ]
    out = run_dir / "artifacts" / "trade_blotter.parquet"
    _write_parquet(rows, out)
    return out


def write_daily_equity_curve(
    run_dir: Path,
    equity_curve: list[DailyPortfolioState],
) -> Path | None:
    """Write artifacts/daily_equity_curve.parquet."""
    if not equity_curve:
        return None
    rows = [
        {
            "date": s.date.isoformat(),
            "equity": s.equity,
            "cash_available": s.cash_available,
            "gross_exposure": s.gross_exposure,
            "net_exposure": s.net_exposure,
            "unrealized_pnl": s.unrealized_pnl,
            "realized_pnl": s.realized_pnl,
            "open_position_count": len(s.open_positions),
            "daily_new_risk_used": s.daily_new_risk_used,
            "peak_equity": s.peak_equity,
            "current_drawdown_pct": s.current_drawdown_pct,
        }
        for s in equity_curve
    ]
    out = run_dir / "artifacts" / "daily_equity_curve.parquet"
    _write_parquet(rows, out)
    return out


def write_position_timeline(
    run_dir: Path,
    trades: list[FilledTrade],
    open_positions: list[OpenPosition] | None = None,
) -> Path | None:
    """Write artifacts/position_timeline.parquet (one row per position)."""
    rows = []
    for t in trades:
        rows.append(
                {
                    "position_id": t.position_id,
                    "event_id": t.event_id,
                    "symbol": t.symbol,
                    "event_date": t.event_date.isoformat() if t.event_date else None,
                    "timing_class": t.timing_class,
                    "engine_id": t.engine_id,
                    "entry_timing_policy": t.entry_timing_policy,
                    "shadow_only": t.shadow_only,
                    "entry_date": t.entry_date.isoformat(),
                    "exit_date": t.exit_date.isoformat(),
                    "entry_price": t.entry_price,
                    "exit_price": t.exit_price,
                "exit_reason": t.exit_reason.value,
                "shares": t.shares,
                "net_pnl": t.net_pnl,
                "r_multiple": t.r_multiple,
                "holding_days": t.holding_days,
                "status": "closed",
            }
        )
    if open_positions:
        for p in open_positions:
            rows.append(
                {
                    "position_id": p.position_id,
                    "event_id": p.plan.candidate.event_id,
                    "symbol": p.plan.candidate.symbol,
                    "event_date": p.plan.event_date.isoformat() if p.plan.event_date else None,
                    "timing_class": p.plan.timing_class,
                    "engine_id": p.plan.engine_id,
                    "entry_timing_policy": p.plan.entry_timing_policy,
                    "shadow_only": p.plan.shadow_only,
                    "entry_date": p.entry_date.isoformat(),
                    "exit_date": None,
                    "entry_price": p.entry_price,
                    "exit_price": None,
                    "exit_reason": None,
                    "shares": p.shares_total,
                    "net_pnl": None,
                    "r_multiple": None,
                    "holding_days": p.days_held,
                    "status": p.status.value,
                }
            )
    if not rows:
        return None
    out = run_dir / "artifacts" / "position_timeline.parquet"
    _write_parquet(rows, out)
    return out


def write_attribution_by_event_type(
    run_dir: Path,
    trades: list[FilledTrade],
    candidate_map: dict[str, Any],
) -> Path:
    """Write metrics/attribution_by_event_type.csv."""
    bucket_data: dict[str, dict[str, float | int]] = defaultdict(
        lambda: {"count": 0, "wins": 0, "net_pnl": 0.0, "avg_r": 0.0, "_r_sum": 0.0}
    )
    for t in trades:
        cand = candidate_map.get(t.trade_id)
        et = getattr(cand, "event_type", "unknown") if cand else "unknown"
        d = bucket_data[et]
        d["count"] = int(d["count"]) + 1
        if t.net_pnl > 0:
            d["wins"] = int(d["wins"]) + 1
        d["net_pnl"] = float(d["net_pnl"]) + t.net_pnl
        d["_r_sum"] = float(d["_r_sum"]) + t.r_multiple

    out = run_dir / "metrics" / "attribution_by_event_type.csv"
    with open(out, "w", newline="") as f:
        writer = csv.DictWriter(
            f, fieldnames=["event_type", "count", "wins", "win_rate", "net_pnl", "avg_r"]
        )
        writer.writeheader()
        for et, d in sorted(bucket_data.items()):
            count = int(d["count"])
            wins = int(d["wins"])
            writer.writerow(
                {
                    "event_type": et,
                    "count": count,
                    "wins": wins,
                    "win_rate": wins / count if count > 0 else 0.0,
                    "net_pnl": round(float(d["net_pnl"]), 4),
                    "avg_r": round(float(d["_r_sum"]) / count if count > 0 else 0.0, 4),
                }
            )
    return out


def write_attribution_by_sector(
    run_dir: Path,
    trades: list[FilledTrade],
    candidate_map: dict[str, Any],
) -> Path:
    """Write metrics/attribution_by_sector.csv."""
    bucket_data: dict[str, dict[str, float | int]] = defaultdict(
        lambda: {"count": 0, "wins": 0, "net_pnl": 0.0, "_r_sum": 0.0}
    )
    for t in trades:
        cand = candidate_map.get(t.trade_id)
        sector = getattr(cand, "sector", "UNKNOWN") if cand else "UNKNOWN"
        d = bucket_data[sector]
        d["count"] = int(d["count"]) + 1
        if t.net_pnl > 0:
            d["wins"] = int(d["wins"]) + 1
        d["net_pnl"] = float(d["net_pnl"]) + t.net_pnl
        d["_r_sum"] = float(d["_r_sum"]) + t.r_multiple

    out = run_dir / "metrics" / "attribution_by_sector.csv"
    with open(out, "w", newline="") as f:
        writer = csv.DictWriter(
            f, fieldnames=["sector", "count", "wins", "win_rate", "net_pnl", "avg_r"]
        )
        writer.writeheader()
        for sector, d in sorted(bucket_data.items()):
            count = int(d["count"])
            wins = int(d["wins"])
            writer.writerow(
                {
                    "sector": sector,
                    "count": count,
                    "wins": wins,
                    "win_rate": wins / count if count > 0 else 0.0,
                    "net_pnl": round(float(d["net_pnl"]), 4),
                    "avg_r": round(float(d["_r_sum"]) / count if count > 0 else 0.0, 4),
                }
            )
    return out


def write_attribution_by_engine(
    run_dir: Path,
    trades: list[FilledTrade],
    per_engine_metrics: dict[str, dict[str, Any]] | None = None,
) -> Path:
    """Write metrics/attribution_by_engine.csv."""
    bucket_data: dict[str, dict[str, float | int | bool]] = defaultdict(
        lambda: {"count": 0, "wins": 0, "net_pnl": 0.0, "_r_sum": 0.0, "shadow_only": False}
    )
    for t in trades:
        engine_id = t.engine_id or "default"
        d = bucket_data[engine_id]
        d["count"] = int(d["count"]) + 1
        if t.net_pnl > 0:
            d["wins"] = int(d["wins"]) + 1
        d["net_pnl"] = float(d["net_pnl"]) + t.net_pnl
        d["_r_sum"] = float(d["_r_sum"]) + t.r_multiple
        d["shadow_only"] = bool(t.shadow_only)

    if per_engine_metrics:
        for engine_id, summary in per_engine_metrics.items():
            d = bucket_data.setdefault(
                engine_id,
                {"count": 0, "wins": 0, "net_pnl": 0.0, "_r_sum": 0.0, "shadow_only": False},
            )
            d["shadow_only"] = bool(summary.get("shadow_only", d["shadow_only"]))

    out = run_dir / "metrics" / "attribution_by_engine.csv"
    with open(out, "w", newline="") as f:
        writer = csv.DictWriter(
            f,
            fieldnames=[
                "engine_id",
                "shadow_only",
                "count",
                "wins",
                "win_rate",
                "net_pnl",
                "avg_r",
            ],
        )
        writer.writeheader()
        for engine_id, d in sorted(bucket_data.items()):
            count = int(d["count"])
            wins = int(d["wins"])
            writer.writerow(
                {
                    "engine_id": engine_id,
                    "shadow_only": bool(d["shadow_only"]),
                    "count": count,
                    "wins": wins,
                    "win_rate": wins / count if count > 0 else 0.0,
                    "net_pnl": round(float(d["net_pnl"]), 4),
                    "avg_r": round(float(d["_r_sum"]) / count if count > 0 else 0.0, 4),
                }
            )
    return out


def write_score_bucket_report(
    run_dir: Path,
    score_bucket_hit_rate: dict[str, float],
    trades: list[FilledTrade],
    candidate_map: dict[str, Any],
) -> Path:
    """Write metrics/score_bucket_report.csv."""
    bucket_counts: dict[str, int] = defaultdict(int)
    for t in trades:
        cand = candidate_map.get(t.trade_id)
        bucket = getattr(cand, "score_bucket", "unknown") if cand else "unknown"
        bucket_counts[bucket] += 1

    out = run_dir / "metrics" / "score_bucket_report.csv"
    with open(out, "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=["score_bucket", "trade_count", "win_rate"])
        writer.writeheader()
        for bucket in sorted(set(list(score_bucket_hit_rate.keys()) + list(bucket_counts.keys()))):
            writer.writerow(
                {
                    "score_bucket": bucket,
                    "trade_count": bucket_counts.get(bucket, 0),
                    "win_rate": round(score_bucket_hit_rate.get(bucket, 0.0), 4),
                }
            )
    return out


def write_plots(run_dir: Path, generate: bool = False) -> Path:
    """Create plots directory. generate=True logs a warning (matplotlib not available)."""
    plots_dir = run_dir / "plots"
    plots_dir.mkdir(exist_ok=True)
    if generate:
        logger.warning(
            "plots_not_implemented",
            message="generate_plots=True is a no-op; matplotlib is not in dependencies.",
        )
    return plots_dir


def write_run_notes(run_dir: Path, notes: str = "") -> Path:
    """Write notes/run_notes.md."""
    out = run_dir / "notes" / "run_notes.md"
    out.write_text(notes or "# Run Notes\n\n_No notes provided._\n")
    return out


def write_per_engine_metrics(
    run_dir: Path,
    per_engine_metrics: dict[str, dict[str, Any]],
) -> Path:
    """Write metrics/per_engine_metrics.json."""
    out = run_dir / "metrics" / "per_engine_metrics.json"
    out.write_text(json.dumps(per_engine_metrics, indent=2, default=str))
    return out


def write_all_artifacts(
    run_dir: Path,
    run_id: str,
    manifest: ExperimentManifest,
    config: BacktestConfig,
    metrics: MetricsBundle,
    trades: list[FilledTrade],
    equity_curve: list[DailyPortfolioState],
    open_positions: list[OpenPosition],
    candidate_map: dict[str, Any],
    started_at: dt.datetime,
    finished_at: dt.datetime,
    git_hash: str,
    total_trading_days: int,
    total_candidates_seen: int,
    total_orders_rejected: int,
    split_name: str | None = None,
    per_engine_metrics: dict[str, dict[str, Any]] | None = None,
) -> dict[str, str]:
    """Write all output files. Returns mapping of artifact_name → file_path."""
    from libs.backtest.manifests import save_manifest, save_resolved_config

    paths: dict[str, str] = {}

    # Core files
    paths["manifest"] = str(save_manifest(manifest, run_dir))
    paths["resolved_config"] = str(save_resolved_config(config, run_dir))
    paths["metadata"] = str(
        write_metadata(
            run_dir, run_id, started_at, finished_at, git_hash,
            total_trading_days, total_candidates_seen, total_orders_rejected,
            split_name=split_name,
        )
    )

    # Metrics
    if config.reporting.write_metrics_summary:
        paths["metrics_summary"] = str(write_metrics_summary(run_dir, metrics))
    paths["attribution_by_event_type"] = str(
        write_attribution_by_event_type(run_dir, trades, candidate_map)
    )
    paths["attribution_by_sector"] = str(
        write_attribution_by_sector(run_dir, trades, candidate_map)
    )
    paths["attribution_by_engine"] = str(
        write_attribution_by_engine(run_dir, trades, per_engine_metrics)
    )
    paths["score_bucket_report"] = str(
        write_score_bucket_report(
            run_dir, metrics.score_bucket_hit_rate, trades, candidate_map
        )
    )
    if per_engine_metrics:
        paths["per_engine_metrics"] = str(write_per_engine_metrics(run_dir, per_engine_metrics))

    # Trade data
    if config.reporting.write_trade_blotter:
        p = write_trade_blotter(run_dir, trades)
        if p:
            paths["trade_blotter"] = str(p)

    if config.reporting.write_equity_curve:
        p = write_daily_equity_curve(run_dir, equity_curve)
        if p:
            paths["daily_equity_curve"] = str(p)

    p = write_position_timeline(run_dir, trades, open_positions)
    if p:
        paths["position_timeline"] = str(p)

    # Plots (no-op)
    paths["plots_dir"] = str(write_plots(run_dir, config.reporting.generate_plots))

    # Notes
    paths["run_notes"] = str(write_run_notes(run_dir, manifest.notes or ""))

    return paths


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _write_parquet(rows: list[dict[str, Any]], path: Path) -> None:
    """Write list of row dicts to Parquet."""
    if not rows:
        return
    keys = list(rows[0].keys())
    columns: dict[str, list[Any]] = {k: [] for k in keys}
    for row in rows:
        for k in keys:
            columns[k].append(row.get(k))
    table = pa.table({k: pa.array(v) for k, v in columns.items()})
    pq.write_table(table, str(path))
    logger.debug("parquet_written", path=str(path), rows=len(rows))
