"""DataFrame Cross-Platform Benchmark Suite.

Provides comprehensive benchmark suite for comparing DataFrame platform performance.
This module enables standardized performance comparisons across different DataFrame
platforms (Polars, Pandas, DataFusion, PySpark, etc.) using the same query workloads.

Key Features:
- Standard test configurations with configurable scale factors and query sets
- Platform capability matrix for GPU, distributed, and streaming modes
- Statistical analysis (mean, std, percentiles, CV)
- Result collection and normalization for fair comparisons
- Support for both expression-based and pandas-like platform families

Usage:
    from benchbox.core.dataframe.benchmark_suite import (
        DataFrameBenchmarkSuite,
        BenchmarkConfig,
        PlatformCapability,
    )

    # Create benchmark suite
    suite = DataFrameBenchmarkSuite(
        config=BenchmarkConfig(
            scale_factor=0.01,
            query_ids=["Q1", "Q3", "Q6", "Q10"],
            warmup_iterations=1,
            benchmark_iterations=3,
        )
    )

    # Run benchmarks across platforms
    results = suite.run_comparison(
        platforms=["polars-df", "pandas-df", "datafusion-df"],
        data_dir="benchmark_runs/datagen/tpch_sf001",
    )

    # Get statistical summary
    summary = suite.get_summary(results)

Copyright 2026 Joe Harris / BenchBox Project

Licensed under the MIT License. See LICENSE file in the project root for details.
"""

from __future__ import annotations

import json
import logging
import math
import statistics
import time
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Any

from benchbox.core.dataframe.profiling import (
    MemoryTracker,
)
from benchbox.core.tpch.dataframe_queries import get_tpch_dataframe_queries
from benchbox.utils.path_utils import get_benchmark_runs_datagen_path

if TYPE_CHECKING:
    from benchbox.core.dataframe.context import DataFrameContext

logger = logging.getLogger(__name__)


class PlatformCategory(Enum):
    """Categories for DataFrame platform comparisons."""

    SINGLE_NODE = "single_node"
    GPU_ACCELERATED = "gpu_accelerated"
    DISTRIBUTED = "distributed"
    MEMORY_EFFICIENT = "memory_efficient"


@dataclass
class PlatformCapability:
    """Describes capabilities of a DataFrame platform.

    Attributes:
        platform_name: Platform identifier (e.g., "polars-df")
        family: Query family ("expression" or "pandas")
        category: Platform category for comparison grouping
        supports_lazy: Whether platform supports lazy evaluation
        supports_streaming: Whether platform supports streaming mode
        supports_gpu: Whether platform supports GPU acceleration
        supports_distributed: Whether platform supports distributed execution
        memory_notes: Notes about memory characteristics
    """

    platform_name: str
    family: str
    category: PlatformCategory
    supports_lazy: bool = False
    supports_streaming: bool = False
    supports_gpu: bool = False
    supports_distributed: bool = False
    memory_notes: str = ""


# Platform capability registry - defines what each platform supports
PLATFORM_CAPABILITIES: dict[str, PlatformCapability] = {
    "polars-df": PlatformCapability(
        platform_name="polars-df",
        family="expression",
        category=PlatformCategory.SINGLE_NODE,
        supports_lazy=True,
        supports_streaming=True,
        memory_notes="Memory-efficient columnar format, lazy evaluation reduces memory pressure",
    ),
    "pandas-df": PlatformCapability(
        platform_name="pandas-df",
        family="pandas",
        category=PlatformCategory.SINGLE_NODE,
        supports_lazy=False,
        memory_notes="In-memory operations, requires full dataset in RAM",
    ),
    "datafusion-df": PlatformCapability(
        platform_name="datafusion-df",
        family="expression",
        category=PlatformCategory.SINGLE_NODE,
        supports_lazy=True,
        supports_streaming=True,
        memory_notes="Arrow-based, supports spilling to disk for large datasets",
    ),
    "pyspark-df": PlatformCapability(
        platform_name="pyspark-df",
        family="expression",
        category=PlatformCategory.DISTRIBUTED,
        supports_lazy=True,
        supports_distributed=True,
        memory_notes="Distributed execution, configurable memory management",
    ),
    "modin-df": PlatformCapability(
        platform_name="modin-df",
        family="pandas",
        category=PlatformCategory.DISTRIBUTED,
        supports_distributed=True,
        memory_notes="Pandas API with Ray/Dask backend for multi-core execution",
    ),
    "cudf-df": PlatformCapability(
        platform_name="cudf-df",
        family="pandas",
        category=PlatformCategory.GPU_ACCELERATED,
        supports_gpu=True,
        memory_notes="GPU memory bound, best for datasets that fit in VRAM",
    ),
    "dask-df": PlatformCapability(
        platform_name="dask-df",
        family="pandas",
        category=PlatformCategory.DISTRIBUTED,
        supports_lazy=True,
        supports_distributed=True,
        memory_notes="Partitioned dataframes, scales beyond memory",
    ),
}


@dataclass
class BenchmarkConfig:
    """Configuration for DataFrame benchmark runs.

    Attributes:
        scale_factor: TPC-H scale factor (0.01, 0.1, 1, etc.)
        query_ids: List of query IDs to run (e.g., ["Q1", "Q3", "Q6"])
        warmup_iterations: Number of warmup iterations before measurement
        benchmark_iterations: Number of measured iterations per query
        track_memory: Whether to track memory usage during execution
        capture_plans: Whether to capture query execution plans
        memory_sample_interval_ms: Memory sampling interval in milliseconds
        timeout_seconds: Maximum time per query execution
    """

    scale_factor: float = 0.01
    query_ids: list[str] | None = None
    warmup_iterations: int = 1
    benchmark_iterations: int = 3
    track_memory: bool = True
    capture_plans: bool = True
    memory_sample_interval_ms: int = 50
    timeout_seconds: float = 300.0


@dataclass
class QueryBenchmarkResult:
    """Result of benchmarking a single query across iterations.

    Attributes:
        query_id: Query identifier
        platform: Platform name
        iterations: Number of successful iterations
        execution_times_ms: List of execution times in milliseconds
        memory_peak_mb: Peak memory usage across iterations
        rows_returned: Number of rows in result
        status: "SUCCESS" or "ERROR"
        error_message: Error message if status is "ERROR"
        query_plan: Captured query plan if available
    """

    query_id: str
    platform: str
    iterations: int
    execution_times_ms: list[float] = field(default_factory=list)
    memory_peak_mb: float = 0.0
    rows_returned: int = 0
    status: str = "SUCCESS"
    error_message: str | None = None
    query_plan: str | None = None

    @property
    def mean_time_ms(self) -> float:
        """Mean execution time in milliseconds."""
        if not self.execution_times_ms:
            return 0.0
        return statistics.mean(self.execution_times_ms)

    @property
    def std_time_ms(self) -> float:
        """Standard deviation of execution times."""
        if len(self.execution_times_ms) < 2:
            return 0.0
        return statistics.stdev(self.execution_times_ms)

    @property
    def min_time_ms(self) -> float:
        """Minimum execution time."""
        if not self.execution_times_ms:
            return 0.0
        return min(self.execution_times_ms)

    @property
    def max_time_ms(self) -> float:
        """Maximum execution time."""
        if not self.execution_times_ms:
            return 0.0
        return max(self.execution_times_ms)

    @property
    def p50_time_ms(self) -> float:
        """50th percentile (median) execution time."""
        if not self.execution_times_ms:
            return 0.0
        return statistics.median(self.execution_times_ms)

    @property
    def p95_time_ms(self) -> float:
        """95th percentile execution time."""
        if len(self.execution_times_ms) < 2:
            return self.max_time_ms
        sorted_times = sorted(self.execution_times_ms)
        idx = int(len(sorted_times) * 0.95)
        return sorted_times[min(idx, len(sorted_times) - 1)]

    @property
    def coefficient_of_variation(self) -> float:
        """Coefficient of variation (CV) for execution times.

        CV = std / mean, expressed as a percentage.
        Lower values indicate more consistent performance.
        """
        if not self.execution_times_ms or self.mean_time_ms == 0:
            return 0.0
        return (self.std_time_ms / self.mean_time_ms) * 100

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "query_id": self.query_id,
            "platform": self.platform,
            "iterations": self.iterations,
            "execution_times_ms": self.execution_times_ms,
            "mean_time_ms": self.mean_time_ms,
            "std_time_ms": self.std_time_ms,
            "min_time_ms": self.min_time_ms,
            "max_time_ms": self.max_time_ms,
            "p50_time_ms": self.p50_time_ms,
            "p95_time_ms": self.p95_time_ms,
            "cv_percent": self.coefficient_of_variation,
            "memory_peak_mb": self.memory_peak_mb,
            "rows_returned": self.rows_returned,
            "status": self.status,
            "error_message": self.error_message,
            "query_plan": self.query_plan,
        }


@dataclass
class PlatformBenchmarkResult:
    """Aggregate results for a single platform across all queries.

    Attributes:
        platform: Platform name
        capability: Platform capability information
        config: Benchmark configuration used
        query_results: Results for each query
        total_time_ms: Total execution time across all queries
        geometric_mean_ms: Geometric mean of query times
        timestamp: When the benchmark was run
    """

    platform: str
    capability: PlatformCapability | None
    config: BenchmarkConfig
    query_results: list[QueryBenchmarkResult] = field(default_factory=list)
    total_time_ms: float = 0.0
    geometric_mean_ms: float = 0.0
    timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

    def __post_init__(self):
        """Calculate aggregate metrics after initialization."""
        self._calculate_aggregates()

    def _calculate_aggregates(self) -> None:
        """Calculate aggregate metrics from query results."""
        successful_results = [r for r in self.query_results if r.status == "SUCCESS"]

        if successful_results:
            mean_times = [r.mean_time_ms for r in successful_results if r.mean_time_ms > 0]
            self.total_time_ms = sum(mean_times)

            if mean_times:
                log_sum = sum(math.log(t) for t in mean_times if t > 0)
                self.geometric_mean_ms = math.exp(log_sum / len(mean_times))

    @property
    def successful_queries(self) -> int:
        """Number of successfully completed queries."""
        return sum(1 for r in self.query_results if r.status == "SUCCESS")

    @property
    def failed_queries(self) -> int:
        """Number of failed queries."""
        return sum(1 for r in self.query_results if r.status == "ERROR")

    @property
    def success_rate(self) -> float:
        """Percentage of successful queries."""
        total = len(self.query_results)
        if total == 0:
            return 0.0
        return (self.successful_queries / total) * 100

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "platform": self.platform,
            "capability": {
                "family": self.capability.family if self.capability else None,
                "category": self.capability.category.value if self.capability else None,
                "supports_lazy": self.capability.supports_lazy if self.capability else False,
                "supports_gpu": self.capability.supports_gpu if self.capability else False,
                "supports_distributed": self.capability.supports_distributed if self.capability else False,
            },
            "config": {
                "scale_factor": self.config.scale_factor,
                "query_ids": self.config.query_ids,
                "warmup_iterations": self.config.warmup_iterations,
                "benchmark_iterations": self.config.benchmark_iterations,
            },
            "query_results": [r.to_dict() for r in self.query_results],
            "summary": {
                "total_time_ms": self.total_time_ms,
                "geometric_mean_ms": self.geometric_mean_ms,
                "successful_queries": self.successful_queries,
                "failed_queries": self.failed_queries,
                "success_rate": self.success_rate,
            },
            "timestamp": self.timestamp.isoformat(),
        }


@dataclass
class ComparisonSummary:
    """Summary of cross-platform comparison results.

    Attributes:
        platforms: List of platforms compared
        fastest_platform: Platform with lowest geometric mean
        slowest_platform: Platform with highest geometric mean
        speedup_matrix: Matrix of speedup ratios between platforms
        query_winners: Best platform for each query
    """

    platforms: list[str]
    fastest_platform: str
    slowest_platform: str
    speedup_matrix: dict[str, dict[str, float]]
    query_winners: dict[str, str]
    total_queries: int
    scale_factor: float

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "platforms": self.platforms,
            "fastest_platform": self.fastest_platform,
            "slowest_platform": self.slowest_platform,
            "speedup_matrix": self.speedup_matrix,
            "query_winners": self.query_winners,
            "total_queries": self.total_queries,
            "scale_factor": self.scale_factor,
        }


class DataFrameBenchmarkSuite:
    """Cross-platform DataFrame benchmark suite.

    Provides comprehensive benchmarking capabilities for comparing DataFrame
    platform performance using standardized query workloads.

    Example:
        suite = DataFrameBenchmarkSuite(
            config=BenchmarkConfig(scale_factor=0.01, query_ids=["Q1", "Q6"])
        )
        results = suite.run_comparison(
            platforms=["polars-df", "pandas-df"],
            data_dir="benchmark_runs/datagen/tpch_sf001",
        )
        summary = suite.get_summary(results)
    """

    def __init__(self, config: BenchmarkConfig | None = None):
        """Initialize the benchmark suite.

        Args:
            config: Benchmark configuration. Defaults to standard config.
        """
        self.config = config or BenchmarkConfig()
        self._query_registry = get_tpch_dataframe_queries()

    def get_available_platforms(self) -> list[str]:
        """Get list of available DataFrame platforms.

        Returns:
            List of platform names that are currently available.
        """
        from benchbox.platforms import list_available_dataframe_platforms

        available = list_available_dataframe_platforms()
        return [name for name, is_available in available.items() if is_available]

    def get_platform_capability(self, platform_name: str) -> PlatformCapability | None:
        """Get capability information for a platform.

        Args:
            platform_name: Platform name

        Returns:
            PlatformCapability or None if unknown
        """
        return PLATFORM_CAPABILITIES.get(platform_name)

    def get_platforms_by_category(self, category: PlatformCategory) -> list[str]:
        """Get platforms in a specific category.

        Args:
            category: Platform category to filter by

        Returns:
            List of platform names in the category
        """
        available = self.get_available_platforms()
        return [
            name
            for name in available
            if name in PLATFORM_CAPABILITIES and PLATFORM_CAPABILITIES[name].category == category
        ]

    def run_comparison(
        self,
        platforms: list[str],
        data_dir: str | Path,
    ) -> list[PlatformBenchmarkResult]:
        """Run benchmark comparison across multiple platforms.

        Args:
            platforms: List of platform names to benchmark
            data_dir: Directory containing TPC-H parquet data

        Returns:
            List of PlatformBenchmarkResult for each platform
        """
        data_path = Path(data_dir)
        if not data_path.exists():
            raise ValueError(f"Data directory not found: {data_dir}")

        results = []
        for platform_name in platforms:
            logger.info(f"Benchmarking platform: {platform_name}")
            try:
                result = self._benchmark_platform(platform_name, data_path)
                results.append(result)
            except Exception as e:
                logger.error(f"Failed to benchmark {platform_name}: {e}")
                results.append(
                    PlatformBenchmarkResult(
                        platform=platform_name,
                        capability=self.get_platform_capability(platform_name),
                        config=self.config,
                        query_results=[
                            QueryBenchmarkResult(
                                query_id="ALL",
                                platform=platform_name,
                                iterations=0,
                                status="ERROR",
                                error_message=str(e),
                            )
                        ],
                    )
                )

        return results

    def _benchmark_platform(
        self,
        platform_name: str,
        data_dir: Path,
    ) -> PlatformBenchmarkResult:
        """Benchmark a single platform.

        Args:
            platform_name: Platform name
            data_dir: Directory containing TPC-H parquet data

        Returns:
            PlatformBenchmarkResult with all query results
        """
        context = self._create_context(platform_name, data_dir)
        capability = self.get_platform_capability(platform_name)
        family = capability.family if capability else "expression"

        query_ids = self.config.query_ids or self._query_registry.get_query_ids()
        query_results = []

        for query_id in query_ids:
            logger.info(f"  Running {query_id}...")
            result = self._benchmark_query(query_id, context, family, platform_name)
            query_results.append(result)

        return PlatformBenchmarkResult(
            platform=platform_name,
            capability=capability,
            config=self.config,
            query_results=query_results,
        )

    def _create_context(self, platform_name: str, data_dir: Path) -> DataFrameContext:
        """Create a DataFrame context for a platform with loaded data.

        Uses the platform adapter pattern to create contexts and load data,
        ensuring consistent behavior across all DataFrame platforms.

        Args:
            platform_name: Platform name
            data_dir: Directory containing TPC-H parquet data

        Returns:
            Initialized DataFrameContext with loaded tables
        """
        from benchbox.platforms import get_dataframe_adapter

        parquet_dir = data_dir / "parquet"
        if not parquet_dir.exists():
            parquet_dir = data_dir

        tables = ["lineitem", "orders", "customer", "supplier", "part", "partsupp", "nation", "region"]

        # Create adapter and context
        adapter = get_dataframe_adapter(platform_name, working_dir=str(data_dir))
        ctx = adapter.create_context()

        # Load tables using the adapter's load_table method
        for table in tables:
            table_path = parquet_dir / f"{table}.parquet"
            if table_path.exists():
                adapter.load_table(ctx, table, [table_path])

        return ctx

    def _benchmark_query(
        self,
        query_id: str,
        context: DataFrameContext,
        family: str,
        platform_name: str,
    ) -> QueryBenchmarkResult:
        """Benchmark a single query.

        Args:
            query_id: Query identifier
            context: DataFrame context with loaded data
            family: Query family ("expression" or "pandas")
            platform_name: Platform name for result tracking

        Returns:
            QueryBenchmarkResult with timing statistics
        """
        query = self._query_registry.get(query_id)
        if query is None:
            return QueryBenchmarkResult(
                query_id=query_id,
                platform=platform_name,
                iterations=0,
                status="ERROR",
                error_message=f"Query {query_id} not found",
            )

        impl = query.get_impl_for_family(family)
        if impl is None:
            return QueryBenchmarkResult(
                query_id=query_id,
                platform=platform_name,
                iterations=0,
                status="ERROR",
                error_message=f"No {family} implementation for {query_id}",
            )

        execution_times = []
        peak_memory = 0.0
        rows_returned = 0
        query_plan = None

        try:
            # Warmup iterations
            for _ in range(self.config.warmup_iterations):
                result = query.execute(context, family)
                if hasattr(result, "collect"):
                    result = result.collect()
                del result

            # Benchmark iterations
            for i in range(self.config.benchmark_iterations):
                if self.config.track_memory:
                    tracker = MemoryTracker(sample_interval_ms=self.config.memory_sample_interval_ms)
                    tracker.start()

                start_time = time.perf_counter()
                result = query.execute(context, family)

                # Collect if lazy
                if hasattr(result, "collect"):
                    result = result.collect()

                elapsed_ms = (time.perf_counter() - start_time) * 1000
                execution_times.append(elapsed_ms)

                if self.config.track_memory:
                    peak = tracker.stop()
                    peak_memory = max(peak_memory, peak)

                # Get row count on first iteration
                if i == 0:
                    try:
                        rows_returned = len(result)
                    except Exception:
                        rows_returned = 0

                del result

            return QueryBenchmarkResult(
                query_id=query_id,
                platform=platform_name,
                iterations=len(execution_times),
                execution_times_ms=execution_times,
                memory_peak_mb=peak_memory,
                rows_returned=rows_returned,
                status="SUCCESS",
                query_plan=query_plan,
            )

        except Exception as e:
            logger.error(f"Error running {query_id} on {platform_name}: {e}")
            return QueryBenchmarkResult(
                query_id=query_id,
                platform=platform_name,
                iterations=len(execution_times),
                execution_times_ms=execution_times,
                status="ERROR",
                error_message=str(e),
            )

    def get_summary(self, results: list[PlatformBenchmarkResult]) -> ComparisonSummary:
        """Generate comparison summary from benchmark results.

        Args:
            results: List of platform benchmark results

        Returns:
            ComparisonSummary with cross-platform comparison metrics
        """
        if not results:
            raise ValueError("No results to summarize")

        platforms = [r.platform for r in results]
        geomeans = {r.platform: r.geometric_mean_ms for r in results if r.geometric_mean_ms > 0}

        fastest = min(geomeans, key=geomeans.get) if geomeans else platforms[0]
        slowest = max(geomeans, key=geomeans.get) if geomeans else platforms[0]

        # Build speedup matrix
        speedup_matrix: dict[str, dict[str, float]] = {}
        for p1 in platforms:
            speedup_matrix[p1] = {}
            for p2 in platforms:
                if p1 in geomeans and p2 in geomeans and geomeans[p1] > 0:
                    speedup_matrix[p1][p2] = geomeans[p2] / geomeans[p1]
                else:
                    speedup_matrix[p1][p2] = 1.0

        # Find query winners
        query_winners: dict[str, str] = {}
        all_query_ids = set()
        for result in results:
            for qr in result.query_results:
                all_query_ids.add(qr.query_id)

        for query_id in all_query_ids:
            best_time = float("inf")
            best_platform = ""
            for result in results:
                for qr in result.query_results:
                    if qr.query_id == query_id and qr.status == "SUCCESS" and qr.mean_time_ms < best_time:
                        best_time = qr.mean_time_ms
                        best_platform = result.platform
            if best_platform:
                query_winners[query_id] = best_platform

        return ComparisonSummary(
            platforms=platforms,
            fastest_platform=fastest,
            slowest_platform=slowest,
            speedup_matrix=speedup_matrix,
            query_winners=query_winners,
            total_queries=len(all_query_ids),
            scale_factor=self.config.scale_factor,
        )

    def export_results(
        self,
        results: list[PlatformBenchmarkResult],
        output_path: str | Path,
        format: str = "json",
    ) -> Path:
        """Export benchmark results to file.

        Args:
            results: List of platform benchmark results
            output_path: Output file path
            format: Output format ("json", "markdown")

        Returns:
            Path to the created file
        """
        output_path = Path(output_path)
        output_path.parent.mkdir(parents=True, exist_ok=True)

        if format == "json":
            data = {
                "benchmark_suite": "dataframe_comparison",
                "timestamp": datetime.now(timezone.utc).isoformat(),
                "config": {
                    "scale_factor": self.config.scale_factor,
                    "query_ids": self.config.query_ids,
                    "warmup_iterations": self.config.warmup_iterations,
                    "benchmark_iterations": self.config.benchmark_iterations,
                },
                "results": [r.to_dict() for r in results],
                "summary": self.get_summary(results).to_dict() if results else None,
            }
            output_path.write_text(json.dumps(data, indent=2), encoding="utf-8")

        elif format == "markdown":
            md_content = self._generate_markdown_report(results)
            output_path.write_text(md_content, encoding="utf-8")

        else:
            raise ValueError(f"Unsupported export format: {format}")

        return output_path

    def _generate_markdown_report(self, results: list[PlatformBenchmarkResult]) -> str:
        """Generate markdown report from benchmark results.

        Args:
            results: List of platform benchmark results

        Returns:
            Markdown formatted report string
        """
        lines = []
        summary = self.get_summary(results)

        lines.append("# DataFrame Cross-Platform Benchmark Report")
        lines.append("")
        lines.append(f"**Scale Factor:** {self.config.scale_factor}")
        lines.append(f"**Benchmark Iterations:** {self.config.benchmark_iterations}")
        lines.append(f"**Total Queries:** {summary.total_queries}")
        lines.append(f"**Generated:** {datetime.now(timezone.utc).isoformat()}")
        lines.append("")

        # Summary section
        lines.append("## Performance Summary")
        lines.append("")
        lines.append(f"**Fastest Platform:** {summary.fastest_platform}")
        lines.append(f"**Slowest Platform:** {summary.slowest_platform}")
        lines.append("")

        # Platform comparison table
        lines.append("### Geometric Mean Comparison")
        lines.append("")
        lines.append("| Platform | Geomean (ms) | Total Time (ms) | Success Rate |")
        lines.append("|----------|--------------|-----------------|--------------|")

        for result in sorted(results, key=lambda r: r.geometric_mean_ms or float("inf")):
            geomean = f"{result.geometric_mean_ms:.2f}" if result.geometric_mean_ms else "N/A"
            total = f"{result.total_time_ms:.2f}" if result.total_time_ms else "N/A"
            success = f"{result.success_rate:.1f}%"
            lines.append(f"| {result.platform} | {geomean} | {total} | {success} |")

        lines.append("")

        # Query-by-query breakdown
        lines.append("## Query-by-Query Results")
        lines.append("")

        query_ids = sorted(summary.query_winners.keys())
        if query_ids:
            header = "| Query | " + " | ".join(r.platform for r in results) + " | Winner |"
            sep = "|-------|" + "|".join("-" * (len(r.platform) + 2) for r in results) + "|--------|"
            lines.append(header)
            lines.append(sep)

            for query_id in query_ids:
                row = f"| {query_id} |"
                for result in results:
                    qr = next((q for q in result.query_results if q.query_id == query_id), None)
                    if qr and qr.status == "SUCCESS":
                        row += f" {qr.mean_time_ms:.2f}ms |"
                    else:
                        row += " ERROR |"
                winner = summary.query_winners.get(query_id, "N/A")
                row += f" {winner} |"
                lines.append(row)

        lines.append("")

        # Speedup matrix
        if len(results) > 1:
            lines.append("## Speedup Matrix")
            lines.append("")
            lines.append("Shows how much faster the row platform is compared to the column platform.")
            lines.append("")

            header = "| | " + " | ".join(r.platform for r in results) + " |"
            sep = "|--|" + "|".join("--" for _ in results) + "|"
            lines.append(header)
            lines.append(sep)

            for r1 in results:
                row = f"| {r1.platform} |"
                for r2 in results:
                    speedup = summary.speedup_matrix.get(r1.platform, {}).get(r2.platform, 1.0)
                    row += f" {speedup:.2f}x |"
                lines.append(row)

        return "\n".join(lines)


@dataclass
class SQLComparisonResult:
    """Result comparing SQL and DataFrame execution for a query.

    Attributes:
        query_id: Query identifier
        sql_platform: SQL platform used (e.g., "duckdb", "sqlite")
        df_platform: DataFrame platform used (e.g., "polars-df", "pandas-df")
        sql_time_ms: SQL execution time in milliseconds
        df_time_ms: DataFrame execution time in milliseconds
        speedup: How much faster DataFrame is vs SQL (> 1 means DF is faster)
        sql_rows: Rows returned by SQL
        df_rows: Rows returned by DataFrame
        results_match: Whether SQL and DataFrame results match
        status: "SUCCESS" or "ERROR"
        error_message: Error message if status is "ERROR"
    """

    query_id: str
    sql_platform: str
    df_platform: str
    sql_time_ms: float = 0.0
    df_time_ms: float = 0.0
    speedup: float = 1.0
    sql_rows: int = 0
    df_rows: int = 0
    results_match: bool = False
    status: str = "SUCCESS"
    error_message: str | None = None

    def __post_init__(self):
        """Calculate speedup after initialization."""
        if self.sql_time_ms > 0 and self.df_time_ms > 0:
            self.speedup = self.sql_time_ms / self.df_time_ms

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "query_id": self.query_id,
            "sql_platform": self.sql_platform,
            "df_platform": self.df_platform,
            "sql_time_ms": self.sql_time_ms,
            "df_time_ms": self.df_time_ms,
            "speedup": self.speedup,
            "sql_rows": self.sql_rows,
            "df_rows": self.df_rows,
            "results_match": self.results_match,
            "status": self.status,
            "error_message": self.error_message,
        }


@dataclass
class SQLVsDataFrameSummary:
    """Summary of SQL vs DataFrame comparison across queries.

    Attributes:
        sql_platform: SQL platform used
        df_platform: DataFrame platform used
        total_queries: Total number of queries compared
        df_faster_count: Queries where DataFrame was faster
        sql_faster_count: Queries where SQL was faster
        average_speedup: Average speedup across queries
        max_speedup: Maximum speedup (DataFrame fastest)
        min_speedup: Minimum speedup (SQL fastest)
        query_results: Individual query comparison results
    """

    sql_platform: str
    df_platform: str
    total_queries: int
    df_faster_count: int
    sql_faster_count: int
    average_speedup: float
    max_speedup: float
    min_speedup: float
    query_results: list[SQLComparisonResult] = field(default_factory=list)

    @property
    def df_wins_percentage(self) -> float:
        """Percentage of queries where DataFrame was faster."""
        if self.total_queries == 0:
            return 0.0
        return (self.df_faster_count / self.total_queries) * 100

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "sql_platform": self.sql_platform,
            "df_platform": self.df_platform,
            "total_queries": self.total_queries,
            "df_faster_count": self.df_faster_count,
            "sql_faster_count": self.sql_faster_count,
            "df_wins_percentage": self.df_wins_percentage,
            "average_speedup": self.average_speedup,
            "max_speedup": self.max_speedup,
            "min_speedup": self.min_speedup,
            "query_results": [r.to_dict() for r in self.query_results],
        }


class SQLVsDataFrameBenchmark:
    """Benchmark suite for comparing SQL vs DataFrame execution.

    Compares the same TPC-H queries executed via SQL (on DuckDB/SQLite)
    versus DataFrame APIs (Polars, Pandas, etc.).

    Example:
        benchmark = SQLVsDataFrameBenchmark(
            config=BenchmarkConfig(scale_factor=0.01, query_ids=["Q1", "Q6"])
        )
        summary = benchmark.run_comparison(
            sql_platform="duckdb",
            df_platform="polars-df",
            data_dir="benchmark_runs/datagen/tpch_sf001",
        )
    """

    def __init__(self, config: BenchmarkConfig | None = None):
        """Initialize the SQL vs DataFrame benchmark.

        Args:
            config: Benchmark configuration. Defaults to standard config.
        """
        self.config = config or BenchmarkConfig()
        self._query_registry = get_tpch_dataframe_queries()

    def run_comparison(
        self,
        sql_platform: str,
        df_platform: str,
        data_dir: str | Path,
    ) -> SQLVsDataFrameSummary:
        """Run SQL vs DataFrame comparison.

        Args:
            sql_platform: SQL platform ("duckdb" or "sqlite")
            df_platform: DataFrame platform (e.g., "polars-df")
            data_dir: Directory containing TPC-H data

        Returns:
            SQLVsDataFrameSummary with comparison results
        """
        data_path = Path(data_dir)

        query_ids = self.config.query_ids or self._query_registry.get_query_ids()
        results: list[SQLComparisonResult] = []

        for query_id in query_ids:
            logger.info(f"Comparing SQL vs DataFrame for {query_id}...")
            result = self._compare_query(
                query_id=query_id,
                sql_platform=sql_platform,
                df_platform=df_platform,
                data_path=data_path,
            )
            results.append(result)

        return self._build_summary(sql_platform, df_platform, results)

    def _compare_query(
        self,
        query_id: str,
        sql_platform: str,
        df_platform: str,
        data_path: Path,
    ) -> SQLComparisonResult:
        """Compare a single query between SQL and DataFrame.

        Args:
            query_id: Query identifier
            sql_platform: SQL platform name
            df_platform: DataFrame platform name
            data_path: Path to data directory

        Returns:
            SQLComparisonResult for this query
        """
        try:
            # Run SQL benchmark
            sql_time, sql_rows = self._run_sql_query(query_id, sql_platform, data_path)

            # Run DataFrame benchmark
            df_time, df_rows = self._run_df_query(query_id, df_platform, data_path)

            # Check if row counts match (basic validation)
            results_match = sql_rows == df_rows

            return SQLComparisonResult(
                query_id=query_id,
                sql_platform=sql_platform,
                df_platform=df_platform,
                sql_time_ms=sql_time,
                df_time_ms=df_time,
                sql_rows=sql_rows,
                df_rows=df_rows,
                results_match=results_match,
                status="SUCCESS",
            )

        except Exception as e:
            logger.error(f"Error comparing {query_id}: {e}")
            return SQLComparisonResult(
                query_id=query_id,
                sql_platform=sql_platform,
                df_platform=df_platform,
                status="ERROR",
                error_message=str(e),
            )

    def _run_sql_query(
        self,
        query_id: str,
        sql_platform: str,
        data_path: Path,
    ) -> tuple[float, int]:
        """Run TPC-H query via SQL.

        Args:
            query_id: Query identifier (e.g., "Q1")
            sql_platform: SQL platform ("duckdb" or "sqlite")
            data_path: Path to data directory

        Returns:
            Tuple of (execution_time_ms, row_count)
        """
        from benchbox.core.tpch.queries import TPCHQuery

        query_num = int(query_id.replace("Q", ""))
        tpch_query = TPCHQuery.get_query(query_num, dialect=sql_platform)

        parquet_dir = data_path / "parquet"
        if not parquet_dir.exists():
            parquet_dir = data_path

        if sql_platform == "duckdb":
            conn = self._connect_duckdb(parquet_dir)
        elif sql_platform == "sqlite":
            conn = self._connect_sqlite(parquet_dir)
        else:
            raise ValueError(f"Unsupported SQL platform: {sql_platform}")

        try:
            return self._warmup_and_benchmark(conn, tpch_query.sql)
        finally:
            conn.close()

    @staticmethod
    def _connect_duckdb(parquet_dir: Path) -> Any:
        """Create an in-memory DuckDB connection with TPC-H tables loaded from Parquet."""
        import duckdb

        conn = duckdb.connect()
        tables = ["lineitem", "orders", "customer", "supplier", "part", "partsupp", "nation", "region"]
        for table in tables:
            table_path = parquet_dir / f"{table}.parquet"
            if table_path.exists():
                conn.execute(f"CREATE TABLE {table} AS SELECT * FROM read_parquet('{table_path}')")
        return conn

    @staticmethod
    def _connect_sqlite(parquet_dir: Path) -> Any:
        """Create an in-memory SQLite connection with TPC-H tables loaded from Parquet via pandas."""
        import sqlite3

        import pandas as pd

        conn = sqlite3.connect(":memory:")
        tables = ["lineitem", "orders", "customer", "supplier", "part", "partsupp", "nation", "region"]
        for table in tables:
            table_path = parquet_dir / f"{table}.parquet"
            if table_path.exists():
                df = pd.read_parquet(str(table_path))
                df.to_sql(table, conn, index=False)
        return conn

    def _warmup_and_benchmark(self, conn: Any, sql: str) -> tuple[float, int]:
        """Run warmup iterations followed by timed benchmark iterations."""
        for _ in range(self.config.warmup_iterations):
            conn.execute(sql).fetchall()

        times = []
        row_count = 0
        for i in range(self.config.benchmark_iterations):
            start = time.perf_counter()
            result = conn.execute(sql).fetchall()
            elapsed_ms = (time.perf_counter() - start) * 1000
            times.append(elapsed_ms)
            if i == 0:
                row_count = len(result)

        return statistics.mean(times), row_count

    def _run_df_query(
        self,
        query_id: str,
        df_platform: str,
        data_path: Path,
    ) -> tuple[float, int]:
        """Run TPC-H query via DataFrame API.

        Args:
            query_id: Query identifier (e.g., "Q1")
            df_platform: DataFrame platform name
            data_path: Path to data directory

        Returns:
            Tuple of (execution_time_ms, row_count)
        """
        from benchbox.platforms import get_dataframe_adapter

        parquet_dir = data_path / "parquet"
        if not parquet_dir.exists():
            parquet_dir = data_path

        tables = ["lineitem", "orders", "customer", "supplier", "part", "partsupp", "nation", "region"]

        # Create adapter and context
        adapter = get_dataframe_adapter(df_platform, working_dir=str(data_path))
        ctx = adapter.create_context()

        # Load tables
        for table in tables:
            table_path = parquet_dir / f"{table}.parquet"
            if table_path.exists():
                adapter.load_table(ctx, table, [table_path])

        # Get query and capability info
        query = self._query_registry.get(query_id)
        if query is None:
            raise ValueError(f"Query {query_id} not found")

        capability = PLATFORM_CAPABILITIES.get(df_platform)
        family = capability.family if capability else "expression"

        impl = query.get_impl_for_family(family)
        if impl is None:
            raise ValueError(f"No {family} implementation for {query_id}")

        # Warmup
        for _ in range(self.config.warmup_iterations):
            result = query.execute(ctx, family)
            if hasattr(result, "collect"):
                result = result.collect()
            del result

        # Benchmark
        times = []
        row_count = 0
        for i in range(self.config.benchmark_iterations):
            start = time.perf_counter()
            result = query.execute(ctx, family)
            if hasattr(result, "collect"):
                result = result.collect()
            elapsed_ms = (time.perf_counter() - start) * 1000
            times.append(elapsed_ms)
            if i == 0:
                try:
                    row_count = len(result)
                except Exception:
                    row_count = 0
            del result

        return statistics.mean(times), row_count

    def _build_summary(
        self,
        sql_platform: str,
        df_platform: str,
        results: list[SQLComparisonResult],
    ) -> SQLVsDataFrameSummary:
        """Build summary from comparison results.

        Args:
            sql_platform: SQL platform used
            df_platform: DataFrame platform used
            results: List of query comparison results

        Returns:
            SQLVsDataFrameSummary
        """
        successful = [r for r in results if r.status == "SUCCESS" and r.speedup > 0]

        if not successful:
            return SQLVsDataFrameSummary(
                sql_platform=sql_platform,
                df_platform=df_platform,
                total_queries=len(results),
                df_faster_count=0,
                sql_faster_count=0,
                average_speedup=1.0,
                max_speedup=1.0,
                min_speedup=1.0,
                query_results=results,
            )

        speedups = [r.speedup for r in successful]
        df_faster = sum(1 for r in successful if r.speedup > 1.0)
        sql_faster = sum(1 for r in successful if r.speedup < 1.0)

        return SQLVsDataFrameSummary(
            sql_platform=sql_platform,
            df_platform=df_platform,
            total_queries=len(results),
            df_faster_count=df_faster,
            sql_faster_count=sql_faster,
            average_speedup=statistics.mean(speedups),
            max_speedup=max(speedups),
            min_speedup=min(speedups),
            query_results=results,
        )

    def generate_report(self, summary: SQLVsDataFrameSummary) -> str:
        """Generate markdown report from comparison summary.

        Args:
            summary: SQL vs DataFrame comparison summary

        Returns:
            Markdown formatted report string
        """
        lines = []
        lines.append("# SQL vs DataFrame Performance Comparison")
        lines.append("")
        lines.append(f"**SQL Platform:** {summary.sql_platform}")
        lines.append(f"**DataFrame Platform:** {summary.df_platform}")
        lines.append(f"**Scale Factor:** {self.config.scale_factor}")
        lines.append(f"**Total Queries:** {summary.total_queries}")
        lines.append("")

        lines.append("## Summary")
        lines.append("")
        lines.append(f"- **DataFrame wins:** {summary.df_faster_count} queries ({summary.df_wins_percentage:.1f}%)")
        lines.append(f"- **SQL wins:** {summary.sql_faster_count} queries")
        lines.append(f"- **Average speedup:** {summary.average_speedup:.2f}x")
        lines.append(f"- **Max speedup (DF fastest):** {summary.max_speedup:.2f}x")
        lines.append(f"- **Min speedup (SQL fastest):** {summary.min_speedup:.2f}x")
        lines.append("")

        lines.append("## Query-by-Query Results")
        lines.append("")
        lines.append("| Query | SQL (ms) | DataFrame (ms) | Speedup | Winner |")
        lines.append("|-------|----------|----------------|---------|--------|")

        for result in summary.query_results:
            if result.status == "SUCCESS":
                winner = "DataFrame" if result.speedup > 1.0 else "SQL"
                lines.append(
                    f"| {result.query_id} | {result.sql_time_ms:.2f} | "
                    f"{result.df_time_ms:.2f} | {result.speedup:.2f}x | {winner} |"
                )
            else:
                lines.append(f"| {result.query_id} | ERROR | ERROR | N/A | - |")

        lines.append("")

        # Interpretation
        lines.append("## Interpretation")
        lines.append("")
        if summary.average_speedup > 1.0:
            lines.append(
                f"DataFrame execution is **{summary.average_speedup:.1f}x faster** on average than SQL "
                f"for this workload."
            )
        else:
            sql_speedup = 1.0 / summary.average_speedup if summary.average_speedup > 0 else 1.0
            lines.append(f"SQL execution is **{sql_speedup:.1f}x faster** on average than DataFrame for this workload.")

        return "\n".join(lines)


def run_quick_comparison(
    platforms: list[str] | None = None,
    scale_factor: float = 0.01,
    query_ids: list[str] | None = None,
    data_dir: str | Path | None = None,
) -> list[PlatformBenchmarkResult]:
    """Run a quick cross-platform comparison.

    Convenience function for running a simple benchmark comparison.

    Args:
        platforms: Platforms to compare. Defaults to all available.
        scale_factor: TPC-H scale factor. Defaults to 0.01.
        query_ids: Query IDs to run. Defaults to ["Q1", "Q3", "Q6", "Q10"].
        data_dir: Data directory. Defaults to standard location.

    Returns:
        List of PlatformBenchmarkResult
    """
    suite = DataFrameBenchmarkSuite(
        config=BenchmarkConfig(
            scale_factor=scale_factor,
            query_ids=query_ids or ["Q1", "Q3", "Q6", "Q10"],
            warmup_iterations=1,
            benchmark_iterations=3,
        )
    )

    if platforms is None:
        platforms = suite.get_available_platforms()

    if data_dir is None:
        data_dir = get_benchmark_runs_datagen_path("tpch", scale_factor)

    return suite.run_comparison(platforms=platforms, data_dir=data_dir)


def run_sql_vs_dataframe(
    sql_platform: str = "duckdb",
    df_platform: str = "polars-df",
    scale_factor: float = 0.01,
    query_ids: list[str] | None = None,
    data_dir: str | Path | None = None,
) -> SQLVsDataFrameSummary:
    """Run SQL vs DataFrame comparison.

    Convenience function for comparing SQL and DataFrame execution.

    Args:
        sql_platform: SQL platform ("duckdb" or "sqlite")
        df_platform: DataFrame platform (e.g., "polars-df")
        scale_factor: TPC-H scale factor. Defaults to 0.01.
        query_ids: Query IDs to run. Defaults to ["Q1", "Q3", "Q6", "Q10"].
        data_dir: Data directory. Defaults to standard location.

    Returns:
        SQLVsDataFrameSummary with comparison results
    """
    benchmark = SQLVsDataFrameBenchmark(
        config=BenchmarkConfig(
            scale_factor=scale_factor,
            query_ids=query_ids or ["Q1", "Q3", "Q6", "Q10"],
            warmup_iterations=1,
            benchmark_iterations=3,
        )
    )

    if data_dir is None:
        data_dir = get_benchmark_runs_datagen_path("tpch", scale_factor)

    return benchmark.run_comparison(
        sql_platform=sql_platform,
        df_platform=df_platform,
        data_dir=data_dir,
    )


class DataFrameComparisonPlotter:
    """Generate ASCII visualizations for DataFrame benchmark comparisons.

    Example:
        plotter = DataFrameComparisonPlotter(results, theme="light")
        charts = plotter.generate_charts(output_dir="charts/")
    """

    def __init__(
        self,
        results: list[PlatformBenchmarkResult],
        theme: str = "light",
    ):
        """Initialize the plotter with benchmark results.

        Args:
            results: List of platform benchmark results
            theme: Theme name ("light" or "dark")
        """
        if not results:
            raise ValueError("No results provided for visualization")
        self.results = results
        self.theme = theme

    def generate_charts(self, output_dir: str | Path, **kwargs: object) -> dict[str, Path]:
        """Generate ASCII charts from DataFrame benchmark results.

        Renders performance_bar, distribution_box, and query_heatmap charts
        as ``.txt`` files in the specified output directory.

        Args:
            output_dir: Directory to write chart files into.

        Returns:
            Mapping of chart type name to exported file path.
        """
        from benchbox.core.visualization.chart_generator import (
            generate_comparison_charts,
            normalized_from_dataframe,
        )

        normalized = normalized_from_dataframe(self.results)
        return generate_comparison_charts(normalized, output_dir, theme=self.theme)


class SQLVsDataFramePlotter:
    """Generate ASCII visualizations for SQL vs DataFrame comparisons.

    Example:
        plotter = SQLVsDataFramePlotter(summary, theme="light")
        charts = plotter.generate_charts(output_dir="charts/")
    """

    def __init__(
        self,
        summary: SQLVsDataFrameSummary,
        theme: str = "light",
    ):
        """Initialize the plotter with comparison summary.

        Args:
            summary: SQL vs DataFrame comparison summary
            theme: Theme name ("light" or "dark")
        """
        self.summary = summary
        self.theme = theme

    def generate_charts(self, output_dir: str | Path, **kwargs: object) -> dict[str, Path]:
        """Generate ASCII charts from SQL vs DataFrame comparison.

        Renders performance_bar, distribution_box, and query_heatmap charts
        as ``.txt`` files in the specified output directory.

        Args:
            output_dir: Directory to write chart files into.

        Returns:
            Mapping of chart type name to exported file path.
        """
        from benchbox.core.visualization.chart_generator import (
            generate_comparison_charts,
            normalized_from_sql_vs_df,
        )

        normalized = normalized_from_sql_vs_df(self.summary)
        return generate_comparison_charts(normalized, output_dir, theme=self.theme)
