import time
import json
import hmac
import hashlib
import re
from typing import Any, Dict, Optional, List
from hmac import compare_digest

from fastapi import APIRouter, Request, HTTPException

from app.analyzer import analyze
from app.config import settings
from app.chatwoot_client import add_private_note
from app.message_cleaner import extract_chatwoot_content
from app.persistence import (
    get_recent_chatwoot_public_context,
    get_state_for_conversation,
    mark_raw_event_error,
    mark_raw_event_processed,
    save_raw_event,
)
from app.schemas import AnalyzeRequest
from app.task_service import auto_complete_task_from_outgoing_message


router = APIRouter(prefix="/webhooks", tags=["webhooks"])


def as_str(value: Any) -> Optional[str]:
    if value is None:
        return None
    return str(value)


def get_nested(data: Dict[str, Any], *keys: str) -> Any:
    current: Any = data

    for key in keys:
        if not isinstance(current, dict):
            return None
        current = current.get(key)

    return current


def compact_text(value: Any, limit: int = 450) -> str:
    """Normaliza texto para contexto LLM compacto."""
    text = str(value or "")
    text = re.sub(r"<[^>]+>", " ", text)
    text = re.sub(r"\s+", " ", text).strip()
    if len(text) > limit:
        return text[: limit - 1].rstrip() + "…"
    return text


def extract_email_subject(payload: Dict[str, Any], message: Dict[str, Any], conversation: Dict[str, Any]) -> str:
    """Tenta extrair o assunto do email a partir das variantes comuns do Chatwoot."""
    candidates = [
        get_nested(message, "content_attributes", "email", "subject"),
        get_nested(payload, "content_attributes", "email", "subject"),
        get_nested(message, "content_attributes", "subject"),
        get_nested(payload, "content_attributes", "subject"),
        get_nested(conversation, "additional_attributes", "mail_subject"),
        get_nested(payload, "conversation", "additional_attributes", "mail_subject"),
        conversation.get("subject") if isinstance(conversation, dict) else None,
        payload.get("subject"),
    ]

    # Em alguns webhooks o assunto vem dentro de uma mensagem do array conversation.messages.
    messages = conversation.get("messages") if isinstance(conversation, dict) else None
    if isinstance(messages, list):
        for item in messages:
            if isinstance(item, dict):
                candidates.append(get_nested(item, "content_attributes", "email", "subject"))
                candidates.append(get_nested(item, "content_attributes", "subject"))

    for candidate in candidates:
        cleaned = compact_text(candidate, limit=180)
        if cleaned:
            return cleaned
    return ""


def message_role(message: Dict[str, Any]) -> str:
    raw_type = str(
        message.get("message_type")
        or message.get("direction")
        or message.get("sender_type")
        or ""
    ).lower()
    if raw_type in {"outgoing", "outbound", "1"}:
        return "BLIF"
    return "Cliente"


def message_timestamp(message: Dict[str, Any]) -> Any:
    return (
        message.get("created_at")
        or message.get("updated_at")
        or message.get("timestamp")
        or message.get("id")
        or 0
    )


def extract_message_content_for_context(message: Dict[str, Any]) -> str:
    try:
        content = extract_chatwoot_content(message, message)
    except Exception:
        content = message.get("content") or message.get("text") or message.get("body") or ""
    return compact_text(content, limit=360)


def extract_recent_conversation_context(
    payload: Dict[str, Any],
    current_message_id: Optional[str],
    max_messages: int = 2,
) -> List[str]:
    """Extrai as últimas mensagens relevantes antes da mensagem atual.

    Não envia a thread inteira ao LLM: usa no máximo `max_messages` linhas compactas.
    """
    conversation = (
        payload.get("conversation")
        if isinstance(payload.get("conversation"), dict)
        else get_nested(payload, "message", "conversation")
        if isinstance(get_nested(payload, "message", "conversation"), dict)
        else {}
    )
    messages = conversation.get("messages") if isinstance(conversation, dict) else None
    if not isinstance(messages, list):
        return []

    normalized: List[Dict[str, Any]] = []
    for item in messages:
        if not isinstance(item, dict):
            continue
        if item.get("private") or item.get("content_type") == "input_select":
            continue
        item_id = as_str(item.get("id") or item.get("message_id"))
        if current_message_id and item_id == current_message_id:
            continue
        content = extract_message_content_for_context(item)
        if not content:
            continue
        normalized.append({
            "sort": message_timestamp(item),
            "role": message_role(item),
            "content": content,
        })

    try:
        normalized.sort(key=lambda row: row["sort"])
    except Exception:
        pass

    recent = normalized[-max_messages:]
    return [f"- {row['role']}: {row['content']}" for row in recent]


def build_triage_previous_context(payload: Dict[str, Any], extracted: Dict[str, Any]) -> str:
    message = payload.get("message") if isinstance(payload.get("message"), dict) else payload
    conversation = (
        payload.get("conversation")
        if isinstance(payload.get("conversation"), dict)
        else message.get("conversation") if isinstance(message.get("conversation"), dict)
        else {}
    )

    lines: List[str] = ["Origem: webhook Chatwoot."]

    subject = extract_email_subject(payload, message, conversation)
    if subject:
        lines.append(f"Assunto: {subject}")

    recent_lines = extract_recent_conversation_context(
        payload=payload,
        current_message_id=extracted.get("source_event_id"),
        max_messages=2,
    )
    if not recent_lines:
        recent_lines = get_recent_chatwoot_public_context(
            extracted.get("conversation_id"),
            current_source_event_id=extracted.get("source_event_id"),
            max_messages=2,
        )

    if recent_lines:
        lines.append("Últimos 2 emails públicos antes da mensagem atual, apenas para contexto:")
        lines.extend(recent_lines)
    else:
        lines.append("Últimos 2 emails públicos: não disponível no webhook nem em raw_events.")

    return "\n".join(lines)


def extract_chatwoot_event(payload: Dict[str, Any]) -> Dict[str, Any]:
    event_type = (
        payload.get("event")
        or payload.get("webhook_event")
        or payload.get("event_type")
        or payload.get("name")
        or "unknown"
    )

    message = payload.get("message") if isinstance(payload.get("message"), dict) else payload

    conversation = (
        payload.get("conversation")
        if isinstance(payload.get("conversation"), dict)
        else message.get("conversation") if isinstance(message.get("conversation"), dict)
        else {}
    )

    sender = (
        payload.get("sender")
        if isinstance(payload.get("sender"), dict)
        else message.get("sender") if isinstance(message.get("sender"), dict)
        else {}
    )

    contact = (
        payload.get("contact")
        if isinstance(payload.get("contact"), dict)
        else conversation.get("contact") if isinstance(conversation.get("contact"), dict)
        else get_nested(conversation, "contact_inbox", "contact")
        or sender
        or {}
    )

    content = extract_chatwoot_content(payload, message)

    conversation_id = (
        payload.get("conversation_id")
        or message.get("conversation_id")
        or conversation.get("id")
        or get_nested(payload, "conversation", "id")
    )

    contact_id = (
        payload.get("contact_id")
        or contact.get("id")
        or sender.get("id")
        or get_nested(conversation, "contact_inbox", "contact", "id")
    )

    source_event_id = (
        payload.get("id")
        or message.get("id")
        or payload.get("message_id")
    )

    message_type = (
        message.get("message_type")
        or payload.get("message_type")
        or message.get("direction")
        or payload.get("direction")
        or ""
    )

    message_type_text = str(message_type).lower()
    is_outgoing = message_type_text in {"outgoing", "outbound", "1"}

    is_private = bool(
        message.get("private")
        or payload.get("private")
        or message.get("content_type") == "input_select"
    )

    sender_name = (
        sender.get("name")
        or sender.get("available_name")
        or sender.get("display_name")
        or sender.get("email")
    )

    sender_type = (
        sender.get("type")
        or sender.get("role")
        or sender.get("account_role")
        or ""
    )

    return {
        "event_type": str(event_type),
        "source_event_id": as_str(source_event_id),
        "conversation_id": as_str(conversation_id),
        "contact_id": as_str(contact_id),
        "content": str(content or "").strip(),
        "message_type": message_type_text,
        "is_outgoing": is_outgoing,
        "is_private": is_private,
        "sender_name": as_str(sender_name),
        "sender_type": as_str(sender_type),
    }


def validate_chatwoot_webhook_secret(request: Request) -> None:
    expected = (settings.clientflow_webhook_secret or "").strip()

    # Em desenvolvimento mantém compatibilidade; em produção exige segredo.
    if not expected:
        if str(settings.env or "").strip().lower() in {"prod", "production", "staging"}:
            raise HTTPException(status_code=500, detail="CLIENTFLOW_WEBHOOK_SECRET not configured")
        return

    received = (
        request.query_params.get("token")
        or request.headers.get("X-ClientFlow-Webhook-Secret")
        or request.headers.get("X-Webhook-Secret")
        or ""
    ).strip()

    if not received or not compare_digest(received, expected):
        raise HTTPException(status_code=401, detail="invalid webhook secret")



def validate_chatwoot_signature(request: Request, raw_body: bytes) -> None:
    secret = (settings.clientflow_webhook_secret or "").strip()

    # Em desenvolvimento mantém compatibilidade; em produção exige assinatura.
    if not secret:
        if str(settings.env or "").strip().lower() in {"prod", "production", "staging"}:
            raise HTTPException(status_code=500, detail="CLIENTFLOW_WEBHOOK_SECRET not configured")
        return

    received_signature = request.headers.get("X-Chatwoot-Signature", "")
    timestamp = request.headers.get("X-Chatwoot-Timestamp", "")

    if not received_signature or not timestamp:
        raise HTTPException(status_code=401, detail="missing chatwoot signature")

    try:
        ts = int(timestamp)
    except ValueError:
        raise HTTPException(status_code=401, detail="invalid chatwoot timestamp")

    # Anti-replay: rejeita eventos com mais de 5 minutos.
    if abs(int(time.time()) - ts) > 300:
        raise HTTPException(status_code=401, detail="expired chatwoot signature")

    signed_payload = timestamp.encode("utf-8") + b"." + raw_body
    expected_signature = "sha256=" + hmac.new(
        secret.encode("utf-8"),
        signed_payload,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(expected_signature, received_signature):
        raise HTTPException(status_code=401, detail="invalid chatwoot signature")

async def process_saved_chatwoot_raw_event(raw_event_id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
    """Processa um raw_event Chatwoot já persistido.

    v4928.1.5.25: o Chatwoot email webhook coloca o sentido da mensagem
    em ``payload.message_type``.  Nos emails recebidos o ``sender.type`` pode
    vir vazio, por isso a ingestão não pode depender de ``sender.type`` do contacto.
    Esta função é partilhada pelo webhook online e pelo script de recovery para
    evitar que raw_events fiquem em ``processed=false`` sem erro visível.
    """
    extracted = extract_chatwoot_event(payload)

    if not extracted["content"]:
        mark_raw_event_processed(
            raw_event_id=raw_event_id,
            ignored=True,
            error="ignored: empty content",
        )
        return {
            "status": "ignored",
            "reason": "empty_content",
            "raw_event_id": raw_event_id,
            "message_type": extracted.get("message_type"),
        }

    if extracted["is_outgoing"]:
        auto_complete_result = auto_complete_task_from_outgoing_message(
            conversation_id=extracted["conversation_id"],
            contact_id=extracted["contact_id"],
            outgoing_message_id=extracted["source_event_id"],
            outgoing_content=extracted["content"],
            sender_name=extracted.get("sender_name"),
            sender_type=extracted.get("sender_type"),
            is_private=bool(extracted.get("is_private")),
        )

        completed = auto_complete_result.get("status") == "auto_completed"

        # Mensagens enviadas manualmente no Chatwoot sem task pendente são
        # normais.  Devem ser ignoradas como informação operacional, não como
        # processing_error, para não poluir System health.
        mark_raw_event_processed(
            raw_event_id=raw_event_id,
            ignored=not completed,
            error=None,
        )

        return {
            "status": "outgoing_processed" if completed else "ignored",
            "reason": auto_complete_result.get("status"),
            "raw_event_id": raw_event_id,
            "conversation_id": extracted["conversation_id"],
            "contact_id": extracted["contact_id"],
            "auto_complete": auto_complete_result,
        }

    current_state = get_state_for_conversation(extracted["conversation_id"])

    analyze_request = AnalyzeRequest(
        last_customer_message=extracted["content"],
        previous_context=build_triage_previous_context(payload, extracted),
        current_state=current_state,
        source="chatwoot",
        conversation_id=extracted["conversation_id"] or raw_event_id,
        contact_id=extracted["contact_id"],
    )

    response = await analyze(analyze_request, raw_event_id=raw_event_id)

    chatwoot_note_result = {
        "status": "skipped",
        "reason": "no conversation_id",
    }

    if extracted["conversation_id"]:
        chatwoot_note_result = await add_private_note(
            conversation_id=extracted["conversation_id"],
            action_result=response.action_result,
        )

    mark_raw_event_processed(
        raw_event_id=raw_event_id,
        action_run_id=response.action_run_id,
        message_id=response.message_id,
        ignored=False,
        error=None,
    )

    return {
        "status": "processed",
        "raw_event_id": raw_event_id,
        "action_run_id": response.action_run_id,
        "message_id": response.message_id,
        "task_id": response.task_id,
        "conversation_id": extracted["conversation_id"],
        "contact_id": extracted["contact_id"],
        "message_type": extracted.get("message_type"),
        "action_decision": response.action_decision.model_dump(),
        "action_result": response.action_result.model_dump(),
        "needs_review": response.needs_review,
        "usage": response.usage.model_dump(),
        "chatwoot_note": chatwoot_note_result,
    }


@router.post("/chatwoot")
async def chatwoot_webhook(request: Request) -> Dict[str, Any]:

    raw_body = await request.body()
    validate_chatwoot_signature(request, raw_body)

    try:
        payload = json.loads(raw_body.decode('utf-8') or '{}')
    except json.JSONDecodeError:
        raise HTTPException(status_code=400, detail='invalid json payload')
    extracted = extract_chatwoot_event(payload)
    if not isinstance(payload, dict) or not payload:
        raise HTTPException(status_code=422, detail="empty chatwoot payload")
    if not extracted.get("conversation_id") and not extracted.get("source_event_id"):
        raise HTTPException(status_code=422, detail="chatwoot payload without conversation/message id")
    if not extracted.get("content"):
        raise HTTPException(status_code=422, detail="chatwoot payload without message content")

    raw_event_info = save_raw_event(
        source_system="chatwoot",
        event_type=extracted["event_type"],
        source_event_id=extracted["source_event_id"],
        conversation_id=extracted["conversation_id"],
        contact_id=extracted["contact_id"],
        payload=payload,
    )
    raw_event_id = raw_event_info["id"]

    if raw_event_info.get("processed"):
        return {
            "status": "duplicate_ignored",
            "raw_event_id": raw_event_id,
            "message_id": raw_event_info.get("message_id"),
            "action_run_id": raw_event_info.get("action_run_id"),
        }

    try:
        return await process_saved_chatwoot_raw_event(raw_event_id, payload)
    except Exception as exc:
        # Não deixar eventos em pending silencioso. O recovery script pode
        # reprocessar com --include-errors depois de corrigida a causa.
        mark_raw_event_error(raw_event_id=raw_event_id, error=f"processing_exception: {type(exc).__name__}: {exc}")
        raise
