"""Command parser: Extract operations from shell commands.

Parses simple shell commands to identify:
- Binary name (executable being called)
- Arguments
- Input files (from arguments)
- Output files (from redirects)
- Stream usage (stdin, stdout, stderr)

Limitations (documented, by design):
- Does NOT support pipes (|), conditionals, loops, functions
- Does NOT handle complex bash syntax
- Does NOT parse Python, Perl, or other language interpreters
- Rejects unsupported syntax with clear error messages

This conservative approach enables safe parsing without false negatives.
For complex commands, users are guided to use allowlist capabilities directly.
"""

import re
import shlex
from dataclasses import dataclass, field
from typing import List, Optional


class ParseError(Exception):
    """Error parsing command syntax."""
    pass


class UnsupportedSyntaxError(ParseError):
    """Command uses unsupported syntax (pipes, conditionals, etc.)."""
    pass


@dataclass
class ParsedCommand:
    """Result of parsing a command string."""

    binary: str  # "ls", "grep", "cat", etc.
    args: List[str] = field(default_factory=list)  # Arguments after binary
    input_files: List[str] = field(default_factory=list)  # Files being read
    output_files: List[str] = field(default_factory=list)  # Files being written
    uses_stdin: bool = False  # True if command reads stdin
    uses_stdout: bool = True  # True if command writes stdout (default)
    uses_stderr: bool = False  # True if command writes stderr


class CommandParser:
    """Parse shell commands to extract operations."""

    # Patterns for unsupported syntax that should be rejected
    _UNSUPPORTED_PATTERNS = [
        (r"\|", "pipes (|) not supported"),
        (r"&&|\|\|", "conditional operators (&&, ||) not supported"),
        (r";\s*(\w+|$)", "command sequences (;) not supported"),
        (r"\$\(", "command substitution $(...) not supported"),
        (r"`", "backtick substitution not supported"),
        (r"\bif\b|\bthen\b|\belse\b", "if/then/else not supported"),
        (r"\bfor\b|\bwhile\b|\bdo\b", "loops not supported"),
        (r"\bfunction\b", "function definitions not supported"),
        (r">\s*&", "stream duplication >&/< not supported"),
        (r"<\(", "process substitution not supported"),
        (r"[{}]", "brace expansion not supported"),
    ]

    @classmethod
    def parse(cls, cmd: str) -> ParsedCommand:
        """
        Parse a command string and extract operations.

        Args:
            cmd: Shell command string

        Returns:
            ParsedCommand with binary, args, files, streams

        Raises:
            ParseError: If command syntax is invalid
            UnsupportedSyntaxError: If command uses unsupported syntax

        Examples:
            parse("cat /var/log/syslog")
            → ParsedCommand(binary="cat", input_files=["/var/log/syslog"], uses_stdout=True)

            parse("grep error /var/log/*")
            → ParsedCommand(binary="grep", args=["error"], input_files=["/var/log/*"], uses_stdout=True)

            parse("echo hello > /tmp/out")
            → ParsedCommand(binary="echo", args=["hello"], output_files=["/tmp/out"], uses_stdout=False)

            parse("cat | grep error")
            → Raises UnsupportedSyntaxError("pipes (|) not supported")
        """
        cmd = cmd.strip()
        if not cmd:
            raise ParseError("Empty command")

        # Check for unsupported syntax first
        cls._check_unsupported_syntax(cmd)

        # Split command into tokens, preserving strings
        try:
            tokens = shlex.split(cmd)
        except ValueError as e:
            raise ParseError(f"Invalid shell syntax: {e}") from e

        if not tokens:
            raise ParseError("No command found")

        binary = tokens[0]
        remaining_tokens = tokens[1:]

        # Parse redirects and arguments
        input_files = []
        output_files = []
        args = []
        uses_stdin = False
        uses_stdout = True

        i = 0
        while i < len(remaining_tokens):
            token = remaining_tokens[i]

            # Output redirect: > file or >> file
            if token in (">", ">>"):
                if i + 1 >= len(remaining_tokens):
                    raise ParseError(f"Redirect {token} requires filename")
                output_file = remaining_tokens[i + 1]
                output_files.append(output_file)
                uses_stdout = False  # Output is redirected, not to stdout
                i += 2
                continue

            # Input redirect: < file
            if token == "<":
                if i + 1 >= len(remaining_tokens):
                    raise ParseError("Redirect < requires filename")
                input_file = remaining_tokens[i + 1]
                input_files.append(input_file)
                uses_stdin = True
                i += 2
                continue

            # Stdin pipe indicator: - (some commands use this)
            if token == "-" and i == len(remaining_tokens) - 1:
                uses_stdin = True
                i += 1
                continue

            # Regular argument (could be a file path)
            args.append(token)
            # Heuristic: if it looks like a file path, it's an input file
            if token.startswith("/") or token.startswith("."):
                input_files.append(token)
            i += 1

        return ParsedCommand(
            binary=binary,
            args=args,
            input_files=input_files,
            output_files=output_files,
            uses_stdin=uses_stdin,
            uses_stdout=uses_stdout,
            uses_stderr=False,  # Stderr is implicit for most commands
        )

    @classmethod
    def _check_unsupported_syntax(cls, cmd: str) -> None:
        """Check for unsupported syntax and raise UnsupportedSyntaxError.

        Respects quoted strings so 'echo "a|b"' is allowed (pipe is inside quotes).
        """
        # Remove quoted strings first so we only check syntax outside quotes
        # This is conservative: if a pattern is in quotes, we ignore it
        cmd_without_quotes = cls._remove_quoted_strings(cmd)

        for pattern, description in cls._UNSUPPORTED_PATTERNS:
            if re.search(pattern, cmd_without_quotes):
                raise UnsupportedSyntaxError(
                    f"Command uses unsupported syntax: {description}\n"
                    f"Command: {cmd}\n"
                    f"v1 supports only simple commands: binary + arguments + redirects (>, >>)\n"
                    f"For complex operations, consider breaking into multiple simple commands "
                    f"or using explicit capabilities."
                )

    @classmethod
    def _remove_quoted_strings(cls, cmd: str) -> str:
        """Remove quoted strings from command so syntax checks don't see content inside quotes.

        Returns command with quoted strings replaced by spaces, preserving structure.
        """
        result = []
        i = 0
        while i < len(cmd):
            if cmd[i] == '"':
                # Skip to closing double quote
                result.append(' ')
                i += 1
                while i < len(cmd) and cmd[i] != '"':
                    result.append(' ')
                    i += 1
                if i < len(cmd):
                    result.append(' ')
                    i += 1
            elif cmd[i] == "'":
                # Skip to closing single quote
                result.append(' ')
                i += 1
                while i < len(cmd) and cmd[i] != "'":
                    result.append(' ')
                    i += 1
                if i < len(cmd):
                    result.append(' ')
                    i += 1
            else:
                result.append(cmd[i])
                i += 1
        return ''.join(result)
