# @File: backend/routers/ai_ticket_summary.py
# @Desc: AI-powered ticket thread summarization.
#
# Flow:
#   1. Auth the signed-in client via the standard Atoms auth dependency.
#   2. Resolve the WHMCS client by the user's email and load the requested
#      ticket via the existing ``whmcs`` service helpers. This also enforces
#      that the ticket belongs to the caller.
#   3. Build a compact conversation text from ticket + replies.
#   4. Check the cache table ``ticket_summaries`` — if we already summarized
#      at the same message count and reply id, return the cached result.
#   5. Otherwise call the AIHub ``gentxt`` service with gemini-2.5-pro and a
#      structured JSON prompt. Persist the JSON summary back to the cache.
import json
import logging
from typing import Any, Dict, List, Optional

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from core.database import get_db
from dependencies.auth import get_current_user
from models.ticket_summaries import Ticket_summaries
from schemas.aihub import ChatMessage, GenTxtRequest
from schemas.auth import UserResponse
from services.aihub import AIHubService
from services.whmcs import call_whmcs, get_client_by_email

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/v1/ai", tags=["ai-ticket-summary"])

SUMMARY_MODEL = "gemini-2.5-pro"

SYSTEM_PROMPT = (
    "You are a helpful support assistant. You are given a support ticket "
    "conversation between a client and support staff. Produce a structured "
    "summary to help the client quickly understand the current state of the "
    "thread.\n\n"
    "Return ONLY valid minified JSON, no prose, no markdown, no code fences. "
    "The JSON must follow this schema exactly:\n"
    "{\n"
    '  "tl_dr": "one-sentence plain-English overview",\n'
    '  "status": "short phrase describing current state (e.g. Awaiting staff reply, Resolved, Action needed from client)",\n'
    '  "key_points": ["bullet", "bullet", "bullet"],\n'
    '  "action_items": ["bullet", "bullet"],\n'
    '  "next_step": "the single most useful thing the client should do next"\n'
    "}\n"
    "Keep each bullet under 160 characters. If there are no action items, "
    "return an empty array. Never invent facts that are not present in the "
    "conversation."
)


def _safe_int(value: Any, default: int = 0) -> int:
    try:
        return int(value)
    except (TypeError, ValueError):
        return default


def _as_list(value: Any) -> List[Dict[str, Any]]:
    if value is None:
        return []
    if isinstance(value, list):
        return [v for v in value if isinstance(v, dict)]
    if isinstance(value, dict):
        return [value]
    return []


class SummarizeTicketRequest(BaseModel):
    ticket_id: int = Field(..., description="WHMCS ticket id")
    force: bool = Field(default=False, description="Ignore cache and regenerate")


class SummarizeTicketResponse(BaseModel):
    ticket_id: int
    model: str
    cached: bool
    tl_dr: str = ""
    status: str = ""
    key_points: List[str] = []
    action_items: List[str] = []
    next_step: str = ""


async def _load_ticket_conversation(
    ticket_id: int, user_email: str
) -> Dict[str, Any]:
    """Fetch + authz-check a WHMCS ticket, returning a compact dict."""
    client = await get_client_by_email(user_email)
    if not client:
        raise HTTPException(
            status_code=404,
            detail="No WHMCS client is linked to this account.",
        )
    client_id = _safe_int(client.get("userid") or client.get("id"))

    result = await call_whmcs("GetTicket", {"ticketid": ticket_id})
    ticket_owner = _safe_int(result.get("userid") or result.get("requestor_id"))
    if ticket_owner > 0 and ticket_owner != client_id:
        raise HTTPException(status_code=404, detail="Ticket not found.")

    messages: List[Dict[str, Any]] = []
    initial_body = result.get("message") or result.get("body")
    if initial_body:
        messages.append({
            "author": result.get("name") or "Client",
            "is_staff": False,
            "date": result.get("date") or "",
            "message": str(initial_body),
        })
    for raw in _as_list((result.get("replies") or {}).get("reply")):
        admin_name = raw.get("admin") or raw.get("adminusername")
        is_staff = bool(admin_name and str(admin_name).strip())
        messages.append({
            "author": admin_name or raw.get("name") or raw.get("email") or "Client",
            "is_staff": is_staff,
            "date": raw.get("date") or "",
            "message": raw.get("message") or "",
        })

    return {
        "id": ticket_id,
        "subject": result.get("subject") or "",
        "status": result.get("status") or "",
        "priority": result.get("priority") or result.get("urgency") or "",
        "department": result.get("deptname") or result.get("department") or "",
        "messages": messages,
    }


def _build_conversation_text(conv: Dict[str, Any]) -> str:
    lines: List[str] = []
    lines.append(f"Subject: {conv.get('subject') or '(no subject)'}")
    lines.append(f"Status: {conv.get('status') or 'Unknown'}")
    lines.append(f"Priority: {conv.get('priority') or 'Unknown'}")
    lines.append(f"Department: {conv.get('department') or 'Unknown'}")
    lines.append("")
    lines.append("Conversation (oldest first):")
    for idx, m in enumerate(conv.get("messages") or [], start=1):
        speaker = "SUPPORT" if m.get("is_staff") else "CLIENT"
        date = m.get("date") or ""
        author = m.get("author") or ""
        body = (m.get("message") or "").strip()
        # Truncate very long messages so we stay within token limits while
        # still giving the model enough context to summarize.
        if len(body) > 2000:
            body = body[:2000] + "… [truncated]"
        lines.append(f"[{idx}] {speaker} — {author} ({date})")
        lines.append(body)
        lines.append("")
    return "\n".join(lines)


def _parse_summary_json(raw: str) -> Dict[str, Any]:
    """Parse the model output into our summary shape, defensively."""
    text = (raw or "").strip()
    # Strip code fences if the model ignored instructions.
    if text.startswith("```"):
        text = text.strip("`")
        # After stripping fences the first token may be a language tag.
        first_nl = text.find("\n")
        if first_nl != -1:
            text = text[first_nl + 1 :]
        text = text.strip().rstrip("`").strip()
    try:
        data = json.loads(text)
    except Exception:
        # Try to locate the first JSON object in the string.
        start = text.find("{")
        end = text.rfind("}")
        if start != -1 and end != -1 and end > start:
            try:
                data = json.loads(text[start : end + 1])
            except Exception:
                data = {}
        else:
            data = {}

    if not isinstance(data, dict):
        data = {}

    def _as_str(v: Any) -> str:
        if v is None:
            return ""
        if isinstance(v, str):
            return v.strip()
        return str(v).strip()

    def _as_list_of_str(v: Any) -> List[str]:
        if isinstance(v, list):
            return [s for s in (_as_str(x) for x in v) if s][:8]
        return []

    return {
        "tl_dr": _as_str(data.get("tl_dr")),
        "status": _as_str(data.get("status")),
        "key_points": _as_list_of_str(data.get("key_points")),
        "action_items": _as_list_of_str(data.get("action_items")),
        "next_step": _as_str(data.get("next_step")),
    }


@router.post("/summarize-ticket", response_model=SummarizeTicketResponse)
async def summarize_ticket(
    data: SummarizeTicketRequest,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    """Summarize a WHMCS ticket thread for the signed-in client."""
    if not current_user.email:
        raise HTTPException(status_code=400, detail="Signed-in user has no email on file.")

    conv = await _load_ticket_conversation(data.ticket_id, current_user.email)
    messages = conv.get("messages") or []
    if len(messages) < 2:
        raise HTTPException(
            status_code=400,
            detail="Not enough messages to summarize (need at least 2).",
        )

    # Cache key components: ticket id + message count. If WHMCS doesn't expose
    # a monotonic reply id we fall back to message count, which is still
    # sufficient to detect new activity.
    cache_key = str(len(messages))
    ticket_key = str(data.ticket_id)

    # Try to read from cache unless the caller forced a regeneration.
    cached_row: Optional[Ticket_summaries] = None
    if not data.force:
        stmt = (
            select(Ticket_summaries)
            .where(Ticket_summaries.ticket_id == ticket_key)
            .where(Ticket_summaries.last_reply_id == cache_key)
            .order_by(Ticket_summaries.id.desc())
            .limit(1)
        )
        res = await db.execute(stmt)
        cached_row = res.scalar_one_or_none()

    if cached_row is not None:
        try:
            payload = json.loads(cached_row.summary) if cached_row.summary else {}
        except Exception:
            payload = {}
        return SummarizeTicketResponse(
            ticket_id=data.ticket_id,
            model=cached_row.model or SUMMARY_MODEL,
            cached=True,
            tl_dr=payload.get("tl_dr", ""),
            status=payload.get("status", ""),
            key_points=payload.get("key_points", []) or [],
            action_items=payload.get("action_items", []) or [],
            next_step=payload.get("next_step", ""),
        )

    # Generate a new summary via AIHub.
    convo_text = _build_conversation_text(conv)
    try:
        service = AIHubService()
        ai_response = await service.gentxt(
            GenTxtRequest(
                model=SUMMARY_MODEL,
                temperature=0.2,
                max_tokens=800,
                messages=[
                    ChatMessage(role="system", content=SYSTEM_PROMPT),
                    ChatMessage(role="user", content=convo_text),
                ],
            )
        )
    except Exception as e:
        logger.error("AI summarization failed: %s", e)
        raise HTTPException(status_code=502, detail="AI service is unavailable. Please try again later.")

    parsed = _parse_summary_json(ai_response.content)
    # Persist to cache (best-effort — do not fail the request on DB errors).
    try:
        row = Ticket_summaries(
            ticket_id=ticket_key,
            last_reply_id=cache_key,
            summary=json.dumps(parsed),
            model=SUMMARY_MODEL,
        )
        db.add(row)
        await db.commit()
    except Exception as e:
        logger.warning("Failed to cache ticket summary: %s", e)
        try:
            await db.rollback()
        except Exception:
            pass

    return SummarizeTicketResponse(
        ticket_id=data.ticket_id,
        model=SUMMARY_MODEL,
        cached=False,
        tl_dr=parsed.get("tl_dr", ""),
        status=parsed.get("status", ""),
        key_points=parsed.get("key_points", []),
        action_items=parsed.get("action_items", []),
        next_step=parsed.get("next_step", ""),
    )