"""
Static Application Security Testing (SAST) Analyzer

Orchestrates comprehensive static code analysis using Claude's semantic
understanding combined with pattern-based vulnerability detection.
"""

import os
import re
import json
import hashlib
from pathlib import Path
from typing import List, Dict, Optional, Set
from dataclasses import dataclass, asdict
from enum import Enum
import anthropic


class Severity(Enum):
    """Vulnerability severity levels"""
    CRITICAL = "CRITICAL"
    HIGH = "HIGH"
    MEDIUM = "MEDIUM"
    LOW = "LOW"
    INFO = "INFO"


class VulnerabilityCategory(Enum):
    """OWASP Top 10 and common vulnerability categories"""
    INJECTION = "injection_attacks"
    BROKEN_AUTH = "authentication_authorization"
    DATA_EXPOSURE = "data_exposure"
    XXE = "xml_external_entities"
    BROKEN_ACCESS_CONTROL = "broken_access_control"
    SECURITY_MISCONFIGURATION = "security_misconfiguration"
    XSS = "cross_site_scripting"
    INSECURE_DESERIALIZATION = "insecure_deserialization"
    VULNERABLE_COMPONENTS = "vulnerable_components"
    INSUFFICIENT_LOGGING = "insufficient_logging"
    CRYPTOGRAPHIC_ISSUES = "cryptographic_issues"
    BUSINESS_LOGIC = "business_logic_flaws"
    API_SECURITY = "api_security"
    CODE_EXECUTION = "code_execution"


@dataclass
class Finding:
    """Represents a security vulnerability finding"""
    id: str
    severity: str
    category: str
    cwe: Optional[str]
    owasp: Optional[str]
    file: str
    line: Optional[int]
    code_snippet: str
    description: str
    impact: str
    remediation: str
    references: List[str]
    cvss_score: Optional[float]
    confidence: str
    
    def to_dict(self):
        """Convert to dictionary"""
        return asdict(self)
    
    def fingerprint(self) -> str:
        """Generate unique fingerprint for deduplication"""
        key = f"{self.file}:{self.line}:{self.category}:{self.code_snippet}"
        return hashlib.md5(key.encode()).hexdigest()


class SASTAnalyzer:
    """
    Orchestrates static application security testing using Claude's
    semantic code understanding combined with pattern-based detection.
    """
    
    def __init__(
        self,
        model: str = "claude-opus-4-1-20250805",
        timeout_minutes: int = 20,
        api_key: Optional[str] = None
    ):
        """
        Initialize SAST analyzer.
        
        Args:
            model: Claude model to use
            timeout_minutes: Max analysis time
            api_key: Anthropic API key (defaults to env var)
        """
        self.model = model
        self.timeout = timeout_minutes
        self.client = anthropic.Anthropic(api_key=api_key)
        
        self.vulnerability_categories = [
            VulnerabilityCategory.INJECTION,
            VulnerabilityCategory.BROKEN_AUTH,
            VulnerabilityCategory.DATA_EXPOSURE,
            VulnerabilityCategory.BROKEN_ACCESS_CONTROL,
            VulnerabilityCategory.SECURITY_MISCONFIGURATION,
            VulnerabilityCategory.XSS,
            VulnerabilityCategory.CRYPTOGRAPHIC_ISSUES,
            VulnerabilityCategory.API_SECURITY,
            VulnerabilityCategory.CODE_EXECUTION
        ]
        
        # Load instruction templates
        self.instructions = self._load_instructions()
        
        # Load false positive filters
        self.fp_filters = self._load_false_positive_filters()
    
    def analyze_codebase(
        self,
        repo_path: str,
        scope: str = "changed_files",
        changed_files: Optional[List[str]] = None,
        custom_policies: Optional[Dict] = None
    ) -> List[Finding]:
        """
        Performs comprehensive SAST analysis.
        
        Args:
            repo_path: Path to repository
            scope: "changed_files" (PR) or "full_codebase"
            changed_files: List of changed files (for PR analysis)
            custom_policies: Custom security policies
        
        Returns:
            List of Finding objects with severity, remediation guidance
        """
        print(f"[SAST] Starting analysis: {scope} in {repo_path}")
        
        # 1. Collect files to analyze
        files_to_analyze = self._collect_files(repo_path, scope, changed_files)
        print(f"[SAST] Analyzing {len(files_to_analyze)} files")
        
        # 2. Analyze each file
        all_findings = []
        for file_path in files_to_analyze:
            findings = self._analyze_file(file_path, custom_policies)
            all_findings.extend(findings)
        
        # 3. Apply false positive filters
        filtered_findings = self.apply_false_positive_filters(all_findings)
        print(f"[SAST] Found {len(all_findings)} issues, {len(filtered_findings)} after filtering")
        
        # 4. Assign CVSS scores
        scored_findings = self._assign_cvss_scores(filtered_findings)
        
        # 5. Sort by severity
        sorted_findings = self._sort_by_severity(scored_findings)
        
        return sorted_findings
    
    def _collect_files(
        self,
        repo_path: str,
        scope: str,
        changed_files: Optional[List[str]]
    ) -> List[str]:
        """Collect files for analysis based on scope"""
        
        if scope == "changed_files" and changed_files:
            # Only analyze changed files
            return [
                os.path.join(repo_path, f) for f in changed_files
                if self._should_analyze_file(f)
            ]
        
        # Full codebase scan
        files = []
        for root, dirs, filenames in os.walk(repo_path):
            # Skip common excluded directories
            dirs[:] = [d for d in dirs if d not in [
                '.git', 'node_modules', 'venv', '__pycache__',
                'build', 'dist', '.next', 'vendor'
            ]]
            
            for filename in filenames:
                file_path = os.path.join(root, filename)
                if self._should_analyze_file(file_path):
                    files.append(file_path)
        
        return files
    
    def _should_analyze_file(self, file_path: str) -> bool:
        """Determine if file should be analyzed"""
        
        # File extensions to analyze
        analyzable_extensions = {
            '.py', '.js', '.jsx', '.ts', '.tsx',
            '.java', '.go', '.rb', '.php',
            '.yml', '.yaml', '.json', '.xml',
            '.tf', '.dockerfile', 'Dockerfile',
            '.sql', '.sh', '.bash'
        }
        
        file_path_lower = file_path.lower()
        
        # Check extension
        if any(file_path_lower.endswith(ext) for ext in analyzable_extensions):
            return True
        
        # Check filename
        if any(name in file_path for name in ['Dockerfile', 'docker-compose']):
            return True
        
        return False
    
    def _analyze_file(
        self,
        file_path: str,
        custom_policies: Optional[Dict]
    ) -> List[Finding]:
        """Analyze a single file for vulnerabilities"""
        
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                code_content = f.read()
        except Exception as e:
            print(f"[SAST] Error reading {file_path}: {e}")
            return []
        
        # Skip empty files
        if not code_content.strip():
            return []
        
        # Build analysis prompt
        prompt = self._build_analysis_prompt(
            file_path,
            code_content,
            custom_policies
        )
        
        # Call Claude for semantic analysis
        try:
            findings_json = self._call_claude_analysis(prompt, code_content)
            findings = self._parse_findings(findings_json, file_path)
            return findings
        except Exception as e:
            print(f"[SAST] Error analyzing {file_path}: {e}")
            return []
    
    def _build_analysis_prompt(
        self,
        file_path: str,
        code_content: str,
        custom_policies: Optional[Dict]
    ) -> str:
        """Build analysis prompt for Claude"""
        
        file_type = self._detect_file_type(file_path)
        
        prompt = f"""You are a senior security engineer performing a comprehensive security code review.

FILE: {file_path}
TYPE: {file_type}

INSTRUCTIONS:
{self.instructions['security-review']}

Analyze the code for security vulnerabilities in these categories:
- SQL/NoSQL Injection
- Cross-Site Scripting (XSS)
- Authentication/Authorization flaws
- Sensitive data exposure
- Cryptographic weaknesses
- Access control issues
- Security misconfigurations
- API security issues
- Command injection
- Insecure deserialization

For each finding, provide:
1. Severity (CRITICAL/HIGH/MEDIUM/LOW/INFO)
2. Category (e.g., "SQL Injection")
3. CWE ID
4. OWASP mapping
5. Line number
6. Code snippet (exact vulnerable code)
7. Description of vulnerability
8. Impact (what attacker can achieve)
9. Remediation (how to fix with code example)
10. References (relevant links)
11. Confidence (HIGH/MEDIUM/LOW)

OUTPUT FORMAT: JSON array of findings
"""
        
        if custom_policies:
            prompt += f"\n\nCUSTOM POLICIES:\n{json.dumps(custom_policies, indent=2)}"
        
        return prompt
    
    def _call_claude_analysis(self, prompt: str, code_content: str) -> str:
        """Call Claude API for code analysis"""
        
        message = self.client.messages.create(
            model=self.model,
            max_tokens=4096,
            temperature=0,  # Deterministic for security analysis
            system=prompt,
            messages=[{
                "role": "user",
                "content": f"Analyze this code for security vulnerabilities:\n\n```\n{code_content}\n```"
            }]
        )
        
        return message.content[0].text
    
    def _parse_findings(self, claude_response: str, file_path: str) -> List[Finding]:
        """Parse Claude's response into Finding objects"""
        
        findings = []
        
        try:
            # Extract JSON from response
            json_match = re.search(r'\[.*\]', claude_response, re.DOTALL)
            if not json_match:
                return findings
            
            findings_data = json.loads(json_match.group(0))
            
            for idx, item in enumerate(findings_data):
                finding = Finding(
                    id=f"FIND-{hashlib.md5(f'{file_path}{idx}'.encode()).hexdigest()[:8].upper()}",
                    severity=item.get('severity', 'MEDIUM'),
                    category=item.get('category', 'Unknown'),
                    cwe=item.get('cwe'),
                    owasp=item.get('owasp'),
                    file=file_path,
                    line=item.get('line'),
                    code_snippet=item.get('code_snippet', ''),
                    description=item.get('description', ''),
                    impact=item.get('impact', ''),
                    remediation=item.get('remediation', ''),
                    references=item.get('references', []),
                    cvss_score=item.get('cvss_score'),
                    confidence=item.get('confidence', 'MEDIUM')
                )
                findings.append(finding)
        
        except json.JSONDecodeError as e:
            print(f"[SAST] Failed to parse Claude response as JSON: {e}")
        
        return findings
    
    def apply_false_positive_filters(self, findings: List[Finding]) -> List[Finding]:
        """
        Reduces noise through semantic filtering.
        Removes low-impact findings, known exceptions.
        """
        
        filtered = []
        
        for finding in findings:
            # Skip if low confidence
            if finding.confidence == "LOW":
                continue
            
            # Check against known false positive patterns
            if self._is_false_positive(finding):
                continue
            
            # Filter test files (lower priority)
            if self._is_test_file(finding.file):
                if finding.severity in ["LOW", "INFO"]:
                    continue
            
            filtered.append(finding)
        
        return filtered
    
    def _is_false_positive(self, finding: Finding) -> bool:
        """Check if finding matches false positive patterns"""
        
        for pattern in self.fp_filters:
            if re.search(pattern, finding.code_snippet, re.IGNORECASE):
                return True
        
        # Framework-specific protections
        if "Django" in finding.file or "django" in finding.code_snippet:
            # Django ORM auto-escapes
            if finding.category == "SQL Injection" and ".objects." in finding.code_snippet:
                return True
        
        if "Flask" in finding.file or "flask" in finding.code_snippet:
            # Flask templates auto-escape
            if finding.category == "XSS" and "render_template" in finding.code_snippet:
                return True
        
        return False
    
    def _is_test_file(self, file_path: str) -> bool:
        """Check if file is a test file"""
        test_indicators = ['test_', '_test.', '/tests/', '/test/', 'spec.', '.spec.']
        return any(indicator in file_path.lower() for indicator in test_indicators)
    
    def _assign_cvss_scores(self, findings: List[Finding]) -> List[Finding]:
        """Assign CVSS v3.1 scores to findings"""
        
        severity_scores = {
            "CRITICAL": 9.5,
            "HIGH": 7.5,
            "MEDIUM": 5.0,
            "LOW": 3.0,
            "INFO": 0.0
        }
        
        for finding in findings:
            if not finding.cvss_score:
                finding.cvss_score = severity_scores.get(finding.severity, 5.0)
        
        return findings
    
    def _sort_by_severity(self, findings: List[Finding]) -> List[Finding]:
        """Sort findings by severity"""
        
        severity_order = {
            "CRITICAL": 0,
            "HIGH": 1,
            "MEDIUM": 2,
            "LOW": 3,
            "INFO": 4
        }
        
        return sorted(
            findings,
            key=lambda f: (severity_order.get(f.severity, 5), -f.cvss_score or 0)
        )
    
    def _load_instructions(self) -> Dict[str, str]:
        """Load instruction files"""
        
        instructions_dir = Path(__file__).parent.parent / "instructions"
        instructions = {}
        
        try:
            with open(instructions_dir / "security-review.md", 'r') as f:
                instructions['security-review'] = f.read()
        except Exception as e:
            print(f"[SAST] Warning: Could not load instructions: {e}")
            instructions['security-review'] = "Perform security code review."
        
        return instructions
    
    def _load_false_positive_filters(self) -> List[str]:
        """Load false positive filter patterns"""
        
        filters_file = Path(__file__).parent.parent / "resources" / "false_positive_filters.txt"
        
        try:
            with open(filters_file, 'r') as f:
                return [line.strip() for line in f if line.strip() and not line.startswith('#')]
        except Exception:
            # Default filters
            return [
                r'# nosec',  # Bandit ignore comment
                r'# skipcq',  # CodeQL ignore
                r'# type: ignore',
                r'# noqa',
            ]
    
    def _detect_file_type(self, file_path: str) -> str:
        """Detect programming language/file type"""
        
        ext_map = {
            '.py': 'Python',
            '.js': 'JavaScript',
            '.jsx': 'React/JSX',
            '.ts': 'TypeScript',
            '.tsx': 'TypeScript/React',
            '.java': 'Java',
            '.go': 'Go',
            '.rb': 'Ruby',
            '.php': 'PHP',
            '.yml': 'YAML',
            '.yaml': 'YAML',
            '.json': 'JSON',
            '.tf': 'Terraform',
            '.sql': 'SQL'
        }
        
        for ext, lang in ext_map.items():
            if file_path.endswith(ext):
                return lang
        
        if 'Dockerfile' in file_path:
            return 'Docker'
        
        return 'Unknown'
    
    def export_findings(self, findings: List[Finding], output_path: str, format: str = "json"):
        """Export findings to file"""
        
        if format == "json":
            with open(output_path, 'w') as f:
                json.dump([f.to_dict() for f in findings], f, indent=2)
        
        elif format == "csv":
            import csv
            with open(output_path, 'w', newline='') as f:
                if findings:
                    writer = csv.DictWriter(f, fieldnames=findings[0].to_dict().keys())
                    writer.writeheader()
                    for finding in findings:
                        writer.writerow(finding.to_dict())
        
        print(f"[SAST] Exported {len(findings)} findings to {output_path}")

