"""
Configuration file validation for security and correctness.

Validates user-provided configuration files before loading to prevent:
- Path traversal attacks
- DoS via large files
- Malformed JSON
- Invalid configuration values
"""

import os
import json
from typing import Dict, List, Optional, Tuple


MAX_CONFIG_FILE_SIZE = 1_000_000  # 1MB


def validate_config_path(path: str) -> Tuple[bool, Optional[str]]:
    """Validate configuration file path is safe.

    Args:
        path: File path to validate.

    Returns:
        Tuple of (is_valid, error_message).
    """
    # Must be provided
    if not path:
        return False, "Configuration path is empty"

    # Must be absolute path
    if not os.path.isabs(path):
        return False, f"Configuration path must be absolute: {path}"

    # Must be .json file
    if not path.endswith('.json'):
        return False, f"Configuration file must be .json: {path}"

    # Resolve symlinks and check it exists
    try:
        real_path = os.path.realpath(path)
    except (OSError, ValueError) as e:
        return False, f"Invalid path: {e}"

    # Must exist and be a file
    if not os.path.exists(real_path):
        return False, f"Configuration file does not exist: {path}"

    if not os.path.isfile(real_path):
        return False, f"Configuration path is not a file: {path}"

    # Check file size (prevent DoS)
    try:
        file_size = os.path.getsize(real_path)
        if file_size > MAX_CONFIG_FILE_SIZE:
            return False, f"Configuration file too large: {file_size} bytes (max {MAX_CONFIG_FILE_SIZE})"
    except OSError as e:
        return False, f"Cannot read file size: {e}"

    return True, None


def load_json_safely(path: str) -> Tuple[Optional[Dict], Optional[str]]:
    """Load JSON file with size and content validation.

    Args:
        path: Validated file path.

    Returns:
        Tuple of (config_dict, error_message).
    """
    # Validate path first
    is_valid, error = validate_config_path(path)
    if not is_valid:
        return None, error

    # Read file with size limit enforced
    try:
        with open(path, 'r', encoding='utf-8') as f:
            # Read with size limit
            content = f.read(MAX_CONFIG_FILE_SIZE + 1)
            if len(content) > MAX_CONFIG_FILE_SIZE:
                return None, "Configuration file content exceeds size limit"

            # Parse JSON
            config = json.loads(content)

            # Basic type check
            if not isinstance(config, dict):
                return None, "Configuration must be a JSON object"

            return config, None

    except json.JSONDecodeError as e:
        return None, f"Invalid JSON: {e}"
    except UnicodeDecodeError as e:
        return None, f"File encoding error: {e}"
    except OSError as e:
        return None, f"Cannot read file: {e}"
    except Exception as e:
        # Catch-all for unexpected errors (but don't expose details)
        return None, "Failed to load configuration file"


def validate_property_mapping_config(config: Dict) -> Tuple[bool, List[str]]:
    """Validate property mapping configuration structure.

    Args:
        config: Configuration dictionary.

    Returns:
        Tuple of (is_valid, error_messages).
    """
    errors = []

    # Allowed top-level keys
    allowed_keys = {
        "industry", "annual_revenue", "employee_count",
        "geography_state", "geography_country",
        "tech_stack", "tech_stack_delimiter",
        "growth_signals", "growth_signal_keywords",
        "content_engagement", "content_engagement_thresholds",
        "purchase_history_deal_count_threshold",
        "decision_maker_access", "budget_authority", "strategic_alignment"
    }

    # Check for unexpected keys
    unexpected = set(config.keys()) - allowed_keys
    if unexpected:
        errors.append(f"Unexpected configuration keys: {', '.join(unexpected)}")

    # Validate types
    if "tech_stack" in config and config["tech_stack"] is not None:
        if not isinstance(config["tech_stack"], str):
            errors.append("tech_stack must be a string")

    if "tech_stack_delimiter" in config:
        if not isinstance(config["tech_stack_delimiter"], str):
            errors.append("tech_stack_delimiter must be a string")
        elif len(config["tech_stack_delimiter"]) != 1:
            errors.append("tech_stack_delimiter must be a single character")

    if "growth_signals" in config and config["growth_signals"] is not None:
        if not isinstance(config["growth_signals"], list):
            errors.append("growth_signals must be an array")
        elif len(config["growth_signals"]) > 50:
            errors.append("growth_signals cannot have more than 50 properties")
        elif not all(isinstance(s, str) for s in config["growth_signals"]):
            errors.append("growth_signals must contain only strings")

    if "growth_signal_keywords" in config:
        if not isinstance(config["growth_signal_keywords"], dict):
            errors.append("growth_signal_keywords must be an object")
        elif len(config["growth_signal_keywords"]) > 100:
            errors.append("growth_signal_keywords cannot have more than 100 entries")

    if "content_engagement_thresholds" in config:
        if not isinstance(config["content_engagement_thresholds"], dict):
            errors.append("content_engagement_thresholds must be an object")
        else:
            required = {"active", "occasional"}
            if not required.issubset(config["content_engagement_thresholds"].keys()):
                errors.append("content_engagement_thresholds must have 'active' and 'occasional' keys")

    return len(errors) == 0, errors


def validate_rfm_thresholds_config(config: Dict) -> Tuple[bool, List[str]]:
    """Validate RFM thresholds configuration structure.

    Args:
        config: Configuration dictionary.

    Returns:
        Tuple of (is_valid, error_messages).
    """
    errors = []

    # Allowed top-level keys
    allowed_keys = {
        "recency_days", "recency_scores",
        "frequency_counts", "frequency_scores",
        "monetary_method", "monetary_percentiles",
        "monetary_fixed_thresholds", "monetary_scores"
    }

    # Check for unexpected keys
    unexpected = set(config.keys()) - allowed_keys
    if unexpected:
        errors.append(f"Unexpected configuration keys: {', '.join(unexpected)}")

    # Validate recency
    if "recency_days" in config:
        if not isinstance(config["recency_days"], list):
            errors.append("recency_days must be an array")
        elif not all(isinstance(d, (int, float)) and d > 0 for d in config["recency_days"]):
            errors.append("recency_days must contain positive numbers")
        elif len(config["recency_days"]) > 10:
            errors.append("recency_days cannot have more than 10 thresholds")
        elif config["recency_days"] != sorted(config["recency_days"]):
            errors.append("recency_days must be in ascending order")

    if "recency_scores" in config:
        if not isinstance(config["recency_scores"], list):
            errors.append("recency_scores must be an array")
        elif not all(isinstance(s, int) and 1 <= s <= 5 for s in config["recency_scores"]):
            errors.append("recency_scores must contain integers between 1 and 5")

    # Validate frequency
    if "frequency_counts" in config:
        if not isinstance(config["frequency_counts"], list):
            errors.append("frequency_counts must be an array")
        elif not all(isinstance(c, int) and c >= 0 for c in config["frequency_counts"]):
            errors.append("frequency_counts must contain non-negative integers")
        elif len(config["frequency_counts"]) > 10:
            errors.append("frequency_counts cannot have more than 10 thresholds")
        elif config["frequency_counts"] != sorted(config["frequency_counts"], reverse=True):
            errors.append("frequency_counts must be in descending order")

    if "frequency_scores" in config:
        if not isinstance(config["frequency_scores"], list):
            errors.append("frequency_scores must be an array")
        elif not all(isinstance(s, int) and 1 <= s <= 5 for s in config["frequency_scores"]):
            errors.append("frequency_scores must contain integers between 1 and 5")

    # Validate monetary method
    if "monetary_method" in config:
        if config["monetary_method"] not in ("percentile", "fixed"):
            errors.append("monetary_method must be 'percentile' or 'fixed'")

    # Validate array length consistency
    if "recency_days" in config and "recency_scores" in config:
        if len(config["recency_scores"]) != len(config["recency_days"]) + 1:
            errors.append("recency_scores must have one more element than recency_days")

    if "frequency_counts" in config and "frequency_scores" in config:
        if len(config["frequency_scores"]) != len(config["frequency_counts"]) + 1:
            errors.append("frequency_scores must have one more element than frequency_counts")

    return len(errors) == 0, errors


def sanitize_error_message(error: Exception) -> str:
    """Sanitize error message to prevent information disclosure.

    Args:
        error: Exception to sanitize.

    Returns:
        Safe error message.
    """
    # Don't expose file paths, stack traces, or internal details
    error_str = str(error)

    # Remove file paths
    import re
    error_str = re.sub(r'/[^\s]+', '[PATH]', error_str)
    error_str = re.sub(r'[A-Z]:\\[^\s]+', '[PATH]', error_str)

    # Truncate if too long
    if len(error_str) > 200:
        error_str = error_str[:200] + "..."

    return error_str
