package hr.com.port.ips.eracun.scheduler;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import hr.com.port.functions.Functions;
import hr.com.port.ips.eracun.provider.mer.MerClient;
import hr.com.port.ips.eracun.provider.mer.enums.MerEreportingInvoiceType;
import hr.com.port.ips.eracun.provider.mer.dto.ereporting.EreportingResponse;
import hr.com.port.ips.eracun.service.MerEreportingService;
import hr.com.port.ips.eracun.dao.MerEreportingDao;
import hr.com.port.ips.eracun.dao.MerEreportingLogDao;
import hr.com.port.ips.eracun.resilience.mer.MerAvailabilityGate;
import org.apache.log4j.Logger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.*;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
import hr.com.port.ips.poruke.PorukaDao;
import hr.com.port.ips.poruke.PorukaModel;
import hr.com.port.ips.eracun.service.EracunServiceUnavailableException;

/**
 * Retry job za eReporting (prijava isporuka bez eRačuna).
 * Kandidati se biraju po zadnjem EREPORTING logu (REQ/ERR/404/legacy EREPORTING)
 * i po statusu u glavnoj tablici (≠ SUCCESS).
 */
public class EreportingRetryJob implements Runnable {

    private static final Logger logger = Logger.getLogger(EreportingRetryJob.class);
    private static final Gson gson = new Gson();

    /** per-endpoint ključ za circuit/guard */
    private static final String OPKEY = "EREPORTING";
    private static final String PROVIDER = "MER";

    private final PorukaDao porukaDao = new PorukaDao();

    private final Supplier<MerClient> merClientSupplier;
    private final Supplier<Connection> connectionSupplier;
    private final int batchSize;

    private final AtomicLong runSeq = new AtomicLong(0);

    public EreportingRetryJob(Supplier<MerClient> merClientSupplier,
                              Supplier<Connection> connectionSupplier,
                              int batchSize) {
        this.merClientSupplier = merClientSupplier;
        this.connectionSupplier = connectionSupplier;
        this.batchSize = batchSize > 0 ? batchSize : 100;
    }

    // Kandidati: po 1 red/poslovni ključ (zadnji log), isključi one koji već imaju EREPORTING_OK
    private static final String SQL_FIND_CANDIDATES_WITH_LAST_LOG =
        "SELECT l.vrsta_dokumenta, l.godina, l.opp, l.onu, l.broj, l.detalji AS detalji_json " +
        "  FROM eracun_ereporting_log l " +
        "  JOIN eracun_ereporting e ON e.vrsta_dokumenta = l.vrsta_dokumenta " +
        "                           AND e.godina = l.godina " +
        "                           AND e.opp = l.opp " +
        "                           AND e.onu = l.onu " +
        "                           AND e.broj = l.broj " +
        " WHERE (e.status_id IS NULL OR e.status_id <> 0) " + // 0 = SUCCESS
        "   AND l.akcija IN ('EREPORTING_REQ','EREPORTING_ERR','EREPORTING_404','EREPORTING') " +
        "   AND NOT EXISTS ( " + // isključi već završene
        "         SELECT 1 FROM eracun_ereporting_log ok " +
        "          WHERE ok.vrsta_dokumenta = l.vrsta_dokumenta AND ok.godina = l.godina " +
        "            AND ok.opp = l.opp AND ok.onu = l.onu AND ok.broj = l.broj " +
        "            AND ok.akcija = 'EREPORTING_OK' " +
        "       ) " +
        "   AND l.id = ( " + // točno zadnji log po ključu
        "         SELECT l3.id FROM eracun_ereporting_log l3 " +
        "          WHERE l3.vrsta_dokumenta = l.vrsta_dokumenta AND l3.godina = l.godina " +
        "            AND l3.opp = l.opp AND l3.onu = l.onu AND l3.broj = l.broj " +
        "            AND l3.akcija IN ('EREPORTING_REQ','EREPORTING_ERR','EREPORTING_404','EREPORTING') " +
        "          ORDER BY l3.datum_promjene DESC, l3.id DESC " +
        "          LIMIT 1 " +
        "       ) " +
        " ORDER BY l.datum_promjene DESC " +
        " LIMIT ?";

    private static final class Candidate {
        int vrstaDokumenta;
        int godina;
        int opp;
        int onu;
        int broj;
        String detailsJson;
    }

    @Override
    public void run() {
        final long runNo = runSeq.incrementAndGet();
        int ok = 0, failed = 0, notFound = 0;
        long t0 = System.currentTimeMillis();

        // Pre-guard: preskoči ciklus ako je circuit OPEN za EREPORTING
        if (!MerAvailabilityGate.allow(OPKEY)) {
            try (Connection conn = connectionSupplier.get()) {
                ensureOutageOpen(conn, "{\"provider\":\"MER\",\"op\":\"EREPORTING\",\"reason\":\"circuit_open\"}");
            } catch (Exception ex) {
                logger.warn(new Functions().logging(ex));
            }
            logger.warn("EreportingRetryJob#" + runNo + " – gate OPEN za " + OPKEY + " → preskačem ovaj ciklus.");
            return;
        }

        logger.info("EreportingRetryJob#" + runNo + " START");
        try (Connection conn = connectionSupplier.get()) {
            conn.setAutoCommit(false);

            List<Candidate> items = loadCandidates(conn, batchSize);
            logger.info("EreportingRetryJob#" + runNo + " candidates=" + items.size());
            if (items.isEmpty()) {
                conn.rollback();
                // prazan ciklus → relaksiraj circuit
                MerAvailabilityGate.onSuccess(OPKEY);
                resolveOutage(conn);
                return;
            }

            for (Candidate c : items) {
                try {
                    // --- izvuci parametre iz zadnjeg EREPORTING_* loga ---
                    String dateIso = LocalDate.now().toString();
                    boolean isCopy = false;
                    String invoiceTypeCode = "IR";
                    String xmlPath = null;

                    if (c.detailsJson != null && !c.detailsJson.trim().isEmpty()) {
                        try {
                            JsonObject root = gson.fromJson(c.detailsJson, JsonObject.class);
                            if (root != null) {
                                if (root.has("deliveryDate") && !root.get("deliveryDate").isJsonNull()) {
                                    dateIso = root.get("deliveryDate").getAsString();
                                }
                                if (root.has("isCopy") && !root.get("isCopy").isJsonNull()) {
                                    isCopy = root.get("isCopy").getAsBoolean();
                                }
                                if (root.has("invoiceType") && !root.get("invoiceType").isJsonNull()) {
                                    invoiceTypeCode = root.get("invoiceType").getAsString();
                                }
                                if (root.has("xmlPath") && !root.get("xmlPath").isJsonNull()) {
                                    xmlPath = root.get("xmlPath").getAsString();
                                }
                            }
                        } catch (Exception jex) {
                            logger.warn("EreportingRetryJob#" + runNo + " JSON parse failed for " +
                                    keyStr(c), jex);
                        }
                    }

                    if (xmlPath == null || xmlPath.trim().isEmpty()) {
                        failed++;
                        logger.warn("EreportingRetryJob#" + runNo + " missing xmlPath for " + keyStr(c));
                        continue;
                    }

                    String xmlInvoice;
                    try {
                        byte[] bytes = Files.readAllBytes(Paths.get(xmlPath));
                        xmlInvoice = new String(bytes, StandardCharsets.UTF_8);
                    } catch (Exception ioex) {
                        failed++;
                        logger.warn("EreportingRetryJob#" + runNo + " cannot read xmlPath=" + xmlPath +
                                " for " + keyStr(c), ioex);
                        continue;
                    }

                    Date deliverySql = null;
                    try {
                        deliverySql = Date.valueOf(dateIso);
                    } catch (Exception ignore) {
                        // fallback is below
                    }

                    MerEreportingInvoiceType invType = MerEreportingInvoiceType.fromCode(invoiceTypeCode);
                    if (invType == null || invType == MerEreportingInvoiceType.UNKNOWN) {
                        invType = MerEreportingInvoiceType.IR;
                    }

                    EreportingResponse resp;
                    try {
                        resp = new MerEreportingService(merClientSupplier.get(), new MerEreportingDao(), new MerEreportingLogDao())
                                .send(
                                        conn,
                                        c.vrstaDokumenta, c.godina, c.opp, c.onu, c.broj,
                                        xmlPath,
                                        xmlInvoice,
                                        (deliverySql != null ? deliverySql : Date.valueOf(LocalDate.now())),
                                        isCopy,
                                        invType
                                );
                    } catch (EracunServiceUnavailableException ex) {
                        // SOFT-FAIL (mreža/5xx) → metrički failed + circuit open; log/status odrađuje servis
                        failed++;
                        logger.warn(new Functions().logging(ex));
                        MerAvailabilityGate.onFailure(OPKEY);
                        ensureOutageOpen(conn, "{\"provider\":\"MER\",\"op\":\"EREPORTING\",\"reason\":\"unavailable\"}");
                        continue;
                    }

                    boolean success = (resp != null && Boolean.TRUE.equals(resp.getIsSuccess()));
                    if (success) {
                        ok++;
                        // uspješan kontakt → relaksiraj circuit
                        MerAvailabilityGate.onSuccess(OPKEY);
                        resolveOutage(conn);
                    } else {
                        if (resp == null) {
                            // Semantika P: servis može koristiti null za 404/NOT FOUND ili “nije spremno”
                            notFound++;
                        } else {
                            failed++;
                        }
                    }

                } catch (Exception ex) {
                    // Ostalo – greška pokušaja (ne availability); log/status odrađuje servis
                    logger.error(new Functions().logging(ex));
                    failed++;
                }
            }

            conn.commit();
        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
        } finally {
            long dt = System.currentTimeMillis() - t0;
            logger.info("EreportingRetryJob#" + runNo + " END in " + dt + " ms"
                    + " | ok=" + ok + " notFound=" + notFound + " failed=" + failed);
        }
    }

    private List<Candidate> loadCandidates(Connection conn, int limit) throws SQLException {
        List<Candidate> list = new ArrayList<Candidate>();
        try (PreparedStatement ps = conn.prepareStatement(SQL_FIND_CANDIDATES_WITH_LAST_LOG)) {
            ps.setInt(1, limit);
            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {
                    Candidate c = new Candidate();
                    c.vrstaDokumenta = rs.getInt("vrsta_dokumenta");
                    c.godina = rs.getInt("godina");
                    c.opp = rs.getInt("opp");
                    c.onu = rs.getInt("onu");
                    c.broj = rs.getInt("broj");
                    c.detailsJson = rs.getString("detalji_json");
                    list.add(c);
                }
            }
        }
        return list;
    }

    // --- Helpers za lokalne "sticky" outage poruke ---

    private String outageCentralId() {
        return "LOCAL:OUTAGE:" + PROVIDER + ":" + OPKEY;
    }

    private void ensureOutageOpen(Connection conn, String reasonJson) {
        try {
            PorukaModel m = new PorukaModel();
            m.setCentralniId(outageCentralId());
            m.setCentralniHash("v1"); // stabilan hash → bez nepotrebnih update-a
            m.setAplikacijaId(0);
            m.setModulId(0);
            m.setPrioritet(100);
            m.setNacinIsporuke(1); // popup/log prema konfiguraciji
            m.setPrikazatiOd(new Timestamp(System.currentTimeMillis()));
            m.setVrijediDo(null);
            m.setNaslov("MojeRačun (MER) nedostupan");
            m.setTekst("Operacija „eReporting” trenutno nije dostupna. Sustav će pokušati ponovno automatski.");
            m.setPayloadJson(reasonJson);
            m.setStanje(0);
            m.setMozeObrisati(true);
            m.setCentralCreatedAt(new Timestamp(System.currentTimeMillis()));
            m.setPrimljenoAt(new Timestamp(System.currentTimeMillis()));
            porukaDao.upsertFromCentral(m, conn);
        } catch (Exception ex) {
            logger.warn(new Functions().logging(ex));
        }
    }

    private void resolveOutage(Connection conn) {
        try {
            porukaDao.deleteByCentralId(outageCentralId(), conn);
        } catch (Exception ex) {
            logger.warn(new Functions().logging(ex));
        }
    }

    private String keyStr(Candidate c) {
        return "[vrsta=" + c.vrstaDokumenta + ", god=" + c.godina + ", opp=" + c.opp +
               ", onu=" + c.onu + ", broj=" + c.broj + "]";
    }
}