"""
MCP Security Scanner - Main orchestration module.

Coordinates all analyzers and produces comprehensive scan results.
"""

import asyncio
import os
import time
import uuid
from datetime import datetime
from pathlib import Path
from typing import List, Optional, Dict, Any
import subprocess
import tempfile
import shutil

from .config import AuditConfig
from .models import (
    ScanResult, Finding, MCPServerInfo, 
    ToolDefinition, PermissionScope, NetworkEndpoint, Dependency
)


class MCPSecurityScanner:
    """Main scanner that orchestrates security analysis."""
    
    VERSION = "1.0.0"
    
    def __init__(
        self,
        config: Optional[AuditConfig] = None,
        analyzers: Optional[List[str]] = None,
        verbose: bool = False
    ):
        self.config = config or AuditConfig.load()
        self.verbose = verbose
        self.analyzers_to_run = analyzers or list(self.config.analyzers.keys())
        self._temp_dir = None
    
    def _log(self, message: str):
        """Log message if verbose mode is enabled."""
        if self.verbose:
            print(f"  [*] {message}")
    
    async def scan(self, target: str) -> ScanResult:
        """
        Perform a complete security scan of an MCP server.
        
        Args:
            target: Path to MCP server directory or Git URL
            
        Returns:
            ScanResult with all findings
        """
        start_time = time.time()
        scan_id = str(uuid.uuid4())[:8]
        
        # Resolve target path
        target_path = await self._resolve_target(target)
        
        self._log(f"Scanning: {target_path}")
        
        # Detect and parse MCP server
        server_info = await self._parse_mcp_server(target_path)
        
        self._log(f"Detected: {server_info.language} / {server_info.framework or 'unknown framework'}")
        self._log(f"Found {len(server_info.tools)} tools")
        
        # Run analyzers
        all_findings: List[Finding] = []
        analyzers_run = []
        
        # Import analyzers
        from ..analyzers import (
            StaticCodeAnalyzer,
            PermissionAnalyzer,
            NetworkAnalyzer,
            DependencyAnalyzer,
            InjectionPatternAnalyzer,
            ConfigurationAnalyzer,
            SecretsAnalyzer,
            ReadOnlyModeAnalyzer
        )
        
        analyzer_map = {
            "static": StaticCodeAnalyzer,
            "permissions": PermissionAnalyzer,
            "network": NetworkAnalyzer,
            "dependencies": DependencyAnalyzer,
            "injection": InjectionPatternAnalyzer,
            "config": ConfigurationAnalyzer,
            "secrets": SecretsAnalyzer,
            "readonly": ReadOnlyModeAnalyzer
        }
        
        for name in self.analyzers_to_run:
            if name in analyzer_map and self.config.analyzers.get(name, True):
                self._log(f"Running {name} analyzer...")
                analyzer_class = analyzer_map[name]
                analyzer = analyzer_class(self.config)
                
                try:
                    findings = await analyzer.analyze(target_path, server_info)
                    all_findings.extend(findings)
                    analyzers_run.append(name)
                    self._log(f"  Found {len(findings)} issues")
                except Exception as e:
                    if self.verbose:
                        print(f"  [!] {name} analyzer failed: {e}")
        
        # Calculate risk score
        risk_score = self._calculate_risk_score(all_findings, server_info)
        
        # Create scan result
        duration = time.time() - start_time
        
        result = ScanResult(
            scan_id=scan_id,
            scan_date=datetime.utcnow().isoformat(),
            target_path=target_path,
            scanner_version=self.VERSION,
            server_info=server_info,
            findings=all_findings,
            risk_score=risk_score,
            scan_duration_seconds=round(duration, 2),
            analyzers_run=analyzers_run
        )
        
        # Cleanup temp directory if created
        if self._temp_dir and os.path.exists(self._temp_dir):
            shutil.rmtree(self._temp_dir)
        
        return result
    
    async def _resolve_target(self, target: str) -> str:
        """Resolve target to a local path, cloning if necessary."""
        if target.startswith(("http://", "https://", "git@")):
            # Clone from Git
            self._temp_dir = tempfile.mkdtemp(prefix="mcp-audit-")
            self._log(f"Cloning repository to {self._temp_dir}")
            
            try:
                subprocess.run(
                    ["git", "clone", "--depth", "1", target, self._temp_dir],
                    check=True,
                    capture_output=True
                )
            except subprocess.CalledProcessError as e:
                raise ValueError(f"Failed to clone repository: {e.stderr.decode()}")
            
            return self._temp_dir
        
        path = Path(target).resolve()
        if not path.exists():
            raise ValueError(f"Target path does not exist: {target}")
        
        return str(path)
    
    async def _parse_mcp_server(self, path: str) -> MCPServerInfo:
        """Parse and extract information from an MCP server."""
        path = Path(path)
        
        # Detect language
        language = self._detect_language(path)
        
        # Detect framework
        framework = self._detect_framework(path, language)
        
        # Extract server name and version
        name, version = self._extract_metadata(path, language)
        
        # Parse tools
        tools = await self._extract_tools(path, language)
        
        # Parse permissions
        permissions = await self._extract_permissions(path, language)
        
        # Parse dependencies
        dependencies = await self._extract_dependencies(path, language)
        
        # Parse network endpoints
        endpoints = await self._extract_endpoints(path, language)
        
        return MCPServerInfo(
            name=name or path.name,
            path=str(path),
            language=language,
            framework=framework,
            version=version,
            tools=tools,
            permissions=permissions,
            dependencies=dependencies,
            endpoints=endpoints
        )
    
    def _detect_language(self, path: Path) -> str:
        """Detect the programming language of the MCP server."""
        # Check for Python indicators
        if (path / "pyproject.toml").exists() or \
           (path / "setup.py").exists() or \
           (path / "requirements.txt").exists() or \
           list(path.glob("*.py")):
            return "python"
        
        # Check for TypeScript/JavaScript indicators
        if (path / "package.json").exists():
            pkg_path = path / "package.json"
            try:
                import json
                with open(pkg_path) as f:
                    pkg = json.load(f)
                    # Check for TypeScript in devDependencies
                    deps = pkg.get("devDependencies", {})
                    if "typescript" in deps or (path / "tsconfig.json").exists():
                        return "typescript"
            except:
                pass
            return "javascript"
        
        if list(path.glob("*.ts")):
            return "typescript"
        
        if list(path.glob("*.js")):
            return "javascript"
        
        return "unknown"
    
    def _detect_framework(self, path: Path, language: str) -> Optional[str]:
        """Detect the MCP framework being used."""
        if language == "python":
            # Check for FastMCP
            for py_file in path.rglob("*.py"):
                try:
                    content = py_file.read_text()
                    if "fastmcp" in content.lower() or "FastMCP" in content:
                        return "fastmcp"
                    if "from mcp" in content or "import mcp" in content:
                        return "mcp-python-sdk"
                except:
                    pass
        
        elif language in ["typescript", "javascript"]:
            # Check package.json for MCP SDK
            pkg_path = path / "package.json"
            if pkg_path.exists():
                try:
                    import json
                    with open(pkg_path) as f:
                        pkg = json.load(f)
                        deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
                        if "@modelcontextprotocol/sdk" in deps:
                            return "mcp-typescript-sdk"
                except:
                    pass
        
        return None
    
    def _extract_metadata(self, path: Path, language: str) -> tuple:
        """Extract server name and version."""
        name = None
        version = None
        
        if language == "python":
            # Check pyproject.toml
            toml_path = path / "pyproject.toml"
            if toml_path.exists():
                try:
                    import tomllib
                    with open(toml_path, "rb") as f:
                        data = tomllib.load(f)
                        if "project" in data:
                            name = data["project"].get("name")
                            version = data["project"].get("version")
                except:
                    pass
        
        elif language in ["typescript", "javascript"]:
            pkg_path = path / "package.json"
            if pkg_path.exists():
                try:
                    import json
                    with open(pkg_path) as f:
                        pkg = json.load(f)
                        name = pkg.get("name")
                        version = pkg.get("version")
                except:
                    pass
        
        return name, version
    
    async def _extract_tools(self, path: Path, language: str) -> List[ToolDefinition]:
        """Extract tool definitions from the MCP server."""
        tools = []
        
        if language == "python":
            tools = await self._extract_python_tools(path)
        elif language in ["typescript", "javascript"]:
            tools = await self._extract_js_tools(path)
        
        return tools
    
    async def _extract_python_tools(self, path: Path) -> List[ToolDefinition]:
        """Extract tools from Python MCP server."""
        tools = []
        import re
        
        for py_file in path.rglob("*.py"):
            try:
                content = py_file.read_text()
                lines = content.split('\n')
                
                # Pattern for @mcp.tool decorator
                tool_pattern = re.compile(r'@(?:mcp|server)\.tool\s*\(([^)]*)\)')
                func_pattern = re.compile(r'(?:async\s+)?def\s+(\w+)\s*\(')
                
                i = 0
                while i < len(lines):
                    line = lines[i]
                    match = tool_pattern.search(line)
                    if match:
                        decorator_args = match.group(1)
                        
                        # Find the function definition
                        j = i + 1
                        while j < len(lines) and not func_pattern.search(lines[j]):
                            j += 1
                        
                        if j < len(lines):
                            func_match = func_pattern.search(lines[j])
                            if func_match:
                                func_name = func_match.group(1)
                                
                                # Extract name from decorator if present
                                name_match = re.search(r'name\s*=\s*["\']([^"\']+)["\']', decorator_args)
                                tool_name = name_match.group(1) if name_match else func_name
                                
                                # Extract annotations
                                annotations = {}
                                ann_match = re.search(r'annotations\s*=\s*\{([^}]+)\}', decorator_args)
                                if ann_match:
                                    ann_str = ann_match.group(1)
                                    for key in ['readOnlyHint', 'destructiveHint', 'idempotentHint']:
                                        val_match = re.search(rf'{key}\s*:\s*(True|False|true|false)', ann_str)
                                        if val_match:
                                            annotations[key] = val_match.group(1).lower() == 'true'
                                
                                # Extract docstring
                                description = None
                                if j + 1 < len(lines):
                                    doc_line = lines[j + 1].strip()
                                    if doc_line.startswith('"""') or doc_line.startswith("'''"):
                                        # Collect docstring
                                        doc_parts = [doc_line.strip('"\' ')]
                                        k = j + 2
                                        while k < len(lines):
                                            if '"""' in lines[k] or "'''" in lines[k]:
                                                doc_parts.append(lines[k].split('"""')[0].split("'''")[0].strip())
                                                break
                                            doc_parts.append(lines[k].strip())
                                            k += 1
                                        description = ' '.join(doc_parts).strip()
                                
                                tools.append(ToolDefinition(
                                    name=tool_name,
                                    description=description,
                                    annotations=annotations,
                                    file_path=str(py_file.relative_to(path)),
                                    line_number=j + 1
                                ))
                    i += 1
            except Exception as e:
                if self.verbose:
                    print(f"  [!] Error parsing {py_file}: {e}")
        
        return tools
    
    async def _extract_js_tools(self, path: Path) -> List[ToolDefinition]:
        """Extract tools from TypeScript/JavaScript MCP server."""
        tools = []
        import re
        
        for ext in ["*.ts", "*.js", "*.mjs"]:
            for js_file in path.rglob(ext):
                if "node_modules" in str(js_file):
                    continue
                    
                try:
                    content = js_file.read_text()
                    
                    # Pattern for server.tool or registerTool
                    tool_patterns = [
                        re.compile(r'\.tool\s*\(\s*["\']([^"\']+)["\']'),
                        re.compile(r'registerTool\s*\(\s*["\']([^"\']+)["\']'),
                        re.compile(r'name:\s*["\']([^"\']+)["\']')
                    ]
                    
                    for pattern in tool_patterns:
                        for match in pattern.finditer(content):
                            tool_name = match.group(1)
                            line_num = content[:match.start()].count('\n') + 1
                            
                            # Avoid duplicates
                            if not any(t.name == tool_name for t in tools):
                                tools.append(ToolDefinition(
                                    name=tool_name,
                                    file_path=str(js_file.relative_to(path)),
                                    line_number=line_num
                                ))
                except Exception as e:
                    if self.verbose:
                        print(f"  [!] Error parsing {js_file}: {e}")
        
        return tools
    
    async def _extract_permissions(self, path: Path, language: str) -> List[PermissionScope]:
        """Extract permission scopes from the MCP server."""
        permissions = []
        # Permission extraction will be handled by the PermissionAnalyzer
        return permissions
    
    async def _extract_dependencies(self, path: Path, language: str) -> List[Dependency]:
        """Extract dependencies from the MCP server."""
        dependencies = []
        
        if language == "python":
            # Check requirements.txt
            req_path = path / "requirements.txt"
            if req_path.exists():
                try:
                    for line in req_path.read_text().split('\n'):
                        line = line.strip()
                        if line and not line.startswith('#'):
                            # Parse requirement
                            import re
                            match = re.match(r'^([a-zA-Z0-9_-]+)([<>=!]+.+)?', line)
                            if match:
                                name = match.group(1)
                                version = match.group(2).strip('=<>!') if match.group(2) else None
                                dependencies.append(Dependency(
                                    name=name,
                                    version=version,
                                    source="pypi"
                                ))
                except:
                    pass
            
            # Check pyproject.toml
            toml_path = path / "pyproject.toml"
            if toml_path.exists():
                try:
                    import tomllib
                    with open(toml_path, "rb") as f:
                        data = tomllib.load(f)
                        deps = data.get("project", {}).get("dependencies", [])
                        for dep in deps:
                            import re
                            match = re.match(r'^([a-zA-Z0-9_-]+)', dep)
                            if match:
                                name = match.group(1)
                                if not any(d.name == name for d in dependencies):
                                    dependencies.append(Dependency(
                                        name=name,
                                        source="pypi"
                                    ))
                except:
                    pass
        
        elif language in ["typescript", "javascript"]:
            pkg_path = path / "package.json"
            if pkg_path.exists():
                try:
                    import json
                    with open(pkg_path) as f:
                        pkg = json.load(f)
                        for dep_type in ["dependencies", "devDependencies"]:
                            for name, version in pkg.get(dep_type, {}).items():
                                dependencies.append(Dependency(
                                    name=name,
                                    version=version.lstrip('^~'),
                                    source="npm"
                                ))
                except:
                    pass
        
        return dependencies
    
    async def _extract_endpoints(self, path: Path, language: str) -> List[NetworkEndpoint]:
        """Extract network endpoints from the MCP server."""
        endpoints = []
        # Endpoint extraction will be handled by NetworkAnalyzer
        return endpoints
    
    def _calculate_risk_score(self, findings: List[Finding], server_info: MCPServerInfo) -> int:
        """
        Calculate overall risk score (0-100).
        
        Higher score = higher risk.
        """
        score = 0
        
        # Score based on findings
        severity_weights = {
            "critical": 25,
            "high": 15,
            "medium": 8,
            "low": 3,
            "info": 1
        }
        
        for finding in findings:
            weight = severity_weights.get(finding.severity, 0)
            # Adjust based on confidence
            if finding.confidence == "low":
                weight *= 0.5
            elif finding.confidence == "medium":
                weight *= 0.75
            score += weight
        
        # Cap at 100
        score = min(score, 100)
        
        # Bonus risk for dangerous tool annotations
        destructive_tools = sum(1 for t in server_info.tools if t.is_destructive)
        score = min(score + (destructive_tools * 5), 100)
        
        return int(score)
