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 Depends, FastAPI, Header, HTTPException, Query, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

from app import audit_store, auth, assist_store, live_presence, push_service
from app.auth_routes import router as auth_router
from app.registration_routes import router as registration_router
from app.mfa_recovery_routes import router as mfa_recovery_router
from app.assist_routes import router as assist_router, process_escalation_webhook
from app.crm_routes import router as crm_router
from app import crm_leads, integration_health
from app.cloudflare_dns import fetch_domain_dns
from app.modules.routes import router as modules_router
from app.vm112_domains_routes import router as vm112_domains_router
from app.carbonio_release_routes import router as carbonio_release_router
from app.migration.router import router as migration_router
from app.billing_routes import router as billing_router
from app.vm123 import vm123_router
from app.agents.routes import router as agents_router
from app.security_routes import router as security_router
from app.collectors.base import run_audit
from app.permissions import (
    can_assign_ticket,
    can_list_webhook_events,
    can_patch_ticket,
    can_read_audit_overview,
    can_read_audit_scorecard,
    can_read_cloudflare_dns,
    can_read_funnel,
    can_read_session_timeline,
    can_read_tickets,
    can_run_audit,
    should_mask_sensitive,
)

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,
    "security": WEBHOOK_SECRET,
    "wazuh": WAZUH_WEBHOOK_SECRET,
}

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

TICKET_EVENTS_BY_SOURCE = {
    # Ticket no início do onboarding (email+senha / criar servidor) — Roger 2026-06-10
    "vm112-onboard": frozenset({"onboarding.started", "onboarding.failed"}),
    "wazuh": frozenset({"wazuh.alert"}),
}

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

ONBOARD_SOURCE = "vm112-onboard"

FUNNEL_EVENT_RANK = {
    "session.started": 0,
    "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({
    "account.created",
    "domain.validated",
    "dns.applied",
    "infra.synced",
    "onboarding.completed",
    "company.validated",
    "webmail.released",
})

ASSIST_ESCALATION_EVENTS = frozenset({"onboarding.escalated", "onboarding.failed"})
ASSIST_LIFECYCLE_EVENTS = frozenset({"onboarding.assist.started", "onboarding.assist.ended"})

TICKET_ACTIVE_STATUSES = frozenset({"open", "escalated", "assisting", "resolved"})

app = FastAPI(title="Ligbox Ops Platform API", version="0.9.0-desk-assist")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
app.include_router(auth_router)
app.include_router(registration_router)
app.include_router(mfa_recovery_router)
app.include_router(assist_router)
app.include_router(crm_router)
app.include_router(modules_router)
app.include_router(vm112_domains_router)
app.include_router(security_router)
app.include_router(carbonio_release_router)
app.include_router(migration_router)
app.include_router(billing_router)
app.include_router(vm123_router)
app.include_router(agents_router)

TICKET_COLUMNS = "id,tenant_id,subject,status,payload,created_at,assigned_to,assigned_at,session_id,assist_mode,assisted_by,assisted_at,client_paused"


def db():
    DB_PATH.parent.mkdir(parents=True, exist_ok=True)
    conn = sqlite3.connect(DB_PATH, timeout=30.0)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA journal_mode=WAL")
    conn.execute("PRAGMA busy_timeout=30000")
    conn.execute("PRAGMA synchronous=NORMAL")
    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, "Ligbox Datacenter — Node VM001", "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),
                )
            else:
                conn.execute(
                    "UPDATE tenants SET name = ?, ip = ?, role = ? WHERE id = ?",
                    (name, ip, role, tid),
                )
        audit_store.init_audit_schema(conn)
        auth.init_auth_schema(conn)
        assist_store.init_assist_schema(conn)
        from app import carbonio_release_store

        carbonio_release_store.init_schema(conn)
        from app.migration import store as migration_store
        from app import billing_store
        migration_store.init_schema(conn)
        billing_store.init_schema(conn)
        from app.vm123 import provision_store as vm123_provision_store

        vm123_provision_store.init_schema(conn)
        from app.agents import store as agent_store

        agent_store.init_agent_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 | None = None
    assigned_to: str | None = None


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")
    ticket["activation_url"] = data.get("activation_url")
    ticket["desk_message"] = data.get("message")
    ticket["registration_role"] = data.get("role")
    ticket["wizard_ticket_id"] = payload.get("wizard_ticket_id") or data.get("wizard_ticket_id")
    ticket["wizard_client_note"] = payload.get("client_note") or data.get("client_note")
    ticket["support_source"] = data.get("source")
    ticket["assist_mode"] = ticket.get("assist_mode")
    ticket["assisted_by"] = ticket.get("assisted_by")
    ticket["assisted_at"] = ticket.get("assisted_at")
    ticket["client_paused"] = bool(ticket.get("client_paused"))
    ticket["crm_track"] = payload.get("crm_track")
    ticket["lead_detected_at"] = payload.get("lead_detected_at")
    ticket["lead_funnel_stage"] = payload.get("lead_funnel_stage")
    ticket["account_email"] = payload.get("account_email") or data.get("email")
    if not ticket.get("source"):
        ticket["source"] = "wazuh" if ticket.get("event") == "wazuh.alert" else "vm112-onboard"
    ticket["payload"] = payload
    return ticket


def _visible_ticket(ticket: dict, user: auth.DeskUser) -> dict:
    if should_mask_sensitive(user.role):
        return auth.mask_ticket(ticket)
    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
    row = conn.execute(
        "SELECT id FROM tickets WHERE session_id = ? ORDER BY id DESC LIMIT 1",
        (sid,),
    ).fetchone()
    if row:
        return int(row["id"])
    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


FUNNEL_BACKFILL_EVENTS = frozenset({
    "domain.validated",
    "dns.applied",
})


def _backfill_funnel_notes(conn, session_id: str, ticket_id: int) -> None:
    """Anexa etapas anteriores ao ticket criado no «Criar servidor»."""
    sid = (session_id or "").strip()
    if not sid:
        return
    row = conn.execute("SELECT payload FROM tickets WHERE id = ?", (ticket_id,)).fetchone()
    if not row:
        return
    payload = _parse_payload(row["payload"])
    notes = list(payload.get("funnel_notes") or [])
    existing = {n.get("event") for n in notes}
    rows = conn.execute(
        """
        SELECT event_type, payload, created_at
        FROM webhook_events
        WHERE source = ?
        ORDER BY id ASC
        """,
        (ONBOARD_SOURCE,),
    ).fetchall()
    for ev_row in rows:
        ev_payload = _parse_payload(ev_row["payload"])
        if (ev_payload.get("session_id") or "").strip() != sid:
            continue
        event_type = ev_row["event_type"]
        if event_type not in FUNNEL_BACKFILL_EVENTS or event_type in existing:
            continue
        notes.append({
            "event": event_type,
            "at": ev_row["created_at"],
            "data": ev_payload.get("data") or {},
            "backfilled": True,
        })
        existing.add(event_type)
    if notes:
        payload["funnel_notes"] = notes[-30:]
        conn.execute(
            "UPDATE tickets SET payload = ? WHERE id = ?",
            (json.dumps(payload), ticket_id),
        )


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 and not (session_id or "").strip():
        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 == "account.created":
        email = (body.data or {}).get("email")
        if email:
            payload["account_email"] = email
            domain = body.domain or payload.get("domain") or "sem dominio"
            conn.execute(
                "UPDATE tickets SET subject = ? WHERE id = ?",
                (f"[onboarding] {domain} — {email}", tid),
            )
    if event == "onboarding.completed":
        payload["ready_for_ops"] = True
        payload["onboarding_outcome"] = "completed"
        payload["crm_track"] = "onboarding_completed"
    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_hours = crm_leads.ONBOARD_STALE_HOURS
    stale_cutoff = (datetime.now(timezone.utc) - timedelta(hours=stale_hours)).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)
        assist = assist_store.get_active_assist(conn, sid)
        ticket_row = assist_store.find_ticket_by_session(conn, sid)
        crm_track = None
        if ticket_row:
            crm_track = _parse_payload(ticket_row["payload"]).get("crm_track")
        assist_status = "observing"
        if assist and assist.get("status") == "active":
            assist_status = "assisting"
        elif ticket_row and ticket_row["status"] in ("escalated", "assisting"):
            assist_status = ticket_row["status"]
        meta = assist_store.session_funnel_meta(conn, sid, FUNNEL_EVENT_RANK, FUNNEL_STAGE_BY_RANK, ONBOARD_SOURCE)
        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,
            "crm_track": crm_track,
            "is_lead": crm_track == "lead",
            "assist_status": assist_status,
            "can_escalate": assist_store.can_intervene(conn, sid, meta),
            "assisted_by": assist.get("initiated_by_user") if assist else (ticket_row["assigned_to"] if ticket_row else None),
        })

    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 body.event in ("onboarding.started", "account.created"):
        if email:
            return f"[onboarding] {domain} — {email}"
        return f"[onboarding] {domain}"
    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 _client_ip_from_request(request: Request | None) -> str | None:
    if request is None:
        return None
    forwarded = request.headers.get("x-forwarded-for")
    if forwarded:
        return forwarded.split(",")[0].strip()
    if request.client:
        return request.client.host
    return None


def _process_ingress(source_key: str, body: WebhookPayload, client_ip: str | None = None) -> dict:
    now = datetime.now(timezone.utc).isoformat()
    stored = body.model_dump()
    stored["source"] = source_key
    if client_ip:
        stored["ingress_client_ip"] = client_ip
        data = stored.get("data")
        if not isinstance(data, dict):
            data = {}
        if not data.get("client_ip"):
            data["client_ip"] = client_ip
        stored["data"] = data
    payload = json.dumps(stored)
    duplicate = False
    ticket_created = False
    ticket_id: int | None = None
    webhook_event_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:
            wh_cur = conn.execute(
                "INSERT INTO webhook_events (event_type,source,payload,created_at) VALUES (?,?,?,?)",
                (body.event, source_key, payload, now),
            )
            webhook_event_id = int(wh_cur.lastrowid)
            if source_key == "vm112-security":
                from app import security_store as sec_store

                if body.event in sec_store.AUTO_TICKET_EVENTS:
                    domain_label = body.domain or "sem domínio"
                    subject = f"[security] {domain_label} — {body.event.replace('security.', '')}"
                    session_id = (body.session_id or "").strip() or None
                    cur = conn.execute(
                        """
                        INSERT INTO tickets (tenant_id, subject, status, payload, created_at, session_id)
                        VALUES (?, ?, 'escalated', ?, ?, ?)
                        """,
                        (sec_store.VM112_TENANT_ID, subject, payload, now, session_id),
                    )
                    ticket_created = True
                    ticket_id = int(cur.lastrowid)
            elif _should_create_ticket(source_key, body):
                session_id = (body.session_id or "").strip() or None
                initial_status = "escalated" if body.event == "onboarding.failed" else "open"
                ticket_payload = _parse_payload(payload)
                if body.event == "onboarding.started":
                    ticket_payload["crm_track"] = "onboarding"
                    ticket_payload["funnel_notes"] = []
                cur = conn.execute(
                    """
                    INSERT INTO tickets
                    (tenant_id,subject,status,payload,created_at,session_id,assigned_to,assigned_at)
                    VALUES (?,?,?,?,?,?,NULL,NULL)
                    """,
                    (
                        tenant_id,
                        _ticket_subject(body, source_key),
                        initial_status,
                        json.dumps(ticket_payload),
                        now,
                        session_id,
                    ),
                )
                ticket_created = True
                ticket_id = int(cur.lastrowid)
                if body.event == "onboarding.started" and session_id:
                    _backfill_funnel_notes(conn, session_id, ticket_id)
                if body.event == "onboarding.failed" and session_id:
                    process_escalation_webhook(conn, body, now)
            elif body.event in ASSIST_ESCALATION_EVENTS and (body.session_id or "").strip():
                ticket_id = process_escalation_webhook(conn, body, now).get("ticket_id")
            elif body.event == "onboarding.assist.started" and (body.session_id or "").strip():
                from app.assist_routes import process_assist_started

                ticket_id = process_assist_started(conn, body, now).get("ticket_id")
            elif body.event == "onboarding.assist.ended" and (body.session_id or "").strip():
                from app.assist_routes import process_assist_ended

                ticket_id = process_assist_ended(conn, body, now).get("ticket_id")
            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 in ("company.validated", "account.created"):
                    session_id = (body.session_id or "").strip() or None
                    fallback_payload = _parse_payload(payload)
                    if body.event == "account.created":
                        fallback_payload["crm_track"] = "onboarding"
                        fallback_payload["funnel_notes"] = []
                    cur = conn.execute(
                        """
                        INSERT INTO tickets
                        (tenant_id,subject,status,payload,created_at,session_id,assigned_to,assigned_at)
                        VALUES (?,?,?,?,?,?,NULL,NULL)
                        """,
                        (
                            tenant_id,
                            _ticket_subject(body, source_key),
                            "open",
                            json.dumps(fallback_payload),
                            now,
                            session_id,
                        ),
                    )
                    ticket_created = True
                    ticket_id = int(cur.lastrowid)
                    if body.event == "company.validated":
                        enriched = _parse_payload(payload)
                        enriched["billing_state"] = "awaiting_billing_validation"
                        conn.execute(
                            "UPDATE tickets SET payload = ? WHERE id = ?",
                            (json.dumps(enriched), ticket_id),
                        )
            if source_key == ONBOARD_SOURCE:
                from app import carbonio_release_store

                tid = ticket_id
                if not tid and (body.session_id or "").strip():
                    tid = _find_ticket_id_by_session(conn, body.session_id or "")
                from app import billing_store
                if body.event == "company.validated" and body.domain:
                    billing_store.upsert_from_company_validated(
                        conn,
                        domain=body.domain,
                        session_id=body.session_id,
                        ticket_id=tid,
                        data=body.data,
                    )
                carbonio_release_store.upsert_from_webhook(
                    conn,
                    event=body.event,
                    domain=body.domain,
                    session_id=body.session_id,
                    data=body.data,
                    webhook_event_id=webhook_event_id,
                    ticket_id=tid,
                )
            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}")
        if source_key == ONBOARD_SOURCE:
            detail = (body.data or {}).get("email") or body.domain or body.session_id or ""
            try:
                push_service.notify_ops_event(body.event, domain=body.domain, detail=str(detail))
            except Exception:
                pass

    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")
@app.get("/api/health")
def health():
    redis.from_url(REDIS_URL).ping()
    return {"status": "ok", "service": "ligbox-ops-api", "version": "0.9.7-spec029-agentic"}


@app.get("/api/v1/integrations")
def list_integrations(user: auth.DeskUser = Depends(auth.get_current_user)):
    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/integrations/health")
def integrations_health(user: auth.DeskUser = Depends(auth.require_internal_or_user)):
    with db() as conn:
        return integration_health.build_health_report(conn)


@app.post("/api/v1/integrations/onboard/test")
def test_onboard_webhook(user: auth.DeskUser = Depends(auth.get_current_user)):
    if user.role not in ("super_admin", "admin"):
        raise HTTPException(403, "insufficient permissions")
    session_id = f"desk-test-{int(datetime.now(timezone.utc).timestamp())}"
    body = WebhookPayload(
        event="integration.test",
        domain="ops-healthcheck.ligbox",
        session_id=session_id,
        data={"triggered_by": user.username, "test": True},
    )
    result = _process_ingress(ONBOARD_SOURCE, body)
    result["domain"] = body.domain
    result["session_id"] = session_id
    result["tested_at"] = datetime.now(timezone.utc).isoformat()
    result["triggered_by"] = user.username
    result["message"] = (
        "Webhook processado com sucesso. O evento aparece no feed SOC e em Eventos."
        if not result.get("duplicate")
        else "Evento duplicado — o pipe está OK, mas este teste já existia na janela de deduplicação."
    )
    return result


@app.get("/api/v1/tenants")
def list_tenants(user: auth.DeskUser = Depends(auth.get_current_user)):
    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),
    user: auth.DeskUser = Depends(auth.get_current_user),
):
    if not can_read_tickets(user.role):
        raise HTTPException(403, "insufficient permissions")
    with db() as conn:
        query = f"SELECT {TICKET_COLUMNS} FROM tickets"
        params: list[Any] = []
        clauses = []
        if status == "active":
            clauses.append(f"status IN ({','.join('?' * len(TICKET_ACTIVE_STATUSES))})")
            params.extend(sorted(TICKET_ACTIVE_STATUSES))
        elif status in TICKET_ACTIVE_STATUSES or status == "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 = [_visible_ticket(_enrich_ticket(r), user) 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, user: auth.DeskUser = Depends(auth.get_current_user)):
    if not can_read_tickets(user.role):
        raise HTTPException(403, "insufficient permissions")
    with db() as conn:
        row = conn.execute(
            f"SELECT {TICKET_COLUMNS} 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:
            timeline = _session_timeline(conn, sid)
            from app.funnel_timing import apply_module_timing

            enriched, timing_meta = apply_module_timing(timeline)
            ticket["timeline"] = enriched
            ticket["related_events"] = enriched[-20:]
            if timing_meta:
                ticket["timing"] = timing_meta
        else:
            ticket["timeline"] = []
            ticket["related_events"] = []
        ticket["ready_for_ops"] = (ticket.get("payload") or {}).get("ready_for_ops", False)
    return _visible_ticket(ticket, user)


@app.patch("/api/v1/desk/tickets/{ticket_id}")
def update_ticket(
    ticket_id: int,
    body: TicketStatusUpdate,
    user: auth.DeskUser = Depends(auth.get_current_user),
):
    if body.status is None and body.assigned_to is None:
        raise HTTPException(400, "status or assigned_to required")
    if body.status is not None and body.status not in ("open", "closed", "escalated", "assisting", "resolved"):
        raise HTTPException(400, "status inválido")
    now = datetime.now(timezone.utc).isoformat()
    with db() as conn:
        row = conn.execute(
            f"SELECT {TICKET_COLUMNS} FROM tickets WHERE id = ?",
            (ticket_id,),
        ).fetchone()
        if not row:
            raise HTTPException(404, "ticket not found")
        ticket = _enrich_ticket(row)
        if body.status is not None and not can_patch_ticket(user.role, ticket, user.username):
            raise HTTPException(403, "insufficient permissions")
        if body.assigned_to is not None and not can_assign_ticket(user.role, body.assigned_to, user.username):
            raise HTTPException(403, "insufficient permissions")
        if body.status is not None:
            conn.execute("UPDATE tickets SET status = ? WHERE id = ?", (body.status, ticket_id))
        if body.assigned_to is not None:
            assignee = body.assigned_to.strip().lower() if body.assigned_to else None
            if assignee == "root":
                assignee = "root"
            conn.execute(
                "UPDATE tickets SET assigned_to = ?, assigned_at = ? WHERE id = ?",
                (assignee, now if assignee else None, ticket_id),
            )
        conn.commit()
        row = conn.execute(
            f"SELECT {TICKET_COLUMNS} FROM tickets WHERE id = ?",
            (ticket_id,),
        ).fetchone()
    return {"ticket": _visible_ticket(_enrich_ticket(row), user)}


@app.get("/api/v1/desk/summary")
def desk_summary(user: auth.DeskUser = Depends(auth.get_current_user)):
    with db() as conn:
        open_count = conn.execute(
            f"SELECT COUNT(*) c FROM tickets WHERE status IN ({','.join('?' * len(TICKET_ACTIVE_STATUSES))})",
            tuple(sorted(TICKET_ACTIVE_STATUSES)),
        ).fetchone()["c"]
        escalated_count = conn.execute("SELECT COUNT(*) c FROM tickets WHERE status = 'escalated'").fetchone()["c"]
        assisting_count = conn.execute("SELECT COUNT(*) c FROM tickets WHERE status = 'assisting'").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(
            f"SELECT {TICKET_COLUMNS} FROM tickets ORDER BY id DESC LIMIT 5"
        ).fetchall()
        leads_count = crm_leads.count_leads(conn)
    summary = {
        "tickets_open": open_count,
        "tickets_escalated": escalated_count,
        "tickets_assisting": assisting_count,
        "tickets_closed": closed_count,
        "tickets_total": open_count + closed_count,
        "leads_abandoned": leads_count,
        "onboard_stale_hours": crm_leads.ONBOARD_STALE_HOURS,
        "live_window_minutes": live_presence.DEFAULT_WINDOW_MINUTES,
        "support_live_hours": live_presence.SUPPORT_LIVE_HOURS,
        "webhook_events": event_count,
        "wazuh_events": wazuh_events,
        "tenants": tenant_count,
        "recent_tickets": [_enrich_ticket(r) for r in recent],
    }
    from app import billing_store
    with db() as conn:
        bs = billing_store.summary(conn)
    summary.update({
        "billing_pending": bs["billing_pending"],
        "billing_active": bs["billing_active"],
        "billing_total": bs["billing_total"],
    })
    if should_mask_sensitive(user.role):
        return auth.mask_summary_for_noc(summary)
    return summary


@app.get("/api/v1/webhooks/events")
def list_webhook_events(
    session_id: str | None = Query(default=None),
    source: str | None = Query(default=None),
    user: auth.DeskUser = Depends(auth.get_current_user),
):
    if user.role == "noc" and not source:
        source = "wazuh"
    if not can_list_webhook_events(user.role, source):
        raise HTTPException(403, "insufficient permissions")
    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),
    user: auth.DeskUser = Depends(auth.get_current_user),
):
    if not can_read_funnel(user.role):
        raise HTTPException(403, "insufficient permissions")
    with db() as conn:
        data = _funnel_summary(conn, window_hours=window_hours)
    if should_mask_sensitive(user.role):
        data["active_sessions"] = [
            {
                "session_id": (s.get("session_id") or "")[:8] + "…",
                "domain": s.get("domain"),
                "current_stage": s.get("current_stage"),
                "last_event_at": s.get("last_event_at"),
                "ticket_id": s.get("ticket_id"),
                "stale": s.get("stale"),
            }
            for s in data.get("active_sessions", [])
        ]
    return data


@app.get("/api/v1/live/presence")
def live_presence_api(
    window_minutes: int = Query(default=live_presence.DEFAULT_WINDOW_MINUTES, ge=5, le=720),
    user: auth.DeskUser = Depends(auth.get_current_user),
):
    if not can_read_tickets(user.role):
        raise HTTPException(403, "insufficient permissions")
    with db() as conn:
        sessions = live_presence.build_presence_sessions(
            conn,
            onboard_source=ONBOARD_SOURCE,
            window_minutes=window_minutes,
            find_ticket_by_session=assist_store.find_ticket_by_session,
        )
    return {
        "sessions": sessions,
        "source": "desk-funnel-fallback",
        "window_minutes": window_minutes,
        "support_live_hours": live_presence.SUPPORT_LIVE_HOURS,
    }


@app.get("/api/v1/onboard/sessions/{session_id}/timeline")
def onboard_session_timeline(session_id: str, user: auth.DeskUser = Depends(auth.get_current_user)):
    if not can_read_session_timeline(user.role):
        raise HTTPException(403, "insufficient permissions")
    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)
    result = {
        "session_id": sid,
        "domain": domain,
        "ticket_id": ticket_id,
        "events": timeline,
    }
    from app.modules import store as module_store
    from app.funnel_timing import build_timing_report

    from app.funnel_timing import apply_module_timing

    if module_store.is_module_enabled("funnel-timing") and timeline:
        enriched, timing_meta = apply_module_timing(timeline)
        result["events"] = enriched
        if timing_meta:
            result["timing"] = timing_meta
    return result


@app.get("/api/v1/audit/overview")
def audit_overview(user: auth.DeskUser = Depends(auth.get_current_user)):
    if not can_read_audit_overview(user.role):
        raise HTTPException(403, "insufficient permissions")
    with db() as conn:
        return audit_store.build_overview(conn)


@app.get("/api/v1/audit/tenants/{tenant_id}/details")
def audit_tenant_details(
    tenant_id: int,
    user: auth.DeskUser = Depends(auth.get_current_user),
):
    if not can_read_audit_overview(user.role):
        raise HTTPException(403, "insufficient permissions")
    with db() as conn:
        details = audit_store.tenant_details(conn, tenant_id)
        if not details:
            raise HTTPException(404, "tenant not found")
        return details


@app.get("/api/v1/dns/cloudflare/records")
async def cloudflare_dns_records(
    domain: str = Query(..., min_length=3),
    email_service: bool | None = Query(default=None),
    user: auth.DeskUser = Depends(auth.get_current_user),
):
    if not can_read_cloudflare_dns(user.role):
        raise HTTPException(403, "insufficient permissions")
    domain = domain.lower().strip()
    return await fetch_domain_dns(domain, email_service=email_service)


@app.get("/api/v1/audit/tenants/{tenant_id}/scorecard")
def audit_scorecard(
    tenant_id: int,
    domain: str = Query(...),
    user: auth.DeskUser = Depends(auth.get_current_user),
):
    if not can_read_audit_scorecard(user.role):
        raise HTTPException(403, "insufficient permissions")
    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(...),
    user: auth.DeskUser = Depends(auth.get_current_user),
):
    if not can_run_audit(user.role):
        raise HTTPException(403, "insufficient permissions")
    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(user: auth.DeskUser = Depends(auth.require_internal_or_user)):
    if user.username not in ("worker", "system") and not can_run_audit(user.role):
        raise HTTPException(403, "insufficient permissions")
    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, _client_ip_from_request(request))


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


@app.post("/api/v1/webhooks/security")
def webhook_security(
    body: WebhookPayload,
    request: Request,
    x_webhook_secret: str | None = Header(default=None),
):
    _verify_secret("security", x_webhook_secret)
    if not body.event.startswith("security."):
        raise HTTPException(400, "event must start with security.")
    return _process_ingress("vm112-security", body, _client_ip_from_request(request))


@app.get("/api/v1/infra/vm112/status")
def vm112_status(user: auth.DeskUser = Depends(auth.get_current_user)):
    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(user: auth.DeskUser = Depends(auth.get_current_user)):
    try:
        with httpx.Client(timeout=8.0, verify=False) as c:
            r = c.get("https://10.10.10.104:55000/")
        online = r.status_code in (200, 401)
        body = r.json() if r.headers.get("content-type", "").startswith("application/json") else r.text[:200]
        return {
            "wazuh_api": body,
            "http_status": r.status_code,
            "api_online": online,
        }
    except Exception as e:
        return {"wazuh_api": None, "http_status": None, "api_online": False, "error": str(e)}
