package hr.com.port.ips.eracun.scheduler;

import org.apache.log4j.Logger;
import com.google.gson.JsonObject;

import hr.com.port.functions.Functions;
import hr.com.port.eracun.codelists.NacinPlacanjaCode;
import hr.com.port.ips.eracun.provider.mer.MerClient;
import hr.com.port.ips.eracun.provider.mer.dto.ereporting.MarkPaidResponse;
import hr.com.port.ips.eracun.resilience.mer.MerAvailabilityGate;
import hr.com.port.ips.eracun.service.EracunServiceUnavailableException;
import hr.com.port.ips.eracun.service.EracunSyncService;
import hr.com.port.ips.poruke.PorukaDao;
import hr.com.port.ips.poruke.PorukaModel;

import java.sql.*;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;

public class MarkPaidRetryJob implements Runnable {

    private final java.util.concurrent.atomic.AtomicLong runSeq = new java.util.concurrent.atomic.AtomicLong(0);

    private static final Logger logger = Logger.getLogger(MarkPaidRetryJob.class);
    private static final String OPKEY = "MARK_PAID"; // per-endpoint ključ za circuit/guard
    private static final String PROVIDER = "MER";

    private final Supplier<MerClient> merClientSupplier;
    private final Supplier<Connection> connectionSupplier;
    private final EracunSyncService syncService;
    private final int batchSize;

    private final PorukaDao porukaDao = new PorukaDao();

    public MarkPaidRetryJob(Supplier<MerClient> merClientSupplier,
                            Supplier<Connection> connectionSupplier,
                            EracunSyncService syncService,
                            int batchSize) {
        this.merClientSupplier = merClientSupplier;
        this.connectionSupplier = connectionSupplier;
        this.syncService = syncService;
        this.batchSize = (batchSize > 0 ? batchSize : 100);
    }

    // Kandidati + zadnje plaćanje (MAX(p.id))
    private static final String SQL_FIND_CANDIDATES_WITH_LAST_PAYMENT =
        "SELECT d.electronic_id AS eid, p.datum AS payment_datum, p.iznos AS payment_iznos, p.nacin_placanja AS nacin_id " +
        "  FROM eracun_dokument d " +
        "  JOIN placanja p ON p.onu = d.onu " +
        "                 AND p.dokument = d.vrsta_dokumenta " +
        "                 AND p.broj = d.broj " +
        "                 AND p.godina = d.godina " +
        "                 AND p.id = (SELECT MAX(pp.id) FROM placanja pp " +
        "                              WHERE pp.onu = d.onu AND pp.dokument = d.vrsta_dokumenta " +
        "                                AND pp.broj = d.broj AND pp.godina = d.godina) " +
        " WHERE (d.placen_status IS NULL OR d.placen_status <> 0) " +   // svi koji nisu SUCCESS
        "   AND d.izlazni = 1 " +
        " ORDER BY d.datum_kreiranja DESC " +
        " LIMIT ?";

    @Override
    public void run() {
        int ok = 0, notFound = 0, failed = 0;
        long runNo = runSeq.incrementAndGet();
        long t0 = System.currentTimeMillis();

        // Pre-guard: preskoči cijeli ciklus ako je circuit OPEN za MARK_PAID – i otvori sticky poruku
        if (!MerAvailabilityGate.allow(OPKEY)) {
            logger.warn("MarkPaidRetryJob#" + runNo + " – gate OPEN za " + OPKEY + " → preskačem ovaj ciklus.");
            Connection c = null;
            try {
                c = connectionSupplier.get();
                ensureOutageOpen(c, "{\"provider\":\"MER\",\"op\":\"MARK_PAID\",\"reason\":\"circuit_open\"}");
                c.commit();
            } catch (Exception ex) {
                logger.warn(new Functions().logging(ex));
                try { if (c != null) c.rollback(); } catch (Exception ignore) {}
            } finally {
                try { if (c != null) c.close(); } catch (Exception ignore) {}
            }
            return;
        }

        logger.info("MarkPaidRetryJob#" + runNo + " START");

        try (Connection conn = connectionSupplier.get()) {
            conn.setAutoCommit(false);

            List<Candidate> candidates = loadCandidates(conn, batchSize);
            logger.info("MarkPaidRetryJob#" + runNo + " candidates=" + candidates.size());
            if (candidates.isEmpty()) {
                // prazan ciklus → relaksiraj circuit i ukloni sticky poruku (ako postoji)
                MerAvailabilityGate.onSuccess(OPKEY);
                resolveOutage(conn);
                conn.rollback(); // ništa nije mijenjano
                return;
            }

            for (Candidate c : candidates) {
                try {
                    String paymentDateIso = toIsoDate(c.paymentDatum); // "yyyy-MM-dd"
                    Double amount = (c.paymentIznos != null) ? c.paymentIznos.doubleValue() : null;
                    String paymentMethod = mapPaymentMethod(c.nacinPlacanjaId); // NacinPlacanjaCode.fiskalOznaka

                    logger.debug("MarkPaidRetryJob#" + runNo + " try eid=" + c.eid +
                            " date=" + c.paymentDatum + " amount=" + c.paymentIznos +
                            " methodId=" + c.nacinPlacanjaId);

                    // MER poziv
                    MarkPaidResponse resp;
                    try { 
                        resp = merClientSupplier.get().markPaid(c.eid, paymentDateIso, amount, paymentMethod);
                    } catch (EracunServiceUnavailableException ex) {
                        // SOFT-FAIL (mreža/5xx/404-empty/gate) → kondenzirani log + circuit open + sticky poruka
                        failed++;
                        //logger.warn(new Functions().logging(ex));
                        MerAvailabilityGate.onFailure(OPKEY);

                        // zapiši retry log i sticky poruku (bez rollbacka – nema promjena prije ovoga)
                        upsertRetryLog(conn, c.eid, "MER", "PAID_ERR",
                                detailsJson(paymentDateIso, amount, paymentMethod, ex));
                        ensureOutageOpen(conn, "{\"provider\":\"MER\",\"op\":\"MARK_PAID\",\"reason\":\"unavailable\"}");
                        continue;
                    }

                    // “normalni” 404 (MerClient vraća null) → kondenzirani log, bez diranja circuit-a
                    if (resp == null) {
                        notFound++;
                        upsertRetryLog(conn, c.eid, "MER", "PAID_404",
                                detailsJson(paymentDateIso, amount, paymentMethod, null));
                        continue;
                    }

                    // Uspjeh → obradi, relaksiraj circuit i ukloni sticky poruku
                    syncService.processMarkPaidResponse(conn, c.eid, resp);
                    ok++;
                    MerAvailabilityGate.onSuccess(OPKEY);
                    resolveOutage(conn);

                } catch (Exception ex) {
                    // Ostalo → “tvrda” greška pokušaja (npr. 4xx → IOException): kondenzirani log
                    logger.error(new Functions().logging(ex));
                    failed++;
                    upsertRetryLog(conn, c.eid, "MER", "PAID_ERR",
                            detailsJson(null, null, null, ex));
                }
            }

            long ms = System.currentTimeMillis() - t0;
            logger.info("MarkPaidRetryJob#" + runNo + " DONE in " + ms + " ms"
                    + " | ok=" + ok + " 404=" + notFound + " fail=" + failed);
            conn.commit();
        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
        }
    }

    private static class Candidate {
        long eid;
        Timestamp paymentDatum;
        java.math.BigDecimal paymentIznos;
        int nacinPlacanjaId;
    }

    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_PAYMENT)) {
            ps.setInt(1, limit);
            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {
                    Candidate c = new Candidate();
                    c.eid = rs.getLong("eid");
                    c.paymentDatum = rs.getTimestamp("payment_datum");
                    c.paymentIznos = rs.getBigDecimal("payment_iznos");
                    c.nacinPlacanjaId = rs.getInt("nacin_id");
                    list.add(c);
                }
            }
        }
        return list;
    }

    private String toIsoDate(Timestamp ts) {
        if (ts == null) return null;
        LocalDate ld = ts.toLocalDateTime().toLocalDate();
        return ld.toString(); // ISO-8601 "yyyy-MM-dd"
    }

    private String mapPaymentMethod(int nacinPlacanjaId) {
        try {
            NacinPlacanjaCode e = NacinPlacanjaCode.fromId(nacinPlacanjaId);
            return (e != null) ? e.getFiskalOznaka() : null;
        } catch (Throwable t) {
            logger.warn("NacinPlacanjaCode mapping failed for id=" + nacinPlacanjaId, t);
            return null;
        }
    }

    private JsonObject detailsJson(String paymentDateIso, Double amount, String method, Exception ex) {
        JsonObject root = new JsonObject();
        root.addProperty("shema_verzija", 1);
        root.addProperty("operacija", "MARK_PAID");
        root.addProperty("status_id", -1);

        JsonObject s = new JsonObject();
        if (paymentDateIso != null) s.addProperty("paymentDate", paymentDateIso);
        if (amount != null)         s.addProperty("amount", amount.doubleValue());
        if (method != null)         s.addProperty("paymentMethod", method);
        root.add("sadrzaj", s);

        if (ex != null) root.addProperty("greska", ex.getClass().getSimpleName() + ": " + ex.getMessage());
        return root;
    }

    // UPSERT (INSERT-SELECT) za kondenzirane retry logove (PAID_ERR, PAID_404)
    private void upsertRetryLog(Connection conn,
                                long electronicId,
                                String izvor,
                                String akcija,
                                JsonObject detaljiJson) throws SQLException {
        final String sql =
            "INSERT INTO eracun_dokument_log (" +
            "  izlazni, godina, posrednik, vrsta_dokumenta, onu, opp, broj, " +
            "  electronic_id, " +
            "  lokalni_status_id, lokalni_status_naziv, " +
            "  transportni_status_id, transportni_status_naziv, " +
            "  procesni_status_id, procesni_status_naziv, " +
            "  datum_statusa, datum_promjene, " +
            "  poruka_greske, opis, putanja_xml, izvor, akcija, oib_operatera, detalji" +
            ") " +
            "SELECT " +
            "  d.izlazni, d.godina, d.posrednik, d.vrsta_dokumenta, d.onu, d.opp, d.broj, " +
            "  d.electronic_id, " +
            "  d.lokalni_status_id, d.lokalni_status_naziv, " +
            "  d.transportni_status_id, d.transportni_status_naziv, " +
            "  d.procesni_status_id, d.procesni_status_naziv, " +
            "  d.datum_zadnjeg_statusa, ?, " + // datum_promjene = now
            "  d.poruka_greske, 'opis' as opis, d.putanja_xml, ?, ?, null as oib_operatera, ? " + // izvor, akcija, detalji
            "FROM eracun_dokument d " +
            "WHERE d.electronic_id = ? " +
            "ON DUPLICATE KEY UPDATE " +
            "  datum_promjene = VALUES(datum_promjene), " +
            "  detalji        = VALUES(detalji)";

        try (PreparedStatement ps = conn.prepareStatement(sql)) {
            int i = 1;
            ps.setTimestamp(i++, new Timestamp(System.currentTimeMillis())); // datum_promjene
            ps.setString(i++, izvor);
            ps.setString(i++, akcija);
            ps.setString(i++, (detaljiJson != null ? detaljiJson.toString() : null)); // detalji JSON
            ps.setLong(i++, electronicId);
            ps.executeUpdate();
        }
    }

    // --- 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");
            m.setAplikacijaId(0);
            m.setModulId(0);
            m.setPrioritet(100);
            m.setNacinIsporuke(1); // popup/log
            m.setPrikazatiOd(new Timestamp(System.currentTimeMillis()));
            m.setVrijediDo(null);
            m.setNaslov("Moj-eRačun nedostupan");
            m.setTekst("Operacija „Označi kao plaćeno (markPaid)” 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));
        }
    }
}