"""
Core data models for execution results and metadata.

These models represent the outputs and state information from pipeline
execution with type safety.
"""

from dataclasses import dataclass, field
from datetime import datetime
from decimal import Decimal
from typing import Any
from uuid import UUID, uuid4


@dataclass
class LLMResponse:
    """Response from a single LLM invocation."""

    text: str
    tokens_in: int
    tokens_out: int
    model: str
    cost: Decimal
    latency_ms: float
    metadata: dict[str, Any] = field(default_factory=dict)
    structured_result: Any = None  # Optional: Pydantic object for structured output (avoids serialize/parse cycle)


@dataclass
class CostEstimate:
    """
    Cost estimation for pipeline execution.

    Provides detailed cost breakdown with Decimal precision to avoid floating-point errors.

    Attributes:
        total_cost: Total cost in USD (Decimal for precision)
        total_tokens: Total tokens consumed (input + output)
        input_tokens: Input tokens sent to LLM
        output_tokens: Output tokens generated by LLM
        rows: Number of rows processed
        breakdown_by_stage: Cost breakdown by pipeline stage
        confidence: Confidence level (estimate, sample-based, actual)

    Example:
        ```python
        result = pipeline.execute()

        # Access costs
        print(f"Total: ${result.costs.total_cost}")
        print(f"Input tokens: {result.costs.input_tokens:,}")
        print(f"Output tokens: {result.costs.output_tokens:,}")
        print(f"Cost per row: ${result.costs.total_cost / result.costs.rows:.4f}")
        ```
    """

    total_cost: Decimal
    total_tokens: int
    input_tokens: int = 0
    output_tokens: int = 0
    rows: int = 0
    breakdown_by_stage: dict[str, Decimal] = field(default_factory=dict)
    confidence: str = "estimate"  # estimate, sample-based, actual


@dataclass
class ProcessingStats:
    """Statistics from pipeline execution."""

    total_rows: int
    processed_rows: int
    failed_rows: int = 0
    skipped_rows: int = 0
    rows_per_second: float = 0.0
    total_duration_seconds: float = 0.0
    stage_durations: dict[str, float] = field(default_factory=dict)


@dataclass
class ErrorInfo:
    """Information about a processing error."""

    row_index: int
    stage: str
    error_type: str
    message: str
    timestamp: datetime = field(default_factory=datetime.now)
    retryable: bool = True


@dataclass
class RowMetadata:
    """Metadata for tracking individual rows through the pipeline."""

    row_index: int
    row_id: Any = None
    custom: dict[str, Any] = field(default_factory=dict)


@dataclass
class PromptBatch:
    """Batch of prompts for processing."""

    prompts: list[str]
    metadata: list[RowMetadata]
    batch_id: int | str = 0


@dataclass
class ResponseBatch:
    """Batch of responses from LLM."""

    responses: (
        list[str] | list["LLMResponse"]
    )  # Can be strings or full LLMResponse objects
    metadata: list[RowMetadata]
    tokens_used: int = 0
    cost: Decimal = Decimal("0")
    batch_id: int | str = 0
    latencies_ms: list[float] = field(default_factory=list)


@dataclass
class CheckpointInfo:
    """Information about a saved checkpoint."""

    checkpoint_id: UUID
    session_id: UUID
    timestamp: datetime
    rows_processed: int
    total_rows: int
    cost_so_far: Decimal
    stage: str
    path: str | None = None


class ExecutionResult:
    """
    Result from pipeline execution.

    Contains the output data, metrics, costs, and any errors.

    Attributes:
        data: Result data as DataContainer (framework-agnostic)
        metrics: Processing statistics
        costs: Cost breakdown
        errors: List of errors encountered
        execution_id: Unique execution identifier
        start_time: When execution started
        end_time: When execution completed
        success: Whether execution succeeded
        metadata: Additional execution metadata

    Example:
        ```python
        result = pipeline.execute()

        # Access raw container (default)
        for row in result.data:
            print(row)

        # Convert to Pandas when needed
        df = result.to_pandas()
        print(df["column"].iloc[0])

        # Or Polars
        pl_df = result.to_polars()

        # Or list of dicts
        rows = result.to_list()

        # Check execution time
        print(f"Duration: {result.duration:.2f}s")
        ```
    """

    def __init__(
        self,
        data: Any,  # ResultContainerImpl or any DataContainer
        metrics: "ProcessingStats",
        costs: "CostEstimate",
        errors: list["ErrorInfo"] | None = None,
        execution_id: UUID | None = None,
        start_time: datetime | None = None,
        end_time: datetime | None = None,
        success: bool = True,
        metadata: dict[str, Any] | None = None,
    ):
        self.data = data  # Store as-is (container)
        self.metrics = metrics
        self.costs = costs
        self.errors = errors or []
        self.execution_id = execution_id or uuid4()
        self.start_time = start_time or datetime.now()
        self.end_time = end_time
        self.success = success
        self.metadata = metadata or {}

    @property
    def duration(self) -> float:
        """Get execution duration in seconds."""
        if self.end_time is None:
            return 0.0
        return (self.end_time - self.start_time).total_seconds()

    @property
    def error_rate(self) -> float:
        """Get error rate as percentage."""
        if self.metrics.total_rows == 0:
            return 0.0
        return (self.metrics.failed_rows / self.metrics.total_rows) * 100

    def to_pandas(self) -> Any:
        """
        Convert result data to Pandas DataFrame.

        Returns:
            pandas.DataFrame
        """
        import pandas as pd

        if isinstance(self.data, pd.DataFrame):
            return self.data

        if hasattr(self.data, "to_pandas"):
            return self.data.to_pandas()

        return pd.DataFrame(self.data)

    def to_polars(self) -> Any:
        """
        Convert result data to Polars DataFrame.

        Returns:
            polars.DataFrame
        """
        if hasattr(self.data, "to_polars"):
            return self.data.to_polars()

        import polars as pl

        return pl.from_pandas(self.to_pandas())

    def to_list(self) -> list[dict[str, Any]]:
        """
        Convert result data to list of dictionaries.

        Returns:
            List of row dictionaries
        """
        if hasattr(self.data, "to_list"):
            return self.data.to_list()

        return self.to_pandas().to_dict(orient="records")

    def validate_output_quality(self, output_columns: list[str]) -> "QualityReport":
        """
        Validate the quality of output data by checking for null/empty values.

        Args:
            output_columns: List of output column names to check

        Returns:
            QualityReport with quality metrics and warnings
        """
        # Get data as list of dicts (works with any container)
        rows = self.to_list()
        total_rows = len(rows)
        total_columns = len(output_columns)
        total_cells = total_rows * total_columns

        # Count null and empty values across ALL output columns
        null_count = 0
        empty_count = 0
        valid_row_count = 0

        for row in rows:
            row_has_valid = False
            for col in output_columns:
                value = row.get(col)
                if value is None:
                    null_count += 1
                elif isinstance(value, str) and value.strip() == "":
                    empty_count += 1
                else:
                    row_has_valid = True
            if row_has_valid:
                valid_row_count += 1

        valid_outputs = valid_row_count
        success_rate = (valid_outputs / total_rows * 100) if total_rows > 0 else 0.0

        # Determine quality score
        if success_rate >= 95.0:
            quality_score = "excellent"
        elif success_rate >= 80.0:
            quality_score = "good"
        elif success_rate >= 50.0:
            quality_score = "poor"
        else:
            quality_score = "critical"

        # Generate warnings and issues
        warnings = []
        issues = []

        if success_rate < 70.0:
            issues.append(
                f"LOW SUCCESS RATE: Only {success_rate:.1f}% of rows have valid data "
                f"({valid_outputs}/{total_rows} rows with at least one valid column)"
            )

        if (
            total_cells > 0 and null_count > total_cells * 0.3
        ):  # > 30% of all cells are null
            issues.append(
                f"HIGH NULL RATE: {null_count} null cells out of {total_cells} total "
                f"({null_count / total_cells * 100:.1f}% of all output cells)"
            )

        if (
            total_cells > 0 and empty_count > total_cells * 0.1
        ):  # > 10% of all cells are empty
            warnings.append(
                f"Empty outputs detected: {empty_count} empty cells out of {total_cells} total "
                f"({empty_count / total_cells * 100:.1f}% of all output cells)"
            )

        # Check if reported metrics match actual data quality
        if self.metrics.failed_rows == 0 and null_count > 0:
            issues.append(
                f"METRICS MISMATCH: Pipeline reported 0 failures but "
                f"{null_count} cells have null outputs. This may indicate silent errors."
            )

        return QualityReport(
            total_rows=total_rows,
            valid_outputs=valid_outputs,
            null_outputs=null_count,
            empty_outputs=empty_count,
            success_rate=success_rate,
            quality_score=quality_score,
            warnings=warnings,
            issues=issues,
        )


@dataclass
class ValidationResult:
    """Result from validation checks."""

    is_valid: bool
    errors: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)

    def add_error(self, error: str) -> None:
        """Add an error message."""
        self.errors.append(error)
        self.is_valid = False

    def add_warning(self, warning: str) -> None:
        """Add a warning message."""
        self.warnings.append(warning)


@dataclass
class QualityReport:
    """Quality assessment of pipeline output."""

    total_rows: int
    valid_outputs: int
    null_outputs: int
    empty_outputs: int
    success_rate: float
    quality_score: str  # "excellent", "good", "poor", "critical"
    warnings: list[str] = field(default_factory=list)
    issues: list[str] = field(default_factory=list)

    @property
    def is_acceptable(self) -> bool:
        """Check if quality is acceptable (>= 70% success)."""
        return self.success_rate >= 70.0

    @property
    def has_issues(self) -> bool:
        """Check if there are any issues."""
        return len(self.issues) > 0 or len(self.warnings) > 0


@dataclass
class WriteConfirmation:
    """Confirmation of successful data write."""

    path: str
    rows_written: int
    bytes_written: int = 0
    format: str = "csv"
    timestamp: datetime = field(default_factory=datetime.now)
