"""System prompt dynamic command renderer.

Processes <trender> tags in system prompts:
- <trender>command</trender> - executes shell commands
- <trender type="include" path="sections/file.md" /> - includes file content
"""

import logging
import re
import subprocess
from pathlib import Path
from typing import Dict, List, Optional, Tuple

logger = logging.getLogger(__name__)


class PromptRenderer:
    """Renders dynamic content in system prompts by executing commands and including files."""

    # Pattern to match <trender>command</trender> tags (shell commands)
    # Excludes matches that are inside backticks (code examples)
    TRENDER_COMMAND_PATTERN = re.compile(r'(?<!`)<trender>(.*?)</trender>(?!`)', re.DOTALL)

    # Pattern to match <trender type="include" path="..." /> tags (file includes)
    # Supports both single and double quotes, optional trailing slash
    TRENDER_INCLUDE_PATTERN = re.compile(
        r'<trender\s+type=["\']include["\']\s+path=["\']([^"\']+)["\']\s*/?>',
        re.DOTALL
    )

    def __init__(self, timeout: int = 5, base_path: Optional[Path] = None):
        """Initialize the prompt renderer.

        Args:
            timeout: Maximum seconds to wait for each command execution.
            base_path: Base directory for resolving relative file paths.
                      If None, uses current working directory.
        """
        self.timeout = timeout
        self._command_cache: Dict[str, str] = {}
        self._file_cache: Dict[str, str] = {}
        self.base_path = base_path or Path.cwd()

    def render(self, prompt_content: str) -> str:
        """Render all <trender> tags in the prompt content.

        Processes both shell commands and file includes in order of appearance.

        Args:
            prompt_content: System prompt content with <trender> tags.

        Returns:
            Processed prompt with tags replaced by their output/content.
        """
        if not prompt_content:
            return prompt_content

        result = prompt_content
        iteration = 0
        max_iterations = 100  # Prevent infinite loops from circular includes

        # Process includes and commands until no more tags remain
        while iteration < max_iterations:
            iteration += 1

            # Step 1: Process file includes first (allows includes to contain commands)
            result = self._process_includes(result)

            # Step 2: Process shell commands
            result = self._process_commands(result)

            # Check if any tags remain
            if not self.TRENDER_INCLUDE_PATTERN.search(result) and \
               not self.TRENDER_COMMAND_PATTERN.search(result):
                break

        if iteration >= max_iterations:
            logger.warning(f"Prompt rendering hit max iteration limit ({max_iterations}) - possible circular includes")

        total_includes = len(self.TRENDER_INCLUDE_PATTERN.findall(prompt_content))
        total_commands = len(self.TRENDER_COMMAND_PATTERN.findall(prompt_content))
        logger.info(f"Rendered {total_includes} includes and {total_commands} commands in {iteration} iteration(s)")

        return result

    def _process_includes(self, content: str) -> str:
        """Process all file include tags in content.

        Args:
            content: Content with include tags

        Returns:
            Content with includes replaced by file contents
        """
        matches = list(self.TRENDER_INCLUDE_PATTERN.finditer(content))

        if not matches:
            return content

        # Process in reverse order to maintain string positions
        result = content
        for match in reversed(matches):
            file_path = match.group(1).strip()
            start_pos = match.start()
            end_pos = match.end()

            # Resolve and read file
            file_content = self._include_file(file_path)

            # Replace the tag with the file content
            result = result[:start_pos] + file_content + result[end_pos:]

        return result

    def _process_commands(self, content: str) -> str:
        """Process all shell command tags in content.

        Args:
            content: Content with command tags

        Returns:
            Content with commands replaced by output
        """
        matches = list(self.TRENDER_COMMAND_PATTERN.finditer(content))

        if not matches:
            return content

        # Process in reverse order to maintain string positions
        result = content
        for match in reversed(matches):
            command = match.group(1).strip()
            start_pos = match.start()
            end_pos = match.end()

            # Execute command and get output
            output = self._execute_command(command)

            # Replace the tag with the output
            result = result[:start_pos] + output + result[end_pos:]

        return result

    def _execute_command(self, command: str) -> str:
        """Execute a shell command and return its output.

        Args:
            command: Shell command to execute.

        Returns:
            Command output or error message.
        """
        # Check cache first
        if command in self._command_cache:
            logger.debug(f"Using cached output for: {command}")
            return self._command_cache[command]

        try:
            logger.debug(f"Executing trender command: {command}")

            result = subprocess.run(
                command,
                shell=True,
                capture_output=True,
                text=True,
                timeout=self.timeout,
                cwd="."  # Execute in current directory
            )

            # Get output (prefer stdout, fallback to stderr)
            output = result.stdout if result.stdout else result.stderr
            output = output.strip()

            if result.returncode != 0:
                error_msg = f"[trender error: command exited with code {result.returncode}]"
                if result.stderr:
                    error_msg += f"\n{result.stderr.strip()}"
                logger.warning(f"Command failed: {command} (exit code: {result.returncode})")
                output = error_msg

            # Cache successful results
            if result.returncode == 0:
                self._command_cache[command] = output

            logger.debug(f"Command output ({len(output)} chars): {output[:100]}")
            return output

        except subprocess.TimeoutExpired:
            error_msg = f"[trender error: command timed out after {self.timeout}s]"
            logger.error(f"Command timed out: {command}")
            return error_msg

        except Exception as e:
            error_msg = f"[trender error: {type(e).__name__}: {str(e)}]"
            logger.error(f"Failed to execute command '{command}': {e}")
            return error_msg

    def _include_file(self, file_path: str) -> str:
        """Include content from a file.

        Args:
            file_path: Path to file to include (relative or absolute)

        Returns:
            File content or error message
        """
        # Check cache first
        if file_path in self._file_cache:
            logger.debug(f"Using cached file content: {file_path}")
            return self._file_cache[file_path]

        try:
            # Resolve path (relative to base_path or absolute)
            path = Path(file_path)
            if not path.is_absolute():
                path = self.base_path / path

            # Resolve any symlinks and .. components
            path = path.resolve()

            logger.debug(f"Including file: {path}")

            # Security check: ensure path is within allowed bounds
            # Don't allow including sensitive system files
            dangerous_paths = ['/etc/passwd', '/etc/shadow', '/etc/hosts',
                            '~/.ssh/', '/proc/', '/sys/', '/dev/']
            if any(str(path).startswith(dangerous) for dangerous in dangerous_paths):
                error_msg = f"[trender error: security violation - cannot include {path}]"
                logger.error(f"Blocked dangerous file include: {path}")
                return error_msg

            # Read file content
            content = path.read_text(encoding="utf-8")

            # Cache successful reads
            self._file_cache[file_path] = content

            logger.debug(f"File content ({len(content)} chars): {content[:100]}")
            return content

        except FileNotFoundError:
            error_msg = f"[trender error: file not found: {file_path}]"
            logger.error(f"File not found: {file_path}")
            return error_msg

        except PermissionError:
            error_msg = f"[trender error: permission denied: {file_path}]"
            logger.error(f"Permission denied: {file_path}")
            return error_msg

        except Exception as e:
            error_msg = f"[trender error: {type(e).__name__}: {str(e)}]"
            logger.error(f"Failed to include file '{file_path}': {e}")
            return error_msg

    def clear_cache(self):
        """Clear the command output and file content cache."""
        self._command_cache.clear()
        self._file_cache.clear()
        logger.debug("Cleared trender caches")

    def get_all_commands(self, prompt_content: str) -> List[str]:
        """Extract all shell commands from trender tags without executing.

        Args:
            prompt_content: System prompt content with <trender> tags.

        Returns:
            List of commands found in trender tags.
        """
        matches = self.TRENDER_COMMAND_PATTERN.findall(prompt_content)
        commands = [cmd.strip() for cmd in matches]
        return commands

    def get_all_includes(self, prompt_content: str) -> List[str]:
        """Extract all file paths from include tags without reading.

        Args:
            prompt_content: System prompt content with <trender> tags.

        Returns:
            List of file paths found in include tags.
        """
        matches = self.TRENDER_INCLUDE_PATTERN.findall(prompt_content)
        return [path.strip() for path in matches]


def render_system_prompt(
    prompt_content: str,
    timeout: int = 5,
    base_path: Optional[Path] = None
) -> str:
    """Convenience function to render a system prompt.

    Args:
        prompt_content: System prompt content with <trender> tags.
        timeout: Maximum seconds to wait for each command execution.
        base_path: Base directory for resolving relative file paths.

    Returns:
        Processed prompt with commands replaced by their output and
        files replaced by their content.
    """
    renderer = PromptRenderer(timeout=timeout, base_path=base_path)
    return renderer.render(prompt_content)
