"""CTF submission pipeline — end-to-end orchestration.

Chains all CTF steps in dependency order:

  1. ``run_local()``      — execute script to produce weights (if weights_df not supplied)
  2. ``validate()``       — 23-rule CTF compliance check
  3. ``compute_metrics()``— Sharpe / annual return / vol / MDD + 1/N benchmark
  4. ``fetch_leaderboard()`` — scrape current standings and estimate rank
  5. ``build_submission()``  — package model.py + weights.csv + optional artifacts
  6. ``submit()``         — HTTP submission (only when submit=True)

Usage::

    from eapctf.ctf import pipeline

    report = pipeline(
        script_path="model.py",
        data_dir="data/ctf/",
        submitter={"first_name": "Jane", "last_name": "Doe", "email": "jane@example.com"},
        model_name="HJKP-Lasso",
    )
    print(report)

    # Skip re-running the script if weights are already computed:
    report = pipeline(
        script_path="model.py",
        submitter={...},
        model_name="HJKP-Lasso",
        weights_df=precomputed_weights,
        daily_ret_df=daily_ret,
    )
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Any

import pandas as pd

from eapctf.ctf.interface import run_local
from eapctf.ctf.leaderboard import LeaderboardResult, fetch_leaderboard
from eapctf.ctf.metrics import MetricsResult, compute_metrics
from eapctf.ctf.package import SubmissionBundle, build_submission
from eapctf.ctf.submit import SubmissionResult
from eapctf.ctf.submit import submit as _submit
from eapctf.ctf.validate import ValidationResult, validate

# ---------------------------------------------------------------------------
# Report dataclass
# ---------------------------------------------------------------------------


@dataclass
class CTFReport:
    """Complete CTF submission report.

    Attributes
    ----------
    validation : ValidationResult
        Per-rule compliance results (AUTO + MANUAL checklist).
    metrics : MetricsResult | None
        Portfolio performance metrics.  None if daily returns were not
        provided and could not be loaded from data_dir.
    leaderboard : LeaderboardResult
        Current leaderboard standings and estimated rank for this submission.
    bundle : SubmissionBundle
        Paths to all generated submission artifacts.
    submission : SubmissionResult | None
        HTTP submission result.  None when ``submit=False`` (dry run).
    estimated_rank : int | None
        Expected leaderboard rank based on this submission's Sharpe ratio.
        None if metrics or leaderboard data is unavailable.
    """

    validation: ValidationResult
    metrics: MetricsResult | None
    leaderboard: LeaderboardResult
    bundle: SubmissionBundle
    submission: SubmissionResult | None = None
    estimated_rank: int | None = None

    def __str__(self) -> str:
        lines = [
            "=" * 65,
            "CTF PIPELINE REPORT",
            "=" * 65,
        ]

        # Validation summary
        n_fail = sum(1 for c in self.validation.checks if c.severity == "FAIL" and c.auto)
        n_warn = sum(1 for c in self.validation.checks if c.severity == "WARNING" and c.auto)
        status = "PASS" if self.validation.passed else "FAIL"
        lines.append(f"  Validation   : {status}  ({n_fail} FAIL, {n_warn} WARNING)")

        # Metrics summary
        if self.metrics is not None:
            lines.append(
                f"  Sharpe       : {self.metrics.sharpe:+.3f}"
                f"  |  Annual return: {self.metrics.annual_return:+.2%}"
                f"  |  MDD: {self.metrics.max_drawdown:.2%}"
            )
        else:
            lines.append("  Metrics      : (not computed — no daily returns provided)")

        # Rank estimate
        if self.estimated_rank is not None:
            n_entries = len(self.leaderboard.entries)
            lines.append(
                f"  Est. rank    : #{self.estimated_rank}"
                f" of {n_entries} leaderboard entries"
                f"  (source: {self.leaderboard.source})"
            )
        else:
            lines.append("  Est. rank    : (unavailable)")

        # Bundle
        lines.append(f"  Bundle       : {self.bundle.script_path.parent}")

        # Submission
        if self.submission is not None:
            sub_info = self.submission.status
            if self.submission.manual_steps_required:
                sub_info += " — see instructions above"
            lines.append(f"  Submission   : {sub_info}")
        else:
            lines.append("  Submission   : (dry run — set submit=True to submit)")

        lines.append("=" * 65)
        return "\n".join(lines)


# ---------------------------------------------------------------------------
# Pipeline entry point
# ---------------------------------------------------------------------------


def pipeline(
    script_path: str | Path,
    data_dir: str | Path | None = None,
    submitter: dict[str, Any] | None = None,
    model_name: str = "unnamed-model",
    weights_df: pd.DataFrame | None = None,
    daily_ret_df: pd.DataFrame | None = None,
    methodology_md: str | None = None,
    output_dir: str | Path = "ctf_submission",
    submit: bool = False,
    chars_file: str = "ctff_chars.parquet",
    features_file: str = "ctff_features.parquet",
    daily_ret_file: str = "ctff_daily_ret.parquet",
) -> CTFReport:
    """Run the complete CTF submission pipeline.

    Parameters
    ----------
    script_path : str | Path
        Path to the Python model script defining ``main()``.
    data_dir : str | Path | None
        Local directory containing the three CTF Parquet files.  Required
        when ``weights_df`` is None (script will be executed to produce
        weights).  Also used to load ``daily_ret_df`` when not supplied.
    submitter : dict | None
        Submitter info with keys ``first_name``, ``last_name``, ``email``.
        Required when ``submit=True``.
    model_name : str
        Display name on the leaderboard.
    weights_df : pd.DataFrame | None
        Pre-computed portfolio weights (id, eom, w).  If None, the script
        is executed via ``run_local()`` using ``data_dir``.
    daily_ret_df : pd.DataFrame | None
        Daily excess returns (id, date, ret_exc) for performance metrics.
        If None, loaded from ``data_dir / daily_ret_file`` when available.
    methodology_md : str | None
        Markdown text for the methodology document.  If provided, a PDF
        is generated (via pandoc or weasyprint) and included in the bundle.
    output_dir : str | Path
        Directory for the submission bundle.  Created if needed.
    submit : bool
        If True, attempt programmatic HTTP submission after bundling.
        Defaults to False (dry run).
    chars_file, features_file, daily_ret_file : str
        Parquet file names within ``data_dir``.

    Returns
    -------
    CTFReport
        Full report: validation, metrics, leaderboard, bundle, submission.

    Raises
    ------
    FileNotFoundError
        If ``script_path`` is missing, or ``data_dir`` files are missing
        when needed.
    ValueError
        If ``submit=True`` but ``submitter`` is not provided.
    """
    script_path = Path(script_path).resolve()
    output_dir = Path(output_dir).resolve()

    if submit and not submitter:
        raise ValueError(
            "'submitter' dict (first_name, last_name, email) is required when submit=True."
        )

    data_dir_path: Path | None = Path(data_dir).resolve() if data_dir is not None else None

    # --- Step 1: Produce weights ---
    if weights_df is None:
        if data_dir_path is None:
            raise ValueError(
                "Either 'data_dir' or 'weights_df' must be provided."
            )
        print("[pipeline] Running script to produce weights ...")
        weights_df = run_local(
            script_path,
            data_dir_path,
            chars_file=chars_file,
            features_file=features_file,
            daily_ret_file=daily_ret_file,
        )
        print(f"[pipeline] Weights produced: {len(weights_df):,} rows")

    # --- Step 2: Load daily returns for metrics (if not supplied) ---
    if daily_ret_df is None and data_dir_path is not None:
        daily_ret_path = data_dir_path / daily_ret_file
        if daily_ret_path.exists():
            print("[pipeline] Loading daily returns for metrics ...")
            daily_ret_df = pd.read_parquet(daily_ret_path)

    # --- Step 3: Validate ---
    print("[pipeline] Running compliance validation ...")
    validation = validate(
        script_path=script_path,
        weights_df=weights_df,
        data_dir=data_dir_path,
    )
    if not validation.passed:
        n_fail = sum(1 for c in validation.checks if c.severity == "FAIL" and c.auto)
        print(f"[pipeline] WARNING: {n_fail} FAIL rule(s) detected. Review before submitting.")
    print(validation)

    # --- Step 4: Compute metrics ---
    metrics: MetricsResult | None = None
    if daily_ret_df is not None:
        print("[pipeline] Computing performance metrics ...")
        metrics = compute_metrics(weights_df, daily_ret_df)
        print(metrics)

    # --- Step 5: Fetch leaderboard ---
    print("[pipeline] Fetching leaderboard ...")
    leaderboard = fetch_leaderboard()
    print(leaderboard)

    estimated_rank: int | None = None
    if metrics is not None and not _is_nan(metrics.sharpe):
        estimated_rank = leaderboard.estimate_rank(metrics.sharpe)
        print(f"[pipeline] Estimated rank: #{estimated_rank} (Sharpe={metrics.sharpe:+.3f})")

    # --- Step 6: Build submission bundle ---
    print("[pipeline] Building submission bundle ...")
    bundle = build_submission(
        script_path=script_path,
        weights_df=weights_df,
        output_dir=output_dir,
        methodology_md=methodology_md,
    )
    print(bundle)

    # --- Step 7: Submit (optional) ---
    submission: SubmissionResult | None = None
    if submit:
        print("[pipeline] Submitting to jkpfactors.com ...")
        submission = _submit(
            bundle=bundle,
            submitter=submitter,  # type: ignore[arg-type]  # checked above
            model_name=model_name,
        )
        print(submission)

    return CTFReport(
        validation=validation,
        metrics=metrics,
        leaderboard=leaderboard,
        bundle=bundle,
        submission=submission,
        estimated_rank=estimated_rank,
    )


def _is_nan(value: float) -> bool:
    """Return True if value is NaN (without importing math)."""
    return value != value
