"""
Results Validator for Atomizer Optimization Studies

Validates optimization results stored in study.db and provides
analysis of trial quality, constraint satisfaction, and data integrity.

Usage:
    from optimization_engine.validators.results_validator import validate_results

    result = validate_results("studies/my_study/2_results/study.db")
    if result.is_valid:
        print("Results are valid")
    else:
        for error in result.errors:
            print(f"ERROR: {error}")
"""

from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Dict, Any, Optional, Tuple
import json


@dataclass
class ResultsError:
    """Represents an error found during results validation."""
    code: str
    message: str
    trial_number: Optional[int] = None

    def __str__(self) -> str:
        if self.trial_number is not None:
            return f"[{self.code}] Trial #{self.trial_number}: {self.message}"
        return f"[{self.code}] {self.message}"


@dataclass
class ResultsWarning:
    """Represents a warning found during results validation."""
    code: str
    message: str
    trial_number: Optional[int] = None

    def __str__(self) -> str:
        if self.trial_number is not None:
            return f"[{self.code}] Trial #{self.trial_number}: {self.message}"
        return f"[{self.code}] {self.message}"


@dataclass
class ResultsInfo:
    """Information about the optimization results."""
    study_name: str = ""
    n_trials: int = 0
    n_completed: int = 0
    n_failed: int = 0
    n_pruned: int = 0
    n_pareto: int = 0
    feasibility_rate: float = 0.0
    is_multi_objective: bool = False
    objective_names: List[str] = field(default_factory=list)
    best_values: Dict[str, float] = field(default_factory=dict)
    parameter_names: List[str] = field(default_factory=list)


@dataclass
class ResultsValidationResult:
    """Complete validation result for optimization results."""
    is_valid: bool
    errors: List[ResultsError]
    warnings: List[ResultsWarning]
    info: ResultsInfo

    def __str__(self) -> str:
        lines = []

        # Status
        status = "[OK] Results validation passed!" if self.is_valid else "[X] Results validation failed!"
        lines.append(status)
        lines.append("")

        # Info
        lines.append("RESULTS SUMMARY")
        lines.append("-" * 40)
        lines.append(f"  Study: {self.info.study_name}")
        lines.append(f"  Total trials: {self.info.n_trials}")
        lines.append(f"  Completed: {self.info.n_completed}")
        lines.append(f"  Failed: {self.info.n_failed}")
        if self.info.n_pruned > 0:
            lines.append(f"  Pruned: {self.info.n_pruned}")
        lines.append(f"  Multi-objective: {'Yes' if self.info.is_multi_objective else 'No'}")
        if self.info.is_multi_objective and self.info.n_pareto > 0:
            lines.append(f"  Pareto-optimal: {self.info.n_pareto}")
        if self.info.feasibility_rate > 0:
            lines.append(f"  Feasibility rate: {self.info.feasibility_rate:.1f}%")
        lines.append("")

        # Best values
        if self.info.best_values:
            lines.append("BEST VALUES")
            lines.append("-" * 40)
            for name, value in self.info.best_values.items():
                lines.append(f"  {name}: {value:.4f}")
            lines.append("")

        # Errors
        if self.errors:
            lines.append("ERRORS")
            lines.append("-" * 40)
            for error in self.errors:
                lines.append(f"  {error}")
            lines.append("")

        # Warnings
        if self.warnings:
            lines.append("WARNINGS")
            lines.append("-" * 40)
            for warning in self.warnings:
                lines.append(f"  {warning}")
            lines.append("")

        return "\n".join(lines)


def validate_results(
    db_path: str,
    config_path: Optional[str] = None,
    min_trials: int = 1
) -> ResultsValidationResult:
    """
    Validate optimization results stored in study.db.

    Args:
        db_path: Path to study.db file
        config_path: Optional path to optimization_config.json for cross-validation
        min_trials: Minimum number of completed trials required

    Returns:
        ResultsValidationResult with errors, warnings, and info
    """
    errors: List[ResultsError] = []
    warnings: List[ResultsWarning] = []
    info = ResultsInfo()

    db_path = Path(db_path)

    # Check database exists
    if not db_path.exists():
        errors.append(ResultsError(
            code="DB_NOT_FOUND",
            message=f"Database not found: {db_path}"
        ))
        return ResultsValidationResult(
            is_valid=False,
            errors=errors,
            warnings=warnings,
            info=info
        )

    # Try to load with Optuna
    try:
        import optuna

        storage_url = f"sqlite:///{db_path}"

        # Get all studies in the database
        storage = optuna.storages.RDBStorage(url=storage_url)
        study_summaries = storage.get_all_studies()

        if not study_summaries:
            errors.append(ResultsError(
                code="NO_STUDIES",
                message="Database contains no optimization studies"
            ))
            return ResultsValidationResult(
                is_valid=False,
                errors=errors,
                warnings=warnings,
                info=info
            )

        # Use the first (usually only) study
        study_summary = study_summaries[0]
        info.study_name = study_summary.study_name

        # Load the full study
        study = optuna.load_study(
            study_name=info.study_name,
            storage=storage_url
        )

        # Basic counts
        info.n_trials = len(study.trials)
        info.n_completed = len([t for t in study.trials
                               if t.state == optuna.trial.TrialState.COMPLETE])
        info.n_failed = len([t for t in study.trials
                            if t.state == optuna.trial.TrialState.FAIL])
        info.n_pruned = len([t for t in study.trials
                            if t.state == optuna.trial.TrialState.PRUNED])

        # Check minimum trials
        if info.n_completed < min_trials:
            errors.append(ResultsError(
                code="INSUFFICIENT_TRIALS",
                message=f"Only {info.n_completed} completed trials (minimum: {min_trials})"
            ))

        # Check for multi-objective
        info.is_multi_objective = len(study.directions) > 1

        # Get parameter names from first completed trial
        for trial in study.trials:
            if trial.state == optuna.trial.TrialState.COMPLETE:
                info.parameter_names = list(trial.params.keys())
                break

        # Analyze Pareto front for multi-objective
        if info.is_multi_objective:
            try:
                pareto_trials = study.best_trials
                info.n_pareto = len(pareto_trials)

                if info.n_pareto == 0 and info.n_completed > 0:
                    warnings.append(ResultsWarning(
                        code="NO_PARETO",
                        message="No Pareto-optimal solutions found despite completed trials"
                    ))
            except Exception as e:
                warnings.append(ResultsWarning(
                    code="PARETO_ERROR",
                    message=f"Could not compute Pareto front: {e}"
                ))
        else:
            # Single objective - get best value
            if info.n_completed > 0:
                try:
                    best_trial = study.best_trial
                    info.best_values["objective"] = best_trial.value
                except Exception:
                    pass

        # Analyze feasibility
        feasible_count = 0
        for trial in study.trials:
            if trial.state == optuna.trial.TrialState.COMPLETE:
                # Check user_attrs for feasibility flag
                is_feasible = trial.user_attrs.get('feasible', True)
                if is_feasible:
                    feasible_count += 1

        if info.n_completed > 0:
            info.feasibility_rate = (feasible_count / info.n_completed) * 100

            if info.feasibility_rate < 50:
                warnings.append(ResultsWarning(
                    code="LOW_FEASIBILITY",
                    message=f"Low feasibility rate ({info.feasibility_rate:.1f}%) - consider relaxing constraints or adjusting bounds"
                ))
            elif info.feasibility_rate < 80:
                warnings.append(ResultsWarning(
                    code="MODERATE_FEASIBILITY",
                    message=f"Moderate feasibility rate ({info.feasibility_rate:.1f}%)"
                ))

        # Check for data quality issues
        _validate_trial_data(study, errors, warnings)

        # Cross-validate with config if provided
        if config_path:
            _cross_validate_with_config(study, config_path, info, errors, warnings)

    except ImportError:
        errors.append(ResultsError(
            code="OPTUNA_NOT_INSTALLED",
            message="Optuna is not installed. Cannot validate results."
        ))
    except Exception as e:
        errors.append(ResultsError(
            code="LOAD_ERROR",
            message=f"Failed to load study: {e}"
        ))

    return ResultsValidationResult(
        is_valid=len(errors) == 0,
        errors=errors,
        warnings=warnings,
        info=info
    )


def _validate_trial_data(study, errors: List[ResultsError], warnings: List[ResultsWarning]):
    """Validate individual trial data quality."""
    import optuna

    for trial in study.trials:
        if trial.state != optuna.trial.TrialState.COMPLETE:
            continue

        # Check for NaN or inf values
        if trial.values:
            for i, val in enumerate(trial.values):
                if val is None:
                    errors.append(ResultsError(
                        code="NULL_OBJECTIVE",
                        message=f"Objective {i} has null value",
                        trial_number=trial.number
                    ))
                elif not isinstance(val, (int, float)):
                    errors.append(ResultsError(
                        code="INVALID_OBJECTIVE_TYPE",
                        message=f"Objective {i} has invalid type: {type(val)}",
                        trial_number=trial.number
                    ))
                elif isinstance(val, float):
                    import math
                    if math.isnan(val):
                        errors.append(ResultsError(
                            code="NAN_OBJECTIVE",
                            message=f"Objective {i} is NaN",
                            trial_number=trial.number
                        ))
                    elif math.isinf(val):
                        warnings.append(ResultsWarning(
                            code="INF_OBJECTIVE",
                            message=f"Objective {i} is infinite",
                            trial_number=trial.number
                        ))

        # Check for missing parameters
        if not trial.params:
            errors.append(ResultsError(
                code="MISSING_PARAMS",
                message="Trial has no parameters recorded",
                trial_number=trial.number
            ))

        # Check for negative values where unexpected
        for param_name, param_value in trial.params.items():
            if 'thickness' in param_name.lower() and param_value <= 0:
                warnings.append(ResultsWarning(
                    code="INVALID_THICKNESS",
                    message=f"{param_name} = {param_value} (non-positive thickness)",
                    trial_number=trial.number
                ))
            elif 'diameter' in param_name.lower() and param_value <= 0:
                warnings.append(ResultsWarning(
                    code="INVALID_DIAMETER",
                    message=f"{param_name} = {param_value} (non-positive diameter)",
                    trial_number=trial.number
                ))


def _cross_validate_with_config(
    study,
    config_path: str,
    info: ResultsInfo,
    errors: List[ResultsError],
    warnings: List[ResultsWarning]
):
    """Cross-validate results with optimization config."""
    import optuna

    config_path = Path(config_path)
    if not config_path.exists():
        warnings.append(ResultsWarning(
            code="CONFIG_NOT_FOUND",
            message=f"Config file not found for cross-validation: {config_path}"
        ))
        return

    try:
        with open(config_path, 'r') as f:
            config = json.load(f)

        # Check parameter names match
        config_params = set()
        for var in config.get('design_variables', []):
            param_name = var.get('parameter', var.get('name', ''))
            if param_name:
                config_params.add(param_name)

        result_params = set(info.parameter_names)

        missing_in_results = config_params - result_params
        extra_in_results = result_params - config_params

        if missing_in_results:
            warnings.append(ResultsWarning(
                code="MISSING_PARAMS_IN_RESULTS",
                message=f"Config params not in results: {missing_in_results}"
            ))

        if extra_in_results:
            warnings.append(ResultsWarning(
                code="EXTRA_PARAMS_IN_RESULTS",
                message=f"Results have extra params not in config: {extra_in_results}"
            ))

        # Check objective count matches
        config_objectives = len(config.get('objectives', []))
        result_objectives = len(study.directions)

        if config_objectives != result_objectives:
            warnings.append(ResultsWarning(
                code="OBJECTIVE_COUNT_MISMATCH",
                message=f"Config has {config_objectives} objectives, results have {result_objectives}"
            ))

        # Get objective names from config
        for obj in config.get('objectives', []):
            obj_name = obj.get('name', '')
            if obj_name:
                info.objective_names.append(obj_name)

        # Check bounds violations
        for trial in study.trials:
            if trial.state != optuna.trial.TrialState.COMPLETE:
                continue

            for var in config.get('design_variables', []):
                param_name = var.get('parameter', var.get('name', ''))
                bounds = var.get('bounds', [])

                if param_name in trial.params and len(bounds) == 2:
                    value = trial.params[param_name]
                    min_val, max_val = bounds

                    # Small tolerance for floating point
                    tolerance = (max_val - min_val) * 0.001

                    if value < min_val - tolerance:
                        warnings.append(ResultsWarning(
                            code="BELOW_MIN_BOUND",
                            message=f"{param_name} = {value} < min ({min_val})",
                            trial_number=trial.number
                        ))
                    elif value > max_val + tolerance:
                        warnings.append(ResultsWarning(
                            code="ABOVE_MAX_BOUND",
                            message=f"{param_name} = {value} > max ({max_val})",
                            trial_number=trial.number
                        ))

    except json.JSONDecodeError as e:
        warnings.append(ResultsWarning(
            code="CONFIG_PARSE_ERROR",
            message=f"Could not parse config JSON: {e}"
        ))
    except Exception as e:
        warnings.append(ResultsWarning(
            code="CONFIG_ERROR",
            message=f"Error reading config: {e}"
        ))


def validate_study_results(study_name: str) -> ResultsValidationResult:
    """
    Convenience function to validate results for a named study.

    Args:
        study_name: Name of the study (folder in studies/)

    Returns:
        ResultsValidationResult
    """
    from pathlib import Path

    study_dir = Path(f"studies/{study_name}")
    db_path = study_dir / "2_results" / "study.db"
    config_path = study_dir / "1_setup" / "optimization_config.json"

    return validate_results(
        db_path=str(db_path),
        config_path=str(config_path) if config_path.exists() else None
    )


def get_pareto_summary(db_path: str) -> Dict[str, Any]:
    """
    Get a summary of Pareto-optimal designs from results.

    Args:
        db_path: Path to study.db

    Returns:
        Dictionary with Pareto front information
    """
    try:
        import optuna

        storage_url = f"sqlite:///{db_path}"
        storage = optuna.storages.RDBStorage(url=storage_url)
        study_summaries = storage.get_all_studies()

        if not study_summaries:
            return {"error": "No studies found"}

        study = optuna.load_study(
            study_name=study_summaries[0].study_name,
            storage=storage_url
        )

        if len(study.directions) < 2:
            # Single objective
            if study.best_trial:
                return {
                    "type": "single_objective",
                    "best_trial": study.best_trial.number,
                    "best_value": study.best_value,
                    "best_params": study.best_params
                }
            return {"error": "No completed trials"}

        # Multi-objective
        pareto_trials = study.best_trials

        designs = []
        for trial in pareto_trials:
            designs.append({
                "trial_number": trial.number,
                "objectives": trial.values,
                "parameters": trial.params,
                "user_attrs": dict(trial.user_attrs)
            })

        # Calculate ranges
        ranges = {}
        if designs:
            for i in range(len(designs[0]["objectives"])):
                values = [d["objectives"][i] for d in designs]
                ranges[f"objective_{i}"] = {
                    "min": min(values),
                    "max": max(values),
                    "spread": max(values) - min(values)
                }

        return {
            "type": "multi_objective",
            "n_pareto": len(pareto_trials),
            "designs": designs,
            "ranges": ranges
        }

    except Exception as e:
        return {"error": str(e)}


if __name__ == "__main__":
    import sys

    if len(sys.argv) < 2:
        print("Usage: python results_validator.py <study_name_or_db_path>")
        print("Example: python results_validator.py uav_arm_optimization")
        print("Example: python results_validator.py studies/my_study/2_results/study.db")
        sys.exit(1)

    arg = sys.argv[1]

    # Check if it's a study name or db path
    if arg.endswith('.db'):
        result = validate_results(arg)
    else:
        result = validate_study_results(arg)

    print(result)
