"""
Read-Only Mode Analyzer for MCP Security Auditor.

Analyzes MCP servers to ensure read-only mode is enforced where appropriate,
particularly for database connections and data store access.

This is critical for enterprise deployments where MCP servers should NOT
have write/delete capabilities to prevent data loss or corruption.
"""

import re
from pathlib import Path
from typing import List, Dict, Tuple

from ..core.config import AuditConfig
from ..core.models import (
    Finding, CodeLocation, Remediation, MCPServerInfo, ToolDefinition
)
from .static import BaseAnalyzer


class ReadOnlyModeAnalyzer(BaseAnalyzer):
    """
    Analyzes MCP servers for read-only mode enforcement.
    
    Critical for:
    - Database connections (PostgreSQL, MySQL, MongoDB, etc.)
    - Cloud storage (S3, GCS, Azure Blob)
    - File system access
    - API integrations with write capabilities
    
    Checks for:
    - Database connections without read-only flags
    - Tools with write/delete operations not marked appropriately
    - Missing readOnlyHint annotations
    - Connection strings without read-only parameters
    - Write-capable drivers/libraries
    """
    
    name = "readonly"
    
    # Database connection patterns that indicate write access risk
    DATABASE_WRITE_PATTERNS = {
        "postgresql": [
            # Connection without read-only mode
            (r"psycopg[23]?\.connect\s*\([^)]*\)", 
             r"options.*-c\s*default_transaction_read_only\s*=\s*on|readonly\s*=\s*true",
             "PostgreSQL connection without read-only mode"),
            (r"asyncpg\.connect\s*\([^)]*\)",
             r"server_settings.*default_transaction_read_only",
             "AsyncPG connection without read-only mode"),
            (r"postgresql://[^\\s]+",
             r"options=-c%20default_transaction_read_only",
             "PostgreSQL URL without read-only option"),
        ],
        "mysql": [
            (r"mysql\.connector\.connect\s*\([^)]*\)",
             r"read_only\s*=\s*True",
             "MySQL connection without read-only mode"),
            (r"pymysql\.connect\s*\([^)]*\)",
             r"read_only\s*=\s*True|init_command.*SET SESSION TRANSACTION READ ONLY",
             "PyMySQL connection without read-only mode"),
            (r"aiomysql\.connect\s*\([^)]*\)",
             r"init_command.*READ ONLY",
             "AioMySQL connection without read-only mode"),
        ],
        "mongodb": [
            (r"pymongo\.MongoClient\s*\([^)]*\)",
             r"readPreference|read_preference",
             "MongoDB connection - verify read-only access"),
            (r"motor\.motor_asyncio\.AsyncIOMotorClient\s*\([^)]*\)",
             r"readPreference",
             "Motor MongoDB connection - verify read-only access"),
        ],
        "sqlite": [
            (r"sqlite3\.connect\s*\([^)]*\)",
             r"mode=ro|uri=True.*mode=ro|\?mode=ro",
             "SQLite connection without read-only mode"),
            (r"aiosqlite\.connect\s*\([^)]*\)",
             r"mode=ro",
             "AioSQLite connection without read-only mode"),
        ],
        "redis": [
            (r"redis\.Redis\s*\([^)]*\)|redis\.from_url\s*\([^)]*\)",
             r"readonly\s*=\s*True",
             "Redis connection - consider read-only replica"),
            (r"aioredis\.(create_redis|from_url)\s*\([^)]*\)",
             r"readonly",
             "AioRedis connection - consider read-only replica"),
        ],
        "elasticsearch": [
            (r"Elasticsearch\s*\([^)]*\)",
             r"read_only",
             "Elasticsearch connection - verify read-only access"),
        ],
    }
    
    # Cloud storage patterns
    CLOUD_STORAGE_PATTERNS = {
        "aws_s3": [
            (r"boto3\.client\s*\(\s*['\"]s3['\"]|boto3\.resource\s*\(\s*['\"]s3['\"]",
             "AWS S3 client - verify IAM policy enforces read-only"),
            (r"s3\.put_object|s3\.delete_object|s3\.upload_file|\.upload_fileobj",
             "S3 write operation detected"),
        ],
        "gcs": [
            (r"storage\.Client\s*\(",
             "Google Cloud Storage client - verify IAM enforces read-only"),
            (r"\.upload_from_|blob\.delete\(|bucket\.delete\(",
             "GCS write operation detected"),
        ],
        "azure_blob": [
            (r"BlobServiceClient\s*\(",
             "Azure Blob client - verify RBAC enforces read-only"),
            (r"\.upload_blob|\.delete_blob|\.delete_container",
             "Azure Blob write operation detected"),
        ],
    }
    
    # SQL write operations
    SQL_WRITE_OPERATIONS = [
        (r"\bINSERT\s+INTO\b", "INSERT operation"),
        (r"\bUPDATE\s+\w+\s+SET\b", "UPDATE operation"),
        (r"\bDELETE\s+FROM\b", "DELETE operation"),
        (r"\bDROP\s+(TABLE|DATABASE|INDEX)\b", "DROP operation"),
        (r"\bTRUNCATE\s+TABLE\b", "TRUNCATE operation"),
        (r"\bALTER\s+TABLE\b", "ALTER TABLE operation"),
        (r"\bCREATE\s+(TABLE|DATABASE|INDEX)\b", "CREATE operation"),
        (r"\bGRANT\s+", "GRANT operation"),
        (r"\bREVOKE\s+", "REVOKE operation"),
    ]
    
    # Tool name patterns that indicate write operations
    WRITE_TOOL_PATTERNS = [
        (r"create|insert|add|new|post", "Creation operation"),
        (r"update|modify|edit|change|patch|put", "Modification operation"),
        (r"delete|remove|drop|destroy|purge|clear", "Deletion operation"),
        (r"write|save|store|upload|push", "Write operation"),
        (r"execute|run|exec", "Execution operation"),
        (r"truncate|reset|wipe", "Destructive operation"),
    ]
    
    # Read-only tool patterns (these are safe)
    READONLY_TOOL_PATTERNS = [
        r"get|fetch|read|list|search|find|query|select|describe|show|count|exists|check|validate|verify"
    ]
    
    async def analyze(self, path: str, server_info: MCPServerInfo) -> List[Finding]:
        """Analyze for read-only mode enforcement."""
        findings = []
        path = Path(path)
        
        # 1. Check tool annotations for readOnlyHint
        findings.extend(self._analyze_tool_readonly_hints(server_info.tools, path))
        
        # 2. Check database connections for read-only mode
        findings.extend(await self._analyze_database_connections(path, server_info))
        
        # 3. Check for SQL write operations
        findings.extend(await self._analyze_sql_operations(path, server_info))
        
        # 4. Check cloud storage access patterns
        findings.extend(await self._analyze_cloud_storage(path, server_info))
        
        # 5. Generate summary finding if write access is detected
        findings.extend(self._generate_summary_finding(findings, server_info))
        
        return findings
    
    def _analyze_tool_readonly_hints(self, tools: List[ToolDefinition], base_path: Path) -> List[Finding]:
        """Analyze tool definitions for proper readOnlyHint annotations."""
        findings = []
        
        write_tools = []
        read_tools = []
        unmarked_tools = []
        
        for tool in tools:
            tool_name_lower = tool.name.lower()
            
            # Check if tool name suggests write operation
            is_write_tool = any(
                re.search(pattern, tool_name_lower) 
                for pattern, _ in self.WRITE_TOOL_PATTERNS
            )
            
            # Check if tool name suggests read-only operation
            is_read_tool = re.search(self.READONLY_TOOL_PATTERNS[0], tool_name_lower)
            
            has_readonly_hint = tool.annotations.get('readOnlyHint', None)
            
            # Case 1: Write tool marked as read-only (CRITICAL)
            if is_write_tool and has_readonly_hint is True:
                findings.append(Finding(
                    title=f"Write tool '{tool.name}' incorrectly marked as read-only",
                    description=f"Tool '{tool.name}' appears to perform write operations but has "
                               f"readOnlyHint=True. This is dangerous as it misleads AI models "
                               f"about the tool's capabilities.",
                    severity="critical",
                    category="permissions",
                    analyzer=self.name,
                    location=CodeLocation(
                        file=tool.file_path or "unknown",
                        line_start=tool.line_number or 0,
                        line_end=tool.line_number or 0
                    ) if tool.file_path else None,
                    remediation=Remediation(
                        description="Set readOnlyHint=False for tools that can modify data. "
                                   "Consider whether this tool should exist at all in a read-only MCP server.",
                        code_example='annotations={"readOnlyHint": False, "destructiveHint": True}'
                    ),
                    confidence="high"
                ))
                write_tools.append(tool.name)
            
            # Case 2: Write tool without readOnlyHint annotation (HIGH)
            elif is_write_tool and has_readonly_hint is None:
                findings.append(Finding(
                    title=f"Write tool '{tool.name}' missing readOnlyHint annotation",
                    description=f"Tool '{tool.name}' appears to perform write operations but does not "
                               f"have readOnlyHint annotation. This should be explicitly set to False.",
                    severity="high",
                    category="permissions",
                    analyzer=self.name,
                    location=CodeLocation(
                        file=tool.file_path or "unknown",
                        line_start=tool.line_number or 0,
                        line_end=tool.line_number or 0
                    ) if tool.file_path else None,
                    remediation=Remediation(
                        description="Add readOnlyHint=False to clearly indicate this tool can modify data.",
                        code_example='annotations={"readOnlyHint": False}'
                    ),
                    confidence="high"
                ))
                write_tools.append(tool.name)
            
            # Case 3: Tool with write capability (readOnlyHint=False) - flag for review
            elif has_readonly_hint is False:
                write_tools.append(tool.name)
                findings.append(Finding(
                    title=f"Tool '{tool.name}' has write access enabled",
                    description=f"Tool '{tool.name}' is marked with readOnlyHint=False, indicating it can "
                               f"modify data. Ensure this is intentional and necessary for your use case.",
                    severity="medium",
                    category="permissions",
                    analyzer=self.name,
                    location=CodeLocation(
                        file=tool.file_path or "unknown",
                        line_start=tool.line_number or 0,
                        line_end=tool.line_number or 0
                    ) if tool.file_path else None,
                    remediation=Remediation(
                        description="If read-only mode is required, remove this tool or change to a "
                                   "read-only implementation."
                    ),
                    confidence="high"
                ))
            
            # Case 4: Ambiguous tool without annotation
            elif not is_read_tool and has_readonly_hint is None:
                unmarked_tools.append(tool.name)
            
            else:
                read_tools.append(tool.name)
        
        # Warn about unmarked tools
        if unmarked_tools:
            findings.append(Finding(
                title=f"{len(unmarked_tools)} tools missing readOnlyHint annotation",
                description=f"Tools without readOnlyHint: {', '.join(unmarked_tools[:5])}"
                           f"{'...' if len(unmarked_tools) > 5 else ''}. "
                           f"Explicitly mark all tools with readOnlyHint for clarity.",
                severity="low",
                category="permissions",
                analyzer=self.name,
                remediation=Remediation(
                    description="Add readOnlyHint=True for read-only tools and readOnlyHint=False "
                               "for tools that can modify data."
                ),
                confidence="high"
            ))
        
        return findings
    
    async def _analyze_database_connections(self, path: Path, server_info: MCPServerInfo) -> List[Finding]:
        """Analyze database connection code for read-only mode."""
        findings = []
        
        if server_info.language == "python":
            extensions = ["*.py"]
        else:
            extensions = ["*.ts", "*.js", "*.mjs"]
        
        for ext in extensions:
            for file_path in path.rglob(ext):
                if self._should_skip_file(file_path):
                    continue
                
                content = self._read_file_content(file_path)
                if not content:
                    continue
                
                relative_path = str(file_path.relative_to(path))
                lines = content.split('\n')
                
                # Check each database type
                for db_type, patterns in self.DATABASE_WRITE_PATTERNS.items():
                    for pattern_tuple in patterns:
                        if len(pattern_tuple) == 3:
                            connect_pattern, readonly_pattern, description = pattern_tuple
                        else:
                            connect_pattern, description = pattern_tuple
                            readonly_pattern = None
                        
                        for i, line in enumerate(lines, 1):
                            if re.search(connect_pattern, line, re.IGNORECASE):
                                # Check surrounding context for read-only configuration
                                context_start = max(0, i - 5)
                                context_end = min(len(lines), i + 5)
                                context = '\n'.join(lines[context_start:context_end])
                                
                                has_readonly = False
                                if readonly_pattern:
                                    has_readonly = bool(re.search(readonly_pattern, context, re.IGNORECASE))
                                
                                if not has_readonly:
                                    findings.append(Finding(
                                        title=f"{db_type.upper()} connection without read-only mode",
                                        description=f"{description}. Database connections should use read-only "
                                                   f"mode to prevent accidental data modification.",
                                        severity="high",
                                        category="permissions",
                                        analyzer=self.name,
                                        location=CodeLocation(
                                            file=relative_path,
                                            line_start=i,
                                            line_end=i,
                                            snippet=self._get_code_snippet(file_path, i)
                                        ),
                                        remediation=Remediation(
                                            description=self._get_db_readonly_remediation(db_type),
                                            code_example=self._get_db_readonly_example(db_type)
                                        ),
                                        confidence="high",
                                        metadata={"database_type": db_type}
                                    ))
                                break  # Only report first occurrence per pattern
        
        return findings
    
    async def _analyze_sql_operations(self, path: Path, server_info: MCPServerInfo) -> List[Finding]:
        """Analyze code for SQL write operations."""
        findings = []
        
        if server_info.language == "python":
            extensions = ["*.py"]
        else:
            extensions = ["*.ts", "*.js", "*.mjs"]
        
        for ext in extensions:
            for file_path in path.rglob(ext):
                if self._should_skip_file(file_path):
                    continue
                
                content = self._read_file_content(file_path)
                if not content:
                    continue
                
                relative_path = str(file_path.relative_to(path))
                lines = content.split('\n')
                
                for pattern, operation_type in self.SQL_WRITE_OPERATIONS:
                    for i, line in enumerate(lines, 1):
                        if re.search(pattern, line, re.IGNORECASE):
                            # Skip if it's in a comment
                            stripped = line.strip()
                            if stripped.startswith('#') or stripped.startswith('//') or stripped.startswith('--'):
                                continue
                            
                            severity = "critical" if operation_type in ["DROP operation", "TRUNCATE operation"] else "high"
                            
                            findings.append(Finding(
                                title=f"SQL {operation_type} detected",
                                description=f"SQL {operation_type} found in code. This indicates the MCP server "
                                           f"can modify database data, which may violate read-only requirements.",
                                severity=severity,
                                category="permissions",
                                analyzer=self.name,
                                location=CodeLocation(
                                    file=relative_path,
                                    line_start=i,
                                    line_end=i,
                                    snippet=self._get_code_snippet(file_path, i)
                                ),
                                remediation=Remediation(
                                    description=f"Remove {operation_type} if read-only mode is required. "
                                               f"Use read-only database connections to enforce at the connection level."
                                ),
                                confidence="high",
                                metadata={"sql_operation": operation_type}
                            ))
        
        return findings
    
    async def _analyze_cloud_storage(self, path: Path, server_info: MCPServerInfo) -> List[Finding]:
        """Analyze cloud storage access patterns."""
        findings = []
        
        if server_info.language == "python":
            extensions = ["*.py"]
        else:
            extensions = ["*.ts", "*.js", "*.mjs"]
        
        for ext in extensions:
            for file_path in path.rglob(ext):
                if self._should_skip_file(file_path):
                    continue
                
                content = self._read_file_content(file_path)
                if not content:
                    continue
                
                relative_path = str(file_path.relative_to(path))
                lines = content.split('\n')
                
                for cloud_type, patterns in self.CLOUD_STORAGE_PATTERNS.items():
                    for pattern_tuple in patterns:
                        pattern, description = pattern_tuple
                        
                        for i, line in enumerate(lines, 1):
                            if re.search(pattern, line, re.IGNORECASE):
                                # Determine if it's a client init or a write operation
                                is_write_op = any(
                                    write_kw in pattern.lower() 
                                    for write_kw in ['put', 'delete', 'upload', 'write']
                                )
                                
                                severity = "high" if is_write_op else "medium"
                                
                                findings.append(Finding(
                                    title=f"{cloud_type.upper().replace('_', ' ')}: {description}",
                                    description=f"{description}. Ensure IAM/RBAC policies enforce read-only access "
                                               f"if write operations are not required.",
                                    severity=severity,
                                    category="permissions",
                                    analyzer=self.name,
                                    location=CodeLocation(
                                        file=relative_path,
                                        line_start=i,
                                        line_end=i,
                                        snippet=self._get_code_snippet(file_path, i)
                                    ),
                                    remediation=Remediation(
                                        description=self._get_cloud_readonly_remediation(cloud_type)
                                    ),
                                    confidence="high" if is_write_op else "medium",
                                    metadata={"cloud_provider": cloud_type}
                                ))
                                break
        
        return findings
    
    def _generate_summary_finding(self, findings: List[Finding], server_info: MCPServerInfo) -> List[Finding]:
        """Generate a summary finding about read-only mode status."""
        summary_findings = []
        
        # Count write-related findings
        write_findings = [
            f for f in findings 
            if any(kw in f.title.lower() for kw in ['write', 'insert', 'update', 'delete', 'drop', 'create'])
        ]
        
        db_findings = [f for f in findings if f.metadata.get('database_type')]
        
        if write_findings or db_findings:
            summary_findings.append(Finding(
                title="⚠️ MCP Server has WRITE ACCESS capabilities",
                description=f"This MCP server has {len(write_findings)} write-capable operations and "
                           f"{len(db_findings)} database connections that may allow data modification. "
                           f"For read-only deployments, remove write operations and enforce read-only "
                           f"mode at the database/storage level.",
                severity="high",
                category="permissions",
                analyzer=self.name,
                remediation=Remediation(
                    description="To enforce read-only mode:\n"
                               "1. Remove all tools with write operations\n"
                               "2. Set readOnlyHint=True on all remaining tools\n"
                               "3. Use read-only database connections\n"
                               "4. Configure IAM/RBAC to deny write permissions"
                ),
                confidence="high",
                metadata={
                    "write_operations_count": len(write_findings),
                    "database_connections_count": len(db_findings)
                }
            ))
        
        return summary_findings
    
    def _get_db_readonly_remediation(self, db_type: str) -> str:
        """Get database-specific read-only remediation advice."""
        remediations = {
            "postgresql": "Use connection options to set default_transaction_read_only=on, "
                         "or connect to a read replica.",
            "mysql": "Set read_only=True in connection parameters or use "
                    "init_command='SET SESSION TRANSACTION READ ONLY'.",
            "mongodb": "Use readPreference='secondaryPreferred' and connect to a read replica. "
                      "Consider using a user with only read permissions.",
            "sqlite": "Open database in read-only mode with '?mode=ro' URI parameter.",
            "redis": "Connect to a read replica with readonly=True parameter.",
            "elasticsearch": "Use a user/role with only read permissions (e.g., read_only cluster privilege).",
        }
        return remediations.get(db_type, "Configure the database connection for read-only access.")
    
    def _get_db_readonly_example(self, db_type: str) -> str:
        """Get database-specific read-only code example."""
        examples = {
            "postgresql": '''# PostgreSQL read-only connection
import psycopg2

conn = psycopg2.connect(
    host="localhost",
    database="mydb",
    user="readonly_user",
    options="-c default_transaction_read_only=on"
)

# Or use a read replica URL
DATABASE_URL = "postgresql://user:pass@read-replica:5432/db"''',

            "mysql": '''# MySQL read-only connection
import mysql.connector

conn = mysql.connector.connect(
    host="localhost",
    database="mydb",
    user="readonly_user",
    init_command="SET SESSION TRANSACTION READ ONLY"
)''',

            "sqlite": '''# SQLite read-only connection
import sqlite3

# Method 1: URI with mode=ro
conn = sqlite3.connect("file:mydb.db?mode=ro", uri=True)

# Method 2: Check connection mode
conn = sqlite3.connect("mydb.db")
conn.execute("PRAGMA query_only = ON")''',

            "mongodb": '''# MongoDB read-only connection
from pymongo import MongoClient, ReadPreference

# Connect to read replica with secondary preference
client = MongoClient(
    "mongodb://readonly_user:pass@host:27017/db",
    readPreference=ReadPreference.SECONDARY_PREFERRED
)''',
        }
        return examples.get(db_type, "# Configure read-only mode for your database")
    
    def _get_cloud_readonly_remediation(self, cloud_type: str) -> str:
        """Get cloud-specific read-only remediation advice."""
        remediations = {
            "aws_s3": "Use IAM policy with only s3:GetObject, s3:ListBucket permissions. "
                     "Deny s3:PutObject, s3:DeleteObject actions.",
            "gcs": "Assign Storage Object Viewer role instead of Storage Object Admin. "
                  "Use IAM Conditions to restrict to specific buckets.",
            "azure_blob": "Assign Storage Blob Data Reader role instead of Contributor. "
                         "Use RBAC to restrict write permissions.",
        }
        return remediations.get(cloud_type, "Configure IAM/RBAC to enforce read-only access.")
