package hr.com.port.ips.eracun.service;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import hr.com.port.functions.Functions;
import hr.com.port.ips.eracun.dao.MerEreportingDao;
import hr.com.port.ips.eracun.dao.MerEreportingLogDao;
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.enums.MerEreportingStatus;
import hr.com.port.ips.eracun.provider.mer.dto.ereporting.EreportingResponse;
import hr.com.port.ips.eracun.modeli.MerEreporting;
import hr.com.port.ips.eracun.resilience.mer.MerAvailabilityGate;
import hr.com.port.ips.eracun.resilience.mer.MerSoftFailContext;
import hr.com.port.ips.eracun.resilience.mer.MerTriageHelper;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.Timestamp;
import org.apache.log4j.Logger;

public class MerEreportingService {

    private static final Logger logger = Logger.getLogger(MerEreportingService.class);
    private static final Gson gson = new Gson();

    private final MerClient merClient;
    private final MerEreportingDao mainDao;
    private final MerEreportingLogDao logDao;

    public MerEreportingService(MerClient merClient,
                                MerEreportingDao mainDao,
                                MerEreportingLogDao logDao) {
        this.merClient = merClient;
        this.mainDao   = mainDao;
        this.logDao    = logDao;
    }

    // Upis/refresh reda u glavnoj tablici, poziv MER-a i ažuriranje statusa.
    // Log politika: REQ/OK append, ERR/404 UPSERT (kondenzirano).
    public EreportingResponse send(Connection conn,
                                   int vrstaDokumenta,
                                   int godina,
                                   int opp,
                                   int onu,
                                   int broj,
                                   String xmlPath,
                                   String xmlInvoice,
                                   Date deliveryDate,
                                   boolean isCopy,
                                   MerEreportingInvoiceType invoiceType) {
        final String key = "EREPORTING";

        // (1) Upis/refresh glavne tablice
        try {
            MerEreporting postojece = mainDao.findByBusinessKey(conn, vrstaDokumenta, godina, opp, onu, broj);
            Timestamp now = new Timestamp(System.currentTimeMillis());

            if (postojece == null) {
                MerEreporting m = new MerEreporting();
                m.setVrstaDokumenta(vrstaDokumenta);
                m.setGodina(godina);
                m.setOpp(opp);
                m.setOnu(onu);
                m.setBroj(broj);
                m.setDeliveryDate(deliveryDate);
                m.setIsCopy(isCopy);
                m.setInvoiceTypeCode(invoiceType != null ? invoiceType.getCode() : null);
                m.setStatusId(MerEreportingStatus.UNKNOWN.getId());
                m.setXmlPath(xmlPath);
                m.setCreatedAt(now);
                m.setUpdatedAt(null);
                mainDao.insert(conn, m);
            } else {
                mainDao.updateXmlAndParamsByBusinessKey(
                        conn, vrstaDokumenta, godina, opp, onu, broj,
                        xmlPath, deliveryDate, isCopy,
                        invoiceType != null ? invoiceType.getCode() : null
                );
            }
        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
            return buildError("Local persistence (insert/update) failed: " + safeMsg(ex));
        }

        // (1a) Append REQ log
        insertLog(conn, vrstaDokumenta, godina, opp, onu, broj,
                "LOCAL", "EREPORTING_REQ",
                MerEreportingStatus.UNKNOWN.getId(),
                null,
                toRequestJson(deliveryDate, isCopy, invoiceType, xmlPath));

        // (2) Circuit pre-guard
        if (!MerAvailabilityGate.allow(key)) {
            MerSoftFailContext.markSoftFail(
                    MerTriageHelper.Category.CONNECTIVITY, null, key,
                    "[EREPORTING] submit – circuit OPEN, preskačem poziv prema MER-u.", null);
            logger.warn("[EREPORTING] submit – gate OPEN → skip");
            return buildError("Circuit OPEN – skipping MER call for eReporting.");
        }

        // (3) Poziv prema MER-u
        EreportingResponse resp;
        try {
            final String deliveryIso = (deliveryDate != null ? deliveryDate.toString() : null);
            final String typeCode    = (invoiceType != null ? invoiceType.getCode() : null);

            resp = merClient.eReporting(xmlInvoice, deliveryIso, isCopy, typeCode);

        } catch (hr.com.port.ips.eracun.service.EracunServiceUnavailableException ex) {
            // SOFT-FAIL: mreža/5xx
            MerSoftFailContext.markSoftFail(
                    MerTriageHelper.Category.CONNECTIVITY, null, key,
                    "[EREPORTING] submit: SOFT-FAIL – " + safeMsg(ex), ex);
            MerAvailabilityGate.onFailure(key);
            logger.warn(new Functions().logging(ex));

            upsertLogJson(conn, vrstaDokumenta, godina, opp, onu, broj,
                    "MER", "EREPORTING_ERR",
                    MerEreportingStatus.ERROR.getId(),
                    safeMsg(ex),
                    toErrorJson(ex));
            return buildError(toErrorJson(ex));

        } catch (Exception ex) {
            // Tvrda greška (treat-as error; circuit se ne dira)
            logger.error(new Functions().logging(ex));
            upsertLogJson(conn, vrstaDokumenta, godina, opp, onu, broj,
                    "MER", "EREPORTING_ERR",
                    MerEreportingStatus.ERROR.getId(),
                    safeMsg(ex),
                    toErrorJson(ex));
            return buildError(toErrorJson(ex));
        }

        // (4) Ažuriraj status i logiraj ishod
        try {
            final int newStatusId = resolveStatusId(resp);
            final String st  = statusOf(resp);
            final String msg = messageOf(resp);

            if (resp == null) {
                // 404 / not found → kondenzirani log
                upsertLogJson(conn, vrstaDokumenta, godina, opp, onu, broj,
                        "MER", "EREPORTING_404",
                        MerEreportingStatus.ERROR.getId(),
                        "Resource not found (null response)",
                        null);
            } else if ("success".equalsIgnoreCase(st)) {
                // uspjeh → relax circuit + append OK log
                MerAvailabilityGate.onSuccess(key);
                insertLog(conn, vrstaDokumenta, godina, opp, onu, broj,
                        "MER", "EREPORTING_OK",
                        MerEreportingStatus.SUCCESS.getId(),
                        msg,
                        safeRespJson(resp));
            } else {
                // MER vratio error → kondenzirani ERR
                upsertLogJson(conn, vrstaDokumenta, godina, opp, onu, broj,
                        "MER", "EREPORTING_ERR",
                        MerEreportingStatus.ERROR.getId(),
                        msg,
                        safeRespJson(resp));
            }

            mainDao.updateStatusByBusinessKey(conn,
                    vrstaDokumenta, godina, opp, onu, broj,
                    Integer.valueOf(newStatusId));

        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
        }

        return (resp != null ? resp : buildError("Null response from MER."));
    }

    /* ================= helpers ================= */

    private int resolveStatusId(EreportingResponse resp) {
        final String st = statusOf(resp);
        if (st == null) return MerEreportingStatus.ERROR.getId();
        if ("success".equalsIgnoreCase(st)) return MerEreportingStatus.SUCCESS.getId();
        if ("error".equalsIgnoreCase(st))   return MerEreportingStatus.ERROR.getId();
        return MerEreportingStatus.UNKNOWN.getId();
    }

    /** Robustno dohvaća status iz v1.8 EreportingResponse (field ili metoda). */
    private static String statusOf(EreportingResponse r) {
        if (r == null) return null;
        try {
            // metoda getStatus()
            try { return (String) r.getClass().getMethod("getStatus").invoke(r); } catch (NoSuchMethodException ignore) {}
            // metoda status()
            try { return (String) r.getClass().getMethod("status").invoke(r); } catch (NoSuchMethodException ignore) {}
            // public field status
            try {
                java.lang.reflect.Field f = r.getClass().getDeclaredField("status");
                f.setAccessible(true);
                Object v = f.get(r);
                return v != null ? String.valueOf(v) : null;
            } catch (NoSuchFieldException ignore) {}
        } catch (Throwable t) { /* best-effort */ }
        return null;
    }

    /** Robustno dohvaća message iz v1.8 EreportingResponse (field ili metoda). */
    private static String messageOf(EreportingResponse r) {
        if (r == null) return null;
        try {
            // metoda getMessage()
            try { return (String) r.getClass().getMethod("getMessage").invoke(r); } catch (NoSuchMethodException ignore) {}
            // metoda message()
            try { return (String) r.getClass().getMethod("message").invoke(r); } catch (NoSuchMethodException ignore) {}
            // public field message
            try {
                java.lang.reflect.Field f = r.getClass().getDeclaredField("message");
                f.setAccessible(true);
                Object v = f.get(r);
                return v != null ? String.valueOf(v) : null;
            } catch (NoSuchFieldException ignore) {}
        } catch (Throwable t) { /* best-effort */ }
        return null;
    }

    private static String safeMsg(Exception ex) {
        return (ex != null && ex.getMessage() != null) ? ex.getMessage() : "error";
    }

    private EreportingResponse buildError(String msg) {
        EreportingResponse r = new EreportingResponse();
        try {
            java.lang.reflect.Field fs = EreportingResponse.class.getDeclaredField("status");
            fs.setAccessible(true);
            fs.set(r, "error");
            java.lang.reflect.Field fm = EreportingResponse.class.getDeclaredField("message");
            fm.setAccessible(true);
            fm.set(r, msg);
        } catch (Exception ignore) { }
        return r;
    }

    private void insertLog(Connection conn,
                           int vrstaDokumenta, int godina, int opp, int onu, int broj,
                           String izvor, String akcija,
                           Integer statusId, String message, String detaljiJson) {
        try {
            if (logDao != null) {
                logDao.insertLogJson(conn, vrstaDokumenta, godina, opp, onu, broj,
                        izvor, akcija, statusId, message, detaljiJson);
            }
        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
        }
    }

    // UPSERT (INSERT ... ON DUPLICATE KEY UPDATE) u eracun_ereporting_log
    private void upsertLogJson(Connection conn,
                               int vrstaDokumenta, int godina, int opp, int onu, int broj,
                               String izvor, String akcija,
                               Integer statusId, String message, String detaljiJson) {
        final String sql =
            "INSERT INTO eracun_ereporting_log (" +
            "  vrsta_dokumenta, godina, opp, onu, broj, " +
            "  datum_promjene, izvor, akcija, status_id, message, detalji" +
            ") VALUES (?,?,?,?,?, NOW(), ?, ?, ?, ?, ?) " +
            "ON DUPLICATE KEY UPDATE " +
            "  datum_promjene = NOW(), " +
            "  status_id = VALUES(status_id), " +
            "  message   = VALUES(message), " +
            "  detalji   = VALUES(detalji)";

        try (PreparedStatement ps = conn.prepareStatement(sql)) {
            int i = 1;
            ps.setInt(i++, vrstaDokumenta);
            ps.setInt(i++, godina);
            ps.setInt(i++, opp);
            ps.setInt(i++, onu);
            ps.setInt(i++, broj);
            ps.setString(i++, izvor);
            ps.setString(i++, akcija);
            if (statusId != null) ps.setInt(i++, statusId); else ps.setNull(i++, java.sql.Types.INTEGER);
            ps.setString(i++, message);
            ps.setString(i++, detaljiJson);
            ps.executeUpdate();
        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
        }
    }

    private String toRequestJson(Date deliveryDate, boolean isCopy,
                                 MerEreportingInvoiceType type, String xmlPath) {
        try {
            JsonObject o = new JsonObject();
            if (deliveryDate != null) o.addProperty("deliveryDate", deliveryDate.toString());
            o.addProperty("isCopy", isCopy);
            if (type != null) o.addProperty("invoiceType", type.getCode());
            if (xmlPath != null) o.addProperty("xmlPath", xmlPath);
            return gson.toJson(o);
        } catch (Exception ex) {
            return "{\"build\":\"failed\"}";
        }
    }

    private String toErrorJson(Exception ex) {
        try {
            JsonObject o = new JsonObject();
            o.addProperty("error", ex.getClass().getSimpleName());
            o.addProperty("message", ex.getMessage());
            return gson.toJson(o);
        } catch (Exception e) {
            return "{\"error\":\"serialization-failed\"}";
        }
    }

    private String safeRespJson(EreportingResponse resp) {
        try {
            return gson.toJson(resp);
        } catch (Exception e) {
            return "{\"resp\":\"serialization-failed\"}";
        }
    }
}
