"""Permission Analyzer for MCP Servers.

Deep analysis of permissions declared vs actually used in MCP server code.
Detects over-privileged (excess) and under-privileged (missing) permissions.
Provides least-privilege recommendations and a permission matrix.

Zero external dependencies -- pure Python stdlib.
"""

from __future__ import annotations

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


# ---------- Permission Categories ----------

class PermissionCategory:
    """Standard MCP permission categories."""

    FILESYSTEM_READ = "filesystem:read"
    FILESYSTEM_WRITE = "filesystem:write"
    NETWORK = "network"
    SUBPROCESS = "subprocess"
    ENV_ACCESS = "env:access"
    DATABASE = "database"
    CRYPTO = "crypto"
    SYSTEM_INFO = "system:info"
    IPC = "ipc"
    DANGEROUS_EVAL = "dangerous:eval"


# All known categories
ALL_CATEGORIES: list[str] = [
    PermissionCategory.FILESYSTEM_READ,
    PermissionCategory.FILESYSTEM_WRITE,
    PermissionCategory.NETWORK,
    PermissionCategory.SUBPROCESS,
    PermissionCategory.ENV_ACCESS,
    PermissionCategory.DATABASE,
    PermissionCategory.CRYPTO,
    PermissionCategory.SYSTEM_INFO,
    PermissionCategory.IPC,
    PermissionCategory.DANGEROUS_EVAL,
]


# ---------- Pattern Definitions ----------

@dataclass
class PermissionPattern:
    """A code pattern that implies a permission requirement."""

    category: str
    pattern: str  # regex pattern
    description: str
    confidence: str = "HIGH"  # HIGH, MEDIUM, LOW

    def to_dict(self) -> dict[str, Any]:
        return {
            "category": self.category,
            "pattern": self.pattern,
            "description": self.description,
            "confidence": self.confidence,
        }


# Patterns that indicate usage of specific permissions
_PERMISSION_PATTERNS: list[PermissionPattern] = [
    # Filesystem read
    PermissionPattern(PermissionCategory.FILESYSTEM_READ,
                      r'\bopen\s*\(.+?["\']r',
                      "File open for reading", "HIGH"),
    PermissionPattern(PermissionCategory.FILESYSTEM_READ,
                      r'\bPath\s*\(.+?\)\s*\.\s*read_text\b',
                      "Path.read_text()", "HIGH"),
    PermissionPattern(PermissionCategory.FILESYSTEM_READ,
                      r'\bPath\s*\(.+?\)\s*\.\s*read_bytes\b',
                      "Path.read_bytes()", "HIGH"),
    PermissionPattern(PermissionCategory.FILESYSTEM_READ,
                      r'\bos\.path\.(exists|isfile|isdir|getsize)\b',
                      "os.path file check", "MEDIUM"),
    PermissionPattern(PermissionCategory.FILESYSTEM_READ,
                      r'\bos\.(listdir|scandir|walk)\b',
                      "Directory listing", "HIGH"),
    PermissionPattern(PermissionCategory.FILESYSTEM_READ,
                      r'\bglob\.(glob|iglob)\b',
                      "Glob file search", "MEDIUM"),
    PermissionPattern(PermissionCategory.FILESYSTEM_READ,
                      r'\bPath\s*\(.+?\)\s*\.\s*(glob|rglob|iterdir)\b',
                      "Path directory iteration", "MEDIUM"),
    PermissionPattern(PermissionCategory.FILESYSTEM_READ,
                      r'\bshutil\.copy',
                      "File copy (implies read)", "MEDIUM"),
    PermissionPattern(PermissionCategory.FILESYSTEM_READ,
                      r'\bopen\s*\([^)]+\)\s*$',
                      "File open (default read)", "MEDIUM"),
    # Filesystem write
    PermissionPattern(PermissionCategory.FILESYSTEM_WRITE,
                      r'\bopen\s*\(.+?["\'][wax]',
                      "File open for writing", "HIGH"),
    PermissionPattern(PermissionCategory.FILESYSTEM_WRITE,
                      r'\bPath\s*\(.+?\)\s*\.\s*write_text\b',
                      "Path.write_text()", "HIGH"),
    PermissionPattern(PermissionCategory.FILESYSTEM_WRITE,
                      r'\bPath\s*\(.+?\)\s*\.\s*write_bytes\b',
                      "Path.write_bytes()", "HIGH"),
    PermissionPattern(PermissionCategory.FILESYSTEM_WRITE,
                      r'\bos\.(remove|unlink|rmdir|makedirs|mkdir)\b',
                      "Filesystem modification", "HIGH"),
    PermissionPattern(PermissionCategory.FILESYSTEM_WRITE,
                      r'\bshutil\.(rmtree|move|copytree)\b',
                      "Bulk filesystem operation", "HIGH"),
    PermissionPattern(PermissionCategory.FILESYSTEM_WRITE,
                      r'\bPath\s*\(.+?\)\s*\.\s*(mkdir|rmdir|unlink|touch|rename)\b',
                      "Path mutation", "HIGH"),
    # Network
    PermissionPattern(PermissionCategory.NETWORK,
                      r'\burllib\.(request|parse)\b',
                      "urllib network access", "HIGH"),
    PermissionPattern(PermissionCategory.NETWORK,
                      r'\bhttp\.(client|server)\b',
                      "HTTP library usage", "HIGH"),
    PermissionPattern(PermissionCategory.NETWORK,
                      r'\bsocket\.(socket|create_connection)\b',
                      "Raw socket access", "HIGH"),
    PermissionPattern(PermissionCategory.NETWORK,
                      r'\brequests\.(get|post|put|delete|patch|head|options)\b',
                      "requests HTTP call", "HIGH"),
    PermissionPattern(PermissionCategory.NETWORK,
                      r'\bhttpx\.(get|post|put|delete|AsyncClient|Client)\b',
                      "httpx HTTP call", "HIGH"),
    PermissionPattern(PermissionCategory.NETWORK,
                      r'\baiohttp\.(ClientSession|request)\b',
                      "aiohttp async HTTP", "HIGH"),
    PermissionPattern(PermissionCategory.NETWORK,
                      r'\bsmtplib\.SMTP\b',
                      "SMTP email sending", "HIGH"),
    PermissionPattern(PermissionCategory.NETWORK,
                      r'\bftplib\.FTP\b',
                      "FTP access", "HIGH"),
    # Subprocess
    PermissionPattern(PermissionCategory.SUBPROCESS,
                      r'\bsubprocess\.(run|Popen|call|check_output|check_call)\b',
                      "Subprocess execution", "HIGH"),
    PermissionPattern(PermissionCategory.SUBPROCESS,
                      r'\bos\.(system|popen|exec[lv]p?e?)\b',
                      "OS command execution", "HIGH"),
    PermissionPattern(PermissionCategory.SUBPROCESS,
                      r'\bos\.spawn[lv]p?e?\b',
                      "OS process spawn", "HIGH"),
    PermissionPattern(PermissionCategory.SUBPROCESS,
                      r'\bshlex\.(split|quote)\b',
                      "Shell command preparation", "LOW"),
    # Environment access
    PermissionPattern(PermissionCategory.ENV_ACCESS,
                      r'\bos\.environ\b',
                      "Environment variable access", "HIGH"),
    PermissionPattern(PermissionCategory.ENV_ACCESS,
                      r'\bos\.getenv\b',
                      "Environment variable read", "HIGH"),
    PermissionPattern(PermissionCategory.ENV_ACCESS,
                      r'\bos\.putenv\b',
                      "Environment variable write", "HIGH"),
    # Database
    PermissionPattern(PermissionCategory.DATABASE,
                      r'\bsqlite3\.(connect|Connection)\b',
                      "SQLite database access", "HIGH"),
    PermissionPattern(PermissionCategory.DATABASE,
                      r'\b(pymysql|psycopg2|asyncpg|cx_Oracle)\b',
                      "Database driver usage", "HIGH"),
    PermissionPattern(PermissionCategory.DATABASE,
                      r'\bcursor\.(execute|executemany|fetchall|fetchone)\b',
                      "SQL cursor operation", "MEDIUM"),
    # Crypto
    PermissionPattern(PermissionCategory.CRYPTO,
                      r'\bhashlib\.(sha256|sha512|md5|sha1|pbkdf2_hmac)\b',
                      "Cryptographic hashing", "MEDIUM"),
    PermissionPattern(PermissionCategory.CRYPTO,
                      r'\bhmac\.(new|digest|compare_digest)\b',
                      "HMAC operations", "MEDIUM"),
    PermissionPattern(PermissionCategory.CRYPTO,
                      r'\bsecrets\.(token_hex|token_bytes|token_urlsafe)\b',
                      "Secrets generation", "MEDIUM"),
    PermissionPattern(PermissionCategory.CRYPTO,
                      r'\bssl\.(create_default_context|SSLContext)\b',
                      "SSL/TLS configuration", "HIGH"),
    # System info
    PermissionPattern(PermissionCategory.SYSTEM_INFO,
                      r'\bplatform\.(system|node|release|machine|processor)\b',
                      "Platform info access", "LOW"),
    PermissionPattern(PermissionCategory.SYSTEM_INFO,
                      r'\bos\.(getpid|getppid|getuid|getgid|uname|cpu_count)\b',
                      "Process/system info", "LOW"),
    PermissionPattern(PermissionCategory.SYSTEM_INFO,
                      r'\bsys\.(platform|version|executable|argv)\b',
                      "Python runtime info", "LOW"),
    # IPC
    PermissionPattern(PermissionCategory.IPC,
                      r'\bmultiprocessing\.(Process|Pool|Queue|Pipe)\b',
                      "Multiprocessing IPC", "HIGH"),
    PermissionPattern(PermissionCategory.IPC,
                      r'\bthreading\.(Thread|Lock|Event|Semaphore)\b',
                      "Threading primitives", "MEDIUM"),
    PermissionPattern(PermissionCategory.IPC,
                      r'\bsignal\.(signal|alarm|pause)\b',
                      "Signal handling", "MEDIUM"),
    # Dangerous eval/exec
    PermissionPattern(PermissionCategory.DANGEROUS_EVAL,
                      r'\b(eval|exec)\s*\(',
                      "Dynamic code execution", "HIGH"),
    PermissionPattern(PermissionCategory.DANGEROUS_EVAL,
                      r'\bcompile\s*\(.+["\']exec["\']',
                      "Code compilation for exec", "HIGH"),
    PermissionPattern(PermissionCategory.DANGEROUS_EVAL,
                      r'\b__import__\s*\(',
                      "Dynamic import", "HIGH"),
    PermissionPattern(PermissionCategory.DANGEROUS_EVAL,
                      r'\bimportlib\.(import_module|reload)\b',
                      "Dynamic module loading", "MEDIUM"),
    # Serialization risks (detected as dangerous eval category)
    PermissionPattern(PermissionCategory.DANGEROUS_EVAL,
                      r'\byaml\.(load|unsafe_load)\b',
                      "Unsafe YAML loading", "HIGH"),
]


# ---------- Data Models ----------

@dataclass
class PermissionUsage:
    """A detected permission usage in code."""

    category: str
    line: int
    code_snippet: str
    description: str
    confidence: str = "HIGH"

    def to_dict(self) -> dict[str, Any]:
        return {
            "category": self.category,
            "line": self.line,
            "code_snippet": self.code_snippet,
            "description": self.description,
            "confidence": self.confidence,
        }


@dataclass
class PermissionIssue:
    """A permission issue (over-privileged or under-privileged)."""

    issue_type: str  # "over-privileged", "under-privileged"
    permission: str
    description: str
    severity: str = "MEDIUM"  # CRITICAL, HIGH, MEDIUM, LOW, INFO
    recommendation: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "issue_type": self.issue_type,
            "permission": self.permission,
            "description": self.description,
            "severity": self.severity,
            "recommendation": self.recommendation,
        }


@dataclass
class ToolPermission:
    """Permission mapping for a single tool."""

    tool_name: str
    required_permissions: list[str] = field(default_factory=list)
    usages: list[PermissionUsage] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        return {
            "tool_name": self.tool_name,
            "required_permissions": self.required_permissions,
            "usages": [u.to_dict() for u in self.usages],
        }


@dataclass
class PermissionMatrix:
    """Matrix of tools x required permissions."""

    tools: list[ToolPermission] = field(default_factory=list)

    @property
    def all_permissions(self) -> list[str]:
        """All unique permissions across all tools."""
        perms: set[str] = set()
        for tool in self.tools:
            perms.update(tool.required_permissions)
        return sorted(perms)

    def to_dict(self) -> dict[str, Any]:
        return {
            "tools": [t.to_dict() for t in self.tools],
            "all_permissions": self.all_permissions,
        }


@dataclass
class PermissionReport:
    """Full permission analysis report."""

    declared_permissions: list[str] = field(default_factory=list)
    used_permissions: list[str] = field(default_factory=list)
    over_privileged: list[PermissionIssue] = field(default_factory=list)
    under_privileged: list[PermissionIssue] = field(default_factory=list)
    usages: list[PermissionUsage] = field(default_factory=list)
    matrix: PermissionMatrix = field(default_factory=PermissionMatrix)
    recommendations: list[str] = field(default_factory=list)
    risk_score: int = 0  # 0-100
    errors: list[str] = field(default_factory=list)

    @property
    def total_issues(self) -> int:
        return len(self.over_privileged) + len(self.under_privileged)

    @property
    def is_least_privilege(self) -> bool:
        """True if permissions exactly match usage (no excess, no missing)."""
        return len(self.over_privileged) == 0 and len(self.under_privileged) == 0

    def to_dict(self) -> dict[str, Any]:
        return {
            "declared_permissions": self.declared_permissions,
            "used_permissions": self.used_permissions,
            "over_privileged": [i.to_dict() for i in self.over_privileged],
            "under_privileged": [i.to_dict() for i in self.under_privileged],
            "usages": [u.to_dict() for u in self.usages],
            "matrix": self.matrix.to_dict(),
            "recommendations": self.recommendations,
            "risk_score": self.risk_score,
            "total_issues": self.total_issues,
            "is_least_privilege": self.is_least_privilege,
            "errors": self.errors,
        }


# ---------- Analysis Engine ----------

def _detect_permissions(code: str) -> list[PermissionUsage]:
    """Scan code for permission usage patterns."""
    usages: list[PermissionUsage] = []
    lines = code.splitlines()
    for line_num, line_text in enumerate(lines, 1):
        stripped = line_text.strip()
        # Skip comments and empty lines
        if stripped.startswith("#") or not stripped:
            continue
        for pat in _PERMISSION_PATTERNS:
            if re.search(pat.pattern, line_text):
                usages.append(PermissionUsage(
                    category=pat.category,
                    line=line_num,
                    code_snippet=stripped[:120],
                    description=pat.description,
                    confidence=pat.confidence,
                ))
    return usages


def _extract_tool_functions(code: str) -> list[tuple[str, int, int]]:
    """Extract function boundaries that look like MCP tool handlers.

    Returns list of (tool_name, start_line, end_line).
    """
    tools: list[tuple[str, int, int]] = []
    lines = code.splitlines()

    # Pattern 1: @server.tool() or @mcp.tool() decorator
    decorator_re = re.compile(
        r'@\w+\.tool\s*\(\s*(?:["\'](\w+)["\'])?\s*\)'
    )
    # Pattern 2: def tool_xxx or def handle_xxx
    func_re = re.compile(r'^(?:async\s+)?def\s+(\w+)\s*\(')

    i = 0
    while i < len(lines):
        line = lines[i].strip()
        tool_name = None

        # Check for decorator
        m = decorator_re.search(line)
        if m:
            tool_name_from_dec = m.group(1)
            # Find the next function definition
            j = i + 1
            while j < len(lines):
                fm = func_re.match(lines[j].strip())
                if fm:
                    tool_name = tool_name_from_dec or fm.group(1)
                    start = j + 1
                    # Find end of function
                    end = _find_function_end(lines, j)
                    tools.append((tool_name, start, end))
                    i = end
                    break
                j += 1
                if j - i > 5:
                    break
        i += 1

    # If no decorated tools found, look for common tool function patterns
    if not tools:
        for i, line in enumerate(lines):
            fm = func_re.match(line.strip())
            if fm:
                name = fm.group(1)
                if any(kw in name.lower() for kw in ("tool", "handle", "execute", "run_")):
                    end = _find_function_end(lines, i)
                    tools.append((name, i + 1, end))

    return tools


def _find_function_end(lines: list[str], func_start: int) -> int:
    """Find the end line of a function starting at func_start."""
    if func_start >= len(lines):
        return func_start

    # Get indentation of the def line
    def_line = lines[func_start]
    indent = len(def_line) - len(def_line.lstrip())

    end = func_start + 1
    while end < len(lines):
        line = lines[end]
        stripped = line.strip()
        if stripped and not stripped.startswith("#"):
            line_indent = len(line) - len(line.lstrip())
            if line_indent <= indent and stripped:
                break
        end += 1
    return end


def _categorize_severity(issue_type: str, permission: str) -> str:
    """Determine severity of a permission issue."""
    dangerous_perms = {
        PermissionCategory.SUBPROCESS,
        PermissionCategory.DANGEROUS_EVAL,
    }
    high_perms = {
        PermissionCategory.FILESYSTEM_WRITE,
        PermissionCategory.NETWORK,
        PermissionCategory.DATABASE,
    }

    if issue_type == "under-privileged":
        if permission in dangerous_perms:
            return "CRITICAL"
        if permission in high_perms:
            return "HIGH"
        return "MEDIUM"
    else:  # over-privileged
        if permission in dangerous_perms:
            return "HIGH"
        if permission in high_perms:
            return "MEDIUM"
        return "LOW"


def _compute_risk_score(over_count: int, under_count: int,
                        dangerous_count: int) -> int:
    """Compute risk score 0-100.

    Under-privileged is riskier (undocumented capabilities).
    Over-privileged is a waste but less dangerous.
    """
    score = 0
    score += under_count * 15
    score += over_count * 5
    score += dangerous_count * 10
    return min(score, 100)


def _generate_recommendations(over: list[PermissionIssue],
                              under: list[PermissionIssue],
                              used: list[str]) -> list[str]:
    """Generate least-privilege recommendations."""
    recs: list[str] = []
    if over:
        excess = [i.permission for i in over]
        recs.append(f"Remove {len(excess)} unused permission(s): {', '.join(excess)}")
    if under:
        missing = [i.permission for i in under]
        recs.append(f"Declare {len(missing)} missing permission(s): {', '.join(missing)}")
    if not over and not under:
        recs.append("Permissions follow the principle of least privilege.")
    dangerous_used = [p for p in used if p in (
        PermissionCategory.SUBPROCESS, PermissionCategory.DANGEROUS_EVAL
    )]
    if dangerous_used:
        recs.append(
            f"Review dangerous permission(s): {', '.join(dangerous_used)}. "
            "Consider sandboxing or removal."
        )
    return recs


# ---------- Public API ----------

def analyze_permissions(
    code: str,
    declared_perms: list[str] | None = None,
) -> PermissionReport:
    """Analyze permissions declared vs used in MCP server code.

    Args:
        code: Source code of the MCP server.
        declared_perms: List of declared permission categories.
            If None, analysis only detects used permissions.

    Returns:
        PermissionReport with full analysis.
    """
    if declared_perms is None:
        declared_perms = []

    report = PermissionReport(
        declared_permissions=list(declared_perms),
    )

    if not code or not code.strip():
        report.errors.append("Empty code provided")
        return report

    # Detect permission usages
    usages = _detect_permissions(code)
    report.usages = usages

    # Deduce used permissions
    used_set: set[str] = set()
    for u in usages:
        used_set.add(u.category)
    report.used_permissions = sorted(used_set)

    # Build tool-level matrix
    tools = _extract_tool_functions(code)
    tool_perms: list[ToolPermission] = []
    lines = code.splitlines()
    for tool_name, start, end in tools:
        tool_code = "\n".join(lines[start - 1:end])
        tool_usages = _detect_permissions(tool_code)
        perm_set: set[str] = set()
        for tu in tool_usages:
            perm_set.add(tu.category)
            # Adjust line numbers to absolute
            tu.line = tu.line + start - 1
        tool_perms.append(ToolPermission(
            tool_name=tool_name,
            required_permissions=sorted(perm_set),
            usages=tool_usages,
        ))
    report.matrix = PermissionMatrix(tools=tool_perms)

    # Compute over/under privileged
    declared_set = set(declared_perms)

    for perm in sorted(declared_set - used_set):
        severity = _categorize_severity("over-privileged", perm)
        report.over_privileged.append(PermissionIssue(
            issue_type="over-privileged",
            permission=perm,
            description=f"Permission '{perm}' is declared but not used in code",
            severity=severity,
            recommendation=f"Remove '{perm}' from declared permissions",
        ))

    for perm in sorted(used_set - declared_set):
        severity = _categorize_severity("under-privileged", perm)
        report.under_privileged.append(PermissionIssue(
            issue_type="under-privileged",
            permission=perm,
            description=f"Permission '{perm}' is used in code but not declared",
            severity=severity,
            recommendation=f"Add '{perm}' to declared permissions",
        ))

    # Risk score
    dangerous_count = sum(
        1 for u in usages
        if u.category in (PermissionCategory.SUBPROCESS,
                          PermissionCategory.DANGEROUS_EVAL)
    )
    report.risk_score = _compute_risk_score(
        len(report.over_privileged),
        len(report.under_privileged),
        dangerous_count,
    )

    # Recommendations
    report.recommendations = _generate_recommendations(
        report.over_privileged,
        report.under_privileged,
        report.used_permissions,
    )

    return report


def get_permission_patterns() -> list[PermissionPattern]:
    """Return all known permission patterns."""
    return list(_PERMISSION_PATTERNS)


def get_permission_categories() -> list[str]:
    """Return all known permission categories."""
    return list(ALL_CATEGORIES)


# ---------- Formatters ----------

def format_permission_text(report: PermissionReport) -> str:
    """Format a PermissionReport as human-readable text."""
    lines: list[str] = []
    lines.append("\n  Permission Analysis Report")
    lines.append(f"  Risk Score: {report.risk_score}/100")
    lines.append(f"  Least Privilege: {'YES' if report.is_least_privilege else 'NO'}")
    lines.append(f"  Declared: {len(report.declared_permissions)}")
    lines.append(f"  Used: {len(report.used_permissions)}")
    lines.append(f"  Issues: {report.total_issues}")
    lines.append("")

    if report.over_privileged:
        lines.append("  OVER-PRIVILEGED (declared but unused):")
        for issue in report.over_privileged:
            lines.append(f"    [{issue.severity}] {issue.permission} -- {issue.description}")
        lines.append("")

    if report.under_privileged:
        lines.append("  UNDER-PRIVILEGED (used but undeclared):")
        for issue in report.under_privileged:
            lines.append(f"    [{issue.severity}] {issue.permission} -- {issue.description}")
        lines.append("")

    if report.matrix.tools:
        lines.append("  PERMISSION MATRIX:")
        for tool in report.matrix.tools:
            perms_str = ", ".join(tool.required_permissions) if tool.required_permissions else "none"
            lines.append(f"    {tool.tool_name}: {perms_str}")
        lines.append("")

    if report.recommendations:
        lines.append("  RECOMMENDATIONS:")
        for rec in report.recommendations:
            lines.append(f"    - {rec}")
        lines.append("")

    if report.errors:
        for err in report.errors:
            lines.append(f"  Error: {err}")
        lines.append("")

    return "\n".join(lines)


def format_permission_json(report: PermissionReport) -> str:
    """Format a PermissionReport as JSON."""
    return json.dumps(report.to_dict(), indent=2, ensure_ascii=False)
