from fastapi import APIRouter, HTTPException, Depends, UploadFile, File
from pydantic import BaseModel, Field, field_validator, ValidationError
from typing import List, Any, Dict
import asyncio
from concurrent.futures import ThreadPoolExecutor
from sqlmodel import select
from app.database import SessionDep
from app.models import ScanRecord, ScanRecordRead, ScanRecordResponse, ScanType
import tempfile
import os
import sys
import json
from pathlib import Path
import logging
import time
import re
import ipaddress
from collections import defaultdict
from sqlalchemy.exc import OperationalError

logger = logging.getLogger(__name__)

from app.utils.audit_logger import log_audit_event


# --- Input Validation Utilities ---

_DOMAIN_PATTERN = re.compile(
    r'^(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})*\.[A-Za-z]{2,}$'
)

_BLOCKED_NETWORKS = [
    ipaddress.ip_network('127.0.0.0/8'),
    ipaddress.ip_network('10.0.0.0/8'),
    ipaddress.ip_network('172.16.0.0/12'),
    ipaddress.ip_network('192.168.0.0/16'),
    ipaddress.ip_network('169.254.0.0/16'),
    ipaddress.ip_network('::1/128'),
    ipaddress.ip_network('fc00::/7'),
]


def validate_domain_or_host(value: str) -> str:
    """Validate a domain/hostname is safe for network scanning."""
    value = value.strip().lower()
    if not value:
        raise ValueError("Domain/host cannot be empty")
    if len(value) > 253:
        raise ValueError("Domain name too long (max 253 characters)")
    # Reject protocol prefixes
    for prefix in ('http://', 'https://', 'ftp://', 'file://'):
        if value.startswith(prefix):
            raise ValueError(f"Provide a hostname, not a URL (remove {prefix})")
    # Check if it is an IP address and block private/internal ranges
    try:
        addr = ipaddress.ip_address(value)
        for network in _BLOCKED_NETWORKS:
            if addr in network:
                raise ValueError(f"Scanning private/internal addresses is not allowed: {value}")
        return value
    except ValueError as e:
        if "not allowed" in str(e):
            raise
        pass  # Not an IP, continue with domain validation
    # Validate as a domain name
    if not _DOMAIN_PATTERN.match(value):
        raise ValueError(f"Invalid domain name: {value}")
    return value


def sanitize_filename(filename: str) -> str:
    """Sanitize an uploaded filename for safe use in temp file suffix."""
    if not filename:
        return "upload"
    name = os.path.basename(filename)
    name = re.sub(r'[\x00-\x1f\x7f]', '', name)
    name = re.sub(r'[^\w.\-]', '_', name)
    if len(name) > 100:
        name = name[:100]
    return name or "upload"


# --- Rate Limiting ---

class SimpleRateLimiter:
    """In-process rate limiter. Not intended for distributed systems."""
    def __init__(self, max_calls: int, period_seconds: int):
        self.max_calls = max_calls
        self.period = period_seconds
        self._calls: dict[str, list[float]] = defaultdict(list)

    def check(self, key: str = "global") -> bool:
        """Returns True if the call is allowed, False if rate-limited."""
        now = time.monotonic()
        calls = self._calls[key]
        self._calls[key] = [t for t in calls if now - t < self.period]
        if len(self._calls[key]) >= self.max_calls:
            return False
        self._calls[key].append(now)
        return True


_scan_limiter = SimpleRateLimiter(max_calls=10, period_seconds=60)
_binary_scan_limiter = SimpleRateLimiter(max_calls=5, period_seconds=60)

MAX_BINARY_UPLOAD_BYTES = 500 * 1024 * 1024  # 500 MB
MAX_CERT_UPLOAD_BYTES = 1 * 1024 * 1024       # 1 MB


def _humanize_connection_error(error: str, domain: str) -> str:
    """Convert low-level socket/SSL error messages into user-friendly strings."""
    e = error.lower()
    if any(k in e for k in ("getaddrinfo", "name or service not known", "nodename nor servname", "name resolution")):
        return f"Domain not found: '{domain}' — please check the domain name and try again."
    if "timed out" in e or "timeout" in e:
        return f"Connection timed out reaching '{domain}' — the host may be unreachable or blocking port 443."
    if "connection refused" in e:
        return f"Connection refused by '{domain}' — the host does not appear to have HTTPS on this port."
    if "failed to retrieve certificate" in e or "no certificate" in e:
        return f"Could not retrieve a TLS certificate from '{domain}' — the host may not support HTTPS."
    return f"Could not connect to '{domain}': {error}"


# Add project root to path for rivet_binary_analyzer imports
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
    # PyInstaller build: modules are in _internal directory
    project_root = str(Path(sys._MEIPASS))
    sys.path.insert(0, project_root)
    sys.path.insert(0, str(Path(sys._MEIPASS) / 'rivet_netcert_analyzer' / 'src'))
else:
    # Development: modules are in parent project directory
    project_root = str(Path(__file__).parent.parent.parent.parent.resolve())
    sys.path.insert(0, str(Path(project_root) / 'rivet_netcert_analyzer' / 'src'))
    if project_root not in sys.path:
        sys.path.insert(0, project_root)
from certificate_library import get_certificate_library

router = APIRouter()


from app.auth import get_current_user
from app.models import User
from app.services.license_limits import enforce_scan_limits
from app.services.license_validator import LicenseValidator

# Dependency injection for certificate library
def get_cert_library():
    return get_certificate_library()


# Dependency injection for binary analyzer
def get_binary_analyzer():
    """Get binary analyzer"""
    try:
        from rivet_binary_analyzer.core.binary_scanner import BinaryScanner
        return BinaryScanner()
    except ImportError as e:
        raise HTTPException(
            status_code=500,
            detail=f"Binary analyzer not available: {str(e)}"
        )


# Thread pool for running sync operations
executor = ThreadPoolExecutor(max_workers=4)


async def run_in_thread(func, *args):
    """Run synchronous function in thread pool"""
    loop = asyncio.get_running_loop()
    return await loop.run_in_executor(executor, func, *args)


class DomainScanRequest(BaseModel):
    # TODO: Deprecated - will be refactored to single domain only
    domains: List[str] = Field(..., description="Domain(s) to scan")
    port: int = Field(default=443, description="Port to scan")

    @field_validator("domains", mode="before")
    @classmethod
    def normalize_domains(cls, v):
        """Convert single string to list"""
        if isinstance(v, str):
            return [v]
        return v

    @field_validator("domains")
    @classmethod
    def validate_single_domain(cls, v):
        """Validate only single domain is provided (multiple domains deprecated)"""
        if len(v) > 1:
            raise ValueError(
                "Multiple domains not supported. Please scan one domain at a time."
            )
        return [validate_domain_or_host(d) for d in v]

    @field_validator("port")
    @classmethod
    def validate_port(cls, v):
        if v < 1 or v > 65535:
            raise ValueError(f"Invalid port: {v}. Port must be between 1 and 65535.")
        return v


class QuantumScanRequest(BaseModel):
    domain: str = Field(..., description="Domain to analyze for quantum security")
    port: int = Field(default=443, description="Port to scan")
    business_criticality: str = Field(default="standard", description="Business criticality level: low, standard, high, critical")
    compliance_requirements: List[str] = Field(default_factory=list, description="Compliance frameworks: fips_140, fedramp, hipaa, pci_dss, gdpr, sox, common_criteria, itar")

    @field_validator("domain")
    @classmethod
    def validate_domain(cls, v):
        """Validate domain is safe for scanning"""
        return validate_domain_or_host(v)

    @field_validator("port")
    @classmethod
    def validate_port(cls, v):
        if v < 1 or v > 65535:
            raise ValueError(f"Invalid port: {v}. Port must be between 1 and 65535.")
        return v

    @field_validator("business_criticality")
    @classmethod
    def validate_business_criticality(cls, v):
        allowed = {"low", "standard", "high", "critical"}
        if v.lower() not in allowed:
            raise ValueError(f"Invalid business_criticality: {v}. Must be one of: {', '.join(sorted(allowed))}")
        return v.lower()

    @field_validator("compliance_requirements")
    @classmethod
    def validate_compliance_requirements(cls, v):
        allowed = {"fips_140", "fedramp", "hipaa", "pci_dss", "gdpr", "sox", "common_criteria", "itar"}
        for req in v:
            if req.lower() not in allowed:
                raise ValueError(f"Invalid compliance requirement: {req}. Must be one of: {', '.join(sorted(allowed))}")
        return [r.lower() for r in v]


class PortScanRequest(BaseModel):
    target: str = Field(..., description="Target to scan")
    ports: List[int] = Field(
        default=[443, 465, 587, 636, 993, 995, 5671, 8443], description="List of ports to scan"
    )
    mode: str = Field(
        default="combined", description="Scan mode: 'port', 'cert', or 'combined'"
    )

    @field_validator("target")
    @classmethod
    def validate_target(cls, v):
        """Validate target is safe for scanning"""
        return validate_domain_or_host(v)

    @field_validator("ports")
    @classmethod
    def validate_ports(cls, v):
        """Validate ports are in valid range (1-65535)"""
        if len(v) > 100:
            raise ValueError("Too many ports. Maximum 100 ports per scan.")
        invalid = [p for p in v if p < 1 or p > 65535]
        if invalid:
            raise ValueError(
                f"Invalid port numbers: {invalid}. Ports must be between 1 and 65535."
            )
        return v


class BinaryScanRequest(BaseModel):
    analysis_type: str = Field(default="crypto", description="Type of analysis: 'crypto', 'full', 'decompile'")
    analysis_depth: str = Field(default="standard", description="Analysis depth: 'quick', 'standard', 'deep'")
    business_criticality: str = Field(default="medium", description="Business criticality: 'low', 'medium', 'high', 'critical'")
    compliance_requirements: List[str] = Field(default=[], description="Compliance frameworks: 'fedramp', 'hipaa', 'pci', 'sox'")
    analysis_backend: str = Field(default="auto", description="Backend: 'auto' (Ghidra if available), 'lief', or 'ghidra'")

    @field_validator("analysis_type")
    @classmethod
    def validate_analysis_type(cls, v):
        """Validate analysis type"""
        valid_types = ["crypto", "full", "decompile"]
        if v not in valid_types:
            raise ValueError(f"Invalid analysis type: {v}. Must be one of: {valid_types}")
        return v

    @field_validator("analysis_depth")
    @classmethod
    def validate_analysis_depth(cls, v):
        """Validate analysis depth"""
        valid_depths = ["quick", "standard", "deep"]
        if v not in valid_depths:
            raise ValueError(f"Invalid analysis depth: {v}. Must be one of: {valid_depths}")
        return v

    @field_validator("business_criticality")
    @classmethod
    def validate_business_criticality(cls, v):
        """Validate business criticality level"""
        valid_levels = ["low", "medium", "high", "critical"]
        if v not in valid_levels:
            raise ValueError(f"Invalid business criticality: {v}. Must be one of: {valid_levels}")
        return v

    @field_validator("analysis_backend")
    @classmethod
    def validate_analysis_backend(cls, v):
        """Validate analysis backend"""
        valid_backends = ["auto", "lief", "ghidra"]
        if v not in valid_backends:
            raise ValueError(f"Invalid analysis backend: {v}. Must be one of: {valid_backends}")
        return v


# Response models
class ScanResponse(BaseModel):
    success: bool
    data: Dict[str, Any]
    message: str | None = None
    scan_id: str | None = None




def save_scan_to_db(
    session: SessionDep, target: str, scan_type: ScanType, scan_data: Dict[str, Any], user_id: str
) -> str:
    """Save a scan result to the database

    Args:
        session: Database session
        target: Target domain/IP that was scanned
        scan_type: Type of scan performed
        scan_data: Raw scan results as JSON
        user_id: User who created this scan (from JWT token)
    """
    scan_record = ScanRecord(target=target, scan_type=scan_type, scan_data=scan_data, user_id=user_id)
    session.add(scan_record)
    session.commit()
    session.refresh(scan_record)
    return scan_record.id


@router.post("/scan/domains", response_model=ScanResponse)
async def scan_domains(
    request: DomainScanRequest,
    session: SessionDep,
    user: User = Depends(get_current_user),
    library=Depends(get_cert_library)
):
    """Scan one or multiple domains' certificates"""
    if not _scan_limiter.check(key=user.id):
        raise HTTPException(status_code=429, detail="Rate limit exceeded. Please wait before scanning again.")

    # License tier limit check
    license_info = LicenseValidator.validate()
    enforce_scan_limits(session, user.id, license_info.get("license_type", "trial"))

    try:
        # Run sync operation in thread pool to avoid blocking event loop
        _t0 = time.perf_counter()
        logger.info("Certificate scan started", extra={"domains": request.domains, "port": request.port})
        result = await run_in_thread(
            library.scan_domains_sync, request.domains, request.port
        )
        logger.info(
            "Certificate scan completed",
            extra={"domains": request.domains, "duration_ms": round((time.perf_counter() - _t0) * 1000)}
        )
        response_data = ScanResponse(
            success=True, data=result, message="Domain scan completed successfully"
        )

        # Save to database (using first domain since multiple domains deprecated)
        save_scan_to_db(
            session, request.domains[0], ScanType.DOMAIN, response_data.dict(), user_id=user.id
        )
        log_audit_event("cert_scan", user_id=user.id, details={"domains": request.domains, "port": request.port})

        return response_data
    except ValidationError as e:
        raise HTTPException(status_code=422, detail=f"Validation error: {str(e)}")
    except ValueError as e:
        raise HTTPException(status_code=400, detail=f"Invalid input: {str(e)}")
    except ConnectionError as e:
        raise HTTPException(status_code=503, detail=f"Connection failed: {str(e)}")
    except TimeoutError as e:
        raise HTTPException(status_code=504, detail=f"Scan timeout: {str(e)}")
    except (AttributeError, ImportError) as e:
        logger.exception("Domain scan internal error")
        raise HTTPException(status_code=500, detail="Analysis service temporarily unavailable")


@router.post("/scan/quantum", response_model=ScanResponse)
async def quantum_analysis(
    request: QuantumScanRequest,
    session: SessionDep,
    user: User = Depends(get_current_user),
    library=Depends(get_cert_library)
):
    """Perform quantum security analysis"""
    if not _scan_limiter.check(key=user.id):
        raise HTTPException(status_code=429, detail="Rate limit exceeded. Please wait before scanning again.")

    # License tier limit check
    license_info = LicenseValidator.validate()
    enforce_scan_limits(session, user.id, license_info.get("license_type", "trial"))

    try:
        # Run sync operation in thread pool to avoid blocking event loop
        result = await run_in_thread(
            library.quantum_assess_sync, request.domain, request.port,
            request.business_criticality, request.compliance_requirements
        )

        # Surface connectivity/DNS failures as proper HTTP errors (don't save to DB)
        if not result.get('success', True):
            raw_err = result.get('error', 'Analysis failed')
            raise HTTPException(status_code=422, detail=_humanize_connection_error(raw_err, request.domain))
        cert_err = (result.get('certificate_analysis') or {}).get('error_message', '')
        if cert_err:
            raise HTTPException(status_code=422, detail=_humanize_connection_error(cert_err, request.domain))

        response_data = ScanResponse(
            success=True, data=result, message="Quantum analysis completed successfully"
        )

        # Save to database
        scan_id = save_scan_to_db(session, request.domain, ScanType.QUANTUM, response_data.dict(), user_id=user.id)
        response_data.scan_id = scan_id
        log_audit_event("quantum_scan", user_id=user.id, details={"domain": request.domain, "port": request.port})

        return response_data
    except ValidationError as e:
        raise HTTPException(status_code=422, detail=f"Validation error: {str(e)}")
    except ValueError as e:
        raise HTTPException(status_code=400, detail=f"Invalid input: {str(e)}")
    except ConnectionError as e:
        raise HTTPException(status_code=503, detail=f"Connection failed: {str(e)}")
    except TimeoutError as e:
        raise HTTPException(status_code=504, detail=f"Analysis timeout: {str(e)}")
    except (AttributeError, ImportError) as e:
        logger.exception("Quantum analysis internal error")
        raise HTTPException(status_code=500, detail="Analysis service temporarily unavailable")


@router.post("/scan/ports", response_model=ScanResponse)
async def port_scan(
    request: PortScanRequest,
    session: SessionDep,
    user: User = Depends(get_current_user),
    library=Depends(get_cert_library)
):
    """Scan ports with optional TLS certificate analysis"""
    if not _scan_limiter.check(key=user.id):
        raise HTTPException(status_code=429, detail="Rate limit exceeded. Please wait before scanning again.")

    # License tier limit check
    license_info = LicenseValidator.validate()
    enforce_scan_limits(session, user.id, license_info.get("license_type", "trial"))

    try:
        # Run sync operation in thread pool to avoid blocking event loop
        result = await run_in_thread(
            library.scan_ports_sync, request.target, request.ports, request.mode
        )

        # Detect DNS failure: domain doesn't exist
        _DNS_KEYWORDS = ('getaddrinfo', 'name or service not known', 'nodename nor servname', 'name resolution')
        # Case 1: top-level exception caught by scan_ports_sync. Previously this
        # only raised for DNS-shaped errors, so ANY other internal failure (a
        # real bug, a permissions error, an unusual OSError) fell through
        # silently to the success response below -- the frontend only checks
        # HTTP status, never the nested result.success, so a fully failed scan
        # was reported to the user as "Port scan completed successfully".
        if not result.get('success', True):
            top_err = str(result.get('error', 'Port scan failed'))
            if any(k in top_err.lower() for k in _DNS_KEYWORDS):
                raise HTTPException(
                    status_code=422,
                    detail=_humanize_connection_error(top_err, request.target)
                )
            raise HTTPException(status_code=422, detail=top_err)
        # Case 2: all individual port results have DNS errors
        raw_results = (result.get('data') or {}).get('rawResults', {})
        closed_ports = raw_results.get('closed_ports', []) if isinstance(raw_results, dict) else []
        if closed_ports and len(closed_ports) == len(request.ports):
            dns_errors = [p for p in closed_ports if any(k in str(p.get('error', '')).lower() for k in _DNS_KEYWORDS)]
            if len(dns_errors) == len(closed_ports):
                raise HTTPException(
                    status_code=422,
                    detail=_humanize_connection_error('getaddrinfo failed', request.target)
                )

        response_data = ScanResponse(
            success=True, data=result, message="Port scan completed successfully"
        )

        # Save to database
        scan_id = save_scan_to_db(session, request.target, ScanType.PORT, response_data.dict(), user_id=user.id)
        response_data.scan_id = scan_id
        log_audit_event("port_scan", user_id=user.id, details={"target": request.target, "ports": request.ports})

        return response_data
    except ValidationError as e:
        raise HTTPException(status_code=422, detail=f"Validation error: {str(e)}")
    except ValueError as e:
        raise HTTPException(status_code=400, detail=f"Invalid input: {str(e)}")
    except ConnectionError as e:
        raise HTTPException(status_code=503, detail=f"Connection failed: {str(e)}")
    except TimeoutError as e:
        raise HTTPException(status_code=504, detail=f"Scan timeout: {str(e)}")
    except (AttributeError, ImportError) as e:
        logger.exception("Port scan internal error")
        raise HTTPException(status_code=500, detail="Analysis service temporarily unavailable")


@router.get("/scan/history", response_model=ScanRecordResponse)
async def get_scan_history(
    session: SessionDep,
    user: User = Depends(get_current_user),
    scan_type: str = None,
    target: str = None,
    target_prefix: str = None,
    project_path: str = None,
    limit: int = 1000,
    offset: int = 0,
):
    """Get scan history from database, optionally filtered by scan type/target/project_path with pagination

    Args:
        scan_type: Filter by scan type (e.g., 'code_analysis')
        target: Exact match filter for target path (e.g., '/path/to/file.ts')
        target_prefix: Prefix match filter for target path (e.g., '/path/to/project/')
        project_path: Exact match filter for project path (scans initiated from this directory)
        limit: Maximum number of results to return
        offset: Number of results to skip for pagination
    """
    try:
        # No hard cap — desktop app, single user
        # Build query with user isolation filter
        statement = select(ScanRecord).where(ScanRecord.user_id == user.id)
        if scan_type:
            statement = statement.where(ScanRecord.scan_type == scan_type)
        if project_path:
            # Match scans from this directory OR any subdirectory beneath it.
            # Uses separator-aware prefix to avoid "qc" matching "qc - Copy".
            # Also includes legacy records where project_path was not set (NULL).
            # Case-insensitive comparison handles mixed drive-letter casing on Windows.
            normalized = project_path.rstrip('/\\')
            from sqlalchemy import or_, func
            statement = statement.where(
                or_(
                    ScanRecord.project_path.is_(None),  # Legacy records without project context
                    func.lower(ScanRecord.project_path) == normalized.lower(),
                    func.lower(ScanRecord.project_path) == project_path.lower(),
                    ScanRecord.project_path.ilike(f"{normalized}\\%"),
                    ScanRecord.project_path.ilike(f"{normalized}/%"),
                )
            )
        elif target:
            statement = statement.where(ScanRecord.target == target)
        elif target_prefix:  # Only apply prefix if exact target not specified
            statement = statement.where(ScanRecord.target.like(f"{target_prefix}%"))
        statement = (
            statement.order_by(ScanRecord.timestamp.desc()).offset(offset).limit(limit)
        )
        scan_records = session.exec(statement).all()

        # Convert to response format, slimming down large scan_data payloads.
        # Only the LATEST record gets full scan_data (enriched_findings, paths, etc).
        # Older records get metadata + summary counts only — the frontend lazy-loads
        # full data on demand via GET /scan/{id}.
        record_items = []
        for idx, scan in enumerate(scan_records):
            item = ScanRecordRead.model_validate(scan)
            sd = item.scan_data
            if isinstance(sd, dict):
                inner = sd.get('data')
                if isinstance(inner, dict):
                    # Promote scanned file list to top-level paths if missing
                    if not sd.get('paths') or not sd['paths'].get('scanned'):
                        inner_scanned = inner.get('paths', {}).get('scanned', [])
                        if inner_scanned:
                            sd.setdefault('paths', {})['scanned'] = inner_scanned
                    # Only strip large raw data for code scans
                    if scan.scan_type == 'code_analysis':
                        inner.pop('results', None)
                        inner.pop('paths', None)

                # Only apply slimming for code scans
                if idx > 0 and scan.scan_type == 'code_analysis':
                    findings = sd.get('enriched_findings', [])
                    scanned = sd.get('paths', {}).get('scanned', [])
                    # Count unique files that have at least one finding
                    affected_files = set()
                    if isinstance(findings, list):
                        for f in findings:
                            fp = f.get('path') if isinstance(f, dict) else None
                            if fp:
                                affected_files.add(fp)
                    sd['_summary'] = {
                        'findings_count': len(findings) if isinstance(findings, list) else 0,
                        'scanned_count': len(scanned) if isinstance(scanned, list) else 0,
                        'files_with_issues': len(affected_files),
                    }
                    sd.pop('enriched_findings', None)
                    sd.pop('data', None)
                    sd.pop('paths', None)
            record_items.append(item)

        # Debug logging (after validation to avoid errors)
        logger.info(f"Scan history query returned {len(scan_records)} total records for project: {project_path}")
        try:
            if scan_records:
                # Count unique parent_directory_scan IDs
                parent_ids = set()
                for record in scan_records:
                    if isinstance(record.scan_data, dict):
                        parent_id = record.scan_data.get('parent_directory_scan')
                        if parent_id:
                            parent_ids.add(parent_id)
                        # DEBUG: Check finding structure in first record
                        if record.scan_data.get('enriched_findings'):
                            findings = record.scan_data['enriched_findings']
                            if findings:
                                sample = findings[0]
                                logger.info(f"[DEBUG] API returning finding: start={sample.get('start')}, start_line={sample.get('start_line')} (normalized), end_line={sample.get('end_line')}, path={sample.get('path')}")
                                break
                logger.info(f"  Contains {len(parent_ids)} unique directory scan sessions (parent_directory_scan IDs)")
        except Exception as debug_error:
            logger.warning(f"Debug logging failed (non-critical): {debug_error}")

        return ScanRecordResponse(
            success=True,
            data=record_items,
            message="Scan history retrieved successfully",
        )
    except Exception as e:
        logger.exception(f"Failed to load scan history for project {project_path}: {str(e)}")
        raise HTTPException(
            status_code=500, detail=f"Failed to load scan history: {str(e)}"
        )


@router.get("/scan/detail/{scan_id}")
async def get_scan_detail(
    scan_id: str,
    session: SessionDep,
    user: User = Depends(get_current_user),
):
    """Get full scan data for a single scan record by ID.
    Used for lazy-loading when the user clicks a history entry."""
    try:
        record = session.get(ScanRecord, scan_id)
        if not record:
            raise HTTPException(status_code=404, detail="Scan not found")
        item = ScanRecordRead.model_validate(record)
        # Apply same promotions as history endpoint
        sd = item.scan_data
        if isinstance(sd, dict):
            inner = sd.get('data')
            if isinstance(inner, dict):
                if not sd.get('paths') or not sd['paths'].get('scanned'):
                    inner_scanned = inner.get('paths', {}).get('scanned', [])
                    if inner_scanned:
                        sd.setdefault('paths', {})['scanned'] = inner_scanned
                inner.pop('results', None)
                inner.pop('paths', None)
        return {"success": True, "data": item}
    except HTTPException:
        raise
    except Exception as e:
        logger.exception(f"Failed to load scan detail {scan_id}: {str(e)}")
        raise HTTPException(status_code=500, detail=f"Failed to load scan detail: {str(e)}")


@router.get("/scan/{scan_id}/clean-files")
async def get_scan_clean_files(
    scan_id: str,
    session: SessionDep,
    user: User = Depends(get_current_user),
):
    """Get list of clean files for a scan (lazy-loaded on demand).
    
    This endpoint returns only the files that were scanned but had no findings.
    Used to reduce the size of the main scan_data JSON and improve performance.
    
    Returns:
        {
            "success": true,
            "clean_files": ["path/to/file1.py", "path/to/file2.ts"],
            "count": 2
        }
    """
    try:
        from app.models import ScanFile
        
        # Verify scan exists
        record = session.get(ScanRecord, scan_id)
        if not record:
            raise HTTPException(status_code=404, detail="Scan not found")
        
        # Check user access only if user is authenticated
        if user and record.user_id != user.id:
            raise HTTPException(status_code=403, detail="Access denied")
        
        # Fetch clean files from separate table
        statement = select(ScanFile).where(
            ScanFile.scan_id == scan_id,
            ScanFile.has_findings == False
        )
        clean_file_records = session.exec(statement).all()
        
        clean_files = [record.file_path for record in clean_file_records]
        
        return {
            "success": True,
            "clean_files": clean_files,
            "count": len(clean_files)
        }
    except HTTPException:
        raise
    except Exception as e:
        logger.exception(f"Failed to load clean files for scan {scan_id}: {str(e)}")
        raise HTTPException(status_code=500, detail=f"Failed to load clean files: {str(e)}")


@router.get("/scan/export/{scan_id}")
async def export_scan(
    scan_id: str,
    format: str = "json",
    session: SessionDep = None,
    user: User = Depends(get_current_user),
):
    """Export a scan result in the specified format (json, csv, html).

    Args:
        scan_id: UUID of the scan to export
        format: Export format - 'json', 'csv', or 'html'
    """
    from fastapi.responses import Response as RawResponse
    from app.utils.scan_export import export_scan_json, export_scan_csv, export_scan_html, export_scan_sarif
    from app.services.license_validator import LicenseValidator
    from app.config.pricing import is_feature_enabled
    from app.services.license_limits import get_user_tier

    record = session.get(ScanRecord, scan_id)
    if not record:
        raise HTTPException(status_code=404, detail="Scan not found")

    # License gate for enterprise-only formats
    if format in ("csv", "json", "sarif"):
        license_info = LicenseValidator.validate()
        tier = get_user_tier(license_info.get("license_type", "trial"))
        feature_map = {"csv": "csv_export", "json": "json_export", "sarif": "sarif_export"}
        if not is_feature_enabled(tier, feature_map[format]):
            raise HTTPException(status_code=403, detail=f"{format.upper()} export requires an Enterprise license.")

    scan_data = record.scan_data or {}
    scan_type = record.scan_type.value if hasattr(record.scan_type, 'value') else str(record.scan_type)

    if format == "json":
        content = export_scan_json(scan_data)
        return RawResponse(
            content=content,
            media_type="application/json",
            headers={"Content-Disposition": f'attachment; filename="scan_{scan_id}.json"'},
        )
    elif format == "csv":
        content = export_scan_csv(scan_data, scan_type)
        return RawResponse(
            content=content,
            media_type="text/csv",
            headers={"Content-Disposition": f'attachment; filename="scan_{scan_id}.csv"'},
        )
    elif format == "html":
        content = export_scan_html(scan_data, scan_type, record.target)
        return RawResponse(
            content=content,
            media_type="text/html",
            headers={"Content-Disposition": f'attachment; filename="scan_{scan_id}.html"'},
        )
    elif format == "sarif":
        content = export_scan_sarif(scan_data, scan_type, record.target)
        return RawResponse(
            content=content,
            media_type="application/json",
            headers={"Content-Disposition": f'attachment; filename="scan_{scan_id}.sarif"'},
        )
    else:
        raise HTTPException(status_code=400, detail=f"Unsupported format: {format}. Use 'json', 'csv', 'html', or 'sarif'.")


@router.get("/scan/projects")
@router.get("/analyze/projects")
async def get_scanned_projects(
    session: SessionDep,
    user: User = Depends(get_current_user),
    scan_type: str = "code_analysis",
):
    """Get unique scanned projects with aggregated data"""
    try:
        from sqlalchemy import func, distinct

        # Single query: get project paths, timestamps, and file counts
        statement = (
            select(
                ScanRecord.project_path,
                func.max(ScanRecord.timestamp).label("last_timestamp"),
                func.count(distinct(ScanRecord.target)).label("files_scanned"),
            )
            .where(
                ScanRecord.scan_type == scan_type,
                ScanRecord.project_path.isnot(None),
            )
            .group_by(ScanRecord.project_path)
            .order_by(func.max(ScanRecord.timestamp).desc())
        )

        results = session.exec(statement).all()

        # For each project, get severity counts from the LATEST scan only
        # (avoids loading every historical scan's JSON blob)
        projects = []
        for project_path, last_timestamp, files_scanned in results:
            total_high = 0
            total_medium = 0
            total_low = 0

            # Fetch only the latest scan's data for this project
            latest_scan_stmt = (
                select(ScanRecord.scan_data)
                .where(
                    ScanRecord.project_path == project_path,
                    ScanRecord.scan_type == scan_type,
                )
                .order_by(ScanRecord.timestamp.desc())
                .limit(1)
            )
            latest_scan_data = session.exec(latest_scan_stmt).first()

            if latest_scan_data:
                scan_data = latest_scan_data if isinstance(latest_scan_data, dict) else {}
                enriched = scan_data.get("enriched_findings", [])

                for finding in enriched:
                    severity = (finding.get("extra", {}).get("severity") or finding.get("severity") or "INFO").upper()
                    if severity in ("ERROR", "HIGH", "CRITICAL"):
                        total_high += 1
                    elif severity in ("WARNING", "MEDIUM"):
                        total_medium += 1
                    else:
                        total_low += 1

            project_name = project_path.rstrip('/').split('/')[-1] or project_path

            projects.append({
                "path": project_path,
                "name": project_name,
                "lastTimestamp": last_timestamp.isoformat(),
                "findings": {
                    "high": total_high,
                    "medium": total_medium,
                    "low": total_low,
                    "total": total_high + total_medium + total_low,
                },
                "filesScanned": files_scanned,
            })

        return {"success": True, "data": projects}

    except Exception as e:
        raise HTTPException(
            status_code=500, detail=f"Failed to load projects: {str(e)}"
        )


@router.get("/scan/stats")
async def get_scan_stats(
    session: SessionDep,
    user: User = Depends(get_current_user),
    scan_type: str = None,
    target_prefix: str = None,
):
    """Get aggregated scan statistics for a directory

    Args:
        scan_type: Filter by scan type (e.g., 'code_analysis')
        target_prefix: Prefix match filter for target path (e.g., '/path/to/project/')

    Returns:
        {
            "success": true,
            "data": {
                "total_scans": 10,
                "files_scanned": 10,
                "files_with_issues": 5,
                "total_vulnerabilities": 23,
                "last_scan_timestamp": "2025-10-15T12:00:00",
                "scan_success_rate": 100.0
            }
        }
    """
    try:
        from sqlalchemy import func, cast, Integer
        from sqlalchemy.sql import literal_column

        # Use SQL aggregation instead of loading all records into Python
        base_filter = [ScanRecord.user_id == user.id]
        if scan_type:
            base_filter.append(ScanRecord.scan_type == scan_type)
        if target_prefix:
            base_filter.append(ScanRecord.target.like(f"{target_prefix}%"))

        statement = select(
            func.count(ScanRecord.id).label("total_scans"),
            func.max(ScanRecord.timestamp).label("last_scan_timestamp"),
            func.count(ScanRecord.scan_data).label("successful_scans"),
        ).where(*base_filter)

        result = session.exec(statement).first()
        total_scans = result.total_scans if result else 0
        last_scan_timestamp = result.last_scan_timestamp.isoformat() if result and result.last_scan_timestamp else None
        successful_scans = result.successful_scans if result else 0

        # For JSON-dependent stats (findings_count), use a lightweight query
        # that fetches only the scan_data column for records with findings
        files_with_issues = 0
        total_vulnerabilities = 0
        if total_scans > 0:
            for (scan_data,) in session.exec(
                select(ScanRecord.scan_data).where(*base_filter)
            ):
                if scan_data:
                    # SQLite may return raw JSON string instead of dict
                    if isinstance(scan_data, str):
                        try:
                            scan_data = json.loads(scan_data)
                        except (json.JSONDecodeError, TypeError):
                            continue
                    
                    # Handle different scan types
                    if isinstance(scan_data, dict):
                        count = 0
                        
                        # For code scans
                        if 'findings_count' in scan_data:
                            count = scan_data.get('findings_count', 0)
                        
                        # For binary scans - check yara_analysis matches
                        elif 'data' in scan_data:
                            data = scan_data.get('data', {})
                            if isinstance(data, dict):
                                yara_analysis = data.get('yara_analysis', {})
                                if isinstance(yara_analysis, dict):
                                    count = yara_analysis.get('matches_found', 0)
                        
                        if count > 0:
                            files_with_issues += 1
                            total_vulnerabilities += count

        scan_success_rate = (successful_scans / total_scans * 100.0) if total_scans > 0 else 0.0

        return {
            "success": True,
            "data": {
                "total_scans": total_scans,
                "files_scanned": total_scans,
                "files_with_issues": files_with_issues,
                "total_vulnerabilities": total_vulnerabilities,
                "last_scan_timestamp": last_scan_timestamp,
                "scan_success_rate": round(scan_success_rate, 1),
            },
            "message": "Scan statistics retrieved successfully"
        }
    except Exception as e:
        raise HTTPException(
            status_code=500, detail=f"Failed to get scan statistics: {str(e)}"
        )


# Delete endpoints for scan history management
# IMPORTANT: Specific routes (/scan/bulk, /scan/all) must come BEFORE generic route (/scan/{scan_id})

@router.delete("/scan/bulk")
async def delete_scans_bulk(
    scan_type: str = None,
    project_path: str = None,
    before_date: str = None,
    older_than_days: int = None,
    session: SessionDep = None,
    user: User = Depends(get_current_user),
):
    """Delete multiple scans based on filters

    Query Parameters:
        scan_type: Filter by scan type (code_analysis, certificate, binary, etc.)
        project_path: Delete all scans for a specific project
        before_date: Delete scans before this ISO date (e.g., 2024-01-01)
        older_than_days: Delete scans older than N days

    Returns:
        Success message with count of deleted scans

    Note:
        - At least one filter must be provided
        - Only deletes scans owned by the current user
        - Filters are combined with AND logic
    """
    from datetime import datetime, timedelta, timezone

    try:
        # Build the WHERE clause
        statement = select(ScanRecord)

        # Require at least one filter
        if not any([scan_type, project_path, before_date, older_than_days]):
            raise HTTPException(
                status_code=400,
                detail="At least one filter parameter is required (scan_type, project_path, before_date, or older_than_days)"
            )

        # Apply filters
        if scan_type:
            try:
                scan_type_enum = ScanType(scan_type)
                statement = statement.where(ScanRecord.scan_type == scan_type_enum)
            except ValueError:
                raise HTTPException(
                    status_code=400,
                    detail=f"Invalid scan_type: {scan_type}. Valid types: {[t.value for t in ScanType]}"
                )

        if project_path:
            statement = statement.where(ScanRecord.project_path == project_path)

        if before_date:
            try:
                cutoff_date = datetime.fromisoformat(before_date.replace('Z', '+00:00'))
                statement = statement.where(ScanRecord.timestamp < cutoff_date)
            except ValueError:
                raise HTTPException(
                    status_code=400,
                    detail=f"Invalid date format: {before_date}. Use ISO format (e.g., 2024-01-01T00:00:00Z)"
                )

        if older_than_days:
            if older_than_days < 1:
                raise HTTPException(
                    status_code=400,
                    detail="older_than_days must be at least 1"
                )
            cutoff_date = datetime.now(timezone.utc) - timedelta(days=older_than_days)
            statement = statement.where(ScanRecord.timestamp < cutoff_date)

        # Get matching scans
        scans = session.exec(statement).all()
        count = len(scans)

        if count == 0:
            return {
                "success": True,
                "message": "No scans matched the deletion criteria",
                "data": {"deleted_count": 0}
            }

        # Delete all matching scans
        for scan in scans:
            # If user is not None, only delete scans owned by user
            if user is not None and hasattr(user, 'id') and scan.user_id != user.id:
                continue
            session.delete(scan)

        session.commit()
        log_audit_event("delete_scans_bulk", user_id=user.id, details={
            "deleted_count": count,
            "filters": {"scan_type": scan_type, "project_path": project_path,
                        "before_date": before_date, "older_than_days": older_than_days},
        })

        return {
            "success": True,
            "message": f"Successfully deleted {count} scan(s)",
            "data": {
                "deleted_count": count,
                "filters": {
                    "scan_type": scan_type,
                    "project_path": project_path,
                    "before_date": before_date,
                    "older_than_days": older_than_days
                }
            }
        }

    except HTTPException:
        raise
    except Exception as e:
        session.rollback()
        raise HTTPException(
            status_code=500,
            detail=f"Failed to delete scans: {str(e)}"
        )


@router.delete("/scan/all")
async def delete_all_scans(
    confirm: bool = False,
    session: SessionDep = None,
    user: User = Depends(get_current_user),
):
    """Delete ALL scan history for the current user

    Query Parameters:
        confirm: Must be set to true to confirm deletion

    Returns:
        Success message with count of deleted scans

    Warning:
        This operation is irreversible and will delete all scan history
        for the current user.
    """
    try:
        if not confirm:
            raise HTTPException(
                status_code=400,
                detail="You must set confirm=true to delete all scans. This operation is irreversible."
            )

        # Get all scans
        statement = select(ScanRecord)
        scans = session.exec(statement).all()
        count = len(scans)

        if count == 0:
            return {
                "success": True,
                "message": "No scan history to delete",
                "data": {"deleted_count": 0}
            }

        # Delete all scans
        for scan in scans:
            session.delete(scan)

        session.commit()

        log_audit_event("delete_all_scans", user_id=user.id, details={"deleted_count": count})

        return {
            "success": True,
            "message": f"Successfully deleted all {count} scan record(s)",
            "data": {"deleted_count": count}
        }

    except HTTPException:
        raise
    except Exception as e:
        session.rollback()
        raise HTTPException(
            status_code=500,
            detail=f"Failed to delete all scans: {str(e)}"
        )


@router.delete("/scan/{scan_id}")
async def delete_scan(
    scan_id: str,
    session: SessionDep = None,
    user: User = Depends(get_current_user),
):
    """Delete a single scan record by ID

    Args:
        scan_id: UUID of the scan to delete

    Returns:
        Success message with deleted scan ID

    Raises:
        404: Scan not found
        403: User doesn't own this scan
    """
    import logging
    logger = logging.getLogger(__name__)

    try:
        # Log incoming request details
        logger.info(f"DELETE /scan/{scan_id} - User: {getattr(user, 'email', None)} (ID: {getattr(user, 'id', None)})")

        # Find the scan record
        statement = select(ScanRecord).where(
            ScanRecord.id == scan_id,
        )
        logger.info(f"Executing query: SELECT * FROM scan_record WHERE id='{scan_id}'")

        scan = session.exec(statement).first()
        logger.info(f"Query result: {'Found scan' if scan else 'No scan found'}")

        if not scan:
            # Check if scan exists but belongs to another user
            check_statement = select(ScanRecord).where(ScanRecord.id == scan_id)
            exists = session.exec(check_statement).first()

            if exists:
                # If user is None, allow deletion (legacy/admin mode)
                if user is not None and hasattr(user, 'id') and exists.user_id != user.id:
                    logger.warning(f"Scan {scan_id} exists but belongs to user {exists.user_id}, not {user.id}")
                    raise HTTPException(
                        status_code=403,
                        detail="You don't have permission to delete this scan"
                    )
                # else: allow
            else:
                logger.warning(f"Scan {scan_id} not found in database")
                raise HTTPException(
                    status_code=404,
                    detail=f"Scan not found: {scan_id}"
                )

        # Delete the scan with retry logic for SQLite locks
        logger.info(f"Deleting scan {scan_id}")

        max_retries = 5
        retry_delay = 0.1  # Start with 100ms

        for attempt in range(max_retries):
            try:
                session.delete(scan)
                session.commit()
                logger.info(f"Successfully deleted scan {scan_id}")
                log_audit_event("delete_scan", user_id=user.id, details={"scan_id": scan_id})
                break
            except OperationalError as e:
                if "database is locked" in str(e) and attempt < max_retries - 1:
                    session.rollback()
                    logger.warning(f"Database locked on attempt {attempt + 1}/{max_retries}, retrying in {retry_delay}s...")
                    time.sleep(retry_delay)
                    retry_delay *= 2  # Exponential backoff

                    # Re-fetch the scan in case session was invalidated
                    statement = select(ScanRecord).where(
                        ScanRecord.id == scan_id,
                    )
                    scan = session.exec(statement).first()
                    if not scan:
                        raise HTTPException(status_code=404, detail="Scan not found after retry")
                else:
                    raise

        return {
            "success": True,
            "message": f"Analysis deleted successfully",
            "data": {"deleted_id": scan_id}
        }

    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error deleting scan {scan_id}: {str(e)}", exc_info=True)
        session.rollback()
        raise HTTPException(
            status_code=500,
            detail="Failed to delete scan"
        )


@router.post("/scan/certificate-file", response_model=ScanResponse)
async def certificate_file_scan(
    file: UploadFile = File(...),
    session: SessionDep = None,
    user: User = Depends(get_current_user),
    library=Depends(get_cert_library)
):
    """Analyze an uploaded certificate file (PEM or DER) for quantum vulnerabilities"""
    if not _scan_limiter.check(key=user.id):
        raise HTTPException(status_code=429, detail="Rate limit exceeded.")

    license_info = LicenseValidator.validate()
    if session:
        enforce_scan_limits(session, user.id, license_info.get("license_type", "trial"))

    if not file.filename:
        raise HTTPException(status_code=400, detail="No file provided")

    allowed_exts = {'.pem', '.crt', '.cer', '.der', '.p7b', '.p7c', '.pfx', '.p12'}
    ext = os.path.splitext(file.filename)[1].lower()
    if ext not in allowed_exts:
        raise HTTPException(status_code=400, detail=f"Unsupported file type: {ext}")

    if file.size and file.size > MAX_CERT_UPLOAD_BYTES:
        raise HTTPException(status_code=413, detail="File too large. Maximum certificate size is 1 MB.")

    try:
        file_bytes = await file.read()
        if len(file_bytes) > MAX_CERT_UPLOAD_BYTES:
            raise HTTPException(status_code=413, detail="File too large. Maximum certificate size is 1 MB.")
        result = await run_in_thread(library.scan_certificate_file_sync, file_bytes, file.filename)
        response_data = ScanResponse(success=True, data=result, message="Certificate file analysis completed")
        if session:
            scan_id = save_scan_to_db(session, file.filename, ScanType.QUANTUM, response_data.dict(), user_id=user.id)
            response_data.scan_id = scan_id
        log_audit_event("cert_file_scan", user_id=user.id, details={"filename": file.filename})
        return response_data
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Certificate analysis failed: {str(e)}")


@router.post("/scan/binary", response_model=ScanResponse)
async def binary_scan(
    file: UploadFile = File(...),
    analysis_type: str = "crypto",
    analysis_depth: str = "standard",
    business_criticality: str = "medium",
    compliance_requirements: str = "",
    analysis_backend: str = "auto",
    session: SessionDep = None,
    user: User = Depends(get_current_user),
    analyzer=Depends(get_binary_analyzer)
):
    """Scan binary file for quantum vulnerabilities"""
    if not _binary_scan_limiter.check(key=user.id):
        raise HTTPException(status_code=429, detail="Rate limit exceeded. Please wait before scanning again.")

    # License tier limit check
    license_info = LicenseValidator.validate()
    if session:
        enforce_scan_limits(session, user.id, license_info.get("license_type", "trial"))

    # Validate file
    if not file.filename:
        raise HTTPException(status_code=400, detail="No file provided")

    # Validate analysis_depth
    if analysis_depth not in ("quick", "standard", "deep"):
        analysis_depth = "standard"

    # Validate analysis_backend
    if analysis_backend not in ("auto", "lief", "ghidra"):
        analysis_backend = "auto"

    # Parse compliance requirements
    compliance_list = [req.strip() for req in compliance_requirements.split(",") if req.strip()]

    if file.size and file.size > MAX_BINARY_UPLOAD_BYTES:
        raise HTTPException(status_code=413, detail="File too large. Maximum binary size is 500 MB.")

    # Create temporary file
    temp_file_path = None
    try:
        # Save uploaded file to temporary location with sanitized filename
        safe_name = sanitize_filename(file.filename)
        with tempfile.NamedTemporaryFile(delete=False, suffix=f"_{safe_name}") as temp_file:
            content = await file.read()
            if len(content) > MAX_BINARY_UPLOAD_BYTES:
                raise HTTPException(status_code=413, detail="File too large. Maximum binary size is 500 MB.")
            temp_file.write(content)
            temp_file_path = temp_file.name

        # Run binary analysis in thread pool
        _t0 = time.perf_counter()
        logger.info(
            "Binary scan started",
            extra={"scan_filename": file.filename, "analysis_type": analysis_type, "depth": analysis_depth, "backend": analysis_backend}
        )
        result = await run_in_thread(
            analyzer.scan_binary,
            temp_file_path,
            analysis_type,
            business_criticality,
            compliance_list,
            analysis_depth,
            analysis_backend
        )
        _duration_ms = round((time.perf_counter() - _t0) * 1000)
        logger.info(
            "Binary scan completed",
            extra={"scan_filename": file.filename, "duration_ms": _duration_ms, "findings": len(result.get("findings", []))}
        )

        # Add original filename and temp path to result for UI display
        result['original_filename'] = file.filename
        result['analyzed_path'] = temp_file_path

        response_data = ScanResponse(
            success=True,
            data=result,
            message="Binary analysis completed successfully"
        )

        # Save to database
        if session:
            scan_id = save_scan_to_db(session, file.filename, ScanType.BINARY, response_data.dict(), user_id=user.id)
            response_data.scan_id = scan_id

        log_audit_event("binary_scan", user_id=user.id, details={
            "filename": file.filename,
            "analysis_type": analysis_type,
            "depth": analysis_depth,
            "findings": len(result.get("findings", [])),
        })

        return response_data

    except ValidationError as e:
        raise HTTPException(status_code=422, detail=f"Validation error: {str(e)}")
    except ValueError as e:
        raise HTTPException(status_code=400, detail=f"Invalid input: {str(e)}")
    except FileNotFoundError as e:
        raise HTTPException(status_code=404, detail="File not found during analysis")
    except Exception as e:
        from app.utils.path_security import sanitize_error_message
        raise HTTPException(status_code=500, detail=f"Binary analysis failed: {sanitize_error_message(e, 'Binary scan')}")
    finally:
        # Clean up temporary file
        if temp_file_path and os.path.exists(temp_file_path):
            try:
                os.unlink(temp_file_path)
            except OSError as e:
                logger.warning(f"Failed to clean up temp file: {e}")


