"""
Variable Substitution - Substitute variables in configuration values

Supports:
- {env:VAR} - Environment variable substitution
- {env:VAR:default} - With default value
- {file:path} - File content substitution
- {{literal}} - Escaped braces (becomes {literal})

Examples:
    sub = ConfigVariableSubstitutor()
    result = sub.substitute({"api_key": "{env:OPENAI_API_KEY}"})
    # result = {"api_key": "sk-..."}

    result = sub.substitute({"prompt": "{file:prompts/system.txt}"})
    # result = {"prompt": "You are a helpful assistant..."}
"""

import os
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Optional


@dataclass
class SubstitutionResult:
    """
    Result of variable substitution

    Attributes:
        config: The config with variables substituted
        success: Whether all substitutions succeeded
        substitutions: List of substitutions made
        warnings: List of warning messages
        errors: List of error messages
    """

    config: dict[str, Any] = field(default_factory=dict)
    success: bool = True
    substitutions: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)
    errors: list[str] = field(default_factory=list)


class ConfigVariableSubstitutor:
    """
    Substitute variables in configuration values

    Supports:
    - {env:VAR} - Environment variable
    - {env:VAR:default} - Environment variable with default
    - {file:path} - File contents
    - {{literal}} - Escaped braces

    Example:
        sub = ConfigVariableSubstitutor()

        config = {
            "api_key": "{env:API_KEY}",
            "prompt": "{file:prompt.txt}",
            "literal": "{{not_a_var}}"
        }

        result = sub.substitute(config)
        # result.config = {
        #     "api_key": "actual_api_key",
        #     "prompt": "contents of prompt.txt",
        #     "literal": "{not_a_var}"
        # }
    """

    # Pattern to match {env:VAR} or {env:VAR:default}
    ENV_PATTERN = re.compile(r"\{env:([A-Za-z_][A-Za-z0-9_]*)(?::([^}]*))?\}")

    # Pattern to match {file:path}
    FILE_PATTERN = re.compile(r"\{file:([^}]+)\}")

    # Pattern to match escaped braces {{...}}
    ESCAPE_PATTERN = re.compile(r"\{\{([^}]*)\}\}")

    def __init__(
        self,
        base_path: Optional[Path] = None,
        env: Optional[dict[str, str]] = None,
        strict: bool = False,
        max_recursion: int = 10,
    ):
        """
        Initialize ConfigVariableSubstitutor

        Args:
            base_path: Base path for relative file references
            env: Environment dict (defaults to os.environ)
            strict: If True, raise errors for missing variables
            max_recursion: Maximum recursion depth for nested substitutions
        """
        self.base_path = base_path or Path.cwd()
        self._env = env if env is not None else dict(os.environ)
        self.strict = strict
        self.max_recursion = max_recursion

    def substitute(self, config: dict[str, Any]) -> SubstitutionResult:
        """
        Substitute variables in a configuration dict

        Args:
            config: Configuration dict with variable placeholders

        Returns:
            SubstitutionResult with substituted config
        """
        substitutions: list[str] = []
        warnings: list[str] = []
        errors: list[str] = []

        try:
            result_config = self._substitute_value(config, substitutions, warnings, errors, depth=0)
        except Exception as e:
            return SubstitutionResult(
                config=config,
                success=False,
                errors=[f"Substitution failed: {e}"],
            )

        success = len(errors) == 0

        return SubstitutionResult(
            config=result_config,
            success=success,
            substitutions=substitutions,
            warnings=warnings,
            errors=errors,
        )

    def _substitute_value(
        self,
        value: Any,
        substitutions: list[str],
        warnings: list[str],
        errors: list[str],
        depth: int,
    ) -> Any:
        """
        Recursively substitute variables in a value

        Args:
            value: Value to process
            substitutions: List to append substitutions to
            warnings: List to append warnings to
            errors: List to append errors to
            depth: Current recursion depth

        Returns:
            Value with variables substituted
        """
        if depth > self.max_recursion:
            errors.append(f"Maximum recursion depth ({self.max_recursion}) exceeded")
            return value

        if isinstance(value, dict):
            return {
                k: self._substitute_value(v, substitutions, warnings, errors, depth + 1)
                for k, v in value.items()
            }
        elif isinstance(value, list):
            return [
                self._substitute_value(item, substitutions, warnings, errors, depth + 1)
                for item in value
            ]
        elif isinstance(value, str):
            return self._substitute_string(value, substitutions, warnings, errors, depth)
        else:
            return value

    def _substitute_string(
        self,
        value: str,
        substitutions: list[str],
        warnings: list[str],
        errors: list[str],
        depth: int,
    ) -> str:
        """
        Substitute variables in a string value

        Args:
            value: String value to process
            substitutions: List to append substitutions to
            warnings: List to append warnings to
            errors: List to append errors to
            depth: Current recursion depth

        Returns:
            String with variables substituted
        """
        result = value

        # Step 1: Replace escaped braces {{...}} with placeholder
        placeholder_open = "\x00OPEN\x00"
        placeholder_close = "\x00CLOSE\x00"

        def replace_escape(match: re.Match) -> str:
            inner = match.group(1)
            return f"{placeholder_open}{inner}{placeholder_close}"

        result = self.ESCAPE_PATTERN.sub(replace_escape, result)

        # Track if we made any substitutions this pass
        made_substitution = False

        # Step 2: Substitute environment variables
        def replace_env(match: re.Match) -> str:
            nonlocal made_substitution
            var_name = match.group(1)
            default = match.group(2)

            if var_name in self._env:
                env_value = self._env[var_name]
                substitutions.append(f"{{env:{var_name}}} -> [value]")
                made_substitution = True
                return env_value
            elif default is not None:
                substitutions.append(f"{{env:{var_name}}} -> {default} (default)")
                made_substitution = True
                return default
            else:
                msg = f"Environment variable not found: {var_name}"
                if self.strict:
                    errors.append(msg)
                    return match.group(0)  # Keep original
                else:
                    warnings.append(msg)
                    return ""  # Replace with empty string

        result = self.ENV_PATTERN.sub(replace_env, result)

        # Step 3: Substitute file contents
        def replace_file(match: re.Match) -> str:
            nonlocal made_substitution
            file_path = match.group(1).strip()

            # Resolve path
            path = Path(file_path)
            if not path.is_absolute():
                path = self.base_path / path

            if not path.exists():
                msg = f"File not found: {file_path}"
                if self.strict:
                    errors.append(msg)
                    return match.group(0)
                else:
                    warnings.append(msg)
                    return ""

            try:
                content = path.read_text(encoding="utf-8").strip()
                substitutions.append(f"{{file:{file_path}}} -> [file content]")
                made_substitution = True
                return content
            except Exception as e:
                msg = f"Error reading file {file_path}: {e}"
                if self.strict:
                    errors.append(msg)
                    return match.group(0)
                else:
                    warnings.append(msg)
                    return ""

        result = self.FILE_PATTERN.sub(replace_file, result)

        # Step 4: Check for recursive substitution (only if we made progress)
        # This must happen BEFORE restoring escaped braces, otherwise escaped
        # variables like {{env:VAR}} would be treated as real variables after restoration
        if made_substitution and depth < self.max_recursion:
            if self.ENV_PATTERN.search(result) or self.FILE_PATTERN.search(result):
                result = self._substitute_string(result, substitutions, warnings, errors, depth + 1)

        # Step 5: Restore escaped braces {{...}} -> {...}
        result = result.replace(placeholder_open, "{").replace(placeholder_close, "}")

        return result

    def substitute_string(self, value: str) -> str:
        """
        Substitute variables in a single string

        Args:
            value: String with variable placeholders

        Returns:
            String with variables substituted
        """
        substitutions: list[str] = []
        warnings: list[str] = []
        errors: list[str] = []

        return self._substitute_string(value, substitutions, warnings, errors, depth=0)

    def has_variables(self, value: Any) -> bool:
        """
        Check if a value contains any variable placeholders

        Args:
            value: Value to check

        Returns:
            True if value contains variables
        """
        if isinstance(value, str):
            if self.ENV_PATTERN.search(value):
                return True
            if self.FILE_PATTERN.search(value):
                return True
            return False
        elif isinstance(value, dict):
            return any(self.has_variables(v) for v in value.values())
        elif isinstance(value, list):
            return any(self.has_variables(item) for item in value)
        return False

    def find_variables(self, config: dict[str, Any]) -> list[str]:
        """
        Find all variable references in a config

        Args:
            config: Configuration dict

        Returns:
            List of variable references found
        """
        variables: list[str] = []
        self._find_variables_recursive(config, variables)
        return variables

    def _find_variables_recursive(self, value: Any, variables: list[str]) -> None:
        """Recursively find variables in a value"""
        if isinstance(value, str):
            for match in self.ENV_PATTERN.finditer(value):
                var_name = match.group(1)
                default = match.group(2)
                if default:
                    variables.append(f"env:{var_name}:{default}")
                else:
                    variables.append(f"env:{var_name}")

            for match in self.FILE_PATTERN.finditer(value):
                variables.append(f"file:{match.group(1)}")

        elif isinstance(value, dict):
            for v in value.values():
                self._find_variables_recursive(v, variables)

        elif isinstance(value, list):
            for item in value:
                self._find_variables_recursive(item, variables)

    def validate_variables(self, config: dict[str, Any]) -> list[str]:
        """
        Validate that all variables can be resolved

        Args:
            config: Configuration dict

        Returns:
            List of validation error messages (empty if valid)
        """
        issues: list[str] = []
        variables = self.find_variables(config)

        for var in variables:
            if var.startswith("env:"):
                parts = var[4:].split(":", 1)
                var_name = parts[0]
                has_default = len(parts) > 1

                if var_name not in self._env and not has_default:
                    issues.append(f"Missing environment variable: {var_name}")

            elif var.startswith("file:"):
                file_path = var[5:]
                path = Path(file_path)
                if not path.is_absolute():
                    path = self.base_path / path

                if not path.exists():
                    issues.append(f"File not found: {file_path}")

        return issues


# Convenience functions


def substitute_variables(
    config: dict[str, Any],
    base_path: Optional[Path] = None,
    env: Optional[dict[str, str]] = None,
    strict: bool = False,
) -> dict[str, Any]:
    """
    Substitute variables in config (convenience function)

    Args:
        config: Configuration dict with variable placeholders
        base_path: Base path for relative file references
        env: Environment dict
        strict: If True, raise on missing variables

    Returns:
        Configuration dict with variables substituted
    """
    sub = ConfigVariableSubstitutor(base_path=base_path, env=env, strict=strict)
    result = sub.substitute(config)

    if strict and not result.success:
        raise ValueError("; ".join(result.errors))

    return result.config


def substitute_variables_with_result(
    config: dict[str, Any],
    base_path: Optional[Path] = None,
    env: Optional[dict[str, str]] = None,
    strict: bool = False,
) -> SubstitutionResult:
    """
    Substitute variables and return detailed result (convenience function)

    Args:
        config: Configuration dict with variable placeholders
        base_path: Base path for relative file references
        env: Environment dict
        strict: If True, keep original on missing variables

    Returns:
        SubstitutionResult with config and metadata
    """
    sub = ConfigVariableSubstitutor(base_path=base_path, env=env, strict=strict)
    return sub.substitute(config)


def substitute_string(
    value: str,
    base_path: Optional[Path] = None,
    env: Optional[dict[str, str]] = None,
) -> str:
    """
    Substitute variables in a single string (convenience function)

    Args:
        value: String with variable placeholders
        base_path: Base path for relative file references
        env: Environment dict

    Returns:
        String with variables substituted
    """
    sub = ConfigVariableSubstitutor(base_path=base_path, env=env)
    return sub.substitute_string(value)
