/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package br.com.ctecinf.nfe;

import br.com.ctecinf.Config;
import br.com.ctecinf.Daruma;
import br.com.ctecinf.Empresa;
import br.com.ctecinf.Utils;
import br.com.ctecinf.swing.OptionPane;
import br.com.ctecinf.text.MaskFormatter;
import br.inf.portalfiscal.nfe.v100.evento.TEnvEvento;
import br.inf.portalfiscal.nfe.v100.evento.TEvento;
import br.inf.portalfiscal.nfe.v100.evento.TProcEvento;
import br.inf.portalfiscal.nfe.v100.evento.TRetEnvEvento;
import br.inf.portalfiscal.nfe.v400.autorizacao.TEnviNFe;
import br.inf.portalfiscal.nfe.v400.autorizacao.TNFe;
import br.inf.portalfiscal.nfe.v400.autorizacao.TNfeProc;
import br.inf.portalfiscal.nfe.v400.autorizacao.TRetEnviNFe;
import br.inf.portalfiscal.nfe.v400.inutilizacao.TInutNFe;
import br.inf.portalfiscal.nfe.v400.inutilizacao.TProcInutNFe;
import br.inf.portalfiscal.nfe.v400.inutilizacao.TRetInutNFe;
import br.inf.portalfiscal.nfe.v400.status.TConsStatServ;
import br.inf.portalfiscal.nfe.v400.status.TRetConsStatServ;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.util.Calendar;
import java.util.zip.GZIPInputStream;
import javax.xml.bind.DatatypeConverter;
import javax.xml.bind.JAXBException;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.MimeHeaders;
import javax.xml.soap.SOAPConnection;
import javax.xml.soap.SOAPConnectionFactory;
import javax.xml.soap.SOAPConstants;
import javax.xml.soap.SOAPMessage;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 *
 * @author Cássio Conceição
 * @since 27/05/2019
 * @version 201905
 * @see http://ctecinf.com.br/
 */
public class SEFAZConnection {

    private static boolean REGISTRADO = false;
    private static boolean DEBUG = true;

//    static {
//
//        new Thread(() -> {
//
//            try {
//
//                String cnpj = Config.get("user.cnpj");
//
//                String query = "SELECT licensa "
//                        + "FROM contribuinte "
//                        + "WHERE cnpj = " + cnpj;
//
//                String url = "jdbc:mysql://ctecinf.com.br/ctecinfc_sistema";
//                String user = "ctecinfc_root";
//                String pass = "S0&zYBQqLX)k";
//
//                Connection conn = DriverManager.getConnection(url, user, pass);
//
//                try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery(query)) {
//
//                    if (rs.next()) {
//
//                        Date now = Utils.dateFromServer().getTime();
//                        Date licensa = rs.getDate("licensa");
//
//                        if (now.after(licensa)) {
//                            System.exit(0);
//                        }
//
//                    } else {
//                        System.exit(0);
//                    }
//                }
//
//            } catch (SQLException ex) {
//                System.err.println(ex);
//                System.exit(0);
//            }
//        }).start();
//
//    }
    private static String chaveAcesso(TNFe nfe) {

        TNFe.InfNFe.Ide ide = nfe.getInfNFe().getIde();

        Calendar dt = Utils.dateNFe2Date(ide.getDhEmi());

        String yy = String.valueOf(dt.get(Calendar.YEAR));
        String mm = Utils.leftPad2(dt.get(Calendar.MONTH) + 1, 2, '0');
        String dataAAMM = yy.substring(2) + mm;

        StringBuilder chave = new StringBuilder();

        chave.append(Utils.leftPad2(ide.getCUF(), 2, '0'));
        chave.append(Utils.leftPad2(dataAAMM, 4, '0'));
        chave.append(Utils.leftPad2(nfe.getInfNFe().getEmit().getCNPJ(), 14, '0'));
        chave.append(Utils.leftPad2(ide.getMod(), 2, '0'));
        chave.append(Utils.leftPad2(ide.getSerie(), 3, '0'));
        chave.append(Utils.leftPad2(ide.getNNF(), 9, '0'));
        chave.append(Utils.leftPad2(ide.getTpEmis(), 1, '0'));
        chave.append(ide.getCNF());

        return chave.toString();
    }

    private static String qrCode(String chaveAcesso, String tpAmb, String cscToken) throws NoSuchAlgorithmException {

        StringBuilder params = new StringBuilder();

        params.append(chaveAcesso.replace("NFe", "")).append("|");
        params.append("2|");
        params.append(tpAmb).append("|");
        params.append("1");
        params.append(cscToken);

        return Constants.URL_CONSULTA + "?p=" + params.toString().replace(cscToken, "") + "|" + Utils.sha1(params.toString()).toUpperCase();
    }

    public static void registrarConexao(boolean debug) throws Exception {

        REGISTRADO = true;
        DEBUG = debug;

        if (DEBUG) {
            System.out.println("Registrando conexão com SEFAZ...");
        }

        PKCS11.load();

        if (DEBUG) {
            System.out.println("Conexão com SEFAZ registrada!");
        }

        TRetConsStatServ ret = consultarStatusServico();

        if (DEBUG) {
            System.out.println(ret.getXMotivo());
        }
    }

    /**
     * Consulta status do serviço
     *
     * @return
     * @throws Exception
     */
    public static TRetConsStatServ consultarStatusServico() throws Exception {

        if (!REGISTRADO) {
            SEFAZConnection.registrarConexao(DEBUG);
        }

        TConsStatServ statServ = new TConsStatServ();
        statServ.setVersao(Constants.VERSAO_NFE);
        statServ.setCUF(String.valueOf(Empresa.getUfIBGE()));
        statServ.setTpAmb(Config.get("nfe.amb"));
        statServ.setXServ("STATUS");

        String xml = Utils.marshaller(statServ);

        URL url = new URL(statServ.getTpAmb().equals("1") ? Constants.URL_PROD_STATUS_SERVICO : Constants.URL_HOMO_STATUS_SERVICO);

        if (DEBUG) {
            System.out.println("\nAguardando resposta SEFAZ...\n\n" + xml);
        }

        SOAPMessage response = sendXML(xml, Constants.XMLNS_STATUS, url);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        response.writeTo(out);

        int begin = out.toString().indexOf("<nfeResultMsg xmlns=\"" + Constants.XMLNS_STATUS + "\">");
        int end = out.toString().indexOf("</nfeResultMsg>");

        String result = null;

        if (begin > -1 && end > begin) {
            result = out.toString().substring(begin, end).replace("<nfeResultMsg xmlns=\"" + Constants.XMLNS_STATUS + "\">", "");
        }

        if (DEBUG) {
            System.out.println("\nResposta SEFAZ: " + result);
        }

        return Utils.unmarshaller(TRetConsStatServ.class, result);
    }

    /**
     * Envia NFC-e para autorizar
     *
     * @param nfce
     * @return
     * @throws Exception
     */
    public static TRetEnviNFe enviarNFe(TNFe nfce) throws Exception {

        Long nnf = Controller.getNextNNF();
        nfce.getInfNFe().getIde().setNNF(nnf.toString());

        TEnviNFe enviNFe = new TEnviNFe();
        enviNFe.getNFe().add(nfce);
        enviNFe.setVersao(Constants.VERSAO_NFE);
        enviNFe.getNFe().get(0).getInfNFe().setVersao(Constants.VERSAO_NFE);
        enviNFe.getNFe().get(0).getInfNFe().getIde().setVerProc(Constants.VER_PROC);
        enviNFe.setIdLote(nnf.toString());
        enviNFe.setIndSinc("1");

        int tpAmb = Integer.parseInt(enviNFe.getNFe().get(0).getInfNFe().getIde().getTpAmb());

        // Configura chave de acesso
        TNFe.InfNFe.Ide ide = enviNFe.getNFe().get(0).getInfNFe().getIde();

        String dhEmi = Utils.date2NFe();

        enviNFe.getNFe().get(0).getInfNFe().getIde().setDhEmi(dhEmi);

        String chave = chaveAcesso(enviNFe.getNFe().get(0));

        if (DEBUG) {
            System.out.println("\nChave de acesso: " + chave);
        }

        enviNFe.getNFe().get(0).getInfNFe().getIde().setCDV(String.valueOf(Utils.modulo11(chave)));

        chave += ide.getCDV();

        enviNFe.getNFe().get(0).getInfNFe().setId("NFe" + chave);

        System.out.println(Utils.marshaller(enviNFe));

        Document document = Signature.nfe(enviNFe);

        if (DEBUG) {
            System.out.println("\nXML Assinado");
        }

        String cscToken = tpAmb == 2 ? Empresa.getCscTokenHomo() : Empresa.getCscTokenProd();

        // set QR-Code
        Element elQrCode = document.createElement("qrCode");
        elQrCode.appendChild(document.createCDATASection(qrCode(chave, String.valueOf(tpAmb), cscToken)));

        Element elURLChave = document.createElement("urlChave");
        elURLChave.appendChild(document.createTextNode(Constants.URL_CONSULTA));

        Node elInf = document.getElementsByTagName("infNFeSupl").item(0);

        if (elInf.hasChildNodes()) {
            for (int i = 0; i < elInf.getChildNodes().getLength(); i++) {
                elInf.removeChild(elInf.getChildNodes().item(i));
            }
        }

        elInf.appendChild(elQrCode);
        elInf.appendChild(elURLChave);

        // String XML
        final String xml = Utils.doc2Str(document);

        if (DEBUG) {
            System.out.println("\nAguardando resposta SEFAZ...\n\n" + xml);
        }

        TEnviNFe nfe = Utils.unmarshaller(TEnviNFe.class, xml);
        enviNFe.getNFe().get(0).setSignature(nfe.getNFe().get(0).getSignature());
        enviNFe.getNFe().get(0).setInfNFeSupl(nfe.getNFe().get(0).getInfNFeSupl());

        URL url = new URL(tpAmb == 1 ? Constants.URL_PROD_AUTORIZACAO : Constants.URL_HOMO_AUTORIZACAO);

        SOAPMessage response = sendXML(xml, Constants.XMLNS_NFE_AUTORIZACAO, url);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        response.writeTo(out);

        int begin = out.toString().indexOf("<nfeResultMsg xmlns=\"" + Constants.XMLNS_NFE_AUTORIZACAO + "\">");
        int end = out.toString().indexOf("</nfeResultMsg>");

        String result = null;

        if (begin > -1 && end > begin) {
            result = out.toString().substring(begin, end).replace("<nfeResultMsg xmlns=\"" + Constants.XMLNS_NFE_AUTORIZACAO + "\">", "");
        }

        if (DEBUG) {
            System.out.println("\nResposta SEFAZ: " + result);
        }

        TRetEnviNFe retEnviNFe = Utils.unmarshaller(TRetEnviNFe.class, result);

        if (retEnviNFe.getCStat().equals(Constants.LOTE_PROCESSADO) && retEnviNFe.getProtNFe().getInfProt().getCStat().equals(Constants.AUTORIZADO_USO_NFE)) {

            new Thread(() -> {

                // Converte para NFCe processada
                TNfeProc nfeProc = new TNfeProc();
                nfeProc.setVersao(enviNFe.getVersao());
                nfeProc.setNFe(enviNFe.getNFe().get(0));
                nfeProc.setProtNFe(retEnviNFe.getProtNFe());

                File file = new File("nfeProc", nfeProc.getNFe().getInfNFe().getIde().getNNF() + ".xml");

                if (!file.getParentFile().exists()) {
                    file.getParentFile().mkdirs();
                }

                try {
                    Utils.writeFile(file, Utils.marshaller(nfeProc), false);
                } catch (JAXBException | IOException ex) {
                    if (DEBUG) {
                        System.err.println(ex);
                    }
                }

                try {
                    // Imprime
                    Daruma.danfeNFCe4(nfeProc);
                } catch (IOException ex) {
                    try {
                        Controller.importarNFCe(file);
                        OptionPane.success("NFC-e emitida, reenicie o sistema e vá em 'NFC-e Emitidas' para imprimir.");
                        System.exit(0);
                    } catch (Exception ex1) {
                        OptionPane.error(ex1);
                    }
                }

                try {

                    // Salva arquivo XML e no banco de dados
                    if (Controller.importarNFCe(file)) {

                        OptionPane.success(retEnviNFe.getProtNFe().getInfProt().getCStat() + ": " + retEnviNFe.getProtNFe().getInfProt().getXMotivo()
                                + "\nNFC-e Número: " + nfeProc.getNFe().getInfNFe().getIde().getNNF()
                                + "\nNúmero Autorização: " + MaskFormatter.format(retEnviNFe.getProtNFe().getInfProt().getNProt(), "### ########## ##")
                                + "\nChave de Acesso: " + MaskFormatter.format(retEnviNFe.getProtNFe().getInfProt().getChNFe(), "#### #### #### #### #### #### #### #### #### #### ####")
                        );
                    }

                } catch (Exception ex) {
                    if (DEBUG) {
                        System.err.println(ex);
                    }
                }

            }).start();

            return null;
        }

        if (retEnviNFe.getCStat().equals(Constants.FALHA_LOTE) || retEnviNFe.getCStat().equals(Constants.FALHA_XML)) {
            Clipboard clip = Toolkit.getDefaultToolkit().getSystemClipboard();
            clip.setContents(new StringSelection(xml), null);
        }

        return retEnviNFe;
    }

    public static TRetInutNFe enviarInutNFe(int nnfIni, int nnfFim) throws Exception {

        if (!REGISTRADO) {
            SEFAZConnection.registrarConexao(DEBUG);
        }

        TInutNFe.InfInut infInut = new TInutNFe.InfInut();
        infInut.setCNPJ(Empresa.getCnpj());
        infInut.setCUF(String.valueOf(Empresa.getUfIBGE()));
        infInut.setMod("65");
        infInut.setSerie(String.valueOf(Empresa.getSerie()));
        infInut.setNNFIni(String.valueOf(nnfIni));
        infInut.setNNFFin(String.valueOf(nnfFim));
        infInut.setTpAmb(Config.get("nfe.amb"));
        infInut.setXJust("Falha no sistema em gerar identificador causou a quebra de sequência.");
        infInut.setXServ("INUTILIZAR");

        TInutNFe inutNFe = new TInutNFe();
        inutNFe.setInfInut(infInut);

        inutNFe.setVersao(Constants.VERSAO_NFE);

        Calendar dt = Utils.dateFromServer();

        inutNFe.getInfInut().setAno(String.valueOf(dt.get(Calendar.YEAR) - 2000));

        String id = "ID";
        id += Utils.leftPad2(inutNFe.getInfInut().getCUF(), 2, '0');
        id += dt.get(Calendar.YEAR) - 2000;
        id += inutNFe.getInfInut().getCNPJ();
        id += inutNFe.getInfInut().getMod();
        id += Utils.leftPad2(inutNFe.getInfInut().getSerie(), 3, '0');
        id += Utils.leftPad2(inutNFe.getInfInut().getNNFIni(), 9, '0');
        id += Utils.leftPad2(inutNFe.getInfInut().getNNFFin(), 9, '0');

        inutNFe.getInfInut().setId(id);

        Document doc = Signature.inutNFe(inutNFe);

        String xml = Utils.doc2Str(doc);

        URL url = new URL(inutNFe.getInfInut().getTpAmb().equals("1") ? Constants.URL_PROD_INUTILIZACAO : Constants.URL_HOMO_INUTILIZACAO);

        if (DEBUG) {
            System.out.println("\nAguardando resposta SEFAZ...\n\n" + xml);
        }

        SOAPMessage response = sendXML(xml, Constants.XMLNS_INUTILIZACAO, url);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        response.writeTo(out);

        int begin = out.toString().indexOf("<nfeResultMsg xmlns=\"" + Constants.XMLNS_INUTILIZACAO + "\">");
        int end = out.toString().indexOf("</nfeResultMsg>");

        String result = null;

        if (begin > -1 && end > begin) {
            result = out.toString().substring(begin, end).replace("<nfeResultMsg xmlns=\"" + Constants.XMLNS_INUTILIZACAO + "\">", "");
        }

        if (DEBUG) {
            System.out.println("\nResposta SEFAZ: " + result);
        }

        TRetInutNFe retInutNFe = Utils.unmarshaller(TRetInutNFe.class, result);

        if (retInutNFe.getInfInut().getCStat().equals(Constants.INUTILIZACAO_HOMOLOGADO)) {

            new Thread(() -> {

                try {
                    // Converte para Inutilização processado
                    TProcInutNFe procInutNFe = new TProcInutNFe();
                    procInutNFe.setInutNFe(inutNFe);
                    procInutNFe.setRetInutNFe(retInutNFe);
                    procInutNFe.setVersao(inutNFe.getVersao());

                    // Salva o XML
                    String xmlProc = Utils.marshaller(procInutNFe);
                    String xmlPath = NFCe.getProcNFePath(dt).getAbsolutePath() + File.separator + retInutNFe.getInfInut().getNProt() + "_v" + procInutNFe.getVersao() + "-procInutNFe.xml";

                    Utils.writeFile(xmlPath, xmlProc);

                    // Salva no banco de dados
                    Controller.saveInutilizacao(retInutNFe, xmlProc, xmlPath);

                } catch (Exception ex) {
                    if (DEBUG) {
                        System.err.println(ex);
                    }
                }
            }).start();
        }

        return retInutNFe;
    }

    public static TRetEnvEvento enviarEventoCancelamento(String chaveAcesso, String justificativa) throws Exception {

        if (!REGISTRADO) {
            SEFAZConnection.registrarConexao(DEBUG);
        }

        String id = "ID110111" + chaveAcesso.replace("NFe", "").trim() + "01";
        String cnpj = Empresa.getCnpj();
        String codigoUF = String.valueOf(Empresa.getUfIBGE());
        String dhEvento = Utils.date2NFe();
        Long idLote = Controller.getIdLoteEvento();
        Long nAut = Controller.getNumeroAutorizacaoNFCe(chaveAcesso);

        TEvento.InfEvento.DetEvento detEvento = new TEvento.InfEvento.DetEvento();
        detEvento.setVersao(Constants.VERSAO_EVENTO);
        detEvento.setNProt(nAut.toString());
        detEvento.setDescEvento("Cancelamento");
        detEvento.setXJust(justificativa.trim());

        TEvento.InfEvento infEvento = new TEvento.InfEvento();
        infEvento.setVerEvento(Constants.VERSAO_EVENTO);
        infEvento.setId(id);
        infEvento.setDhEvento(dhEvento);
        infEvento.setChNFe(chaveAcesso.replace("NFe", "").trim());
        infEvento.setCNPJ(cnpj.trim());
        infEvento.setCOrgao(codigoUF.trim());
        infEvento.setTpAmb(Config.get("nfe.amb"));
        infEvento.setTpEvento("110111");
        infEvento.setDetEvento(detEvento);
        infEvento.setNSeqEvento("1");

        TEvento evento = new TEvento();
        evento.setVersao(Constants.VERSAO_EVENTO);
        evento.setInfEvento(infEvento);

        TEnvEvento envEvento = new TEnvEvento();
        envEvento.setIdLote(idLote.toString());
        envEvento.getEvento().add(evento);
        envEvento.setVersao(Constants.VERSAO_EVENTO);

        Document document = Signature.evento(envEvento);

        if (DEBUG) {
            System.out.println("\nXML Assinado");
        }

        // String XML
        String xml = Utils.doc2Str(document);

        URL url = new URL(envEvento.getEvento().get(0).getInfEvento().getTpAmb().equals("1") ? Constants.URL_PROD_RECEPCAO_EVENTO : Constants.URL_HOMO_RECEPCAO_EVENTO);

        if (DEBUG) {
            System.out.println("\nAguardando resposta SEFAZ...\n\n" + xml);
        }

        SOAPMessage response = sendXML(xml, Constants.XMLNS_EVENTO, url);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        response.writeTo(out);

        int begin = out.toString().indexOf("<nfeResultMsg xmlns=\"" + Constants.XMLNS_EVENTO + "\">");
        int end = out.toString().indexOf("</nfeResultMsg>");

        String result = null;

        if (begin > -1 && end > begin) {
            result = out.toString().substring(begin, end).replace("<nfeResultMsg xmlns=\"" + Constants.XMLNS_EVENTO + "\">", "");
        }

        if (DEBUG) {
            System.out.println("\nResposta SEFAZ: " + result);
        }

        TRetEnvEvento retEnvEvento = Utils.unmarshaller(TRetEnvEvento.class, result);

        if (retEnvEvento.getCStat().equals(Constants.LOTE_EVENTO_PROCESSADO) && retEnvEvento.getRetEvento().get(0).getInfEvento().getCStat().equals(Constants.EVENTO_REGISTRADO_VINCULADO)) {

            new Thread(() -> {

                try {

                    // Converte para Evento propcessado
                    TProcEvento procEvento = new TProcEvento();
                    procEvento.setVersao(envEvento.getVersao());
                    procEvento.setEvento(envEvento.getEvento().get(0));
                    procEvento.setRetEvento(retEnvEvento.getRetEvento().get(0));

                    // Salva o XML
                    String xmlProc = Utils.marshaller(procEvento);
                    String xmlPath = NFCe.getProcNFePath(Utils.dateNFe2Date(dhEvento)).getAbsolutePath() + File.separator + procEvento.getEvento().getInfEvento().getChNFe() + "_" + procEvento.getEvento().getInfEvento().getTpEvento() + "-procEventoNFe.xml";

                    Utils.writeFile(xmlPath, xmlProc);

                    // Salva no banco de dados
                    Controller.saveEvento(idLote, procEvento, xmlProc, xmlPath);

                } catch (Exception ex) {
                    if (DEBUG) {
                        System.err.println(ex);
                    }
                }
            }).start();
        }

        return retEnvEvento;
    }

    private static SOAPMessage sendXML(String xml, String xmlns, URL url) throws Exception {

        StringBuilder soap = new StringBuilder();

        soap.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
        soap.append("<soap12:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap12=\"http://www.w3.org/2003/05/soap-envelope\">");
        soap.append("<soap12:Body>");
        soap.append("<nfeDadosMsg xmlns=\"").append(xmlns).append("\">");
        soap.append(xml);
        soap.append("</nfeDadosMsg>");
        soap.append("</soap12:Body>");
        soap.append("</soap12:Envelope>");

        MessageFactory messageFactory = MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL);

        MimeHeaders header = new MimeHeaders();
        header.addHeader("Content-Type", "application/soap+xml; charset=utf-8");

        SOAPMessage request = messageFactory.createMessage(header, new ByteArrayInputStream(soap.toString().getBytes()));

        SOAPConnection connection = SOAPConnectionFactory.newInstance().createConnection();

        return connection.call(request, url);
    }

    public static String decodeGZipToXml(final String conteudoEncode) throws Exception {

        if (conteudoEncode == null || conteudoEncode.length() == 0) {
            return "";
        }
        //final byte[] conteudo = Base64.getDecoder().decode(conteudoEncode);//java 8

        final byte[] conteudo = DatatypeConverter.parseBase64Binary(conteudoEncode);//java 7

        try (GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(conteudo))) {

            try (BufferedReader bf = new BufferedReader(new InputStreamReader(gis, StandardCharsets.UTF_8))) {

                StringBuilder outStr = new StringBuilder();

                String line;
                while ((line = bf.readLine()) != null) {
                    outStr.append(line);
                }

                return outStr.toString();
            }
        }
    }
}
