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.dto.ereporting.RejectResponse;
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 org.apache.log4j.Logger;

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.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;

// Retry job za MER /reject – kondenzirani log: REJECT_REQ, REJECT_ERR (UPSERT), REJECT_OK.
public class RejectRetryJob implements Runnable {

    private static final Logger logger = Logger.getLogger(RejectRetryJob.class);
    private static final Gson gson = new Gson();

    private static final String OPKEY = "REJECT"; // per-endpoint ključ za guard/circuit

    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 AtomicLong runSeq = new AtomicLong(0);
    private final PorukaDao porukaDao = new PorukaDao();

    public RejectRetryJob(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 (po 1 red/EID): prioritet ERR → 404 → REQ; isključeni oni s REJECT_OK
    private static final String SQL_FIND_CANDIDATES =
        "SELECT l.electronic_id AS eid, l.detalji AS detalji_json " +
        "  FROM eracun_dokument_log l " +
        "  JOIN eracun_dokument d ON d.electronic_id = l.electronic_id " +
        " WHERE d.izlazni = 1 " +
        "   AND (d.odbijen_status IS NULL OR d.odbijen_status <> 0) " + // 0 = SUCCESS
        "   AND l.akcija IN ('REJECT_REQ','REJECT_ERR','REJECT_404') " +
        "   AND NOT EXISTS ( " +
        "         SELECT 1 FROM eracun_dokument_log ok " +
        "          WHERE ok.electronic_id = l.electronic_id " +
        "            AND ok.akcija = 'REJECT_OK' " +
        "       ) " +
        "   AND l.id = ( " + // 1 red po EID (prioritet akcije, pa vrijeme, pa id)
        "         SELECT l3.id FROM eracun_dokument_log l3 " +
        "          WHERE l3.electronic_id = l.electronic_id " +
        "            AND l3.akcija IN ('REJECT_REQ','REJECT_ERR','REJECT_404') " +
        "          ORDER BY FIELD(l3.akcija,'REJECT_ERR','REJECT_404','REJECT_REQ') DESC, " +
        "                   l3.datum_promjene DESC, l3.id DESC " +
        "          LIMIT 1 " +
        "       ) " +
        " ORDER BY l.datum_promjene DESC " +
        " LIMIT ?";

    private static final class Candidate {
        long eid;
        String detailsJson;
    }

    @Override
    public void run() {
        final long runNo = runSeq.incrementAndGet();
        int ok = 0, notFound = 0, failed = 0;
        long t0 = System.currentTimeMillis();

        // Pre-guard: preskoči cijeli ciklus ako je circuit OPEN za REJECT
        if (!MerAvailabilityGate.allow(OPKEY)) {
            logger.warn("RejectRetryJob#" + runNo + " – gate OPEN za " + OPKEY + " → preskačem ovaj ciklus.");
            Connection c = null;
            try {
                c = connectionSupplier.get();
                ensureOutageOpen(c, "{\"provider\":\"MER\",\"op\":\"REJECT\",\"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("RejectRetryJob#" + runNo + " START");
        Connection conn = null;
        try {
            conn = connectionSupplier.get();
            conn.setAutoCommit(false);

            List<Candidate> items = loadCandidates(conn, batchSize);
            logger.info("RejectRetryJob#" + runNo + " candidates=" + items.size());
            if (items.isEmpty()) {
                if (conn != null) conn.rollback();
                // prazan ciklus → relaksiraj circuit i ukloni sticky poruku
                MerAvailabilityGate.onSuccess(OPKEY);
                resolveOutage(conn);
                return;
            }

            for (Candidate c : items) {
                try {
                    // --- izvuci parametre iz zadnjeg REJECT_* loga ---
                    String dateIso = LocalDate.now().toString();
                    String reasonType = null;
                    String desc = null;

                    if (c.detailsJson != null && !c.detailsJson.trim().isEmpty()) {
                        try {
                            JsonObject root = gson.fromJson(c.detailsJson, JsonObject.class);
                            JsonObject s = (root != null && root.has("sadrzaj") && root.get("sadrzaj").isJsonObject())
                                    ? root.getAsJsonObject("sadrzaj") : null;
                            if (s != null) {
                                if (s.has("rejectionDate") && !s.get("rejectionDate").isJsonNull())
                                    dateIso = s.get("rejectionDate").getAsString();
                                if (s.has("rejectionReasonType") && !s.get("rejectionReasonType").isJsonNull())
                                    reasonType = s.get("rejectionReasonType").getAsString();
                                if (s.has("rejectionDescription") && !s.get("rejectionDescription").isJsonNull())
                                    desc = s.get("rejectionDescription").getAsString();
                            }
                        } catch (Exception jex) {
                            logger.warn("RejectRetryJob#" + runNo + " JSON parse failed for eid=" + c.eid, jex);
                        }
                    }

                    if (reasonType == null || reasonType.trim().isEmpty()) {
                        failed++;
                        upsertRetryLog(conn, c.eid, "MER", "REJECT_ERR",
                                detailsJson(dateIso, "ReasonType missing in log", desc, null));
                        continue;
                    }

                    RejectResponse resp;
                    try {
                        resp = merClientSupplier.get().reject(c.eid, dateIso, reasonType, desc);
                    } catch (EracunServiceUnavailableException ex) {
                        // SOFT-FAIL: mreža/5xx → kondenzirani log + circuit open + sticky poruka
                        failed++;
                        upsertRetryLog(conn, c.eid, "MER", "REJECT_ERR",
                                detailsJson(dateIso, reasonType, desc, ex));
                        logger.warn(new Functions().logging(ex));
                        MerAvailabilityGate.onFailure(OPKEY);
                        ensureOutageOpen(conn, "{\"provider\":\"MER\",\"op\":\"REJECT\",\"reason\":\"unavailable\"}");
                        try { conn.rollback(); } catch (Exception ignore) {}
                        continue;
                    }

                    // 404 → nema resursa (MER vratio null) → zabilježi kondenzirano pa dalje
                    if (resp == null) {
                        notFound++;
                        upsertRetryLog(conn, c.eid, "MER", "REJECT_404",
                                detailsJson(dateIso, reasonType, desc, null));
                        // 404 nije availability problem → circuit ne diramo
                        continue;
                    }

                    // Uspjeh → obradi i relaksiraj circuit
                    syncService.processRejectResponse(conn, c.eid, resp);
                    ok++;
                    MerAvailabilityGate.onSuccess(OPKEY);
                    resolveOutage(conn);

                } catch (Exception ex) {
                    // Ostalo – tretiraj kao grešku pokušaja (ne availability), zapiši kondenzirani log
                    logger.error(new Functions().logging(ex));
                    failed++;
                    upsertRetryLog(conn, c.eid, "MER", "REJECT_ERR",
                            detailsJson(null, null, null, ex));
                    // circuit ne diramo (nije connectivity)
                }
            }

            if (conn != null) conn.commit();
        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
            try { if (conn != null) conn.rollback(); } catch (Exception ignore) {}
        } finally {
            try { if (conn != null) conn.close(); } catch (Exception ignore) {}
            long dt = System.currentTimeMillis() - t0;
            logger.info("RejectRetryJob#" + 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)) {
            ps.setInt(1, limit);
            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {
                    Candidate c = new Candidate();
                    c.eid = rs.getLong("eid");
                    c.detailsJson = rs.getString("detalji_json");
                    list.add(c);
                }
            }
        }
        return list;
    }

    private JsonObject detailsJson(String dateIso, String reasonType, String desc, Exception ex) {
        JsonObject root = new JsonObject();
        root.addProperty("shema_verzija", 1);
        root.addProperty("operacija", "REJECT");
        root.addProperty("status_id", -1);

        JsonObject s = new JsonObject();
        if (dateIso != null)      s.addProperty("rejectionDate", dateIso);
        if (reasonType != null)   s.addProperty("rejectionReasonType", reasonType);
        if (desc != null)         s.addProperty("rejectionDescription", desc);
        root.add("sadrzaj", s);

        if (ex != null) root.addProperty("greska", ex.getClass().getSimpleName() + ": " + ex.getMessage());
        return root;
    }

    // UPSERT (INSERT-SELECT) za kondenzirane retry logove (REJECT_ERR/404)
	// Jedinstvenost: ux_retry_eid_izvor_akcija (electronic_id, izvor, akcija)
	// Ako već postoji zapis, samo osvježi datum_promjene i detalji.
	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
			"  NULL, NULL, 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);
            m.setPrikazatiOd(new Timestamp(System.currentTimeMillis()));
            m.setVrijediDo(null);
            m.setNaslov("Moj-eRačun nedostupan");
            m.setTekst("Operacija „Odbij (reject)” 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));
        }
    }
}
