"""
Vulnerability Management Router

Endpoints for CVE management, prioritization, and AI analysis.
"""
from typing import Optional, List
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status, Query, BackgroundTasks
from sqlalchemy.orm import Session
from sqlalchemy import desc, asc, func
from sqlalchemy.exc import IntegrityError
from pydantic import BaseModel, field_validator
import json
import os
import logging

logger = logging.getLogger(__name__)

from app.database import get_db
from app.models.user import User
from app.models.vulnerability import Vulnerability, VulnerabilitySeverity, VulnerabilityStatus
from app.models.asset import Asset
from app.models.ai_analysis import AIAnalysis
from app.models.audit_log import AuditLog, AuditEventType
from app.auth.dependencies import get_current_user, RequireEditor
from app.integrations.wazuh_client import get_wazuh_client, WazuhClient
from app.models.setting import Setting
from app.integrations.infomaniak_ai_client import InfomaniakAIClient, get_infomaniak_ai_client, AIAnalysisService
from app.models.ai_report import AIReport
from app.services.enrichment_service import (
    enrich_vulnerabilities,
    enrich_vulnerability_by_id,
    enrich_all_open_vulnerabilities,
    fetch_kev_catalog,
    EnrichmentError,
)
from app.services.vuln_override_service import (
    correct_vulnerability_scores,
    VulnOverrideService,
)
from app.services.override_jobs import (
    start_vulnrichment_job,
    get_job as get_override_job,
    list_jobs as list_override_jobs,
)

# Settings key for Nessus configuration
SETTING_KEY = "nessus_config"

router = APIRouter(prefix="/api/v1/vulnerabilities", tags=["Vulnerabilities"])


# ============================================
# Pydantic Schemas
# ============================================

class VulnerabilityResponse(BaseModel):
    id: int
    cve_id: str
    asset_id: int
    asset_hostname: str
    cvss_score: Optional[float]
    severity: VulnerabilitySeverity
    status: VulnerabilityStatus
    title: Optional[str]
    package_name: Optional[str]
    package_version: Optional[str]
    fixed_version: Optional[str]
    exploitable: bool
    exploit_available: bool
    detected_at: datetime
    priority_score: float
    priority_breakdown: Optional[dict] = None
    cpr_score: Optional[float] = None
    epss_score: Optional[float] = None
    epss_percentile: Optional[float] = None
    kev_listed: bool = False
    kev_ransomware_use: bool = False
    kev_date_added: Optional[datetime] = None
    euvd_listed: bool = False
    euvd_critical: bool = False
    euvd_date_added: Optional[datetime] = None
    euvd_id: Optional[str] = None
    enrichment_sources: Optional[str] = None
    enrichment_updated_at: Optional[datetime] = None
    # Multi-scanner attribution (Nessus integration)
    sources: List[str] = []
    cross_confirmed: bool = False
    first_detected_by: Optional[str] = None
    nessus_plugin_id: Optional[str] = None
    nessus_vpr_score: Optional[float] = None
    exploitation_status: Optional[str] = None
    exploitation_source: Optional[str] = None     # vulnrichment | nvd | cvelistv5 | nessus
    ssvc_technical_impact: Optional[str] = None   # partial | total
    ssvc_automatable: Optional[str] = None        # yes | no
    is_pseudo_cve: bool = False
    assigned_user_id: Optional[int] = None
    assigned_user_name: Optional[str] = None
    assigned_group_id: Optional[int] = None
    assigned_group_name: Optional[str] = None
    deferred_until: Optional[datetime] = None
    defer_reason: Optional[str] = None
    notification_suppressed: bool = False
    # Per-package detail (1:N child rows). Empty for pseudo-CVEs.
    packages: List["PackageInfo"] = []
    has_fix_any: bool = False     # at least one package has a real fix

    class Config:
        from_attributes = True


class PackageInfo(BaseModel):
    package_name: str
    package_version: Optional[str] = None
    fixed_version: Optional[str] = None
    source: Optional[str] = None
    has_fix: bool = False

    class Config:
        from_attributes = True


class AssignRequest(BaseModel):
    user_id: Optional[int] = None
    group_id: Optional[int] = None


class DeferRequest(BaseModel):
    deferred_until: datetime
    reason: str


class VulnerabilityDetailResponse(VulnerabilityResponse):
    description: Optional[str]
    cvss_vector: Optional[str]
    exploit_maturity: Optional[str]
    published_date: Optional[datetime]
    patched_at: Optional[datetime]
    references: Optional[str]
    cwe_id: Optional[str]
    ai_analysis: Optional[dict]


# Resolve forward ref — VulnerabilityResponse declares `packages: List["PackageInfo"]`
# above the PackageInfo class itself.
VulnerabilityResponse.model_rebuild()
VulnerabilityDetailResponse.model_rebuild()


class VulnerabilityUpdateRequest(BaseModel):
    status: VulnerabilityStatus
    # `reason` is status-agnostic — captures WHO/HOW for the audit trail
    # regardless of which status the row moves to (patched via WSUS,
    # FP because vendor confirmed N/A, accepted_risk per ITSM ticket).
    # `defer_reason` kept for backward compatibility on existing UI.
    reason: Optional[str] = None
    defer_reason: Optional[str] = None
    deferred_until: Optional[datetime] = None

    @field_validator('status', mode='before')
    @classmethod
    def normalize_status(cls, v):
        if isinstance(v, str):
            return v.lower()
        return v


class BulkVulnerabilityUpdateRequest(BaseModel):
    vulnerability_ids: List[int]
    assigned_user_id: Optional[int] = None
    assigned_group_id: Optional[int] = None
    status: Optional[VulnerabilityStatus] = None

    @field_validator('status', mode='before')
    @classmethod
    def normalize_status(cls, v):
        if isinstance(v, str):
            return v.lower()
        return v


class VulnerabilityFilterParams(BaseModel):
    severity: Optional[VulnerabilitySeverity] = None
    status: Optional[VulnerabilityStatus] = None
    exploitable: Optional[bool] = None
    asset_id: Optional[int] = None
    search: Optional[str] = None  # Suche in CVE-ID, Package-Name, Title


class DashboardStatsResponse(BaseModel):
    total_vulnerabilities: int
    open_vulnerabilities: int
    critical_count: int
    high_count: int
    medium_count: int
    low_count: int
    exploitable_count: int
    avg_cvss_score: float
    affected_assets: int
    total_assets: int
    scanned_assets: int
    severity_history: List[dict]
    oldest_vulnerability_days: int


# ============================================
# Helper-Funktionen
# ============================================

def log_vulnerability_change(
    db: Session,
    user_id: Optional[int],
    vuln_id: int,
    old_status,
    new_status,
    reason: Optional[str] = None,
    cve_id: Optional[str] = None,
    source: Optional[str] = None,
):
    """Audit-Log entry for a vulnerability status change.

    `user_id=None` for automated transitions (Nessus / Wazuh sync
    auto-patching when a CVE disappears from the scan result).
    `reason` is a free-text rationale persisted as JSON in new_value
    alongside the new status — readable by the audit-log UI.
    `source` tags WHERE the change came from
    (`manual` / `nessus_sync` / `wazuh_sync` / `verify_patch_rescan`).
    """
    old_val = old_status.value if hasattr(old_status, "value") else str(old_status)
    new_val = new_status.value if hasattr(new_status, "value") else str(new_status)
    new_payload = json.dumps({
        "status": new_val,
        "reason": reason,
        "source": source or "manual",
        "cve_id": cve_id,
    })
    audit_log = AuditLog(
        user_id=user_id,
        event_type=AuditEventType.VULNERABILITY_UPDATED,
        event_description=(
            f"Vulnerability {cve_id or ''} status changed: {old_val} → {new_val}"
            + (f" ({reason})" if reason else "")
        )[:500],
        resource_type="vulnerability",
        resource_id=str(vuln_id),
        old_value=old_val,
        new_value=new_payload,
        timestamp=datetime.now()
    )
    db.add(audit_log)
    db.commit()


def _build_vuln_response(vuln: Vulnerability) -> dict:
    """Build a vulnerability response dict with assigned user info"""
    breakdown = vuln.calculate_priority_breakdown()
    # Per-package detail (1:N). Cheaper than a JOIN in queries that
    # already loaded the parent — SQLAlchemy's default-lazy load is
    # one extra round-trip per vuln. Fine for detail; list endpoints
    # may want a joinedload upstream later.
    pkgs = getattr(vuln, "packages", []) or []
    packages_payload = [
        {
            "package_name": p.package_name,
            "package_version": p.package_version,
            "fixed_version": p.fixed_version,
            "source": p.source,
            "has_fix": p.has_fix,
        }
        for p in pkgs
    ]
    has_fix_any = any(p.has_fix for p in pkgs) if pkgs else bool(
        vuln.fixed_version and vuln.fixed_version != vuln.package_version
    )
    return {
        "id": vuln.id,
        "cve_id": vuln.cve_id,
        "asset_id": vuln.asset_id,
        "asset_hostname": vuln.asset.hostname if vuln.asset else "Unknown",
        "cvss_score": vuln.cvss_score,
        "severity": vuln.severity,
        "status": vuln.status,
        "title": vuln.title,
        "package_name": vuln.package_name,
        "package_version": vuln.package_version,
        "fixed_version": vuln.fixed_version,
        "exploitable": vuln.exploitable,
        "exploit_available": vuln.exploit_available,
        "detected_at": vuln.detected_at,
        "priority_score": breakdown["total"],
        "priority_breakdown": breakdown,
        "cpr_score": breakdown.get("cpr"),
        "epss_score": vuln.epss_score,
        "epss_percentile": vuln.epss_percentile,
        "kev_listed": vuln.kev_listed,
        "kev_ransomware_use": vuln.kev_ransomware_use,
        "kev_date_added": vuln.kev_date_added,
        "euvd_listed": vuln.euvd_listed,
        "euvd_critical": vuln.euvd_critical,
        "euvd_date_added": vuln.euvd_date_added,
        "euvd_id": vuln.euvd_id,
        "enrichment_sources": vuln.enrichment_sources,
        "enrichment_updated_at": vuln.enrichment_updated_at,
        # Multi-scanner attribution (Nessus integration)
        "sources": vuln.source_list,
        "cross_confirmed": vuln.cross_confirmed,
        "first_detected_by": vuln.first_detected_by,
        "nessus_plugin_id": vuln.nessus_plugin_id,
        "nessus_vpr_score": vuln.nessus_vpr_score,
        # SSVC decision points from CISA Vulnrichment.
        # exploitation_status was previously missing from _build_vuln_response
        # — Pydantic schema declared the field but the dict never set it,
        # so the frontend POC/ACTIVE/WIDESPREAD pill never fired even when
        # the score breakdown showed +2.0 from Exploit (ssvc).
        "exploitation_status": vuln.exploitation_status,
        "exploitation_source": vuln.exploitation_source,
        "ssvc_technical_impact": vuln.ssvc_technical_impact,
        "ssvc_automatable": vuln.ssvc_automatable,
        "is_pseudo_cve": vuln.is_pseudo_cve,
        "assigned_user_id": vuln.assigned_user_id,
        "assigned_user_name": vuln.assigned_user.username if vuln.assigned_user else None,
        "assigned_group_id": vuln.assigned_group_id,
        "assigned_group_name": vuln.group.name if vuln.group else None,
        "deferred_until": vuln.deferred_until,
        "defer_reason": vuln.defer_reason,
        "notification_suppressed": vuln.notification_suppressed,
        "packages": packages_payload,
        "has_fix_any": has_fix_any,
        # Fields for DetailResponse (will be None if not detail)
        "description": getattr(vuln, 'description', None),
        "cvss_vector": getattr(vuln, 'cvss_vector', None),
        "exploit_maturity": getattr(vuln, 'exploit_maturity', None),
        "published_date": getattr(vuln, 'published_date', None),
        "patched_at": getattr(vuln, 'patched_at', None),
        "references": getattr(vuln, 'references', None),
        "cwe_id": getattr(vuln, 'cwe_id', None),
    }


# ============================================
# Endpoints
# ============================================

@router.post("/bulk-update")
async def bulk_update_vulnerabilities(
    update_data: BulkVulnerabilityUpdateRequest,
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor)
):
    """Updates multiple vulnerabilities at once (assignment, status)"""
    vulnerabilities = db.query(Vulnerability).filter(Vulnerability.id.in_(update_data.vulnerability_ids)).all()
    
    if not vulnerabilities:
        raise HTTPException(status_code=404, detail="No vulnerabilities found")

    update_fields = {}
    if update_data.assigned_user_id is not None:
        update_fields["assigned_user_id"] = update_data.assigned_user_id if update_data.assigned_user_id != -1 else None
        # Clear group when assigning user (mutually exclusive)
        update_fields["assigned_group_id"] = None
    if update_data.assigned_group_id is not None:
        update_fields["assigned_group_id"] = update_data.assigned_group_id if update_data.assigned_group_id != -1 else None
        # Clear user when assigning group (mutually exclusive)
        update_fields["assigned_user_id"] = None
    if update_data.status is not None:
        update_fields["status"] = update_data.status

    if not update_fields:
         raise HTTPException(status_code=400, detail="No update fields specified")

    count = 0
    now = datetime.now()
    for vuln in vulnerabilities:
        for field, value in update_fields.items():
            # If setting status to PATCHED, also set patched_at logic
            if field == "status" and value == VulnerabilityStatus.patched:
                if vuln.status != VulnerabilityStatus.patched:
                    vuln.patched_at = now
            setattr(vuln, field, value)
        count += 1
    
    db.commit()

    # Audit-Log
    audit_log = AuditLog(
        user_id=current_user.id,
        event_type=AuditEventType.VULNERABILITY_UPDATED,
        event_description=f"Bulk update performed for {count} vulnerabilities",
        resource_type="vulnerability",
        resource_id="multiple",
        timestamp=datetime.now()
    )
    db.add(audit_log)
    db.commit()

    return {"message": f"{count} vulnerabilities successfully updated", "count": count}


@router.get("")
async def list_vulnerabilities(
    severity: Optional[VulnerabilitySeverity] = Query(None, description="Filter nach Severity"),
    status: Optional[str] = Query(None, description="Filter nach Status (enum value or 'all')"),
    exploitable: Optional[bool] = Query(None, description="Nur exploitable CVEs"),
    kev_only: Optional[bool] = Query(None, description="Nur CISA-KEV-gelistete CVEs"),
    euvd_only: Optional[bool] = Query(None, description="Nur ENISA EUVD-gelistete CVEs"),
    eu_critical: Optional[bool] = Query(None, description="Nur ENISA-Critical-CVEs"),
    in_any_catalog: Optional[bool] = Query(None, description="CVE in mind. einem Catalog (KEV oder EUVD)"),
    in_both_catalogs: Optional[bool] = Query(None, description="CVE in BEIDEN Catalogs (KEV und EUVD)"),
    epss_min: Optional[float] = Query(None, description="Minimaler EPSS-Score (0.0-1.0)"),
    source: Optional[str] = Query(None, description="Filter by scanner source: wazuh, nessus, manual"),
    cross_confirmed: Optional[bool] = Query(None, description="Only CVEs reported by 2+ scanners"),
    asset_id: Optional[int] = Query(None, description="Filter nach Asset"),
    search: Optional[str] = Query(None, description="Suche in CVE-ID, Package, Title"),
    sort_by: str = Query("priority", description="Sortierung: priority, cvss, detected_at"),
    sort_order: str = Query("desc", description="Reihenfolge: asc, desc"),
    limit: int = Query(100, le=1000),
    offset: int = Query(0),
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """
    Liste aller Vulnerabilities mit Filterung und Sortierung

    Sortier-Optionen:
        - priority: Nach berechnetem Priority-Score (CVSS + Exploitability + Age)
        - cvss: Nach CVSS-Score
        - detected_at: Nach Erkennungsdatum
    """
    # Base Query mit OUTER JOIN zu Assets (damit Vulns auch ohne Asset-Match angezeigt werden)
    query = db.query(Vulnerability).outerjoin(Asset)

    # Filter anwenden
    if severity:
        query = query.filter(Vulnerability.severity == severity)

    # Status filter — supports literal enum values + two convenience groups.
    # Default (no value) = active states only (open + pending_verification +
    # patch_failed). 'all' returns every status. 'closed' returns operator-
    # closed states (false_positive + accepted_risk + deferred + patched).
    _ACTIVE_STATES = (
        VulnerabilityStatus.open,
        VulnerabilityStatus.pending_verification,
        VulnerabilityStatus.patch_failed,
    )
    _CLOSED_STATES = (
        VulnerabilityStatus.patched,
        VulnerabilityStatus.false_positive,
        VulnerabilityStatus.accepted_risk,
        VulnerabilityStatus.deferred,
    )
    if status == 'all':
        pass  # no filter
    elif status == 'active':
        query = query.filter(Vulnerability.status.in_(_ACTIVE_STATES))
    elif status == 'closed':
        query = query.filter(Vulnerability.status.in_(_CLOSED_STATES))
    elif status:
        # literal enum value (open / patched / false_positive / ...)
        query = query.filter(Vulnerability.status == status)
    else:
        # Default — active states only (SOC workflow default)
        query = query.filter(Vulnerability.status.in_(_ACTIVE_STATES))

    if exploitable is not None:
        query = query.filter(Vulnerability.exploitable == exploitable)

    if kev_only:
        query = query.filter(Vulnerability.kev_listed == True)

    if euvd_only:
        query = query.filter(Vulnerability.euvd_listed == True)

    if eu_critical:
        query = query.filter(Vulnerability.euvd_critical == True)

    if in_any_catalog:
        query = query.filter(
            (Vulnerability.kev_listed == True) | (Vulnerability.euvd_listed == True)
        )

    if in_both_catalogs:
        query = query.filter(
            Vulnerability.kev_listed == True,
            Vulnerability.euvd_listed == True,
        )

    if epss_min is not None:
        query = query.filter(Vulnerability.epss_score >= epss_min)

    # Source filter (sources column is a JSON list stored as text).
    # `wazuh_only` would be too brittle, use string contains.
    if source:
        src = source.strip().lower()
        if src in {"wazuh", "nessus", "manual"}:
            query = query.filter(Vulnerability.sources.contains(f'"{src}"'))

    if cross_confirmed:
        # Pragmatic: rows with at least one comma in the JSON list have >=2 sources.
        # `["wazuh"]` has no comma, `["wazuh","nessus"]` does.
        query = query.filter(Vulnerability.sources.contains(','))

    if asset_id:
        query = query.filter(Vulnerability.asset_id == asset_id)

    if search:
        import re
        # Parse search terms, respecting quoted strings
        # Example: '!"Mozilla Thunderbird" kernel' -> ['!"Mozilla Thunderbird"', 'kernel']
        term_pattern = re.compile(r'!?"[^"]+"|[^\s]+')
        terms = term_pattern.findall(search.strip())
        
        for term in terms:
            is_negation = term.startswith('!')
            clean_term = term[1:] if is_negation else term
            
            # Remove surrounding quotes if present
            if clean_term.startswith('"') and clean_term.endswith('"'):
                clean_term = clean_term[1:-1]
            
            if not clean_term:
                continue
                
            search_pattern = f"%{clean_term}%"
            
            if is_negation:
                # Negation (Exclusion)
                query = query.filter(
                    ~((Vulnerability.cve_id.ilike(search_pattern)) |
                      (Vulnerability.package_name.ilike(search_pattern)) |
                      (Vulnerability.title.ilike(search_pattern)))
                )
            else:
                # Normal Search (Inclusion)
                query = query.filter(
                    (Vulnerability.cve_id.ilike(search_pattern)) |
                    (Vulnerability.package_name.ilike(search_pattern)) |
                    (Vulnerability.title.ilike(search_pattern))
                )

    # Get total count before pagination
    total_count = query.count()

    # Sortierung
    # Supported sort_by values map to SQL columns.
    # `priority` is special (needs Python-side breakdown), `cpr` is computed,
    # `published_date` and `cve_id` fall through to dedicated branches below.
    sort_map = {
        "cvss": Vulnerability.cvss_score,
        "cvss_score": Vulnerability.cvss_score,
        "detected_at": Vulnerability.detected_at,
        "published_date": Vulnerability.published_date,
        "updated_at": Vulnerability.updated_at,    # DB-write timestamp — "recently changed risk"
        "epss": Vulnerability.epss_score,
        "epss_score": Vulnerability.epss_score,
        "kev": Vulnerability.kev_listed,
        "kev_listed": Vulnerability.kev_listed,
        "euvd": Vulnerability.euvd_listed,
        "euvd_listed": Vulnerability.euvd_listed,
        "cve_id": Vulnerability.cve_id,
        "severity": Vulnerability.severity,
        "status": Vulnerability.status,
        # Source: groups rows by which scanner first reported the finding.
        # Cross-confirmed rows fall into the bucket of whichever saw it first.
        "source": Vulnerability.first_detected_by,
        "first_detected_by": Vulnerability.first_detected_by,
    }

    order_fn = desc if sort_order == "desc" else asc

    def _python_sort_and_build(vulns, key_fn):
        ordered = sorted(vulns, key=key_fn, reverse=(sort_order == "desc"))
        return [_build_vuln_response(v) for v in ordered]

    if sort_by == "priority":
        # Sort by materialised priority_score (migration 024). NULLs go
        # last on desc, first on asc. Sync/enrich/override paths keep
        # the column current via Vulnerability.refresh_scores().
        if sort_order == "desc":
            query = query.order_by(
                Vulnerability.priority_score.is_(None),
                desc(Vulnerability.priority_score),
                desc(Vulnerability.id),
            )
        else:
            query = query.order_by(
                asc(Vulnerability.priority_score),
                asc(Vulnerability.id),
            )
        vulnerabilities = query.offset(offset).limit(limit).all()
        return {"items": [_build_vuln_response(v) for v in vulnerabilities],
                "total": total_count}

    if sort_by == "cpr":
        # Sort by materialised cpr_score (migration 024). Old SQL proxy
        # `cvss * epss` was wrong vs the real `CVSS×10×0.6 + EPSS%×0.4`
        # formula and only sorted the current page anyway.
        if sort_order == "desc":
            query = query.order_by(
                Vulnerability.cpr_score.is_(None),
                desc(Vulnerability.cpr_score),
                desc(Vulnerability.id),
            )
        else:
            query = query.order_by(
                asc(Vulnerability.cpr_score),
                asc(Vulnerability.id),
            )
        vulnerabilities = query.offset(offset).limit(limit).all()
        return {"items": [_build_vuln_response(v) for v in vulnerabilities],
                "total": total_count}

    if sort_by == "published_date":
        # CVE-IDs are NOT chronological — CISA/MITRE assign them in batches.
        # Order by official published_date with a detected_at fallback so
        # CVEs we have not yet enriched still get a reasonable position.
        from sqlalchemy import func as sa_func
        ordering_expr = sa_func.coalesce(
            Vulnerability.published_date, Vulnerability.detected_at
        )
        if sort_order == "desc":
            query = query.order_by(
                ordering_expr.is_(None), desc(ordering_expr), desc(Vulnerability.id)
            )
        else:
            query = query.order_by(asc(ordering_expr), asc(Vulnerability.id))
        vulnerabilities = query.offset(offset).limit(limit).all()
        return {"items": [_build_vuln_response(v) for v in vulnerabilities],
                "total": total_count}

    if sort_by == "cve_id":
        # Natural-numeric sort: lex on "CVE-2026-8401" sorts BEFORE
        # "CVE-2026-35440" because "8" > "3" character-wise. Cast the
        # numeric suffix to int so 35440 wins.
        # Pseudo-CVEs (NESSUS-PLUGIN-*) fall back to lex at the end.
        from sqlalchemy import func as sa_func, case
        # Postgres: split_part(cve_id, '-', 2)::int as year, ('', 3)::int as num
        # Pseudo-CVEs ("NESSUS-PLUGIN-123456") have non-numeric year segment;
        # NULLIF + regexp catches the cast safely.
        year_part = sa_func.nullif(sa_func.split_part(Vulnerability.cve_id, '-', 2), '')
        num_part = sa_func.nullif(sa_func.split_part(Vulnerability.cve_id, '-', 3), '')
        # Cast only when both segments are pure digits (defensive vs pseudo-CVEs)
        year_int = case(
            (year_part.op('~')('^[0-9]+$'), sa_func.cast(year_part, __import__('sqlalchemy').Integer)),
            else_=None,
        )
        num_int = case(
            (num_part.op('~')('^[0-9]+$'), sa_func.cast(num_part, __import__('sqlalchemy').Integer)),
            else_=None,
        )
        if sort_order == "desc":
            query = query.order_by(
                year_int.is_(None),       # pseudo-CVEs to the end
                desc(year_int),
                desc(num_int),
                desc(Vulnerability.cve_id),  # tiebreak for pseudo
            )
        else:
            query = query.order_by(asc(year_int), asc(num_int), asc(Vulnerability.cve_id))
        vulnerabilities = query.offset(offset).limit(limit).all()
        return {"items": [_build_vuln_response(v) for v in vulnerabilities],
                "total": total_count}

    sort_column = sort_map.get(sort_by, Vulnerability.cvss_score)

    # NULL handling: put NULLs last on desc, first on asc (sensible UX)
    if sort_order == "desc":
        query = query.order_by(sort_column.is_(None), desc(sort_column))
    else:
        query = query.order_by(asc(sort_column))

    vulnerabilities = query.offset(offset).limit(limit).all()
    results = [_build_vuln_response(v) for v in vulnerabilities]
    return {"items": results, "total": total_count}


@router.get("/ai-prioritization")
def get_ai_prioritization(
    limit: int = Query(20, le=50),
    severity: Optional[VulnerabilitySeverity] = Query(None, description="Nach Severity filtern"),
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor),
    ai_client: InfomaniakAIClient = Depends(get_infomaniak_ai_client)
):
    """
    Generiert KI-basierte Patch-Empfehlungen
    """
    ai_service = AIAnalysisService(db, ai_client)
    try:
        recommendations = ai_service.get_priority_recommendations(limit=limit, severity=severity)
        
        # Save Report
        if "recommendations" in recommendations and recommendations["recommendations"]:
             # Extract data safely
            strategy = recommendations.get("global_strategy", "No strategy provided.")
            recs_json = json.dumps(recommendations.get("recommendations", []))
            
            report = AIReport(
                created_by_id=current_user.id,
                global_strategy=strategy,
                recommendations=recs_json
            )
            db.add(report)
            db.commit()

        return recommendations
    except Exception as e:
        logger.error(f"AI prioritization failed: {e}")
        raise HTTPException(
            status_code=500,
            detail="AI prioritization failed. Check server logs for details."
        )


@router.get("/ai-history")
async def get_ai_history(
    limit: int = 10,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """
    Liefert die letzten AI-Scan-Berichte zurück
    """
    reports = db.query(AIReport).order_by(desc(AIReport.created_at)).limit(limit).all()
    
    result = []
    for r in reports:
        try:
            recs = json.loads(r.recommendations)
        except (json.JSONDecodeError, TypeError):
            recs = []
            
        result.append({
            "id": r.id,
            "created_at": r.created_at,
            "global_strategy": r.global_strategy,
            "recommendations_count": len(recs),
            "recommendations": recs # Optional: full data
        })
    return result


@router.get("/reports/dashboard", response_model=DashboardStatsResponse)
async def get_dashboard_statistics(
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """
    Dashboard-Statistiken für Frontend

    Liefert aggregierte Daten:
        - Anzahl Vulnerabilities pro Severity
        - Offene vs. Geschlossene
        - Exploitable-Count
        - Durchschnittlicher CVSS-Score
        - Betroffene Assets
        - Älteste offene Vulnerability
    """
    # Base Query: Nur offene Vulnerabilities
    open_vulns = db.query(Vulnerability).filter(
        Vulnerability.status == VulnerabilityStatus.open
    )

    # Counts pro Severity
    critical_count = open_vulns.filter(
        Vulnerability.severity == VulnerabilitySeverity.critical
    ).count()

    high_count = open_vulns.filter(
        Vulnerability.severity == VulnerabilitySeverity.high
    ).count()

    medium_count = open_vulns.filter(
        Vulnerability.severity == VulnerabilitySeverity.medium
    ).count()

    low_count = open_vulns.filter(
        Vulnerability.severity == VulnerabilitySeverity.low
    ).count()

    # Exploitable Count
    exploitable_count = open_vulns.filter(
        Vulnerability.exploit_available == True
    ).count()

    # Durchschnittlicher CVSS-Score
    avg_cvss = db.query(func.avg(Vulnerability.cvss_score)).filter(
        Vulnerability.status == VulnerabilityStatus.open
    ).scalar() or 0.0

    # Betroffene Assets
    affected_assets = db.query(func.count(func.distinct(Vulnerability.asset_id))).filter(
        Vulnerability.status == VulnerabilityStatus.open
    ).scalar() or 0

    # Älteste Vulnerability
    oldest_vuln = open_vulns.order_by(asc(Vulnerability.detected_at)).first()
    oldest_days = 0
    if oldest_vuln:
        oldest_days = (datetime.now() - oldest_vuln.detected_at).days

    # Verlauf der letzten 7 Tage (Erkennungsdatum)
    severity_history = []
    from datetime import timedelta, time, datetime as dt_class
    now = datetime.now()
    for i in range(6, -1, -1):
        day_date = (now - timedelta(days=i)).date()
        start_of_day = dt_class.combine(day_date, time.min)
        end_of_day = dt_class.combine(day_date, time.max)
        
        count = db.query(Vulnerability).filter(
            Vulnerability.detected_at >= start_of_day,
            Vulnerability.detected_at <= end_of_day,
            Vulnerability.status == VulnerabilityStatus.open
        ).count()
        severity_history.append({
            "day": day_date.strftime("%a"),
            "count": count
        })

    # Gesamt-Zahlen
    total_vulns = db.query(Vulnerability).count()
    open_vulns_count = open_vulns.count()

    total_assets = db.query(Asset).count()
    # Assets, die mindestens einmal gescannt wurden (last_scan is not null)
    scanned_assets = db.query(Asset).filter(Asset.last_scan != None).count()

    return {
        "total_vulnerabilities": total_vulns,
        "open_vulnerabilities": open_vulns_count,
        "critical_count": critical_count,
        "high_count": high_count,
        "medium_count": medium_count,
        "low_count": low_count,
        "exploitable_count": exploitable_count,
        "avg_cvss_score": round(avg_cvss, 2),
        "affected_assets": affected_assets,
        "total_assets": total_assets,
        "scanned_assets": scanned_assets,
        "severity_history": severity_history,
        "oldest_vulnerability_days": oldest_days
    }


@router.get("/{vuln_id}", response_model=VulnerabilityDetailResponse)
async def get_vulnerability_detail(
    vuln_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """
    Detail-Ansicht einer Vulnerability inkl. KI-Analyse (falls vorhanden)
    """
    vuln = db.query(Vulnerability).filter(Vulnerability.id == vuln_id).first()

    if not vuln:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Vulnerability not found"
        )

    # Hole KI-Analyse falls vorhanden
    ai_analysis = None
    if vuln.ai_analysis:
        ai_analysis = {
            "analysis_text": vuln.ai_analysis.analysis_text,
            "threat_level": vuln.ai_analysis.threat_level,
            "exploits_found": vuln.ai_analysis.exploits_found,
            "workarounds": vuln.ai_analysis.workarounds,
            "remediation_steps": vuln.ai_analysis.remediation_steps,
            "confidence_score": vuln.ai_analysis.confidence_score,
            "analysis_timestamp": vuln.ai_analysis.analysis_timestamp
        }

    resp = _build_vuln_response(vuln)
    resp["ai_analysis"] = ai_analysis
    return resp


@router.patch("/{vuln_id}", response_model=VulnerabilityResponse)
async def update_vulnerability_status(
    vuln_id: int,
    update_data: VulnerabilityUpdateRequest,
    background_tasks: BackgroundTasks,
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor)  # Editor/Admin-Berechtigung erforderlich
):
    """
    Ändert Status einer Vulnerability

    Patch-Verifizierungs-Workflow:
        1. User markiert CVE als "patched"
        2. Status wird auf "pending_verification" gesetzt
        3. Wazuh Syscollector-Scan wird getriggert (Background-Task)
        4. Nach Scan-Completion: Automatische Verifizierung
    """
    try:
        vuln = db.query(Vulnerability).filter(Vulnerability.id == vuln_id).first()

        if not vuln:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail="Vulnerability not found"
            )

        old_status = vuln.status
        new_status = update_data.status

        # Status-Änderung
        vuln.status = new_status
        # Persist reason in the matching column when it makes sense.
        # `reason` is the new status-agnostic field; `defer_reason` is
        # kept for the deferred-status legacy UI.
        if update_data.defer_reason is not None:
            vuln.defer_reason = update_data.defer_reason
        if update_data.deferred_until is not None:
            vuln.deferred_until = update_data.deferred_until

        # The audit-trail reason — accept either field for back-compat.
        audit_reason = update_data.reason or update_data.defer_reason

        # Wenn als "patched" markiert: Trigger Rescan
        if new_status == VulnerabilityStatus.patched:
            # Check if asset has wazuh agent
            if vuln.asset and vuln.asset.wazuh_agent_id:
                vuln.status = VulnerabilityStatus.pending_verification
                vuln.patched_at = datetime.now()

                # Background-Task für Rescan
                background_tasks.add_task(
                    verify_patch_with_rescan,
                    vuln_id,
                    vuln.asset.wazuh_agent_id
                )
            else:
                # Kein Agent -> Direkt als Patched markieren
                vuln.patched_at = datetime.now()

        db.commit()
        db.refresh(vuln)

        # Audit-Log — includes reason + cve_id for the revisionssicher view
        log_vulnerability_change(
            db, current_user.id, vuln_id, old_status, vuln.status,
            reason=audit_reason, cve_id=vuln.cve_id, source="manual",
        )

        return _build_vuln_response(vuln)
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error updating vulnerability {vuln_id}: {e}", exc_info=True)
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Internal Server Error: {str(e)}"
        )


def verify_patch_with_rescan(
    vuln_id: int,
    agent_id: str
):
    """
    Snychroner Background-Task für Patch-Verifizierung (läuft in eigenem Thread)

    1. Triggert Syscollector-Scan
    2. Wartet auf Completion (max. 10 Min)
    3. Re-fetcht Vulnerabilities
    4. Prüft ob CVE noch vorhanden
    5. Aktualisiert Status
    """
    from app.database import get_db_context
    from app.models.setting import Setting
    import json

    with get_db_context() as db:
        try:
            from app.auth.setting_crypto import read_setting_value
            raw_wazuh = read_setting_value(db, "wazuh_config")
            if not raw_wazuh:
                logger.error("Patch verification aborted: Wazuh configuration missing")
                return

            config = json.loads(raw_wazuh)
            wazuh = WazuhClient(
                base_url=config.get("api_url"),
                username=config.get("username"),
                password=config.get("password"),
                indexer_url=config.get("indexer_url"),
                indexer_username=config.get("indexer_username"),
                indexer_password=config.get("indexer_password"),
                verify_ssl=config.get("verify_ssl", True)
            )

            # 1. Trigger Rescan
            try:
                wazuh.trigger_syscollector_scan(agent_id)
            except Exception as e:
                logger.error(f"Could not trigger Syscollector scan for agent {agent_id}: {e}")
                # Wir machen trotzdem weiter, evtl. lief gerade schon einer

            # 2. Warte auf Scan-Completion (blockiert nur diesen Thread, nicht den Server)
            scan_completed = wazuh.wait_for_scan_completion(
                agent_id,
                timeout=600,  # 10 Min
                check_interval=30
            )

            if not scan_completed:
                # Timeout: Markiere als Patch-Failed
                vuln = db.query(Vulnerability).filter(Vulnerability.id == vuln_id).first()
                if vuln:
                    vuln.status = VulnerabilityStatus.patch_failed
                    db.commit()
                return

            # 3. Re-fetch Vulnerabilities
            current_vulns = wazuh.get_vulnerabilities(agent_id)
            vuln = db.query(Vulnerability).filter(Vulnerability.id == vuln_id).first()

            if not vuln:
                return

            # 4. Prüfe ob CVE noch in Wazuh-Daten
            cve_still_exists = any(v.get("cve") == vuln.cve_id for v in current_vulns)

            if cve_still_exists:
                # Patch fehlgeschlagen
                vuln.status = VulnerabilityStatus.patch_failed
            else:
                # Patch erfolgreich verifiziert
                vuln.status = VulnerabilityStatus.patched

            db.commit()

        except Exception as e:
            logger.error(f"Error in verify_patch_with_rescan: {e}")
            # Bei Fehler: Markiere als Patch-Failed
            vuln = db.query(Vulnerability).filter(Vulnerability.id == vuln_id).first()
            if vuln:
                vuln.status = VulnerabilityStatus.patch_failed
                db.commit()


@router.post("/{vuln_id}/analyze")
def analyze_vulnerability_with_ai(
    vuln_id: int,
    force_refresh: bool = Query(False, description="Ignore cache"),
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor),
    ai_client: InfomaniakAIClient = Depends(get_infomaniak_ai_client)
):
    """
    Triggert KI-Analyse für eine Vulnerability

    Nutzt Infomaniak AI API mit Web-Search für:
        - Exploit-Detection
        - Threat-Intelligence
        - Workarounds
        - Remediation-Steps
    """
    vuln = db.query(Vulnerability).filter(Vulnerability.id == vuln_id).first()

    if not vuln:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Vulnerability not found"
        )

    # Service-Layer für DB-Integration
    ai_service = AIAnalysisService(db, ai_client)

    try:
        analysis = ai_service.get_or_create_analysis(vuln_id, force_refresh)

        # Audit-Log
        audit_log = AuditLog(
            user_id=current_user.id,
            event_type=AuditEventType.AI_ANALYSIS_REQUESTED,
            event_description=f"AI analysis requested for {vuln.cve_id}",
            resource_type="vulnerability",
            resource_id=str(vuln_id),
            timestamp=datetime.now()
        )
        db.add(audit_log)
        db.commit()

        return {
            "analysis_text": analysis.analysis_text,
            "threat_level": analysis.threat_level,
            "exploits_found": analysis.exploits_found,
            "workarounds": analysis.workarounds,
            "remediation_steps": analysis.remediation_steps,
            "threat_intel_sources": analysis.threat_intel_sources,
            "confidence_score": analysis.confidence_score,
            "model_version": analysis.model_version,
            "analysis_timestamp": analysis.analysis_timestamp,
            "cached": not force_refresh
        }

    except Exception as e:
        logger.error(f"AI analysis failed: {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="AI analysis failed. Check server logs for details."
        )








@router.post("/{vuln_id}/enrich")
async def enrich_single_vulnerability(
    vuln_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor)
):
    """Trigger EPSS + CISA KEV enrichment for one vulnerability."""
    try:
        stats = enrich_vulnerability_by_id(db, vuln_id)
    except EnrichmentError as e:
        raise HTTPException(status_code=404, detail=str(e))

    vuln = db.query(Vulnerability).filter(Vulnerability.id == vuln_id).first()

    audit_log = AuditLog(
        user_id=current_user.id,
        event_type=AuditEventType.VULNERABILITY_UPDATED,
        event_description=f"Threat intel enrichment for {vuln.cve_id if vuln else vuln_id}",
        resource_type="vulnerability",
        resource_id=str(vuln_id),
        timestamp=datetime.now()
    )
    db.add(audit_log)
    db.commit()

    return {"stats": stats, "vulnerability": _build_vuln_response(vuln) if vuln else None}


class BulkEnrichRequest(BaseModel):
    vulnerability_ids: Optional[List[int]] = None
    only_open: bool = True


@router.post("/enrich/bulk")
async def bulk_enrich_vulnerabilities(
    payload: BulkEnrichRequest,
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor)
):
    """
    Bulk enrich vulnerabilities with EPSS + CISA KEV.

    - If `vulnerability_ids` is provided, only those are enriched.
    - Otherwise enriches every open vulnerability (or all if only_open=false).
    """
    if payload.vulnerability_ids:
        vulns = db.query(Vulnerability).filter(
            Vulnerability.id.in_(payload.vulnerability_ids)
        ).all()
    else:
        q = db.query(Vulnerability)
        if payload.only_open:
            q = q.filter(Vulnerability.status == VulnerabilityStatus.open)
        vulns = q.all()

    if not vulns:
        return {"stats": {"epss_updated": 0, "kev_marked": 0, "kev_cleared": 0, "total": 0}}

    stats = enrich_vulnerabilities(db, vulns)

    audit_log = AuditLog(
        user_id=current_user.id,
        event_type=AuditEventType.VULNERABILITY_UPDATED,
        event_description=f"Bulk threat intel enrichment for {stats['total']} CVEs",
        resource_type="vulnerability",
        resource_id="multiple",
        timestamp=datetime.now()
    )
    db.add(audit_log)
    db.commit()

    return {"stats": stats}


@router.post("/enrich/kev/refresh")
async def refresh_kev_catalog(
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor)
):
    """Force-refresh the cached CISA KEV catalog from CISA feed."""
    try:
        kev_map = fetch_kev_catalog(db, force_refresh=True)
    except EnrichmentError as e:
        raise HTTPException(status_code=502, detail=str(e))
    return {"kev_entries": len(kev_map), "refreshed_at": datetime.now().isoformat()}


@router.patch("/{vuln_id}/assign")
async def assign_vulnerability(
    vuln_id: int,
    assign_data: AssignRequest,
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor)
):
    """Assigns a user to a vulnerability"""
    vuln = db.query(Vulnerability).filter(Vulnerability.id == vuln_id).first()
    if not vuln:
        raise HTTPException(status_code=404, detail="Vulnerability not found")

    if assign_data.user_id is not None:
        user = db.query(User).filter(User.id == assign_data.user_id).first()
        if not user:
            raise HTTPException(status_code=404, detail="User not found")
    
    if assign_data.group_id is not None:
        from app.models.group import Group
        group = db.query(Group).filter(Group.id == assign_data.group_id).first()
        if not group:
            raise HTTPException(status_code=404, detail="Group not found")

    vuln.assigned_user_id = assign_data.user_id
    vuln.assigned_group_id = assign_data.group_id
    db.commit()
    db.refresh(vuln)
    return _build_vuln_response(vuln)


@router.patch("/{vuln_id}/defer")
async def defer_vulnerability(
    vuln_id: int,
    defer_data: DeferRequest,
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor)
):
    """Defers a vulnerability with a target date and reason"""
    vuln = db.query(Vulnerability).filter(Vulnerability.id == vuln_id).first()
    if not vuln:
        raise HTTPException(status_code=404, detail="Vulnerability not found")

    old_status = vuln.status
    vuln.status = VulnerabilityStatus.deferred
    vuln.deferred_until = defer_data.deferred_until
    vuln.defer_reason = defer_data.reason
    vuln.notification_suppressed = True
    db.commit()
    db.refresh(vuln)

    log_vulnerability_change(db, current_user.id, vuln_id, old_status, vuln.status)
    return _build_vuln_response(vuln)


@router.patch("/{vuln_id}/undefer")
async def undefer_vulnerability(
    vuln_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor)
):
    """Removes deferral, resets to OPEN"""
    vuln = db.query(Vulnerability).filter(Vulnerability.id == vuln_id).first()
    if not vuln:
        raise HTTPException(status_code=404, detail="Vulnerability nicht gefunden")

    old_status = vuln.status
    vuln.status = VulnerabilityStatus.open
    vuln.deferred_until = None
    vuln.defer_reason = None
    vuln.notification_suppressed = False
    db.commit()
    db.refresh(vuln)

    log_vulnerability_change(db, current_user.id, vuln_id, old_status, vuln.status)
    return _build_vuln_response(vuln)


@router.patch("/{vuln_id}/suppress-notifications")
async def toggle_notification_suppression(
    vuln_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor)
):
    """Toggles notification suppression for a vulnerability"""
    vuln = db.query(Vulnerability).filter(Vulnerability.id == vuln_id).first()
    if not vuln:
        raise HTTPException(status_code=404, detail="Vulnerability nicht gefunden")

    vuln.notification_suppressed = not vuln.notification_suppressed
    db.commit()
    db.refresh(vuln)
    return _build_vuln_response(vuln)


class FalsePositiveRequest(BaseModel):
    reason: Optional[str] = None


@router.patch("/{vuln_id}/false-positive")
async def mark_false_positive(
    vuln_id: int,
    payload: FalsePositiveRequest,
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor),
):
    """
    Mark a vulnerability as false positive. Bookmark used after manual
    review (e.g. confirming a Nessus finding is a false alarm). Sets
    status=false_positive and suppresses further notifications.
    """
    vuln = db.query(Vulnerability).filter(Vulnerability.id == vuln_id).first()
    if not vuln:
        raise HTTPException(status_code=404, detail="Vulnerability not found")

    old_status = vuln.status
    vuln.status = VulnerabilityStatus.false_positive
    vuln.notification_suppressed = True
    if payload.reason:
        vuln.defer_reason = payload.reason  # reuse existing field for context
    db.commit()
    db.refresh(vuln)

    log_vulnerability_change(db, current_user.id, vuln_id, old_status, vuln.status)
    return _build_vuln_response(vuln)


@router.patch("/{vuln_id}/unmark-false-positive")
async def unmark_false_positive(
    vuln_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor),
):
    """Revert a false-positive marking back to open."""
    vuln = db.query(Vulnerability).filter(Vulnerability.id == vuln_id).first()
    if not vuln:
        raise HTTPException(status_code=404, detail="Vulnerability not found")
    if vuln.status != VulnerabilityStatus.false_positive:
        raise HTTPException(status_code=400, detail="Not currently marked as false positive")

    old_status = vuln.status
    vuln.status = VulnerabilityStatus.open
    vuln.notification_suppressed = False
    vuln.defer_reason = None
    db.commit()
    db.refresh(vuln)

    log_vulnerability_change(db, current_user.id, vuln_id, old_status, vuln.status)
    return _build_vuln_response(vuln)


@router.post("/sync/wazuh")
async def sync_vulnerabilities_from_wazuh(
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor)
):
    """
    Synchronizes vulnerabilities from Wazuh (synchronous)
    
    Executes sync immediately and returns statistics.
    """
    try:
        result = run_wazuh_vulnerability_sync(db)
        return result
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))


def run_wazuh_vulnerability_sync(db: Session) -> dict:
    """Synchronous Wazuh vulnerability sync with statistics return"""
    
    # 1. Get Wazuh Config from DB (transparently decrypted)
    from app.auth.setting_crypto import read_setting_value
    raw_wazuh = read_setting_value(db, "wazuh_config")
    if not raw_wazuh:
        raise HTTPException(status_code=400, detail="Wazuh configuration not found. Please configure Wazuh in Settings.")

    try:
        config = json.loads(raw_wazuh)
        api_url = config.get("api_url")
        username = config.get("username")
        password = config.get("password")
        indexer_url = config.get("indexer_url")
        indexer_username = config.get("indexer_username")
        indexer_password = config.get("indexer_password")
    except json.JSONDecodeError:
        raise HTTPException(status_code=400, detail="Invalid Wazuh configuration format.")

    if not all([api_url, username, password]):
        raise HTTPException(status_code=400, detail="Incomplete Wazuh configuration. Please configure API URL, username and password.")

    wazuh = WazuhClient(
        base_url=api_url, 
        username=username, 
        password=password, 
        indexer_url=indexer_url,
        indexer_username=indexer_username,
        indexer_password=indexer_password,
        verify_ssl=config.get("verify_ssl", False)
    )

    stats = {
        "agents_synced": 0,
        "vulns_created": 0,
        "vulns_updated": 0,
        "vulns_patched": 0,
        "total_vulns_from_wazuh": 0
    }
    # IDs of vulns created during this sync — handed to the digest dispatcher
    # after the full sync completes (one summary email per recipient instead
    # of one email per CVE).
    newly_created_vuln_ids: list[int] = []

    try:
        # Hole alle aktiven Agents
        agents = wazuh.get_agents(status="active")
        
        for agent in agents:
            agent_id = agent["id"]

            # Hole oder erstelle Asset
            asset = db.query(Asset).filter(
                Asset.wazuh_agent_id == agent_id
            ).first()

            if not asset:
                continue  # Skip if asset not in DB

            # Record a Scan-Jobs entry per agent so the manual "Sync
            # Data (Wazuh)" button shows up in /scans the same way
            # the Nessus sync does (commit 4bca41e). Closed out at
            # the bottom of the agent loop.
            from app.models.scan import Scan, ScanType, ScanStatus
            scan_row = Scan(
                asset_id=asset.id,
                scan_type=ScanType.WAZUH,
                status=ScanStatus.RUNNING,
                started_at=datetime.now(),
            )
            db.add(scan_row)

            # Get vulns from Wazuh
            wazuh_vulns = wazuh.get_vulnerabilities(agent_id)
            stats["total_vulns_from_wazuh"] += len(wazuh_vulns)
            
            # 1. Deduplicate Wazuh vulns by CVE ID for this asset
            unique_cves = {}
            for vuln_data in wazuh_vulns:
                cve_id = vuln_data.get("cve")
                if not cve_id:
                    continue
                
                if cve_id not in unique_cves:
                    unique_cves[cve_id] = {
                        "data": vuln_data,
                        "packages": set(),
                        "versions": set()
                    }
                
                pkg_name = vuln_data.get("name")
                pkg_version = vuln_data.get("version")
                if pkg_name:
                    unique_cves[cve_id]["packages"].add(pkg_name)
                if pkg_version:
                    unique_cves[cve_id]["versions"].add(pkg_version)

            active_cves = set(unique_cves.keys())
            
            # 2. Process unique CVEs
            for cve_id, cve_info in unique_cves.items():
                vuln_data = cve_info["data"]
                
                # Merge package strings
                merged_packages = ", ".join(sorted(cve_info["packages"]))
                merged_versions = ", ".join(sorted(cve_info["versions"]))
                
                # Truncate to fit DB columns
                if len(merged_packages) > 250:
                    merged_packages = merged_packages[:247] + "..."
                if len(merged_versions) > 95:
                    merged_versions = merged_versions[:92] + "..."
                
                cvss_data = vuln_data.get("cvss", {})
                cvss3 = cvss_data.get("cvss3", {})
                score = cvss3.get("base_score")
                wazuh_severity = vuln_data.get("severity")

                # Check if exists
                existing = db.query(Vulnerability).filter(
                    Vulnerability.cve_id == cve_id,
                    Vulnerability.asset_id == asset.id
                ).first()
                
                if existing:
                    # Track wazuh as a source; reopen if Wazuh sees it again
                    # after it was marked patched.
                    existing.add_source("wazuh")
                    if existing.status == VulnerabilityStatus.patched:
                        existing.status = VulnerabilityStatus.open
                        existing.patched_at = None
                    elif existing.status not in (
                        VulnerabilityStatus.pending_verification,
                        VulnerabilityStatus.accepted_risk,
                        VulnerabilityStatus.false_positive,
                        VulnerabilityStatus.deferred,
                    ):
                        existing.status = VulnerabilityStatus.open

                    # Update package info (merged)
                    existing.package_name = merged_packages
                    existing.package_version = merged_versions
                    # Backfill fixed_version when missing (older syncs
                    # didn't extract it; new syncs do).
                    fv = vuln_data.get("fixed_version")
                    if fv and not existing.fixed_version:
                        existing.fixed_version = fv

                    # Update score/severity if changed
                    if score and existing.cvss_score != score:
                        existing.cvss_score = score
                        existing.severity = map_severity(score, wazuh_severity)
                        stats["vulns_updated"] += 1
                else:
                    # Create new — savepoint so IntegrityError (race duplicate)
                    # only rolls back this insert, not the whole sync.
                    try:
                        severity = map_severity(score, wazuh_severity)
                        vuln = Vulnerability(
                            cve_id=cve_id,
                            asset_id=asset.id,
                            cvss_score=score,
                            severity=severity,
                            status=VulnerabilityStatus.open,
                            title=vuln_data.get("title"),
                            package_name=merged_packages,
                            package_version=merged_versions,
                            fixed_version=vuln_data.get("fixed_version"),
                            detected_at=datetime.now(),
                            sources='["wazuh"]',
                            first_detected_by="wazuh",
                        )
                        with db.begin_nested():
                            db.add(vuln)
                            db.flush()
                        stats["vulns_created"] += 1
                        newly_created_vuln_ids.append(vuln.id)

                    except IntegrityError:
                        # Savepoint rolled back; outer transaction intact.
                        logger.debug(f"Skipping duplicate CVE {cve_id} for asset {asset.id}")
                        continue

            # Source-aware backfill: drop "wazuh" from vulns Wazuh no longer
            # reports. Only mark patched when ALL sources are gone so Nessus-
            # only findings are not accidentally closed here.
            #
            # Safety: when Wazuh returns 0 vulns for an agent (transient API
            # glitch, indexer warming up after a manager restart, etc.) the
            # naive backfill below would mark every existing finding patched.
            # Skip in that case — a real "everything fixed" state is
            # vanishingly rare and not worth the risk of wiping the inventory.
            if not active_cves and not wazuh_vulns:
                logger.warning(
                    "Wazuh returned 0 vulns for asset %s — skipping backfill "
                    "to avoid mass-patching from a transient empty response.",
                    asset.hostname,
                )
                stats["agents_synced"] += 1
                scan_row.status = ScanStatus.COMPLETED
                scan_row.completed_at = datetime.now()
                scan_row.vulnerabilities_found = 0
                scan_row.error_message = "Wazuh returned 0 vulns (skipped backfill)"
                continue
            stale_wazuh = (
                db.query(Vulnerability)
                .filter(
                    Vulnerability.asset_id == asset.id,
                    Vulnerability.status == VulnerabilityStatus.open,
                    Vulnerability.sources.contains('"wazuh"'),
                )
                .all()
            )
            for v in stale_wazuh:
                if v.cve_id in active_cves:
                    continue
                v.remove_source("wazuh")
                if not v.source_list:
                    old_st = v.status
                    v.status = VulnerabilityStatus.patched
                    v.patched_at = datetime.now()
                    stats["vulns_patched"] += 1
                    try:
                        log_vulnerability_change(
                            db, None, v.id, old_st, v.status,
                            reason=f"Wazuh full sync: agent {agent_id} no longer reports this CVE on {asset.hostname}",
                            cve_id=v.cve_id,
                            source="wazuh_sync",
                        )
                    except Exception as e:
                        logger.warning("audit log for wazuh full-sync auto-patch failed (vuln_id=%s): %s", v.id, e)

            stats["agents_synced"] += 1
            # Close out the per-agent Scan row so the Scan-Jobs page
            # shows the manual sync run alongside Nessus runs.
            scan_row.status = ScanStatus.COMPLETED
            scan_row.completed_at = datetime.now()
            scan_row.vulnerabilities_found = len(active_cves)

        db.commit()
        wazuh.close()

        # Best-effort enrichment of newly created vulns (by ID, not a
        # broad scan of all un-enriched rows which could be very slow).
        if newly_created_vuln_ids:
            try:
                new_vulns = db.query(Vulnerability).filter(
                    Vulnerability.id.in_(newly_created_vuln_ids)
                ).all()
                if new_vulns:
                    enrich_stats = enrich_vulnerabilities(db, new_vulns)
                    stats["enrichment"] = enrich_stats
            except Exception as e:
                logger.warning(f"Wazuh sync: enrichment of new vulns failed: {e}")

        # Dispatch ONE digest email per recipient containing all new vulns
        # above the configured severity threshold. Replaces the per-CVE
        # inline send that used to spam SMTP at the rate of 1 mail per CVE
        # per recipient. Mode override via setting `notification_mode`.
        if newly_created_vuln_ids:
            try:
                from app.services.email_service import dispatch_new_vuln_notifications
                fresh_vulns = db.query(Vulnerability).filter(
                    Vulnerability.id.in_(newly_created_vuln_ids)
                ).all()
                notif_stats = dispatch_new_vuln_notifications(db, fresh_vulns)
                stats["notifications"] = notif_stats
                logger.info(
                    f"Wazuh sync notifications: {notif_stats.get('emails_sent', 0)} sent, "
                    f"{notif_stats.get('emails_failed', 0)} failed, "
                    f"{notif_stats.get('recipients', 0)} recipients"
                )
            except Exception as e:
                logger.warning(f"Wazuh sync: new-vuln notification dispatch failed: {e}")

        return {
            "message": f"Synced {stats['agents_synced']} agents. Created: {stats['vulns_created']}, Updated: {stats['vulns_updated']}, Patched: {stats['vulns_patched']}",
            **stats
        }
        
    except Exception as e:
        db.rollback()
        wazuh.close()
        logger.error(f"Wazuh sync failed: {e}")
        raise HTTPException(status_code=500, detail="Wazuh sync failed. Check server logs for details.")


async def sync_wazuh_vulnerabilities(db_session: Optional[Session] = None):
    """Background-Task for Wazuh-Sync (for Scheduler)"""
    from app.database import SessionLocal

    db = db_session if db_session is not None else SessionLocal()
    try:
        result = run_wazuh_vulnerability_sync(db)
        logger.info(f"Scheduled Sync: {result['message']}")
    except Exception as e:
        logger.error(f"Scheduled Sync Error: {e}")
    finally:
        if db_session is None:
            db.close()


def sync_agent_vulnerabilities(db: Session, wazuh: WazuhClient, agent_id: str, asset: Asset):
    """Sync vulnerabilities for a single agent from Wazuh"""
    from app.models.vulnerability import Vulnerability, VulnerabilityStatus
    from app.models.vulnerability_package import VulnerabilityPackage
    from datetime import datetime

    logger.info(f"Sync: Fetching vulnerabilities for agent {agent_id} ({asset.hostname})...")
    raw_vulns = wazuh.get_vulnerabilities(agent_id)
    logger.info(f"Sync: Agent {agent_id} has {len(raw_vulns)} raw vulnerability entries.")

    # 1. Deduplicate by CVE ID + collect per-package detail.
    # `pkg_rows` is a dict keyed by package_name so we keep one row per
    # (cve, package) pair across the agent. fixed_version comes straight
    # from Wazuh (often empty — backfill via override service).
    unique_cves: dict = {}
    for vuln_data in raw_vulns:
        cve_id = vuln_data.get("cve")
        if not cve_id:
            continue

        if cve_id not in unique_cves:
            unique_cves[cve_id] = {
                "data": vuln_data,
                "pkg_rows": {},          # name → {version, fixed}
                "packages": set(),       # legacy summary
                "versions": set(),       # legacy summary
            }

        pkg_name = vuln_data.get("name")
        pkg_version = vuln_data.get("version")
        pkg_fix = vuln_data.get("fixed_version")
        if pkg_name:
            unique_cves[cve_id]["packages"].add(pkg_name)
            existing_pkg = unique_cves[cve_id]["pkg_rows"].get(pkg_name)
            if existing_pkg is None:
                unique_cves[cve_id]["pkg_rows"][pkg_name] = {
                    "version": pkg_version,
                    "fixed": pkg_fix,
                }
            else:
                # Prefer first non-empty fix
                if pkg_fix and not existing_pkg.get("fixed"):
                    existing_pkg["fixed"] = pkg_fix
        if pkg_version:
            unique_cves[cve_id]["versions"].add(pkg_version)

    active_cves = set(unique_cves.keys())
    logger.info(f"Sync: Agent {agent_id} has {len(active_cves)} unique CVEs after dedup.")

    # Log severity breakdown from Wazuh data
    severity_counts = {}
    for cve_id_tmp, cve_info_tmp in unique_cves.items():
        sev = (cve_info_tmp["data"].get("severity") or "unknown").lower()
        severity_counts[sev] = severity_counts.get(sev, 0) + 1
    logger.info(f"Sync: Agent {agent_id} Wazuh severity breakdown: {severity_counts}")

    new_count = 0
    updated_count = 0
    # Track new vuln IDs so we can send a single digest mail per recipient
    # at the end of this agent sync instead of one mail per CVE.
    new_vuln_ids: list[int] = []

    def _upsert_packages(vuln_id: int, rows: dict, sync_time: datetime) -> None:
        """Upsert one VulnerabilityPackage row per (vuln, package).
        Updates last_seen_at for existing entries so the next stale-
        package pass can prune ones Wazuh stopped reporting."""
        if not rows:
            return
        existing_pkgs = {
            p.package_name: p
            for p in db.query(VulnerabilityPackage)
                       .filter(VulnerabilityPackage.vulnerability_id == vuln_id)
                       .all()
        }
        for pkg_name, info in rows.items():
            row = existing_pkgs.get(pkg_name)
            if row is None:
                row = VulnerabilityPackage(
                    vulnerability_id=vuln_id,
                    package_name=pkg_name[:255],
                    package_version=(info.get("version") or None),
                    fixed_version=(info.get("fixed") or None),
                    source="wazuh",
                    first_detected_at=sync_time,
                    last_seen_at=sync_time,
                )
                db.add(row)
            else:
                row.last_seen_at = sync_time
                # Only fill fixed_version when empty (override service
                # may have written a better value from Vulnrichment).
                if info.get("fixed") and not row.fixed_version:
                    row.fixed_version = info["fixed"][:100]
                if info.get("version") and info["version"] != row.package_version:
                    row.package_version = info["version"][:100]
                if not row.source:
                    row.source = "wazuh"

    # 2. Process each unique CVE
    for cve_id, cve_info in unique_cves.items():
        vuln_data = cve_info["data"]

        # Merge package strings (legacy parent-row summary)
        merged_packages = ", ".join(sorted(cve_info["packages"]))
        merged_versions = ", ".join(sorted(cve_info["versions"]))
        
        # Truncate to fit DB columns
        if len(merged_packages) > 250:
            merged_packages = merged_packages[:247] + "..."
        if len(merged_versions) > 95:
            merged_versions = merged_versions[:92] + "..."
        
        score = vuln_data.get("cvss", {}).get("cvss3", {}).get("base_score")
        wazuh_severity = vuln_data.get("severity")

        # Check if already exists
        existing = db.query(Vulnerability).filter(
            Vulnerability.cve_id == cve_id,
            Vulnerability.asset_id == asset.id
        ).first()

        if existing:
            # Update existing entry — track wazuh as a source and reopen
            # if Wazuh sees a previously-patched vuln again.
            existing.add_source("wazuh")
            if existing.status == VulnerabilityStatus.patched:
                existing.status = VulnerabilityStatus.open
                existing.patched_at = None
            elif existing.status not in (
                VulnerabilityStatus.pending_verification,
                VulnerabilityStatus.accepted_risk,
                VulnerabilityStatus.false_positive,
                VulnerabilityStatus.deferred,
            ):
                existing.status = VulnerabilityStatus.open

            existing.package_name = merged_packages
            existing.package_version = merged_versions
            fv = vuln_data.get("fixed_version")
            if fv and not existing.fixed_version:
                existing.fixed_version = fv
            if score:
                existing.cvss_score = score
            existing.severity = map_severity(score, wazuh_severity)
            _upsert_packages(existing.id, cve_info["pkg_rows"], datetime.now())
            existing.refresh_scores()
            updated_count += 1
        else:
            # Create new — use savepoint so an IntegrityError (duplicate race)
            # only rolls back this single insert, not the entire sync transaction.
            try:
                severity = map_severity(score, wazuh_severity)
                vuln = Vulnerability(
                    cve_id=cve_id,
                    asset_id=asset.id,
                    cvss_score=score,
                    severity=severity,
                    status=VulnerabilityStatus.open,
                    title=vuln_data.get("title"),
                    package_name=merged_packages,
                    package_version=merged_versions,
                    fixed_version=vuln_data.get("fixed_version"),
                    detected_at=datetime.now(),
                    sources='["wazuh"]',
                    first_detected_by="wazuh",
                )
                with db.begin_nested():
                    db.add(vuln)
                    db.flush()
                _upsert_packages(vuln.id, cve_info["pkg_rows"], datetime.now())
                vuln.refresh_scores()
                new_count += 1
                new_vuln_ids.append(vuln.id)
                logger.info(f"Sync: New vulnerability {cve_id} on {asset.hostname} - severity={severity.value}, cvss={score}, wazuh_severity={wazuh_severity}")

            except IntegrityError:
                # Savepoint rolled back; outer transaction intact.
                logger.debug(f"Skipping duplicate CVE {cve_id} for asset {asset.id}")
                continue

    # 3. Source-aware backfill: remove "wazuh" from sources for any open vuln
    # on this asset that Wazuh no longer reports.  Only mark as PATCHED when
    # ALL sources are gone — Nessus-only findings must not be patched here.
    #
    # Safety: when Wazuh returns 0 vulns for an agent (transient API outage,
    # indexer warm-up after a manager restart, etc.) the loop below would
    # mass-patch the entire history for the agent. Skip in that case —
    # genuinely "everything fixed" is so rare that the false-positive risk
    # is not worth taking.
    if not active_cves and not raw_vulns:
        logger.warning(
            "Wazuh returned 0 vulns for agent %s (%s) — skipping backfill "
            "to avoid mass-patching from a transient empty response.",
            agent_id, asset.hostname,
        )
        asset.last_scan = datetime.now()
        db.commit()
        return new_count, updated_count, new_vuln_ids
    stale_wazuh = (
        db.query(Vulnerability)
        .filter(
            Vulnerability.asset_id == asset.id,
            Vulnerability.status == VulnerabilityStatus.open,
            Vulnerability.sources.contains('"wazuh"'),
        )
        .all()
    )
    for v in stale_wazuh:
        if v.cve_id in active_cves:
            continue  # still seen by Wazuh — keep source
        v.remove_source("wazuh")
        if not v.source_list:
            old_st = v.status
            v.status = VulnerabilityStatus.patched
            v.patched_at = datetime.now()
            try:
                log_vulnerability_change(
                    db, None, v.id, old_st, v.status,
                    reason=f"Wazuh agent {agent_id} no longer reports this CVE on {asset.hostname}",
                    cve_id=v.cve_id,
                    source="wazuh_sync",
                )
            except Exception as e:
                logger.warning("audit log for wazuh auto-patch failed (vuln_id=%s): %s", v.id, e)

    # Update asset last_scan
    asset.last_scan = datetime.now()
    db.commit()
    logger.info(f"Sync: Completed for agent {agent_id} ({asset.hostname}): {new_count} new, {updated_count} updated, {len(active_cves)} total active CVEs")

    # 4. Enrich newly created vulns with EPSS + KEV (best-effort, non-fatal)
    if new_count > 0:
        try:
            new_vulns = db.query(Vulnerability).filter(
                Vulnerability.asset_id == asset.id,
                Vulnerability.cve_id.in_(active_cves),
                Vulnerability.enrichment_updated_at.is_(None)
            ).all()
            if new_vulns:
                enrich_vulnerabilities(db, new_vulns)
        except Exception as e:
            logger.warning(f"Sync: Enrichment skipped for agent {agent_id}: {e}")

    # 5. Dispatch ONE digest mail per recipient for this agent's newly
    # discovered vulns (replaces per-CVE inline send). When this helper
    # is called from a multi-agent loop the caller may prefer to skip
    # per-agent dispatch and instead aggregate IDs and send a single
    # cross-agent digest at the very end — currently each per-agent call
    # produces its own digest.
    if new_vuln_ids:
        try:
            from app.services.email_service import dispatch_new_vuln_notifications
            fresh_vulns = db.query(Vulnerability).filter(
                Vulnerability.id.in_(new_vuln_ids)
            ).all()
            dispatch_new_vuln_notifications(db, fresh_vulns)
        except Exception as e:
            logger.warning(f"Sync: notification dispatch failed for agent {agent_id}: {e}")



def map_severity(cvss_score: Optional[float], wazuh_severity: Optional[str] = None) -> VulnerabilitySeverity:
    """Mappt CVSS-Score zu Severity-Enum, mit Wazuh-Severity als Fallback"""
    if cvss_score and cvss_score > 0:
        if cvss_score >= 9.0:
            return VulnerabilitySeverity.critical
        elif cvss_score >= 7.0:
            return VulnerabilitySeverity.high
        elif cvss_score >= 4.0:
            return VulnerabilitySeverity.medium
        else:
            return VulnerabilitySeverity.low

    # Fallback: Wazuh's eigene Severity-Einstufung verwenden
    if wazuh_severity:
        severity_lower = wazuh_severity.strip().lower()
        severity_map = {
            "critical": VulnerabilitySeverity.critical,
            "high": VulnerabilitySeverity.high,
            "medium": VulnerabilitySeverity.medium,
            "low": VulnerabilitySeverity.low,
        }
        mapped = severity_map.get(severity_lower)
        if mapped:
            logger.info(f"No CVSS score available, using Wazuh severity fallback: {wazuh_severity} -> {mapped.value}")
            return mapped

    return VulnerabilitySeverity.none


# ============================================
# CVSS Override Endpoints (CISA Vulnrichment)
# ============================================

class OverrideDryRunResponse(BaseModel):
    """Response for dry-run mode"""
    message: str
    would_be_updated: int
    checked: int
    placeholder_scores_found: int
    changes_preview: List[dict]


class OverrideStatsResponse(BaseModel):
    """Response after override execution"""
    message: str
    updated: int
    checked: int
    errors: int
    changes: List[dict]


@router.get("/override/check")
async def check_incorrect_scores(
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """
    Findet alle Vulnerabilities mit wahrscheinlich falschen Scores.
    
    Erkennt:
    - Platzhalter-Scores (10.0) von Wazuh
    - Unplausible Kombinationen (CVSS 10 + LOW Severity)
    
    Gibt eine Liste mit Empfehlungen zurück.
    """
    service = VulnOverrideService(db)
    incorrect = service.find_incorrect_scores()
    
    return {
        "total_found": len(incorrect),
        "placeholder_scores": len([i for i in incorrect if i.get("issue") == "placeholder_score_10"]),
        "severity_mismatches": len([i for i in incorrect if i.get("issue") == "severity_mismatch"]),
        "details": incorrect
    }


@router.post("/override/nessus/{asset_id}")
async def override_from_nessus(
    asset_id: int,
    scan_id: Optional[int] = None,
    dry_run: bool = Query(True, description="Nur simulieren ohne DB-Änderungen"),
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor)
):
    """
    Korrigiert CVSS-Scores und Severity-Werte basierend auf Nessus-Daten.
    
    Verwendet Nessus Plugin Output für verifizierte Scores.
    
    - **asset_id**: Asset-DB-ID
    - **scan_id**: Nessus Scan-ID (optional, wird aus Config ermittelt)
    - **dry_run**: Wenn True, nur simulieren ohne Änderungen
    """
    # Load Nessus config (transparently decrypted)
    from app.auth.setting_crypto import read_setting_value
    raw_nessus = read_setting_value(db, SETTING_KEY)
    if not raw_nessus:
        raise HTTPException(
            status_code=400,
            detail="Nessus configuration not set. Please configure Nessus in Settings."
        )

    try:
        config = json.loads(raw_nessus)
    except json.JSONDecodeError:
        raise HTTPException(status_code=400, detail="Invalid Nessus configuration format.")
    
    from app.integrations.nessus_client import NessusClient
    
    try:
        nessus_client = NessusClient(
            base_url=config["base_url"],
            access_key=config["access_key"],
            secret_key=config["secret_key"],
            verify_ssl=bool(config.get("verify_ssl", True)),
        )
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    
    service = VulnOverrideService(db)
    
    try:
        # Load verified data from Nessus
        if scan_id:
            verified_data = service.load_nessus_verified_data(asset_id, scan_id, nessus_client)
        else:
            # Use latest scan for this asset
            from app.models.asset import Asset
            asset = db.query(Asset).filter(Asset.id == asset_id).first()
            if not asset:
                raise HTTPException(status_code=404, detail=f"Asset {asset_id} not found")
            
            # Get recent scan IDs
            scans = nessus_client.list_scans()
            if not scans:
                raise HTTPException(status_code=404, detail="No Nessus scans found")
            
            # Find scan for this asset's IP
            target_scan_id = None
            for scan in scans:
                targets = scan.get("targets", "")
                if asset.ip_address and asset.ip_address in targets:
                    target_scan_id = int(scan["id"])
                    break
            
            if not target_scan_id:
                target_scan_id = int(scans[0]["id"])  # Fallback to first scan
            
            verified_data = service.load_nessus_verified_data(asset_id, target_scan_id, nessus_client)
        
        # Apply overrides
        if not verified_data:
            return {
                "message": "No verified CVE data found in Nessus for this asset",
                "updated": 0,
                "checked": 0
            }
        
        stats = service.apply_overrides(asset_id, verified_data, dry_run=dry_run)
        
        # Audit log
        if not dry_run and stats["updated"] > 0:
            audit_log = AuditLog(
                user_id=current_user.id,
                event_type=AuditEventType.VULNERABILITY_UPDATED,
                event_description=f"CVSS override applied from Nessus for {stats['updated']} vulnerabilities",
                resource_type="vulnerability",
                resource_id=f"asset:{asset_id}",
                timestamp=datetime.now()
            )
            db.add(audit_log)
            db.commit()
        
        return {
            "message": "Dry run - no changes applied" if dry_run else "Override applied successfully",
            **stats
        }
        
    finally:
        nessus_client.close()


@router.post("/override/vulnrichment")
async def override_from_vulnrichment(
    cve_ids: Optional[List[str]] = Query(None, description="CVE-IDs zum Korrigieren (leer = alle)"),
    asset_ids: Optional[List[int]] = Query(None, description="Asset-IDs zum Korrigieren (leer = alle)"),
    dry_run: bool = Query(True, description="Nur simulieren ohne DB-Änderungen"),
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor)
):
    """
    Korrigiert CVSS-Scores und Severity-Werte basierend auf CISA Vulnrichment.
    
    Verwendet den offiziellen CISA Vulnrichment JSON Feed für verifizierte
    CVSSv3.1 Base Scores und SSVC-Exploitation-Status.
    
    - **cve_ids**: Optionale CVE-Filterliste (leer = alle offenen CVEs)
    - **asset_ids**: Optionale Asset-Filterliste (leer = alle Assets)
    - **dry_run**: Wenn True, nur simulieren ohne Änderungen
    
    **Beispiel für CVE-2026-8390:**
    - Wazuh (falsch): CVSS 10.0, Severity CRITICAL
    - Vulnrichment (korrekt): CVSS 7.3, Severity HIGH, Exploitation "none"
    """
    result = correct_vulnerability_scores(
        db,
        cve_ids=cve_ids,
        asset_ids=asset_ids,
        dry_run=dry_run
    )
    
    # Audit log
    if not dry_run and result.get("updated", 0) > 0:
        audit_log = AuditLog(
            user_id=current_user.id,
            event_type=AuditEventType.VULNERABILITY_UPDATED,
            event_description=f"CVSS override from CISA Vulnrichment: {result['updated']} vulnerabilities corrected",
            resource_type="vulnerability",
            resource_id="bulk:vulnrichment",
            old_value=json.dumps({"method": "cisa_vulnrichment"}),
            new_value=json.dumps({"updated": result["updated"], "changes": result.get("changes", [])[:5]}),  # Limit to 5 for audit
            timestamp=datetime.now()
        )
        db.add(audit_log)
        db.commit()
    
    return result


@router.post("/override/vulnrichment/start")
async def start_vulnrichment_correction_job(
    cve_ids: Optional[List[str]] = Query(None, description="CVE-IDs zum Korrigieren (leer = alle)"),
    asset_ids: Optional[List[int]] = Query(None, description="Asset-IDs (leer = alle)"),
    dry_run: bool = Query(False, description="Nur simulieren ohne DB-Änderungen"),
    current_user: User = Depends(RequireEditor),
):
    """
    Async-Variante des Vulnrichment-Overrides.

    Spawnt einen Background-Thread und liefert sofort eine ``job_id``
    zurück. Fortschritt + Endergebnis via
    ``GET /override/vulnrichment/status/{job_id}``.

    Für Bulk-Korrekturen (Frontend "Correct CVSS" Button) verwenden —
    der synchrone Endpoint blockiert die Browser-Verbindung und löst
    bei großen Datenbanken den 23-Min-Hänger des Frontends aus.
    """
    job_id = start_vulnrichment_job(
        user_id=current_user.id,
        cve_ids=cve_ids,
        asset_ids=asset_ids,
        dry_run=dry_run,
    )
    return {
        "job_id": job_id,
        "status": "queued",
        "poll_url": f"/api/v1/vulnerabilities/override/vulnrichment/status/{job_id}",
    }


@router.get("/override/vulnrichment/status/{job_id}")
async def get_vulnrichment_correction_status(
    job_id: str,
    current_user: User = Depends(get_current_user),
):
    """
    Snapshot eines mit ``/override/vulnrichment/start`` gestarteten Jobs.

    Felder:
    - ``status``: queued | running | completed | failed
    - ``stage``: human-readable progress label (z.B. "downloading ZIP")
    - ``total`` / ``done``: numerische Progress-Hinweise
    - ``updated`` / ``checked`` / ``not_found``: End-Stats wenn fertig
    - ``error``: gesetzt bei status == "failed"
    """
    job = get_override_job(job_id)
    if not job:
        raise HTTPException(
            status_code=404,
            detail=f"Job {job_id} not found (may have expired after backend restart)",
        )
    return job


@router.get("/override/vulnrichment/jobs")
async def list_vulnrichment_correction_jobs(
    limit: int = Query(10, le=50),
    current_user: User = Depends(get_current_user),
):
    """Most-recent-first list of override jobs (for an admin overview)."""
    return list_override_jobs(limit=limit)


@router.post("/recompute-scores")
async def recompute_priority_scores(
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor),
):
    """One-off backfill of priority_score + cpr_score columns.

    Migration 024 adds the columns but leaves them NULL. Forward
    sync / enrich / override paths keep them current going forward.
    Existing rows need this endpoint once after the upgrade. Cheap —
    pure Python, ~5s for 15k rows.
    """
    updated = 0
    for vuln in db.query(Vulnerability).all():
        try:
            vuln.refresh_scores()
            updated += 1
        except Exception as e:
            logger.warning("refresh_scores failed for vuln_id=%s: %s", vuln.id, e)
    db.commit()
    return {"updated": updated}


@router.get("/override/stats")
async def get_override_stats(
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """
    Gibt Statistiken über potenzielle Score-Fehler zurück.
    
    Zeigt:
    - Anzahl Platzhalter-Scores (10.0)
    - Anzahl Severity-Fehlanpassungen
    - Verteilung nach Severity-Level
    """
    from sqlalchemy import func
    
    # Count placeholder scores
    placeholder_count = db.query(func.count(Vulnerability.id)).filter(
        Vulnerability.cvss_score == 10.0
    ).scalar()
    
    # Count severity mismatches (CVSS >= 9 but not CRITICAL)
    mismatch_count = db.query(func.count(Vulnerability.id)).filter(
        Vulnerability.cvss_score >= 9.0,
        Vulnerability.severity != VulnerabilitySeverity.critical
    ).scalar()
    
    # Severity distribution
    severity_dist = db.query(
        Vulnerability.severity,
        func.count(Vulnerability.id)
    ).filter(
        Vulnerability.status == VulnerabilityStatus.open
    ).group_by(Vulnerability.severity).all()
    
    # CVSS distribution
    cvss_ranges = [
        ("0.0-3.9", 0.0, 3.9),
        ("4.0-6.9", 4.0, 6.9),
        ("7.0-8.9", 7.0, 8.9),
        ("9.0-10.0", 9.0, 10.0),
    ]
    cvss_dist = []
    for label, low, high in cvss_ranges:
        count = db.query(func.count(Vulnerability.id)).filter(
            Vulnerability.cvss_score >= low,
            Vulnerability.cvss_score < high if high < 10.0 else Vulnerability.cvss_score <= high
        ).scalar()
        cvss_dist.append({"range": label, "count": count})
    
    return {
        "placeholder_scores_10": placeholder_count,
        "severity_mismatches": mismatch_count,
        "severity_distribution": {str(k): v for k, v in severity_dist},
        "cvss_distribution": cvss_dist,
        "recommendation": "Run POST /api/v1/vulnerabilities/override/vulnrichment with dry_run=false to correct values"
    }
