from typing import Optional
"""
Simplified Generate Stage Validation

Essential validation functions for the generate stage that follow KISS principles.
Provides fail-fast validation during immediate write processing with only the
critical checks needed for functional demo billing data.

Key validation criteria:
1. Profile existence (fail-fast before generation)
2. Reference data filter results (must return >0 records)
3. Schema compliance (required columns present)
4. Basic completeness (non-empty, no nulls, positive costs, date coverage)
5. Variance detection (costs must vary, no flat costs)
6. Cost conservation (within ±5% of configured cost)
7. File write success (output file exists)
"""

from dataclasses import dataclass
from datetime import datetime
from pathlib import Path

import polars as pl

from profiles.core_profiles import (
    list_anomaly_profiles,
    list_variance_profiles,
    validate_profile_exists,
)


@dataclass
class ValidationResult:
    """Simple validation result with success status and message."""

    success: bool
    message: str

    @classmethod
    def success_result(cls, message: str = "Validation passed") -> "ValidationResult":
        """Create a successful validation result."""
        return cls(success=True, message=message)

    @classmethod
    def fail(cls, message: str) -> "ValidationResult":
        """Create a failed validation result."""
        return cls(success=False, message=message)


def validate_profiles_exist(
    variance_profile: Optional[str], anomaly_profile: Optional[str]
) -> ValidationResult:
    """
    Validate that specified variance and anomaly profiles exist.

    This is the first validation check - fail fast if profiles don't exist
    before any expensive generation work begins.

    Args:
        variance_profile: Variance profile name to validate
        anomaly_profile: Anomaly profile name to validate

    Returns:
        ValidationResult indicating success or failure with details
    """
    if not variance_profile and not anomaly_profile:
        return ValidationResult.success_result(
            "No profiles specified, skipping validation"
        )

    if variance_profile:
        validate_profile_exists(variance_profile, "variance")

    if anomaly_profile:
        validate_profile_exists(anomaly_profile, "anomaly")

    return ValidationResult.success_result("All profiles exist and are valid")


def validate_reference_data_filter(filtered_df: pl.DataFrame) -> ValidationResult:
    """
    Validate that reference data filtering returned at least one record.

    If filters return 0 records, we should fail immediately rather than
    proceeding with generation that will produce no meaningful output.

    Args:
        filtered_df: DataFrame after applying reference data filters

    Returns:
        ValidationResult indicating success or failure
    """
    if filtered_df.height == 0:
        return ValidationResult.fail(
            "Reference data filters returned 0 records. "
            "Check your filter configuration against available reference data."
        )

    return ValidationResult.success_result(
        f"Reference data filter returned {filtered_df.height} records"
    )


def validate_required_schema(df: pl.DataFrame) -> ValidationResult:
    """
    Validate that generated data has required columns with correct structure.

    Essential columns for billing data functionality in downstream processing.

    Args:
        df: Generated DataFrame to validate

    Returns:
        ValidationResult indicating schema compliance
    """
    required_columns = {
        "timestamp": pl.Datetime,
        "cost": pl.Float64,
        "resource_id": pl.Utf8,
        "cloud_provider": pl.Utf8,
        "service": pl.Utf8,
    }

    missing_columns = []
    wrong_type_columns = []

    for col_name, expected_type in required_columns.items():
        if col_name not in df.columns:
            missing_columns.append(col_name)
        else:
            actual_type = df[col_name].dtype
            # Allow compatible types (e.g., Float32 for Float64, String for Utf8)
            if not _types_compatible(actual_type, expected_type):
                wrong_type_columns.append(
                    f"{col_name}: expected {expected_type}, got {actual_type}"
                )

    if missing_columns or wrong_type_columns:
        error_parts = []
        if missing_columns:
            error_parts.append(f"Missing columns: {missing_columns}")
        if wrong_type_columns:
            error_parts.append(f"Wrong types: {wrong_type_columns}")

        return ValidationResult.fail(
            f"Schema validation failed. {'. '.join(error_parts)}"
        )

    return ValidationResult.success_result(
        "Schema validation passed - all required columns present"
    )


def validate_basic_completeness(
    df: pl.DataFrame, start_date: str, end_date: str
) -> ValidationResult:
    """
    Validate basic completeness of generated data.

    "Basic completeness" definition:
    - Non-empty dataset (> 0 rows)
    - No null values in critical columns
    - All costs > 0 (no zero or negative costs)
    - Timestamp coverage spans the configured date range

    Args:
        df: Generated DataFrame to validate
        start_date: Expected start date (YYYY-MM-DD format)
        end_date: Expected end date (YYYY-MM-DD format)

    Returns:
        ValidationResult indicating completeness status
    """
    # Check non-empty dataset
    if df.height == 0:
        return ValidationResult.fail("Dataset is empty (0 rows)")

    # Check for null values in critical columns
    critical_columns = ["timestamp", "cost", "resource_id"]
    null_issues = []

    for col in critical_columns:
        if col in df.columns:
            null_count = df[col].null_count()
            if null_count > 0:
                null_issues.append(f"{col}: {null_count} null values")

    if null_issues:
        return ValidationResult.fail(
            f"Null values found in critical columns: {null_issues}"
        )

    # Check all costs > 0
    if "cost" in df.columns:
        min_cost = df["cost"].min()
        if min_cost <= 0:
            zero_cost_count = df.filter(pl.col("cost") <= 0).height
            return ValidationResult.fail(
                f"Found {zero_cost_count} records with cost <= 0. Minimum cost: {min_cost}"
            )

    # Check timestamp coverage
    if "timestamp" in df.columns:
        # Handle both string and date inputs from YAML parsing
        if hasattr(start_date, "isoformat"):
            start_date = start_date.isoformat()
        if hasattr(end_date, "isoformat"):
            end_date = end_date.isoformat()

        expected_start = datetime.fromisoformat(start_date)
        expected_end = datetime.fromisoformat(end_date)

            actual_min = df["timestamp"].min()
            actual_max = df["timestamp"].max()

            # Convert actual timestamps if they're strings
            if isinstance(actual_min, str):
                actual_min = datetime.fromisoformat(actual_min)
            if isinstance(actual_max, str):
                actual_max = datetime.fromisoformat(actual_max)

            # Allow some tolerance for date boundaries
            if actual_min > expected_start or actual_max < expected_end:
                return ValidationResult.fail(
                    f"Timestamp coverage insufficient. "
                    f"Expected: {expected_start} to {expected_end}, "
                    f"Actual: {actual_min} to {actual_max}"
                )
        except (ValueError, TypeError) as e:
            return ValidationResult.fail(f"Date validation error: {e}")

    return ValidationResult.success_result(
        f"Basic completeness passed: {df.height} rows, all costs > 0, date coverage OK"
    )


def validate_cost_variance(
    df: pl.DataFrame, min_variance: float = 0.10
) -> ValidationResult:
    """
    Ensure costs show variance - flat costs are unacceptable for demo purposes.

    Validates that costs vary sufficiently to create compelling demo patterns.
    Uses both coefficient of variation and range-based variance checks.

    Args:
        df: Generated DataFrame with cost column
        min_variance: Minimum required variance ratio (default 10%)

    Returns:
        ValidationResult indicating variance adequacy
    """
    if df.height == 0:
        return ValidationResult.fail("Cannot validate variance on empty dataset")

    if "cost" not in df.columns:
        return ValidationResult.fail("Cost column missing for variance validation")

    costs = df["cost"]

    # Calculate basic statistics
    cost_std = costs.std()
    cost_mean = costs.mean()
    cost_min = costs.min()
    cost_max = costs.max()

    if cost_mean == 0:
        return ValidationResult.fail("Cost mean is zero - cannot calculate variance")

    # Calculate coefficient of variation (standard deviation / mean)
    coefficient_of_variation = cost_std / cost_mean if cost_mean > 0 else 0

    # Calculate range-based variance (max - min) / mean
    range_variance = (cost_max - cost_min) / cost_mean if cost_mean > 0 else 0

    # Pass if either variance measure meets threshold
    if coefficient_of_variation >= min_variance or range_variance >= min_variance:
        return ValidationResult.success_result(
            f"Cost variance adequate: CV={coefficient_of_variation:.3f}, "
            f"Range variance={range_variance:.3f} (threshold={min_variance})"
        )
    else:
        return ValidationResult.fail(
            f"Insufficient cost variance (flat costs detected). "
            f"CV={coefficient_of_variation:.3f}, Range variance={range_variance:.3f}, "
            f"Required threshold={min_variance}. Check variance profile configuration."
        )


def validate_cost_conservation(
    df: pl.DataFrame, expected_cost: float, tolerance: float = 0.05
) -> ValidationResult:
    """
    Validate that total generated cost is within tolerance of expected cost.

    Essential for maintaining financial realism in demo scenarios.

    Args:
        df: Generated DataFrame with cost column
        expected_cost: Expected total cost from configuration
        tolerance: Allowed variance as decimal (0.05 = 5%)

    Returns:
        ValidationResult indicating cost conservation status
    """
    if df.height == 0:
        return ValidationResult.fail(
            "Cannot validate cost conservation on empty dataset"
        )

    if "cost" not in df.columns:
        return ValidationResult.fail(
            "Cost column missing for cost conservation validation"
        )

    if expected_cost <= 0:
        return ValidationResult.fail(f"Invalid expected cost: {expected_cost}")

    # Calculate total generated cost
    total_generated_cost = df["cost"].sum()

    # Calculate variance percentage
    variance = abs(total_generated_cost - expected_cost)
    variance_pct = (variance / expected_cost) * 100

    if variance_pct <= (tolerance * 100):
        return ValidationResult.success_result(
            f"Cost conservation OK: Generated ${total_generated_cost:.2f}, "
            f"Expected ${expected_cost:.2f}, Variance {variance_pct:.2f}%"
        )
    else:
        return ValidationResult.fail(
            f"Cost conservation violated: Generated ${total_generated_cost:.2f}, "
            f"Expected ${expected_cost:.2f}, Variance {variance_pct:.2f}% "
            f"(tolerance {tolerance * 100}%)"
        )


def validate_file_write_success(output_file: Path) -> ValidationResult:
    """
    Validate that file was successfully written to disk.

    Simple check that immediate write operation completed successfully.

    Args:
        output_file: Path to the output file that should exist

    Returns:
        ValidationResult indicating file write success
    """
    if not output_file.exists():
        return ValidationResult.fail(f"File write failed: {output_file} does not exist")

    # Check that file is not empty
    try:
        file_size = output_file.stat().st_size
        if file_size == 0:
            return ValidationResult.fail(f"File write failed: {output_file} is empty")

        return ValidationResult.success_result(
            f"File write successful: {output_file} ({file_size} bytes)"
        )

    except Exception as e:
        return ValidationResult.fail(f"File validation error: {e}")


def _types_compatible(actual_type: pl.DataType, expected_type: pl.DataType) -> bool:
    """
    Check if actual data type is compatible with expected type.

    Allows for reasonable type compatibility (e.g., Float32 vs Float64).
    """
    # Exact match
    if actual_type == expected_type:
        return True

    # Float compatibility
    if expected_type == pl.Float64 and actual_type in [pl.Float32, pl.Float64]:
        return True

    # String compatibility
    if expected_type == pl.Utf8 and actual_type in [pl.Utf8, pl.String]:
        return True

    # Datetime compatibility
    if expected_type == pl.Datetime and str(actual_type).startswith("Datetime"):
        return True

    return False


def validate_config_part_processing(
    config_part,  # ConfigPart type
    filtered_ref_df: pl.DataFrame,
    generated_df: pl.DataFrame,
    output_file: Path,
) -> ValidationResult:
    """
    Complete validation for config part processing with fail-fast approach.

    Runs all essential validation checks in order, failing fast on first error.
    This is the main validation function called during immediate write processing.

    Args:
        config_part: ConfigPart being processed
        filtered_ref_df: Reference data after filtering
        generated_df: Generated billing data
        output_file: Path to output file

    Returns:
        ValidationResult with overall validation status
    """
    # 1. Profile validation (should be done earlier, but double-check)
    profile_result = validate_profiles_exist(
        config_part.variance_profile, config_part.anomaly_profile
    )
    if not profile_result.success:
        return profile_result

    # 2. Reference data filter validation
    ref_result = validate_reference_data_filter(filtered_ref_df)
    if not ref_result.success:
        return ref_result

    # 3. Generated data existence
    if generated_df.height == 0:
        return ValidationResult.fail("Generation produced 0 rows")

    # 4. Schema compliance
    schema_result = validate_required_schema(generated_df)
    if not schema_result.success:
        return schema_result

    # 5. Basic completeness
    completeness_result = validate_basic_completeness(
        generated_df, config_part.start_date, config_part.end_date
    )
    if not completeness_result.success:
        return completeness_result

    # 6. Variance detection (ensure costs vary)
    variance_result = validate_cost_variance(generated_df)
    if not variance_result.success:
        return variance_result

    # 7. Cost conservation
    cost_result = validate_cost_conservation(generated_df, config_part.total_cost)
    if not cost_result.success:
        return cost_result

    # 8. File write success
    file_result = validate_file_write_success(output_file)
    if not file_result.success:
        return file_result

    return ValidationResult.success_result(
        f"All validation checks passed for {config_part.provider}/{config_part.service}"
    )
