"""
Support ticket intake + admin management APIs.
"""

from __future__ import annotations

import base64
import enum
import hashlib
import hmac
import json
import logging
import os
import secrets
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Literal
from uuid import uuid4

from fastapi import APIRouter, Depends, Header, HTTPException, Query
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select, func, or_
from sqlalchemy.ext.asyncio import AsyncSession

from ...auth_core import get_current_identity
from ...auth_security import AdminContext, require_admin
from ... import settings
from ...db import get_session
from ...models import (
    Account,
    Job,
    SupportTicket,
    SupportTicketNote,
    TicketNoteDirection,
    TicketSeverity,
    TicketSource,
    TicketStatus,
    TicketType,
    UsageQuotaModel,
)
from ...services.admin_audit import log_admin_action
from ...services.admin_alerts import notify_support_ticket_created
from ...services.gmail_support import GmailSupportService
from ...services.support_triage import SupportTriageService
from ...services.sos_enrichment import enrich_ticket_with_telemetry
from ...storage import get_storage_backend

log = logging.getLogger(__name__)

router = APIRouter(prefix="/support", tags=["Support"])
admin_router = APIRouter(prefix="/admin/support-tickets", tags=["Admin Support"])

ALLOWED_IMAGE_TYPES = {"image/png", "image/jpeg", "image/gif", "image/webp"}
MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024
MAX_ATTACHMENTS_PER_TICKET = 5


class AttachmentRef(BaseModel):
    object_id: str = Field(..., min_length=1)
    filename: Optional[str] = None
    content_type: str = "image/png"
    size_bytes: Optional[int] = None
    get_url: Optional[str] = None


class SupportTicketCreate(BaseModel):
    type: Literal["support", "feature"] = "support"
    category: str = Field(..., min_length=2, max_length=64)
    severity: Optional[Literal["critical", "high", "medium", "low"]] = None
    summary: str = Field(..., min_length=8, max_length=120)
    description: str = Field(..., min_length=20)
    job_id: Optional[str] = None
    job_verified: Optional[bool] = None
    doc_id: Optional[str] = None
    source: Optional[
        Literal["gsheets_addon", "web", "api", "email", "gremlin"]
    ] = None
    context_json: Optional[Dict[str, Any]] = None
    attachments: Optional[List[AttachmentRef]] = Field(default=None, max_length=5)

    @field_validator("severity")
    @classmethod
    def severity_required_for_support(cls, value: Optional[str], info: Any) -> Optional[str]:
        ticket_type = (info.data or {}).get("type")
        if ticket_type == "support" and value is None:
            raise ValueError("severity required for support tickets")
        return value


class SupportTicketUpdate(BaseModel):
    status: Optional[TicketStatus] = None
    severity: Optional[TicketSeverity] = None
    category: Optional[str] = None
    resolution: Optional[str] = None


class NoteCreate(BaseModel):
    content: str = Field(..., min_length=1)
    is_internal: bool = True


class SupportEmailSend(BaseModel):
    body: str = Field(..., min_length=1)
    subject_override: Optional[str] = None
    to_email: Optional[str] = None


class TriageMode(str, enum.Enum):
    FULL = "full"
    REPLY_ONLY = "reply_only"


class AITriageRequest(BaseModel):
    mode: TriageMode = TriageMode.FULL
    force: bool = False


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _to_snippet(text: Optional[str], limit: int = 140) -> Optional[str]:
    if not text:
        return None
    return text.strip()[:limit]


def _json_size(value: Any) -> int:
    try:
        return len(json.dumps(value, default=str).encode("utf-8"))
    except TypeError:
        return len(json.dumps(str(value)).encode("utf-8"))


def _truncate_context_json(
    context: Optional[Dict[str, Any]],
    max_bytes: int = 65536,
    preserve_keys: Optional[List[str]] = None,
) -> Optional[Dict[str, Any]]:
    if context is None:
        return None

    safe = dict(context)
    preserve = set(preserve_keys or [])
    if _json_size(safe) <= max_bytes:
        return safe

    for key in ("debug_bundle", "activity_log", "diagnostics", "diagnostics_snapshot", "sheet_snapshot"):
        if key in safe and key not in preserve:
            safe[key] = None
            if _json_size(safe) <= max_bytes:
                safe["truncated"] = True
                return safe

    truncated = {"truncated": True, "reason": "payload_too_large"}
    for key in preserve:
        if key in context:
            truncated[key] = context[key]
    if _json_size(truncated) <= max_bytes:
        return truncated
    return {"truncated": True, "reason": "payload_too_large"}


async def _build_account_snapshot(
    db: AsyncSession, account_id: Optional[str]
) -> Optional[Dict[str, Any]]:
    if not account_id:
        return None
    account = await db.get(Account, account_id)
    quota = await db.get(UsageQuotaModel, account_id)

    snapshot: Dict[str, Any] = {
        "captured_at": datetime.utcnow().isoformat(),
        "account": {
            "id": str(account_id),
            "name": account.name if account else None,
        },
    }

    if quota:
        snapshot["usage"] = {
            "tier": quota.tier,
            "subscription_status": quota.subscription_status,
            "monthly_rows_used": quota.monthly_rows_used,
            "monthly_rows_limit": quota.monthly_match_entitlement,
            "enrichment_credits_used": quota.enrichment_credits_used,
            "enrichment_credits_limit": quota.monthly_enrichment_entitlement,
            "daily_rows_used": quota.daily_rows_used,
            "daily_runs_used": quota.daily_runs_used,
            "active_schedules": quota.active_schedules,
            "grace_uses_remaining": quota.grace_uses_remaining,
            "tier_synced_at": quota.tier_synced_at.isoformat() if quota.tier_synced_at else None,
            "usage_updated_at": quota.updated_at.isoformat() if quota.updated_at else None,
            "reset_at": quota.reset_at.isoformat() if quota.reset_at else None,
            "tier_sync_source": quota.tier_sync_source,
        }

    return snapshot


def _generate_public_id() -> str:
    token = secrets.token_hex(2).upper()
    year = datetime.utcnow().strftime("%y")
    return f"SUP-{token}-{year}"


def _compute_identity_key(account_id: Optional[str], user_email: str) -> str:
    if account_id:
        return str(account_id)
    normalized = (user_email or "").strip().lower()
    return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16]


def _get_secret() -> str:
    secret = (
        getattr(settings, "SECRET_KEY", None)
        or getattr(settings, "WEBHOOK_HMAC_SECRET", None)
        or getattr(settings, "JWT_SECRET_KEY", None)
    )
    return secret or "dev-secret"


def _make_token(payload: dict, secret: str) -> str:
    body = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode()
    sig = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return base64.urlsafe_b64encode(body).decode() + "." + sig


def _maybe_make_absolute(url: str) -> str:
    base = os.getenv("PUBLIC_BASE_URL") or getattr(settings, "PUBLIC_BASE_URL", "")
    if not url or url.startswith("http://") or url.startswith("https://"):
        return url
    if not base:
        return url
    base = str(base).rstrip("/")
    normalized = url if str(url).startswith("/") else f"/{url}"
    return f"{base}{normalized}"


async def _generate_unique_public_id(db: AsyncSession, attempts: int = 8) -> str:
    for _ in range(attempts):
        public_id = _generate_public_id()
        existing = await db.execute(
            select(SupportTicket.id).where(SupportTicket.public_id == public_id)
        )
        if not existing.scalar_one_or_none():
            return public_id
    raise HTTPException(500, "Failed to generate support ticket ID")


async def _ensure_account(account_id: Optional[str], db: AsyncSession) -> None:
    if not account_id:
        return
    account = await db.get(Account, account_id)
    if account:
        return
    account = Account(id=account_id, name=f"Clerk Org {account_id}")
    db.add(account)
    await db.flush()


def _serialize_ticket(ticket: SupportTicket) -> Dict[str, Any]:
    return {
        "id": ticket.id,
        "public_id": ticket.public_id,
        "type": ticket.type.value if ticket.type else None,
        "category": ticket.category,
        "severity": ticket.severity.value if ticket.severity else None,
        "status": ticket.status.value if ticket.status else None,
        "source": ticket.source.value if ticket.source else None,
        "summary": ticket.summary,
        "description": ticket.description,
        "description_snippet": ticket.description_snippet,
        "job_id": ticket.job_id,
        "job_verified": ticket.job_verified,
        "doc_id": ticket.doc_id,
        "context_json": ticket.context_json,
        "gmail_thread_id": ticket.gmail_thread_id,
        "resolution": ticket.resolution,
        "resolved_by": ticket.resolved_by,
        "created_at": ticket.created_at.isoformat() if ticket.created_at else None,
        "updated_at": ticket.updated_at.isoformat() if ticket.updated_at else None,
        "resolved_at": ticket.resolved_at.isoformat() if ticket.resolved_at else None,
        "user_email": ticket.user_email,
        "account_id": ticket.account_id,
    }


def _serialize_note(note: SupportTicketNote) -> Dict[str, Any]:
    return {
        "id": note.id,
        "ticket_id": note.ticket_id,
        "author_email": note.author_email,
        "content": note.content,
        "gmail_message_id": note.gmail_message_id,
        "rfc822_message_id": note.rfc822_message_id,
        "gmail_thread_id": note.gmail_thread_id,
        "direction": note.direction.value if note.direction else None,
        "is_internal": note.is_internal,
        "created_at": note.created_at.isoformat() if note.created_at else None,
    }


def _format_email_subject(
    public_id: str, subject_text: str, is_reply: bool
) -> str:
    base = (subject_text or "").strip() or "Support request"
    token = f"[{public_id}]"
    if token not in base:
        base = f"{token} {base}".strip()
    if is_reply and not base.lower().startswith("re:"):
        base = f"Re: {base}"
    return base


def _collect_thread_headers(
    notes: List[SupportTicketNote],
) -> tuple[Optional[str], List[str]]:
    references: List[str] = []
    seen = set()
    for note in notes:
        if not note.rfc822_message_id:
            continue
        value = note.rfc822_message_id.strip()
        if value and value not in seen:
            references.append(value)
            seen.add(value)

    inbound_reply = None
    outbound_reply = None
    for note in reversed(notes):
        if not note.rfc822_message_id:
            continue
        if note.direction == TicketNoteDirection.INBOUND:
            inbound_reply = note.rfc822_message_id
            break
        if note.direction == TicketNoteDirection.OUTBOUND and outbound_reply is None:
            outbound_reply = note.rfc822_message_id

    return inbound_reply or outbound_reply, references


async def _load_ticket_context(
    db: AsyncSession, ticket: SupportTicket
) -> tuple[Optional[Account], Optional[UsageQuotaModel], List[Dict[str, Any]]]:
    account = None
    quota = None
    jobs: List[Dict[str, Any]] = []

    if ticket.account_id:
        account = await db.get(Account, ticket.account_id)
        quota = await db.get(UsageQuotaModel, ticket.account_id)

        job_stmt = (
            select(Job)
            .where(Job.account_id == ticket.account_id)
            .order_by(Job.created_at.desc())
            .limit(5)
        )
        job_rows = (await db.execute(job_stmt)).scalars().all()
        jobs = [
            {
                "id": row.id,
                "status": row.status.value if row.status else None,
                "job_type": row.job_type.value if row.job_type else None,
                "created_at": row.created_at.isoformat() if row.created_at else None,
                "updated_at": row.updated_at.isoformat() if row.updated_at else None,
            }
            for row in job_rows
        ]

    return account, quota, jobs


@router.post("/tickets/attach/presign")
async def presign_attachment(
    content_type: str = Query(default="image/png"),
    filename: Optional[str] = Query(default=None),
    identity: Dict[str, Any] = Depends(get_current_identity),
) -> Dict[str, Any]:
    if content_type not in ALLOWED_IMAGE_TYPES:
        raise HTTPException(400, f"Unsupported content type: {content_type}")

    account_id = identity.get("org_id")
    user_email = identity.get("email") or "unknown"
    if not account_id:
        raise HTTPException(401, "No organization selected on token")

    identity_key = _compute_identity_key(str(account_id), user_email)
    ext = content_type.split("/")[-1]
    object_id = f"support-attachments/{identity_key}/{uuid4()}.{ext}"

    storage = get_storage_backend()
    if settings.STORAGE_BACKEND != "local":
        try:
            put_url = await storage.generate_presigned_url(
                object_id, operation="put_object"
            )
            get_url = await storage.generate_presigned_url(
                object_id, operation="get_object"
            )
        except Exception as exc:
            raise HTTPException(500, f"presign_failed: {exc}")

        return {
            "ok": True,
            "data": {"object_id": object_id, "put_url": put_url, "get_url": get_url},
        }

    ttl = int(os.getenv("SIGNED_URL_TTL_SEC", "86400"))
    token = _make_token(
        {
            "key": object_id,
            "ct": content_type,
            "acct": str(account_id),
            "exp": int((datetime.utcnow() + timedelta(seconds=ttl)).timestamp()),
        },
        _get_secret(),
    )
    put_url = _maybe_make_absolute(f"/api/v2/uploads/put/{object_id}?token={token}")

    try:
        get_url = await storage.generate_presigned_url(
            object_id, operation="get_object"
        )
    except Exception:
        get_url = f"/api/v1/storage/download/{object_id}?token={token}"

    return {
        "ok": True,
        "data": {
            "object_id": object_id,
            "put_url": put_url,
            "get_url": _maybe_make_absolute(get_url),
        },
    }


@router.post("/tickets")
async def create_ticket(
    ticket: SupportTicketCreate,
    idempotency_key: Optional[str] = Header(None, alias="Idempotency-Key"),
    identity: Dict[str, Any] = Depends(get_current_identity),
    db: AsyncSession = Depends(get_session),
) -> Dict[str, Any]:
    account_id = identity.get("org_id")
    user_email = identity.get("email") or "unknown"

    if not account_id:
        raise HTTPException(401, "No organization selected on token")

    identity_key = _compute_identity_key(str(account_id), user_email)

    await _ensure_account(account_id, db)

    if idempotency_key:
        existing = await db.execute(
            select(SupportTicket).where(
                SupportTicket.identity_key == identity_key,
                SupportTicket.idempotency_key == idempotency_key,
            )
        )
        ticket_row = existing.scalar_one_or_none()
        if ticket_row:
            return {
                "ok": True,
                "data": {
                    "id": ticket_row.id,
                    "public_id": ticket_row.public_id,
                    "status": "exists",
                },
            }

    attachments_payload: Optional[List[Dict[str, Any]]] = None
    if ticket.attachments:
        if len(ticket.attachments) > MAX_ATTACHMENTS_PER_TICKET:
            raise HTTPException(
                400, f"Max {MAX_ATTACHMENTS_PER_TICKET} attachments allowed"
            )
        attachments_payload = []
        for attachment in ticket.attachments:
            if attachment.content_type not in ALLOWED_IMAGE_TYPES:
                raise HTTPException(
                    400, f"Unsupported content type: {attachment.content_type}"
                )
            if attachment.size_bytes and attachment.size_bytes > MAX_ATTACHMENT_SIZE:
                raise HTTPException(400, "Attachment exceeds 10MB limit")
            if not attachment.object_id.startswith(
                f"support-attachments/{identity_key}/"
            ):
                raise HTTPException(403, "Invalid attachment ownership")
            attachments_payload.append(attachment.model_dump())

    context_json = dict(ticket.context_json or {})
    if attachments_payload:
        context_json["attachments"] = attachments_payload
    snapshot = await _build_account_snapshot(db, str(account_id))
    if snapshot:
        context_json["server_snapshot"] = snapshot
    context_json = _truncate_context_json(
        context_json,
        preserve_keys=["attachments", "server_snapshot"],
    )
    description_snippet = _to_snippet(ticket.description)
    public_id = await _generate_unique_public_id(db)

    record = SupportTicket(
        public_id=public_id,
        idempotency_key=idempotency_key or None,
        identity_key=identity_key,
        account_id=str(account_id),
        user_email=user_email,
        type=TicketType(ticket.type),
        category=ticket.category,
        severity=TicketSeverity(ticket.severity) if ticket.severity else None,
        status=TicketStatus.OPEN,
        source=TicketSource(ticket.source)
        if ticket.source
        else TicketSource.GSHEETS_ADDON,
        summary=ticket.summary.strip(),
        description=ticket.description,
        description_snippet=description_snippet,
        job_id=ticket.job_id,
        job_verified=bool(ticket.job_verified) if ticket.job_verified is not None else False,
        doc_id=ticket.doc_id,
        context_json=context_json,
    )

    db.add(record)
    await db.flush()

    try:
        sheet_id = (context_json or {}).get("sheet_id") or record.doc_id
        telemetry_bundle = await enrich_ticket_with_telemetry(
            db=db,
            org_id=str(account_id),
            job_id=record.job_id,
            sheet_id=sheet_id,
            ticket_created_at=record.created_at or datetime.now(timezone.utc),
            context_json=context_json,
        )
        record.context_json = {
            **(record.context_json or {}),
            "telemetry_bundle": telemetry_bundle,
        }
    except Exception as exc:
        log.warning("Telemetry enrichment failed: %s", exc)
        record.context_json = {
            **(record.context_json or {}),
            "telemetry_bundle": {"error": "telemetry_lookup_failed"},
        }

    await db.commit()
    await db.refresh(record)

    try:
        await notify_support_ticket_created(
            public_id=record.public_id,
            account_id=record.account_id,
            user_email=record.user_email,
            ticket_type=record.type.value if record.type else None,
            category=record.category,
            severity=record.severity.value if record.severity else None,
            source=record.source.value if record.source else None,
            summary=record.summary,
            description_snippet=record.description_snippet or record.description,
            created_at=record.created_at,
        )
    except Exception as exc:
        log.warning("Failed to send support ticket admin alert: %s", exc)

    return {
        "ok": True,
        "data": {
            "id": record.id,
            "public_id": record.public_id,
            "status": "created",
        },
    }


@router.get("/tickets/{public_id}")
async def get_ticket_status(
    public_id: str,
    identity: Dict[str, Any] = Depends(get_current_identity),
    db: AsyncSession = Depends(get_session),
) -> Dict[str, Any]:
    account_id = identity.get("org_id")
    if not account_id:
        raise HTTPException(401, "No organization selected on token")

    result = await db.execute(
        select(SupportTicket).where(
            SupportTicket.public_id == public_id,
            SupportTicket.account_id == account_id,
        )
    )
    ticket = result.scalar_one_or_none()
    if not ticket:
        raise HTTPException(404, "Ticket not found")

    return {"ok": True, "data": _serialize_ticket(ticket)}


@admin_router.get("")
async def list_tickets(
    admin_ctx: AdminContext = Depends(require_admin),
    db: AsyncSession = Depends(get_session),
    status: Optional[TicketStatus] = Query(None),
    severity: Optional[TicketSeverity] = Query(None),
    type: Optional[TicketType] = Query(None),
    source: Optional[TicketSource] = Query(None),
    account_id: Optional[str] = Query(None),
    search: Optional[str] = Query(None),
    page: int = Query(1, ge=1),
    limit: int = Query(50, ge=1, le=100),
) -> Dict[str, Any]:
    # Build filters list to apply to both count and main query
    filters = []
    if status:
        filters.append(SupportTicket.status == status)
    if severity:
        filters.append(SupportTicket.severity == severity)
    if type:
        filters.append(SupportTicket.type == type)
    if source:
        filters.append(SupportTicket.source == source)
    if account_id:
        filters.append(SupportTicket.account_id == account_id)

    # Search filter needs join, so handle separately
    search_filter = None
    if search:
        like = f"%{search}%"
        search_filter = or_(
            SupportTicket.public_id.ilike(like),
            SupportTicket.user_email.ilike(like),
            SupportTicket.summary.ilike(like),
            Account.name.ilike(like),
        )

    # Count query - explicit select_from to avoid ambiguity
    count_stmt = (
        select(func.count(SupportTicket.id))
        .select_from(SupportTicket)
        .outerjoin(Account, SupportTicket.account_id == Account.id)
    )
    for f in filters:
        count_stmt = count_stmt.where(f)
    if search_filter is not None:
        count_stmt = count_stmt.where(search_filter)

    total = (await db.execute(count_stmt)).scalar_one()

    # Main query with all joins
    stmt = (
        select(SupportTicket, Account, UsageQuotaModel)
        .select_from(SupportTicket)
        .outerjoin(Account, SupportTicket.account_id == Account.id)
        .outerjoin(UsageQuotaModel, UsageQuotaModel.account_id == Account.id)
    )
    for f in filters:
        stmt = stmt.where(f)
    if search_filter is not None:
        stmt = stmt.where(search_filter)

    stmt = stmt.order_by(SupportTicket.created_at.desc())
    stmt = stmt.offset((page - 1) * limit).limit(limit)

    rows = (await db.execute(stmt)).all()
    tickets: List[Dict[str, Any]] = []
    for ticket, account, quota in rows:
        rows_used = quota.monthly_rows_used if quota else 0
        rows_limit = quota.monthly_match_entitlement if quota else 0
        usage_pct = None
        if rows_limit:
            usage_pct = round((rows_used / rows_limit) * 100, 1)

        tickets.append(
            {
                "public_id": ticket.public_id,
                "summary": ticket.summary,
                "description_snippet": ticket.description_snippet,
                "category": ticket.category,
                "severity": ticket.severity.value if ticket.severity else None,
                "status": ticket.status.value if ticket.status else None,
                "type": ticket.type.value if ticket.type else None,
                "source": ticket.source.value if ticket.source else None,
                "user_email": ticket.user_email,
                "account_id": ticket.account_id,
                "account_name": account.name if account else None,
                "tier": quota.tier if quota else None,
                "usage_percent": usage_pct,
                "created_at": ticket.created_at.isoformat() if ticket.created_at else None,
                "updated_at": ticket.updated_at.isoformat() if ticket.updated_at else None,
            }
        )

    await log_admin_action(
        admin_user_id=admin_ctx.user_id,
        admin_email=admin_ctx.email,
        action="list_support_tickets",
        db=db,
        details={
            "page": page,
            "limit": limit,
            "status": status.value if status else None,
            "severity": severity.value if severity else None,
            "type": type.value if type else None,
            "search": search,
        },
    )

    return {
        "ok": True,
        "data": {
            "tickets": tickets,
            "pagination": {
                "page": page,
                "limit": limit,
                "total": total,
                "pages": (total + limit - 1) // limit,
            },
        },
    }


@admin_router.get("/{public_id}")
async def get_ticket_detail(
    public_id: str,
    admin_ctx: AdminContext = Depends(require_admin),
    db: AsyncSession = Depends(get_session),
) -> Dict[str, Any]:
    result = await db.execute(
        select(SupportTicket).where(SupportTicket.public_id == public_id)
    )
    ticket = result.scalar_one_or_none()
    if not ticket:
        raise HTTPException(404, "Ticket not found")

    account = None
    quota = None
    jobs: List[Dict[str, Any]] = []
    if ticket.account_id:
        account = await db.get(Account, ticket.account_id)
        quota = await db.get(UsageQuotaModel, ticket.account_id)

        job_stmt = (
            select(Job)
            .where(Job.account_id == ticket.account_id)
            .order_by(Job.created_at.desc())
            .limit(5)
        )
        job_rows = (await db.execute(job_stmt)).scalars().all()
        jobs = [
            {
                "id": row.id,
                "status": row.status.value if row.status else None,
                "job_type": row.job_type.value if row.job_type else None,
                "created_at": row.created_at.isoformat() if row.created_at else None,
                "updated_at": row.updated_at.isoformat() if row.updated_at else None,
            }
            for row in job_rows
        ]

    notes_stmt = (
        select(SupportTicketNote)
        .where(SupportTicketNote.ticket_id == ticket.id)
        .order_by(SupportTicketNote.created_at.asc())
    )
    notes = (await db.execute(notes_stmt)).scalars().all()

    # Build response data BEFORE audit logging to avoid issues if audit
    # logging fails and triggers a rollback (which would detach objects)
    usage = None
    if quota:
        usage = {
            "tier": quota.tier,
            "status": quota.subscription_status,
            "monthly_rows_used": quota.monthly_rows_used,
            "monthly_rows_limit": quota.monthly_match_entitlement,
            "enrichment_credits_used": quota.enrichment_credits_used,
            "enrichment_credits_limit": quota.monthly_enrichment_entitlement,
        }

    account_data = None
    if account:
        account_data = {
            "id": account.id,
            "name": account.name,
            "stripe_customer_id": account.stripe_customer_id,
            "stripe_subscription_id": account.stripe_subscription_id,
            "data_region": account.data_region,
        }

    ticket_data = _serialize_ticket(ticket)
    notes_data = [_serialize_note(note) for note in notes]

    # Audit logging happens after response data is built
    await log_admin_action(
        admin_user_id=admin_ctx.user_id,
        admin_email=admin_ctx.email,
        action="view_support_ticket",
        db=db,
        target_account_id=ticket.account_id,
        details={"public_id": public_id},
    )

    return {
        "ok": True,
        "data": {
            "ticket": ticket_data,
            "account": account_data,
            "usage": usage,
            "recent_jobs": jobs,
            "notes": notes_data,
        },
    }


@admin_router.post("/{public_id}/ai-triage")
async def run_ai_triage(
    public_id: str,
    request: AITriageRequest,
    admin_ctx: AdminContext = Depends(require_admin),
    db: AsyncSession = Depends(get_session),
) -> Dict[str, Any]:
    result = await db.execute(
        select(SupportTicket).where(SupportTicket.public_id == public_id)
    )
    ticket = result.scalar_one_or_none()
    if not ticket:
        raise HTTPException(404, "Ticket not found")

    account, quota, jobs = await _load_ticket_context(db, ticket)

    try:
        service = SupportTriageService()
    except Exception as exc:
        log.exception("AI triage initialization failed: %s", exc)
        raise HTTPException(503, "AI triage unavailable")

    try:
        triage_result, context_hash, cached = await service.run_triage(
            db=db,
            ticket=ticket,
            account=account,
            usage=quota,
            recent_jobs=jobs,
            mode=request.mode.value,
            force=request.force,
        )
    except Exception as exc:
        message = str(exc).lower()
        if "reauthentication" in message or "application-default login" in message:
            log.warning("AI triage requires gcloud auth: %s", exc)
            raise HTTPException(
                503,
                "Vertex AI auth required. Run `gcloud auth application-default login`.",
            )
        if isinstance(exc, TimeoutError) or "timed out" in message:
            log.warning("AI triage timed out: %s", exc)
            raise HTTPException(
                504,
                "Vertex AI request timed out. Try again or use reply-only triage.",
            )
        log.exception("AI triage run failed: %s", exc)
        raise HTTPException(500, "AI triage failed")

    ticket.ai_triage_hash = context_hash
    ticket.ai_triage_last_run_at = datetime.now(timezone.utc)
    db.add(ticket)

    if not cached:
        note = SupportTicketNote(
            ticket_id=ticket.id,
            author_email="ai@foundryops.io",
            content=json.dumps(triage_result),
            is_internal=True,
            direction=TicketNoteDirection.INTERNAL,
        )
        db.add(note)

    await db.commit()

    await log_admin_action(
        admin_user_id=admin_ctx.user_id,
        admin_email=admin_ctx.email,
        action="run_ai_triage",
        db=db,
        target_account_id=ticket.account_id,
        details={"public_id": public_id, "mode": request.mode.value, "force": request.force},
    )

    return {"ok": True, "data": triage_result, "cached": cached}


@admin_router.patch("/{public_id}")
async def update_ticket(
    public_id: str,
    payload: SupportTicketUpdate,
    admin_ctx: AdminContext = Depends(require_admin),
    db: AsyncSession = Depends(get_session),
) -> Dict[str, Any]:
    result = await db.execute(
        select(SupportTicket).where(SupportTicket.public_id == public_id)
    )
    ticket = result.scalar_one_or_none()
    if not ticket:
        raise HTTPException(404, "Ticket not found")

    if payload.status:
        ticket.status = payload.status
        if payload.status == TicketStatus.RESOLVED:
            ticket.resolved_at = datetime.now(timezone.utc)
        elif payload.status != TicketStatus.RESOLVED:
            ticket.resolved_at = None

    if payload.severity is not None:
        ticket.severity = payload.severity

    if payload.category is not None:
        ticket.category = payload.category

    if payload.resolution is not None:
        ticket.resolution = payload.resolution
        ticket.resolved_by = admin_ctx.email or admin_ctx.user_id

    db.add(ticket)
    await db.commit()
    await db.refresh(ticket)

    await log_admin_action(
        admin_user_id=admin_ctx.user_id,
        admin_email=admin_ctx.email,
        action="update_support_ticket",
        db=db,
        target_account_id=ticket.account_id,
        details={"public_id": public_id, "status": ticket.status.value},
    )

    return {"ok": True, "data": _serialize_ticket(ticket)}


@admin_router.post("/{public_id}/send-email")
async def send_support_email(
    public_id: str,
    payload: SupportEmailSend,
    admin_ctx: AdminContext = Depends(require_admin),
    db: AsyncSession = Depends(get_session),
) -> Dict[str, Any]:
    result = await db.execute(
        select(SupportTicket).where(SupportTicket.public_id == public_id)
    )
    ticket = result.scalar_one_or_none()
    if not ticket:
        raise HTTPException(404, "Ticket not found")

    to_email = (payload.to_email or ticket.user_email or "").strip()
    if not to_email:
        raise HTTPException(400, "Recipient email is required")

    subject_text = payload.subject_override or ticket.summary or "Support request"
    is_reply = bool(ticket.gmail_thread_id)
    subject = _format_email_subject(ticket.public_id, subject_text, is_reply)

    notes_stmt = (
        select(SupportTicketNote)
        .where(SupportTicketNote.ticket_id == ticket.id)
        .order_by(SupportTicketNote.created_at.asc())
    )
    notes = (await db.execute(notes_stmt)).scalars().all()
    in_reply_to, references = _collect_thread_headers(notes)

    try:
        gmail = await GmailSupportService.from_secret(db)
        if ticket.gmail_thread_id:
            send_result = await gmail.send_reply(
                to_email=to_email,
                ticket_public_id=ticket.public_id,
                subject=subject,
                body=payload.body,
                gmail_thread_id=ticket.gmail_thread_id,
                in_reply_to=in_reply_to,
                references=references,
            )
        else:
            send_result = await gmail.send_new_ticket_email(
                to_email=to_email,
                ticket_public_id=ticket.public_id,
                subject=subject,
                body=payload.body,
            )
    except Exception as exc:
        log.error("Gmail send failed: %s", exc)
        raise HTTPException(503, "Gmail send failed")

    gmail_thread_id = send_result.get("gmail_thread_id")
    if gmail_thread_id and ticket.gmail_thread_id != gmail_thread_id:
        ticket.gmail_thread_id = gmail_thread_id
        db.add(ticket)

    note_content = payload.body.strip()
    if subject:
        note_content = f"Subject: {subject}\n\n{note_content}"

    note = SupportTicketNote(
        ticket_id=ticket.id,
        author_email=admin_ctx.email or admin_ctx.user_id,
        content=note_content,
        is_internal=False,
        direction=TicketNoteDirection.OUTBOUND,
        gmail_message_id=send_result.get("gmail_message_id"),
        rfc822_message_id=send_result.get("rfc822_message_id"),
        gmail_thread_id=gmail_thread_id,
    )
    db.add(note)
    await db.commit()
    await db.refresh(note)

    await log_admin_action(
        admin_user_id=admin_ctx.user_id,
        admin_email=admin_ctx.email,
        action="send_support_ticket_email",
        db=db,
        target_account_id=ticket.account_id,
        details={"public_id": public_id, "to_email": to_email},
    )

    return {
        "ok": True,
        "data": {
            "ticket": _serialize_ticket(ticket),
            "note": _serialize_note(note),
            "gmail_thread_id": gmail_thread_id,
        },
    }


@admin_router.post("/{public_id}/notes")
async def add_note(
    public_id: str,
    payload: NoteCreate,
    admin_ctx: AdminContext = Depends(require_admin),
    db: AsyncSession = Depends(get_session),
) -> Dict[str, Any]:
    result = await db.execute(
        select(SupportTicket).where(SupportTicket.public_id == public_id)
    )
    ticket = result.scalar_one_or_none()
    if not ticket:
        raise HTTPException(404, "Ticket not found")

    note = SupportTicketNote(
        ticket_id=ticket.id,
        author_email=admin_ctx.email or admin_ctx.user_id,
        content=payload.content,
        is_internal=payload.is_internal,
        direction=TicketNoteDirection.INTERNAL,
    )
    db.add(note)
    await db.commit()
    await db.refresh(note)

    await log_admin_action(
        admin_user_id=admin_ctx.user_id,
        admin_email=admin_ctx.email,
        action="add_support_ticket_note",
        db=db,
        target_account_id=ticket.account_id,
        details={"public_id": public_id},
    )

    return {"ok": True, "data": _serialize_note(note)}
