"""
Shared audit-event writers for sync-driven changes.

Tester requirement (revisionssicher): a finding newly created by a
Wazuh/Nessus sync must leave an initial "VULNERABILITY_DETECTED" trail —
previously the audit history only began with the first status change.
"""
import logging
from datetime import datetime
from typing import Iterable

from sqlalchemy.orm import Session

from app.models.audit_log import AuditLog, AuditEventType
from app.models.vulnerability import Vulnerability

logger = logging.getLogger(__name__)


def audit_new_vulnerabilities(
    db: Session,
    vuln_ids: Iterable[int],
    *,
    source: str,
) -> int:
    """Write one VULNERABILITY_DETECTED audit row per newly created finding.

    `source` names the detector ("wazuh", "nessus", "eol_check", ...).
    user_id stays NULL → the audit UI renders it as System/Auto, matching
    the asset-lifecycle events. Caller commits. Returns rows written.
    """
    ids = [i for i in vuln_ids if i]
    if not ids:
        return 0
    rows = (
        db.query(Vulnerability)
        .filter(Vulnerability.id.in_(ids))
        .all()
    )
    written = 0
    now = datetime.now()
    for v in rows:
        hostname = v.asset.hostname if v.asset else f"asset #{v.asset_id}"
        desc = (
            f"New finding detected: {v.cve_id} on {hostname} "
            f"(severity={v.severity.value if hasattr(v.severity, 'value') else v.severity}, "
            f"source={source})"
        )
        db.add(AuditLog(
            user_id=None,  # System/Auto
            event_type=AuditEventType.VULNERABILITY_DETECTED,
            event_description=desc[:500],
            resource_type="vulnerability",
            resource_id=str(v.id),
            timestamp=now,
        ))
        written += 1
    if written:
        logger.info("audit: %d VULNERABILITY_DETECTED events (%s)", written, source)
    return written


def reopen_if_patched(db: Session, vuln, *, reason: str, source: str) -> bool:
    """Flip a patched finding back to OPEN and AUDIT the transition.

    Every scanner reopens findings it sees again after they were closed, but
    each one did it inline without writing a status-change row — so only the
    positive direction (open → patched) ever appeared in the audit log and the
    per-CVE Change History. A finding could silently go patched → open, which
    is exactly the transition an auditor most wants to see (tester flagged it).

    Returns True when a reopen actually happened.
    """
    from app.models.vulnerability import VulnerabilityStatus
    if vuln.status != VulnerabilityStatus.patched:
        return False
    old_status = vuln.status
    vuln.status = VulnerabilityStatus.open
    vuln.patched_at = None
    try:
        from app.routers.vulnerabilities import log_vulnerability_change
        # Name the host so the row is attributable and searchable. Lazy-loads
        # the asset, which is fine: reopens are rare (unlike bulk resolves).
        hostname = None
        try:
            hostname = vuln.asset.hostname if vuln.asset else None
        except Exception:
            hostname = None
        log_vulnerability_change(
            db, None, vuln.id, old_status, vuln.status,
            reason=reason, cve_id=vuln.cve_id, source=source, hostname=hostname,
        )
    except Exception as e:
        logger.warning("audit log for reopen failed (vuln_id=%s): %s", vuln.id, e)
    return True


def record_affected_package(db: Session, vuln, *, name: str, version: str = None,
                            fixed_version: str = None, source: str = None) -> None:
    """Record ONE affected product on a finding, in vulnerability_packages.

    A finding is unique per (cve_id, asset_id), so when two DIFFERENT products
    on the same host are hit by the same CVE — e.g. CVE-2026-16417 affects both
    Google Chrome and Microsoft Edge, which carry completely different build
    schemes — the single row's package_name can only name one of them. The
    per-package table is what keeps both visible (the CVE detail page already
    renders every entry, and the API returns them as `packages`).

    Idempotent per (vulnerability, package name): re-detection refreshes the
    version/fix and last_seen_at instead of appending duplicates.
    """
    from app.models.vulnerability_package import VulnerabilityPackage
    if not name:
        return
    name = name[:255]
    now = datetime.now()
    try:
        row = (db.query(VulnerabilityPackage)
                 .filter(VulnerabilityPackage.vulnerability_id == vuln.id,
                         VulnerabilityPackage.package_name == name)
                 .first())
        if row is None:
            # A plain query does NOT see rows added earlier in this same
            # (unflushed) transaction, and one scan run legitimately hits the
            # same (finding, product) twice — e.g. several inventory entries
            # carrying the same product name, or two CVE entries for one
            # package. Without this the second add hit uq_vulnpkg_vuln_package
            # and the IntegrityError aborted the WHOLE app-scan with a 502.
            for pending in db.new:
                if (isinstance(pending, VulnerabilityPackage)
                        and pending.vulnerability_id == vuln.id
                        and pending.package_name == name):
                    row = pending
                    break
        if row is not None:
            if version:
                row.package_version = version[:100]
            if fixed_version:
                row.fixed_version = fixed_version[:100]
            # Provenance is per-package and cumulative: a product confirmed by
            # app-scan AND MSRC must show BOTH, otherwise whichever scanner
            # wrote first owns the "via …" line forever (tester: an app-scan
            # detection kept showing 'via MSRC' after the MSRC run).
            if source:
                have = [s for s in (row.source or "").split(",") if s]
                if source not in have:
                    have.append(source)
                    row.source = ",".join(have)[:60]
            row.last_seen_at = now
            return
        # Savepoint so a lost race (parallel scan inserting the same pair)
        # rolls back only this insert, never the caller's transaction.
        with db.begin_nested():
            db.add(VulnerabilityPackage(
                vulnerability_id=vuln.id,
                package_name=name,
                package_version=(version or None) and version[:100],
                fixed_version=(fixed_version or None) and fixed_version[:100],
                source=source,
                first_detected_at=now,
                last_seen_at=now,
            ))
    except Exception as e:
        # Never let per-package bookkeeping break a scan — it is display data.
        logger.warning("per-package record skipped (vuln_id=%s, %s): %s",
                       vuln.id, name, e)
