// ============================================================
// FISCAL MODULE — NFS-e ABRASF v2.03
// Integração SOAP direta com a prefeitura (padrão "meu município")
// ============================================================
import {
  Injectable, Logger, NotFoundException, BadRequestException, InternalServerErrorException,
  Module, Controller, Get, Post, Patch, Param, Body, Query, UseGuards, Request,
} from '@nestjs/common';
import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm';
import {
  Repository, DataSource, Entity, PrimaryGeneratedColumn, Column,
  CreateDateColumn, UpdateDateColumn,
} from 'typeorm';
import * as forge from 'node-forge';
import { SignedXml } from 'xml-crypto';
import axios from 'axios';
import { AuthGuard } from '@nestjs/passport';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiPropertyOptional, ApiProperty } from '@nestjs/swagger';
import { IsOptional, IsString, IsNumber, IsBoolean, IsDateString, Min } from 'class-validator';
import { Cron, CronExpression } from '@nestjs/schedule';
import { TenantsService, TenantsModule } from '../tenants/tenants.module';
import { Charge } from '../payments/payments.module';
import { Client, ClientsModule } from '../clients/clients.module';
import { MailService, EmailModule } from '../email/email.module';

// ============================================================
// ABRASF XML Builder
// Constrói o XML do RPS conforme manual v2.03
// ============================================================
@Injectable()
export class AbrasXmlBuilder {
  private readonly NAMESPACE = 'http://www.abrasf.org.br/nfse.xsd';
  private readonly logger = new Logger(AbrasXmlBuilder.name);

  buildRps(params: {
    rpsNumber: number;
    rpsSerie: string;
    rpsType: number;
    competenceDate: string;        // YYYY-MM-DD — período de competência (<Competencia>)
    emissionDate?: string;         // YYYY-MM-DD — data de transmissão (<DataEmissao>); padrão = hoje
    providerCnpj: string;
    providerMunicipalInscription: string;
    // Tomador obrigatório — buscado do cliente vinculado à cobrança
    borrowerDocument: string;
    borrowerName: string;
    borrowerMunicipalInscription?: string;
    borrowerEmail?: string;
    borrowerAddress?: string;
    borrowerAddressNumber?: string;
    borrowerAddressComplement?: string;
    borrowerNeighborhood?: string;
    borrowerIbgeCode?: string;
    borrowerUf?: string;
    borrowerZipCode?: string;
    serviceAmount: number;
    issAliquota: number;
    issRetained: boolean;
    serviceListItem: string;
    discrimination: string;
    serviceCityCode: string;
    issExigibility: number;
    simpleNational: boolean;
    fiscalIncentive: boolean;
  }): string {
    const v = params;
    const fmt = (n: number) => n.toFixed(2);
    const providerTag = v.providerCnpj.replace(/\D/g, '').length === 11 ? 'Cpf' : 'Cnpj';
    const borrowerTag = v.borrowerDocument.replace(/\D/g, '').length === 11 ? 'Cpf' : 'Cnpj';
    // Usa o ibgeCode real do tomador para evitar [EL83] (conflito de município).
    // Se não informado, usa o código da cidade do serviço como fallback.
    const borrowerCityCode = v.borrowerIbgeCode || v.serviceCityCode;
    const ibgeUfMap: Record<string, string> = {
      '11':'RO','12':'AC','13':'AM','14':'RR','15':'PA','16':'AP','17':'TO',
      '21':'MA','22':'PI','23':'CE','24':'RN','25':'PB','26':'PE','27':'AL','28':'SE','29':'BA',
      '31':'MG','32':'ES','33':'RJ','35':'SP',
      '41':'PR','42':'SC','43':'RS',
      '50':'MS','51':'MT','52':'GO','53':'DF',
    };
    // Deriva UF do tomador a partir do IBGE quando não informada explicitamente
    const derivedBorrowerUf = v.borrowerUf
      || (v.borrowerIbgeCode ? ibgeUfMap[v.borrowerIbgeCode.substring(0, 2)] : undefined);
    // Só envia detalhes de rua quando temos logradouro E número (E361 exige número quando
    // logradouro é informado). CEP e bairro também têm validação municipal: só incluí-los
    // quando os dados estiverem presentes e o CEP tiver exatamente 8 dígitos.
    // Enviados mesmo quando o tomador é de outra UF: algumas prefeituras (ex: [EL83])
    // rejeitam o logradouro nesse caso, mas outras exigem os campos (ex: E119/E122/E125/E361)
    // independentemente da UF — sem uma flag por município, prioriza-se enviar o endereço completo.
    const cleanZip = (v.borrowerZipCode ?? '').replace(/\D/g, '');
    const includeStreetDetail = !!v.borrowerAddress?.trim() && !!v.borrowerAddressNumber?.trim();

    return `<?xml version="1.0" encoding="UTF-8"?>
<GerarNfseEnvio xmlns="${this.NAMESPACE}">
  <Rps>
    <InfDeclaracaoPrestacaoServico Id="rps${v.rpsNumber}">
      <Rps>
        <IdentificacaoRps>
          <Numero>${v.rpsNumber}</Numero>
          <Serie>${v.rpsSerie}</Serie>
          <Tipo>${v.rpsType}</Tipo>
        </IdentificacaoRps>
        <DataEmissao>${v.emissionDate ?? v.competenceDate}</DataEmissao>
        <Status>1</Status>
      </Rps>
      <Competencia>${v.emissionDate ?? v.competenceDate}</Competencia>
      <Servico>
        <Valores>
          <ValorServicos>${fmt(v.serviceAmount)}</ValorServicos>
          <ValorIss>${fmt(v.serviceAmount * v.issAliquota / 100)}</ValorIss>
          <Aliquota>${fmt(v.issAliquota)}</Aliquota>
        </Valores>
        <IssRetido>${v.issRetained ? 1 : 2}</IssRetido>
        <ItemListaServico>${v.serviceListItem}</ItemListaServico>
        <Discriminacao>${this.escapeXml(v.discrimination)}</Discriminacao>
        <CodigoMunicipio>${v.serviceCityCode}</CodigoMunicipio>
        <ExigibilidadeISS>${v.issExigibility}</ExigibilidadeISS>
      </Servico>
      <Prestador>
        <CpfCnpj>
          <${providerTag}>${v.providerCnpj.replace(/\D/g, '')}</${providerTag}>
        </CpfCnpj>
        <InscricaoMunicipal>${v.providerMunicipalInscription}</InscricaoMunicipal>
      </Prestador>
      <Tomador>
        <IdentificacaoTomador>
          <CpfCnpj>
            <${borrowerTag}>${v.borrowerDocument.replace(/\D/g, '')}</${borrowerTag}>
          </CpfCnpj>
          ${v.borrowerMunicipalInscription ? `<InscricaoMunicipal>${v.borrowerMunicipalInscription}</InscricaoMunicipal>` : ''}
        </IdentificacaoTomador>
        <RazaoSocial>${this.escapeXml(v.borrowerName)}</RazaoSocial>
        <Endereco>
          ${includeStreetDetail ? `<Endereco>${this.escapeXml(v.borrowerAddress.trim())}</Endereco>` : ''}
          ${includeStreetDetail ? `<Numero>${this.escapeXml(v.borrowerAddressNumber.trim())}</Numero>` : ''}
          ${includeStreetDetail && v.borrowerAddressComplement?.trim() ? `<Complemento>${this.escapeXml(v.borrowerAddressComplement)}</Complemento>` : ''}
          ${includeStreetDetail && v.borrowerNeighborhood?.trim() ? `<Bairro>${this.escapeXml(v.borrowerNeighborhood.trim())}</Bairro>` : ''}
          <CodigoMunicipio>${borrowerCityCode}</CodigoMunicipio>
          ${derivedBorrowerUf ? `<Uf>${derivedBorrowerUf}</Uf>` : ''}
          ${includeStreetDetail && cleanZip.length === 8 ? `<Cep>${cleanZip}</Cep>` : ''}
        </Endereco>
        ${v.borrowerEmail ? `
        <Contato>
          <Email>${v.borrowerEmail}</Email>
        </Contato>` : ''}
      </Tomador>
      <OptanteSimplesNacional>${v.simpleNational ? 1 : 2}</OptanteSimplesNacional>
      <IncentivoFiscal>${v.fiscalIncentive ? 1 : 2}</IncentivoFiscal>
    </InfDeclaracaoPrestacaoServico>
  </Rps>
</GerarNfseEnvio>`;
  }

  buildCancelRequest(params: {
    nfseNumber: string;
    providerCnpj: string;
    municipalInscription: string;
    cityCode: string;
    cancelCode: number; // 1=Erro emissão 2=Serviço não prestado 4=Duplicidade 9=Outros
    reason?: string;
  }): string {
    const providerTag = params.providerCnpj.replace(/\D/g, '').length === 11 ? 'Cpf' : 'Cnpj';
    return `<?xml version="1.0" encoding="UTF-8"?>
<CancelarNfseEnvio xmlns="${this.NAMESPACE}">
  <Pedido>
    <InfPedidoCancelamento Id="cancel${params.nfseNumber}">
      <IdentificacaoNfse>
        <Numero>${params.nfseNumber}</Numero>
        <CpfCnpj>
          <${providerTag}>${params.providerCnpj.replace(/\D/g, '')}</${providerTag}>
        </CpfCnpj>
        <InscricaoMunicipal>${params.municipalInscription}</InscricaoMunicipal>
        <CodigoMunicipio>${params.cityCode}</CodigoMunicipio>
      </IdentificacaoNfse>
      <CodigoCancelamento>${params.cancelCode}</CodigoCancelamento>
      ${params.reason ? `<MotivoCancelamento>${this.escapeXml(params.reason)}</MotivoCancelamento>` : ''}
    </InfPedidoCancelamento>
  </Pedido>
</CancelarNfseEnvio>`;
  }

  buildConsultarPorRps(params: {
    rpsNumber: number;
    rpsSerie: string;
    rpsType: number;
    providerCnpj: string;
    providerMunicipalInscription: string;
  }): string {
    const v = params;
    const providerTag = v.providerCnpj.replace(/\D/g, '').length === 11 ? 'Cpf' : 'Cnpj';
    return `<?xml version="1.0" encoding="UTF-8"?>
<ConsultarNfseRpsEnvio xmlns="${this.NAMESPACE}">
  <IdentificacaoRps>
    <Numero>${v.rpsNumber}</Numero>
    <Serie>${v.rpsSerie}</Serie>
    <Tipo>${v.rpsType}</Tipo>
  </IdentificacaoRps>
  <Prestador>
    <CpfCnpj>
      <${providerTag}>${v.providerCnpj.replace(/\D/g, '')}</${providerTag}>
    </CpfCnpj>
    <InscricaoMunicipal>${v.providerMunicipalInscription}</InscricaoMunicipal>
  </Prestador>
</ConsultarNfseRpsEnvio>`;
  }

  parseNfseResponse(xml: string): {
    nfseNumber?: string;
    chaveAcesso?: string;
    verifyCode?: string;
    rpsNumber?: number;
    nfseUrl?: string;
    errors?: Array<{ code: string; message: string }>;
    acknowledgment?: string;
  } {
    let content = xml;

    // 1. Extrai conteúdo de <outputXML> (padrão meu-município e similares)
    //    O valor pode ser entity-encoded ou CDATA dentro da tag.
    const outputXmlMatch = xml.match(/<outputXML[^>]*>([\s\S]*?)<\/outputXML>/i);
    if (outputXmlMatch) {
      const raw = outputXmlMatch[1].trim();
      if (raw.startsWith('<![CDATA[')) {
        content = raw.replace(/^<!\[CDATA\[/, '').replace(/\]\]>$/, '');
      } else {
        content = raw
          .replace(/&lt;/g, '<').replace(/&gt;/g, '>')
          .replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&apos;/g, "'");
      }
    } else {
      // 2. Fallback: CDATA direto no body SOAP
      const cdataMatch = xml.match(/<!\[CDATA\[([\s\S]*?)\]\]>/);
      if (cdataMatch) {
        content = cdataMatch[1];
      } else {
        // 3. Fallback: entity-encoding direto no body SOAP
        const entityMatch = xml.match(/>(\s*&lt;[\s\S]*?)(<\/)/);
        if (entityMatch) {
          content = entityMatch[1]
            .replace(/&lt;/g, '<').replace(/&gt;/g, '>')
            .replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&apos;/g, "'");
        }
      }
    }

    // Helper: match tag com ou sem prefixo de namespace (ex: <ns2:Numero> ou <Numero>)
    const tagRe = (name: string) => new RegExp(`<(?:[\\w]+:)?${name}>([\\s\\S]*?)<\\/(?:[\\w]+:)?${name}>`, 'i');
    const tagVal = (str: string, name: string) => str.match(tagRe(name))?.[1]?.trim();

    // Número da NFS-e: busca dentro de <InfNfse> para não confundir com <Numero> do RPS.
    // Suporta prefixos de namespace (ex: <ns2:InfNfse>).
    let nfseNumber: string | undefined;
    let rpsNumber: number | undefined;
    const infNfseBlock = content.match(/<(?:[\w]+:)?InfNfse[\s\S]*?<\/(?:[\w]+:)?InfNfse>/i);
    if (infNfseBlock) {
      const block = infNfseBlock[0];
      nfseNumber = tagVal(block, 'Numero') ?? undefined;
      // RPS confirmado pela prefeitura (dentro de <IdentificacaoRps>)
      const rpsIdBlock = block.match(/<(?:[\w]+:)?IdentificacaoRps>([\s\S]*?)<\/(?:[\w]+:)?IdentificacaoRps>/i);
      if (rpsIdBlock) {
        const n = tagVal(rpsIdBlock[0], 'Numero');
        if (n && /^\d+$/.test(n)) rpsNumber = parseInt(n, 10);
        // Se <InfNfse> abre com <Numero> = NFS-e e depois tem <IdentificacaoRps><Numero> = RPS
        // garantir que nfseNumber não foi sobrescrito pelo número do RPS
        if (nfseNumber === n) {
          // busca o primeiro <Numero> ANTES do bloco de IdentificacaoRps
          const beforeRps = block.substring(0, block.indexOf(rpsIdBlock[0]));
          const candidato = tagVal(beforeRps, 'Numero');
          if (candidato && /^\d+$/.test(candidato)) nfseNumber = candidato;
        }
      }
    }
    if (!nfseNumber) {
      // Fallback: qualquer <Numero> no conteúdo (apenas dígitos)
      const m = content.match(/<(?:[\w]+:)?Numero>(\d+)<\/(?:[\w]+:)?Numero>/i);
      if (m) nfseNumber = m[1];
    }

    const verifyCode = tagVal(content, 'CodigoVerificacao') ?? undefined;

    // Chave de acesso: tag <ChaveAcesso> (50 dígitos) — prioridade sobre CodigoVerificacao
    const chaveAcesso = tagVal(content, 'ChaveAcesso') ?? undefined;

    // URL de visualização/download da NFS-e na prefeitura
    const nfseUrl =
      tagVal(content, 'LinkNota') ||
      tagVal(content, 'NfseUrl')  ||
      tagVal(content, 'UrlNfse')  ||
      undefined;

    // Erros: cada bloco <MensagemRetorno>
    const errBlocks = content.match(/<(?:[\w]+:)?MensagemRetorno>([\s\S]*?)<\/(?:[\w]+:)?MensagemRetorno>/gi);
    const errors = errBlocks
      ?.map((block) => ({
        code:    tagVal(block, 'Codigo') ?? '',
        message: tagVal(block, 'Mensagem') ?? '',
      }))
      .filter((e) => e.code || e.message);

    // Confirmação assíncrona: prefeitura aceita o RPS mas processa em segundo plano.
    const acknowledgment = !nfseNumber && !errors?.length
      ? (tagVal(content, 'Mensagem') ?? undefined)
      : undefined;

    return {
      nfseNumber,
      chaveAcesso,
      verifyCode,
      rpsNumber,
      nfseUrl,
      errors: errors?.length ? errors : undefined,
      acknowledgment,
    };
  }

  signRpsXml(rpsXml: string, certBase64: string, certPass: string, rpsNumber: number): string {
    const pfxDer = forge.util.decode64(certBase64);
    const pfxAsn1 = forge.asn1.fromDer(pfxDer);
    const p12 = forge.pkcs12.pkcs12FromAsn1(pfxAsn1, false, certPass);

    const keyBags = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag });
    const keyBag = (keyBags[forge.pki.oids.pkcs8ShroudedKeyBag] ?? [])[0];
    if (!keyBag?.key) throw new Error('Chave privada não encontrada no certificado A1');
    const privateKeyPem = forge.pki.privateKeyToPem(keyBag.key);

    const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
    const certBag = (certBags[forge.pki.oids.certBag] ?? [])[0];
    if (!certBag?.cert) throw new Error('Certificado digital não encontrado no PFX');
    const certPem = forge.pki.certificateToPem(certBag.cert);

    const elementId = `rps${rpsNumber}`;
    const sig = new SignedXml();
    sig.privateKey = privateKeyPem;
    sig.publicCert = certPem;
    sig.canonicalizationAlgorithm = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
    sig.signatureAlgorithm = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';

    sig.addReference({
      xpath: `//*[@Id='${elementId}']`,
      transforms: [
        'http://www.w3.org/2000/09/xmldsig#enveloped-signature',
        'http://www.w3.org/TR/2001/REC-xml-c14n-20010315',
      ],
      digestAlgorithm: 'http://www.w3.org/2000/09/xmldsig#sha1',
    });

    sig.computeSignature(rpsXml, {
      location: { reference: '//*[local-name()="Rps"]', action: 'append' },
    });

    this.logger.debug(`RPS #${rpsNumber} assinado com certificado A1`);
    return sig.getSignedXml();
  }

  private escapeXml(str: string): string {
    return str
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&apos;');
  }
}

// ============================================================
// SOAP Client — envia XML para a prefeitura
// Detecta automaticamente namespace, SOAPAction e elemento de
// request via WSDL para funcionar com qualquer provedor ABRASF.
// ============================================================

interface WsdlInfo {
  targetNamespace: string;
  gerarAction: string;
  gerarElement: string;
  cancelarAction: string;
  cancelarElement: string;
  consultarRpsAction: string;
  consultarRpsElement: string;
}

@Injectable()
export class NfseSoapClient {
  private readonly logger = new Logger(NfseSoapClient.name);
  private readonly wsdlCache = new Map<string, WsdlInfo>();

  private readonly CABECALHO = `<?xml version="1.0" encoding="UTF-8"?><cabecalho xmlns="http://www.abrasf.org.br/nfse.xsd" versao="2.03"><versaoDados>2.03</versaoDados></cabecalho>`;

  // Remove sufixo ?wsdl que alguns tenants salvam junto com a URL do endpoint.
  private cleanUrl(url: string): string {
    return url.replace(/[?&]wsdl$/i, '').replace(/\/$/, '');
  }

  // Busca e cacheia o WSDL do endpoint para saber namespace/action/elemento corretos.
  private async getWsdlInfo(endpointUrl: string): Promise<WsdlInfo> {
    const url = this.cleanUrl(endpointUrl);
    if (this.wsdlCache.has(url)) return this.wsdlCache.get(url)!;

    const fallback: WsdlInfo = {
      targetNamespace: 'http://nfse.abrasf.org.br',
      gerarAction: 'GerarNfse',
      gerarElement: 'GerarNfse',
      cancelarAction: 'CancelarNfse',
      cancelarElement: 'CancelarNfse',
      consultarRpsAction: 'ConsultarNfsePorRps',
      consultarRpsElement: 'ConsultarNfsePorRps',
    };

    try {
      const wsdlUrl = `${url}?wsdl`;
      const { data: wsdl } = await axios.get(wsdlUrl, { timeout: 10000 });

      // targetNamespace do serviço
      const ns = wsdl.match(/targetNamespace="([^"]+)"/)?.[1] ?? fallback.targetNamespace;

      // SOAPAction: deve ser extraído do bloco <wsdl:binding> (não do portType)
      const bindingBlock = wsdl.match(/<wsdl:binding[\s\S]*?<\/wsdl:binding>/)?.[0] ?? wsdl;
      const gerarBindBlock = bindingBlock.match(/name="GerarNfse"[\s\S]*?<\/wsdl:operation>/)?.[0] ?? '';
      const gerarAction = gerarBindBlock.match(/soapAction="([^"]*)"/)?.[1] ?? fallback.gerarAction;
      const cancelBindBlock = bindingBlock.match(/name="CancelarNfse"[\s\S]*?<\/wsdl:operation>/)?.[0] ?? '';
      const cancelarAction = cancelBindBlock.match(/soapAction="([^"]*)"/)?.[1] ?? fallback.cancelarAction;

      // Elemento de request: extraído das mensagens (portType)
      const gerarMsgName = wsdl.match(/name="GerarNfseIn"[\s\S]*?element="(?:tns:)?([^"]+)"/)?.[1] ?? fallback.gerarElement;
      const cancelarMsgName = wsdl.match(/name="CancelarNfseIn"[\s\S]*?element="(?:tns:)?([^"]+)"/)?.[1] ?? fallback.cancelarElement;

      const consultarBindBlock = bindingBlock.match(/name="ConsultarNfsePorRps"[\s\S]*?<\/wsdl:operation>/)?.[0] ?? '';
      const consultarRpsAction = consultarBindBlock.match(/soapAction="([^"]*)"/)?.[1] ?? fallback.consultarRpsAction;
      const consultarRpsMsgName = wsdl.match(/name="ConsultarNfsePorRpsIn"[\s\S]*?element="(?:tns:)?([^"]+)"/)?.[1] ?? fallback.consultarRpsElement;

      const info: WsdlInfo = {
        targetNamespace: ns,
        gerarAction,
        gerarElement: gerarMsgName,
        cancelarAction,
        cancelarElement: cancelarMsgName,
        consultarRpsAction,
        consultarRpsElement: consultarRpsMsgName,
      };

      this.wsdlCache.set(url, info);
      this.logger.log(`WSDL ${endpointUrl} → ns=${ns} gerarAction=${gerarAction} gerarElement=${gerarMsgName}`);
      return info;
    } catch (err) {
      this.logger.warn(`Não foi possível obter WSDL de ${endpointUrl}: ${err.message} — usando defaults ABRASF`);
      return fallback;
    }
  }

  async sendGerarNfse(endpointUrl: string, xmlBody: string): Promise<string> {
    const endpointClean = this.cleanUrl(endpointUrl);
    const w = await this.getWsdlInfo(endpointClean);

    const soapEnvelope = `<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tns="${w.targetNamespace}">
  <soapenv:Header/>
  <soapenv:Body>
    <tns:${w.gerarElement}>
      <nfseCabecMsg><![CDATA[${this.CABECALHO}]]></nfseCabecMsg>
      <nfseDadosMsg><![CDATA[${xmlBody}]]></nfseDadosMsg>
    </tns:${w.gerarElement}>
  </soapenv:Body>
</soapenv:Envelope>`;

    try {
      const { data } = await axios.post(endpointClean, soapEnvelope, {
        headers: { 'Content-Type': 'text/xml; charset=utf-8', SOAPAction: w.gerarAction },
        timeout: 30000,
      });
      return data;
    } catch (err) {
      this.logger.error('SOAP GerarNfse falhou', err.message);
      throw err;
    }
  }

  async sendCancelarNfse(endpointUrl: string, xmlBody: string): Promise<string> {
    const endpointClean = this.cleanUrl(endpointUrl);
    const w = await this.getWsdlInfo(endpointClean);

    const soapEnvelope = `<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tns="${w.targetNamespace}">
  <soapenv:Header/>
  <soapenv:Body>
    <tns:${w.cancelarElement}>
      <nfseCabecMsg><![CDATA[${this.CABECALHO}]]></nfseCabecMsg>
      <nfseDadosMsg><![CDATA[${xmlBody}]]></nfseDadosMsg>
    </tns:${w.cancelarElement}>
  </soapenv:Body>
</soapenv:Envelope>`;

    const { data } = await axios.post(endpointClean, soapEnvelope, {
      headers: { 'Content-Type': 'text/xml; charset=utf-8', SOAPAction: w.cancelarAction },
      timeout: 30000,
    });
    return data;
  }

  async sendConsultarNfsePorRps(endpointUrl: string, xmlBody: string): Promise<string> {
    const endpointClean = this.cleanUrl(endpointUrl);
    const w = await this.getWsdlInfo(endpointClean);

    const soapEnvelope = `<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tns="${w.targetNamespace}">
  <soapenv:Header/>
  <soapenv:Body>
    <tns:${w.consultarRpsElement}>
      <nfseCabecMsg><![CDATA[${this.CABECALHO}]]></nfseCabecMsg>
      <nfseDadosMsg><![CDATA[${xmlBody}]]></nfseDadosMsg>
    </tns:${w.consultarRpsElement}>
  </soapenv:Body>
</soapenv:Envelope>`;

    const { data } = await axios.post(endpointClean, soapEnvelope, {
      headers: { 'Content-Type': 'text/xml; charset=utf-8', SOAPAction: w.consultarRpsAction },
      timeout: 30000,
    });
    return data;
  }
}

// ============================================================
// ENTITY: Invoice
// ============================================================

@Entity('invoices')
export class Invoice {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ name: 'tenant_id' })
  tenantId: string;

  @Column({ name: 'charge_id', nullable: true })
  chargeId: string;

  @Column({ name: 'contract_id', nullable: true })
  contractId: string;

  @Column({ name: 'client_id' })
  clientId: string;

  @Column({ name: 'rps_number', type: 'bigint' })
  rpsNumber: number;

  @Column({ name: 'rps_serie', length: 5, default: 'RPS' })
  rpsSerie: string;

  @Column({ name: 'rps_type', type: 'smallint', default: 1 })
  rpsType: number;

  @Column({ name: 'competence_date', type: 'date' })
  competenceDate: string;

  @Column({ name: 'nfse_number', nullable: true })
  nfseNumber: string;

  @Column({ name: 'nfse_verify_code', length: 60, nullable: true })
  nfseVerifyCode: string;

  @Column({ name: 'nfse_url', nullable: true })
  nfseUrl: string;

  @Column({ name: 'nfse_xml', type: 'text', nullable: true })
  nfseXml: string;

  @Column({ name: 'nfse_issued_at', type: 'timestamptz', nullable: true })
  nfseIssuedAt: Date;

  @Column({ name: 'service_amount', type: 'numeric', precision: 15, scale: 2 })
  serviceAmount: number;

  @Column({ name: 'iss_amount', type: 'numeric', precision: 15, scale: 2, nullable: true })
  issAmount: number;

  @Column({ name: 'iss_retained', default: false })
  issRetained: boolean;

  @Column({ name: 'iss_aliquota', type: 'numeric', precision: 4, scale: 2, nullable: true })
  issAliquota: number;

  @Column({ name: 'net_amount', type: 'numeric', precision: 15, scale: 2 })
  netAmount: number;

  @Column({ name: 'service_list_item', length: 5 })
  serviceListItem: string;

  @Column({ name: 'cnae_code', length: 15, nullable: true })
  cnaeCode: string;

  @Column({ type: 'text' })
  discrimination: string;

  @Column({ name: 'service_city_code', length: 7 })
  serviceCityCode: string;

  @Column({ name: 'iss_exigibility', type: 'smallint', default: 1 })
  issExigibility: number;

  @Column({ name: 'cancel_code', type: 'smallint', nullable: true })
  cancelCode: number;

  @Column({ name: 'cancel_reason', nullable: true })
  cancelReason: string;

  @Column({ name: 'cancelled_at', type: 'timestamptz', nullable: true })
  cancelledAt: Date;

  @Column({ name: 'cancel_xml', type: 'text', nullable: true })
  cancelXml: string;

  // pending | queued | issued | error | cancelled
  @Column({ default: 'pending' })
  status: string;

  @Column({ name: 'error_message', type: 'text', nullable: true })
  errorMessage: string;

  @Column({ name: 'retry_count', type: 'smallint', default: 0 })
  retryCount: number;

  @Column({ name: 'consulta_fail_count', type: 'smallint', default: 0 })
  consultaFailCount: number;

  @Column({ name: 'queued_at', type: 'timestamptz', nullable: true })
  queuedAt: Date;

  @Column({ name: 'abrasf_protocol', nullable: true })
  abrasfProtocol: string;

  @Column({ name: 'email_sent_at', type: 'timestamptz', nullable: true })
  emailSentAt: Date;

  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date;

  @UpdateDateColumn({ name: 'updated_at' })
  updatedAt: Date;
}

// ============================================================
// SERVICE: FiscalService
// ============================================================

export interface IssueInvoiceParams {
  tenantId: string;
  chargeId?: string;
  contractId?: string;
  clientId: string;
  // Tenant fiscal config
  providerCnpj: string;
  providerMunicipalInscription: string;
  nfseUrl: string;
  simpleNational: boolean;
  fiscalIncentive: boolean;
  nfseCertificate?: string;
  nfseCertificatePass?: string;
  // Serviço
  serviceAmount: number;
  issAliquota: number;
  issRetained: boolean;
  serviceListItem: string;
  discrimination: string;
  serviceCityCode: string;
  issExigibility: number;
  competenceDate: string;
  emissionDate?: string;        // data de transmissão; se omitido usa a data atual no momento do envio
  // Tomador (obrigatório — buscado do cliente vinculado à cobrança)
  borrowerDocument: string;
  borrowerName: string;
  borrowerMunicipalInscription?: string;
  borrowerEmail?: string;
  borrowerAddress?: string;
  borrowerAddressNumber?: string;
  borrowerAddressComplement?: string;
  borrowerNeighborhood?: string;
  borrowerIbgeCode?: string;
  borrowerUf?: string;
  borrowerZipCode?: string;
}

@Injectable()
export class FiscalService {
  private readonly logger = new Logger(FiscalService.name);

  constructor(
    @InjectRepository(Invoice)
    private readonly invoiceRepo: Repository<Invoice>,

    private readonly xmlBuilder: AbrasXmlBuilder,
    private readonly soapClient: NfseSoapClient,
    private readonly dataSource: DataSource,
    private readonly mailService: MailService,
  ) {}

  // ----------------------------------------------------------
  // Gera próximo número de RPS por tenant (atômico)
  // ----------------------------------------------------------
  private async nextRpsNumber(tenantId: string): Promise<number> {
    const result = await this.dataSource.query(
      `UPDATE tenant_sequences
       SET last_value = last_value + 1
       WHERE tenant_id = $1 AND entity = 'rps'
       RETURNING last_value`,
      [tenantId],
    );

    const rows = Array.isArray(result[0]) ? result[0] : result;
    if (!rows?.length) {
      // Inicializa se não existe
      const r2 = await this.dataSource.query(
        `INSERT INTO tenant_sequences (tenant_id, entity, last_value)
         VALUES ($1, 'rps', 1)
         ON CONFLICT (tenant_id, entity) DO UPDATE SET last_value = tenant_sequences.last_value + 1
         RETURNING last_value`,
        [tenantId],
      );
      const r2row = Array.isArray(r2[0]) ? r2[0][0] : r2[0];
      return Number(r2row.last_value);
    }

    return Number(rows[0].last_value);
  }

  private validateBorrowerFields(params: IssueInvoiceParams): void {
    const required = [
      { key: 'borrowerDocument', label: 'Documento do tomador' },
      { key: 'borrowerAddress',  label: 'Endereço do tomador' },
      { key: 'borrowerNeighborhood', label: 'Bairro do tomador' },
      { key: 'borrowerUf',      label: 'UF do tomador' },
      { key: 'borrowerZipCode', label: 'CEP do tomador' },
    ];
    const missing = required
      .filter((item) => !params[item.key as keyof IssueInvoiceParams])
      .map((item) => item.label);
    if (missing.length) {
      throw new BadRequestException(
        `Dados do tomador incompletos: ${missing.join(', ')}. Verifique o cadastro do cliente.`,
      );
    }
  }

  // ----------------------------------------------------------
  // Emite NFS-e diretamente (síncrono)
  // ----------------------------------------------------------
  async issueInvoice(params: IssueInvoiceParams): Promise<Invoice> {
    this.validateBorrowerFields(params);
    const rpsNumber = await this.nextRpsNumber(params.tenantId);

    // Cria registro pendente
    const invoice = this.invoiceRepo.create({
      tenantId: params.tenantId,
      chargeId: params.chargeId,
      contractId: params.contractId,
      clientId: params.clientId,
      rpsNumber,
      rpsSerie: 'RPS',
      rpsType: 1,
      competenceDate: params.competenceDate,
      serviceAmount: params.serviceAmount,
      issAliquota: params.issAliquota,
      issRetained: params.issRetained,
      issAmount: +(params.serviceAmount * params.issAliquota / 100).toFixed(2),
      netAmount: params.serviceAmount,
      serviceListItem: params.serviceListItem,
      discrimination: params.discrimination,
      serviceCityCode: params.serviceCityCode,
      issExigibility: params.issExigibility,
      status: 'queued',
    });

    await this.invoiceRepo.save(invoice);
    await this.processEmission(invoice.id, params);
    this.logger.log(`RPS #${rpsNumber} emitido [invoice: ${invoice.id}]`);
    return this.invoiceRepo.findOneBy({ id: invoice.id });
  }

  // ----------------------------------------------------------
  // Processador da fila (chamado pelo BullMQ worker)
  // ----------------------------------------------------------
  private async resolveIbgeCode(ibgeCode: string | undefined, zipCode: string | undefined): Promise<string | undefined> {
    if (ibgeCode) return ibgeCode;
    if (!zipCode) return undefined;
    try {
      const cep = zipCode.replace(/\D/g, '');
      const { data } = await axios.get(`https://viacep.com.br/ws/${cep}/json/`, { timeout: 5000 });
      return data?.ibge ?? undefined;
    } catch {
      return undefined;
    }
  }

  async processEmission(invoiceId: string, params: IssueInvoiceParams): Promise<void> {
    const invoice = await this.invoiceRepo.findOneBy({ id: invoiceId });
    if (!invoice) return;

    try {
      // Resolve ibgeCode do tomador via ViaCEP quando não cadastrado no cliente
      const borrowerIbgeCode = await this.resolveIbgeCode(params.borrowerIbgeCode, params.borrowerZipCode);

      let xml = this.xmlBuilder.buildRps({
        rpsNumber: invoice.rpsNumber,
        rpsSerie: invoice.rpsSerie,
        rpsType: invoice.rpsType,
        competenceDate: invoice.competenceDate,
        emissionDate: new Date().toLocaleDateString('sv-SE', { timeZone: 'America/Sao_Paulo' }),
        providerCnpj: params.providerCnpj,
        providerMunicipalInscription: params.providerMunicipalInscription,
        borrowerDocument: params.borrowerDocument,
        borrowerName: params.borrowerName,
        borrowerMunicipalInscription: params.borrowerMunicipalInscription,
        borrowerEmail: params.borrowerEmail,
        borrowerAddress: params.borrowerAddress,
        borrowerAddressNumber: params.borrowerAddressNumber,
        borrowerAddressComplement: params.borrowerAddressComplement,
        borrowerNeighborhood: params.borrowerNeighborhood,
        borrowerIbgeCode: borrowerIbgeCode,
        borrowerUf: params.borrowerUf,
        borrowerZipCode: params.borrowerZipCode,
        serviceAmount: params.serviceAmount,
        issAliquota: params.issAliquota,
        issRetained: params.issRetained,
        serviceListItem: params.serviceListItem,
        discrimination: params.discrimination,
        serviceCityCode: params.serviceCityCode,
        issExigibility: params.issExigibility,
        simpleNational: params.simpleNational,
        fiscalIncentive: params.fiscalIncentive,
      });

      if (params.nfseCertificate && params.nfseCertificatePass) {
        xml = this.xmlBuilder.signRpsXml(xml, params.nfseCertificate, params.nfseCertificatePass, invoice.rpsNumber);
      } else {
        this.logger.warn(`Certificado A1 não configurado — RPS #${invoice.rpsNumber} enviado sem assinatura digital`);
      }

      const responseXml = await this.soapClient.sendGerarNfse(params.nfseUrl, xml);
      const parsed = this.xmlBuilder.parseNfseResponse(responseXml);

      if (parsed.nfseNumber) {
        invoice.nfseNumber = parsed.nfseNumber;
        // Prioriza ChaveAcesso (50 dígitos) sobre CodigoVerificacao curto
        invoice.nfseVerifyCode = parsed.chaveAcesso ?? parsed.verifyCode;
        invoice.nfseXml = responseXml;
        invoice.nfseIssuedAt = new Date();
        invoice.status = 'issued';
        if (parsed.nfseUrl) invoice.nfseUrl = parsed.nfseUrl;
        // Sincroniza número do RPS com o que a prefeitura confirmou
        if (parsed.rpsNumber && parsed.rpsNumber !== Number(invoice.rpsNumber)) {
          this.logger.log(`RPS sync: prefeitura confirmou #${parsed.rpsNumber} (armazenado #${invoice.rpsNumber}) [invoice: ${invoiceId}]`);
          invoice.rpsNumber = parsed.rpsNumber;
          await this.dataSource.query(
            `UPDATE tenant_sequences SET last_value = GREATEST(last_value, $1) WHERE tenant_id = $2 AND entity = 'rps'`,
            [parsed.rpsNumber, invoice.tenantId],
          );
        }
      } else if (parsed.acknowledgment) {
        // Prefeitura aceitou o RPS mas processa de forma assíncrona.
        // Mantém status 'queued' e registra a confirmação como informação.
        invoice.nfseXml = responseXml;
        invoice.status = 'queued';
        invoice.queuedAt = new Date();
        invoice.errorMessage = `Aguardando Sefaz: ${parsed.acknowledgment}`;
        invoice.consultaFailCount = 0;
        this.logger.log(`RPS #${invoice.rpsNumber} aceito pela prefeitura — processamento assíncrono: ${parsed.acknowledgment}`);
      } else {
        invoice.status = 'error';
        invoice.nfseXml = responseXml;
        invoice.errorMessage = parsed.errors?.length
          ? parsed.errors.map((e) => `[${e.code}] ${e.message}`).join('; ')
          : 'Prefeitura não retornou número da NFS-e. Verifique o XML de resposta.';
        invoice.retryCount += 1;

        // E10: RPS já usado — atualiza sequência automaticamente para o próximo válido
        const e10 = parsed.errors?.find((e) => e.code === 'E10');
        if (e10) {
          const m = e10.message.match(/Próximo RPS Válido:\s*(\d+)/i);
          if (m) {
            const nextRps = parseInt(m[1], 10);
            await this.dataSource.query(
              `UPDATE tenant_sequences SET last_value = GREATEST(last_value, $1) WHERE tenant_id = $2 AND entity = 'rps'`,
              [nextRps - 1, invoice.tenantId],
            );
            this.logger.log(`E10: sequência RPS atualizada → próximo será #${nextRps} [tenant: ${invoice.tenantId}]`);
          }
        }
      }
    } catch (err) {
      invoice.status = 'error';
      invoice.errorMessage = err.message;
      invoice.retryCount += 1;
      this.logger.error(`Falha ao emitir NFS-e invoice=${invoiceId}`, err.message);
    }

    await this.invoiceRepo.save(invoice);

    if (invoice.status === 'issued' && !invoice.emailSentAt) {
      await this.autoSendInvoiceEmail(invoice.tenantId, invoice.id);
    }
  }

  // Envia a NFS-e por e-mail assim que ela é confirmada (emissão síncrona ou
  // consulta assíncrona), sem exigir clique manual em "Enviar". Falhas (SMTP
  // não configurado, cliente sem e-mail, etc.) só geram log — não devem
  // interromper o fluxo de emissão/consulta.
  private async autoSendInvoiceEmail(tenantId: string, invoiceId: string): Promise<void> {
    try {
      await this.sendInvoiceEmail(tenantId, invoiceId);
      this.logger.log(`E-mail de NFS-e enviado automaticamente [invoice: ${invoiceId}]`);
    } catch (err) {
      this.logger.warn(`Falha ao enviar e-mail automático da NFS-e [invoice: ${invoiceId}]: ${(err as Error).message}`);
    }
  }

  // ----------------------------------------------------------
  // Cancelar NFS-e
  // ----------------------------------------------------------
  async cancelInvoice(
    tenantId: string,
    invoiceId: string,
    params: {
      providerCnpj: string;
      municipalInscription: string;
      cityCode: string;
      nfseUrl: string;
      cancelCode: number;
      reason: string;
    },
  ): Promise<Invoice> {
    const invoice = await this.invoiceRepo.findOne({
      where: { id: invoiceId, tenantId, status: 'issued' },
    });
    if (!invoice) throw new NotFoundException('NFS-e não encontrada ou não pode ser cancelada');

    const xml = this.xmlBuilder.buildCancelRequest({
      nfseNumber: invoice.nfseNumber,
      providerCnpj: params.providerCnpj,
      municipalInscription: params.municipalInscription,
      cityCode: params.cityCode,
      cancelCode: params.cancelCode,
      reason: params.reason,
    });

    const responseXml = await this.soapClient.sendCancelarNfse(params.nfseUrl, xml);

    invoice.status = 'cancelled';
    invoice.cancelCode = params.cancelCode;
    invoice.cancelReason = params.reason;
    invoice.cancelledAt = new Date();
    invoice.cancelXml = responseXml;

    return this.invoiceRepo.save(invoice);
  }

  async retryInvoice(tenantId: string, invoiceId: string, params: IssueInvoiceParams, newRpsNumber?: number): Promise<Invoice> {
    this.validateBorrowerFields(params);
    const invoice = await this.invoiceRepo.findOne({ where: { id: invoiceId, tenantId } });
    if (!invoice) throw new NotFoundException('NFS-e não encontrada');
    if (!['error', 'queued'].includes(invoice.status)) {
      throw new BadRequestException('Apenas NFS-e com erro ou aguardando podem ser retransmitidas');
    }

    if (newRpsNumber && newRpsNumber !== invoice.rpsNumber) {
      invoice.rpsNumber = newRpsNumber;
      await this.dataSource.query(
        `UPDATE tenant_sequences SET last_value = GREATEST(last_value, $1) WHERE tenant_id = $2 AND entity = 'rps'`,
        [newRpsNumber, tenantId],
      );
      this.logger.log(`RPS atualizado para #${newRpsNumber} antes da retransmissão [invoice: ${invoiceId}]`);
    }

    invoice.status = 'queued';
    invoice.errorMessage = null;
    invoice.queuedAt = new Date();
    invoice.consultaFailCount = 0;
    await this.invoiceRepo.save(invoice);
    await this.processEmission(invoice.id, params);
    this.logger.log(`RPS #${invoice.rpsNumber} retransmitido [invoice: ${invoice.id}]`);
    return this.invoiceRepo.findOneBy({ id: invoice.id });
  }

  async consultarInvoice(
    tenantId: string,
    invoiceId: string,
    params: { providerCnpj: string; providerMunicipalInscription: string; nfseUrl: string },
  ): Promise<Invoice> {
    const invoice = await this.invoiceRepo.findOne({ where: { id: invoiceId, tenantId } });
    if (!invoice) throw new NotFoundException('NFS-e não encontrada');
    // Permite consultar notas 'issued' que ainda não têm número (parser não capturou na emissão)
    const canConsult = ['queued', 'error'].includes(invoice.status)
      || (invoice.status === 'issued' && !invoice.nfseNumber);
    if (!canConsult) {
      throw new BadRequestException('Apenas NFS-e em fila, com erro ou emitidas sem número podem ser consultadas na prefeitura');
    }

    try {
      const xml = this.xmlBuilder.buildConsultarPorRps({
        rpsNumber: invoice.rpsNumber,
        rpsSerie: invoice.rpsSerie,
        rpsType: invoice.rpsType,
        providerCnpj: params.providerCnpj,
        providerMunicipalInscription: params.providerMunicipalInscription,
      });

      const responseXml = await this.soapClient.sendConsultarNfsePorRps(params.nfseUrl, xml);
      const parsed = this.xmlBuilder.parseNfseResponse(responseXml);

      if (parsed.nfseNumber) {
        invoice.nfseNumber = parsed.nfseNumber;
        invoice.nfseVerifyCode = parsed.chaveAcesso ?? parsed.verifyCode;
        invoice.nfseXml = responseXml;
        invoice.nfseIssuedAt = new Date();
        invoice.status = 'issued';
        invoice.errorMessage = null;
        invoice.consultaFailCount = 0;
        if (parsed.nfseUrl) invoice.nfseUrl = parsed.nfseUrl;
        // Sincroniza número do RPS com o que a prefeitura confirmou
        if (parsed.rpsNumber && parsed.rpsNumber !== Number(invoice.rpsNumber)) {
          this.logger.log(`RPS sync (consulta): prefeitura confirmou #${parsed.rpsNumber} (armazenado #${invoice.rpsNumber}) [invoice: ${invoiceId}]`);
          invoice.rpsNumber = parsed.rpsNumber;
          await this.dataSource.query(
            `UPDATE tenant_sequences SET last_value = GREATEST(last_value, $1) WHERE tenant_id = $2 AND entity = 'rps'`,
            [parsed.rpsNumber, tenantId],
          );
        }
        this.logger.log(`Consulta RPS #${invoice.rpsNumber}: NFS-e ${parsed.nfseNumber} confirmada [invoice: ${invoiceId}]`);
      } else if (parsed.acknowledgment) {
        invoice.nfseXml = responseXml;
        invoice.errorMessage = `Aguardando Sefaz: ${parsed.acknowledgment}`;
        invoice.consultaFailCount = 0;
        this.logger.log(`Consulta RPS #${invoice.rpsNumber}: ainda processando — ${parsed.acknowledgment}`);
      } else if (parsed.errors?.length) {
        const e4 = parsed.errors.find((e) => e.code === 'E4');
        if (e4) {
          // E4: RPS não encontrado na base de dados da prefeitura
          // Incrementa contador de falhas e verifica se já atingiu 3 tentativas
          invoice.consultaFailCount = (invoice.consultaFailCount || 0) + 1;
          invoice.nfseXml = responseXml;

          if (invoice.consultaFailCount >= 3) {
            // Após 3 E4 consecutivos, considera como erro permanente
            invoice.status = 'error';
            invoice.errorMessage = `Prefeitura nunca recebeu o RPS #${invoice.rpsNumber} (${invoice.consultaFailCount} consultas retornaram E4). Tente retransmitir com novo número de RPS.`;
            this.logger.warn(`RPS #${invoice.rpsNumber}: 3 E4 recebidos — mudando para status ERROR [invoice: ${invoiceId}]`);
          } else {
            // Ainda em queue mas com aumento de falhas
            invoice.errorMessage = `Aguardando Sefaz: RPS recebido pela prefeitura, aguardando processamento (${invoice.consultaFailCount}/3 consultas E4)`;
            this.logger.debug(`RPS #${invoice.rpsNumber}: E4 recebido (${invoice.consultaFailCount}/3) [invoice: ${invoiceId}]`);
          }
        } else {
          invoice.status = 'error';
          invoice.nfseXml = responseXml;
          invoice.errorMessage = parsed.errors.map((e) => `[${e.code}] ${e.message}`).join('; ');
          invoice.consultaFailCount = 0;
        }
      } else {
        invoice.errorMessage = 'Consulta realizada, mas prefeitura não retornou resultado';
      }
    } catch (err) {
      throw new BadRequestException(`Falha ao consultar NFS-e na prefeitura: ${(err as Error).message}`);
    }

    let saved: Invoice;
    try {
      saved = await this.invoiceRepo.save(invoice);
    } catch (err) {
      this.logger.error(`Erro ao salvar invoice ${invoiceId} após consulta: ${(err as Error).message}`);
      throw new InternalServerErrorException(`Erro ao atualizar nota fiscal: ${(err as Error).message}`);
    }

    if (saved.status === 'issued' && !saved.emailSentAt) {
      await this.autoSendInvoiceEmail(tenantId, saved.id);
    }

    return saved;
  }

  // ----------------------------------------------------------
  // Auto-retry stuck invoices (scheduled task)
  // Retransmite notas fiscais que ficaram presas em 'queued'
  // por mais de 24 horas, com novo número de RPS
  // ----------------------------------------------------------
  @Cron(CronExpression.EVERY_6_HOURS)
  async autoRetryStuckInvoices(): Promise<void> {
    const TIMEOUT_HOURS = 24;
    const cutoffTime = new Date(Date.now() - TIMEOUT_HOURS * 60 * 60 * 1000);

    try {
      const stuckInvoices = await this.invoiceRepo.find({
        where: {
          status: 'queued',
          consultaFailCount: 2, // Only auto-retry if already consulted twice without success
        },
      });

      for (const invoice of stuckInvoices) {
        if (!invoice.queuedAt || invoice.queuedAt < cutoffTime) {
          this.logger.log(
            `Auto-retrying stuck RPS #${invoice.rpsNumber} [invoice: ${invoice.id}, tenant: ${invoice.tenantId}]`,
          );

          const nextRpsNumber = await this.nextRpsNumber(invoice.tenantId);
          invoice.rpsNumber = nextRpsNumber;
          invoice.status = 'queued';
          invoice.queuedAt = new Date();
          invoice.consultaFailCount = 0;
          invoice.errorMessage = 'Retransmissão automática após timeout (RPS anterior não processado)';
          invoice.retryCount += 1;

          await this.invoiceRepo.save(invoice);

          // Busca os dados de tenant e cliente para poder reemitir
          const tenant = await this.dataSource.query(
            `SELECT * FROM tenants WHERE id = $1`,
            [invoice.tenantId],
          );
          const client = await this.dataSource.query(
            `SELECT * FROM clients WHERE id = $1 AND tenant_id = $2`,
            [invoice.clientId, invoice.tenantId],
          );

          if (!tenant?.length || !client?.length) {
            this.logger.warn(
              `Tenant ou cliente não encontrado para auto-retry [invoice: ${invoice.id}]`,
            );
            continue;
          }

          const t = tenant[0];
          const c = client[0];

          const params: IssueInvoiceParams = {
            tenantId: invoice.tenantId,
            chargeId: invoice.chargeId,
            contractId: invoice.contractId,
            clientId: invoice.clientId,
            providerCnpj: t.document,
            providerMunicipalInscription: t.municipal_inscription,
            nfseUrl: t.nfse_url,
            simpleNational: t.simple_national,
            fiscalIncentive: t.fiscal_incentive,
            nfseCertificate: t.nfse_certificate ?? undefined,
            nfseCertificatePass: t.nfse_certificate_pass ?? undefined,
            serviceAmount: Number(invoice.serviceAmount),
            issAliquota: Number(invoice.issAliquota ?? 0),
            issRetained: invoice.issRetained,
            serviceListItem: invoice.serviceListItem,
            discrimination: invoice.discrimination,
            serviceCityCode: invoice.serviceCityCode,
            issExigibility: invoice.issExigibility,
            competenceDate: invoice.competenceDate,
            borrowerDocument: c.document,
            borrowerName: c.name,
            borrowerMunicipalInscription: c.municipal_inscription,
            borrowerEmail: c.email_nf ?? c.email,
            borrowerAddress: c.address,
            borrowerAddressNumber: c.address_number,
            borrowerAddressComplement: c.address_complement,
            borrowerNeighborhood: c.neighborhood,
            borrowerIbgeCode: c.ibge_code,
            borrowerUf: c.uf,
            borrowerZipCode: c.zip_code,
          };

          await this.processEmission(invoice.id, params);
          this.logger.log(
            `RPS #${nextRpsNumber} retransmitido automaticamente [invoice: ${invoice.id}]`,
          );
        }
      }
    } catch (err) {
      this.logger.error(
        `Erro ao processar auto-retry de invoices stuck`,
        err instanceof Error ? err.message : String(err),
      );
    }
  }

  // ----------------------------------------------------------
  // Sincroniza dados das notas já emitidas que estão
  // desatualizadas (sem nfseUrl, sem verifyCode, etc.)
  // Consulta a prefeitura por RPS para cada uma.
  // ----------------------------------------------------------
  async syncIssuedInvoices(
    tenantId: string,
    params: { providerCnpj: string; providerMunicipalInscription: string; nfseUrl: string },
  ): Promise<{ total: number; updated: number; skipped: number; errors: number }> {
    const [issuedAll, queuedAll] = await Promise.all([
      this.invoiceRepo.find({ where: { tenantId, status: 'issued' }, order: { createdAt: 'ASC' } }),
      this.invoiceRepo.find({ where: { tenantId, status: 'queued' }, order: { createdAt: 'ASC' } }),
    ]);

    // Notas emitidas com dados incompletos + todas as notas na fila
    const issuedMissing = issuedAll.filter((inv) => !inv.nfseUrl || !inv.nfseVerifyCode || !inv.nfseNumber);
    const toSync = [...issuedMissing, ...queuedAll];

    let updated = 0;
    let errors = 0;

    for (const invoice of toSync) {
      try {
        if (invoice.status === 'queued') {
          // Usa o mesmo fluxo do botão "Consultar" para notas na fila
          const prevStatus = invoice.status;
          const result = await this.consultarInvoice(tenantId, invoice.id, params);
          if (result.status !== prevStatus) updated++;
        } else {
          // Notas emitidas com campos faltando — consulta por RPS para completar
          const xml = this.xmlBuilder.buildConsultarPorRps({
            rpsNumber: invoice.rpsNumber,
            rpsSerie: invoice.rpsSerie,
            rpsType: invoice.rpsType,
            providerCnpj: params.providerCnpj,
            providerMunicipalInscription: params.providerMunicipalInscription,
          });

          const responseXml = await this.soapClient.sendConsultarNfsePorRps(params.nfseUrl, xml);
          const parsed = this.xmlBuilder.parseNfseResponse(responseXml);

          if (!parsed.nfseNumber) {
            this.logger.warn(`Sync: prefeitura não retornou NFS-e para RPS #${invoice.rpsNumber} [invoice: ${invoice.id}]`);
            errors++;
            continue;
          }

          let changed = false;
          if (parsed.nfseNumber && parsed.nfseNumber !== invoice.nfseNumber) { invoice.nfseNumber = parsed.nfseNumber; changed = true; }
          const newVerify = parsed.chaveAcesso ?? parsed.verifyCode;
          if (newVerify && newVerify !== invoice.nfseVerifyCode) { invoice.nfseVerifyCode = newVerify; changed = true; }
          if (parsed.nfseUrl && parsed.nfseUrl !== invoice.nfseUrl) { invoice.nfseUrl = parsed.nfseUrl; changed = true; }
          if (!invoice.nfseXml) { invoice.nfseXml = responseXml; changed = true; }
          if (parsed.rpsNumber && parsed.rpsNumber !== Number(invoice.rpsNumber)) {
            this.logger.log(`Sync RPS: ${invoice.rpsNumber} → ${parsed.rpsNumber} [invoice: ${invoice.id}]`);
            invoice.rpsNumber = parsed.rpsNumber;
            await this.dataSource.query(
              `UPDATE tenant_sequences SET last_value = GREATEST(last_value, $1) WHERE tenant_id = $2 AND entity = 'rps'`,
              [parsed.rpsNumber, tenantId],
            );
            changed = true;
          }

          if (changed) {
            await this.invoiceRepo.save(invoice);
            updated++;
            this.logger.log(`Sync OK: RPS #${invoice.rpsNumber} → NFS-e ${invoice.nfseNumber} [invoice: ${invoice.id}]`);
          }
        }

        await new Promise((r) => setTimeout(r, 300));
      } catch (err) {
        errors++;
        this.logger.warn(`Sync falhou [invoice: ${invoice.id}]: ${(err as Error).message}`);
      }
    }

    return { total: toSync.length, updated, skipped: toSync.length - updated - errors, errors };
  }

  async getRpsSequence(tenantId: string): Promise<number> {
    try {
      const rows = await this.dataSource.query(
        `SELECT last_value FROM tenant_sequences WHERE tenant_id = $1 AND entity = 'rps'`,
        [tenantId],
      );
      return rows[0] ? Number(rows[0].last_value) : 0;
    } catch {
      // Tabela ainda não criada (cold start antes do onModuleInit) — retorna 0
      return 0;
    }
  }

  async setRpsSequence(tenantId: string, value: number): Promise<void> {
    await this.dataSource.query(
      `INSERT INTO tenant_sequences (tenant_id, entity, last_value)
       VALUES ($1, 'rps', $2)
       ON CONFLICT (tenant_id, entity) DO UPDATE SET last_value = $2`,
      [tenantId, value],
    );
  }

  // ----------------------------------------------------------
  // Envia a NFS-e por e-mail para o tomador
  // ----------------------------------------------------------
  async sendInvoiceEmail(tenantId: string, invoiceId: string): Promise<Invoice> {
    const invoice = await this.invoiceRepo.findOne({ where: { id: invoiceId, tenantId } });
    if (!invoice) throw new NotFoundException('NFS-e não encontrada');
    if (invoice.status !== 'issued') throw new BadRequestException('Apenas NFS-e emitidas podem ser enviadas por e-mail');

    const [tenantRows, clientRows] = await Promise.all([
      this.dataSource.query(`SELECT name, email FROM tenants WHERE id = $1`, [tenantId]),
      this.dataSource.query(`SELECT name, email, email_nf FROM clients WHERE id = $1 AND tenant_id = $2`, [invoice.clientId, tenantId]),
    ]);

    const tenant = tenantRows?.[0];
    const client = clientRows?.[0];
    const to = client?.email_nf || client?.email;
    if (!to) throw new BadRequestException('Cliente não possui e-mail cadastrado para envio de NFS-e');

    const fmtBRL = (v: number) =>
      new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(v);

    const html = `
<!DOCTYPE html>
<html lang="pt-BR">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
<body style="margin:0;padding:0;background:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif">
  <table width="100%" cellpadding="0" cellspacing="0" style="background:#f4f4f5;padding:40px 0">
    <tr><td align="center">
      <table width="560" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:12px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,.08)">
        <!-- Header -->
        <tr><td style="background:#0B3D91;padding:28px 32px">
          <p style="margin:0;color:#C9A24E;font-size:22px;font-weight:700;letter-spacing:-.5px">${tenant?.name ?? 'Sua Empresa'}</p>
          <p style="margin:4px 0 0;color:rgba(255,255,255,.7);font-size:13px">Nota Fiscal de Serviço Eletrônica</p>
        </td></tr>
        <!-- Body -->
        <tr><td style="padding:32px">
          <p style="margin:0 0 8px;font-size:15px;color:#111827">Olá, <strong>${client?.name ?? 'cliente'}</strong></p>
          <p style="margin:0 0 24px;font-size:14px;color:#6B7280">Segue a NFS-e referente ao serviço prestado.</p>

          <table width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #E5E7EB;border-radius:8px;overflow:hidden;margin-bottom:24px">
            <tr style="background:#F9FAFB">
              <td style="padding:10px 16px;font-size:12px;font-weight:600;color:#6B7280;text-transform:uppercase;letter-spacing:.05em">Campo</td>
              <td style="padding:10px 16px;font-size:12px;font-weight:600;color:#6B7280;text-transform:uppercase;letter-spacing:.05em">Valor</td>
            </tr>
            <tr style="border-top:1px solid #E5E7EB">
              <td style="padding:10px 16px;font-size:13px;color:#374151">Número da NFS-e</td>
              <td style="padding:10px 16px;font-size:13px;font-weight:600;color:#111827">${invoice.nfseNumber ?? '—'}</td>
            </tr>
            <tr style="border-top:1px solid #E5E7EB;background:#F9FAFB">
              <td style="padding:10px 16px;font-size:13px;color:#374151">Número do RPS</td>
              <td style="padding:10px 16px;font-size:13px;color:#111827">${invoice.rpsNumber}</td>
            </tr>
            <tr style="border-top:1px solid #E5E7EB">
              <td style="padding:10px 16px;font-size:13px;color:#374151">Competência</td>
              <td style="padding:10px 16px;font-size:13px;color:#111827">${invoice.competenceDate ?? '—'}</td>
            </tr>
            <tr style="border-top:1px solid #E5E7EB;background:#F9FAFB">
              <td style="padding:10px 16px;font-size:13px;color:#374151">Discriminação</td>
              <td style="padding:10px 16px;font-size:13px;color:#111827">${invoice.discrimination ?? '—'}</td>
            </tr>
            <tr style="border-top:1px solid #E5E7EB">
              <td style="padding:10px 16px;font-size:13px;color:#374151">Valor do serviço</td>
              <td style="padding:10px 16px;font-size:14px;font-weight:700;color:#0B3D91">${fmtBRL(Number(invoice.serviceAmount))}</td>
            </tr>
            ${invoice.nfseVerifyCode ? `
            <tr style="border-top:1px solid #E5E7EB;background:#F9FAFB">
              <td style="padding:10px 16px;font-size:13px;color:#374151">Chave de acesso</td>
              <td style="padding:10px 16px;font-size:11px;font-family:monospace;color:#374151;word-break:break-all">${invoice.nfseVerifyCode}</td>
            </tr>` : ''}
          </table>

          ${invoice.nfseUrl ? `
          <div style="text-align:center;margin-bottom:24px">
            <a href="${invoice.nfseUrl}" style="display:inline-block;background:#0B3D91;color:#ffffff;font-size:14px;font-weight:600;padding:12px 28px;border-radius:8px;text-decoration:none">
              Visualizar NFS-e na prefeitura →
            </a>
          </div>` : ''}

          <p style="margin:0;font-size:13px;color:#9CA3AF">Em caso de dúvidas, entre em contato com ${tenant?.name ?? 'nossa empresa'}.</p>
        </td></tr>
        <!-- Footer -->
        <tr><td style="background:#F9FAFB;border-top:1px solid #E5E7EB;padding:16px 32px;text-align:center">
          <p style="margin:0;font-size:11px;color:#9CA3AF">Enviado via <strong style="color:#6B7280">Klavo</strong> · Plataforma de gestão financeira</p>
        </td></tr>
      </table>
    </td></tr>
  </table>
</body>
</html>`;

    await this.mailService.sendMail(tenantId, {
      to: to,
      subject: `NFS-e ${invoice.nfseNumber ? `Nº ${invoice.nfseNumber}` : `RPS ${invoice.rpsNumber}`} — ${tenant?.name ?? ''}`,
      html,
    });

    invoice.emailSentAt = new Date();
    return this.invoiceRepo.save(invoice);
  }

  async findByTenant(tenantId: string, status?: string): Promise<Invoice[]> {
    const where: any = { tenantId };
    if (status) where.status = status;
    return this.invoiceRepo.find({ where, order: { createdAt: 'DESC' } });
  }

  async findOne(tenantId: string, id: string): Promise<Invoice> {
    const inv = await this.invoiceRepo.findOne({ where: { id, tenantId } });
    if (!inv) throw new NotFoundException('NFS-e não encontrada');
    return inv;
  }
}

// ============================================================
// CONTROLLER
// ============================================================

export class CreateInvoiceDto {
  @ApiProperty({ description: 'ID da cobrança vinculada (obrigatório)' }) @IsString() chargeId: string;
  @ApiPropertyOptional() @IsOptional() @IsString() contractId?: string;
  @ApiPropertyOptional({ description: 'Valor do serviço (padrão: valor da cobrança)' }) @IsOptional() @IsNumber() @Min(0.01) serviceAmount?: number;
  @ApiPropertyOptional() @IsOptional() @IsNumber() @Min(0) issAliquota?: number;
  @ApiPropertyOptional() @IsOptional() @IsBoolean() issRetained?: boolean;
  @ApiProperty() @IsString() serviceListItem: string;
  @ApiPropertyOptional() @IsOptional() @IsString() discrimination?: string;
  @ApiPropertyOptional() @IsOptional() @IsNumber() issExigibility?: number;
  @ApiProperty({ example: '2025-06-01' }) @IsDateString() competenceDate: string;
}

export class RetryInvoiceDto {
  @ApiPropertyOptional({ description: 'Novo número de RPS — substitui o atual se informado', example: 17 })
  @IsOptional()
  @IsNumber()
  @Min(1)
  rpsNumber?: number;
}

export class CancelInvoiceDto {
  @ApiProperty({ enum: [1, 2, 4], description: '1=Erro emissão 2=Serviço não prestado 4=Duplicidade' })
  @IsNumber()
  cancelCode: number;

  @ApiProperty()
  @IsString()
  reason: string;
}

@ApiTags('NFS-e / Fiscal')
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'))
@Controller('invoices')
export class FiscalController {
  constructor(
    private readonly fiscalService: FiscalService,
    private readonly tenantsService: TenantsService,
    @InjectRepository(Invoice) private readonly invoiceRepo: Repository<Invoice>,
    @InjectRepository(Charge) private readonly chargeRepo: Repository<Charge>,
    @InjectRepository(Client) private readonly clientRepo: Repository<Client>,
  ) {}

  @Get()
  @ApiOperation({ summary: 'Listar NFS-e do tenant' })
  findAll(@Request() req: any, @Query('status') status?: string) {
    return this.fiscalService.findByTenant(req.user.tenantId, status);
  }

  @Get('rps-sequence')
  @ApiOperation({ summary: 'Obter número atual da sequência RPS do tenant' })
  async getRpsSequence(@Request() req: any) {
    return { current: await this.fiscalService.getRpsSequence(req.user.tenantId) };
  }

  @Patch('rps-sequence')
  @ApiOperation({ summary: 'Corrigir número da sequência RPS (alinha com o portal da prefeitura)' })
  async setRpsSequence(@Request() req: any, @Body() body: { value: number }) {
    if (!body.value || body.value < 1) throw new BadRequestException('Informe um número de sequência válido (≥ 1)');
    await this.fiscalService.setRpsSequence(req.user.tenantId, body.value);
    return { current: body.value };
  }

  @Get(':id')
  @ApiOperation({ summary: 'Detalhar NFS-e' })
  findOne(@Request() req: any, @Param('id') id: string) {
    return this.fiscalService.findOne(req.user.tenantId, id);
  }

  @Post()
  @ApiOperation({ summary: 'Emitir nova NFS-e (requer cobrança vinculada)' })
  async create(@Request() req: any, @Body() dto: CreateInvoiceDto) {
    const tenantId = req.user.tenantId;

    const charge = await this.chargeRepo.findOne({ where: { id: dto.chargeId, tenantId } });
    if (!charge) throw new BadRequestException('Cobrança não encontrada');
    if (charge.status === 'cancelled') throw new BadRequestException('Não é possível emitir NFS-e para uma cobrança cancelada');

    const existingInvoice = await this.invoiceRepo.findOne({ where: { chargeId: dto.chargeId, tenantId } });
    if (existingInvoice && existingInvoice.status !== 'cancelled') {
      const label = existingInvoice.status === 'issued' ? 'emitida' : 'em andamento';
      throw new BadRequestException(`Esta cobrança já possui uma NFS-e ${label} (RPS #${existingInvoice.rpsNumber})`);
    }

    const tenant = await this.tenantsService.findOne(tenantId);
    if (!tenant.document) throw new BadRequestException('CNPJ do emitente não cadastrado. Configure em Configurações > Empresa.');
    if (!tenant.nfseUrl) throw new BadRequestException('URL do WebService NFS-e não configurada. Configure em Configurações > Empresa.');
    if (!tenant.municipalInscription) throw new BadRequestException('Inscrição municipal não configurada. Configure em Configurações > Empresa.');
    if (!tenant.ibgeCode) throw new BadRequestException('Código IBGE do município não configurado. Configure em Configurações > Empresa.');

    const client = await this.clientRepo.findOne({ where: { id: charge.clientId, tenantId } });
    if (!client) throw new BadRequestException('Cliente vinculado à cobrança não encontrado');
    if (!client.document) throw new BadRequestException(`Cliente "${client.name}" não possui CPF/CNPJ cadastrado. Cadastre o documento antes de emitir NFS-e.`);

    return this.fiscalService.issueInvoice({
      tenantId,
      clientId: client.id,
      chargeId: charge.id,
      contractId: dto.contractId ?? charge.contractId,
      providerCnpj: tenant.document,
      providerMunicipalInscription: tenant.municipalInscription,
      nfseUrl: tenant.nfseUrl,
      simpleNational: tenant.simpleNational,
      fiscalIncentive: tenant.fiscalIncentive,
      nfseCertificate: tenant.nfseCertificate ?? undefined,
      nfseCertificatePass: tenant.nfseCertificatePass ?? undefined,
      serviceAmount: dto.serviceAmount ?? Number(charge.amount),
      issAliquota: dto.issAliquota ?? 0,
      issRetained: dto.issRetained ?? false,
      serviceListItem: dto.serviceListItem,
      discrimination: dto.discrimination ?? charge.description ?? '',
      serviceCityCode: tenant.ibgeCode,
      issExigibility: dto.issExigibility ?? 1,
      competenceDate: dto.competenceDate,
      borrowerDocument: client.document,
      borrowerName: client.name,
      borrowerMunicipalInscription: client.municipalInscription,
      borrowerEmail: client.emailNf ?? client.email,
      borrowerAddress: client.address,
      borrowerAddressNumber: client.addressNumber,
      borrowerAddressComplement: client.addressComplement,
      borrowerNeighborhood: client.neighborhood,
      borrowerIbgeCode: client.ibgeCode,
      borrowerUf: client.uf,
      borrowerZipCode: client.zipCode,
    });
  }

  @Post(':id/retry')
  @ApiOperation({ summary: 'Retransmitir NFS-e com erro (opcionalmente com novo número de RPS)' })
  async retry(@Request() req: any, @Param('id') id: string, @Body() dto: RetryInvoiceDto) {
    const tenantId = req.user.tenantId;

    const invoice = await this.invoiceRepo.findOne({ where: { id, tenantId } });
    if (!invoice) throw new NotFoundException('NFS-e não encontrada');
    if (!['error', 'queued'].includes(invoice.status)) {
      throw new BadRequestException('Apenas NFS-e com erro ou aguardando podem ser retransmitidas');
    }

    const tenant = await this.tenantsService.findOne(tenantId);
    if (!tenant.document) throw new BadRequestException('CNPJ do emitente não cadastrado. Configure em Configurações > Empresa.');
    if (!tenant.nfseUrl) throw new BadRequestException('URL do WebService NFS-e não configurada. Configure em Configurações > Empresa.');

    const client = await this.clientRepo.findOne({ where: { id: invoice.clientId, tenantId } });
    if (!client) throw new BadRequestException('Cliente vinculado à nota não encontrado');
    if (!client.document) throw new BadRequestException(`Cliente "${client.name}" não possui CPF/CNPJ cadastrado`);

    return this.fiscalService.retryInvoice(tenantId, id, {
      tenantId,
      chargeId: invoice.chargeId,
      contractId: invoice.contractId,
      clientId: invoice.clientId,
      providerCnpj: tenant.document,
      providerMunicipalInscription: tenant.municipalInscription,
      nfseUrl: tenant.nfseUrl,
      simpleNational: tenant.simpleNational,
      fiscalIncentive: tenant.fiscalIncentive,
      nfseCertificate: tenant.nfseCertificate ?? undefined,
      nfseCertificatePass: tenant.nfseCertificatePass ?? undefined,
      serviceAmount: Number(invoice.serviceAmount),
      issAliquota: Number(invoice.issAliquota ?? 0),
      issRetained: invoice.issRetained,
      serviceListItem: invoice.serviceListItem,
      discrimination: invoice.discrimination,
      serviceCityCode: invoice.serviceCityCode,
      issExigibility: invoice.issExigibility,
      competenceDate: invoice.competenceDate,
      borrowerDocument: client.document,
      borrowerName: client.name,
      borrowerMunicipalInscription: client.municipalInscription,
      borrowerEmail: client.emailNf ?? client.email,
      borrowerAddress: client.address,
      borrowerAddressNumber: client.addressNumber,
      borrowerAddressComplement: client.addressComplement,
      borrowerNeighborhood: client.neighborhood,
      borrowerIbgeCode: client.ibgeCode,
      borrowerUf: client.uf,
      borrowerZipCode: client.zipCode,
    }, dto.rpsNumber);
  }

  @Post(':id/send-email')
  @ApiOperation({ summary: 'Envia a NFS-e por e-mail para o tomador' })
  async sendEmail(@Request() req: any, @Param('id') id: string) {
    return this.fiscalService.sendInvoiceEmail(req.user.tenantId, id);
  }

  @Post(':id/consultar')
  @ApiOperation({ summary: 'Consultar NFS-e na prefeitura pelo número do RPS' })
  async consultar(@Request() req: any, @Param('id') id: string) {
    const tenantId = req.user.tenantId;
    const tenant = await this.tenantsService.findOne(tenantId);
    if (!tenant.nfseUrl) throw new BadRequestException('URL do WebService NFS-e não configurada. Configure em Configurações > Empresa.');
    if (!tenant.document) throw new BadRequestException('CNPJ do emitente não cadastrado. Configure em Configurações > Empresa.');

    return this.fiscalService.consultarInvoice(tenantId, id, {
      providerCnpj: tenant.document,
      providerMunicipalInscription: tenant.municipalInscription,
      nfseUrl: tenant.nfseUrl,
    });
  }

  @Post('sync')
  @ApiOperation({ summary: 'Sincroniza dados das NFS-e já emitidas que estão desatualizados (nfseUrl, verifyCode, rpsNumber)' })
  async syncIssued(@Request() req: any) {
    const tenantId = req.user.tenantId;
    const tenant = await this.tenantsService.findOne(tenantId);
    if (!tenant.nfseUrl) throw new BadRequestException('URL do WebService NFS-e não configurada. Configure em Configurações > Empresa.');
    if (!tenant.document) throw new BadRequestException('CNPJ do emitente não cadastrado. Configure em Configurações > Empresa.');

    return this.fiscalService.syncIssuedInvoices(tenantId, {
      providerCnpj: tenant.document,
      providerMunicipalInscription: tenant.municipalInscription,
      nfseUrl: tenant.nfseUrl,
    });
  }

  @Post(':id/cancel')
  @ApiOperation({ summary: 'Cancelar NFS-e na prefeitura' })
  async cancel(@Request() req: any, @Param('id') id: string, @Body() dto: CancelInvoiceDto) {
    const tenantId = req.user.tenantId;
    const tenant = await this.tenantsService.findOne(tenantId);

    if (!tenant.nfseUrl) {
      throw new BadRequestException('URL do WebService NFS-e não configurada');
    }

    return this.fiscalService.cancelInvoice(tenantId, id, {
      providerCnpj: tenant.document,
      municipalInscription: tenant.municipalInscription,
      cityCode: tenant.ibgeCode ?? '0000000',
      nfseUrl: tenant.nfseUrl,
      cancelCode: dto.cancelCode,
      reason: dto.reason,
    });
  }
}

// ============================================================
// MODULE
// ============================================================

@Module({
  imports: [
    TypeOrmModule.forFeature([Invoice, Charge, Client]),
    TenantsModule,
    ClientsModule,
    EmailModule,
  ],
  controllers: [FiscalController],
  providers: [FiscalService, AbrasXmlBuilder, NfseSoapClient],
  exports: [FiscalService],
})
export class FiscalModule {}
