"""Core scanning logic."""
import hashlib
import json
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Set
from urllib.parse import urlparse

from .patterns import Finding, Severity, scan_content, scan_dependencies
from .ast_analyzer import analyze_python_ast, is_python_file
from .cve_checker import check_dependencies_for_cves
from .js_analyzer import analyze_javascript, is_javascript_file
from .binary_scanner import scan_binary_file, is_binary_file, is_image_file, BINARY_EXTENSIONS, IMAGE_EXTENSIONS
from .base64_detector import detect_base64_payloads
from .obfuscation import analyze_obfuscation
from .js_ast_analyzer import analyze_js_ast


# File extensions to scan (text files)
SCANNABLE_EXTENSIONS = {
    '.py', '.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs',  # Code
    '.sh', '.bash', '.zsh', '.fish',                       # Shell
    '.md', '.markdown', '.txt', '.rst',                    # Docs
    '.html', '.htm', '.xml',                               # Markup
    '.yaml', '.yml', '.json', '.toml', '.ini', '.cfg',     # Config
    '.env', '.env.example', '.env.local',                  # Environment
}

# Binary extensions handled separately
BINARY_SCAN_EXTENSIONS = BINARY_EXTENSIONS | IMAGE_EXTENSIONS

# Dependency files that need special scanning
DEPENDENCY_FILES = {
    'requirements.txt', 'requirements-dev.txt', 'requirements-test.txt',
    'Pipfile', 'pyproject.toml', 'setup.py', 'setup.cfg',
    'package.json', 'package-lock.json', 'yarn.lock',
    'Gemfile', 'Gemfile.lock',
    'go.mod', 'go.sum',
    'Cargo.toml', 'Cargo.lock',
}

# Files to always scan regardless of extension
ALWAYS_SCAN = {'SKILL.md', 'README.md', 'AGENTS.md', 'Dockerfile', 'Makefile'}

# Directories to skip
SKIP_DIRS = {
    'node_modules', '__pycache__', '.git', 'venv', '.venv', 
    'dist', 'build', '.tox', '.pytest_cache', '.mypy_cache',
    'egg-info', '.eggs', 'htmlcov', '.coverage',
}

# Max file size to scan (1MB)
MAX_FILE_SIZE = 1024 * 1024


@dataclass
class ScanResult:
    """Result of scanning a skill."""
    path: str
    findings: List[Finding] = field(default_factory=list)
    files_scanned: int = 0
    files_skipped: int = 0
    errors: List[str] = field(default_factory=list)
    warnings: List[str] = field(default_factory=list)
    file_hashes: Dict[str, str] = field(default_factory=dict)
    symlinks_found: List[str] = field(default_factory=list)
    
    @property
    def trust_level(self) -> str:
        """Determine overall trust level based on findings."""
        danger_count = sum(1 for f in self.findings if f.severity == Severity.DANGER)
        warn_count = sum(1 for f in self.findings if f.severity == Severity.WARN)
        
        if danger_count > 0:
            return "DANGER"
        elif warn_count > 3:
            return "WARN"
        elif warn_count > 0:
            return "CAUTION"
        elif self.symlinks_found:
            return "CAUTION"  # Symlinks are suspicious
        else:
            return "SAFE"
    
    @property
    def exit_code(self) -> int:
        """Exit code for CLI."""
        level = self.trust_level
        if level == "DANGER":
            return 2
        elif level in ("WARN", "CAUTION"):
            return 1
        else:
            return 0
    
    def summary(self) -> dict:
        """Get summary statistics."""
        return {
            "trust_level": self.trust_level,
            "files_scanned": self.files_scanned,
            "files_skipped": self.files_skipped,
            "findings": {
                "danger": sum(1 for f in self.findings if f.severity == Severity.DANGER),
                "warn": sum(1 for f in self.findings if f.severity == Severity.WARN),
                "info": sum(1 for f in self.findings if f.severity == Severity.INFO),
            },
            "errors": len(self.errors),
            "warnings": len(self.warnings),
            "symlinks": len(self.symlinks_found),
        }
    
    @property 
    def content_hash(self) -> str:
        """Get hash of all scanned content for caching."""
        if not self.file_hashes:
            return ""
        combined = "|".join(f"{k}:{v}" for k, v in sorted(self.file_hashes.items()))
        return hashlib.sha256(combined.encode()).hexdigest()[:16]


def should_scan_file(path: Path) -> bool:
    """Determine if a file should be scanned (text files)."""
    if path.name in ALWAYS_SCAN:
        return True
    if path.name in DEPENDENCY_FILES:
        return True
    if path.suffix.lower() in SCANNABLE_EXTENSIONS:
        return True
    return False


def should_scan_binary(path: Path) -> bool:
    """Determine if a binary file should be scanned."""
    return path.suffix.lower() in BINARY_SCAN_EXTENSIONS


def is_dependency_file(path: Path) -> bool:
    """Check if file is a dependency manifest."""
    return path.name in DEPENDENCY_FILES


def check_symlink_safety(path: Path, base_dir: Path) -> tuple[bool, Optional[str]]:
    """
    Check if a symlink is safe (resolves within the skill directory).
    Returns (is_safe, error_message).
    """
    if not path.is_symlink():
        return True, None
    
    try:
        resolved = path.resolve()
        base_resolved = base_dir.resolve()
        
        # Check if resolved path is within base directory
        try:
            resolved.relative_to(base_resolved)
            return True, None
        except ValueError:
            return False, f"Symlink escapes skill directory: {path} -> {resolved}"
    except Exception as e:
        return False, f"Could not resolve symlink {path}: {e}"


def hash_content(content: str) -> str:
    """Hash content for caching."""
    return hashlib.sha256(content.encode()).hexdigest()[:16]


def scan_file(path: Path, base_dir: Path, check_cves: bool = False) -> tuple[List[Finding], Optional[str], Optional[str]]:
    """
    Scan a single file for security issues.
    Returns (findings, error, content_hash).
    """
    try:
        # Check file size
        if path.stat().st_size > MAX_FILE_SIZE:
            return [], f"File too large: {path}", None
        
        # Read content
        try:
            content = path.read_text(encoding='utf-8')
        except UnicodeDecodeError:
            try:
                content = path.read_text(encoding='latin-1')
            except Exception:
                return [], f"Could not decode: {path}", None
        
        # Hash for caching
        content_hash = hash_content(content)
        
        # Scan content with regex patterns
        findings = scan_content(content, str(path))
        
        # AST analysis for Python files (catches evasion that regex misses)
        if is_python_file(str(path)):
            ast_findings = analyze_python_ast(content, str(path))
            findings.extend(ast_findings)
        
        # JavaScript analysis (including minified code handling)
        if is_javascript_file(str(path)):
            js_findings = analyze_javascript(content, str(path))
            findings.extend(js_findings)
            
            # JS AST analysis (catches aliasing, dynamic requires, etc.)
            js_ast_findings = analyze_js_ast(content, str(path))
            findings.extend(js_ast_findings)
        
        # Base64 payload detection (all files)
        b64_findings = detect_base64_payloads(content, str(path))
        findings.extend(b64_findings)
        
        # Obfuscation analysis (code files only)
        obfusc_findings = analyze_obfuscation(content, str(path))
        findings.extend(obfusc_findings)
        
        # Additional scanning for dependency files
        if is_dependency_file(path):
            dep_findings = scan_dependencies(content, str(path))
            findings.extend(dep_findings)
            
            # CVE checking (requires network)
            if check_cves:
                cve_findings = check_dependencies_for_cves(content, str(path))
                findings.extend(cve_findings)
        
        return findings, None, content_hash
        
    except Exception as e:
        return [], f"Error scanning {path}: {e}", None


def scan_directory(path: Path, check_cves: bool = False) -> ScanResult:
    """Scan a skill directory for security issues."""
    result = ScanResult(path=str(path))
    base_dir = path.resolve()
    
    if not path.exists():
        result.errors.append(f"Path does not exist: {path}")
        return result
    
    if not path.is_dir():
        # Single file scan
        if should_scan_file(path):
            # Check symlink safety for single file
            is_safe, symlink_error = check_symlink_safety(path, path.parent)
            if not is_safe:
                result.symlinks_found.append(str(path))
                result.findings.append(Finding(
                    severity=Severity.DANGER,
                    pattern_id='unsafe_symlink',
                    description=symlink_error or 'Symlink to external location',
                    file=str(path),
                    line=0,
                    match=str(path),
                ))
            
            findings, error, content_hash = scan_file(path, path.parent, check_cves=check_cves)
            result.findings.extend(findings)
            result.files_scanned = 1
            if content_hash:
                result.file_hashes[str(path)] = content_hash
            if error:
                result.errors.append(error)
        else:
            result.files_skipped = 1
        return result
    
    # Track seen inodes to detect symlink loops
    seen_inodes: Set[int] = set()
    
    # Walk directory
    for root, dirs, files in os.walk(path, followlinks=False):
        root_path = Path(root)
        
        # Skip hidden directories and common non-code dirs
        dirs[:] = [d for d in dirs if not d.startswith('.') and d not in SKIP_DIRS]
        
        # Check directory symlinks
        for d in list(dirs):
            dir_path = root_path / d
            if dir_path.is_symlink():
                is_safe, symlink_error = check_symlink_safety(dir_path, base_dir)
                if not is_safe:
                    result.symlinks_found.append(str(dir_path))
                    result.findings.append(Finding(
                        severity=Severity.DANGER,
                        pattern_id='unsafe_symlink_dir',
                        description=symlink_error or 'Directory symlink to external location',
                        file=str(dir_path),
                        line=0,
                        match=str(dir_path),
                    ))
                    dirs.remove(d)  # Don't traverse unsafe symlinks
        
        for filename in files:
            filepath = root_path / filename
            
            # Check for symlinks
            if filepath.is_symlink():
                is_safe, symlink_error = check_symlink_safety(filepath, base_dir)
                if not is_safe:
                    result.symlinks_found.append(str(filepath))
                    result.findings.append(Finding(
                        severity=Severity.DANGER,
                        pattern_id='unsafe_symlink',
                        description=symlink_error or 'Symlink to external location',
                        file=str(filepath),
                        line=0,
                        match=str(filepath),
                    ))
                    continue
            
            # Check for inode loops
            try:
                inode = filepath.stat().st_ino
                if inode in seen_inodes:
                    result.warnings.append(f"Duplicate inode (possible hard link): {filepath}")
                    continue
                seen_inodes.add(inode)
            except Exception:
                pass
            
            if should_scan_file(filepath):
                findings, error, content_hash = scan_file(filepath, base_dir, check_cves=check_cves)
                result.findings.extend(findings)
                result.files_scanned += 1
                if content_hash:
                    result.file_hashes[str(filepath)] = content_hash
                if error:
                    result.errors.append(error)
            elif should_scan_binary(filepath):
                # Scan binary files (pyc, images, etc.)
                bin_findings, bin_error = scan_binary_file(filepath)
                result.findings.extend(bin_findings)
                result.files_scanned += 1
                if bin_error:
                    result.errors.append(bin_error)
            else:
                result.files_skipped += 1
    
    return result


def scan_skill(path_or_url: str, check_cves: bool = False) -> ScanResult:
    """Main entry point: scan a skill from path or URL."""
    # Check if it's a URL
    parsed = urlparse(path_or_url)
    if parsed.scheme in ('http', 'https'):
        # TODO: Download and scan
        result = ScanResult(path=path_or_url)
        result.errors.append("URL scanning not yet implemented - download the skill first")
        return result
    
    # Local path
    path = Path(path_or_url).resolve()
    return scan_directory(path, check_cves=check_cves)
