"""
Asset/Inventory Management Router

Endpoints for asset management (Wazuh Agents + manual systems).
"""
from typing import Optional, List, Any
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status, Query, Response
from sqlalchemy.orm import Session
from sqlalchemy import asc, desc, func, nulls_last
from pydantic import BaseModel, field_validator

from app.database import get_db
from app.models.user import User
from app.models.asset import Asset, AssetSource, AssetStatus
from app.models.policy import Policy
from app.models.vulnerability import Vulnerability, VulnerabilityStatus
from app.models.audit_log import AuditLog, AuditEventType
from app.auth.dependencies import get_current_user, RequireEditor, RequireAdmin
import logging

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/v1/assets", tags=["Assets"])


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

class AssetResponse(BaseModel):
    id: int
    hostname: str
    ip_address: Optional[str]
    wazuh_agent_id: Optional[str]
    nessus_host_uuid: Optional[str] = None
    # Intune/Defender ids — surfaced so the UI can offer the software/app-scan
    # actions on MDM-managed (iOS/macOS/Android) assets, not just Wazuh hosts.
    intune_device_id: Optional[str] = None
    defender_machine_id: Optional[str] = None
    operating_system: Optional[str]
    os_version: Optional[str]
    source: AssetSource
    status: AssetStatus
    last_scan: Optional[datetime]
    last_seen: Optional[datetime]
    vulnerability_count: Optional[int] = 0
    policy_id: Optional[int] = None
    description: Optional[str] = None
    location: Optional[str] = None
    owner: Optional[str] = None
    assigned_user_id: Optional[int] = None
    assigned_user_name: Optional[str] = None
    # assigned_group_id obsolete, replaced by groups list
    groups: List[str] = [] # List of Team Names for display
    policy_name: Optional[str] = None
    criticality: Optional[str] = None
    # Network-exposure risk dimension
    network_exposure_score: Optional[float] = None
    exposed_services: Optional[List[dict]] = None
    exposure_updated_at: Optional[datetime] = None
    # Risk Dimensions (crown-jewel roles)
    high_value_score: Optional[float] = None
    risk_dimensions: Optional[List[dict]] = None

    @field_validator('exposed_services', 'risk_dimensions', mode='before')
    @classmethod
    def parse_json_list(cls, v: Any) -> Optional[List[dict]]:
        if v is None or isinstance(v, list):
            return v
        if isinstance(v, str):
            try:
                import json as _j
                parsed = _j.loads(v)
                return parsed if isinstance(parsed, list) else None
            except Exception:
                return None
        return None

    @field_validator('groups', mode='before')
    @classmethod
    def map_groups_to_names(cls, v: Any) -> List[str]:
        if not v:
            return []
        # Wenn es bereits Strings sind (z.B. manuell gesetzt)
        if isinstance(v, list) and all(isinstance(x, str) for x in v):
            return v
        # Wenn es SQLAlchemy-Objekte sind
        return [g.name for g in v if hasattr(g, 'name')]

    class Config:
        from_attributes = True


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


class AssetCreateRequest(BaseModel):
    hostname: str
    ip_address: Optional[str] = None
    operating_system: Optional[str] = None
    os_version: Optional[str] = None
    description: Optional[str] = None
    location: Optional[str] = None
    owner: Optional[str] = None
    policy_id: Optional[int] = None


class AssetUpdateRequest(BaseModel):
    hostname: Optional[str] = None
    ip_address: Optional[str] = None
    operating_system: Optional[str] = None
    os_version: Optional[str] = None
    status: Optional[AssetStatus] = None
    description: Optional[str] = None
    location: Optional[str] = None
    owner: Optional[str] = None
    policy_id: Optional[int] = None
    # Plan E — URS inputs (criticality multiplier + framework list)
    criticality: Optional[str] = None        # low | normal | high | critical
    compliance_frameworks: Optional[List[str]] = None


class BulkAssetUpdateRequest(BaseModel):
    asset_ids: List[int]
    assigned_user_id: Optional[int] = None
    assigned_group_id: Optional[int] = None
    policy_id: Optional[int] = None


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

@router.post("/bulk-update")
async def bulk_update_assets(
    update_data: BulkAssetUpdateRequest,
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor)
):
    """Updates multiple assets at once (assignment, policy)"""
    assets = db.query(Asset).filter(Asset.id.in_(update_data.asset_ids)).all()
    
    if not assets:
        raise HTTPException(status_code=404, detail="No assets 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
    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
    if update_data.policy_id is not None:
        update_fields["policy_id"] = update_data.policy_id if update_data.policy_id != -1 else None

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

    count = 0
    from app.models.group import Group
    
    for asset in assets:
        for field, value in update_fields.items():
            if field == "assigned_group_id":
                # Special handling for legacy group assignment
                if value is None:
                    asset.groups = []
                else:
                    group = db.query(Group).filter(Group.id == value).first()
                    if group:
                        asset.groups = [group]
            else:
                setattr(asset, field, value)
        count += 1
    
    db.commit()

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

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


@router.post("/reconcile-lifecycle")
async def reconcile_lifecycle(
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Manually run the soft-inactive reconcile (also runs 04:15 nightly).

    Flips assets no source reported within asset_inactive_after_days to
    INACTIVE; revives recently-seen INACTIVE ones. History + audit kept.
    """
    from app.services.asset_lifecycle import reconcile_asset_lifecycle
    return reconcile_asset_lifecycle(db)


@router.get("/{asset_id}/coverage-gap")
def asset_coverage_gap(
    asset_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Coverage-gap report: installed packages with NO vuln finding.

    Tester: Wazuh's vuln-detector misses some products (MS365,
    mRemoteNG, …). This surfaces the GAP — packages present in
    syscollector but not matched to any open finding — so the operator
    can investigate manually. Deliberately makes NO automatic CVE
    assignment (zero false positives). Annotates each gap package with
    its endoflife.date status when known.
    """
    import json as _json
    from app.auth.setting_crypto import read_setting_value
    from app.integrations.wazuh_client import WazuhClient
    from app.services import eol_service

    asset = db.query(Asset).filter(Asset.id == asset_id).first()
    if not asset:
        raise HTTPException(404, "Asset not found")
    if not asset.wazuh_agent_id:
        raise HTTPException(422, "Asset has no Wazuh agent — no syscollector inventory")

    raw = read_setting_value(db, "wazuh_config")
    if not raw:
        raise HTTPException(400, "Wazuh is not configured")
    cfg = _json.loads(raw)
    wazuh = WazuhClient(
        base_url=cfg.get("api_url"), username=cfg.get("username"),
        password=cfg.get("password"), indexer_url=cfg.get("indexer_url"),
        indexer_username=cfg.get("indexer_username"),
        indexer_password=cfg.get("indexer_password"),
        verify_ssl=cfg.get("verify_ssl", False),
    )
    try:
        packages = wazuh.get_packages(asset.wazuh_agent_id) or []
    except Exception as e:
        raise HTTPException(502, f"Could not fetch packages from Wazuh: {e}")

    # Build the "covered" set: package names that already have an open
    # finding on this asset (parent package_name + child rows).
    covered_blob = " ".join(
        (v.package_name or "").lower()
        for v in db.query(Vulnerability)
            .filter(Vulnerability.asset_id == asset_id,
                    Vulnerability.status == VulnerabilityStatus.open)
            .all()
    )

    seen: set = set()
    gap: list[dict] = []
    covered_count = 0
    for pkg in packages:
        name = (pkg.get("name") or "").strip()
        version = (pkg.get("version") or "").strip()
        if not name:
            continue
        key = (name.lower(), version)
        if key in seen:
            continue
        seen.add(key)
        # Covered if the package name (token) appears in any finding's
        # package_name blob. Token match avoids substring noise on very
        # short names by requiring length >= 3.
        nl = name.lower()
        is_covered = len(nl) >= 3 and nl in covered_blob
        if is_covered:
            covered_count += 1
            continue
        # Annotate EOL status if endoflife knows the product (cheap —
        # cached). Only adds a hint, never a CVE claim.
        eol_hint = None
        try:
            st = eol_service.check_eol(db, name, version) if eol_service.resolve_product_slug(name) else None
            if st:
                if st.is_eol:
                    eol_hint = "EOL"
                elif st.is_eol_soon:
                    eol_hint = f"EOL SOON ({st.days_to_eol}d)"
                elif st.is_eoas:
                    eol_hint = "out-of-active-support"
        except Exception:
            pass
        gap.append({"name": name[:255], "version": version[:100], "eol_hint": eol_hint})

    gap.sort(key=lambda g: (g["eol_hint"] is None, g["name"].lower()))
    return {
        "asset_id": asset_id,
        "hostname": asset.hostname,
        "total_packages": len(seen),
        "covered": covered_count,
        "gap_count": len(gap),
        "gap": gap,
    }


@router.get("/{asset_id}/software")
def asset_software(
    asset_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """On-demand installed-software inventory for one asset.

    Pulled LIVE (not persisted) from whichever source backs the asset:
    Wazuh syscollector packages, else Intune detectedApps. This is the same
    inventory the app-CVE scanner consumes — surfaced read-only so the operator
    can see what's installed without a DB table.
    """
    import json as _json
    from app.auth.setting_crypto import read_setting_value

    asset = db.query(Asset).filter(Asset.id == asset_id).first()
    if not asset:
        raise HTTPException(404, "Asset not found")

    software: list[dict] = []
    source = None
    if asset.wazuh_agent_id:
        from app.integrations.wazuh_client import WazuhClient
        raw = read_setting_value(db, "wazuh_config")
        if not raw:
            raise HTTPException(400, "Wazuh is not configured")
        cfg = _json.loads(raw)
        wazuh = WazuhClient(
            base_url=cfg.get("api_url"), username=cfg.get("username"),
            password=cfg.get("password"), indexer_url=cfg.get("indexer_url"),
            indexer_username=cfg.get("indexer_username"),
            indexer_password=cfg.get("indexer_password"),
            verify_ssl=cfg.get("verify_ssl", False),
        )
        try:
            pkgs = wazuh.get_packages(asset.wazuh_agent_id) or []
        except Exception as e:
            raise HTTPException(502, f"Could not fetch packages from Wazuh: {e}")
        source = "wazuh"
        software = [{"name": (p.get("name") or "").strip(),
                     "version": (p.get("version") or "").strip(),
                     "vendor": (p.get("vendor") or p.get("format") or "").strip()} for p in pkgs]
    elif asset.intune_device_id:
        from app.services.intune_service import load_intune_config, _build_client
        icfg = load_intune_config(db)
        if not icfg:
            raise HTTPException(400, "Intune is not configured")
        client = _build_client(icfg)
        try:
            apps = client.get_detected_apps(asset.intune_device_id) or []
        except Exception as e:
            raise HTTPException(502, f"Could not fetch detectedApps from Intune: {e}")
        finally:
            client.close()
        source = "intune"
        software = [{"name": (a.get("name") or "").strip(),
                     "version": (a.get("version") or "").strip(),
                     "vendor": (a.get("vendor") or "").strip()} for a in apps]
    else:
        raise HTTPException(422, "Asset has no Wazuh agent or Intune device — no live software inventory")

    # Dedup (name, version) + drop nameless, sort by name.
    seen: set = set()
    out: list[dict] = []
    for s in software:
        if not s["name"]:
            continue
        key = (s["name"].lower(), s["version"])
        if key in seen:
            continue
        seen.add(key)
        out.append(s)
    out.sort(key=lambda s: s["name"].lower())
    return {"asset_id": asset_id, "hostname": asset.hostname, "source": source,
            "count": len(out), "software": out}


@router.post("/refresh-exposure")
def refresh_exposure(
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Refresh network-exposure scores from Wazuh syscollector ports.

    Classifies risky listeners (VNC/RDP/Telnet/SMB/…) per asset into a
    0-100 exposure score + service list. Informational risk dimension —
    does not auto-change criticality.
    """
    import json as _json
    from app.auth.setting_crypto import read_setting_value
    from app.integrations.wazuh_client import WazuhClient
    from app.services.exposure_service import refresh_all_exposure

    raw = read_setting_value(db, "wazuh_config")
    if not raw:
        raise HTTPException(400, "Wazuh is not configured")
    try:
        cfg = _json.loads(raw)
    except _json.JSONDecodeError:
        raise HTTPException(400, "Invalid Wazuh configuration format")
    if not all([cfg.get("api_url"), cfg.get("username"), cfg.get("password")]):
        raise HTTPException(400, "Incomplete Wazuh configuration")
    wazuh = WazuhClient(
        base_url=cfg.get("api_url"),
        username=cfg.get("username"),
        password=cfg.get("password"),
        indexer_url=cfg.get("indexer_url"),
        indexer_username=cfg.get("indexer_username"),
        indexer_password=cfg.get("indexer_password"),
        verify_ssl=cfg.get("verify_ssl", False),
    )
    return refresh_all_exposure(db, wazuh)


@router.get("", response_model=List[AssetResponse])
async def list_assets(
    status: Optional[AssetStatus] = Query(None),
    source: Optional[AssetSource] = Query(None),
    search: Optional[str] = Query(None, description="Suche in Hostname, IP"),
    include_inactive: bool = Query(False, description="Also include DECOMMISSIONED assets (INACTIVE are shown by default)"),
    sort_by: str = Query("hostname", description="hostname, ip_address, operating_system, status, last_scan, network_exposure_score, policy_name, assigned_user_name"),
    sort_order: str = Query("asc", description="asc, desc"),
    limit: int = Query(100, le=1000),
    offset: int = Query(0),
    response: Response = None,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """Liste aller Assets mit Filterung.

    Default zeigt ACTIVE **und** INACTIVE — der Sinn der Status-Spalte ist
    ja, soft-inactive Assets (kein source-sync innerhalb
    asset_inactive_after_days) mit Badge sichtbar zu halten, nicht sie zu
    verstecken. Nur DECOMMISSIONED (operator-final) ist per default
    ausgeblendet; include_inactive=true zeigt auch die.
    Vuln-historie + audit bleiben erhalten.
    """
    query = db.query(Asset)

    if status:
        query = query.filter(Asset.status == status)
    elif include_inactive:
        # everything (active + inactive + decommissioned)
        pass
    else:
        # active + inactive visible; hide only operator-retired
        query = query.filter(Asset.status != AssetStatus.DECOMMISSIONED)

    if source:
        # Filter by the actual sync linkage, not the creation-time `source`
        # enum: an asset first created by Wazuh and later matched by Nessus
        # keeps source=WAZUH but carries a nessus_host_uuid. The per-scanner
        # id columns are the multi-source truth, so a merged asset correctly
        # appears under every scanner that sees it.
        def _has_vuln_source(tag: str):
            # Asset has ≥1 finding reported by `tag` (the vuln-level `sources`
            # list is the maintained multi-source truth). Catches Nessus
            # imports whose host had no nessus_host_uuid (matched by hostname).
            return Asset.id.in_(
                db.query(Vulnerability.asset_id)
                  .filter(Vulnerability.sources.contains(f'"{tag}"'))
            )

        if source == AssetSource.WAZUH:
            query = query.filter(Asset.wazuh_agent_id.isnot(None) | _has_vuln_source("wazuh"))
        elif source == AssetSource.NESSUS:
            query = query.filter(Asset.nessus_host_uuid.isnot(None) | _has_vuln_source("nessus"))
        elif source == AssetSource.INTUNE:
            # Intune devices often carry no findings → id columns are the signal.
            query = query.filter(
                Asset.intune_device_id.isnot(None) | Asset.defender_machine_id.isnot(None)
                | _has_vuln_source("intune") | _has_vuln_source("defender"))
        elif source == AssetSource.MANUAL:
            # genuinely manual = no scanner linkage AND no scanner-sourced vuln
            query = query.filter(
                Asset.wazuh_agent_id.is_(None), Asset.nessus_host_uuid.is_(None),
                Asset.intune_device_id.is_(None), Asset.defender_machine_id.is_(None),
                ~_has_vuln_source("wazuh"), ~_has_vuln_source("nessus"),
                ~_has_vuln_source("intune"), ~_has_vuln_source("defender"))
        else:
            query = query.filter(Asset.source == source)

    if search:
        search_pattern = f"%{search}%"
        query = query.filter(
            (Asset.hostname.ilike(search_pattern)) |
            (Asset.ip_address.ilike(search_pattern))
        )

    # Whitelist-driven ORDER BY. Always map to a real SQLAlchemy column;
    # never pass `sort_by` straight to .order_by() (SQL-injection surface).
    # Joins are added lazily so non-join sorts don't pay the join cost.
    _SORT_MAP_DIRECT = {
        "hostname": Asset.hostname,
        "ip_address": Asset.ip_address,
        "operating_system": Asset.operating_system,
        "status": Asset.status,
        "last_scan": Asset.last_scan,
        "network_exposure_score": Asset.network_exposure_score,
        "high_value_score": Asset.high_value_score,
    }
    sort_dir = desc if sort_order == "desc" else asc
    if sort_by in _SORT_MAP_DIRECT:
        col = _SORT_MAP_DIRECT[sort_by]
        if sort_by == "last_scan":
            # NULL-safe on BOTH directions — never-scanned assets land at
            # the bottom regardless of asc/desc (PG: NULLS LAST is independent
            # of the primary direction).
            query = query.order_by(nulls_last(sort_dir(col)))
        elif sort_by == "hostname":
            # Case-insensitive alpha sort — a host called "alpine" must
            # not outrank "Webserver" because the literal `A` > `W`.
            query = query.order_by(nulls_last(sort_dir(func.lower(col))))
        else:
            # NULLs always last (both directions) — sorting Exposure/Risk
            # desc must surface the real high scores first, not the empty
            # ("—") rows. (Tester: desc showed blanks before real values.)
            query = query.order_by(nulls_last(sort_dir(col)))
    elif sort_by == "policy_name":
        query = query.outerjoin(Policy, Asset.policy_id == Policy.id).order_by(sort_dir(Policy.name))
    elif sort_by == "assigned_user_name":
        query = query.outerjoin(User, Asset.assigned_user_id == User.id).order_by(sort_dir(User.username))
    else:
        # Unknown / unsupported sort_by — fall back to hostname asc.
        query = query.order_by(asc(Asset.hostname))

    # Total (pre-pagination) so the UI can page; returned as a header to
    # keep the response body a plain array (other consumers expect a list).
    total_count = query.order_by(None).count()
    response.headers["X-Total-Count"] = str(total_count)
    response.headers["Access-Control-Expose-Headers"] = "X-Total-Count"

    assets = query.offset(offset).limit(limit).all()

    # Vulnerability-Counts + assigned user info
    results = []
    for asset in assets:
        vuln_count = db.query(Vulnerability).filter(
            Vulnerability.asset_id == asset.id,
            Vulnerability.status == VulnerabilityStatus.open
        ).count()

        asset.vulnerability_count = vuln_count
        asset.assigned_user_name = asset.assigned_user.username if asset.assigned_user else None
        asset.policy_name = asset.policy.name if asset.policy else None
        results.append(asset)

    return results


@router.get("/{asset_id}", response_model=AssetResponse)
async def get_asset_detail(
    asset_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """Asset-Details"""
    asset = db.query(Asset).filter(Asset.id == asset_id).first()

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

    vuln_count = db.query(Vulnerability).filter(
        Vulnerability.asset_id == asset_id,
        Vulnerability.status == VulnerabilityStatus.open
    ).count()

    asset.vulnerability_count = vuln_count
    return asset


@router.post("", status_code=status.HTTP_201_CREATED, response_model=AssetResponse)
async def create_asset(
    asset_data: AssetCreateRequest,
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor)
):
    """
    Erstellt manuelles Asset (für Systeme ohne Wazuh-Agent)
    """
    # Check for duplicate hostname
    existing_hostname = db.query(Asset).filter(
        Asset.hostname == asset_data.hostname
    ).first()
    if existing_hostname:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Asset with this hostname already exists"
        )

    # Check for duplicate IP (only if IP is provided)
    if asset_data.ip_address:
        existing_ip = db.query(Asset).filter(
            Asset.ip_address == asset_data.ip_address
        ).first()
        if existing_ip:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Asset with this IP address already exists"
            )

    # Erstelle Asset
    asset = Asset(
        **asset_data.model_dump(),
        source=AssetSource.MANUAL,
        status=AssetStatus.ACTIVE
    )

    db.add(asset)
    db.commit()
    db.refresh(asset)

    # Audit-Log
    audit_log = AuditLog(
        user_id=current_user.id,
        event_type=AuditEventType.ASSET_CREATED,
        event_description=f"Manual asset created: {asset.hostname}",
        resource_type="asset",
        resource_id=str(asset.id),
        timestamp=datetime.now()
    )
    db.add(audit_log)
    db.commit()

    asset.vulnerability_count = 0
    return asset


@router.put("/{asset_id}", response_model=AssetResponse)
async def update_asset(
    asset_id: int,
    update_data: AssetUpdateRequest,
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor)
):
    """Aktualisiert Asset-Daten"""
    asset = db.query(Asset).filter(Asset.id == asset_id).first()

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

    # Update nur gesetzte Felder
    import json as _json
    for field, value in update_data.model_dump(exclude_unset=True).items():
        if field == "compliance_frameworks":
            # Stored as TEXT JSON list
            value = _json.dumps(value or []) if value is not None else None
        if field == "criticality" and value is not None:
            value = value.lower()
            if value not in ("low", "normal", "high", "critical"):
                raise HTTPException(400, "criticality must be one of: low, normal, high, critical")
        setattr(asset, field, value)

    db.commit()
    db.refresh(asset)

    # Audit-Log
    audit_log = AuditLog(
        user_id=current_user.id,
        event_type=AuditEventType.ASSET_UPDATED,
        event_description=f"Asset updated: {asset.hostname}",
        resource_type="asset",
        resource_id=str(asset.id),
        timestamp=datetime.now()
    )
    db.add(audit_log)
    db.commit()

    vuln_count = db.query(Vulnerability).filter(
        Vulnerability.asset_id == asset_id,
        Vulnerability.status == VulnerabilityStatus.open
    ).count()

    asset.vulnerability_count = vuln_count
    return asset


@router.delete("/{asset_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_asset(
    asset_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireAdmin)
):
    """
    Löscht Asset (Admin-only)

    ACHTUNG: Löscht auch alle zugehörigen Vulnerabilities (CASCADE)
    """
    asset = db.query(Asset).filter(Asset.id == asset_id).first()

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

    # Revisionssicher audit — snapshot the full asset + cascade impact
    # BEFORE deletion so the trail says WHO deleted WHAT (and how much
    # history it took with it), analog to the CVE status-change entries.
    import json as _json
    vuln_count = db.query(Vulnerability).filter(
        Vulnerability.asset_id == asset.id
    ).count()
    snapshot = {
        "hostname": asset.hostname,
        "ip_address": asset.ip_address,
        "operating_system": asset.operating_system,
        "source": asset.source.value if hasattr(asset.source, "value") else str(asset.source),
        "status": asset.status.value if hasattr(asset.status, "value") else str(asset.status),
        "wazuh_agent_id": asset.wazuh_agent_id,
        "vulnerabilities_cascade_deleted": vuln_count,
        "deleted_by": current_user.username,
    }
    audit_log = AuditLog(
        user_id=current_user.id,
        event_type=AuditEventType.ASSET_DELETED,
        event_description=(
            f"Asset '{asset.hostname}' ({asset.ip_address or 'no-ip'}) deleted by "
            f"{current_user.username} — {vuln_count} vulnerabilities cascade-removed"
        )[:500],
        resource_type="asset",
        resource_id=str(asset.id),
        old_value=_json.dumps(snapshot),
        new_value="deleted",
        timestamp=datetime.now()
    )
    db.add(audit_log)
    db.commit()  # persist audit first so it survives even if delete fails

    db.delete(asset)
    db.commit()

    return None


@router.patch("/{asset_id}/assign")
async def assign_asset(
    asset_id: int,
    assign_data: AssetAssignRequest,
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor)
):
    """Assigns a user to an asset"""
    asset = db.query(Asset).filter(Asset.id == asset_id).first()
    if not asset:
        raise HTTPException(status_code=404, detail="Asset 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")
    
    asset.assigned_user_id = assign_data.user_id

    # Handle single group assignment (legacy support)
    from app.models.group import Group
    if assign_data.group_id is None:
        # Clear groups when explicitly set to null
        asset.groups = []
    else:
        group = db.query(Group).filter(Group.id == assign_data.group_id).first()
        if group:
            asset.groups = [group]
    
    db.commit()
    db.refresh(asset)

    vuln_count = db.query(Vulnerability).filter(
        Vulnerability.asset_id == asset_id,
        Vulnerability.status == VulnerabilityStatus.open
    ).count()
    asset.vulnerability_count = vuln_count
    asset.assigned_user_name = asset.assigned_user.username if asset.assigned_user else None
    return asset


@router.get("/{asset_id}/vulnerabilities")
async def get_asset_vulnerabilities(
    asset_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user)
):
    """Alle Vulnerabilities eines Assets"""
    asset = db.query(Asset).filter(Asset.id == asset_id).first()

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

    vulns = db.query(Vulnerability).filter(
        Vulnerability.asset_id == asset_id
    ).all()

    return [{
        "id": v.id,
        "cve_id": v.cve_id,
        "severity": v.severity,
        "status": v.status,
        "cvss_score": v.cvss_score,
        "package_name": v.package_name,
        "detected_at": v.detected_at
    } for v in vulns]

# --- Wazuh Sync Endpoint ---
import json
from app.integrations.wazuh_client import WazuhClient, WazuhAPIError
from app.models.setting import Setting

@router.post("/sync_wazuh", response_model=dict)
def sync_wazuh_assets(
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor)
):
    """
    Synchronizes assets from Wazuh Manager.
    fetch active agents -> update/create local assets.
    """
    # 1. Get Wazuh Config (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.")

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

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

    indexer_url = config.get("indexer_url")
    indexer_username = config.get("indexer_username")
    indexer_password = config.get("indexer_password")

    stats = {"created": 0, "updated": 0, "total_from_wazuh": 0, "assets_inactivated": 0, "assets_reactivated": 0}
    seen_wazuh_agent_ids: set = set()  # for sync-driven asset reconciliation

    # 2. Fetch Agents from Wazuh
    try:
        with 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)
        ) as client:
            agents = client.get_agents(status="active") # Only sync active agents? or all?
            # Let's get all to be safe, filter by active later if needed
            # agents = client.get_agents() 
            
            stats["total_from_wazuh"] = len(agents)

            for agent in agents:
                agent_id = agent.get("id")
                if agent_id == "000": continue # Skip manager itself
                seen_wazuh_agent_ids.add(agent_id)

                hostname = agent.get("name")
                ip = agent.get("ip")
                os_info = agent.get("os", {})
                os_name = os_info.get("name")
                os_version = os_info.get("version")
                
                # Check if exists by Wazuh ID
                asset = db.query(Asset).filter(Asset.wazuh_agent_id == agent_id).first()
                
                if not asset:
                    # Check by hostname to avoid duplicates if manually added
                    asset = db.query(Asset).filter(Asset.hostname == hostname).first()

                if asset:
                    # Update
                    asset.wazuh_agent_id = agent_id
                    asset.ip_address = ip or asset.ip_address
                    asset.operating_system = os_name or asset.operating_system
                    asset.os_version = os_version or asset.os_version
                    asset.last_seen = datetime.now()
                    asset.source = AssetSource.WAZUH
                    asset.status = AssetStatus.ACTIVE if agent.get("status") == "active" else AssetStatus.INACTIVE
                    stats["updated"] += 1
                else:
                    # Create
                    # Default Group Logic
                    default_group_setting = db.query(Setting).filter(Setting.key == "default_group_id").first()
                    group_id = int(default_group_setting.value) if default_group_setting and default_group_setting.value else None

                    new_asset = Asset(
                        hostname=hostname,
                        ip_address=ip,
                        wazuh_agent_id=agent_id,
                        operating_system=os_name,
                        os_version=os_version,
                        source=AssetSource.WAZUH,
                        status=AssetStatus.ACTIVE,
                        last_seen=datetime.now()
                    )
                    
                    # Handle default group
                    if group_id:
                        from app.models.group import Group
                        default_group = db.query(Group).filter(Group.id == group_id).first()
                        if default_group:
                            new_asset.groups.append(default_group)

                    db.add(new_asset)
                    stats["created"] += 1

            db.commit()

            # Sync-driven reconciliation: any ACTIVE WAZUH-sourced asset whose
            # wazuh_agent_id was not returned by this sync (deleted on the
            # Wazuh server, or no longer in any agent list) is flipped to
            # INACTIVE. Vice-versa for re-activation. Audit-logged.
            from app.services.asset_lifecycle import reconcile_missing_from_sync
            recon = reconcile_missing_from_sync(
                db,
                source=AssetSource.WAZUH,
                seen_ids=seen_wazuh_agent_ids,
                id_field=Asset.wazuh_agent_id,
                reason="not in latest Wazuh agents list",
            )
            stats["assets_inactivated"] = recon["inactivated"]
            stats["assets_reactivated"] = recon["reactivated"]
            if recon["inactivated"] or recon["reactivated"]:
                db.commit()

    except Exception as e:
        logger.error(f"Wazuh Sync Failed: {e}")
        raise HTTPException(status_code=500, detail="Wazuh sync failed. Check server logs for details.")

    return stats


@router.post("/{asset_id}/rescan", response_model=dict)
def rescan_asset(
    asset_id: int,
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireEditor)
):
    """
    Triggert einen gezielten Rescan eines einzelnen Assets via Wazuh.
    """
    from app.routers.vulnerabilities import sync_agent_vulnerabilities
    
    asset = db.query(Asset).filter(Asset.id == asset_id).first()
    if not asset:
        raise HTTPException(status_code=404, detail="Asset not found")
    
    if asset.source != AssetSource.WAZUH or not asset.wazuh_agent_id:
        raise HTTPException(status_code=400, detail="Rescan only possible for Wazuh-managed assets")

    # 1. Wazuh Config holen (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 missing")

    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", False)
    )

    try:
        # Sync Vulnerabilities for this specific agent
        sync_agent_vulnerabilities(db, wazuh, asset.wazuh_agent_id, asset)
        
        # Audit Log
        audit = AuditLog(
            user_id=current_user.id,
            event_type=AuditEventType.SCAN_TRIGGERED,
            event_description=f"Manual rescan started for asset {asset.hostname} (ID: {asset.wazuh_agent_id})",
            resource_type="asset",
            resource_id=str(asset_id),
            timestamp=datetime.now()
        )
        db.add(audit)
        db.commit()
        
        return {"message": f"Rescan for {asset.hostname} successfully completed.", "last_scan": asset.last_scan}
    except Exception as e:
        logger.error(f"Rescan error: {e}")
        raise HTTPException(status_code=500, detail=f"Rescan failed: {str(e)}")
