"""MCP Toxic Data Flow Analysis — Detect dangerous data flows in MCP servers.

Analyzes Python MCP server source code for toxic data flows:
- User input reaching subprocess/os.system without sanitization
- Tool results interpolated into prompts (prompt injection vector)
- File data passed to eval/compile
- Environment variables used in dangerous sinks
- Database query results used unsanitized
- Network data reaching code evaluation

Uses AST-based analysis for Python. Zero external dependencies.
Part of mcpaudit v0.8.0.

Usage:
    mcpaudit flows /path/to/server.py
    mcpaudit flows /path/to/server.py --format json
"""

from __future__ import annotations

import ast
import json
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from .models import Finding, Severity


# --- Flow Pattern definitions ---

@dataclass
class FlowPattern:
    """Definition of a toxic data flow pattern."""

    pattern_id: str
    name: str
    description: str
    severity: Severity
    source_type: str  # e.g. "user_input", "tool_result", "file_read"
    sink_type: str    # e.g. "subprocess", "code_eval", "prompt"
    cwe: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "pattern_id": self.pattern_id,
            "name": self.name,
            "description": self.description,
            "severity": self.severity.value,
            "source_type": self.source_type,
            "sink_type": self.sink_type,
            "cwe": self.cwe,
        }


@dataclass
class FlowFinding:
    """A detected toxic data flow."""

    pattern_id: str
    severity: Severity
    title: str
    description: str
    file: str
    source_line: int
    sink_line: int
    source_code: str = ""
    sink_code: str = ""
    flow_path: list[str] = field(default_factory=list)
    cwe: str = ""

    def to_finding(self) -> Finding:
        """Convert to standard Finding for unified output."""
        path_str = " -> ".join(self.flow_path) if self.flow_path else ""
        desc = self.description
        if path_str:
            desc += f" Flow: {path_str}"

        return Finding(
            rule_id=self.pattern_id,
            severity=self.severity,
            title=self.title,
            description=desc,
            file=self.file,
            line=self.sink_line,
            code_snippet=self.sink_code or self.source_code,
            recommendation=_get_recommendation(self.pattern_id),
            cwe=self.cwe,
        )

    def to_dict(self) -> dict[str, Any]:
        return {
            "pattern_id": self.pattern_id,
            "severity": self.severity.value,
            "title": self.title,
            "description": self.description,
            "file": self.file,
            "source_line": self.source_line,
            "sink_line": self.sink_line,
            "source_code": self.source_code,
            "sink_code": self.sink_code,
            "flow_path": self.flow_path,
            "cwe": self.cwe,
        }


@dataclass
class FlowResult:
    """Result of flow analysis on a file or directory."""

    file: str
    flows: list[FlowFinding] = field(default_factory=list)
    files_analyzed: int = 0
    parse_errors: list[str] = field(default_factory=list)

    @property
    def total_flows(self) -> int:
        return len(self.flows)

    @property
    def severity_counts(self) -> dict[str, int]:
        counts = {s.value: 0 for s in Severity}
        for f in self.flows:
            counts[f.severity.value] += 1
        return counts

    @property
    def has_critical(self) -> bool:
        return any(f.severity == Severity.CRITICAL for f in self.flows)

    @property
    def has_high(self) -> bool:
        return any(f.severity == Severity.HIGH for f in self.flows)

    @property
    def exit_code(self) -> int:
        if self.has_critical:
            return 2
        if self.has_high:
            return 1
        return 0

    def to_dict(self) -> dict[str, Any]:
        return {
            "file": self.file,
            "files_analyzed": self.files_analyzed,
            "total_flows": self.total_flows,
            "severity_counts": self.severity_counts,
            "exit_code": self.exit_code,
            "flows": [f.to_dict() for f in self.flows],
            "parse_errors": self.parse_errors,
        }


def _get_recommendation(pattern_id: str) -> str:
    """Get remediation recommendation for a flow pattern."""
    recommendations = {
        "FLOW-001": (
            "Never pass user input directly to subprocess or os.system. "
            "Use parameterized commands, allowlists, or shlex.quote()."
        ),
        "FLOW-002": (
            "Never interpolate tool results directly into prompts. "
            "Use structured output, XML escaping, or delimiter-based injection guards."
        ),
        "FLOW-003": (
            "Never pass file contents to code evaluation functions. "
            "Parse with a safe parser (json.loads, ast.literal_eval) instead."
        ),
        "FLOW-004": (
            "Never use environment variables directly in subprocess calls. "
            "Validate and sanitize env values before use."
        ),
        "FLOW-005": (
            "Never use database query results in code evaluation or subprocess "
            "without sanitization. Use parameterized queries and validate output."
        ),
        "FLOW-006": (
            "Never pass network-received data to code evaluation functions. "
            "Validate, sanitize, and use allowlists."
        ),
    }
    return recommendations.get(pattern_id, "Sanitize data between source and sink.")


def _get_all_flow_patterns() -> list[FlowPattern]:
    """Return all built-in flow patterns."""
    return [
        FlowPattern(
            pattern_id="FLOW-001",
            name="User input to command execution",
            description=(
                "Data from user input (request params, tool args) reaches "
                "subprocess.run/Popen/os.system without sanitization."
            ),
            severity=Severity.CRITICAL,
            source_type="user_input",
            sink_type="subprocess",
            cwe="CWE-78",
        ),
        FlowPattern(
            pattern_id="FLOW-002",
            name="Tool result to prompt interpolation",
            description=(
                "Data from MCP tool results is interpolated directly into "
                "prompts or messages, creating a prompt injection vector."
            ),
            severity=Severity.HIGH,
            source_type="tool_result",
            sink_type="prompt",
            cwe="CWE-74",
        ),
        FlowPattern(
            pattern_id="FLOW-003",
            name="File data to code evaluation",
            description=(
                "Data read from files is passed to code evaluation or "
                "compilation functions without validation."
            ),
            severity=Severity.CRITICAL,
            source_type="file_read",
            sink_type="code_eval",
            cwe="CWE-95",
        ),
        FlowPattern(
            pattern_id="FLOW-004",
            name="Environment variable to subprocess",
            description=(
                "Environment variable values are used directly in "
                "subprocess commands without sanitization."
            ),
            severity=Severity.HIGH,
            source_type="env_var",
            sink_type="subprocess",
            cwe="CWE-78",
        ),
        FlowPattern(
            pattern_id="FLOW-005",
            name="Database result to code execution",
            description=(
                "Data from database queries reaches code evaluation or "
                "subprocess without sanitization."
            ),
            severity=Severity.CRITICAL,
            source_type="db_query",
            sink_type="code_eval",
            cwe="CWE-89",
        ),
        FlowPattern(
            pattern_id="FLOW-006",
            name="Network data to code execution",
            description=(
                "Data received from HTTP requests or sockets reaches "
                "code evaluation or subprocess without sanitization."
            ),
            severity=Severity.CRITICAL,
            source_type="network",
            sink_type="code_eval",
            cwe="CWE-94",
        ),
    ]


# --- AST-based Source/Sink detection ---

# NOTE: This module detects PATTERNS of dangerous code in scanned targets.
# The string literals below (like "eval", "subprocess.run") are detection
# signatures, NOT invocations of those functions.

# Source detectors: functions/patterns that introduce tainted data
_SOURCE_PATTERNS: dict[str, list[str]] = {
    "user_input": [
        "input", "request.args", "request.form", "request.json",
        "request.data", "request.get_json", "request.params",
        "arguments", "params", "ctx.params",
    ],
    "tool_result": [
        "tool_result", "call_tool", "run_tool", "execute_tool",
        "tool_response", "tool_output",
    ],
    "file_read": [
        "read_text", "read_bytes", "readlines", "readline",
    ],
    "env_var": [
        "os.environ", "os.getenv", "environ.get", "os.environ.get",
    ],
    "db_query": [
        "cursor.execute", "cursor.fetchone", "cursor.fetchall",
        "session.execute", "session.query", "db.execute",
        "fetchone", "fetchall", "fetchmany",
    ],
    "network": [
        "requests.get", "requests.post", "requests.put",
        "urllib.request.urlopen", "urlopen",
        "httpx.get", "httpx.post",
        "socket.recv", "conn.recv",
        "response.json", "response.text", "response.content",
    ],
}

# Sink detectors: dangerous functions where tainted data causes harm
_SINK_PATTERNS: dict[str, list[str]] = {
    "subprocess": [
        "subprocess.run", "subprocess.call", "subprocess.Popen",
        "subprocess.check_output", "subprocess.check_call",
        "os.system", "os.popen",
    ],
    "code_eval": [
        # These are DETECTION SIGNATURES for scanning target code
        "eval", "compile",
    ],
    "prompt": [
        "prompt", "messages", "system_prompt",
        "create_message", "complete", "generate",
    ],
}

# Sanitizer functions that break the flow
_SANITIZERS: set[str] = {
    "shlex.quote", "shlex.split", "html.escape", "urllib.parse.quote",
    "json.dumps", "json.loads", "ast.literal_eval",
    "sanitize", "escape", "validate", "clean",
    "allowlist", "whitelist",
}


class _SourceSinkVisitor(ast.NodeVisitor):
    """AST visitor that identifies source and sink nodes."""

    def __init__(self, source: str) -> None:
        self.source = source
        self.lines = source.splitlines()
        self.sources: list[tuple[str, int, str]] = []  # (type, line, code)
        self.sinks: list[tuple[str, int, str]] = []    # (type, line, code)
        self.assignments: dict[str, tuple[str, int]] = {}  # var -> (source_type, line)
        self._current_func: str = ""

    def _get_line_text(self, lineno: int) -> str:
        """Get source text for a line number."""
        if 0 < lineno <= len(self.lines):
            return self.lines[lineno - 1].strip()
        return ""

    def _get_name(self, node: ast.AST) -> str:
        """Extract a readable name from an AST node."""
        if isinstance(node, ast.Name):
            return node.id
        if isinstance(node, ast.Attribute):
            value_name = self._get_name(node.value)
            return f"{value_name}.{node.attr}" if value_name else node.attr
        if isinstance(node, ast.Call):
            return self._get_name(node.func)
        return ""

    def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
        old = self._current_func
        self._current_func = node.name
        self.generic_visit(node)
        self._current_func = old

    visit_AsyncFunctionDef = visit_FunctionDef

    def visit_Call(self, node: ast.Call) -> None:
        name = self._get_name(node.func)
        line = getattr(node, "lineno", 0)
        code = self._get_line_text(line)

        # Check if this call is a source
        for source_type, patterns in _SOURCE_PATTERNS.items():
            for pattern in patterns:
                if name == pattern or name.endswith(f".{pattern}"):
                    self.sources.append((source_type, line, code))
                    break

        # Check if this call is a sink
        for sink_type, patterns in _SINK_PATTERNS.items():
            for pattern in patterns:
                if name == pattern or name.endswith(f".{pattern}"):
                    self.sinks.append((sink_type, line, code))
                    break

        self.generic_visit(node)

    def visit_Assign(self, node: ast.Assign) -> None:
        """Track variable assignments from source calls."""
        if isinstance(node.value, ast.Call):
            name = self._get_name(node.value.func)
            for source_type, patterns in _SOURCE_PATTERNS.items():
                for pattern in patterns:
                    if name == pattern or name.endswith(f".{pattern}"):
                        for target in node.targets:
                            var_name = self._get_name(target)
                            if var_name:
                                self.assignments[var_name] = (source_type, node.lineno)
                        break
        self.generic_visit(node)

    def visit_JoinedStr(self, node: ast.JoinedStr) -> None:
        """Detect f-strings that might interpolate tainted data."""
        line = getattr(node, "lineno", 0)
        code = self._get_line_text(line)
        # Check if f-string references a tainted variable
        for value in node.values:
            if isinstance(value, ast.FormattedValue):
                var_name = self._get_name(value.value)
                if var_name in self.assignments:
                    self.sinks.append(("prompt", line, code))
                    break
        self.generic_visit(node)


class FlowAnalyzer:
    """Analyzes MCP server source code for toxic data flows.

    Uses AST analysis to detect source-to-sink data flows where
    user input, tool results, or file data reaches dangerous sinks
    (subprocess, code evaluation, prompt interpolation) without sanitization.
    """

    def __init__(self, patterns: list[FlowPattern] | None = None) -> None:
        """Initialize flow analyzer.

        Args:
            patterns: Optional list of flow patterns. If None, all built-in patterns.
        """
        self.patterns = patterns if patterns is not None else _get_all_flow_patterns()
        self._active_pattern_ids = {p.pattern_id for p in self.patterns}
        self._pattern_map = {p.pattern_id: p for p in self.patterns}

    def analyze_file(self, filepath: Path) -> FlowResult:
        """Analyze a single Python file for toxic data flows.

        Args:
            filepath: Path to the Python source file.

        Returns:
            FlowResult with detected flows.
        """
        result = FlowResult(file=str(filepath), files_analyzed=1)

        try:
            source = filepath.read_text(encoding="utf-8", errors="replace")
        except (OSError, UnicodeDecodeError) as e:
            result.parse_errors.append(f"Cannot read {filepath}: {e}")
            return result

        return self.analyze_source(source, str(filepath))

    def analyze_source(self, source: str, filepath: str = "<stdin>") -> FlowResult:
        """Analyze source code text for toxic data flows.

        Args:
            source: Python source code text.
            filepath: Display path for findings.

        Returns:
            FlowResult with detected flows.
        """
        result = FlowResult(file=filepath, files_analyzed=1)

        try:
            tree = ast.parse(source, filename=filepath)
        except SyntaxError as e:
            result.parse_errors.append(f"Parse error: {e}")
            return result

        # Phase 1: Collect sources and sinks via AST
        visitor = _SourceSinkVisitor(source)
        visitor.visit(tree)

        # Phase 2: Match sources to sinks and detect flows
        flows = self._match_flows(
            visitor.sources,
            visitor.sinks,
            visitor.assignments,
            source,
            filepath,
        )
        result.flows = flows

        # Phase 3: Regex-based detection for patterns AST might miss
        regex_flows = self._regex_detect(source, filepath)
        # Deduplicate by (pattern_id, source_line, sink_line)
        existing = {(f.pattern_id, f.source_line, f.sink_line) for f in result.flows}
        for rf in regex_flows:
            key = (rf.pattern_id, rf.source_line, rf.sink_line)
            if key not in existing:
                result.flows.append(rf)
                existing.add(key)

        return result

    def analyze_directory(self, dirpath: Path) -> FlowResult:
        """Analyze all Python files in a directory.

        Args:
            dirpath: Path to directory to analyze.

        Returns:
            FlowResult with all detected flows.
        """
        combined = FlowResult(file=str(dirpath))

        for py_file in sorted(dirpath.rglob("*.py")):
            file_result = self.analyze_file(py_file)
            combined.flows.extend(file_result.flows)
            combined.files_analyzed += file_result.files_analyzed
            combined.parse_errors.extend(file_result.parse_errors)

        return combined

    def _match_flows(
        self,
        sources: list[tuple[str, int, str]],
        sinks: list[tuple[str, int, str]],
        assignments: dict[str, tuple[str, int]],
        source_text: str,
        filepath: str,
    ) -> list[FlowFinding]:
        """Match source-sink pairs to detect toxic flows."""
        findings: list[FlowFinding] = []
        lines = source_text.splitlines()

        # Map: source_type -> sink_type -> pattern_id
        flow_map: dict[tuple[str, str], str] = {
            ("user_input", "subprocess"): "FLOW-001",
            ("user_input", "code_eval"): "FLOW-001",
            ("tool_result", "prompt"): "FLOW-002",
            ("tool_result", "code_eval"): "FLOW-002",
            ("file_read", "code_eval"): "FLOW-003",
            ("file_read", "subprocess"): "FLOW-003",
            ("env_var", "subprocess"): "FLOW-004",
            ("env_var", "code_eval"): "FLOW-004",
            ("db_query", "code_eval"): "FLOW-005",
            ("db_query", "subprocess"): "FLOW-005",
            ("network", "code_eval"): "FLOW-006",
            ("network", "subprocess"): "FLOW-006",
        }

        for source_type, source_line, source_code in sources:
            for sink_type, sink_line, sink_code in sinks:
                # Only consider flows where sink comes after source
                if sink_line <= source_line:
                    continue

                key = (source_type, sink_type)
                pattern_id = flow_map.get(key)
                if not pattern_id:
                    continue

                if pattern_id not in self._active_pattern_ids:
                    continue

                # Check for sanitizers between source and sink
                if self._has_sanitizer(lines, source_line, sink_line):
                    continue

                pattern = self._pattern_map[pattern_id]
                findings.append(FlowFinding(
                    pattern_id=pattern_id,
                    severity=pattern.severity,
                    title=pattern.name,
                    description=pattern.description,
                    file=filepath,
                    source_line=source_line,
                    sink_line=sink_line,
                    source_code=source_code,
                    sink_code=sink_code,
                    flow_path=[
                        f"L{source_line}: {source_code[:50]}",
                        f"L{sink_line}: {sink_code[:50]}",
                    ],
                    cwe=pattern.cwe,
                ))

        return findings

    def _has_sanitizer(self, lines: list[str], start: int, end: int) -> bool:
        """Check if any sanitizer call exists between two lines."""
        for i in range(start, min(end, len(lines))):
            line = lines[i] if i < len(lines) else ""
            for sanitizer in _SANITIZERS:
                if sanitizer in line:
                    return True
        return False

    def _regex_detect(self, source: str, filepath: str) -> list[FlowFinding]:
        """Regex-based flow detection for patterns AST might miss."""
        findings: list[FlowFinding] = []
        lines = source.splitlines()

        if "FLOW-001" in self._active_pattern_ids:
            findings.extend(self._detect_input_to_subprocess(lines, filepath))
        if "FLOW-002" in self._active_pattern_ids:
            findings.extend(self._detect_tool_to_prompt(lines, filepath))
        if "FLOW-003" in self._active_pattern_ids:
            findings.extend(self._detect_file_to_code_eval(lines, filepath))
        if "FLOW-004" in self._active_pattern_ids:
            findings.extend(self._detect_env_to_subprocess(lines, filepath))
        if "FLOW-005" in self._active_pattern_ids:
            findings.extend(self._detect_db_to_code_eval(lines, filepath))
        if "FLOW-006" in self._active_pattern_ids:
            findings.extend(self._detect_network_to_code_eval(lines, filepath))

        return findings

    def _detect_input_to_subprocess(self, lines: list[str],
                                    filepath: str) -> list[FlowFinding]:
        """Detect user input flowing to subprocess via regex."""
        findings: list[FlowFinding] = []
        pattern = self._pattern_map["FLOW-001"]

        input_re = re.compile(
            r'(?:request\.(args|form|json|data|params)|arguments\[|params\[|'
            r'input\s*\(|ctx\.params)'
        )
        subprocess_re = re.compile(
            r'(?:subprocess\.(run|call|Popen|check_output|check_call)|'
            r'os\.(system|popen))\s*\('
        )

        input_lines: list[tuple[int, str]] = []
        for i, line in enumerate(lines, 1):
            if input_re.search(line):
                input_lines.append((i, line.strip()))

        for i, line in enumerate(lines, 1):
            if subprocess_re.search(line):
                for src_line, src_code in input_lines:
                    if src_line < i:
                        findings.append(FlowFinding(
                            pattern_id="FLOW-001",
                            severity=pattern.severity,
                            title=pattern.name,
                            description=pattern.description,
                            file=filepath,
                            source_line=src_line,
                            sink_line=i,
                            source_code=src_code[:80],
                            sink_code=line.strip()[:80],
                            flow_path=[
                                f"L{src_line}: {src_code[:50]}",
                                f"L{i}: {line.strip()[:50]}",
                            ],
                            cwe=pattern.cwe,
                        ))

        return findings

    def _detect_tool_to_prompt(self, lines: list[str],
                               filepath: str) -> list[FlowFinding]:
        """Detect tool results interpolated into prompts."""
        findings: list[FlowFinding] = []
        pattern = self._pattern_map["FLOW-002"]

        tool_re = re.compile(
            r'(?:tool_result|call_tool|run_tool|execute_tool|tool_response|tool_output)'
        )
        prompt_re = re.compile(
            r'(?:f["\'].*\{|\.format\(|prompt\s*[=+]|messages\s*[\[=]|'
            r'system_prompt|content\s*=\s*f["\'])'
        )

        tool_lines: list[tuple[int, str]] = []
        for i, line in enumerate(lines, 1):
            if tool_re.search(line):
                tool_lines.append((i, line.strip()))

        for i, line in enumerate(lines, 1):
            if prompt_re.search(line):
                for src_line, src_code in tool_lines:
                    if src_line < i:
                        findings.append(FlowFinding(
                            pattern_id="FLOW-002",
                            severity=pattern.severity,
                            title=pattern.name,
                            description=pattern.description,
                            file=filepath,
                            source_line=src_line,
                            sink_line=i,
                            source_code=src_code[:80],
                            sink_code=line.strip()[:80],
                            flow_path=[
                                f"L{src_line}: {src_code[:50]}",
                                f"L{i}: {line.strip()[:50]}",
                            ],
                            cwe=pattern.cwe,
                        ))

        return findings

    def _detect_file_to_code_eval(self, lines: list[str],
                                  filepath: str) -> list[FlowFinding]:
        """Detect file reads flowing to code evaluation."""
        findings: list[FlowFinding] = []
        pattern = self._pattern_map["FLOW-003"]

        read_re = re.compile(
            r'(?:\.read\(\)|\.read_text\(\)|\.readlines\(\)|\.readline\(\)|'
            r'open\(.+\)\.read)'
        )
        # Detect dangerous code evaluation calls in target code
        code_eval_re = re.compile(r'(?:(?<!\w)eval\s*\(|compile\s*\()')

        read_lines: list[tuple[int, str]] = []
        for i, line in enumerate(lines, 1):
            if read_re.search(line):
                read_lines.append((i, line.strip()))

        for i, line in enumerate(lines, 1):
            if code_eval_re.search(line):
                for src_line, src_code in read_lines:
                    if src_line < i:
                        findings.append(FlowFinding(
                            pattern_id="FLOW-003",
                            severity=pattern.severity,
                            title=pattern.name,
                            description=pattern.description,
                            file=filepath,
                            source_line=src_line,
                            sink_line=i,
                            source_code=src_code[:80],
                            sink_code=line.strip()[:80],
                            flow_path=[
                                f"L{src_line}: {src_code[:50]}",
                                f"L{i}: {line.strip()[:50]}",
                            ],
                            cwe=pattern.cwe,
                        ))

        return findings

    def _detect_env_to_subprocess(self, lines: list[str],
                                  filepath: str) -> list[FlowFinding]:
        """Detect env vars flowing to subprocess."""
        findings: list[FlowFinding] = []
        pattern = self._pattern_map["FLOW-004"]

        env_re = re.compile(
            r'(?:os\.environ|os\.getenv\(|environ\.get\(|environ\[)'
        )
        subprocess_re = re.compile(
            r'(?:subprocess\.(run|call|Popen|check_output|check_call)|'
            r'os\.(system|popen))\s*\('
        )

        env_lines: list[tuple[int, str]] = []
        for i, line in enumerate(lines, 1):
            if env_re.search(line):
                env_lines.append((i, line.strip()))

        for i, line in enumerate(lines, 1):
            if subprocess_re.search(line):
                for src_line, src_code in env_lines:
                    if src_line < i:
                        findings.append(FlowFinding(
                            pattern_id="FLOW-004",
                            severity=pattern.severity,
                            title=pattern.name,
                            description=pattern.description,
                            file=filepath,
                            source_line=src_line,
                            sink_line=i,
                            source_code=src_code[:80],
                            sink_code=line.strip()[:80],
                            flow_path=[
                                f"L{src_line}: {src_code[:50]}",
                                f"L{i}: {line.strip()[:50]}",
                            ],
                            cwe=pattern.cwe,
                        ))

        return findings

    def _detect_db_to_code_eval(self, lines: list[str],
                                filepath: str) -> list[FlowFinding]:
        """Detect database results flowing to code eval/subprocess."""
        findings: list[FlowFinding] = []
        pattern = self._pattern_map["FLOW-005"]

        db_re = re.compile(
            r'(?:cursor\.(execute|fetchone|fetchall|fetchmany)|'
            r'session\.(execute|query)|db\.execute|\.fetchone|\.fetchall)'
        )
        dangerous_re = re.compile(
            r'(?:(?<!\w)eval\s*\(|compile\s*\(|subprocess\.(run|call|Popen))'
        )

        db_lines: list[tuple[int, str]] = []
        for i, line in enumerate(lines, 1):
            if db_re.search(line):
                db_lines.append((i, line.strip()))

        for i, line in enumerate(lines, 1):
            if dangerous_re.search(line):
                for src_line, src_code in db_lines:
                    if src_line < i:
                        findings.append(FlowFinding(
                            pattern_id="FLOW-005",
                            severity=pattern.severity,
                            title=pattern.name,
                            description=pattern.description,
                            file=filepath,
                            source_line=src_line,
                            sink_line=i,
                            source_code=src_code[:80],
                            sink_code=line.strip()[:80],
                            flow_path=[
                                f"L{src_line}: {src_code[:50]}",
                                f"L{i}: {line.strip()[:50]}",
                            ],
                            cwe=pattern.cwe,
                        ))

        return findings

    def _detect_network_to_code_eval(self, lines: list[str],
                                     filepath: str) -> list[FlowFinding]:
        """Detect network data flowing to code eval."""
        findings: list[FlowFinding] = []
        pattern = self._pattern_map["FLOW-006"]

        net_re = re.compile(
            r'(?:requests\.(get|post|put)|urllib\.request\.urlopen|urlopen|'
            r'httpx\.(get|post)|socket\.recv|\.recv\(|response\.(json|text|content))'
        )
        dangerous_re = re.compile(
            r'(?:(?<!\w)eval\s*\(|compile\s*\(|subprocess\.(run|call|Popen)|os\.system)'
        )

        net_lines: list[tuple[int, str]] = []
        for i, line in enumerate(lines, 1):
            if net_re.search(line):
                net_lines.append((i, line.strip()))

        for i, line in enumerate(lines, 1):
            if dangerous_re.search(line):
                for src_line, src_code in net_lines:
                    if src_line < i:
                        findings.append(FlowFinding(
                            pattern_id="FLOW-006",
                            severity=pattern.severity,
                            title=pattern.name,
                            description=pattern.description,
                            file=filepath,
                            source_line=src_line,
                            sink_line=i,
                            source_code=src_code[:80],
                            sink_code=line.strip()[:80],
                            flow_path=[
                                f"L{src_line}: {src_code[:50]}",
                                f"L{i}: {line.strip()[:50]}",
                            ],
                            cwe=pattern.cwe,
                        ))

        return findings


# --- Formatting ---


def format_flows_text(result: FlowResult) -> str:
    """Format flow results as human-readable text."""
    lines: list[str] = []
    lines.append(f"\n{'=' * 60}")
    lines.append("  MCP Flow Analysis — Toxic Data Flows")
    lines.append(f"{'=' * 60}")
    lines.append(f"  Target: {result.file}")
    lines.append(f"  Files analyzed: {result.files_analyzed}")
    lines.append(f"  Total flows: {result.total_flows}")

    counts = result.severity_counts
    lines.append(
        f"  CRITICAL: {counts['CRITICAL']} | HIGH: {counts['HIGH']} | "
        f"MEDIUM: {counts['MEDIUM']} | LOW: {counts['LOW']}"
    )
    lines.append(f"{'=' * 60}\n")

    if not result.flows:
        lines.append("  [OK] No toxic data flows detected.\n")
        return "\n".join(lines)

    if result.parse_errors:
        for err in result.parse_errors:
            lines.append(f"  [!] {err}")
        lines.append("")

    for flow in result.flows:
        icon = flow.severity.icon_plain if hasattr(flow.severity, 'icon_plain') else "[*]"
        lines.append(f"  {icon} [{flow.pattern_id}] {flow.title}")
        lines.append(f"      {flow.description}")
        lines.append(f"      Source (L{flow.source_line}): {flow.source_code[:60]}")
        lines.append(f"      Sink   (L{flow.sink_line}): {flow.sink_code[:60]}")
        if flow.flow_path:
            lines.append(f"      Flow:  {' -> '.join(flow.flow_path)}")
        recommendation = _get_recommendation(flow.pattern_id)
        lines.append(f"      Fix:   {recommendation}")
        lines.append("")

    return "\n".join(lines)


def format_flows_json(result: FlowResult) -> str:
    """Format flow results as JSON."""
    return json.dumps(result.to_dict(), indent=2, ensure_ascii=False)


def get_all_flow_patterns() -> list[FlowPattern]:
    """Public accessor for all built-in flow patterns."""
    return _get_all_flow_patterns()
