"""
Argument Substitution - Substitute variables in command templates

Supports:
- $ARGUMENTS - All arguments as a single string
- $1, $2, $3, etc. - Individual positional arguments
- ${VAR} - Environment variables
- $$ - Escaped dollar sign

Examples:
    substitute("Run tests in $1 module", "auth")
    → "Run tests in auth module"

    substitute("Deploy $ARGUMENTS to prod", "api --env=staging")
    → "Deploy api --env=staging to prod"

    substitute("User is ${USER}", "")
    → "User is johndoe"
"""

import os
import re
import shlex
from dataclasses import dataclass
from typing import Optional


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

    Attributes:
        output: The substituted template string
        success: Whether all required substitutions were made
        warnings: List of warning messages (e.g., missing args)
        missing_args: List of missing positional arguments
        missing_vars: List of missing environment variables
    """

    output: str
    success: bool = True
    warnings: list[str] = None  # type: ignore
    missing_args: list[str] = None  # type: ignore
    missing_vars: list[str] = None  # type: ignore

    def __post_init__(self):
        if self.warnings is None:
            self.warnings = []
        if self.missing_args is None:
            self.missing_args = []
        if self.missing_vars is None:
            self.missing_vars = []


class ArgumentSubstitutor:
    """
    Substitute variables in command templates

    Supports:
    - $ARGUMENTS - All arguments as a single string
    - $1, $2, $3, etc. - Individual positional arguments (1-indexed)
    - ${VAR} - Environment variables
    - $$ - Escaped dollar sign (becomes literal $)

    Example:
        sub = ArgumentSubstitutor()
        result = sub.substitute("Run $1 tests", "unit integration")
        print(result.output)  # "Run unit tests"
    """

    # Regex patterns for different substitution types
    ESCAPED_DOLLAR = re.compile(r"\$\$")
    ENV_VAR = re.compile(r"\$\{([A-Z_][A-Z0-9_]*)\}", re.IGNORECASE)
    POSITIONAL_ARG = re.compile(r"\$([0-9]+)")
    ALL_ARGS = re.compile(r"\$ARGUMENTS\b", re.IGNORECASE)

    def __init__(self, env: Optional[dict[str, str]] = None):
        """
        Initialize ArgumentSubstitutor

        Args:
            env: Optional environment dict (defaults to os.environ)
        """
        self._env = env if env is not None else dict(os.environ)

    def substitute(
        self,
        template: str,
        args: str = "",
        strict: bool = False,
    ) -> SubstitutionResult:
        """
        Substitute variables in a template

        Args:
            template: Template string with variables
            args: Arguments string (space-separated)
            strict: If True, fail on missing variables

        Returns:
            SubstitutionResult with output and status

        Example:
            result = sub.substitute("Test $1 module", "auth")
            print(result.output)  # "Test auth module"
        """
        # Parse arguments
        arg_list = self._parse_args(args)

        # Track warnings and missing items
        warnings: list[str] = []
        missing_args: list[str] = []
        missing_vars: list[str] = []

        # Start with the template
        output = template

        # Step 1: Replace escaped dollars with placeholder
        placeholder = "\x00DOLLAR\x00"
        output = self.ESCAPED_DOLLAR.sub(placeholder, output)

        # Step 2: Replace $ARGUMENTS with all args
        output = self.ALL_ARGS.sub(args, output)

        # Step 3: Replace positional arguments $1, $2, etc.
        def replace_positional(match: re.Match) -> str:
            index = int(match.group(1))
            if index == 0:
                # $0 is conventionally the command name, but we don't have that
                # so treat it as $ARGUMENTS
                return args
            if index <= len(arg_list):
                return arg_list[index - 1]
            else:
                missing_args.append(f"${index}")
                warnings.append(f"Missing argument ${index}")
                if strict:
                    return match.group(0)  # Keep original
                return ""  # Replace with empty string

        output = self.POSITIONAL_ARG.sub(replace_positional, output)

        # Step 4: Replace environment variables ${VAR}
        def replace_env_var(match: re.Match) -> str:
            var_name = match.group(1)
            if var_name in self._env:
                return self._env[var_name]
            else:
                missing_vars.append(var_name)
                warnings.append(f"Missing environment variable ${{{var_name}}}")
                if strict:
                    return match.group(0)  # Keep original
                return ""  # Replace with empty string

        output = self.ENV_VAR.sub(replace_env_var, output)

        # Step 5: Restore escaped dollars
        output = output.replace(placeholder, "$")

        # Determine success
        success = len(missing_args) == 0 and len(missing_vars) == 0

        return SubstitutionResult(
            output=output,
            success=success,
            warnings=warnings,
            missing_args=missing_args,
            missing_vars=missing_vars,
        )

    def _parse_args(self, args: str) -> list[str]:
        """
        Parse arguments string into list

        Uses shell-like parsing to handle quoted strings.

        Args:
            args: Arguments string

        Returns:
            List of argument strings
        """
        if not args or not args.strip():
            return []

        try:
            # Use shlex for shell-like parsing (handles quotes)
            return shlex.split(args)
        except ValueError:
            # If shlex fails (unmatched quotes), fall back to simple split
            return args.split()

    def get_required_args(self, template: str) -> list[int]:
        """
        Get list of required positional argument indices

        Args:
            template: Template string

        Returns:
            Sorted list of required argument indices (1-indexed)

        Example:
            indices = sub.get_required_args("Test $1 and $3")
            # Returns: [1, 3]
        """
        indices = set()
        for match in self.POSITIONAL_ARG.finditer(template):
            idx = int(match.group(1))
            if idx > 0:  # Skip $0
                indices.add(idx)
        return sorted(indices)

    def get_required_env_vars(self, template: str) -> list[str]:
        """
        Get list of required environment variables

        Args:
            template: Template string

        Returns:
            List of required environment variable names

        Example:
            vars = sub.get_required_env_vars("User ${USER} at ${HOST}")
            # Returns: ["USER", "HOST"]
        """
        return [match.group(1) for match in self.ENV_VAR.finditer(template)]

    def has_substitutions(self, template: str) -> bool:
        """
        Check if template contains any substitution markers

        Args:
            template: Template string

        Returns:
            True if template has substitutions
        """
        # Check for $ARGUMENTS, $1, $2, etc., or ${VAR}
        if self.ALL_ARGS.search(template):
            return True
        if self.POSITIONAL_ARG.search(template):
            return True
        if self.ENV_VAR.search(template):
            return True
        return False


# Convenience function for quick substitution
def substitute(
    template: str,
    args: str = "",
    env: Optional[dict[str, str]] = None,
    strict: bool = False,
) -> str:
    """
    Substitute variables in a template (convenience function)

    Args:
        template: Template string with variables
        args: Arguments string (space-separated)
        env: Optional environment dict
        strict: If True, keep unresolved variables

    Returns:
        Substituted string

    Example:
        result = substitute("Run $1 tests", "unit")
        # Returns: "Run unit tests"
    """
    sub = ArgumentSubstitutor(env=env)
    result = sub.substitute(template, args, strict=strict)
    return result.output


def substitute_with_result(
    template: str,
    args: str = "",
    env: Optional[dict[str, str]] = None,
    strict: bool = False,
) -> SubstitutionResult:
    """
    Substitute variables and return full result (convenience function)

    Args:
        template: Template string with variables
        args: Arguments string (space-separated)
        env: Optional environment dict
        strict: If True, keep unresolved variables

    Returns:
        SubstitutionResult with output and status
    """
    sub = ArgumentSubstitutor(env=env)
    return sub.substitute(template, args, strict=strict)
