"""
gofind.py
=========
Integração com a GoFind API — envio de Notas Fiscais via XML NF-e.

Fluxo (método SEND):
    1. Consulta TBCLIENTEXNOTAS filtrando por empresa/filial
    2. Para cada registro, lê o XML da NF-e indicado em NMARQUIVOXML
    3. Computa MD5 do conteúdo do XML e compara com o salvo em TBRELACIONAID
    4. Registros sem alteração são ignorados (controle de reenvio)
    5. Monta o payload GoFind a partir do XML e envia em lotes de até 100 NFs
    6. Grava resultado em TBRELACIONAID e no arquivo de saída para o Cobol

Autenticação:
    Token passado no header X-Api-Key — configurado em TBTOKEN (NMTOKEN1API).

Limitações da API GoFind:
    - Máximo de 100 NFs por requisição
    - Tamanho máximo de 5MB por requisição

Execução:
    python3 ws.py gofind SEND api/subscribe/invoice {banco} {lkgrupo} {empresa} {filial} {saida} {saida_cobol}
"""
import xmltodict

from generic.gn_api     import gn_api
from generic.gn_request import Request


class gofind(gn_api):

    def __init__(self, argv: list):
        self.db           = False
        self.output_count = 0          # Controle de linhas já gravadas no arquivo de saída
        self.suffix       = "_app_pedidos3"

        # Carrega configurações do banco (empresa, filial, tokens etc.)
        self.setup(argv)

        # Token de autenticação da GoFind — salvo em TBTOKEN.NMTOKEN1API
        api_key = self.get_token_data('Token')

        if not api_key:
            self.createLog(401, "Token GoFind não encontrado em TBTOKEN")
            exit(1)

        # O nome {apiName}_request é capturado automaticamente pelo gn_api
        # e exposto como self.api_request para uso interno da classe base
        self.gofind_request = Request(
            base_url = "https://app.gofind.online/",
            headers  = {
                "Content-Type" : "application/json",
                "X-Api-Key"    : api_key,
            }
        )

        # Dispara gn_api.__init__() → bootstrap() → handle_api_map()
        # Para método SEND com methods ["POST","PUT"], chama self.post_put_handler()
        super(gofind, self).__init__()

        print("Fim GoFind")


    # ══════════════════════════════════════════════════════════════════════
    # HANDLER PRINCIPAL — acionado via método SEND pelo gn_api
    # ══════════════════════════════════════════════════════════════════════

    def post_put_handler(self):
        """
        Sobrescreve o post_put_handler padrão do gn_api.

        O fluxo padrão constrói o payload a partir de colunas do banco via map.json.
        Aqui o payload vem do XML da NF-e (NMARQUIVOXML), então o ciclo completo
        — consulta, leitura do XML, envio e gravação no TBRELACIONAID — é controlado
        manualmente, mas enviando uma NF por requisição (igual ao padrão do framework).
        """

        # Consulta apenas NFs ainda não enviadas: LEFT JOIN filtra registros
        # ausentes no TBRELACIONAID (rel.IDAPI IS NULL = nunca enviado para GoFind)
        sql = f"""
            SELECT
                cx.VLNRODOC,
                cx.NMARQUIVOXML
            FROM  TBCLIENTEXNOTAS cx
            LEFT JOIN TBRELACIONAID rel
                ON  rel.IDSOFTDIB = cx.VLNRODOC
                AND rel.NMTABELA  = 'TBCLIENTEXNOTAS'
                AND rel.NMNOMEAPI = '{self.apiName}'
            WHERE
                    cx.CDEMPRESA IN ({int(self.cdempresa)}, 0)
                AND cx.CDFILIAL  IN ({int(self.cdfilial)},  0)
                AND cx.NMARQUIVOXML IS NOT NULL
                AND cx.NMARQUIVOXML <> ''
                AND rel.IDAPI IS NULL
        """

        registros = self.select(sql)

        if not registros:
            self.createLog(200, "Nenhum registro pendente de envio em TBCLIENTEXNOTAS")
            return

        self.createLog(200, f"{len(registros)} NF(s) pendente(s) de envio")

        # Processa e envia uma NF por vez — mesma cadência do post_put_handler padrão
        for reg in registros:
            vlnrodoc = reg.get('VLNRODOC')
            xml_path = (reg.get('NMARQUIVOXML') or '').strip()

            xml_content = self.read_file(xml_path)
            if not xml_content:
                self.createLog(400, f"XML não encontrado: {xml_path} | NF {vlnrodoc}")
                continue

            invoice = self._xml_to_invoice(xml_content, vlnrodoc)
            if not invoice:
                continue

            self._enviar_invoice(vlnrodoc, invoice)


    # ══════════════════════════════════════════════════════════════════════
    # ENVIO E PROCESSAMENTO DA RESPOSTA
    # ══════════════════════════════════════════════════════════════════════

    def _enviar_invoice(self, vlnrodoc, invoice: dict):
        """
        Envia uma única NF para a GoFind e processa a resposta.

        A API retorna sempre um array invoicesResult mesmo para envio unitário,
        portanto lemos o primeiro (e único) elemento do array.

        Sucesso (status "200"):
            - Grava TBRELACIONAID marcando a NF como enviada
            - Escreve "OK|vlnrodoc|id_gofind|mensagem" no arquivo de saída
        Erro:
            - Escreve "ER|vlnrodoc||mensagem" no arquivo de saída
        """
        try:
            # GoFind sempre recebe um array, mesmo com uma única NF
            response = self.gofind_request.doRequest(
                rote   = "api/subscribe/invoice",
                method = "post",
                json   = [invoice]
            )
        except Exception as e:
            self.createLog(500, f"Erro de conexão | NF {vlnrodoc}: {e}")
            self._gravar_saida("ER", vlnrodoc, "", str(e)[:200])
            return

        self.response_log(response)

        if response.status_code != 200:
            self.createLog(response.status_code, f"Erro GoFind | NF {vlnrodoc}: {response.text[:300]}")
            self._gravar_saida("ER", vlnrodoc, "", response.text[:200])
            return

        try:
            result  = response.json().get('invoicesResult', [{}])[0]
        except Exception as e:
            self.createLog(500, f"Erro ao parsear resposta | NF {vlnrodoc}: {e}")
            self._gravar_saida("ER", vlnrodoc, "", str(e)[:100])
            return

        nf_id   = result.get('id',      '')
        status  = str(result.get('status', ''))
        message = result.get('message', '')
        flag    = "OK" if status == "200" else "ER"

        self.createLog(200 if flag == "OK" else 400, f"NF {vlnrodoc}: {flag} — {message}")

        if flag == "OK":
            self.relatement({
                "NMNOMEAPI"         : self.apiName,
                "NMTABELA"          : "TBCLIENTEXNOTAS",
                "IDSOFTDIB"         : str(vlnrodoc),
                "DTULTIMAALTERACAO" : "now()",
                "IDAPI"             : nf_id,
            })

        self._gravar_saida(flag, vlnrodoc, nf_id, message)


    # ══════════════════════════════════════════════════════════════════════
    # PARSE DO XML NF-e → PAYLOAD GOFIND
    # ══════════════════════════════════════════════════════════════════════

    def _xml_to_invoice(self, xml_content: str, vlnrodoc) -> dict:
        """
        Converte o conteúdo de um XML NF-e para o formato de invoice da GoFind.

        Suporta:
            - nfeProc (XML com envelope de protocolo retornado pela SEFAZ)
            - NFe     (XML puro, sem protocolo)

        Campos omitidos quando None/vazio para não poluir o payload.
        Retorna None se o parse falhar ou o XML estiver inválido.
        """
        try:
            doc = xmltodict.parse(xml_content)
        except Exception as e:
            self.createLog(500, f"Erro ao parsear XML | NF {vlnrodoc}: {e}")
            return None

        # Navega até infNFe suportando ambos os envelopes
        nfe_proc = doc.get('nfeProc') or doc
        nfe      = nfe_proc.get('NFe') or nfe_proc
        inf      = nfe.get('infNFe', {})

        if not inf:
            self.createLog(400, f"XML sem nó infNFe | NF {vlnrodoc}")
            return None

        # Chave SEFAZ: atributo Id="NFe{44chars}" — remove o prefixo "NFe"
        nfe_id   = inf.get('@Id', '').replace('NFe', '').strip()

        ide_raw  = inf.get('ide',     {})
        emit_raw = inf.get('emit',    {})
        dest_raw = inf.get('dest',    {})
        entr_raw = inf.get('entrega', {})
        det_raw  = inf.get('det',     [])

        # xmltodict retorna dict (1 item) ou list (N itens) — normaliza para lista
        if isinstance(det_raw, dict):
            det_raw = [det_raw]

        # ── ide: identificação da nota ────────────────────────────────────
        ide = {
            "dhEmi" : ide_raw.get('dhEmi'),
            "nNF"   : ide_raw.get('nNF'),
            "serie" : ide_raw.get('serie'),
        }
        if ide_raw.get('dhSaiEnt'):
            ide["dhSaiEnt"] = ide_raw['dhSaiEnt']

        # ── emit: emitente ────────────────────────────────────────────────
        ender_emit_raw = emit_raw.get('enderEmit', {})
        emit = {
            "CNPJ"     : emit_raw.get('CNPJ'),
            "xNome"    : emit_raw.get('xNome'),
            "enderEmit": {k: v for k, v in {
                "CEP"    : ender_emit_raw.get('CEP'),
                "fone"   : ender_emit_raw.get('fone'),
                "nro"    : ender_emit_raw.get('nro'),
                "UF"     : ender_emit_raw.get('UF'),
                "xBairro": ender_emit_raw.get('xBairro'),
                "xCpl"   : ender_emit_raw.get('xCpl'),
                "xLgr"   : ender_emit_raw.get('xLgr'),
                "xMun"   : ender_emit_raw.get('xMun'),
                "xPais"  : ender_emit_raw.get('xPais'),
            }.items() if v},
        }
        if emit_raw.get('xFant'): emit["xFant"] = emit_raw['xFant']
        if emit_raw.get('IE')   : emit["IE"]    = emit_raw['IE']

        # ── dest: destinatário ────────────────────────────────────────────
        ender_dest_raw = dest_raw.get('enderDest', {})
        dest = {"xNome": dest_raw.get('xNome')}
        for campo, chave in [('CNPJ','CNPJ'), ('CPF','CPF'), ('RUC','RUC'),
                              ('xFant','xFant'), ('IE','IE'), ('email','email')]:
            if dest_raw.get(campo):
                dest[chave] = dest_raw[campo]

        ender_dest = {k: v for k, v in {
            "CEP"    : ender_dest_raw.get('CEP'),
            "fone"   : ender_dest_raw.get('fone'),
            "nro"    : ender_dest_raw.get('nro'),
            "UF"     : ender_dest_raw.get('UF'),
            "xBairro": ender_dest_raw.get('xBairro'),
            "xCpl"   : ender_dest_raw.get('xCpl'),
            "xLgr"   : ender_dest_raw.get('xLgr'),
            "xMun"   : ender_dest_raw.get('xMun'),
            "xPais"  : ender_dest_raw.get('xPais'),
        }.items() if v}
        if ender_dest:
            dest["enderDest"] = ender_dest

        # ── entrega: endereço de entrega (quando diferente do destinatário) ──
        entrega = {k: v for k, v in {
            "CEP"    : entr_raw.get('CEP'),
            "fone"   : entr_raw.get('fone'),
            "nro"    : entr_raw.get('nro'),
            "UF"     : entr_raw.get('UF'),
            "xBairro": entr_raw.get('xBairro'),
            "xCpl"   : entr_raw.get('xCpl'),
            "xLgr"   : entr_raw.get('xLgr'),
            "xMun"   : entr_raw.get('xMun'),
            "xPais"  : entr_raw.get('xPais'),
        }.items() if v}

        # ── det: itens da nota ────────────────────────────────────────────
        det = []
        for item in det_raw:
            prod_raw = item.get('prod', {})
            prod = {k: v for k, v in {
                "cEAN"    : prod_raw.get('cEAN'),
                "cEANTrib": prod_raw.get('cEANTrib'),
                "CFOP"    : prod_raw.get('CFOP'),
                "cProd"   : prod_raw.get('cProd'),
                "NCM"     : prod_raw.get('NCM'),
                "qCom"    : f"{int(float(prod_raw.get('qCom')))}",
                "qTrib"   : f"{int(float(prod_raw.get('qTrib')))}",
                "uCom"    : prod_raw.get('uCom'),
                "uTrib"   : prod_raw.get('uTrib'),
                "vUnCom"  : prod_raw.get('vUnCom'),
                "vUnTrib" : prod_raw.get('vUnTrib'),
                "vProd"   : prod_raw.get('vProd'),
                "xProd"   : prod_raw.get('xProd'),
            }.items() if v is not None}

            det.append({
                "nItem": str(item.get('@nItem', '')),
                "prod" : prod,
            })

        # ── invoice final ─────────────────────────────────────────────────
        invoice = {
            "ide" : ide,
            "emit": emit,
            "dest": dest,
            "det" : det,
        }
        if nfe_id:
            invoice["Id"] = nfe_id
        if entrega:
            invoice["entrega"] = entrega

        return invoice


    # ══════════════════════════════════════════════════════════════════════
    # UTILITÁRIOS
    # ══════════════════════════════════════════════════════════════════════

    def _gravar_saida(self, flag: str, vlnrodoc, nf_id: str, message: str):
        """Grava uma linha de resultado no arquivo de saída para o Cobol."""
        file_flag = "w+" if self.output_count == 0 else "a+"
        self.write_file(
            f"{flag}|{vlnrodoc}|{nf_id}|{message}\n",
            self.saida_cobol,
            flag     = file_flag,
            encoding = 'iso-8859-1'
        )
        self.output_count += 1

    def saida_cobol_simples(self, message: str):
        """Grava uma linha simples no arquivo de saída do Cobol (sobrescreve)."""
        self.write_file(message, self.saida_cobol, flag='w+', encoding='iso-8859-1')

    def response_log(self, response):
        """Loga status e body da resposta da GoFind."""
        try:
            body = response.json()
        except Exception:
            body = response.text
        self.createLog(
            response.status_code,
            f"GoFind response | status: {response.status_code} | body: {body}"
        )

    def __del__(self):
        self.createLog("999", "Final do envio GoFind")
