import json
import os
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

import httpx
import redis
from fastapi import FastAPI, Header, HTTPException, Query, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

from app import audit_store
from app.collectors.base import run_audit

DB_PATH = Path(os.getenv("SQLITE_PATH", "/data/ops.db"))
REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379/0")
VM112_API = os.getenv("VM112_API_URL", "http://10.10.10.112:8090")
MAIL_PUBLIC_IP = os.getenv("MAIL_PUBLIC_IP", "")
AUDIT_INTERVAL_SEC = int(os.getenv("AUDIT_INTERVAL_SEC", "600"))
WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET", "ligbox-ops-dev-secret")
WAZUH_WEBHOOK_SECRET = os.getenv("WAZUH_WEBHOOK_SECRET", "ligbox-wazuh-dev-secret")
WAZUH_MIN_TICKET_LEVEL = int(os.getenv("WAZUH_MIN_TICKET_LEVEL", "10"))

INTEGRATION_SECRETS = {
    "onboard": WEBHOOK_SECRET,
    "wazuh": WAZUH_WEBHOOK_SECRET,
}

INTEGRATION_SOURCES = {
    "onboard": "vm112-onboard",
    "wazuh": "wazuh",
}

TICKET_EVENTS_BY_SOURCE = {
    "vm112-onboard": frozenset({"account.created", "onboarding.failed"}),
    "wazuh": frozenset({"wazuh.alert"}),
}

TENANT_BY_SOURCE = {
    "vm112-onboard": 1,
    "wazuh": 2,
}

ONBOARD_SOURCE = "vm112-onboard"

FUNNEL_EVENT_RANK = {
    "onboarding.started": 1,
    "domain.validated": 2,
    "dns.applied": 3,
    "account.created": 4,
    "infra.synced": 5,
    "onboarding.completed": 6,
    "company.validated": 7,
    "webmail.released": 8,
    "onboarding.failed": 99,
}

FUNNEL_STAGE_BY_RANK = {
    1: "started",
    2: "domain_validated",
    3: "dns_applied",
    4: "account_created",
    5: "infra_synced",
    6: "completed",
    7: "company_validated",
    8: "webmail_released",
    99: "failed",
}

FUNNEL_NOTE_EVENTS = frozenset({
    "domain.validated",
    "dns.applied",
    "infra.synced",
    "onboarding.completed",
    "company.validated",
    "webmail.released",
})

app = FastAPI(title="Ligbox Ops Platform API", version="0.5.0-company-gate")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])


def db():
    DB_PATH.parent.mkdir(parents=True, exist_ok=True)
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn


def init_db():
    with db() as conn:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS tenants (
                id INTEGER PRIMARY KEY, name TEXT NOT NULL, ip TEXT NOT NULL,
                role TEXT NOT NULL, created_at TEXT NOT NULL);
            CREATE TABLE IF NOT EXISTS tickets (
                id INTEGER PRIMARY KEY, tenant_id INTEGER, subject TEXT NOT NULL,
                status TEXT NOT NULL DEFAULT 'open', payload TEXT, created_at TEXT NOT NULL);
            CREATE TABLE IF NOT EXISTS webhook_events (
                id INTEGER PRIMARY KEY, event_type TEXT NOT NULL, source TEXT NOT NULL,
                payload TEXT, created_at TEXT NOT NULL);
        """)
        now = datetime.now(timezone.utc).isoformat()
        defaults = [
            (1, "VM112 Ligbox Onboard", "10.10.10.112", "onboarding_portal"),
            (2, "VM104 Wazuh SOC", "10.10.10.104", "security_monitoring"),
        ]
        for tid, name, ip, role in defaults:
            if conn.execute("SELECT COUNT(*) c FROM tenants WHERE id = ?", (tid,)).fetchone()["c"] == 0:
                conn.execute(
                    "INSERT INTO tenants (id,name,ip,role,created_at) VALUES (?,?,?,?,?)",
                    (tid, name, ip, role, now),
                )
        audit_store.init_audit_schema(conn)
        conn.commit()


def _run_audit_for_domain(tenant_id: int, domain: str) -> dict:
    now = datetime.now(timezone.utc).isoformat()
    results = run_audit(
        tenant_id,
        domain,
        vm112_api=VM112_API,
        mail_public_ip=MAIL_PUBLIC_IP or None,
    )
    with db() as conn:
        for check_id, item in results.items():
            audit_store.upsert_check(
                conn,
                tenant_id,
                domain,
                check_id,
                item.get("status", "error"),
                item.get("message", ""),
                item.get("evidence"),
                now,
            )
        conn.commit()
    return {"tenant_id": tenant_id, "domain": domain, "checks": len(results), "checked_at": now}


def _audit_cycle() -> dict:
    with db() as conn:
        added = audit_store.sync_domains_from_webhooks(conn)
        domains = audit_store.list_audit_domains(conn)
    ran = []
    for d in domains:
        ran.append(_run_audit_for_domain(d["tenant_id"], d["domain"]))
    return {"domains_synced": added, "audits_run": len(ran), "details": ran}


class WebhookPayload(BaseModel):
    event: str
    domain: str | None = None
    session_id: str | None = None
    data: dict | None = None


class TicketStatusUpdate(BaseModel):
    status: str


def _parse_payload(raw: str | None) -> dict:
    if not raw:
        return {}
    try:
        return json.loads(raw)
    except json.JSONDecodeError:
        return {}


def _enrich_ticket(row: sqlite3.Row) -> dict:
    ticket = dict(row)
    payload = _parse_payload(ticket.get("payload"))
    data = payload.get("data") or {}
    ticket["event"] = payload.get("event")
    ticket["domain"] = payload.get("domain")
    ticket["session_id"] = payload.get("session_id")
    ticket["source"] = payload.get("source") or data.get("source")
    ticket["email"] = data.get("email")
    ticket["account_verified"] = data.get("account_verified")
    ticket["needs_review"] = data.get("needs_review")
    ticket["dns_mode"] = data.get("dns_mode")
    ticket["severity"] = data.get("level")
    ticket["rule_id"] = data.get("rule_id")
    ticket["description"] = data.get("description")
    ticket["agent"] = data.get("agent")
    ticket["billing_state"] = payload.get("billing_state") or data.get("billing_state")
    ticket["webmail_released"] = payload.get("webmail_released")
    ticket["company_profile"] = payload.get("company_profile") or data.get("company_profile")
    if not ticket.get("source"):
        ticket["source"] = "wazuh" if ticket.get("event") == "wazuh.alert" else "vm112-onboard"
    ticket["payload"] = payload
    return ticket


def _enrich_event(row: sqlite3.Row) -> dict:
    ev = dict(row)
    payload = _parse_payload(ev.get("payload"))
    data = payload.get("data") or {}
    ev["payload"] = payload
    ev["domain"] = payload.get("domain")
    ev["session_id"] = payload.get("session_id")
    ev["severity"] = data.get("level")
    return ev


def _funnel_stage_for_event(event_type: str) -> str | None:
    rank = FUNNEL_EVENT_RANK.get(event_type)
    if rank is None:
        return None
    return FUNNEL_STAGE_BY_RANK.get(rank)


def _session_timeline(conn, session_id: str) -> list[dict]:
    sid = (session_id or "").strip()
    if not sid:
        return []
    rows = conn.execute(
        """
        SELECT id, event_type, source, payload, created_at
        FROM webhook_events
        WHERE source = ?
        ORDER BY id ASC
        LIMIT 500
        """,
        (ONBOARD_SOURCE,),
    ).fetchall()
    timeline = []
    for row in rows:
        payload = _parse_payload(row["payload"])
        if (payload.get("session_id") or "").strip() != sid:
            continue
        timeline.append({
            "id": row["id"],
            "event_type": row["event_type"],
            "stage": _funnel_stage_for_event(row["event_type"]),
            "domain": payload.get("domain"),
            "data": payload.get("data") or {},
            "created_at": row["created_at"],
        })
    return timeline


def _find_ticket_id_by_session(conn, session_id: str) -> int | None:
    sid = (session_id or "").strip()
    if not sid:
        return None
    rows = conn.execute(
        "SELECT id, payload FROM tickets ORDER BY id DESC LIMIT 300"
    ).fetchall()
    for row in rows:
        payload = _parse_payload(row["payload"])
        if (payload.get("session_id") or "").strip() == sid:
            return int(row["id"])
    return None


def _find_ticket_id_by_domain(conn, domain: str) -> int | None:
    dom = (domain or "").strip().lower()
    if not dom:
        return None
    rows = conn.execute(
        "SELECT id, payload FROM tickets ORDER BY id DESC LIMIT 300"
    ).fetchall()
    for row in rows:
        payload = _parse_payload(row["payload"])
        if (payload.get("domain") or "").strip().lower() == dom:
            return int(row["id"])
    return None


def _attach_funnel_note(
    conn,
    session_id: str,
    event: str,
    body: WebhookPayload,
    now: str,
) -> int | None:
    tid = _find_ticket_id_by_session(conn, session_id)
    if not tid and body.domain:
        tid = _find_ticket_id_by_domain(conn, body.domain)
    if not tid:
        return None
    row = conn.execute("SELECT payload FROM tickets WHERE id = ?", (tid,)).fetchone()
    payload = _parse_payload(row["payload"])
    notes = list(payload.get("funnel_notes") or [])
    notes.append({"event": event, "at": now, "data": body.data or {}})
    payload["funnel_notes"] = notes[-30:]
    if event == "onboarding.completed":
        payload["ready_for_ops"] = True
    if event == "company.validated":
        payload["billing_state"] = (body.data or {}).get("billing_state") or "awaiting_billing_validation"
        if body.data and body.data.get("company_profile"):
            payload["company_profile"] = body.data["company_profile"]
    if event == "webmail.released":
        payload["webmail_released"] = True
        payload["webmail_released_at"] = (body.data or {}).get("webmail_released_at")
    conn.execute(
        "UPDATE tickets SET payload = ? WHERE id = ?",
        (json.dumps(payload), tid),
    )
    return tid


def _funnel_summary(conn, window_hours: int = 48) -> dict:
    from datetime import timedelta

    cutoff = (datetime.now(timezone.utc) - timedelta(hours=window_hours)).isoformat()
    rows = conn.execute(
        """
        SELECT event_type, payload, created_at
        FROM webhook_events
        WHERE source = ? AND created_at >= ?
        ORDER BY id ASC
        """,
        (ONBOARD_SOURCE, cutoff),
    ).fetchall()

    sessions: dict[str, dict] = {}
    for row in rows:
        payload = _parse_payload(row["payload"])
        sid = (payload.get("session_id") or "").strip()
        if not sid:
            continue
        rank = FUNNEL_EVENT_RANK.get(row["event_type"], 0)
        sess = sessions.setdefault(
            sid,
            {
                "session_id": sid,
                "domain": payload.get("domain"),
                "max_rank": 0,
                "last_event_at": row["created_at"],
                "failed": False,
            },
        )
        if payload.get("domain"):
            sess["domain"] = payload.get("domain")
        if row["created_at"] >= sess["last_event_at"]:
            sess["last_event_at"] = row["created_at"]
        if row["event_type"] == "onboarding.failed":
            sess["failed"] = True
            sess["max_rank"] = max(sess["max_rank"], 99)
        elif rank > sess["max_rank"] and not sess["failed"]:
            sess["max_rank"] = rank

    stage_counts = {label: 0 for label in FUNNEL_STAGE_BY_RANK.values()}
    stale_cutoff = (datetime.now(timezone.utc) - timedelta(hours=24)).isoformat()
    active_sessions = []

    for sid, sess in sessions.items():
        if sess["failed"]:
            stage = "failed"
        else:
            stage = FUNNEL_STAGE_BY_RANK.get(sess["max_rank"], "started")
        stage_counts[stage] = stage_counts.get(stage, 0) + 1
        ticket_id = _find_ticket_id_by_session(conn, sid)
        stale = sess["last_event_at"] < stale_cutoff and stage not in ("completed", "failed")
        active_sessions.append({
            "session_id": sid,
            "domain": sess.get("domain"),
            "current_stage": stage,
            "last_event_at": sess["last_event_at"],
            "ticket_id": ticket_id,
            "stale": stale,
        })

    active_sessions.sort(key=lambda x: x["last_event_at"], reverse=True)
    return {
        "window_hours": window_hours,
        "stages": stage_counts,
        "active_sessions": active_sessions[:50],
        "sessions_total": len(sessions),
    }


def _normalize_wazuh_alert(alert: dict[str, Any]) -> WebhookPayload:
    rule = alert.get("rule") or {}
    agent = alert.get("agent") or {}
    data_field = alert.get("data") if isinstance(alert.get("data"), dict) else {}
    level = rule.get("level", 0)
    return WebhookPayload(
        event="wazuh.alert",
        domain=agent.get("name") or "unknown-agent",
        session_id=str(alert.get("id") or alert.get("uuid") or ""),
        data={
            "level": level,
            "rule_id": rule.get("id"),
            "description": rule.get("description"),
            "agent": agent.get("name"),
            "agent_ip": agent.get("ip"),
            "srcip": data_field.get("srcip"),
            "source": "wazuh",
            "raw_rule_groups": rule.get("groups"),
        },
    )


def _ticket_subject(body: WebhookPayload, source_key: str) -> str:
    if source_key == "wazuh":
        data = body.data or {}
        level = data.get("level", "?")
        agent = data.get("agent") or body.domain or "agent"
        desc = (data.get("description") or "alerta")[:80]
        return f"[wazuh L{level}] {agent} — {desc}"
    if body.event == "company.validated":
        domain = body.domain or "sem dominio"
        profile = (body.data or {}).get("company_profile") or {}
        legal = (profile.get("legal_name") or domain)[:60]
        return f"[billing-validation] {domain} — {legal}"
    domain = body.domain or "sem dominio"
    email = (body.data or {}).get("email")
    if email:
        return f"[{body.event}] {domain} — {email}"
    return f"[{body.event}] {domain}"


def _should_create_ticket(source_key: str, body: WebhookPayload) -> bool:
    if body.event not in TICKET_EVENTS_BY_SOURCE.get(source_key, frozenset()):
        return False
    if source_key == "wazuh":
        level = (body.data or {}).get("level") or 0
        return int(level) >= WAZUH_MIN_TICKET_LEVEL
    return True


def _is_duplicate_event(
    conn,
    source_key: str,
    event: str,
    session_id: str | None,
    domain: str | None,
) -> bool:
    sid = (session_id or "").strip()
    dom = (domain or "").strip().lower()
    if not sid:
        return False
    rows = conn.execute(
        "SELECT payload FROM webhook_events WHERE event_type = ? AND source = ? ORDER BY id DESC LIMIT 300",
        (event, source_key),
    ).fetchall()
    for row in rows:
        payload = _parse_payload(row["payload"])
        row_sid = (payload.get("session_id") or "").strip()
        row_dom = (payload.get("domain") or "").strip().lower()
        if row_sid == sid and (not dom or row_dom == dom):
            return True
    return False


def _process_ingress(source_key: str, body: WebhookPayload) -> dict:
    now = datetime.now(timezone.utc).isoformat()
    stored = body.model_dump()
    stored["source"] = source_key
    payload = json.dumps(stored)
    duplicate = False
    ticket_created = False
    ticket_id: int | None = None
    tenant_id = TENANT_BY_SOURCE.get(source_key, 1)

    with db() as conn:
        duplicate = _is_duplicate_event(conn, source_key, body.event, body.session_id, body.domain)
        if not duplicate:
            conn.execute(
                "INSERT INTO webhook_events (event_type,source,payload,created_at) VALUES (?,?,?,?)",
                (body.event, source_key, payload, now),
            )
            if _should_create_ticket(source_key, body):
                cur = conn.execute(
                    "INSERT INTO tickets (tenant_id,subject,status,payload,created_at) VALUES (?,?,?,?,?)",
                    (tenant_id, _ticket_subject(body, source_key), "open", payload, now),
                )
                ticket_created = True
                ticket_id = int(cur.lastrowid)
            elif (
                source_key == ONBOARD_SOURCE
                and body.event in FUNNEL_NOTE_EVENTS
                and ((body.session_id or "").strip() or (body.domain or "").strip())
            ):
                ticket_id = _attach_funnel_note(conn, body.session_id or "", body.event, body, now)
                if not ticket_id and body.event == "company.validated":
                    cur = conn.execute(
                        "INSERT INTO tickets (tenant_id,subject,status,payload,created_at) VALUES (?,?,?,?,?)",
                        (tenant_id, _ticket_subject(body, source_key), "open", payload, now),
                    )
                    ticket_created = True
                    ticket_id = int(cur.lastrowid)
                    enriched = _parse_payload(payload)
                    enriched["billing_state"] = "awaiting_billing_validation"
                    conn.execute(
                        "UPDATE tickets SET payload = ? WHERE id = ?",
                        (json.dumps(enriched), ticket_id),
                    )
            conn.commit()
        elif source_key == ONBOARD_SOURCE and (body.session_id or "").strip():
            ticket_id = _find_ticket_id_by_session(conn, body.session_id or "")

    if not duplicate:
        redis.from_url(REDIS_URL).lpush("ops:events", f"{source_key}:{body.event}")

    return {
        "accepted": True,
        "status": "accepted",
        "event": body.event,
        "source": source_key,
        "duplicate": duplicate,
        "ticket_created": ticket_created,
        "ticket_id": ticket_id,
    }


def _verify_secret(integration: str, provided: str | None) -> None:
    expected = INTEGRATION_SECRETS.get(integration)
    if not expected or provided != expected:
        raise HTTPException(401, "invalid webhook secret")


@app.on_event("startup")
def startup():
    init_db()
    try:
        with db() as conn:
            audit_store.sync_domains_from_webhooks(conn)
    except Exception:
        pass


@app.get("/health")
def health():
    redis.from_url(REDIS_URL).ping()
    return {"status": "ok", "service": "ligbox-ops-api", "version": "0.5.0-company-gate"}


@app.get("/api/v1/integrations")
def list_integrations():
    return {
        "integrations": [
            {"id": "onboard", "source": "vm112-onboard", "tenant_id": 1, "description": "Portal onboarding VM112"},
            {"id": "wazuh", "source": "wazuh", "tenant_id": 2, "description": "Wazuh SOC VM104", "min_ticket_level": WAZUH_MIN_TICKET_LEVEL},
        ]
    }


@app.get("/api/v1/tenants")
def list_tenants():
    with db() as conn:
        rows = conn.execute("SELECT id,name,ip,role,created_at FROM tenants ORDER BY id").fetchall()
    return {"tenants": [dict(r) for r in rows]}


@app.get("/api/v1/desk/tickets")
def list_tickets(status: str | None = Query(default=None), source: str | None = Query(default=None)):
    with db() as conn:
        query = "SELECT id,tenant_id,subject,status,payload,created_at FROM tickets"
        params: list[Any] = []
        clauses = []
        if status in ("open", "closed"):
            clauses.append("status = ?")
            params.append(status)
        if clauses:
            query += " WHERE " + " AND ".join(clauses)
        query += " ORDER BY id DESC LIMIT 100"
        rows = conn.execute(query, params).fetchall()
    tickets = [_enrich_ticket(r) for r in rows]
    if source:
        tickets = [
            t for t in tickets
            if t.get("source") == source
            or (t.get("payload") or {}).get("source") == source
        ]
    return {"tickets": tickets}


@app.get("/api/v1/desk/tickets/{ticket_id}")
def get_ticket(ticket_id: int):
    with db() as conn:
        row = conn.execute(
            "SELECT id,tenant_id,subject,status,payload,created_at FROM tickets WHERE id = ?",
            (ticket_id,),
        ).fetchone()
        if not row:
            raise HTTPException(404, "ticket not found")
        ticket = _enrich_ticket(row)
        sid = ticket.get("session_id")
        if sid:
            ticket["timeline"] = _session_timeline(conn, sid)
            ticket["related_events"] = ticket["timeline"][-20:]
        else:
            ticket["timeline"] = []
            ticket["related_events"] = []
        ticket["ready_for_ops"] = (ticket.get("payload") or {}).get("ready_for_ops", False)
    return ticket


@app.patch("/api/v1/desk/tickets/{ticket_id}")
def update_ticket(ticket_id: int, body: TicketStatusUpdate):
    if body.status not in ("open", "closed"):
        raise HTTPException(400, "status must be open or closed")
    with db() as conn:
        cur = conn.execute("UPDATE tickets SET status = ? WHERE id = ?", (body.status, ticket_id))
        conn.commit()
        if cur.rowcount == 0:
            raise HTTPException(404, "ticket not found")
        row = conn.execute(
            "SELECT id,tenant_id,subject,status,payload,created_at FROM tickets WHERE id = ?",
            (ticket_id,),
        ).fetchone()
    return {"ticket": _enrich_ticket(row)}


@app.get("/api/v1/desk/summary")
def desk_summary():
    with db() as conn:
        open_count = conn.execute("SELECT COUNT(*) c FROM tickets WHERE status = 'open'").fetchone()["c"]
        closed_count = conn.execute("SELECT COUNT(*) c FROM tickets WHERE status = 'closed'").fetchone()["c"]
        event_count = conn.execute("SELECT COUNT(*) c FROM webhook_events").fetchone()["c"]
        wazuh_events = conn.execute("SELECT COUNT(*) c FROM webhook_events WHERE source = 'wazuh'").fetchone()["c"]
        tenant_count = conn.execute("SELECT COUNT(*) c FROM tenants").fetchone()["c"]
        recent = conn.execute(
            "SELECT id,tenant_id,subject,status,payload,created_at FROM tickets ORDER BY id DESC LIMIT 5"
        ).fetchall()
    return {
        "tickets_open": open_count,
        "tickets_closed": closed_count,
        "tickets_total": open_count + closed_count,
        "webhook_events": event_count,
        "wazuh_events": wazuh_events,
        "tenants": tenant_count,
        "recent_tickets": [_enrich_ticket(r) for r in recent],
    }


@app.get("/api/v1/webhooks/events")
def list_webhook_events(
    session_id: str | None = Query(default=None),
    source: str | None = Query(default=None),
):
    with db() as conn:
        if source:
            rows = conn.execute(
                "SELECT id,event_type,source,payload,created_at FROM webhook_events WHERE source = ? ORDER BY id DESC LIMIT 100",
                (source,),
            ).fetchall()
        else:
            rows = conn.execute(
                "SELECT id,event_type,source,payload,created_at FROM webhook_events ORDER BY id DESC LIMIT 100"
            ).fetchall()
        if session_id:
            sid = session_id.strip()
            rows = [
                r for r in rows
                if (_parse_payload(r["payload"]).get("session_id") or "").strip() == sid
            ]
    return {"events": [_enrich_event(r) for r in rows[:50]]}


@app.get("/api/v1/onboard/funnel")
def onboard_funnel(window_hours: int = Query(default=48, ge=1, le=168)):
    with db() as conn:
        return _funnel_summary(conn, window_hours=window_hours)


@app.get("/api/v1/onboard/sessions/{session_id}/timeline")
def onboard_session_timeline(session_id: str):
    sid = session_id.strip()
    if not sid:
        raise HTTPException(400, "session_id required")
    with db() as conn:
        timeline = _session_timeline(conn, sid)
        domain = timeline[-1]["domain"] if timeline else None
        if not domain:
            for row in timeline:
                if row.get("domain"):
                    domain = row["domain"]
                    break
        ticket_id = _find_ticket_id_by_session(conn, sid)
    return {
        "session_id": sid,
        "domain": domain,
        "ticket_id": ticket_id,
        "events": timeline,
    }


@app.get("/api/v1/audit/overview")
def audit_overview():
    with db() as conn:
        return audit_store.build_overview(conn)


@app.get("/api/v1/audit/tenants/{tenant_id}/scorecard")
def audit_scorecard(tenant_id: int, domain: str = Query(...)):
    domain = domain.lower().strip()
    if not domain:
        raise HTTPException(400, "domain query param required")
    with db() as conn:
        row = conn.execute("SELECT id FROM tenants WHERE id = ?", (tenant_id,)).fetchone()
        if not row:
            raise HTTPException(404, "tenant not found")
        return audit_store.scorecard(conn, tenant_id, domain)


@app.post("/api/v1/audit/run/{tenant_id}")
def audit_run(tenant_id: int, domain: str = Query(...)):
    domain = domain.lower().strip()
    if not domain:
        raise HTTPException(400, "domain query param required")
    with db() as conn:
        row = conn.execute("SELECT id FROM tenants WHERE id = ?", (tenant_id,)).fetchone()
        if not row:
            raise HTTPException(404, "tenant not found")
        conn.execute(
            """
            INSERT OR IGNORE INTO audit_domains (tenant_id, domain, source, created_at)
            VALUES (?, ?, 'manual', ?)
            """,
            (tenant_id, domain, datetime.now(timezone.utc).isoformat()),
        )
        conn.commit()
    result = _run_audit_for_domain(tenant_id, domain)
    return {"status": "completed", **result}


@app.post("/api/v1/audit/cycle")
def audit_cycle():
    return _audit_cycle()


@app.post("/api/v1/webhooks/ingress/{integration}")
async def webhook_ingress(
    integration: str,
    request: Request,
    x_webhook_secret: str | None = Header(default=None),
):
    if integration not in INTEGRATION_SOURCES:
        raise HTTPException(404, f"unknown integration: {integration}")
    _verify_secret(integration, x_webhook_secret)
    source_key = INTEGRATION_SOURCES[integration]
    raw = await request.json()

    if integration == "wazuh" and isinstance(raw, dict) and "rule" in raw:
        body = _normalize_wazuh_alert(raw)
    else:
        body = WebhookPayload.model_validate(raw)

    return _process_ingress(source_key, body)


@app.post("/api/v1/webhooks/onboard")
def webhook_onboard(body: WebhookPayload, x_webhook_secret: str | None = Header(default=None)):
    _verify_secret("onboard", x_webhook_secret)
    return _process_ingress("vm112-onboard", body)


@app.get("/api/v1/infra/vm112/status")
def vm112_status():
    try:
        with httpx.Client(timeout=8.0) as c:
            r = c.get(f"{VM112_API}/api/onboarding/health")
            return {"vm112": r.json(), "http_status": r.status_code}
    except Exception as e:
        return {"vm112": None, "error": str(e)}


@app.get("/api/v1/infra/wazuh/status")
def wazuh_status():
    try:
        with httpx.Client(timeout=8.0) as c:
            r = c.get("https://10.10.10.104:55000/", verify=False)
            return {"wazuh_api": r.json() if r.headers.get("content-type", "").startswith("application/json") else r.text[:200], "http_status": r.status_code}
    except Exception as e:
        return {"wazuh_api": None, "error": str(e)}
