"""
backend/routers/sentinel.py - Sentinel WebSocket and biometric endpoints.
"""

from __future__ import annotations

import asyncio
import json
import logging
import re
import uuid
from datetime import datetime, timezone
from typing import Any, Set

import asyncpg
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect
from pydantic import BaseModel

from backend.auth.dependencies import UserPrincipal, require_role
from backend.db.pool import get_pool
from backend.services.auto_mode_matcher import auto_mode_match_session
from backend.services.nemoclaw_client import score_qd, tag_lead

logger = logging.getLogger("velocity.sentinel")

router = APIRouter()
_UUID_RE = re.compile(
    r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"
)


class SentinelConnectionManager:
    def __init__(self) -> None:
        self._channels: dict[str, Set[WebSocket]] = {
            "notifications": set(),
            "perception": set(),
        }

    async def connect(self, ws: WebSocket, channel: str) -> None:
        await ws.accept()
        self._channels.setdefault(channel, set()).add(ws)
        logger.info("WS connected: channel=%s total=%d", channel, len(self._channels[channel]))

    def disconnect(self, ws: WebSocket, channel: str) -> None:
        self._channels.get(channel, set()).discard(ws)

    async def broadcast(self, payload: dict[str, Any], channel: str = "notifications") -> None:
        dead: Set[WebSocket] = set()
        for ws in list(self._channels.get(channel, set())):
            try:
                await ws.send_text(json.dumps(payload))
            except Exception:
                dead.add(ws)
        self._channels[channel] -= dead

    async def broadcast_all(self, payload: dict[str, Any]) -> None:
        for channel in self._channels:
            await self.broadcast(payload, channel)


manager = SentinelConnectionManager()


def _is_uuid(value: str | None) -> bool:
    return bool(value and _UUID_RE.match(value))


async def _resolve_scene_label(
    conn: asyncpg.Connection,
    video_asset_id: str | None,
    video_ts_ms: int,
) -> str | None:
    if not video_asset_id:
        return None
    row = await conn.fetchrow(
        """
        SELECT room_type, description
        FROM video_scene_maps
        WHERE video_asset_id = $1
          AND start_ms <= $2
          AND end_ms >= $2
        ORDER BY start_ms DESC
        LIMIT 1
        """,
        video_asset_id,
        video_ts_ms,
    )
    if not row:
        return None
    description = row["description"]
    return f"{row['room_type']} - {description}" if description else str(row["room_type"])


async def _ensure_session_row(
    conn: asyncpg.Connection,
    *,
    session_id: str,
    session_mode: str,
    lead_id: str | None,
    video_asset_id: str | None,
) -> None:
    await conn.execute(
        """
        INSERT INTO perception_sessions (id, session_mode, lead_id, video_asset_id, auto_mode_evidence)
        VALUES ($1::uuid, $2::session_mode_enum, $3::uuid, $4, '{}'::jsonb)
        ON CONFLICT (id) DO UPDATE
        SET video_asset_id = EXCLUDED.video_asset_id,
            lead_id = COALESCE(perception_sessions.lead_id, EXCLUDED.lead_id)
        """,
        session_id,
        session_mode,
        lead_id if _is_uuid(lead_id) else None,
        video_asset_id or "unknown",
    )


async def _resolve_canonical_context(
    conn: asyncpg.Connection,
    *,
    person_id: str | None,
    canonical_lead_id: str | None,
    legacy_lead_id: str | None,
) -> dict[str, Any] | None:
    if _is_uuid(person_id):
        row = await conn.fetchrow(
            """
            SELECT
                p.person_id::text AS person_id,
                p.full_name,
                p.primary_phone,
                p.buyer_type,
                p.legacy_li_id::text AS legacy_li_id,
                cl.lead_id::text AS lead_id,
                cl.status AS lead_status,
                cl.budget_band,
                cl.urgency,
                COALESCE((
                    SELECT current_value
                    FROM intel_qd_scores
                    WHERE person_id = p.person_id AND score_type = 'engagement_score'
                    ORDER BY computed_at DESC
                    LIMIT 1
                ), 0.50) AS engagement_score
            FROM crm_people p
            LEFT JOIN crm_leads cl ON cl.person_id = p.person_id
            WHERE p.person_id = $1::uuid
            ORDER BY cl.created_at DESC NULLS LAST
            LIMIT 1
            """,
            person_id,
        )
        if row:
            return dict(row)

    if _is_uuid(canonical_lead_id):
        row = await conn.fetchrow(
            """
            SELECT
                p.person_id::text AS person_id,
                p.full_name,
                p.primary_phone,
                p.buyer_type,
                p.legacy_li_id::text AS legacy_li_id,
                cl.lead_id::text AS lead_id,
                cl.status AS lead_status,
                cl.budget_band,
                cl.urgency,
                COALESCE((
                    SELECT current_value
                    FROM intel_qd_scores
                    WHERE person_id = p.person_id AND score_type = 'engagement_score'
                    ORDER BY computed_at DESC
                    LIMIT 1
                ), 0.50) AS engagement_score
            FROM crm_leads cl
            INNER JOIN crm_people p ON p.person_id = cl.person_id
            WHERE cl.lead_id = $1::uuid
            LIMIT 1
            """,
            canonical_lead_id,
        )
        if row:
            return dict(row)

    if _is_uuid(legacy_lead_id):
        row = await conn.fetchrow(
            """
            SELECT
                p.person_id::text AS person_id,
                p.full_name,
                p.primary_phone,
                p.buyer_type,
                p.legacy_li_id::text AS legacy_li_id,
                cl.lead_id::text AS lead_id,
                cl.status AS lead_status,
                cl.budget_band,
                cl.urgency,
                COALESCE((
                    SELECT current_value
                    FROM intel_qd_scores
                    WHERE person_id = p.person_id AND score_type = 'engagement_score'
                    ORDER BY computed_at DESC
                    LIMIT 1
                ), 0.50) AS engagement_score
            FROM crm_people p
            LEFT JOIN crm_leads cl ON cl.person_id = p.person_id
            WHERE p.legacy_li_id = $1::uuid
            ORDER BY cl.created_at DESC NULLS LAST
            LIMIT 1
            """,
            legacy_lead_id,
        )
        if row:
            return dict(row)

    return None


async def _ensure_canonical_interaction(
    conn: asyncpg.Connection,
    *,
    person_id: str,
    canonical_lead_id: str | None,
    session_id: str,
    session_mode: str,
    video_asset_id: str | None,
) -> str:
    existing = await conn.fetchrow(
        """
        SELECT interaction_id::text
        FROM intel_interactions
        WHERE source_ref = $1
          AND channel = 'perception_session'::intel_channel
        ORDER BY created_at DESC
        LIMIT 1
        """,
        session_id,
    )
    if existing:
        return existing["interaction_id"]

    row = await conn.fetchrow(
        """
        INSERT INTO intel_interactions (
            person_id, lead_id, channel, interaction_type, source_ref, summary, metadata_json
        )
        VALUES (
            $1::uuid,
            $2::uuid,
            'perception_session'::intel_channel,
            'sentinel_live_session',
            $3,
            $4,
            $5::jsonb
        )
        ON CONFLICT DO NOTHING
        RETURNING interaction_id::text
        """,
        person_id,
        canonical_lead_id if _is_uuid(canonical_lead_id) else None,
        session_id,
        f"Sentinel live session ({session_mode})",
        json.dumps(
            {
                "session_id": session_id,
                "session_mode": session_mode,
                "video_asset_id": video_asset_id,
            }
        ),
    )
    return row["interaction_id"]


async def _persist_canonical_qd(
    conn: asyncpg.Connection,
    *,
    person_id: str,
    interaction_id: str,
    session_id: str,
    scene_label: str | None,
    video_ts_ms: int,
    result_qd_score: int,
    baseline_score: int,
    blend_shapes: dict[str, float],
    session_mode: str,
) -> None:
    normalized_score = max(0.0, min(1.0, result_qd_score / 100.0))
    normalized_delta = max(-1.0, min(1.0, (result_qd_score - baseline_score) / 100.0))
    event_type = "engagement_sample"
    if result_qd_score - baseline_score >= 8:
        event_type = "engagement_spike"
    elif result_qd_score - baseline_score <= -8:
        event_type = "negative_shift"

    metadata = {
        "interaction_id": interaction_id,
        "scene_label": scene_label,
        "video_ts_ms": video_ts_ms,
        "qd_before": baseline_score,
        "qd_after": result_qd_score,
        "delta": result_qd_score - baseline_score,
        "session_mode": session_mode,
        "blend_shapes": blend_shapes,
    }

    await conn.execute(
        """
        INSERT INTO intel_perception_events (
            person_id, session_ref, event_type, engagement_score, media_ref, happened_at, metadata_json
        )
        VALUES ($1::uuid, $2, $3, $4, $5, NOW(), $6::jsonb)
        """,
        person_id,
        session_id,
        event_type,
        normalized_score,
        scene_label or f"video_ts:{video_ts_ms}",
        json.dumps(metadata),
    )

    await conn.execute(
        """
        INSERT INTO intel_qd_scores (
            person_id, score_type, current_value, computed_at, evidence_refs_json, reasoning, metadata_json
        )
        VALUES (
            $1::uuid,
            'engagement_score',
            $2,
            NOW(),
            $3::jsonb,
            $4,
            $5::jsonb
        )
        ON CONFLICT (person_id, score_type)
        DO UPDATE SET
            current_value = EXCLUDED.current_value,
            computed_at = EXCLUDED.computed_at,
            evidence_refs_json = EXCLUDED.evidence_refs_json,
            reasoning = EXCLUDED.reasoning,
            metadata_json = EXCLUDED.metadata_json
        """,
        person_id,
        normalized_score,
        json.dumps([session_id, interaction_id]),
        f"Sentinel session updated engagement to {result_qd_score}/100",
        json.dumps(metadata),
    )

    await conn.execute(
        """
        INSERT INTO intel_qd_timeseries (
            person_id, score_type, signal_source, timestamp, value, delta, evidence_ref, metadata_json
        )
        VALUES (
            $1::uuid,
            'engagement_score',
            'sentinel_live_session',
            NOW(),
            $2,
            $3,
            $4,
            $5::jsonb
        )
        """,
        person_id,
        normalized_score,
        normalized_delta,
        session_id,
        json.dumps(metadata),
    )


@router.websocket("/ws/notifications")
async def notifications_ws(ws: WebSocket) -> None:
    await manager.connect(ws, "notifications")
    try:
        while True:
            data = await ws.receive_text()
            await ws.send_text(json.dumps({"type": "ack", "data": data}))
    except WebSocketDisconnect:
        manager.disconnect(ws, "notifications")


@router.websocket("/ws/perception")
async def perception_ws(ws: WebSocket) -> None:
    await manager.connect(ws, "perception")
    pool: asyncpg.Pool | None = getattr(ws.app.state, "db_pool", None)
    if pool is None:
        await ws.send_text(json.dumps({"type": "system", "data": {"error": "Database unavailable"}}))
        await ws.close(code=1011)
        return

    try:
        while True:
            raw = await ws.receive_text()
            try:
                packet = json.loads(raw)
            except json.JSONDecodeError:
                continue

            if packet.get("event") != "BIOMETRIC_PACKET":
                continue

            person_id = packet.get("person_id")
            canonical_lead_id = packet.get("canonical_lead_id")
            lead_id = packet.get("lead_id")
            session_id = packet.get("session_id")
            session_mode = packet.get("session_mode", "assigned")
            video_ts_ms = int(packet.get("video_ts_ms", 0))
            video_asset_id = packet.get("video_asset_id")
            blend_shapes = packet.get("blend_shapes", {})

            if (
                not session_id
                or not _is_uuid(session_id)
                or session_mode not in {"assigned", "auto"}
                or not isinstance(blend_shapes, dict)
                or not blend_shapes
            ):
                continue

            async def _score(
                sid: str = session_id,
                lid: str | None = lead_id,
                mode: str = session_mode,
                bts: int = video_ts_ms,
                bs: dict[str, float] = blend_shapes,
                asset_id: str | None = video_asset_id,
            ) -> None:
                try:
                    async with pool.acquire() as conn:
                        async with conn.transaction():
                            canonical = await _resolve_canonical_context(
                                conn,
                                person_id=person_id,
                                canonical_lead_id=canonical_lead_id,
                                legacy_lead_id=lid,
                            )

                            await _ensure_session_row(
                                conn,
                                session_id=sid,
                                session_mode=mode,
                                lead_id=(canonical["legacy_li_id"] if canonical and canonical.get("legacy_li_id") else lid),
                                video_asset_id=asset_id,
                            )

                            lead_row = None
                            legacy_lead_id = canonical["legacy_li_id"] if canonical and canonical.get("legacy_li_id") else lid
                            if _is_uuid(legacy_lead_id):
                                lead_row = await conn.fetchrow(
                                    """
                                    SELECT quantum_dynamics_score, budget, interest, tags
                                    FROM leads_intelligence
                                    WHERE id = $1::uuid
                                    """,
                                    legacy_lead_id,
                                )

                            session_row = await conn.fetchrow(
                                """
                                SELECT final_qd_score, auto_mode_evidence
                                FROM perception_sessions
                                WHERE id = $1::uuid
                                """,
                                sid,
                            )
                            scene_label = await _resolve_scene_label(conn, asset_id, bts)

                            crm = {
                                "budget": (canonical["budget_band"] if canonical and canonical.get("budget_band") else None)
                                or (lead_row["budget"] if lead_row else None)
                                or "unknown",
                                "interest": (lead_row["interest"] if lead_row else None) or "unknown",
                                "prior_interaction_count": await conn.fetchval(
                                    """
                                    SELECT COUNT(*)
                                    FROM omnichannel_logs
                                    WHERE lead_id = $1::uuid
                                    """,
                                    legacy_lead_id,
                                )
                                if _is_uuid(legacy_lead_id)
                                else 0,
                                "tags": list((lead_row["tags"] if lead_row else None) or []),
                                "session_mode": mode,
                            }

                            baseline_score = (
                                lead_row["quantum_dynamics_score"]
                                if lead_row and lead_row["quantum_dynamics_score"] is not None
                                else (
                                    int(round(float(canonical["engagement_score"]) * 100))
                                    if canonical and canonical.get("engagement_score") is not None
                                    else ((session_row["final_qd_score"] if session_row else None) or 50)
                                )
                            )
                            result = await score_qd(
                                lead_id=(canonical["person_id"] if canonical else None) or lid or sid,
                                batch_id=sid,
                                blend_shapes=bs,
                                video_ts_ms=bts,
                                scene_label=scene_label,
                                crm_context=crm,
                                current_qd_score=baseline_score,
                            )

                            evidence = dict((session_row["auto_mode_evidence"] if session_row else {}) or {})
                            evidence.update(
                                {
                                    "last_scene_label": scene_label,
                                    "last_video_ts_ms": bts,
                                    "person_id": canonical["person_id"] if canonical else None,
                                }
                            )
                            await conn.execute(
                                """
                                UPDATE perception_sessions
                                SET final_qd_score = $1,
                                    auto_mode_evidence = $2::jsonb
                                WHERE id = $3::uuid
                                """,
                                result.qd_score,
                                evidence,
                                sid,
                            )

                            if canonical and _is_uuid(canonical["person_id"]):
                                interaction_id = await _ensure_canonical_interaction(
                                    conn,
                                    person_id=canonical["person_id"],
                                    canonical_lead_id=canonical["lead_id"],
                                    session_id=sid,
                                    session_mode=mode,
                                    video_asset_id=asset_id,
                                )
                                await _persist_canonical_qd(
                                    conn,
                                    person_id=canonical["person_id"],
                                    interaction_id=interaction_id,
                                    session_id=sid,
                                    scene_label=scene_label,
                                    video_ts_ms=bts,
                                    result_qd_score=result.qd_score,
                                    baseline_score=baseline_score,
                                    blend_shapes=bs,
                                    session_mode=mode,
                                )

                            if lead_row and _is_uuid(legacy_lead_id):
                                await conn.execute(
                                    """
                                    INSERT INTO omnichannel_logs (event_type, lead_id, payload, video_timestamp_ms)
                                    VALUES ('SENTIMENT_SPIKE', $1::uuid, $2::jsonb, $3)
                                    """,
                                    legacy_lead_id,
                                    json.dumps(
                                        {
                                            "blend_shapes": bs,
                                            "scene_label": scene_label,
                                            "qd_before": baseline_score,
                                            "qd_after": result.qd_score,
                                            "confidence": result.confidence,
                                            "session_id": sid,
                                            "person_id": canonical["person_id"] if canonical else None,
                                        }
                                    ),
                                    bts,
                                )
                                await conn.execute(
                                    """
                                    UPDATE leads_intelligence
                                    SET quantum_dynamics_score = $1, updated_at = NOW()
                                    WHERE id = $2::uuid
                                    """,
                                    result.qd_score,
                                    legacy_lead_id,
                                )

                    event = {
                        "type": "QD_UPDATED",
                        "data": {
                            "person_id": canonical["person_id"] if canonical else None,
                            "lead_id": legacy_lead_id,
                            "session_id": sid,
                            "qd_score": result.qd_score,
                            "delta": result.qd_score - baseline_score,
                            "reasoning": result.reasoning,
                            "scene_label": scene_label,
                            "timestamp": datetime.now(timezone.utc).isoformat(),
                        },
                    }
                    await manager.broadcast_all(event)
                except Exception as exc:
                    logger.exception("QD scoring failed for session %s: %s", sid, exc)

            asyncio.create_task(_score())
    except WebSocketDisconnect:
        manager.disconnect(ws, "perception")


class ConsentRequest(BaseModel):
    lead_id: str
    ip_address: str | None = None
    user_agent: str | None = None


class TagLeadRequest(BaseModel):
    lead_id: str
    phone: str
    budget: str | None = None
    message_text: str


class SessionCompleteRequest(BaseModel):
    session_id: str
    session_mode: str
    person_id: str | None = None
    canonical_lead_id: str | None = None
    lead_id: str | None = None
    lead_name: str | None = None
    final_qd_score: int | None = None


@router.post("/consent", status_code=201, summary="Record biometric consent")
async def record_consent(
    body: ConsentRequest,
    pool: asyncpg.Pool = Depends(get_pool),
) -> dict[str, str]:
    async with pool.acquire() as conn:
        await conn.execute(
            """
            INSERT INTO consent_log (lead_id, ip_address, user_agent, action)
            VALUES ($1::uuid, $2, $3, 'granted')
            """,
            body.lead_id,
            body.ip_address,
            body.user_agent,
        )
    return {"status": "consent_recorded"}


@router.post("/session/complete", summary="Close a perception session and finalize auto mode if needed")
async def complete_session(
    body: SessionCompleteRequest,
    pool: asyncpg.Pool = Depends(get_pool),
) -> dict[str, Any]:
    if not _is_uuid(body.session_id):
        raise HTTPException(status_code=400, detail="session_id must be a UUID.")
    if body.session_mode not in {"assigned", "auto"}:
        raise HTTPException(status_code=400, detail="session_mode must be assigned or auto.")

    async with pool.acquire() as conn:
        async with conn.transaction():
            await _ensure_session_row(
                conn,
                session_id=body.session_id,
                session_mode=body.session_mode,
                lead_id=body.lead_id,
                video_asset_id=None,
            )

            canonical = await _resolve_canonical_context(
                conn,
                person_id=body.person_id,
                canonical_lead_id=body.canonical_lead_id,
                legacy_lead_id=body.lead_id,
            )
            await conn.execute(
                """
                UPDATE perception_sessions
                SET ended_at = NOW(),
                    final_qd_score = COALESCE($1, final_qd_score)
                WHERE id = $2::uuid
                """,
                body.final_qd_score,
                body.session_id,
            )

            if canonical and body.final_qd_score is not None:
                interaction_id = await _ensure_canonical_interaction(
                    conn,
                    person_id=canonical["person_id"],
                    canonical_lead_id=canonical["lead_id"],
                    session_id=body.session_id,
                    session_mode=body.session_mode,
                    video_asset_id=None,
                )
                await _persist_canonical_qd(
                    conn,
                    person_id=canonical["person_id"],
                    interaction_id=interaction_id,
                    session_id=body.session_id,
                    scene_label="session_complete",
                    video_ts_ms=0,
                    result_qd_score=body.final_qd_score,
                    baseline_score=body.final_qd_score,
                    blend_shapes={},
                    session_mode=body.session_mode,
                )

            if body.session_mode == "auto":
                result = await auto_mode_match_session(conn, session_id=body.session_id)
                event = {
                    "type": "LEAD_TAGGED",
                    "data": {
                        "lead_id": result.lead_id,
                        "tags": result.tags_applied,
                        "lead_name": "Auto-matched lead",
                        "session_id": body.session_id,
                    },
                }
                await manager.broadcast(event, "notifications")
                return {
                    "status": "completed",
                    "session_id": body.session_id,
                    "lead_id": result.lead_id,
                    "match_action": result.action,
                    "match_confidence": result.confidence,
                    "tags_applied": result.tags_applied,
                }

    return {"status": "completed", "session_id": body.session_id}


@router.post("/tag-lead", summary="Apply NemoClaw lead tagging to a CRM lead")
async def tag_lead_route(
    body: TagLeadRequest,
    pool: asyncpg.Pool = Depends(get_pool),
    user: UserPrincipal = Depends(require_role("SENIOR_BROKER")),
) -> dict[str, Any]:
    result = await tag_lead(
        lead_id=body.lead_id,
        phone=body.phone,
        budget=body.budget,
        message_text=body.message_text,
    )
    async with pool.acquire() as conn:
        await conn.execute(
            """
            UPDATE leads_intelligence
            SET tags = ARRAY(
                SELECT DISTINCT unnest(
                    COALESCE(tags, ARRAY[]::text[]) || $1::text[]
                )
            )
            WHERE id = $2::uuid
            """,
            result.tags_to_add,
            body.lead_id,
        )
        await conn.execute(
            """
            INSERT INTO omnichannel_logs (event_type, lead_id, payload)
            VALUES ('LEAD_TAGGED', $1::uuid, $2::jsonb)
            """,
            body.lead_id,
            json.dumps(
                {
                    "tags_added": result.tags_to_add,
                    "tags_removed": result.tags_to_remove,
                    "actor_user_id": user.user_id,
                }
            ),
        )

    event = {
        "type": "LEAD_TAGGED",
        "data": {
            "lead_id": body.lead_id,
            "tags": result.tags_to_add,
        },
    }
    await manager.broadcast(event, "notifications")
    return {
        "lead_id": body.lead_id,
        "tags_to_add": result.tags_to_add,
        "tags_to_remove": result.tags_to_remove,
    }


@router.get("/qd-score/{lead_id}", summary="Current Quantum Dynamics score for a lead")
async def get_qd_score(
    lead_id: str,
    pool: asyncpg.Pool = Depends(get_pool),
    user: UserPrincipal = Depends(require_role("SENIOR_BROKER")),
) -> dict[str, Any]:
    del user
    async with pool.acquire() as conn:
        row = await conn.fetchrow(
            "SELECT quantum_dynamics_score, tags FROM leads_intelligence WHERE id = $1::uuid",
            lead_id,
        )
    if not row:
        raise HTTPException(status_code=404, detail="Lead not found.")
    return {
        "lead_id": lead_id,
        "qd_score": row["quantum_dynamics_score"],
        "tags": list(row["tags"] or []),
    }


async def broadcast_sentinel_event(payload: dict[str, Any]) -> None:
    await manager.broadcast(payload, "notifications")
