"""Calibration report generation.

This module provides utilities for generating human-readable reports
from calibration results, including recommendations and visualizations.
"""

from __future__ import annotations

import json
from dataclasses import dataclass
from datetime import datetime
from io import StringIO
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from .optimizer import CalibrationResult


@dataclass
class CalibrationReport:
    """Human-readable calibration report.

    Provides formatted output of calibration results including:
    - Optimal threshold recommendation
    - Tier distribution analysis
    - Quality/cost/latency tradeoffs
    - Pareto frontier summary
    - Cross-validation results

    Example:
        result = calibrator.calibrate(dataset)
        report = CalibrationReport.from_result(result)
        print(report.to_text())
        report.save("calibration_report.json")
    """

    # Core results
    optimal_thresholds: dict
    tier_distribution: dict
    quality_metrics: dict
    cost_metrics: dict
    latency_metrics: dict

    # Analysis
    pareto_summary: dict
    cv_summary: dict | None
    recommendations: list[str]

    # Metadata
    timestamp: str
    n_samples: int
    n_iterations: int
    search_strategy: str

    @classmethod
    def from_result(cls, result: "CalibrationResult") -> "CalibrationReport":
        """Create report from calibration result.

        Args:
            result: CalibrationResult from calibrator.

        Returns:
            CalibrationReport instance.
        """
        metrics = result.optimal_metrics

        # Extract tier distribution
        tier_dist = {
            tier.name: {
                "fraction": round(frac * 100, 1),
                "n_queries": int(frac * metrics.n_queries),
            }
            for tier, frac in metrics.tier_distribution.items()
        }

        # Quality metrics
        quality = {
            "overall_accuracy": round(metrics.overall_accuracy * 100, 1),
            "frac_of_oracle": round(metrics.frac_of_oracle * 100, 1),
            "overall_regret": round(metrics.overall_regret, 4),
            "per_tier": {
                tier.name: {
                    "accuracy": round(tm.accuracy * 100, 1),
                    "regret": round(tm.regret, 4),
                }
                for tier, tm in metrics.tier_metrics.items()
                if tm.n_queries > 0
            },
        }

        # Cost metrics
        cost = {
            "avg_cost_per_query": round(metrics.avg_cost_per_query, 6),
            "avg_cost_per_1k": round(metrics.avg_cost_per_query * 1000, 2),
            "total_cost": round(metrics.total_cost, 4),
            "per_tier": {
                tier.name: round(tm.avg_cost, 6)
                for tier, tm in metrics.tier_metrics.items()
                if tm.n_queries > 0
            },
        }

        # Latency metrics
        latency = {
            "avg_latency_ms": round(metrics.avg_latency_ms, 1),
            "per_tier": {
                tier.name: round(tm.avg_latency_ms, 1)
                for tier, tm in metrics.tier_metrics.items()
                if tm.n_queries > 0
            },
        }

        # Pareto summary
        pareto = {
            "n_points": len(result.pareto_frontier),
            "quality_range": [
                round(result.pareto_frontier.quality_range[0] * 100, 1),
                round(result.pareto_frontier.quality_range[1] * 100, 1),
            ],
            "cost_range": [
                round(result.pareto_frontier.cost_range[0] * 1000, 2),
                round(result.pareto_frontier.cost_range[1] * 1000, 2),
            ],
        }

        # Generate recommendations
        recommendations = cls._generate_recommendations(result)

        return cls(
            optimal_thresholds={
                "trivial_mean_logit": round(
                    result.optimal_thresholds.trivial_mean_logit, 3
                ),
                "uncertain_mean_logit": round(
                    result.optimal_thresholds.uncertain_mean_logit, 3
                ),
                "uncertain_logit_margin": round(
                    result.optimal_thresholds.uncertain_logit_margin, 3
                ),
            },
            tier_distribution=tier_dist,
            quality_metrics=quality,
            cost_metrics=cost,
            latency_metrics=latency,
            pareto_summary=pareto,
            cv_summary=result.cv_summary,
            recommendations=recommendations,
            timestamp=datetime.now().isoformat(),
            n_samples=metrics.n_queries,
            n_iterations=result.n_iterations,
            search_strategy="bayesian",  # Default, could be extracted from result
        )

    @staticmethod
    def _generate_recommendations(result: "CalibrationResult") -> list[str]:
        """Generate actionable recommendations."""
        recommendations = []
        metrics = result.optimal_metrics

        # Tier balance recommendations
        trivial_pct = metrics.tier_distribution.get(
            next(t for t in metrics.tier_distribution if t.name == "TRIVIAL"), 0
        )
        uncertain_pct = metrics.tier_distribution.get(
            next(t for t in metrics.tier_distribution if t.name == "UNCERTAIN"), 0
        )

        if trivial_pct < 0.1:
            recommendations.append(
                "Low TRIVIAL tier usage (<10%). Consider lowering "
                "trivial_mean_logit threshold to capture more easy queries."
            )

        if uncertain_pct > 0.3:
            recommendations.append(
                "High UNCERTAIN tier usage (>30%). Consider raising "
                "uncertain thresholds to reduce ensemble costs."
            )

        # Quality recommendations
        if metrics.frac_of_oracle < 0.85:
            recommendations.append(
                "Quality below 85% of oracle. Consider more conservative "
                "thresholds or using ensemble more often."
            )

        # Validity issues
        if not metrics.is_valid:
            for issue in metrics.validity_issues:
                recommendations.append(f"Validity issue: {issue}")

        # CV stability
        if result.cv_summary:
            quality_std = result.cv_summary.get("quality", {}).get("std", 0)
            if quality_std > 0.05:
                recommendations.append(
                    f"High CV variance (std={quality_std:.3f}). Thresholds may "
                    "not generalize well. Consider more training data."
                )

        if not recommendations:
            recommendations.append(
                "Thresholds appear well-calibrated. Monitor production "
                "metrics for drift."
            )

        return recommendations

    def to_text(self) -> str:
        """Generate human-readable text report."""
        buf = StringIO()
        w = buf.write

        w("=" * 70 + "\n")
        w("EB1-GNN THRESHOLD CALIBRATION REPORT\n")
        w("=" * 70 + "\n\n")

        w(f"Generated: {self.timestamp}\n")
        w(f"Samples: {self.n_samples}\n")
        w(f"Iterations: {self.n_iterations}\n\n")

        # Optimal thresholds
        w("-" * 40 + "\n")
        w("OPTIMAL THRESHOLDS\n")
        w("-" * 40 + "\n")
        for key, value in self.optimal_thresholds.items():
            w(f"  {key}: {value}\n")
        w("\n")

        # Tier distribution
        w("-" * 40 + "\n")
        w("TIER DISTRIBUTION\n")
        w("-" * 40 + "\n")
        for tier, data in self.tier_distribution.items():
            w(f"  {tier}: {data['fraction']}% ({data['n_queries']} queries)\n")
        w("\n")

        # Quality metrics
        w("-" * 40 + "\n")
        w("QUALITY METRICS\n")
        w("-" * 40 + "\n")
        w(f"  Overall accuracy: {self.quality_metrics['overall_accuracy']}%\n")
        w(f"  Fraction of oracle: {self.quality_metrics['frac_of_oracle']}%\n")
        w(f"  Overall regret: {self.quality_metrics['overall_regret']}\n")
        w("  Per-tier:\n")
        for tier, data in self.quality_metrics.get("per_tier", {}).items():
            w(f"    {tier}: accuracy={data['accuracy']}%, regret={data['regret']}\n")
        w("\n")

        # Cost metrics
        w("-" * 40 + "\n")
        w("COST METRICS\n")
        w("-" * 40 + "\n")
        w(f"  Avg cost per query: ${self.cost_metrics['avg_cost_per_query']:.6f}\n")
        w(f"  Avg cost per 1K queries: ${self.cost_metrics['avg_cost_per_1k']:.2f}\n")
        w("\n")

        # Latency metrics
        w("-" * 40 + "\n")
        w("LATENCY METRICS\n")
        w("-" * 40 + "\n")
        w(f"  Avg latency: {self.latency_metrics['avg_latency_ms']}ms\n")
        w("\n")

        # Pareto summary
        w("-" * 40 + "\n")
        w("PARETO FRONTIER\n")
        w("-" * 40 + "\n")
        w(f"  Points on frontier: {self.pareto_summary['n_points']}\n")
        w(f"  Quality range: {self.pareto_summary['quality_range'][0]}-"
          f"{self.pareto_summary['quality_range'][1]}%\n")
        w(f"  Cost range: ${self.pareto_summary['cost_range'][0]}-"
          f"${self.pareto_summary['cost_range'][1]} per 1K\n")
        w("\n")

        # CV summary
        if self.cv_summary:
            w("-" * 40 + "\n")
            w("CROSS-VALIDATION\n")
            w("-" * 40 + "\n")
            w(f"  Folds: {self.cv_summary['n_folds']}\n")
            q = self.cv_summary["quality"]
            w(f"  Quality: {q['mean']*100:.1f}% +/- {q['std']*100:.1f}%\n")
            w("\n")

        # Recommendations
        w("-" * 40 + "\n")
        w("RECOMMENDATIONS\n")
        w("-" * 40 + "\n")
        for i, rec in enumerate(self.recommendations, 1):
            w(f"  {i}. {rec}\n")
        w("\n")

        w("=" * 70 + "\n")

        return buf.getvalue()

    def to_dict(self) -> dict:
        """Convert report to dictionary for serialization."""
        return {
            "optimal_thresholds": self.optimal_thresholds,
            "tier_distribution": self.tier_distribution,
            "quality_metrics": self.quality_metrics,
            "cost_metrics": self.cost_metrics,
            "latency_metrics": self.latency_metrics,
            "pareto_summary": self.pareto_summary,
            "cv_summary": self.cv_summary,
            "recommendations": self.recommendations,
            "metadata": {
                "timestamp": self.timestamp,
                "n_samples": self.n_samples,
                "n_iterations": self.n_iterations,
                "search_strategy": self.search_strategy,
            },
        }

    def save(self, path: str | Path) -> None:
        """Save report to file.

        Args:
            path: Output path. Extension determines format:
                  .json -> JSON format
                  .txt -> Text format
                  .yaml -> YAML format (requires pyyaml)
        """
        path = Path(path)

        if path.suffix == ".json":
            with open(path, "w") as f:
                json.dump(self.to_dict(), f, indent=2)

        elif path.suffix == ".txt":
            with open(path, "w") as f:
                f.write(self.to_text())

        elif path.suffix in (".yaml", ".yml"):
            try:
                import yaml

                with open(path, "w") as f:
                    yaml.safe_dump(self.to_dict(), f, default_flow_style=False)
            except ImportError as err:
                raise ImportError("pyyaml required for YAML output") from err

        else:
            # Default to JSON
            with open(path, "w") as f:
                json.dump(self.to_dict(), f, indent=2)

    def to_yaml_config(self) -> str:
        """Generate YAML configuration for deployment.

        Returns thresholds in a format suitable for production config.
        """
        lines = [
            "# Generated by threshold calibration",
            f"# {self.timestamp}",
            "",
            "tiered_routing:",
            "  enabled: true",
            "",
            "  thresholds:",
        ]

        for key, value in self.optimal_thresholds.items():
            lines.append(f"    {key}: {value}")

        lines.extend([
            "",
            "  # Expected metrics:",
            f"  # Quality: {self.quality_metrics['frac_of_oracle']}% of oracle",
            f"  # Cost: ${self.cost_metrics['avg_cost_per_1k']}/1K queries",
            f"  # Latency: {self.latency_metrics['avg_latency_ms']}ms avg",
        ])

        return "\n".join(lines)
