from __future__ import annotations

import json
import logging
import uuid
from datetime import datetime, timezone
from typing import Any, Literal

from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from pydantic import BaseModel, Field

from backend.auth.dependencies import UserPrincipal, get_current_user
from backend.crm.canonical_schema import ensure_canonical_crm_schema

logger = logging.getLogger(__name__)

crm_router = APIRouter()
analytics_router = APIRouter()

_CRM_SCHEMA_CACHE_KEY = "_crm_schema_ready"
_KANBAN_STAGE_MAP = {
    "new": "New",
    "new_inquiries": "New",
    "contacted": "Qualifying",
    "qualifying": "Qualifying",
    "qualified": "Qualifying",
    "site_visit_scheduled": "Site Visit",
    "site_visited": "Site Visit",
    "site_visit": "Site Visit",
    "site visit": "Site Visit",
    "negotiation": "Negotiation",
    "booking_initiated": "Closed",
    "booked": "Closed",
    "lost": "Closed",
    "closed": "Closed",
    "closed_won": "Closed",
    "closed/won": "Closed",
}
_CANONICAL_STATUS_MAP = {
    "new": "new",
    "qualifying": "qualified",
    "site_visit": "site_visit_scheduled",
    "negotiation": "negotiation",
    "closed": "booked",
    "contacted": "contacted",
    "qualified": "qualified",
    "site_visit_scheduled": "site_visit_scheduled",
    "site_visited": "site_visited",
    "booking_initiated": "booking_initiated",
    "booked": "booked",
    "lost": "lost",
    "dormant": "dormant",
}
_INTEL_CHANNEL_MAP = {
    "whatsapp": "whatsapp",
    "phone": "phone",
    "email": "email",
    "walkin": "site_visit",
    "site_visit": "site_visit",
    "office_meeting": "office_meeting",
}


def _now() -> datetime:
    return datetime.now(timezone.utc)


def _normalize_stage(value: str | None) -> str:
    if not value:
        return "New"
    return _KANBAN_STAGE_MAP.get(value.strip().lower(), value.strip())


def _stage_key(value: str) -> str:
    stage = _normalize_stage(value)
    return stage.lower().replace(" ", "_")


def _infer_qualification(score: int | None, source: str | None, notes: str | None) -> str:
    joined = f"{source or ''} {notes or ''}".lower()
    if score is None:
        return "UNKNOWN"
    if score >= 90 or "cash" in joined or "hnw" in joined or "family office" in joined:
        return "WHALE"
    if score >= 70:
        return "POTENTIAL"
    if score >= 45:
        return "HOT"
    return "TIRE_KICKER"


async def _broadcast_crm_event(request: Request, payload: dict[str, Any]) -> None:
    broadcaster = getattr(request.app.state, "broadcast_crm_event", None)
    if broadcaster is not None:
        await broadcaster(payload)


async def _get_pool(request: Request):
    pool = getattr(request.app.state, "db_pool", None)
    if pool is None:
        raise HTTPException(status_code=503, detail="Database unavailable.")
    return pool


async def _ensure_schema(request: Request) -> None:
    if getattr(request.app.state, _CRM_SCHEMA_CACHE_KEY, False):
        return

    pool = await _get_pool(request)
    default_tenant = "tenant_velocity"
    async with pool.acquire() as conn:
        await conn.execute(
            """
            CREATE TABLE IF NOT EXISTS leads (
                id TEXT PRIMARY KEY,
                tenant_id TEXT NOT NULL DEFAULT 'tenant_velocity',
                name TEXT NOT NULL,
                email TEXT,
                phone TEXT,
                source TEXT NOT NULL DEFAULT 'website',
                notes TEXT NOT NULL DEFAULT '',
                qualification TEXT NOT NULL DEFAULT 'UNKNOWN',
                score INTEGER NOT NULL DEFAULT 0,
                kanban_status TEXT NOT NULL DEFAULT 'New',
                budget TEXT NOT NULL DEFAULT '',
                unit_interest TEXT NOT NULL DEFAULT '',
                metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
                created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
                updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
            )
            """
        )
        await conn.execute(
            """
            CREATE TABLE IF NOT EXISTS chat_logs (
                id TEXT PRIMARY KEY,
                tenant_id TEXT NOT NULL DEFAULT 'tenant_velocity',
                lead_id TEXT NOT NULL REFERENCES leads(id) ON DELETE CASCADE,
                sender TEXT NOT NULL,
                channel TEXT NOT NULL DEFAULT 'oracle',
                content TEXT NOT NULL,
                metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
                created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
            )
            """
        )
        await conn.execute("ALTER TABLE leads ADD COLUMN IF NOT EXISTS tenant_id TEXT")
        await conn.execute("ALTER TABLE chat_logs ADD COLUMN IF NOT EXISTS tenant_id TEXT")
        await conn.execute(
            """
            UPDATE leads
            SET tenant_id = $1
            WHERE tenant_id IS NULL OR tenant_id = ''
            """,
            default_tenant,
        )
        await conn.execute(
            """
            UPDATE chat_logs
            SET tenant_id = $1
            WHERE tenant_id IS NULL OR tenant_id = ''
            """,
            default_tenant,
        )
        await conn.execute("ALTER TABLE leads ALTER COLUMN tenant_id SET DEFAULT 'tenant_velocity'")
        await conn.execute("ALTER TABLE chat_logs ALTER COLUMN tenant_id SET DEFAULT 'tenant_velocity'")
        await conn.execute("CREATE INDEX IF NOT EXISTS idx_leads_stage ON leads (kanban_status)")
        await conn.execute("CREATE INDEX IF NOT EXISTS idx_leads_score ON leads (score DESC)")
        await conn.execute("CREATE INDEX IF NOT EXISTS idx_leads_tenant_stage ON leads (tenant_id, kanban_status)")
        await conn.execute("CREATE INDEX IF NOT EXISTS idx_chat_logs_lead_id ON chat_logs (lead_id, created_at DESC)")
        await conn.execute("CREATE INDEX IF NOT EXISTS idx_chat_logs_tenant_lead_id ON chat_logs (tenant_id, lead_id, created_at DESC)")

    setattr(request.app.state, _CRM_SCHEMA_CACHE_KEY, True)


class LeadUpsertRequest(BaseModel):
    name: str = Field(..., min_length=1, max_length=200)
    email: str | None = Field(default=None, max_length=255)
    phone: str | None = Field(default=None, max_length=64)
    source: str = Field(default="website", max_length=64)
    notes: str = Field(default="", max_length=5000)
    qualification: str | None = Field(default=None, max_length=64)
    score: int = Field(default=0, ge=0, le=100)
    kanban_status: str = Field(default="New", max_length=64)
    budget: str = Field(default="", max_length=255)
    unit_interest: str = Field(default="", max_length=255)
    metadata: dict[str, Any] = Field(default_factory=dict)


class KanbanMoveRequest(BaseModel):
    lead_id: str
    target_status: str


class ChatLogCreateRequest(BaseModel):
    lead_id: str
    sender: Literal["lead", "oracle", "system", "broker"] = "oracle"
    channel: str = Field(default="oracle", max_length=64)
    content: str = Field(..., min_length=1, max_length=8000)
    metadata: dict[str, Any] = Field(default_factory=dict)


class SyntheticSeedRequest(BaseModel):
    count: int = Field(default=100, ge=1, le=500)


def _serialize_lead(row: Any) -> dict[str, Any]:
    if not hasattr(row, "get"):
        row = dict(row)
    score = int(row["score"] or 0)
    status_label = _normalize_stage(row["kanban_status"])
    qualification = row["qualification"] or _infer_qualification(score, row.get("source"), row.get("notes"))
    return {
        "id": row["id"],
        "name": row["name"],
        "email": row["email"],
        "phone": row["phone"],
        "source": row["source"],
        "notes": row["notes"],
        "qualification": qualification,
        "score": score,
        "kanban_status": status_label,
        "stage": _stage_key(status_label),
        "budget": row["budget"],
        "unit_interest": row["unit_interest"],
        "metadata": row["metadata"] or {},
        "created_at": row["created_at"].isoformat() if row["created_at"] else None,
        "updated_at": row["updated_at"].isoformat() if row["updated_at"] else None,
    }


def _serialize_chat_log(row: Any) -> dict[str, Any]:
    return {
        "id": row["id"],
        "lead_id": row["lead_id"],
        "sender": row["sender"],
        "channel": row["channel"],
        "content": row["content"],
        "metadata": row["metadata"] or {},
        "created_at": row["created_at"].isoformat() if row["created_at"] else None,
    }


def _tenant_scope(user: UserPrincipal) -> str:
    return user.tenant_id


def _legacy_stage_to_canonical_status(value: str | None) -> str:
    normalized = _stage_key(_normalize_stage(value))
    return _CANONICAL_STATUS_MAP.get(normalized, "new")


def _legacy_score_to_urgency(score: int | None) -> str:
    if score is None:
        return "low"
    if score >= 90:
        return "high"
    if score >= 70:
        return "medium"
    return "low"


def _legacy_channel_to_intel_channel(channel: str | None) -> str:
    normalized = (channel or "").strip().lower()
    return _INTEL_CHANNEL_MAP.get(normalized, "system")


def _canonical_lead_score(row: Any) -> int:
    intent = float(row.get("intent_score") or 0.0)
    engagement = float(row.get("engagement_score") or 0.0)
    return max(0, min(100, round(max(intent, engagement) * 100)))


def _serialize_canonical_lead(row: Any) -> dict[str, Any]:
    score = _canonical_lead_score(row)
    source = (row.get("source") or "website").strip().lower()
    if source not in {"website", "walkin", "whatsapp"}:
        source = "website"
    notes = row.get("notes") or ""
    status_label = _normalize_stage(row.get("kanban_status"))
    qualification = _infer_qualification(score, source, notes)
    metadata: dict[str, Any] = {}
    person_metadata = row.get("person_metadata") or {}
    lead_metadata = row.get("lead_metadata") or {}
    if isinstance(person_metadata, dict):
        metadata.update(person_metadata)
    if isinstance(lead_metadata, dict):
        metadata.update(lead_metadata)
    metadata["canonical_person_id"] = row.get("canonical_person_id")
    metadata["canonical_lead_id"] = row.get("canonical_lead_id")
    metadata["source_of_truth"] = "canonical_crm"
    legacy_id = row.get("legacy_lead_id")
    if legacy_id:
        metadata["legacy_lead_id"] = legacy_id

    created_at = row.get("created_at") or row.get("person_created_at")
    updated_at = row.get("updated_at") or row.get("person_updated_at")

    return {
        "id": row["id"],
        "name": row["name"],
        "email": row.get("email"),
        "phone": row.get("phone"),
        "source": source,
        "notes": notes,
        "qualification": qualification,
        "score": score,
        "kanban_status": status_label,
        "stage": _stage_key(status_label),
        "budget": row.get("budget") or "",
        "unit_interest": row.get("unit_interest") or "",
        "metadata": metadata,
        "created_at": created_at.isoformat() if created_at else None,
        "updated_at": updated_at.isoformat() if updated_at else None,
    }


async def _fetch_canonical_leads(
    conn: Any,
    tenant_id: str,
    search: str | None = None,
) -> list[dict[str, Any]]:
    clauses = ["cl.tenant_id = $1", "p.tenant_id = $1"]
    params: list[Any] = [tenant_id]
    if search:
        params.append(f"%{search.lower()}%")
        clauses.append(
            f"(LOWER(p.full_name) LIKE ${len(params)} OR LOWER(COALESCE(p.primary_email, '')) LIKE ${len(params)} OR LOWER(COALESCE(p.primary_phone, '')) LIKE ${len(params)})"
        )
    where = " AND ".join(clauses)
    rows = await conn.fetch(
        f"""
        SELECT
            COALESCE(NULLIF(p.legacy_lead_id, ''), NULLIF(cl.legacy_lead_id, ''), cl.lead_id::text, p.person_id::text) AS id,
            p.full_name AS name,
            p.primary_email AS email,
            p.primary_phone AS phone,
            COALESCE(NULLIF(cl.source_system, ''), 'website') AS source,
            COALESCE(latest_interaction.summary, cl.metadata_json->>'notes', '') AS notes,
            cl.status AS kanban_status,
            cl.budget_band AS budget,
            COALESCE(primary_interest.unit_preference, primary_interest.configuration, primary_interest.project_name, '') AS unit_interest,
            p.metadata_json AS person_metadata,
            cl.metadata_json AS lead_metadata,
            p.legacy_lead_id AS legacy_lead_id,
            p.person_id::text AS canonical_person_id,
            cl.lead_id::text AS canonical_lead_id,
            COALESCE(qs.intent_value, 0.0) AS intent_score,
            COALESCE(qs.engagement_value, qs.intent_value, 0.0) AS engagement_score,
            p.created_at AS person_created_at,
            p.updated_at AS person_updated_at,
            cl.created_at,
            cl.updated_at
        FROM crm_leads cl
        INNER JOIN crm_people p
            ON p.person_id = cl.person_id
           AND p.tenant_id = cl.tenant_id
        LEFT JOIN LATERAL (
            SELECT ii.summary
            FROM intel_interactions ii
            WHERE ii.tenant_id = cl.tenant_id
              AND ii.person_id = p.person_id
            ORDER BY ii.happened_at DESC
            LIMIT 1
        ) latest_interaction ON TRUE
        LEFT JOIN LATERAL (
            SELECT cpi.project_name, cpi.unit_preference, cpi.configuration
            FROM crm_property_interests cpi
            WHERE cpi.tenant_id = cl.tenant_id
              AND cpi.person_id = p.person_id
            ORDER BY cpi.priority ASC, cpi.created_at DESC
            LIMIT 1
        ) primary_interest ON TRUE
        LEFT JOIN LATERAL (
            SELECT
                MAX(CASE WHEN score_type = 'intent_score' THEN current_value END) AS intent_value,
                MAX(CASE WHEN score_type = 'engagement_score' THEN current_value END) AS engagement_value
            FROM intel_qd_scores iqs
            WHERE iqs.tenant_id = cl.tenant_id
              AND iqs.person_id = p.person_id
        ) qs ON TRUE
        WHERE {where}
        ORDER BY COALESCE(qs.intent_value, 0.0) DESC, cl.updated_at DESC, cl.created_at DESC
        """,
        *params,
    )
    return [_serialize_canonical_lead(dict(row)) for row in rows]


def _apply_lead_filters(
    leads: list[dict[str, Any]],
    kanban_status: str | None = None,
    qualification: str | None = None,
) -> list[dict[str, Any]]:
    filtered = leads
    if kanban_status:
        target_status = _normalize_stage(kanban_status)
        filtered = [lead for lead in filtered if lead["kanban_status"] == target_status]
    if qualification:
        target_qualification = qualification.upper()
        filtered = [lead for lead in filtered if str(lead["qualification"]).upper() == target_qualification]
    return filtered


def _merge_lead_sources(
    canonical_leads: list[dict[str, Any]],
    legacy_leads: list[dict[str, Any]],
) -> list[dict[str, Any]]:
    merged: dict[str, dict[str, Any]] = {}
    shadow_ids: set[str] = set()
    for lead in canonical_leads:
        merged[lead["id"]] = lead
        metadata = lead.get("metadata") or {}
        legacy_id = metadata.get("legacy_lead_id")
        if isinstance(legacy_id, str) and legacy_id:
            shadow_ids.add(legacy_id)
    for lead in legacy_leads:
        if lead["id"] in merged or lead["id"] in shadow_ids:
            continue
        merged[lead["id"]] = lead
    return sorted(
        merged.values(),
        key=lambda lead: (
            int(lead.get("score") or 0),
            lead.get("updated_at") or "",
            lead.get("created_at") or "",
        ),
        reverse=True,
    )


def _relative_time_label(value: Any) -> str:
    if value is None:
        return "No contact yet"
    if isinstance(value, str):
        try:
            parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
        except ValueError:
            return value
    else:
        parsed = value
    if parsed.tzinfo is None:
        parsed = parsed.replace(tzinfo=timezone.utc)
    delta = _now() - parsed.astimezone(timezone.utc)
    if delta.days > 0:
        return f"{delta.days}d ago"
    hours = int(delta.total_seconds() // 3600)
    if hours > 0:
        return f"{hours}h ago"
    minutes = int(delta.total_seconds() // 60)
    if minutes > 0:
        return f"{minutes}m ago"
    return "Just now"


def _kanban_stage_payload(stage_id: str, label: str, emoji: str, leads: list[dict[str, Any]]) -> dict[str, Any]:
    return {
        "id": stage_id,
        "label": label,
        "emoji": emoji,
        "leads": [
            {
                "id": lead["id"],
                "name": lead["name"],
                "location": lead.get("unit_interest") or lead.get("budget") or lead.get("source"),
                "qdScore": int(lead.get("score") or 0),
                "qdDelta": int((lead.get("metadata") or {}).get("qd_delta") or 0),
                "lastContactRelative": _relative_time_label(lead.get("updated_at") or lead.get("created_at")),
                "lastContactChannel": lead.get("source") or "crm",
                "isVaultActive": bool((lead.get("metadata") or {}).get("vault_active")),
            }
            for lead in leads
        ],
    }


async def _fetch_legacy_chat_logs(conn: Any, tenant_id: str, lead_id: str | None = None, channel: str | None = None) -> list[dict[str, Any]]:
    clauses: list[str] = ["tenant_id = $1"]
    params: list[Any] = [tenant_id]
    if lead_id:
        params.append(lead_id)
        clauses.append(f"lead_id = ${len(params)}")
    if channel:
        params.append(channel)
        clauses.append(f"channel = ${len(params)}")
    where = f"WHERE {' AND '.join(clauses)}"
    query = f"""
        SELECT id, lead_id, sender, channel, content, metadata, created_at
        FROM chat_logs
        {where}
        ORDER BY created_at DESC
    """
    rows = await conn.fetch(query, *params)
    return [_serialize_chat_log(row) for row in rows]


async def _fetch_canonical_chat_logs(conn: Any, tenant_id: str, lead_id: str, channel: str | None = None) -> list[dict[str, Any]]:
    params: list[Any] = [tenant_id, lead_id]
    channel_clause = ""
    if channel:
        params.append(channel)
        channel_clause = f"AND ii.channel = ${len(params)}"
    rows = await conn.fetch(
        f"""
        SELECT
            ii.interaction_id::text AS id,
            COALESCE(NULLIF(p.legacy_lead_id, ''), NULLIF(cl.legacy_lead_id, ''), cl.lead_id::text, p.person_id::text) AS lead_id,
            CASE
                WHEN LOWER(COALESCE(ii.metadata_json->>'sender_role', '')) = 'lead' THEN 'lead'
                ELSE 'oracle'
            END AS sender,
            COALESCE(ii.channel::text, 'oracle') AS channel,
            COALESCE(ii.summary, ii.metadata_json->>'message_text', ii.interaction_type, 'Interaction logged') AS content,
            ii.metadata_json AS metadata,
            ii.happened_at AS created_at
        FROM intel_interactions ii
        INNER JOIN crm_people p
            ON p.person_id = ii.person_id
           AND p.tenant_id = ii.tenant_id
        LEFT JOIN crm_leads cl
            ON cl.lead_id = ii.lead_id
           AND cl.tenant_id = ii.tenant_id
        WHERE ii.tenant_id = $1
          AND (
            COALESCE(NULLIF(p.legacy_lead_id, ''), NULLIF(cl.legacy_lead_id, ''), cl.lead_id::text, p.person_id::text) = $2
            OR cl.lead_id::text = $2
            OR p.person_id::text = $2
          )
          {channel_clause}
        ORDER BY ii.happened_at DESC
        """,
        *params,
    )
    return [_serialize_chat_log(dict(row)) for row in rows]


async def _sync_canonical_lead_bridge(
    request: Request,
    conn: Any,
    user: UserPrincipal,
    legacy_lead: dict[str, Any],
) -> dict[str, str] | None:
    try:
        await ensure_canonical_crm_schema(request.app)
        tenant_id = _tenant_scope(user)
        legacy_lead_id = str(legacy_lead["id"])
        binding = await conn.fetchrow(
            """
            SELECT p.person_id::text AS person_id, cl.lead_id::text AS lead_id, cl.status AS current_status
            FROM crm_people p
            LEFT JOIN crm_leads cl
                ON cl.person_id = p.person_id
               AND cl.tenant_id = p.tenant_id
               AND cl.legacy_lead_id = $2
            WHERE p.tenant_id = $1
              AND p.legacy_lead_id = $2
            ORDER BY cl.updated_at DESC NULLS LAST, cl.created_at DESC NULLS LAST
            LIMIT 1
            """,
            tenant_id,
            legacy_lead_id,
        )
        if binding is not None:
            binding = dict(binding)
        if binding is None:
            binding = await conn.fetchrow(
                """
                SELECT person_id::text AS person_id, lead_id::text AS lead_id, status AS current_status
                FROM crm_leads
                WHERE tenant_id = $1
                  AND legacy_lead_id = $2
                ORDER BY updated_at DESC, created_at DESC
                LIMIT 1
                """,
                tenant_id,
                legacy_lead_id,
            )
            if binding is not None:
                binding = dict(binding)

        person_id = str(binding["person_id"]) if binding and binding.get("person_id") else str(uuid.uuid4())
        lead_id = str(binding["lead_id"]) if binding and binding.get("lead_id") else str(uuid.uuid4())
        current_status = str(binding["current_status"]) if binding and binding.get("current_status") else None
        next_status = _legacy_stage_to_canonical_status(legacy_lead.get("kanban_status"))
        metadata_json = json.dumps(
            {
                **(legacy_lead.get("metadata") or {}),
                "legacy_bridge": "routes_crm",
                "legacy_notes": legacy_lead.get("notes") or "",
                "legacy_budget": legacy_lead.get("budget") or "",
                "legacy_unit_interest": legacy_lead.get("unit_interest") or "",
            }
        )

        if binding and binding.get("person_id"):
            await conn.execute(
                """
                UPDATE crm_people
                SET full_name = $3,
                    primary_email = $4,
                    primary_phone = $5,
                    legacy_lead_id = $6,
                    metadata_json = COALESCE(metadata_json, '{}'::jsonb) || $7::jsonb,
                    updated_at = NOW()
                WHERE tenant_id = $1
                  AND person_id = $2::uuid
                """,
                tenant_id,
                person_id,
                legacy_lead["name"],
                legacy_lead.get("email"),
                legacy_lead.get("phone"),
                legacy_lead_id,
                metadata_json,
            )
        else:
            await conn.execute(
                """
                INSERT INTO crm_people (
                    person_id, tenant_id, full_name, primary_email, primary_phone,
                    legacy_lead_id, metadata_json, created_at, updated_at
                ) VALUES (
                    $1::uuid, $2, $3, $4, $5, $6, $7::jsonb, NOW(), NOW()
                )
                """,
                person_id,
                tenant_id,
                legacy_lead["name"],
                legacy_lead.get("email"),
                legacy_lead.get("phone"),
                legacy_lead_id,
                metadata_json,
            )

        if binding and binding.get("lead_id"):
            await conn.execute(
                """
                UPDATE crm_leads
                SET person_id = $3::uuid,
                    source_system = $4,
                    status = $5::crm_lead_status,
                    budget_band = $6,
                    urgency = $7,
                    legacy_lead_id = $8,
                    metadata_json = COALESCE(metadata_json, '{}'::jsonb) || $9::jsonb,
                    updated_at = NOW()
                WHERE tenant_id = $1
                  AND lead_id = $2::uuid
                """,
                tenant_id,
                lead_id,
                person_id,
                legacy_lead.get("source") or "website",
                next_status,
                legacy_lead.get("budget") or "",
                _legacy_score_to_urgency(legacy_lead.get("score")),
                legacy_lead_id,
                metadata_json,
            )
        else:
            await conn.execute(
                """
                INSERT INTO crm_leads (
                    lead_id, tenant_id, person_id, source_system, status, budget_band,
                    urgency, legacy_lead_id, metadata_json, created_at, updated_at
                ) VALUES (
                    $1::uuid, $2, $3::uuid, $4, $5::crm_lead_status, $6,
                    $7, $8, $9::jsonb, NOW(), NOW()
                )
                """,
                lead_id,
                tenant_id,
                person_id,
                legacy_lead.get("source") or "website",
                next_status,
                legacy_lead.get("budget") or "",
                _legacy_score_to_urgency(legacy_lead.get("score")),
                legacy_lead_id,
                metadata_json,
            )

        if current_status and current_status != next_status:
            await conn.execute(
                """
                INSERT INTO crm_stage_history (
                    history_id, lead_id, from_status, to_status, changed_by_type, notes, happened_at
                ) VALUES (
                    $1::uuid, $2::uuid, $3, $4, 'system', $5, NOW()
                )
                """,
                str(uuid.uuid4()),
                lead_id,
                current_status,
                next_status,
                f"Legacy CRM bridge update from lead {legacy_lead_id}",
            )

        unit_interest = (legacy_lead.get("unit_interest") or "").strip()
        if unit_interest:
            existing_interest = await conn.fetchrow(
                """
                SELECT interest_id::text AS interest_id
                FROM crm_property_interests
                WHERE tenant_id = $1
                  AND person_id = $2::uuid
                  AND lead_id = $3::uuid
                ORDER BY priority ASC, created_at DESC
                LIMIT 1
                """,
                tenant_id,
                person_id,
                lead_id,
            )
            if existing_interest:
                await conn.execute(
                    """
                    UPDATE crm_property_interests
                    SET project_name = $4,
                        unit_preference = $5,
                        configuration = $6,
                        notes = $7
                    WHERE tenant_id = $1
                      AND person_id = $2::uuid
                      AND interest_id = $3::uuid
                    """,
                    tenant_id,
                    person_id,
                    existing_interest["interest_id"],
                    unit_interest,
                    unit_interest,
                    unit_interest,
                    legacy_lead.get("notes") or None,
                )
            else:
                await conn.execute(
                    """
                    INSERT INTO crm_property_interests (
                        interest_id, tenant_id, person_id, lead_id, project_name,
                        unit_preference, configuration, notes, created_at
                    ) VALUES (
                        $1::uuid, $2, $3::uuid, $4::uuid, $5, $6, $7, $8, NOW()
                    )
                    """,
                    str(uuid.uuid4()),
                    tenant_id,
                    person_id,
                    lead_id,
                    unit_interest,
                    unit_interest,
                    unit_interest,
                    legacy_lead.get("notes") or None,
                )

        return {"person_id": person_id, "lead_id": lead_id}
    except Exception as exc:
        logger.warning(
            "Canonical CRM write bridge unavailable for tenant %s legacy lead %s: %s",
            _tenant_scope(user),
            legacy_lead.get("id"),
            exc,
        )
        return None


async def _sync_canonical_chat_log_bridge(
    request: Request,
    conn: Any,
    user: UserPrincipal,
    legacy_chat_log: dict[str, Any],
    legacy_lead: dict[str, Any],
) -> None:
    binding = await _sync_canonical_lead_bridge(request, conn, user, legacy_lead)
    if binding is None:
        return

    try:
        await ensure_canonical_crm_schema(request.app)
        interaction_id = str(uuid.uuid4())
        await conn.execute(
            """
            INSERT INTO intel_interactions (
                interaction_id, tenant_id, person_id, lead_id, channel,
                interaction_type, happened_at, summary, source_ref, metadata_json, created_at
            ) VALUES (
                $1::uuid, $2, $3::uuid, $4::uuid, $5::intel_channel,
                'message', NOW(), $6, $7, $8::jsonb, NOW()
            )
            """,
            interaction_id,
            _tenant_scope(user),
            binding["person_id"],
            binding["lead_id"],
            _legacy_channel_to_intel_channel(legacy_chat_log.get("channel")),
            legacy_chat_log.get("content") or "",
            legacy_chat_log.get("id"),
            json.dumps(
                {
                    **(legacy_chat_log.get("metadata") or {}),
                    "legacy_bridge": "routes_crm",
                    "sender_role": legacy_chat_log.get("sender") or "oracle",
                    "message_text": legacy_chat_log.get("content") or "",
                    "legacy_chat_log_id": legacy_chat_log.get("id"),
                }
            ),
        )
        await conn.execute(
            """
            INSERT INTO intel_messages (
                message_id, interaction_id, sender_role, message_text, delivered_at, metadata_json
            ) VALUES (
                $1::uuid, $2::uuid, $3, $4, NOW(), $5::jsonb
            )
            """,
            str(uuid.uuid4()),
            interaction_id,
            legacy_chat_log.get("sender") or "oracle",
            legacy_chat_log.get("content") or "",
            json.dumps(
                {
                    **(legacy_chat_log.get("metadata") or {}),
                    "legacy_bridge": "routes_crm",
                    "legacy_chat_log_id": legacy_chat_log.get("id"),
                }
            ),
        )
    except Exception as exc:
        logger.warning(
            "Canonical CRM chat-log write bridge unavailable for tenant %s legacy log %s: %s",
            _tenant_scope(user),
            legacy_chat_log.get("id"),
            exc,
        )


async def _delete_canonical_lead_bridge(
    request: Request,
    conn: Any,
    user: UserPrincipal,
    legacy_lead_id: str,
) -> None:
    try:
        await ensure_canonical_crm_schema(request.app)
        tenant_id = _tenant_scope(user)
        binding = await conn.fetchrow(
            """
            SELECT lead_id::text AS lead_id, person_id::text AS person_id
            FROM crm_leads
            WHERE tenant_id = $1
              AND legacy_lead_id = $2
            ORDER BY updated_at DESC, created_at DESC
            LIMIT 1
            """,
            tenant_id,
            legacy_lead_id,
        )
        if not binding:
            return

        await conn.execute(
            "DELETE FROM crm_property_interests WHERE tenant_id = $1 AND lead_id = $2::uuid",
            tenant_id,
            binding["lead_id"],
        )
        await conn.execute(
            "DELETE FROM crm_stage_history WHERE lead_id = $1::uuid",
            binding["lead_id"],
        )
        await conn.execute(
            "DELETE FROM crm_leads WHERE tenant_id = $1 AND lead_id = $2::uuid",
            tenant_id,
            binding["lead_id"],
        )
        remaining_leads = await conn.fetchval(
            "SELECT COUNT(*) FROM crm_leads WHERE tenant_id = $1 AND person_id = $2::uuid",
            tenant_id,
            binding["person_id"],
        )
        if int(remaining_leads or 0) == 0:
            person_row = await conn.fetchrow(
                """
                SELECT legacy_lead_id
                FROM crm_people
                WHERE tenant_id = $1
                  AND person_id = $2::uuid
                """,
                tenant_id,
                binding["person_id"],
            )
            if person_row is not None:
                person_row = dict(person_row)
            if person_row and person_row.get("legacy_lead_id") == legacy_lead_id:
                await conn.execute(
                    "DELETE FROM crm_people WHERE tenant_id = $1 AND person_id = $2::uuid",
                    tenant_id,
                    binding["person_id"],
                )
    except Exception as exc:
        logger.warning(
            "Canonical CRM delete bridge unavailable for tenant %s legacy lead %s: %s",
            _tenant_scope(user),
            legacy_lead_id,
            exc,
        )


def _build_synthetic_leads(count: int) -> list[dict[str, Any]]:
    first_names = ["Amina", "Omar", "Farah", "Rayan", "Maya", "Khalid", "Noor", "Zara", "Ibrahim", "Layla"]
    last_names = ["Rahman", "Al-Farsi", "Kapoor", "Haddad", "Mehta", "Nadeem", "Shaikh", "Rao", "Wilson", "Chen"]
    sources = ["website", "walkin", "whatsapp"]
    stages = ["New", "Qualifying", "Site Visit", "Negotiation", "Closed"]
    interests = ["2BHK Marina View", "3BHK Corner Unit", "Penthouse Sky Deck", "Investment Studio", "4BHK Sea View"]
    budgets = ["AED 2.4M", "AED 4.8M", "AED 7.2M", "AED 12M", "AED 18M"]
    rows: list[dict[str, Any]] = []
    for idx in range(count):
        score = 35 + ((idx * 7) % 61)
        if idx % 12 == 0:
            score = 94
        name = f"{first_names[idx % len(first_names)]} {last_names[(idx * 3) % len(last_names)]}"
        source = sources[idx % len(sources)]
        notes = (
            "Cash-ready HNI buyer focusing on waterfront premium inventory."
            if score >= 90
            else "Follow-up required on payment plan and amenity preferences."
        )
        rows.append(
            {
                "id": str(uuid.uuid4()),
                "name": name,
                "email": f"{name.lower().replace(' ', '.')}@synthetic.velocity.local",
                "phone": f"+9715000{idx:05d}",
                "source": source,
                "notes": notes,
                "qualification": _infer_qualification(score, source, notes).upper(),
                "score": score,
                "kanban_status": stages[idx % len(stages)],
                "budget": budgets[idx % len(budgets)],
                "unit_interest": interests[idx % len(interests)],
                "metadata": {
                    "synthetic": True,
                    "campaign": "verification-seed",
                    "batch": "sprint1-root-integration",
                },
            }
        )
    return rows


@crm_router.get("/leads")
async def list_leads(
    request: Request,
    kanban_status: str | None = None,
    qualification: str | None = None,
    search: str | None = Query(default=None, min_length=1),
    user: UserPrincipal = Depends(get_current_user),
) -> dict[str, Any]:
    await _ensure_schema(request)
    pool = await _get_pool(request)
    async with pool.acquire() as conn:
        try:
            canonical_leads = _apply_lead_filters(
                await _fetch_canonical_leads(conn, _tenant_scope(user), search),
                kanban_status=kanban_status,
                qualification=qualification,
            )
        except Exception as exc:
            logger.warning("Canonical CRM lead bridge unavailable for tenant %s: %s", _tenant_scope(user), exc)
            canonical_leads = []

        clauses: list[str] = ["tenant_id = $1"]
        params: list[Any] = [_tenant_scope(user)]
        if kanban_status:
            params.append(_normalize_stage(kanban_status))
            clauses.append(f"kanban_status = ${len(params)}")
        if qualification:
            params.append(qualification.upper())
            clauses.append(f"qualification = ${len(params)}")
        if search:
            params.append(f"%{search.lower()}%")
            clauses.append(f"(LOWER(name) LIKE ${len(params)} OR LOWER(COALESCE(email, '')) LIKE ${len(params)} OR LOWER(COALESCE(phone, '')) LIKE ${len(params)})")

        where = f"WHERE {' AND '.join(clauses)}"
        query = f"""
            SELECT id, name, email, phone, source, notes, qualification, score, kanban_status,
                   budget, unit_interest, metadata, created_at, updated_at
            FROM leads
            {where}
            ORDER BY score DESC, updated_at DESC, created_at DESC
        """
        rows = await conn.fetch(query, *params)
    leads = _merge_lead_sources(canonical_leads, [_serialize_lead(row) for row in rows])
    return {"status": "ok", "data": leads, "meta": {"count": len(leads)}}


@crm_router.post("/leads", status_code=status.HTTP_201_CREATED)
async def create_lead(
    request: Request,
    payload: LeadUpsertRequest,
    user: UserPrincipal = Depends(get_current_user),
) -> dict[str, Any]:
    await _ensure_schema(request)
    pool = await _get_pool(request)
    lead_id = str(uuid.uuid4())
    qualification = (payload.qualification or _infer_qualification(payload.score, payload.source, payload.notes)).upper()
    stage = _normalize_stage(payload.kanban_status)
    async with pool.acquire() as conn:
        row = await conn.fetchrow(
            """
            INSERT INTO leads (
                id, tenant_id, name, email, phone, source, notes, qualification, score, kanban_status,
                budget, unit_interest, metadata, created_at, updated_at
            ) VALUES (
                $1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
                $11, $12, $13::jsonb, NOW(), NOW()
            )
            RETURNING id, name, email, phone, source, notes, qualification, score, kanban_status,
                      budget, unit_interest, metadata, created_at, updated_at
            """,
            lead_id,
            _tenant_scope(user),
            payload.name,
            payload.email,
            payload.phone,
            payload.source,
            payload.notes,
            qualification,
            payload.score,
            stage,
            payload.budget,
            payload.unit_interest,
            json.dumps(payload.metadata),
        )
        await _sync_canonical_lead_bridge(request, conn, user, dict(row))
    data = _serialize_lead(row)
    await _broadcast_crm_event(request, {"type": "lead_created", "entity": "lead", "data": data})
    return {"status": "ok", "data": data}


@crm_router.put("/leads/{lead_id}")
async def update_lead(
    lead_id: str,
    request: Request,
    payload: LeadUpsertRequest,
    user: UserPrincipal = Depends(get_current_user),
) -> dict[str, Any]:
    await _ensure_schema(request)
    pool = await _get_pool(request)
    qualification = (payload.qualification or _infer_qualification(payload.score, payload.source, payload.notes)).upper()
    stage = _normalize_stage(payload.kanban_status)
    async with pool.acquire() as conn:
        row = await conn.fetchrow(
            """
            UPDATE leads
            SET name = $2,
                email = $3,
                phone = $4,
                source = $5,
                notes = $6,
                qualification = $7,
                score = $8,
                kanban_status = $9,
                budget = $10,
                unit_interest = $11,
                metadata = $12::jsonb,
                updated_at = NOW()
            WHERE id = $1 AND tenant_id = $13
            RETURNING id, name, email, phone, source, notes, qualification, score, kanban_status,
                      budget, unit_interest, metadata, created_at, updated_at
            """,
            lead_id,
            payload.name,
            payload.email,
            payload.phone,
            payload.source,
            payload.notes,
            qualification,
            payload.score,
            stage,
            payload.budget,
            payload.unit_interest,
            json.dumps(payload.metadata),
            _tenant_scope(user),
        )
        if row is not None:
            await _sync_canonical_lead_bridge(request, conn, user, dict(row))
    if row is None:
        raise HTTPException(status_code=404, detail=f"Lead '{lead_id}' not found.")
    data = _serialize_lead(row)
    await _broadcast_crm_event(request, {"type": "lead_updated", "entity": "lead", "data": data})
    return {"status": "ok", "data": data}


@crm_router.delete("/leads/{lead_id}")
async def delete_lead(
    lead_id: str,
    request: Request,
    user: UserPrincipal = Depends(get_current_user),
) -> dict[str, Any]:
    await _ensure_schema(request)
    pool = await _get_pool(request)
    async with pool.acquire() as conn:
        result = await conn.execute("DELETE FROM leads WHERE id = $1 AND tenant_id = $2", lead_id, _tenant_scope(user))
        if not result.endswith("0"):
            await _delete_canonical_lead_bridge(request, conn, user, lead_id)
    if result.endswith("0"):
        raise HTTPException(status_code=404, detail=f"Lead '{lead_id}' not found.")
    await _broadcast_crm_event(request, {"type": "lead_deleted", "entity": "lead", "entity_id": lead_id})
    return {"status": "ok", "data": {"id": lead_id, "deleted": True}}


@crm_router.post("/leads/seed-synthetic", status_code=status.HTTP_201_CREATED)
async def seed_synthetic_leads(
    request: Request,
    payload: SyntheticSeedRequest,
    user: UserPrincipal = Depends(get_current_user),
) -> dict[str, Any]:
    await _ensure_schema(request)
    pool = await _get_pool(request)
    synthetic_rows = _build_synthetic_leads(payload.count)
    inserted = 0
    chat_logs_inserted = 0
    async with pool.acquire() as conn:
        for row in synthetic_rows:
            await conn.execute(
                """
                INSERT INTO leads (
                    id, tenant_id, name, email, phone, source, notes, qualification, score, kanban_status,
                    budget, unit_interest, metadata, created_at, updated_at
                ) VALUES (
                    $1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
                    $11, $12, $13::jsonb, NOW(), NOW()
                )
                ON CONFLICT (id) DO NOTHING
                """,
                row["id"],
                _tenant_scope(user),
                row["name"],
                row["email"],
                row["phone"],
                row["source"],
                row["notes"],
                row["qualification"],
                row["score"],
                row["kanban_status"],
                row["budget"],
                row["unit_interest"],
                json.dumps(row["metadata"]),
            )
            inserted += 1
            for sender, channel, content in [
                ("lead", "whatsapp", f"{row['name']} asked for availability on {row['unit_interest']}."),
                ("oracle", "oracle", "Oracle generated a guided follow-up based on budget, stage, and source quality."),
            ]:
                await conn.execute(
                    """
                    INSERT INTO chat_logs (id, tenant_id, lead_id, sender, channel, content, metadata, created_at)
                    VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, NOW())
                    """,
                    str(uuid.uuid4()),
                    _tenant_scope(user),
                    row["id"],
                    sender,
                    channel,
                    content,
                    json.dumps({"synthetic": True}),
                )
                chat_logs_inserted += 1
    result = {
        "status": "ok",
        "data": {
            "seeded": inserted,
            "chat_logs_seeded": chat_logs_inserted,
            "batch": "sprint1-root-integration",
        },
    }
    await _broadcast_crm_event(
        request,
        {
            "type": "crm_seeded",
            "entity": "lead_batch",
            "data": result["data"],
        },
    )
    return result


@crm_router.get("/leads/demographics")
async def lead_demographics(
    request: Request,
    user: UserPrincipal = Depends(get_current_user),
) -> dict[str, Any]:
    await _ensure_schema(request)
    pool = await _get_pool(request)
    async with pool.acquire() as conn:
        source_rows = await conn.fetch(
            """
            SELECT source, COUNT(*)::int AS lead_count, COALESCE(AVG(score), 0)::float AS avg_score
            FROM leads
            WHERE tenant_id = $1
            GROUP BY source
            ORDER BY lead_count DESC, source ASC
            """,
            _tenant_scope(user),
        )
        qualification_rows = await conn.fetch(
            """
            SELECT qualification, COUNT(*)::int AS lead_count
            FROM leads
            WHERE tenant_id = $1
            GROUP BY qualification
            ORDER BY lead_count DESC, qualification ASC
            """,
            _tenant_scope(user),
        )
    return {
        "status": "ok",
        "data": {
            "by_source": [dict(row) for row in source_rows],
            "by_qualification": [dict(row) for row in qualification_rows],
        },
    }


@crm_router.get("/leads/{lead_id}")
async def get_lead(
    lead_id: str,
    request: Request,
    user: UserPrincipal = Depends(get_current_user),
) -> dict[str, Any]:
    await _ensure_schema(request)
    pool = await _get_pool(request)
    async with pool.acquire() as conn:
        try:
            canonical_lead = next(
                (
                    lead
                    for lead in await _fetch_canonical_leads(conn, _tenant_scope(user))
                    if lead["id"] == lead_id
                    or (lead.get("metadata") or {}).get("canonical_lead_id") == lead_id
                    or (lead.get("metadata") or {}).get("canonical_person_id") == lead_id
                ),
                None,
            )
            if canonical_lead is not None:
                return {"status": "ok", "data": canonical_lead}
        except Exception as exc:
            logger.warning("Canonical CRM lead lookup unavailable for tenant %s: %s", _tenant_scope(user), exc)
        row = await conn.fetchrow(
            """
            SELECT id, name, email, phone, source, notes, qualification, score, kanban_status,
                   budget, unit_interest, metadata, created_at, updated_at
            FROM leads
            WHERE id = $1 AND tenant_id = $2
            """,
            lead_id,
            _tenant_scope(user),
        )
    if row is None:
        raise HTTPException(status_code=404, detail=f"Lead '{lead_id}' not found.")
    return {"status": "ok", "data": _serialize_lead(row)}


@crm_router.get("/chat-logs")
async def list_chat_logs(
    request: Request,
    lead_id: str | None = None,
    channel: str | None = None,
    user: UserPrincipal = Depends(get_current_user),
) -> dict[str, Any]:
    await _ensure_schema(request)
    pool = await _get_pool(request)
    async with pool.acquire() as conn:
        data = await _fetch_legacy_chat_logs(conn, _tenant_scope(user), lead_id=lead_id, channel=channel)
        if lead_id and not data:
            try:
                data = await _fetch_canonical_chat_logs(conn, _tenant_scope(user), lead_id, channel=channel)
            except Exception as exc:
                logger.warning("Canonical CRM chat-log bridge unavailable for tenant %s lead %s: %s", _tenant_scope(user), lead_id, exc)
    return {"status": "ok", "data": data, "meta": {"count": len(data)}}


@crm_router.post("/chat-logs", status_code=status.HTTP_201_CREATED)
async def create_chat_log(
    request: Request,
    payload: ChatLogCreateRequest,
    user: UserPrincipal = Depends(get_current_user),
) -> dict[str, Any]:
    await _ensure_schema(request)
    pool = await _get_pool(request)
    log_id = str(uuid.uuid4())
    async with pool.acquire() as conn:
        lead = await conn.fetchrow(
            """
            SELECT id, name, email, phone, source, notes, qualification, score, kanban_status,
                   budget, unit_interest, metadata, created_at, updated_at
            FROM leads
            WHERE id = $1 AND tenant_id = $2
            """,
            payload.lead_id,
            _tenant_scope(user),
        )
        if lead is None:
            raise HTTPException(status_code=404, detail=f"Lead '{payload.lead_id}' not found.")
        row = await conn.fetchrow(
            """
            INSERT INTO chat_logs (id, tenant_id, lead_id, sender, channel, content, metadata, created_at)
            VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, NOW())
            RETURNING id, lead_id, sender, channel, content, metadata, created_at
            """,
            log_id,
            _tenant_scope(user),
            payload.lead_id,
            payload.sender,
            payload.channel,
            payload.content,
            json.dumps(payload.metadata),
        )
        await _sync_canonical_chat_log_bridge(request, conn, user, dict(row), dict(lead))
    data = _serialize_chat_log(row)
    await _broadcast_crm_event(request, {"type": "chat_log_created", "entity": "chat_log", "data": data})
    return {"status": "ok", "data": data}


@crm_router.get("/kanban/board")
async def get_kanban_board(
    request: Request,
    user: UserPrincipal = Depends(get_current_user),
) -> dict[str, Any]:
    await _ensure_schema(request)
    pool = await _get_pool(request)
    ordered_stages = ["New", "Qualifying", "Site Visit", "Negotiation", "Closed"]
    async with pool.acquire() as conn:
        rows = await conn.fetch(
            """
            SELECT id, name, email, phone, source, notes, qualification, score, kanban_status,
                   budget, unit_interest, metadata, created_at, updated_at
            FROM leads
            WHERE tenant_id = $1
            ORDER BY score DESC, updated_at DESC, created_at DESC
            """,
            _tenant_scope(user),
        )
    leads = [_serialize_lead(row) for row in rows]
    grouped = {stage: [] for stage in ordered_stages}
    for lead in leads:
        grouped.setdefault(lead["kanban_status"], []).append(lead)
    board = [
        {
            "status": stage,
            "stage": _stage_key(stage),
            "count": len(grouped.get(stage, [])),
            "items": grouped.get(stage, []),
        }
        for stage in ordered_stages
    ]
    return {"status": "ok", "data": board}


@crm_router.get("/pipeline/kanban")
async def get_pipeline_kanban(
    request: Request,
    user: UserPrincipal = Depends(get_current_user),
) -> list[dict[str, Any]]:
    """Velocity-OS Pipeline board contract.

    The simplified WebOS pillar expects raw stage arrays, while the older CRM
    board endpoint returns a wrapped administrative payload. Keep this route
    explicit so the UI gets exactly the shape it renders.
    """
    await _ensure_schema(request)
    pool = await _get_pool(request)
    tenant_id = _tenant_scope(user)
    async with pool.acquire() as conn:
        try:
            canonical_leads = await _fetch_canonical_leads(conn, tenant_id)
        except Exception as exc:
            logger.warning("Canonical CRM pipeline bridge unavailable for tenant %s: %s", tenant_id, exc)
            canonical_leads = []
        legacy_rows = await conn.fetch(
            """
            SELECT id, name, email, phone, source, notes, qualification, score, kanban_status,
                   budget, unit_interest, metadata, created_at, updated_at
            FROM leads
            WHERE tenant_id = $1
            ORDER BY score DESC, updated_at DESC, created_at DESC
            """,
            tenant_id,
        )
    leads = _merge_lead_sources(canonical_leads, [_serialize_lead(row) for row in legacy_rows])
    stage_defs = [
        ("new", "New", "N"),
        ("qualifying", "Qualifying", "Q"),
        ("site_visit", "Site Visit", "V"),
        ("negotiation", "Negotiation", "D"),
        ("closed", "Closed", "C"),
    ]
    grouped: dict[str, list[dict[str, Any]]] = {stage_id: [] for stage_id, _, _ in stage_defs}
    for lead in leads:
        grouped.setdefault(lead.get("stage") or "new", []).append(lead)
    return [
        _kanban_stage_payload(stage_id, label, emoji, grouped.get(stage_id, []))
        for stage_id, label, emoji in stage_defs
    ]


@crm_router.put("/kanban/move")
async def move_kanban_card(
    request: Request,
    payload: KanbanMoveRequest,
    user: UserPrincipal = Depends(get_current_user),
) -> dict[str, Any]:
    await _ensure_schema(request)
    pool = await _get_pool(request)
    stage = _normalize_stage(payload.target_status)
    async with pool.acquire() as conn:
        row = await conn.fetchrow(
            """
            UPDATE leads
            SET kanban_status = $2,
                qualification = CASE
                    WHEN score >= 90 THEN 'WHALE'
                    WHEN score >= 70 THEN 'POTENTIAL'
                    WHEN score >= 45 THEN 'HOT'
                    ELSE qualification
                END,
                updated_at = NOW()
            WHERE id = $1 AND tenant_id = $3
            RETURNING id, name, email, phone, source, notes, qualification, score, kanban_status,
                      budget, unit_interest, metadata, created_at, updated_at
            """,
            payload.lead_id,
            stage,
            _tenant_scope(user),
        )
        if row is not None:
            await _sync_canonical_lead_bridge(request, conn, user, dict(row))
    if row is None:
        raise HTTPException(status_code=404, detail=f"Lead '{payload.lead_id}' not found.")
    data = _serialize_lead(row)
    await _broadcast_crm_event(
        request,
        {
            "type": "kanban_moved",
            "entity": "lead",
            "entity_id": payload.lead_id,
            "data": data,
        },
    )
    return {"status": "ok", "data": data}


@analytics_router.get("/sentiment-scatter")
async def sentiment_scatter(
    request: Request,
    user: UserPrincipal = Depends(get_current_user),
) -> list[dict[str, Any]]:
    await _ensure_schema(request)
    pool = await _get_pool(request)
    async with pool.acquire() as conn:
        rows = await conn.fetch(
            """
            SELECT id, name, score, qualification, kanban_status, source, notes, updated_at
            FROM leads
            WHERE tenant_id = $1 AND score IS NOT NULL
            ORDER BY score DESC, updated_at DESC
            """,
            _tenant_scope(user),
        )
    points: list[dict[str, Any]] = []
    for row in rows:
        score = int(row["score"] or 0)
        qualification = row["qualification"] or _infer_qualification(score, row["source"], row["notes"])
        points.append(
            {
                "id": row["id"],
                "name": row["name"],
                "sentiment_score": max(0, min(100, int(score * 0.82) + 10)),
                "response_time_ms": max(120, 10000 - (score * 55)),
                "score": score,
                "qualification": qualification,
                "kanban_status": _normalize_stage(row["kanban_status"]),
            }
        )
    return points
