"""OpenAI/file_search email reply agent for ClientFlow safe draft mode.

Phase 1 goal: generate structured, editable customer reply drafts from a task,
using the approved BLIF knowledge stored in an OpenAI vector store.  This module
never sends messages; it only returns a suggested draft and metadata that the
operator can review in ClientFlow.
"""
from __future__ import annotations

import json
from typing import Any, Dict, List, Sequence

import httpx

from app.business_knowledge_service import KnowledgeMatch
from app.config import settings
from app.message_cleaner import extract_customer_reply_text
from app.reply_recipient_utils import apply_preferred_greeting, resolve_reply_recipient


class EmailReplyAgentError(Exception):
    """Raised when the external reply agent cannot produce a safe draft."""


AGENT_INTENTS = [
    "pedido_preco",
    "confirmacao_encomenda",
    "pagamento",
    "envio_transitario",
    "instalacao",
    "instalacao_condominio",
    "mobi_e_dpc",
    "pedido_tecnico",
    "suporte_instalacao",
    "balanceador",
    "rfid",
    "sem_internet_historico",
    "garantia",
    "prazo_entrega",
    "reclamacao",
    "desconto",
    "outro",
]


AGENT_SCHEMA: Dict[str, Any] = {
    "type": "object",
    "properties": {
        "resumo_pedido": {
            "type": "string",
            "description": "Resumo curto do que o cliente pediu.",
        },
        "intencao": {
            "type": "string",
            "enum": AGENT_INTENTS,
            "description": (
                "Categoria principal da mensagem. Se o cliente mencionar condomínio e faturação separada, "
                "consumo fora da fatura do condomínio, eletricidade comum, administrador, contador do cliente, "
                "MOBI.E ou DPC, usar mobi_e_dpc. Se perguntar apenas sobre instalação em condomínio, usar instalacao_condominio."
            ),
        },
        "prioridade": {
            "type": "string",
            "enum": ["baixa", "normal", "alta", "urgente"],
        },
        "informacao_usada_da_base": {
            "type": "string",
            "description": "Resumo da informação encontrada na base de conhecimento usada para responder. Não incluir a palavra Fonte nem referências vazias.",
        },
        "informacao_em_falta": {
            "type": "string",
            "description": "Dados que faltam para responder melhor. Usar string vazia se não faltar nada.",
        },
        "precisa_revisao_humana": {"type": "boolean"},
        "motivo_revisao": {
            "type": "string",
            "description": "Motivo da revisão humana. Usar string vazia se não precisar.",
        },
        "resposta_sugerida": {
            "type": "string",
            "description": (
                "Resposta pronta a enviar ao cliente. Deve ser clara, bem formatada, "
                "com parágrafos curtos e listas com travessão quando houver preços, "
                "funcionalidades, prazos, condições ou próximos passos."
            ),
        },
        "nivel_confianca": {
            "type": "string",
            "enum": ["baixo", "medio", "alto"],
        },
    },
    "required": [
        "resumo_pedido",
        "intencao",
        "prioridade",
        "informacao_usada_da_base",
        "informacao_em_falta",
        "precisa_revisao_humana",
        "motivo_revisao",
        "resposta_sugerida",
        "nivel_confianca",
    ],
    "additionalProperties": False,
}


def email_reply_agent_enabled() -> bool:
    return bool(getattr(settings, "clientflow_email_reply_agent_enabled", False))


def email_reply_agent_configured() -> bool:
    return bool(
        str(getattr(settings, "openai_api_key", "") or "").strip()
        and str(getattr(settings, "openai_vector_store_id", "") or "").strip()
    )


def build_agent_prompt() -> str:
    return """És um assistente de atendimento comercial e suporte da Blif.

Contexto da empresa:
- A Blif atua no setor da mobilidade elétrica.
- A Blif fabrica carregadores para veículos elétricos e vende acessórios.
- A Blif não presta serviços diretos de instalação.
- A instalação dos carregadores pode ser feita por qualquer eletricista qualificado.
- A Blif pode dar suporte remoto ao eletricista do cliente, quando necessário.

Objetivo:
- Analisar emails ou mensagens recebidas por clientes.
- Consultar a base de conhecimento antes de responder sobre preços, produtos, acessórios, instalação, prazos, faturação, pagamento, envio, MOBI.E DPC ou condições comerciais.
- Gerar uma resposta pronta a enviar ao cliente, em português de Portugal.

Regras obrigatórias:
- Não inventar preços, prazos, condições, descontos, disponibilidade ou funcionalidades.
- Se a informação não estiver na base de conhecimento, dizer que é necessário confirmar internamente.
- Não prometer descontos ou exceções comerciais.
- Não confirmar encomendas, pagamentos, envios, stock ou condições especiais sem validação humana.
- Não dizer que o email foi enviado.
- Se faltarem dados importantes, pedir apenas a informação em falta.

Preços, IVA e faturação:
- Apresentar preços sempre como “s/IVA”, salvo se o cliente pedir explicitamente valores com IVA.
- Não escrever “+ IVA”; usar “s/IVA”.
- Não calcular valores com IVA, exceto se a taxa de IVA estiver explicitamente definida na base de conhecimento.
- Se o cliente pedir valores com IVA e a taxa não estiver definida na base, apresentar os valores s/IVA e dizer que os valores finais com IVA serão apresentados na proposta/fatura.
- Indicar que a fatura é emitida após confirmação de pagamento quando o tema for pagamento/encomenda.
- Se for necessário emitir fatura, pedir dados de faturação e morada, caso não tenham sido fornecidos.

Instalação:
- Se o cliente pedir instalação, explicar que a Blif não presta instalação direta.
- Indicar que qualquer eletricista qualificado pode instalar o equipamento.
- Indicar que a Blif pode dar suporte remoto ao eletricista, se necessário.
- Em pedidos sobre condomínios, explicar que a instalação é viável e pode ser analisada conforme o caso.

Condomínios e MOBI.E DPC:
- Se o cliente mencionar condomínio e faturação separada, consumo fora da fatura do condomínio, eletricidade comum, administrador, contador do cliente, MOBI.E ou DPC, classificar a intenção como mobi_e_dpc.
- Usar instalacao_condominio apenas quando o cliente perguntar genericamente sobre viabilidade ou instalação em condomínio, sem foco em faturação separada.
- Para pedidos sobre condomínios e faturação separada, explicar MOBI.E DPC de forma simples.
- Não prometer integração DPC/MOBI.E sem validação técnica; pedir os dados técnicos necessários se faltarem.

Envio e transitário:
- Para a maioria dos clientes, não mencionar transitário nem envio gratuito, salvo se o cliente perguntar diretamente.
- Só falar em transitário se o cliente mencionar ilhas, Madeira, Açores, transitário ou envio para fora do continente.
- Para clientes no continente, responder apenas com o prazo normal de entrega quando relevante.
- Se o cliente mencionar Madeira, Açores, ilhas ou transitário, explicar que a Blif pode enviar para transitário indicado pelo cliente no continente.
- Em casos de ilhas/transitário, pedir contactos e morada do transitário.
- Não apresentar envio gratuito como benefício geral.
- Marcar revisão humana se houver condição especial, exceção, dúvida de elegibilidade ou informação incompleta para confirmar o envio.

Regras comerciais e revisão humana:
- Se o cliente pedir desconto, marcar revisão humana.
- Se o cliente pedir cancelamento, reclamação, devolução, reembolso ou exceção comercial, marcar revisão humana.
- Se o cliente pedir dados de pagamento sem encomenda clara, marcar revisão humana.
- Se o cliente pedir confirmação de encomenda, envio, stock ou condições especiais, marcar revisão humana.
- Se houver conflito, reclamação ou insatisfação, responder com empatia e marcar revisão humana.

Regras técnicas da Blif:
- Em edifícios com vários pisos, betão, garagem subterrânea, estrutura densa ou grande distância, recomendar balanceador com fios Modbus/RS485.
- Se o cliente mencionar 50–60 metros de cabo, grande distância ou cablagem entre pisos, não inventar cabo extra incluído; explicar que a cablagem/instalação deve ser validada e orçamentada pelo eletricista/instalador.
- Para perguntas sobre RFID, explicar que o leitor RFID permite cartões ilimitados e separação de consumos/histórico por utilizador.
- Para perguntas sobre funcionamento sem Internet, explicar que o histórico pode ser consultado localmente por Wi-Fi/Bluetooth e que se perde em caso de reset de fábrica.
- Para pedidos de suporte ao eletricista, explicar que a Blif pode dar apoio técnico remoto.
- Para pedidos de ficha técnica, datasheet, manual, dimensões ou documentação técnica, preferir URL público quando esse URL existir no contexto/base. Se o cliente pedir explicitamente anexo, mencionar anexo e incluir também o URL público quando disponível. Nunca inventar URLs.
- Para pedidos sobre ficha/tomada no carregador, adaptadores ou ligação a tomada, responder apenas com informação tecnicamente validada no contexto/base. Não recomendar adaptações inseguras; quando aplicável, indicar que deve ser validado por eletricista qualificado.

Correções de rascunho pelo operador:
- Quando o operador pedir para corrigir ou melhorar um rascunho, preservar a intenção do texto atual e aplicar apenas a instrução dada.
- Se o operador pedir para adicionar preços, usar apenas preços explícitos no contexto/base; se não existirem, indicar que é necessário confirmar.
- Se o operador pedir para adicionar ficha técnica ou documentos, usar apenas URLs/anexos existentes no contexto/base; não inventar.

Tom e estilo da resposta:
- Usar português de Portugal.
- Ser profissional, claro, simpático, objetivo e natural.
- Escrever como uma pessoa da equipa escreveria.
- Preferir cumprimento formal quando houver nome completo da pessoa que escreveu a última mensagem: “Bom dia Sr./Sra. Nome Completo,”, “Boa tarde Sr./Sra. Nome Completo,” ou equivalente.
- A pessoa que assina a última mensagem tem prioridade sobre o nome da empresa, cliente fiscal ou oportunidade.
- Nunca cumprimentar com designações de empresa, LDA, SA, UNIPESSOAL ou nomes fiscais; usar essas designações apenas como contexto.
- Se o email do cliente começar por “Boa tarde”, responder com “Boa tarde”; se começar por “Bom dia”, responder com “Bom dia”.
- Se o nome completo não estiver disponível, usar apenas “Bom dia,”, “Boa tarde,” ou “Olá,”.
- Só usar “Sr.”, “Sra.” ou o nome completo se essa informação estiver disponível no email ou no CRM; não inventar nomes.
- Se houver referência de outra pessoa no email ou no contexto, agradecer essa referência de forma natural; não inventar referências.
- Evitar respostas demasiado longas quando o pedido é simples.
- A resposta sugerida deve ser curta, natural e pronta a enviar.
- Terminar com uma frase curta, sem fórmulas excessivamente formais.
- Em respostas comerciais, propostas, encomendas ou pedidos de preço, terminar com:
  Com os melhores cumprimentos,
  Sérgio Araújo
  Blif

Formato visual e legibilidade:
- Escrever respostas fáceis de ler, com parágrafos curtos.
- Separar claramente agradecimento, proposta/equipamento, funcionalidades, prazo de entrega e próximo passo.
- Usar listas com travessão “–” quando houver preços, funcionalidades, condições, prazos ou próximos passos.
- Evitar blocos grandes de texto.
- Em pedidos de preço ou proposta, usar estrutura de proposta comercial.
- Se o cliente fizer follow-up sobre morada/endereço, não responder genericamente “estamos a analisar”. Confirmar a receção da morada, resumir a morada se estiver na mensagem e indicar o próximo passo concreto.
- Não usar linguagem demasiado automática.

Estrutura para pedidos de preço/proposta:
- Quando a intenção for pedido_preco, proposta ou confirmacao_encomenda, seguir esta estrutura quando aplicável:
  1. Cumprimento formal personalizado, se houver nome completo.
  2. Agradecimento pelo contacto.
  3. Referência à proposta/equipamento solicitado.
  4. Linha do produto com preço s/IVA.
  5. Lista curta de funcionalidades incluídas.
  6. Prazo de entrega, se existir na base.
  7. Próximo passo: ficha técnica, proposta formal, dados de faturação ou confirmação.
  8. Assinatura:
     Com os melhores cumprimentos,
     Sérgio Araújo
     Blif

Regras para os campos JSON:
- Devolver sempre JSON válido, sem texto fora do JSON.
- No campo informacao_usada_da_base, resumir apenas a informação encontrada na base de conhecimento.
- No campo informacao_usada_da_base, não escrever “Fonte:” nem referências vazias.
- No campo informacao_em_falta, usar string vazia se não faltar informação relevante.
- No campo motivo_revisao, usar string vazia se não precisar de revisão humana.
"""


def _trim(text: Any, max_chars: int = 5000) -> str:
    value = str(text or "").strip()
    if len(value) <= max_chars:
        return value
    return value[: max_chars - 1] + "…"


def _document_context(selected_documents: Sequence[Dict[str, Any]]) -> List[Dict[str, Any]]:
    docs: List[Dict[str, Any]] = []
    for doc in selected_documents or []:
        kind = str(doc.get("document_kind") or "").lower()
        number = str(doc.get("document_number") or doc.get("external_id") or "")
        operational_role = "proforma_orc" if kind == "quotation" and number.upper().startswith("ORC") else kind
        docs.append({
            "id": doc.get("id"),
            "kind": kind,
            "number": number,
            "amount": doc.get("total_amount") or doc.get("amount"),
            "currency": doc.get("currency") or "EUR",
            "operational_role": operational_role,
        })
    return docs


def build_agent_input(
    *,
    task: Dict[str, Any],
    knowledge: KnowledgeMatch,
    selected_documents: Sequence[Dict[str, Any]],
) -> str:
    customer_message = extract_customer_reply_text(task)
    recipient = resolve_reply_recipient(task, cleaned_customer_message=customer_message)
    context = {
        "destinatario_resposta": {
            "nome_pessoa": recipient.get("person_name") or "",
            "nome_empresa": recipient.get("company_name") or "",
            "cumprimento_preferido": recipient.get("preferred_greeting") or "",
            "origem_cumprimento": recipient.get("greeting_source") or "",
            "regra": "Usar nome_pessoa na saudação quando existir. Não usar nome_empresa/nome_fiscal como destinatário da saudação.",
        },
        "cliente": {
            "nome_contacto": task.get("customer_name") or "",
            "nome_fiscal": task.get("linked_customer_name") or "",
            "email": task.get("customer_email") or task.get("linked_customer_email") or "",
            "nif": task.get("linked_customer_tax_id") or "",
        },
        "tarefa": {
            "id": task.get("id") or "",
            "action_code": task.get("action_code") or "",
            "route": task.get("route") or "",
            "subject": task.get("message_subject") or task.get("subject") or "",
        },
        "objetivo_operador": task.get("communication_objective") or {},
        "instrucao_operador": task.get("operator_instruction") or "",
        "oportunidade": {
            "id": task.get("opportunity_id") or "",
            "stage": task.get("opportunity_stage") or task.get("status") or "",
        },
        "documentos_selecionados": _document_context(selected_documents),
        "historico_recente_conversa": task.get("recent_conversation_context") or task.get("conversation_history") or task.get("previous_context") or [],
        "conhecimento_deterministico_clientflow": knowledge.to_prompt_context(),
    }
    return (
        "Mensagem recebida do cliente:\n"
        f"{_trim(customer_message, 6000)}\n\n"
        "Contexto ClientFlow para apoiar a resposta:\n"
        f"{json.dumps(context, ensure_ascii=False, indent=2, default=str)}\n\n"
        "Tarefa: gera uma resposta pronta a enviar ao cliente, obedecendo ao objetivo_operador quando existir. "
        "Não alteres o objetivo operacional, não inventes anexos e não contradigas documentos selecionados. "
        "Regra BLIF/ClientFlow: quando action_code/objetivo for SEND_PROFORMA, a pró-forma operacional é o orçamento Jasmin ORC.* selecionado; "
        "diz que esse orçamento/proforma segue em anexo e não escrevas que ainda vais enviar proposta formal."
    )


def _extract_output_text(data: Dict[str, Any]) -> str:
    direct = str(data.get("output_text") or "").strip()
    if direct:
        return direct
    parts: List[str] = []
    for item in data.get("output") or []:
        if not isinstance(item, dict):
            continue
        for content in item.get("content") or []:
            if not isinstance(content, dict):
                continue
            if content.get("type") in {"output_text", "text"}:
                text = str(content.get("text") or "").strip()
                if text:
                    parts.append(text)
    return "\n".join(parts).strip()


def _normalize_agent_result(result: Dict[str, Any]) -> Dict[str, Any]:
    normalized = {
        "resumo_pedido": str(result.get("resumo_pedido") or "").strip(),
        "intencao": str(result.get("intencao") or "outro").strip() or "outro",
        "prioridade": str(result.get("prioridade") or "normal").strip() or "normal",
        "informacao_usada_da_base": str(result.get("informacao_usada_da_base") or "").strip(),
        "informacao_em_falta": str(result.get("informacao_em_falta") or "").strip(),
        "precisa_revisao_humana": bool(result.get("precisa_revisao_humana")),
        "motivo_revisao": str(result.get("motivo_revisao") or "").strip(),
        "resposta_sugerida": str(result.get("resposta_sugerida") or "").strip(),
        "nivel_confianca": str(result.get("nivel_confianca") or "medio").strip() or "medio",
    }
    if normalized["intencao"] not in AGENT_INTENTS:
        normalized["intencao"] = "outro"
    if normalized["prioridade"] not in {"baixa", "normal", "alta", "urgente"}:
        normalized["prioridade"] = "normal"
    if normalized["nivel_confianca"] not in {"baixo", "medio", "alto"}:
        normalized["nivel_confianca"] = "medio"
    return normalized


FOLLOW_UP_SCHEMA: Dict[str, Any] = {
    "type": "object",
    "properties": {
        "objetivo_follow_up": {
            "type": "string",
            "description": "Objetivo curto do follow-up: proposta, pró-forma, pagamento, interesse ou genérico.",
        },
        "informacao_usada": {
            "type": "string",
            "description": "Dados concretos usados do contexto: cliente, documento, produto ou valor. Usar string vazia se não foram usados.",
        },
        "informacao_em_falta": {
            "type": "string",
            "description": "Informação que faltou para personalizar melhor. Usar string vazia se não faltar nada crítico.",
        },
        "precisa_revisao_humana": {"type": "boolean"},
        "motivo_revisao": {
            "type": "string",
            "description": "Motivo da revisão humana. Usar string vazia se não precisar.",
        },
        "resposta_sugerida": {
            "type": "string",
            "description": "Mensagem curta de follow-up pronta a editar/enviar ao cliente.",
        },
        "nivel_confianca": {
            "type": "string",
            "enum": ["baixo", "medio", "alto"],
        },
    },
    "required": [
        "objetivo_follow_up",
        "informacao_usada",
        "informacao_em_falta",
        "precisa_revisao_humana",
        "motivo_revisao",
        "resposta_sugerida",
        "nivel_confianca",
    ],
    "additionalProperties": False,
}


def build_follow_up_agent_prompt() -> str:
    return """És um assistente comercial da Blif especializado em follow-ups.

Contexto da empresa:
- A Blif atua no setor da mobilidade elétrica.
- A Blif fabrica/fornece carregadores para veículos elétricos e acessórios.
- A Blif não presta instalação direta; a instalação pode ser feita por eletricista qualificado.
- A Blif pode dar suporte remoto ao eletricista quando necessário.

Objetivo único:
- Gerar uma mensagem curta de follow-up para a Blif enviar ao cliente.
- A mensagem deve ser personalizada com dados disponíveis do cliente, oportunidade, documento, valor, produto e histórico recente.
- A mensagem nunca é enviada automaticamente; será revista pelo operador.

Regras obrigatórias para FOLLOW_UP_PAYMENT:
- O objetivo é confirmar se o cliente recebeu os dados/documento de pagamento e se precisa de alguma informação para avançar.
- Não escrever que a Blif vai confirmar internamente se o pagamento foi recebido.
- Não afirmar que o pagamento foi recebido.
- Não pedir comprovativo salvo se o contexto disser explicitamente que esse é o objetivo.
- Não transformar o follow-up numa resposta operacional interna.
- No ClientFlow/BLIF, um orçamento Jasmin ORC.* pode funcionar como pró-forma operacional.
- Se o documento selecionado for ORC.* e o objetivo/follow-up for pró-forma ou pagamento, podes chamar-lhe “orçamento/proforma” ou “pró-forma”.
- Não inventar uma pró-forma separada se o documento selecionado for o ORC.*.

Regras obrigatórias para FOLLOW_UP_QUOTE:
- Confirmar se o cliente recebeu o orçamento/proposta e se ficou com dúvidas.
- Pode perguntar se pretende avançar para emissão de pró-forma quando isso fizer sentido.

Regras obrigatórias para FOLLOW_UP_PROFORMA:
- Confirmar se recebeu a pró-forma/dados de pagamento e se está tudo claro para avançar.

Regras obrigatórias para FOLLOW_UP_CUSTOMER_REVIEW:
- Confirmar se mantém interesse e se precisa de informação adicional.

Regras gerais:
- Usar português de Portugal.
- Ser profissional, natural, curto e humano.
- Não inventar preços, documentos, anexos, prazos, stock, descontos, URLs, condições especiais ou estado de pagamento.
- Usar apenas dados explícitos no contexto/documentos/base de conhecimento.
- Não mencionar notas internas, backfill, metadata, task, sistema, IA, ClientFlow ou classificação.
- Não mencionar informação fiscal incompleta, a menos que a mensagem tenha como objetivo pedir esses dados.
- Não usar tom agressivo, pressão excessiva ou urgência falsa.
- Se houver cumprimento_preferido com confiança média/alta, começar exatamente por esse cumprimento.
- Se o nome vier de email pessoal com confiança alta, pode usar primeiro nome formal, por exemplo “Bom dia Sr. Nuno,”.
- Se não houver nome de pessoa seguro, usar cumprimento neutro: “Bom dia,” ou “Olá,”.
- Se houver documento/valor explícito, pode mencionar de forma simples; se não houver, escrever mensagem neutra.
- Não assinar com nome de operador se não existir operador confirmado no contexto. Preferir “Obrigado.” ou “Com os melhores cumprimentos,\nBlif”.

Formato da resposta:
- Devolver sempre JSON válido, sem texto fora do JSON.
- O campo resposta_sugerida deve conter só a mensagem para o cliente, pronta a editar/enviar.
"""


def _follow_up_objective(action_code: str) -> str:
    code = str(action_code or "").strip().upper()
    return {
        "FOLLOW_UP_PAYMENT": "Confirmar se recebeu a pró-forma/dados de pagamento e se precisa de informação adicional para avançar.",
        "FOLLOW_UP_PROFORMA": "Confirmar se recebeu a pró-forma/dados de pagamento e se está tudo claro para avançar.",
        "FOLLOW_UP_QUOTE": "Confirmar se recebeu o orçamento/proposta e se ficou com dúvidas.",
        "FOLLOW_UP_CUSTOMER_REVIEW": "Confirmar se mantém interesse e se precisa de informação adicional.",
        "FOLLOW_UP_GENERIC": "Dar seguimento ao processo e confirmar se precisa de informação adicional.",
    }.get(code, "Dar seguimento ao processo e confirmar se precisa de informação adicional.")


def build_follow_up_agent_input(
    *,
    task: Dict[str, Any],
    knowledge: KnowledgeMatch,
    selected_documents: Sequence[Dict[str, Any]],
    baseline_message: str,
) -> str:
    customer_message = extract_customer_reply_text(task)
    recipient = resolve_reply_recipient(task, cleaned_customer_message=customer_message)
    action_code = str(task.get("action_code") or "").strip().upper()
    context = {
        "modo": "follow_up_personalizado",
        "objetivo_obrigatorio": _follow_up_objective(action_code),
        "destinatario_resposta": {
            "nome_pessoa": recipient.get("person_name") or "",
            "primeiro_nome": recipient.get("person_first_name") or "",
            "confianca_nome_pessoa": recipient.get("person_confidence") or "",
            "origem_nome_pessoa": recipient.get("greeting_source") or "",
            "email": recipient.get("email") or "",
            "nome_empresa": recipient.get("company_name") or "",
            "cumprimento_preferido": recipient.get("preferred_greeting") or "",
            "regra": "Começar exatamente por cumprimento_preferido quando existir. Se origem_nome_pessoa=email e confianca_nome_pessoa for alto, é permitido usar saudação formal com primeiro nome. Não usar nome fiscal/empresa como destinatário da saudação.",
        },
        "cliente": {
            "nome_contacto": task.get("customer_name") or "",
            "nome_fiscal": task.get("linked_customer_name") or "",
            "email": task.get("customer_email") or task.get("linked_customer_email") or "",
            "nif": task.get("linked_customer_tax_id") or "",
        },
        "tarefa": {
            "id": task.get("id") or "",
            "action_code": action_code,
            "route": task.get("route") or "",
            "subject": task.get("message_subject") or task.get("subject") or "",
            "nota_interna_nao_copiar": _trim(task.get("note") or task.get("message_body") or "", 1200),
        },
        "oportunidade": {
            "id": task.get("opportunity_id") or "",
            "stage": task.get("opportunity_stage") or task.get("status") or "",
            "produto_interesse": task.get("product_interest") or task.get("opportunity_product_interest") or "",
            "valor": task.get("value_amount") or task.get("opportunity_value_amount") or "",
            "moeda": task.get("currency") or task.get("opportunity_currency") or "EUR",
        },
        "documentos_selecionados": _document_context(selected_documents),
        "historico_recente_conversa": task.get("recent_conversation_context") or task.get("conversation_history") or task.get("previous_context") or [],
        "mensagem_base_segura": baseline_message,
        "conhecimento_deterministico_clientflow": knowledge.to_prompt_context(),
        "restricoes_criticas": [
            "Preservar o objetivo_obrigatorio; não mudar para outra ação operacional.",
            "Para FOLLOW_UP_PAYMENT, não dizer que vamos confirmar internamente se o pagamento foi recebido.",
            "Não dizer que o pagamento foi recebido.",
            "Não incluir a nota interna nem texto de backfill.",
            "Não inventar documento, preço, URL, stock, prazo, assinatura ou nome de pessoa.",
        ],
    }
    return (
        "Gera um rascunho personalizado de follow-up ao cliente. "
        "Usa a mensagem_base_segura como base e melhora apenas com dados concretos do contexto.\n\n"
        f"{json.dumps(context, ensure_ascii=False, indent=2, default=str)}"
    )


def _normalize_follow_up_agent_result(result: Dict[str, Any]) -> Dict[str, Any]:
    normalized = {
        "objetivo_follow_up": str(result.get("objetivo_follow_up") or "").strip(),
        "informacao_usada": str(result.get("informacao_usada") or "").strip(),
        "informacao_em_falta": str(result.get("informacao_em_falta") or "").strip(),
        "precisa_revisao_humana": bool(result.get("precisa_revisao_humana")),
        "motivo_revisao": str(result.get("motivo_revisao") or "").strip(),
        "resposta_sugerida": str(result.get("resposta_sugerida") or "").strip(),
        "nivel_confianca": str(result.get("nivel_confianca") or "medio").strip() or "medio",
    }
    if normalized["nivel_confianca"] not in {"baixo", "medio", "alto"}:
        normalized["nivel_confianca"] = "medio"
    return normalized


REVISION_SCHEMA: Dict[str, Any] = {
    "type": "object",
    "properties": {
        "resposta_revisada": {
            "type": "string",
            "description": "Nova versão completa do rascunho, pronta a editar/enviar.",
        },
        "alteracoes_aplicadas": {
            "type": "string",
            "description": "Resumo curto do que foi alterado.",
        },
        "avisos": {
            "type": "string",
            "description": "Avisos para o operador. Usar string vazia se não houver.",
        },
        "precisa_revisao_humana": {"type": "boolean"},
        "motivo_revisao": {
            "type": "string",
            "description": "Motivo da revisão humana. Usar string vazia se não precisar.",
        },
        "informacao_em_falta": {
            "type": "string",
            "description": "Dados que faltam para aplicar a correção com segurança. Usar string vazia se não faltar nada.",
        },
    },
    "required": [
        "resposta_revisada",
        "alteracoes_aplicadas",
        "avisos",
        "precisa_revisao_humana",
        "motivo_revisao",
        "informacao_em_falta",
    ],
    "additionalProperties": False,
}


def build_revision_agent_input(
    *,
    task: Dict[str, Any],
    knowledge: KnowledgeMatch,
    selected_documents: Sequence[Dict[str, Any]],
    current_body: str,
    instruction: str,
) -> str:
    customer_message = extract_customer_reply_text(task)
    recipient = resolve_reply_recipient(task, cleaned_customer_message=customer_message)
    context = {
        "instrucao_do_operador": str(instruction or "").strip(),
        "rascunho_atual": _trim(current_body, 8000),
        "mensagem_atual_cliente": _trim(customer_message, 6000),
        "historico_recente_conversa": task.get("recent_conversation_context") or task.get("conversation_history") or task.get("previous_context") or [],
        "destinatario_resposta": {
            "nome_pessoa": recipient.get("person_name") or "",
            "nome_empresa": recipient.get("company_name") or "",
            "cumprimento_preferido": recipient.get("preferred_greeting") or "",
            "regra": "Preservar/usar nome_pessoa na saudação quando existir. Não usar nome_empresa/nome_fiscal como destinatário da saudação.",
        },
        "cliente": {
            "nome_contacto": task.get("customer_name") or "",
            "nome_fiscal": task.get("linked_customer_name") or "",
            "email": task.get("customer_email") or task.get("linked_customer_email") or "",
            "nif": task.get("linked_customer_tax_id") or "",
        },
        "tarefa": {
            "id": task.get("id") or "",
            "action_code": task.get("action_code") or "",
            "route": task.get("route") or "",
            "subject": task.get("message_subject") or task.get("subject") or "",
        },
        "objetivo_operador": task.get("communication_objective") or {},
        "instrucao_operador": task.get("operator_instruction") or "",
        "oportunidade": {
            "id": task.get("opportunity_id") or "",
            "stage": task.get("opportunity_stage") or task.get("status") or "",
        },
        "documentos_selecionados": _document_context(selected_documents),
        "conhecimento_deterministico_clientflow": knowledge.to_prompt_context(),
        "regras_de_correcao": [
            "Aplicar a instrução do operador sem inventar preços, URLs, anexos, stock, descontos ou condições comerciais.",
            "Se a instrução pedir preço, usar apenas preço existente no contexto/base. Se não existir, indicar que falta confirmação.",
            "Se a instrução pedir ficha técnica/URL/anexo, usar apenas documentos/URLs existentes no contexto/base.",
            "Considerar as últimas 2-3 mensagens da conversa para evitar repetir ou contradizer informação já trocada.",
            "Manter português de Portugal e resposta pronta a enviar.",
        ],
    }
    return (
        "Corrige o rascunho atual de resposta ao cliente com base na instrução do operador.\n"
        "Devolve uma versão completa do rascunho, não apenas o trecho alterado.\n\n"
        f"{json.dumps(context, ensure_ascii=False, indent=2, default=str)}"
    )


def revise_email_reply_agent(
    *,
    task: Dict[str, Any],
    knowledge: KnowledgeMatch,
    selected_documents: Sequence[Dict[str, Any]],
    current_body: str,
    instruction: str,
) -> Dict[str, Any]:
    """Revise an existing editable draft using OpenAI/file_search and operator instructions."""
    if not email_reply_agent_enabled():
        raise EmailReplyAgentError("CLIENTFLOW_EMAIL_REPLY_AGENT_ENABLED=false")
    if not email_reply_agent_configured():
        raise EmailReplyAgentError("OPENAI_API_KEY ou OPENAI_VECTOR_STORE_ID não configurados.")
    instruction = str(instruction or "").strip()
    if not instruction:
        raise EmailReplyAgentError("Instrução de correção vazia.")
    current_body = str(current_body or "").strip()
    if not current_body:
        raise EmailReplyAgentError("Rascunho atual vazio.")

    api_key = str(getattr(settings, "openai_api_key", "") or "").strip()
    vector_store_id = str(getattr(settings, "openai_vector_store_id", "") or "").strip()
    model = str(getattr(settings, "clientflow_email_reply_agent_model", "") or "").strip() or "gpt-4.1-mini"
    timeout = float(getattr(settings, "clientflow_email_reply_agent_timeout_seconds", 30) or 30)
    max_results = int(getattr(settings, "clientflow_email_reply_agent_max_results", 6) or 6)
    url = str(getattr(settings, "openai_responses_url", "") or "https://api.openai.com/v1/responses").strip()

    payload: Dict[str, Any] = {
        "model": model,
        "instructions": build_agent_prompt(),
        "input": build_revision_agent_input(
            task=task,
            knowledge=knowledge,
            selected_documents=selected_documents,
            current_body=current_body,
            instruction=instruction,
        ),
        "tools": [
            {
                "type": "file_search",
                "vector_store_ids": [vector_store_id],
                "max_num_results": max_results,
            }
        ],
        "text": {
            "format": {
                "type": "json_schema",
                "name": "clientflow_blif_email_reply_revision",
                "schema": REVISION_SCHEMA,
                "strict": True,
            }
        },
    }
    headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
    try:
        with httpx.Client(timeout=timeout) as client:
            response = client.post(url, headers=headers, json=payload)
        if response.status_code >= 400:
            raise EmailReplyAgentError(f"OpenAI HTTP {response.status_code}: {response.text[:500]}")
        data = response.json()
        output_text = _extract_output_text(data)
        if not output_text:
            raise EmailReplyAgentError("OpenAI não devolveu output_text.")
        parsed = json.loads(output_text)
        if not isinstance(parsed, dict):
            raise ValueError("JSON gerado não é objeto.")
        revised = str(parsed.get("resposta_revisada") or "").strip()
        if not revised:
            raise EmailReplyAgentError("OpenAI não devolveu resposta_revisada.")
        recipient = resolve_reply_recipient(task, cleaned_customer_message=extract_customer_reply_text(task))
        parsed["resposta_revisada"] = apply_preferred_greeting(revised, recipient.get("preferred_greeting") or "")
        parsed["metadata"] = {
            "enabled": True,
            "used": True,
            "status": "used",
            "provider": "openai_responses_file_search",
            "mode": "draft_revision",
            "model": model,
            "vector_store_id": vector_store_id,
            "prompt_version": str(getattr(settings, "clientflow_email_reply_agent_prompt_version", "") or "blif-email-agent-phase1-readability-ui-formal-20260613"),
            "response_id": data.get("id") or "",
            "operator_instruction": instruction,
        }
        return parsed
    except EmailReplyAgentError:
        raise
    except Exception as exc:
        raise EmailReplyAgentError(str(exc)) from exc


def generate_email_reply_agent(
    *,
    task: Dict[str, Any],
    knowledge: KnowledgeMatch,
    selected_documents: Sequence[Dict[str, Any]],
) -> Dict[str, Any]:
    """Call OpenAI Responses API and return a normalized agent result.

    The returned object is safe metadata + a suggested message body.  It never
    sends a message and never mutates ClientFlow state directly.
    """
    if not email_reply_agent_enabled():
        raise EmailReplyAgentError("CLIENTFLOW_EMAIL_REPLY_AGENT_ENABLED=false")
    if not email_reply_agent_configured():
        raise EmailReplyAgentError("OPENAI_API_KEY ou OPENAI_VECTOR_STORE_ID não configurados.")

    api_key = str(getattr(settings, "openai_api_key", "") or "").strip()
    vector_store_id = str(getattr(settings, "openai_vector_store_id", "") or "").strip()
    model = str(getattr(settings, "clientflow_email_reply_agent_model", "") or "").strip() or "gpt-4.1-mini"
    timeout = float(getattr(settings, "clientflow_email_reply_agent_timeout_seconds", 30) or 30)
    max_results = int(getattr(settings, "clientflow_email_reply_agent_max_results", 6) or 6)
    url = str(getattr(settings, "openai_responses_url", "") or "https://api.openai.com/v1/responses").strip()

    payload: Dict[str, Any] = {
        "model": model,
        "instructions": build_agent_prompt(),
        "input": build_agent_input(task=task, knowledge=knowledge, selected_documents=selected_documents),
        "tools": [
            {
                "type": "file_search",
                "vector_store_ids": [vector_store_id],
                "max_num_results": max_results,
            }
        ],
        "text": {
            "format": {
                "type": "json_schema",
                "name": "clientflow_blif_email_reply_agent",
                "schema": AGENT_SCHEMA,
                "strict": True,
            }
        },
    }
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    }

    try:
        with httpx.Client(timeout=timeout) as client:
            response = client.post(url, headers=headers, json=payload)
        if response.status_code >= 400:
            raise EmailReplyAgentError(f"OpenAI HTTP {response.status_code}: {response.text[:500]}")
        data = response.json()
        output_text = _extract_output_text(data)
        if not output_text:
            raise EmailReplyAgentError("OpenAI não devolveu output_text.")
        parsed = json.loads(output_text)
        if not isinstance(parsed, dict):
            raise ValueError("JSON gerado não é objeto.")
        normalized = _normalize_agent_result(parsed)
        recipient = resolve_reply_recipient(task, cleaned_customer_message=extract_customer_reply_text(task))
        normalized["resposta_sugerida"] = apply_preferred_greeting(
            normalized.get("resposta_sugerida") or "",
            recipient.get("preferred_greeting") or "",
        )
        normalized["metadata"] = {
            "enabled": True,
            "used": True,
            "status": "used",
            "provider": "openai_responses_file_search",
            "model": model,
            "vector_store_id": vector_store_id,
            "prompt_version": str(getattr(settings, "clientflow_email_reply_agent_prompt_version", "") or "blif-email-agent-phase1-readability-ui-formal-20260613"),
            "response_id": data.get("id") or "",
            "recipient_person_name": recipient.get("person_name") or "",
            "recipient_company_name": recipient.get("company_name") or "",
            "preferred_greeting": recipient.get("preferred_greeting") or "",
            "greeting_source": recipient.get("greeting_source") or "",
            "intencao": normalized["intencao"],
            "prioridade": normalized["prioridade"],
            "precisa_revisao_humana": normalized["precisa_revisao_humana"],
            "motivo_revisao": normalized["motivo_revisao"],
            "nivel_confianca": normalized["nivel_confianca"],
            "informacao_usada_da_base": normalized["informacao_usada_da_base"],
            "informacao_em_falta": normalized["informacao_em_falta"],
        }
        return normalized
    except EmailReplyAgentError:
        raise
    except Exception as exc:
        raise EmailReplyAgentError(str(exc)) from exc



def generate_follow_up_draft_agent(
    *,
    task: Dict[str, Any],
    knowledge: KnowledgeMatch,
    selected_documents: Sequence[Dict[str, Any]],
    baseline_message: str,
) -> Dict[str, Any]:
    """Generate a dedicated OpenAI follow-up draft.

    This intentionally does not reuse the generic email reply prompt because a
    follow-up is not a customer support answer.  The agent receives the safe
    template as baseline and may only personalize it with explicit context.
    """
    if not email_reply_agent_enabled():
        raise EmailReplyAgentError("CLIENTFLOW_EMAIL_REPLY_AGENT_ENABLED=false")
    if not email_reply_agent_configured():
        raise EmailReplyAgentError("OPENAI_API_KEY ou OPENAI_VECTOR_STORE_ID não configurados.")

    api_key = str(getattr(settings, "openai_api_key", "") or "").strip()
    vector_store_id = str(getattr(settings, "openai_vector_store_id", "") or "").strip()
    model = str(getattr(settings, "clientflow_email_reply_agent_model", "") or "").strip() or "gpt-4.1-mini"
    timeout = float(getattr(settings, "clientflow_email_reply_agent_timeout_seconds", 30) or 30)
    max_results = int(getattr(settings, "clientflow_email_reply_agent_max_results", 6) or 6)
    url = str(getattr(settings, "openai_responses_url", "") or "https://api.openai.com/v1/responses").strip()

    payload: Dict[str, Any] = {
        "model": model,
        "instructions": build_follow_up_agent_prompt(),
        "input": build_follow_up_agent_input(
            task=task,
            knowledge=knowledge,
            selected_documents=selected_documents,
            baseline_message=baseline_message,
        ),
        "tools": [
            {
                "type": "file_search",
                "vector_store_ids": [vector_store_id],
                "max_num_results": max_results,
            }
        ],
        "text": {
            "format": {
                "type": "json_schema",
                "name": "clientflow_blif_follow_up_draft_agent",
                "schema": FOLLOW_UP_SCHEMA,
                "strict": True,
            }
        },
    }
    headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}

    try:
        with httpx.Client(timeout=timeout) as client:
            response = client.post(url, headers=headers, json=payload)
        if response.status_code >= 400:
            raise EmailReplyAgentError(f"OpenAI HTTP {response.status_code}: {response.text[:500]}")
        data = response.json()
        output_text = _extract_output_text(data)
        if not output_text:
            raise EmailReplyAgentError("OpenAI não devolveu output_text.")
        parsed = json.loads(output_text)
        if not isinstance(parsed, dict):
            raise ValueError("JSON gerado não é objeto.")
        normalized = _normalize_follow_up_agent_result(parsed)
        if not normalized["resposta_sugerida"]:
            raise EmailReplyAgentError("OpenAI não devolveu resposta_sugerida.")

        recipient = resolve_reply_recipient(task, cleaned_customer_message=extract_customer_reply_text(task))
        normalized["resposta_sugerida"] = apply_preferred_greeting(
            normalized.get("resposta_sugerida") or "",
            recipient.get("preferred_greeting") or "",
        )
        normalized["metadata"] = {
            "enabled": True,
            "used": True,
            "status": "used",
            "provider": "openai_responses_file_search",
            "mode": "follow_up_draft",
            "model": model,
            "vector_store_id": vector_store_id,
            "prompt_version": "blif-follow-up-draft-v4928-1-5-24-contact-person",
            "response_id": data.get("id") or "",
            "action_code": task.get("action_code") or "",
            "objetivo_follow_up": normalized["objetivo_follow_up"],
            "informacao_usada": normalized["informacao_usada"],
            "informacao_em_falta": normalized["informacao_em_falta"],
            "precisa_revisao_humana": normalized["precisa_revisao_humana"],
            "motivo_revisao": normalized["motivo_revisao"],
            "nivel_confianca": normalized["nivel_confianca"],
            "recipient_person_name": recipient.get("person_name") or "",
            "recipient_company_name": recipient.get("company_name") or "",
            "preferred_greeting": recipient.get("preferred_greeting") or "",
            "recipient_first_name": recipient.get("person_first_name") or "",
            "recipient_name_confidence": recipient.get("person_confidence") or "",
            "greeting_source": recipient.get("greeting_source") or "",
        }
        return normalized
    except EmailReplyAgentError:
        raise
    except Exception as exc:
        raise EmailReplyAgentError(str(exc)) from exc


def disabled_agent_state(error: str = "") -> Dict[str, Any]:
    return {
        "enabled": email_reply_agent_enabled(),
        "used": False,
        "status": "disabled" if not email_reply_agent_enabled() else "fallback",
        "provider": "openai_responses_file_search",
        "error": error,
    }
