"""Deterministic reproduction mode for byte-identical audit results.

This module provides comprehensive determinism controls to ensure that
identical configurations and seeds produce byte-identical results across
different runs and environments. Essential for regulatory compliance
where reproducibility is mandatory.
"""

import logging
import os
import random
import warnings
from typing import Any

import numpy as np

logger = logging.getLogger(__name__)


def set_repro(
    seed: int = 42,
    strict: bool = True,
    thread_control: bool = True,
    warn_on_failure: bool = True,
) -> dict[str, Any]:
    """Set comprehensive deterministic reproduction mode.

    This function configures all known sources of randomness and parallelism
    to ensure byte-identical results across runs. Critical for regulatory
    compliance where audit results must be exactly reproducible.

    Args:
        seed: Master random seed for all libraries
        strict: Whether to enforce strict determinism (may impact performance)
        thread_control: Whether to control thread counts for deterministic parallel processing
        warn_on_failure: Whether to warn if some determinism controls fail

    Returns:
        Dictionary with status of each determinism control

    Examples:
        >>> # Basic deterministic mode
        >>> status = set_repro(seed=42)

        >>> # Strict regulatory mode
        >>> status = set_repro(seed=42, strict=True, thread_control=True)

        >>> # Performance mode (some non-determinism allowed)
        >>> status = set_repro(seed=42, strict=False, thread_control=False)

    """
    logger.info(f"Setting deterministic reproduction mode (seed={seed}, strict={strict})")

    status = {
        "seed": seed,
        "strict_mode": strict,
        "thread_control": thread_control,
        "controls": {},
    }

    # 1. Python built-in random
    status["controls"]["python_random"] = _set_python_random(seed, warn_on_failure)

    # 2. NumPy random
    status["controls"]["numpy_random"] = _set_numpy_random(seed, warn_on_failure)

    # 3. Environment variables for determinism
    status["controls"]["environment"] = _set_environment_determinism(strict, warn_on_failure)

    # 4. Thread control for parallel libraries
    if thread_control:
        status["controls"]["threads"] = _set_thread_control(warn_on_failure)

    # 5. XGBoost determinism
    status["controls"]["xgboost"] = _set_xgboost_determinism(seed, strict, warn_on_failure)

    # 6. LightGBM determinism
    status["controls"]["lightgbm"] = _set_lightgbm_determinism(seed, strict, warn_on_failure)

    # 7. Scikit-learn determinism
    status["controls"]["sklearn"] = _set_sklearn_determinism(warn_on_failure)

    # 8. SHAP determinism
    status["controls"]["shap"] = _set_shap_determinism(seed, warn_on_failure)

    # 9. Pandas determinism
    status["controls"]["pandas"] = _set_pandas_determinism(warn_on_failure)

    # 10. System-level determinism
    status["controls"]["system"] = _set_system_determinism(strict, warn_on_failure)

    # Count successful controls
    successful = sum(1 for control in status["controls"].values() if control.get("success", False))
    total = len(status["controls"])

    logger.info(f"Deterministic reproduction configured: {successful}/{total} controls successful")

    if strict and successful < total:
        failed_controls = [name for name, control in status["controls"].items() if not control.get("success", False)]
        logger.warning(f"Strict mode enabled but some controls failed: {failed_controls}")

    return status


def _set_python_random(seed: int, warn_on_failure: bool) -> dict[str, Any]:
    """Set Python built-in random seed."""
    try:
        random.seed(seed)
        # Verify it worked
        test_val = random.random()
        random.seed(seed)
        test_val2 = random.random()
        success = test_val == test_val2

        return {
            "success": success,
            "seed": seed,
            "library": "random",
            "test_reproducible": success,
        }
    except Exception as e:
        if warn_on_failure:
            logger.warning(f"Failed to set Python random seed: {e}")
        return {"success": False, "error": str(e)}


def _set_numpy_random(seed: int, warn_on_failure: bool) -> dict[str, Any]:
    """Set NumPy random seed."""
    try:
        np.random.seed(seed)
        # Also set the new Generator-based API
        rng = np.random.default_rng(seed)

        # Verify reproducibility
        np.random.seed(seed)
        test_val = np.random.random()
        np.random.seed(seed)
        test_val2 = np.random.random()
        success = test_val == test_val2

        return {
            "success": success,
            "seed": seed,
            "library": "numpy",
            "version": np.__version__,
            "test_reproducible": success,
        }
    except Exception as e:
        if warn_on_failure:
            logger.warning(f"Failed to set NumPy random seed: {e}")
        return {"success": False, "error": str(e)}


def _set_environment_determinism(strict: bool, warn_on_failure: bool) -> dict[str, Any]:
    """Set environment variables for deterministic behavior."""
    env_vars = {
        "PYTHONHASHSEED": "0",  # Deterministic hash seeds
        "CUBLAS_WORKSPACE_CONFIG": ":4096:8",  # CUDA determinism
    }

    if strict:
        env_vars.update(
            {
                "TF_DETERMINISTIC_OPS": "1",  # TensorFlow determinism
                "TF_CUDNN_DETERMINISTIC": "1",  # cuDNN determinism
            },
        )

    set_vars = {}
    failed_vars = {}

    for var, value in env_vars.items():
        try:
            old_value = os.environ.get(var)
            os.environ[var] = value
            set_vars[var] = {"old": old_value, "new": value}
        except Exception as e:
            failed_vars[var] = str(e)
            if warn_on_failure:
                logger.warning(f"Failed to set environment variable {var}: {e}")

    return {
        "success": len(failed_vars) == 0,
        "set_variables": set_vars,
        "failed_variables": failed_vars,
        "strict_mode": strict,
    }


def _set_thread_control(warn_on_failure: bool) -> dict[str, Any]:
    """Control thread counts for deterministic parallel processing."""
    thread_vars = {
        "OMP_NUM_THREADS": "1",
        "MKL_NUM_THREADS": "1",
        "OPENBLAS_NUM_THREADS": "1",
        "BLIS_NUM_THREADS": "1",
        "VECLIB_MAXIMUM_THREADS": "1",
    }

    set_vars = {}
    failed_vars = {}

    for var, value in thread_vars.items():
        try:
            old_value = os.environ.get(var)
            os.environ[var] = value
            set_vars[var] = {"old": old_value, "new": value}
        except Exception as e:
            failed_vars[var] = str(e)
            if warn_on_failure:
                logger.warning(f"Failed to set thread control variable {var}: {e}")

    return {
        "success": len(failed_vars) == 0,
        "set_variables": set_vars,
        "failed_variables": failed_vars,
        "note": "Single-threaded mode for determinism (may impact performance)",
    }


def _set_xgboost_determinism(seed: int, strict: bool, warn_on_failure: bool) -> dict[str, Any]:
    """Set XGBoost deterministic parameters."""
    try:
        import xgboost as xgb

        # XGBoost deterministic parameters
        deterministic_params = {
            "random_state": seed,
            "seed": seed,
            "nthread": 1,  # Single thread for determinism
        }

        if strict:
            deterministic_params.update(
                {
                    "deterministic_histogram": True,
                    "force_row_wise": True,  # Deterministic tree construction
                },
            )

        return {
            "success": True,
            "library": "xgboost",
            "version": xgb.__version__,
            "parameters": deterministic_params,
            "strict_mode": strict,
        }
    except ImportError:
        return {"success": True, "note": "XGBoost not available"}
    except Exception as e:
        if warn_on_failure:
            logger.warning(f"Failed to configure XGBoost determinism: {e}")
        return {"success": False, "error": str(e)}


def _set_lightgbm_determinism(seed: int, strict: bool, warn_on_failure: bool) -> dict[str, Any]:
    """Set LightGBM deterministic parameters."""
    try:
        import lightgbm as lgb

        # LightGBM deterministic parameters
        deterministic_params = {
            "random_state": seed,
            "seed": seed,
            "num_threads": 1,  # Single thread for determinism
            "deterministic": True,
        }

        if strict:
            deterministic_params.update(
                {
                    "force_row_wise": True,
                    "histogram_pool_size": -1,  # Disable histogram pool for determinism
                },
            )

        return {
            "success": True,
            "library": "lightgbm",
            "version": lgb.__version__,
            "parameters": deterministic_params,
            "strict_mode": strict,
        }
    except ImportError:
        return {"success": True, "note": "LightGBM not available"}
    except Exception as e:
        if warn_on_failure:
            logger.warning(f"Failed to configure LightGBM determinism: {e}")
        return {"success": False, "error": str(e)}


def _set_sklearn_determinism(warn_on_failure: bool) -> dict[str, Any]:
    """Set scikit-learn deterministic behavior."""
    try:
        import sklearn

        # Scikit-learn uses random_state parameters in estimators
        # We can't set global defaults, but we can verify the library is available
        return {
            "success": True,
            "library": "sklearn",
            "version": sklearn.__version__,
            "note": "Use random_state parameter in estimators",
        }
    except ImportError:
        return {"success": True, "note": "Scikit-learn not available"}
    except Exception as e:
        if warn_on_failure:
            logger.warning(f"Failed to configure scikit-learn determinism: {e}")
        return {"success": False, "error": str(e)}


def _set_shap_determinism(seed: int, warn_on_failure: bool) -> dict[str, Any]:
    """Set SHAP deterministic behavior."""
    try:
        # Lazy import to avoid NumPy 2.x compatibility issues during module load
        import shap

        # SHAP uses numpy random, which we've already set
        # Some SHAP explainers have their own random_state parameters
        return {
            "success": True,
            "library": "shap",
            "version": shap.__version__,
            "seed": seed,
            "note": "Uses NumPy random (already configured)",
        }
    except ImportError:
        return {"success": True, "note": "SHAP not available"}
    except (TypeError, AttributeError) as e:
        # TypeError can occur with NumPy 2.x compatibility issues during SHAP import
        if warn_on_failure:
            logger.warning(f"Failed to configure SHAP determinism (likely NumPy 2.x compatibility): {e}")
        return {"success": False, "error": str(e), "note": "Try: pip install 'shap==0.48.0'"}
    except Exception as e:
        if warn_on_failure:
            logger.warning(f"Failed to configure SHAP determinism: {e}")
        return {"success": False, "error": str(e)}


def _set_pandas_determinism(warn_on_failure: bool) -> dict[str, Any]:
    """Set pandas deterministic behavior."""
    try:
        import pandas as pd

        # Pandas operations are generally deterministic
        # But we can disable some optimizations that might introduce non-determinism
        return {
            "success": True,
            "library": "pandas",
            "version": pd.__version__,
            "note": "Generally deterministic by default",
        }
    except ImportError:
        return {"success": True, "note": "Pandas not available"}
    except Exception as e:
        if warn_on_failure:
            logger.warning(f"Failed to configure pandas determinism: {e}")
        return {"success": False, "error": str(e)}


def _set_system_determinism(strict: bool, warn_on_failure: bool) -> dict[str, Any]:
    """Set system-level deterministic behavior."""
    controls = {}

    # Disable Python warnings that might affect output
    if strict:
        try:
            warnings.filterwarnings("ignore")
            controls["warnings_disabled"] = True
        except Exception as e:
            controls["warnings_disabled"] = False
            if warn_on_failure:
                logger.warning(f"Failed to disable warnings: {e}")

    # Set locale for consistent string operations
    try:
        import locale

        locale.setlocale(locale.LC_ALL, "C")
        controls["locale_set"] = True
    except Exception as e:
        controls["locale_set"] = False
        if warn_on_failure:
            logger.warning(f"Failed to set C locale: {e}")

    success = all(controls.values()) if controls else True

    return {
        "success": success,
        "controls": controls,
        "strict_mode": strict,
    }


def get_repro_status() -> dict[str, Any]:
    """Get current reproducibility status without changing settings.

    Returns:
        Dictionary with current reproducibility configuration

    """
    status = {
        "environment_variables": {},
        "library_versions": {},
        "random_states": {},
    }

    # Check environment variables
    repro_vars = [
        "PYTHONHASHSEED",
        "OMP_NUM_THREADS",
        "MKL_NUM_THREADS",
        "OPENBLAS_NUM_THREADS",
        "TF_DETERMINISTIC_OPS",
        "CUBLAS_WORKSPACE_CONFIG",
    ]

    for var in repro_vars:
        status["environment_variables"][var] = os.environ.get(var, "not_set")

    # Check library versions
    libraries = ["numpy", "pandas", "sklearn", "xgboost", "lightgbm", "shap"]
    for lib in libraries:
        try:
            if lib == "shap":
                # Special handling for SHAP to avoid NumPy 2.x compatibility issues
                try:
                    module = __import__(lib)
                    status["library_versions"][lib] = getattr(module, "__version__", "unknown")
                except (ImportError, TypeError) as e:
                    # TypeError can occur with NumPy 2.x compatibility issues
                    status["library_versions"][lib] = f"import_error: {e}"
            else:
                module = __import__(lib)
                status["library_versions"][lib] = getattr(module, "__version__", "unknown")
        except ImportError:
            status["library_versions"][lib] = "not_available"

    # Test random state reproducibility
    try:
        # Python random
        random.seed(42)
        val1 = random.random()
        random.seed(42)
        val2 = random.random()
        status["random_states"]["python_random"] = val1 == val2

        # NumPy random
        np.random.seed(42)
        val1 = np.random.random()
        np.random.seed(42)
        val2 = np.random.random()
        status["random_states"]["numpy_random"] = val1 == val2

    except Exception as e:
        status["random_states"]["error"] = str(e)

    return status


def validate_repro(expected_seed: int = 42) -> dict[str, Any]:
    """Validate that reproducibility controls are working correctly.

    Args:
        expected_seed: Expected seed value for validation

    Returns:
        Dictionary with validation results

    """
    logger.info("Validating reproducibility controls")

    validation = {
        "seed": expected_seed,
        "tests": {},
        "overall_success": True,
    }

    # Test 1: Python random reproducibility
    try:
        random.seed(expected_seed)
        vals1 = [random.random() for _ in range(10)]
        random.seed(expected_seed)
        vals2 = [random.random() for _ in range(10)]

        validation["tests"]["python_random"] = {
            "success": vals1 == vals2,
            "values_match": vals1 == vals2,
        }
    except Exception as e:
        validation["tests"]["python_random"] = {"success": False, "error": str(e)}

    # Test 2: NumPy random reproducibility
    try:
        np.random.seed(expected_seed)
        vals1 = np.random.random(10).tolist()
        np.random.seed(expected_seed)
        vals2 = np.random.random(10).tolist()

        validation["tests"]["numpy_random"] = {
            "success": vals1 == vals2,
            "values_match": vals1 == vals2,
        }
    except Exception as e:
        validation["tests"]["numpy_random"] = {"success": False, "error": str(e)}

    # Test 3: Environment variables
    required_vars = ["PYTHONHASHSEED", "OMP_NUM_THREADS"]
    env_status = {}
    for var in required_vars:
        env_status[var] = os.environ.get(var, "not_set")

    validation["tests"]["environment"] = {
        "success": all(val != "not_set" for val in env_status.values()),
        "variables": env_status,
    }

    # Overall success
    validation["overall_success"] = all(test.get("success", False) for test in validation["tests"].values())

    if validation["overall_success"]:
        logger.info("Reproducibility validation passed")
    else:
        failed_tests = [name for name, test in validation["tests"].items() if not test.get("success", False)]
        logger.warning(f"Reproducibility validation failed for: {failed_tests}")

    return validation


def reset_repro() -> dict[str, Any]:
    """Reset reproducibility controls to default/random state.

    Returns:
        Dictionary with reset status

    """
    logger.info("Resetting reproducibility controls")

    reset_status = {
        "environment_variables": {},
        "random_seeds": {},
    }

    # Reset environment variables (remove deterministic settings)
    repro_vars = [
        "PYTHONHASHSEED",
        "OMP_NUM_THREADS",
        "MKL_NUM_THREADS",
        "OPENBLAS_NUM_THREADS",
        "TF_DETERMINISTIC_OPS",
        "CUBLAS_WORKSPACE_CONFIG",
    ]

    for var in repro_vars:
        old_value = os.environ.get(var)
        if old_value is not None:
            try:
                del os.environ[var]
                reset_status["environment_variables"][var] = f"removed (was: {old_value})"
            except Exception as e:
                reset_status["environment_variables"][var] = f"failed to remove: {e}"
        else:
            reset_status["environment_variables"][var] = "not_set"

    # Reset random seeds to random values
    try:
        import time

        new_seed = int(time.time() * 1000000) % 2**32

        random.seed(new_seed)
        np.random.seed(new_seed)

        reset_status["random_seeds"] = {
            "python_random": new_seed,
            "numpy_random": new_seed,
        }
    except Exception as e:
        reset_status["random_seeds"] = {"error": str(e)}

    logger.info("Reproducibility controls reset")
    return reset_status
