r"""Pipe support for v2: Parse and validate shell pipelines safely.

This module enables support for command pipelines like:
  cat /var/log/syslog | grep error | wc -l

Key design decisions:
1. Conservative: Only support simple pipes, no complex bash syntax
2. Secure: Validate all commands and file accesses in the pipeline
3. Clear errors: Tell user exactly which command/operation is blocked

Limitations (by design):
- No command substitution $(...)
- No process substitution <(...)
- No conditionals (&&, ||, ;)
- No variable expansion $VAR
- No quoting except for single/double quoted strings

Use cases:
- cat /var/log/syslog | grep error
- ls /var/log | grep '\.log$' | wc -l
- find /var/log -name '*.log' | xargs grep error
"""

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

from rpipe.command_parser import CommandParser, ParseError, UnsupportedSyntaxError, ParsedCommand


class PipeParseError(ParseError):
    """Error parsing pipeline syntax."""
    pass


@dataclass
class Pipeline:
    """Parsed pipeline structure."""

    commands: List[ParsedCommand]  # Commands in the pipeline
    num_stages: int  # Number of pipe stages

    def __repr__(self) -> str:
        """String representation for debugging."""
        return f"Pipeline({self.num_stages} stages: {' | '.join(c.binary for c in self.commands)})"


class PipelineParser:
    """Parse shell pipelines and extract commands."""

    # Pattern to split on pipes, but be careful with quoted strings
    PIPE_PATTERN = re.compile(r'\|(?=(?:[^\'"]|[\'"][^\'"]*[\'"])*$)')

    @classmethod
    def parse(cls, cmd: str) -> Pipeline:
        """
        Parse a pipeline command string.

        Args:
            cmd: Pipeline command like "cat file | grep error | wc -l"

        Returns:
            Pipeline with list of ParsedCommand objects for each stage

        Raises:
            PipeParseError: If pipeline syntax is invalid
            UnsupportedSyntaxError: If unsupported syntax is used

        Examples:
            parse("cat /var/log/syslog | grep error")
            → Pipeline([ParsedCommand(binary="cat", ...), ParsedCommand(binary="grep", ...)])

            parse("ls | head -10 | tail -5")
            → Pipeline with 3 stages

            parse("cat /var/log/syslog | grep | error")
            → PipeParseError: Invalid pipe syntax
        """
        cmd = cmd.strip()
        if not cmd:
            raise PipeParseError("Empty pipeline")

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

        # Split by pipe character (being careful with quotes)
        # This is a simple regex-based approach; doesn't handle all edge cases
        # but is conservative and safe
        stages = cls._split_by_pipe(cmd)

        if not stages:
            raise PipeParseError("No commands in pipeline")

        if len(stages) == 1:
            # Single command, not actually a pipeline
            # Parse it normally
            return Pipeline(
                commands=[CommandParser.parse(stages[0])],
                num_stages=1
            )

        # Parse each stage as a command
        commands = []
        for i, stage in enumerate(stages):
            stage = stage.strip()
            if not stage:
                raise PipeParseError(f"Empty command at pipe stage {i + 1}")

            try:
                parsed = CommandParser.parse(stage)
                commands.append(parsed)
            except ParseError as e:
                raise PipeParseError(
                    f"Error parsing stage {i + 1} ('{stage}'): {e}"
                ) from e

        return Pipeline(commands=commands, num_stages=len(stages))

    @classmethod
    def _split_by_pipe(cls, cmd: str) -> List[str]:
        r"""
        Split command by pipe character, respecting quoted strings.

        This is a simple implementation that handles most common cases:
        - Single-quoted strings: 'don\'t split|here'
        - Double-quoted strings: "don't|split|here"
        - Escaped characters: don\|t split here

        Edge cases NOT handled (acceptable for v2):
        - Nested quotes of different types
        - Complex escape sequences
        - Non-ASCII quoted strings
        """
        stages = []
        current = []
        in_single = False
        in_double = False
        i = 0

        while i < len(cmd):
            ch = cmd[i]

            # Handle quote toggling
            if ch == "'" and not in_double:
                in_single = not in_single
                current.append(ch)
                i += 1
                continue

            if ch == '"' and not in_single:
                in_double = not in_double
                current.append(ch)
                i += 1
                continue

            # Handle pipe character (only outside quotes)
            if ch == "|" and not in_single and not in_double:
                # Check that next character isn't another pipe
                if i + 1 < len(cmd) and cmd[i + 1] == "|":
                    raise PipeParseError("Conditional operators (||) not supported")

                # Found a pipe separator
                # IMPORTANT: preserve empty stages to detect leading/trailing pipes
                stage = "".join(current).strip()
                stages.append(stage)  # Append even if empty
                current = []
                i += 1
                continue

            # Regular character
            current.append(ch)
            i += 1

        # Add final stage (even if empty, to detect trailing pipes)
        stage = "".join(current).strip()
        stages.append(stage)

        return stages

    @classmethod
    def _check_unsupported_syntax(cls, cmd: str) -> None:
        """Check for unsupported syntax in pipeline."""
        # Don't allow command substitution
        if "$(" in cmd or "`" in cmd:
            raise UnsupportedSyntaxError(
                "Command substitution not supported in pipelines (v2)"
            )

        # Don't allow conditionals with pipes
        if "||" in cmd and "|" in cmd:
            raise UnsupportedSyntaxError(
                "Conditional operators (||) not supported in pipelines"
            )

        # Don't allow process substitution
        if "<(" in cmd or ">(" in cmd:
            raise UnsupportedSyntaxError(
                "Process substitution not supported in pipelines"
            )

        # Don't allow complex redirects with pipes
        if ">>" in cmd and "|" in cmd:
            # This is complex to parse correctly, so just reject it
            raise UnsupportedSyntaxError(
                "Append redirect (>>) with pipes not supported (v2)"
            )


def validate_pipeline_against_capabilities(
    cmd: str,
    capability_set,
) -> None:
    """
    Validate that all commands in a pipeline are permitted.

    Args:
        cmd: Pipeline command string
        capability_set: CapabilitySet with allowed operations

    Raises:
        PipeParseError: If pipeline syntax is invalid
        PolicyError: If any command violates policy

    Algorithm:
    1. Parse the pipeline into individual commands
    2. For each command, validate it separately
    3. Ensure intermediate pipes (stdin/stdout) are allowed
    4. Clear error messages on failure
    """
    from rpipe.policy_capabilities import validate_command_against_capabilities

    # Parse pipeline
    pipeline = PipelineParser.parse(cmd)

    # Validate each stage
    for i, parsed_cmd in enumerate(pipeline.commands):
        stage_num = i + 1
        stage_desc = f"Stage {stage_num}/{pipeline.num_stages}: {parsed_cmd.binary}"

        try:
            # For each command in the pipeline, we need to validate:
            # 1. The binary itself (execute_binary)
            # 2. Input files (if it's the first stage or has input files)
            # 3. Output files (if it's the last stage and has output files)
            # 4. Pipes (intermediate stdin/stdout)

            # All commands in pipeline need stdout except last might redirect
            if i < len(pipeline.commands) - 1:
                # Intermediate stages need to write stdout (for the pipe)
                parsed_cmd.uses_stdout = True
                # And read from previous stage's stdout (stdin)
                parsed_cmd.uses_stdin = True

            # Validate as normal command
            validate_command_against_capabilities(
                f"{parsed_cmd.binary} {' '.join(parsed_cmd.args)}",
                capability_set
            )

        except Exception as e:
            raise Exception(
                f"Pipeline validation failed at {stage_desc}: {e}"
            ) from e
