package hr.com.port.ips.eracun.provider.mer;

import hr.com.port.ips.eracun.provider.mer.dto.document.SendRequest;
import hr.com.port.ips.eracun.provider.mer.dto.document.SendResponse;
import hr.com.port.ips.eracun.provider.mer.dto.common.ErrorResponse;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import hr.com.port.ips.eracun.provider.mer.dto.document.QueryOutboxRequest;
import hr.com.port.ips.eracun.provider.mer.dto.document.OutboxDocumentHeader;
import hr.com.port.ips.eracun.provider.mer.dto.document.QueryOutboxResponse;
import java.lang.reflect.Type;
import com.google.gson.reflect.TypeToken;
import hr.com.port.functions.Functions;
import hr.com.port.ips.eracun.helper.JsonLogSanitizer;
import hr.com.port.ips.eracun.provider.mer.dto.errors.MerAdvisedError;
import hr.com.port.ips.eracun.provider.mer.dto.errors.MerErrors;
import hr.com.port.ips.eracun.provider.mer.enums.MerIdentifierType;
import hr.com.port.ips.eracun.provider.mer.dto.document.QueryInboxRequest;
import hr.com.port.ips.eracun.provider.mer.dto.document.ReceiveRequest;
import hr.com.port.ips.eracun.provider.mer.dto.common.CheckIdentifierResponse;
import hr.com.port.ips.eracun.provider.mer.dto.ereporting.EreportingResponse;
import hr.com.port.ips.eracun.provider.mer.dto.document.FiscalizationStatusResponse;
import hr.com.port.ips.eracun.provider.mer.dto.document.InboxDocumentHeader;
import hr.com.port.ips.eracun.provider.mer.dto.ereporting.MarkPaidResponse;
import hr.com.port.ips.eracun.provider.mer.dto.document.QueryDocumentProcessStatusInboxResponse;
import hr.com.port.ips.eracun.provider.mer.dto.document.QueryDocumentProcessStatusOutboxResponse;
import hr.com.port.ips.eracun.provider.mer.dto.document.QueryInboxResponse;
import hr.com.port.ips.eracun.provider.mer.dto.document.ReceiveResponse;
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.resilience.mer.MerSoftFailContext;
import hr.com.port.ips.eracun.resilience.mer.MerTriageHelper;
import hr.com.port.ips.eracun.service.EracunServiceUnavailableException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLException;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.apache.log4j.Logger;


public class MerClient {
	
	static Logger logger = Logger.getLogger(MerClient.class);
	
	private static final String OP_SEND        = "mer:/send";
	private static final String OP_Q_INBOX     = "mer:/qInbox";
	private static final String OP_Q_OUTBOX    = "mer:/qOutbox";
	private static final String OP_Q_PSTAT_IN  = "mer:/qProcIn";
	private static final String OP_Q_PSTAT_OUT = "mer:/qProcOut";
	private static final String OP_FISCAL      = "mer:/fiscalStatus";
	private static final String OP_MARKPAID    = "mer:/markPaid";
	private static final String OP_RECEIVE     = "mer:/receive";
	private static final String OP_REJECT      = "mer:/reject";
	private static final String OP_CHECKID     = "mer:/checkIdentifier";
	private static final String OP_EREPORT     = "mer:/eReporting";
	
    private static final String ENDPOINT_SEND = "/apis/v2/send";
    private final OkHttpClient httpClient;
    private final String baseUrl;
    private final Gson gson = new Gson();

    // DEFAULT (CORE) PARAMETRI
    private final long username;
    private final String password;
    private final String companyId;
    private final String companyBu;
    private final String softwareId;
    
    // Konstruktor s default parametrima
    public MerClient(String baseUrl, long username, String password, String companyId, String companyBu, String softwareId) {
        this.baseUrl = baseUrl;
		//this.baseUrl = "http://10.255.255.1:9999"; //fake za test
        this.httpClient = new OkHttpClient().newBuilder()
				.connectTimeout(2, TimeUnit.SECONDS)
				.readTimeout(30,TimeUnit.SECONDS)
				.writeTimeout(30, TimeUnit.SECONDS)
				.build();
        this.username = username;
        this.password = password;
        this.companyId = companyId;
        this.companyBu = companyBu;
        this.softwareId = softwareId;
    }
	
	public int getPosrednikId() {
		return 0;
	}
	
	public String getBaseUrl() {
		return baseUrl;
	}

	public long getUsername() {
		return username;
	}

	public String getPassword() {
		return password;
	}

	public String getCompanyId() {
		return companyId;
	}

	public String getCompanyBu() {
		return companyBu;
	}

	public String getSoftwareId() {
		return softwareId;
	}
	
    //  Šalje elektronički dokument (eračun) putem MER API.
    public SendResponse send(String xmlFileContent) throws IOException {
        return send(xmlFileContent, null, null);
    }

    public SendResponse send(String xmlFileContent, Boolean highImportanceReceive, String attachment) throws IOException {
        SendRequest request = new SendRequest(
            username, password, companyId, companyBu, softwareId, xmlFileContent
        );
        if (highImportanceReceive != null) request.HighImportanceReceive = highImportanceReceive;
        if (attachment != null) request.Attachment = attachment;
        return send(request);
    }

    // Osnovna privatna metoda koja zapravo šalje send request.
    private SendResponse send(SendRequest request) throws IOException {
		final String url = baseUrl + ENDPOINT_SEND; // promijeni na "/apis/v2/send" (malo v)

		MerSoftFailContext.clear();
		if (!MerAvailabilityGate.allow(OP_SEND)) {
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_SEND, "Gate OPEN", null);
			logger.warn("MER gate OPEN, preskačem send.");
			return null; // SOFT-FAIL
		}

		logger.debug("Slanje na MER API: URL=" + url + ", JSON body=" +
				JsonLogSanitizer.toSafeJson(gson, request));

		String jsonBody = gson.toJson(request);
		RequestBody body = RequestBody.create(jsonBody, MediaType.parse("application/json; charset=utf-8"));
		Request httpRequest = new Request.Builder()
				.url(url)
				.addHeader("Accept", "application/json")
				.post(body)
				.build();

		try (Response response = httpClient.newCall(httpRequest).execute()) {
			final int code = response.code();
			final String responseBody = (response.body() != null) ? response.body().string() : null;
			logger.debug("Odgovor MER API: HTTP " + code + ", body=" +
					(responseBody != null ? responseBody : "<prazno>"));

			// 2xx → OK, ali provjeri 200-with-error (message/description u tijelu)
			if (code >= 200 && code < 300) {
				if (responseBody == null || responseBody.trim().isEmpty()) {
					throw new IOException("MER API empty response body on send.");
				}

				try {
					JsonObject obj = gson.fromJson(responseBody, JsonObject.class);
					if (obj != null && (obj.has("message") || obj.has("description"))) {
						MerAdvisedError err = MerErrors.parseFromJson(obj);
						logger.error("MER send 200-with-error → " + err.formatForLog());
						throw new IOException("MER API logical error on send: " + err.formatForLog());
					}
				} catch (JsonSyntaxException ignored) {
					// nije JSON → onda tretiramo kao normalan send response
				}

				MerAvailabilityGate.onSuccess(OP_SEND);
				return gson.fromJson(responseBody, SendResponse.class);
			}

			// 5xx → SOFT-FAIL
			if (code >= 500) {
				MerSoftFailContext.markSoftFail(MerTriageHelper.Category.SERVER_5XX, code, OP_SEND, "HTTP " + code, null);
				MerAvailabilityGate.onFailure(OP_SEND);
				logger.warn("MER send: HTTP " + code + " → SOFT-FAIL");
				return null;
			}

			// 4xx → HARD-ERROR kroz helper
			MerAdvisedError err = MerErrors.parseFromRaw(responseBody);
			logger.error("MER send 4xx → " + err.formatForLog());
			throw new IOException("MER API error on send: " + err.formatForLog());

		} catch (UnknownHostException | ConnectException | SSLException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_SEND, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_SEND);
			return null;
		} catch (SocketTimeoutException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_SEND, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_SEND);
			return null;
		} catch (InterruptedIOException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_SEND, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_SEND);
			return null;
		}
		 finally {
			MerSoftFailContext.clear();
		}
	}
	
	public QueryInboxResponse queryInbox(
        String filter,           // "Undelivered" ili null
        Long electronicId,    // null ako ne filtriraš
        Integer statusId,        // 30 ili 40 ili null
        String fromDate,         // ISO "YYYY-MM-DD" ili "YYYY-MM-DDThh:mm:ss" ili null
        String toDate            // isto kao fromDate
		) throws IOException {

		QueryInboxRequest req = new QueryInboxRequest(
            this.username,
            this.password,
            this.companyId,
            this.companyBu,
            this.softwareId
		);

		if (filter != null && !filter.isEmpty()) {
	        req.setFilter(filter);
	    }
	    if (electronicId != null) {
	        req.setElectronicId(electronicId);
	    }
	    if (statusId != null) {
	        req.setStatusId(statusId);
	    }
	    if (fromDate != null && !fromDate.isEmpty()) {
	        req.setFrom(fromDate);
	    }
	    if (toDate != null && !toDate.isEmpty()) {
	        req.setTo(toDate);
	    }

		return queryInbox(req);
	}
	
	public QueryInboxResponse queryInbox(QueryInboxRequest request) throws IOException {
		final String url = baseUrl + "/apis/v2/queryInbox";

		MerSoftFailContext.clear();
		if (!MerAvailabilityGate.allow(OP_Q_INBOX)) {
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_INBOX, "Gate OPEN", null);
			logger.warn("MER gate OPEN, preskačem queryInbox.");
			QueryInboxResponse empty = new QueryInboxResponse();
			empty.documents = new ArrayList<InboxDocumentHeader>();
			return empty; // SOFT-FAIL
		}

		final String jsonBody = gson.toJson(request);
		logger.debug("MER queryInbox request jsonBody: " + JsonLogSanitizer.toSafeJson(gson, jsonBody));

		final RequestBody body = RequestBody.create(jsonBody, MediaType.parse("application/json; charset=utf-8"));

		final Request httpRequest = new Request.Builder()
				.url(url)
				.post(body)
				.header("Content-Type", "application/json; charset=utf-8")
				.header("Accept", "application/json")
				.build();

		Response response = null;
		try {
			response = httpClient.newCall(httpRequest).execute();
			final int code = response.code();
			final String responseBody = (response.body() != null) ? response.body().string() : null;

			// 2xx
			if (code >= 200 && code < 300) {
				if (responseBody == null || responseBody.trim().isEmpty()) {
					throw new IOException("MER API empty response body.");
				}

				try {
					JsonObject obj = gson.fromJson(responseBody, JsonObject.class);
					if (obj != null && (obj.has("message") || obj.has("description"))) {
						MerAdvisedError err = MerErrors.parseFromJson(obj);
						logger.error("MER queryInbox 200-with-error → " + err.formatForLog());
						throw new IOException("MER API logical error on queryInbox: " + err.formatForLog());
					}
				} catch (JsonSyntaxException ignored) {
					// nije JSON → normalan flow
				}

				logger.debug("MER queryInbox responseBody: " + responseBody);
				Type listType = new TypeToken<List<InboxDocumentHeader>>() {}.getType();
				List<InboxDocumentHeader> list = gson.fromJson(responseBody, listType);

				MerAvailabilityGate.onSuccess(OP_Q_INBOX);
				QueryInboxResponse wrapper = new QueryInboxResponse();
				wrapper.documents = list;
				return wrapper;
			}

			// 5xx → SOFT-FAIL
			if (code >= 500) {
				MerSoftFailContext.markSoftFail(MerTriageHelper.Category.SERVER_5XX, code, OP_Q_INBOX, "HTTP " + code, null);
				MerAvailabilityGate.onFailure(OP_Q_INBOX);
				logger.warn("MER queryInbox: HTTP " + code + " → SOFT-FAIL (empty)");
				QueryInboxResponse empty = new QueryInboxResponse();
				empty.documents = new ArrayList<InboxDocumentHeader>();
				return empty;
			}

			// 4xx → HARD-ERROR kroz helper
			MerAdvisedError err = MerErrors.parseFromRaw(responseBody);
			logger.error("MER queryInbox 4xx → " + err.formatForLog());
			throw new IOException("MER API error on queryInbox: " + err.formatForLog());

		} catch (UnknownHostException | ConnectException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_INBOX, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_INBOX);
			QueryInboxResponse empty = new QueryInboxResponse();
			empty.documents = new ArrayList<InboxDocumentHeader>();
			return empty;
		} catch (SocketTimeoutException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_INBOX, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_INBOX);
			QueryInboxResponse empty = new QueryInboxResponse();
			empty.documents = new ArrayList<InboxDocumentHeader>();
			return empty;
		} catch (InterruptedIOException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_INBOX, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_INBOX);
			QueryInboxResponse empty = new QueryInboxResponse();
			empty.documents = new ArrayList<InboxDocumentHeader>();
			return empty;
		} catch (javax.net.ssl.SSLException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_INBOX, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_INBOX);
			QueryInboxResponse empty = new QueryInboxResponse();
			empty.documents = new ArrayList<InboxDocumentHeader>();
			return empty;
		} finally {
			if (response != null) try { response.close(); } catch (Exception ignore) {}
			MerSoftFailContext.clear();
		}
	}
	
	public QueryOutboxResponse queryOutbox(
        Long electronicId,
        Integer statusId,
        Integer year,
        String number,
        String fromDate,
        String toDate
	) throws IOException {

		// Kreiranje requesta s core parametrima
		QueryOutboxRequest request = new QueryOutboxRequest(
            this.username,
            this.password,
            this.companyId,
            this.companyBu,
            this.softwareId
		);

		// Postavljanje opcionalnih filtera
		if (electronicId != null) {
			request.setElectronicId(electronicId);
		}
		if (statusId != null) {
	        request.setStatusId(statusId);
	    }
	    if (year != null) {
	        request.setInvoiceYear(year);
	    }
	    if (number != null) {
	        request.setInvoiceNumber(number);
	    }
	    if (fromDate != null && !fromDate.trim().isEmpty()) {
	        request.setFrom(fromDate);
	    }
	    if (toDate != null && !toDate.trim().isEmpty()) {
	        request.setTo(toDate);
	    }
	    // Poziv postojeće metode koja zapravo šalje request i obrađuje response
	    return queryOutbox(request);
	}
	
	public QueryOutboxResponse queryOutbox(QueryOutboxRequest request) throws IOException {
		final String url = baseUrl + "/apis/v2/queryOutbox";

		MerSoftFailContext.clear();
		if (!MerAvailabilityGate.allow(OP_Q_OUTBOX)) {
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_OUTBOX, "Gate OPEN", null);
			logger.warn("MER gate OPEN, preskačem queryOutbox.");
			QueryOutboxResponse empty = new QueryOutboxResponse();
			empty.setDocumentHeaders(new ArrayList<OutboxDocumentHeader>());
			return empty;
		}

		final String jsonBody = gson.toJson(request);
		logger.debug("MER queryOutbox request jsonBody: " + JsonLogSanitizer.toSafeJson(gson, jsonBody));

		final RequestBody body = RequestBody.create(jsonBody, MediaType.parse("application/json; charset=utf-8"));
		final Request httpRequest = new Request.Builder()
				.url(url)
				.post(body)
				.header("Content-Type", "application/json; charset=utf-8")
				.header("Accept", "application/json")
				.build();

		Response response = null;
		try {
			response = httpClient.newCall(httpRequest).execute();
			final int code = response.code();
			final String respBody = (response.body() != null) ? response.body().string() : null;

			// 2xx
			if (code >= 200 && code < 300) {
				if (respBody == null || respBody.trim().isEmpty()) {
					throw new IOException("MER API empty response body on queryOutbox.");
				}

				String trimmed = respBody.trim();

				try {
					JsonObject obj = gson.fromJson(trimmed, JsonObject.class);
					if (obj != null && (obj.has("message") || obj.has("description"))) {
						MerAdvisedError err = MerErrors.parseFromJson(obj);
						logger.error("MER queryOutbox 200-with-error → " + err.formatForLog());
						throw new IOException("MER API logical error on queryOutbox: " + err.formatForLog());
					}
				} catch (JsonSyntaxException ignored) {
					// nije JSON, možda array
				}

				MerAvailabilityGate.onSuccess(OP_Q_OUTBOX);

				// MER ponekad vrati object ili array – podrži oba
				if (trimmed.startsWith("{")) {
					return gson.fromJson(trimmed, QueryOutboxResponse.class);
				}
				if (trimmed.startsWith("[")) {
					Type listType = new TypeToken<List<OutboxDocumentHeader>>() {}.getType();
					List<OutboxDocumentHeader> list = gson.fromJson(trimmed, listType);
					QueryOutboxResponse wrapper = new QueryOutboxResponse();
					wrapper.setDocumentHeaders(list);
					return wrapper;
				}

				throw new IOException("MER queryOutbox returned unrecognized response: " + trimmed);
			}

			// 5xx → SOFT-FAIL
			if (code >= 500) {
				MerSoftFailContext.markSoftFail(MerTriageHelper.Category.SERVER_5XX, code, OP_Q_OUTBOX, "HTTP " + code, null);
				MerAvailabilityGate.onFailure(OP_Q_OUTBOX);
				logger.warn("MER queryOutbox: HTTP " + code + " → SOFT-FAIL (empty)");
				QueryOutboxResponse empty = new QueryOutboxResponse();
				empty.setDocumentHeaders(new ArrayList<OutboxDocumentHeader>());
				return empty;
			}

			// 4xx → HARD-ERROR kroz helper
			MerAdvisedError err = MerErrors.parseFromRaw(respBody);
			logger.error("MER queryOutbox 4xx → " + err.formatForLog());
			throw new IOException("MER API error on queryOutbox: " + err.formatForLog());

		} catch (UnknownHostException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_OUTBOX, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_OUTBOX);
			QueryOutboxResponse empty = new QueryOutboxResponse();
			empty.setDocumentHeaders(new ArrayList<OutboxDocumentHeader>());
			return empty;
		} catch (ConnectException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_OUTBOX, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_OUTBOX);
			QueryOutboxResponse empty = new QueryOutboxResponse();
			empty.setDocumentHeaders(new ArrayList<OutboxDocumentHeader>());
			return empty;
		} catch (SocketTimeoutException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_OUTBOX, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_OUTBOX);
			QueryOutboxResponse empty = new QueryOutboxResponse();
			empty.setDocumentHeaders(new ArrayList<OutboxDocumentHeader>());
			return empty;
		} catch (InterruptedIOException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_OUTBOX, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_OUTBOX);
			QueryOutboxResponse empty = new QueryOutboxResponse();
			empty.setDocumentHeaders(new ArrayList<OutboxDocumentHeader>());
			return empty;
		} catch (javax.net.ssl.SSLException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_OUTBOX, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_OUTBOX);
			QueryOutboxResponse empty = new QueryOutboxResponse();
			empty.setDocumentHeaders(new ArrayList<OutboxDocumentHeader>());
			return empty;
		} finally {
			if (response != null) try { response.close(); } catch (Exception ignore) {}
			MerSoftFailContext.clear();
		}
	}
	
	public ReceiveResponse receive(ReceiveRequest receiveRequest) throws IOException {
		final String url = baseUrl + "/apis/v2/receive";
		MerSoftFailContext.clear();

		if (!MerAvailabilityGate.allow(OP_RECEIVE)) {
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_RECEIVE, "Gate OPEN", null);
			logger.warn("MER gate OPEN, preskačem receive.");
			return null;
		}

		RequestBody body = RequestBody.create(gson.toJson(receiveRequest), MediaType.parse("application/json; charset=utf-8"));
		Request request = new Request.Builder()
				.url(url)
				.post(body)
				.header("Accept", "application/json")
				.build();

		logger.debug("MER receive request body JSON: " + JsonLogSanitizer.toSafeJson(gson, request));

		try (Response response = httpClient.newCall(request).execute()) {
			final int code = response.code();
			final String responseBody = (response.body() != null) ? response.body().string() : null;

			if (code >= 200 && code < 300) {
				String contentType = response.header("Content-Type", "");
				MerAvailabilityGate.onSuccess(OP_RECEIVE);

				if (contentType.contains("xml") || (responseBody != null && responseBody.trim().startsWith("<"))) {
					ReceiveResponse rr = new ReceiveResponse();
					rr.xmlContent = responseBody;
					return rr;
				}

				// nije XML → provjeri error
				try {
					JsonObject obj = gson.fromJson(responseBody, JsonObject.class);
					if (obj != null && (obj.has("message") || obj.has("description"))) {
						MerAdvisedError err = MerErrors.parseFromJson(obj);
						logger.error("MER receive 200-with-error → " + err.formatForLog());
						throw new IOException("MER API logical error on receive: " + err.formatForLog());
					}
				} catch (JsonSyntaxException ex) {
					logger.error(new Functions().logging(ex));
					throw new IOException("MER receive: invalid JSON in response.", ex);
				}

				throw new IOException("MER receive: unexpected non-XML, non-error response.");
			}

			if (code >= 500) {
				MerSoftFailContext.markSoftFail(MerTriageHelper.Category.SERVER_5XX, code, OP_RECEIVE, "HTTP " + code, null);
				MerAvailabilityGate.onFailure(OP_RECEIVE);
				logger.warn("MER receive: HTTP " + code + " → SOFT-FAIL");
				return null;
			}

			// 4xx
			MerAdvisedError err = MerErrors.parseFromRaw(responseBody);
			logger.error("MER receive 4xx → " + err.formatForLog());
			throw new IOException("MER API error on receive: " + err.formatForLog());

		} catch (UnknownHostException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_RECEIVE, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_RECEIVE);
			return null;
		} catch (ConnectException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_RECEIVE, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_RECEIVE);
			return null;
		} catch (SocketTimeoutException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_RECEIVE, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_RECEIVE);
			return null;
		} catch (InterruptedIOException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_RECEIVE, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_RECEIVE);
			return null;
		} catch (javax.net.ssl.SSLException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_RECEIVE, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_RECEIVE);
			return null;
		} finally {
			MerSoftFailContext.clear();
		}
	}
	
	// Pomoćna metoda za generičko izvlačenje svih error poruka iz JSON-a (rekurzivno)
	private String parseErrorMessages(JsonObject error) {
		StringBuilder sb = new StringBuilder();
		for (Map.Entry<String, JsonElement> entry : error.entrySet()) {
	        JsonElement value = entry.getValue();
	        if (value.isJsonObject()) {
	            sb.append(parseErrorMessages(value.getAsJsonObject()));
	        } else if (value.isJsonArray()) {
	            for (JsonElement el : value.getAsJsonArray()) {
	                sb.append(el.getAsString()).append(" ");
	            }
	        } else {
	            sb.append(value.getAsString()).append(" ");
	        }
	    }
	    return sb.toString().trim();
	}
	
	public QueryDocumentProcessStatusInboxResponse queryDocumentProcessStatusInbox(
        Long electronicId,
        Integer statusId,
        Integer invoiceYear,
        String invoiceNumber,
        String from,
        String to,
        Boolean byUpdateDate
	) throws Exception {

		HttpUrl url = HttpUrl.parse(baseUrl + "/apis/v2/queryDocumentProcessStatusInbox");
		if (url == null) throw new IllegalArgumentException("Bad baseUrl for inbox process status");

		// reži milisekunde (stabilnije na MER-u)
		String fromNoMs = (from != null) ? from.replaceFirst("\\.\\d{1,9}", "") : null;
		String toNoMs   = (to   != null) ? to.replaceFirst("\\.\\d{1,9}", "")   : null;

		MerSoftFailContext.clear();
		if (!MerAvailabilityGate.allow(OP_Q_PSTAT_IN)) {
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_PSTAT_IN, "Gate OPEN", null);
			logger.warn("MER gate OPEN, preskačem queryDocumentProcessStatusInbox.");
			QueryDocumentProcessStatusInboxResponse out = new QueryDocumentProcessStatusInboxResponse();
			out.setDocumentHeaders(java.util.Collections.<InboxDocumentHeader>emptyList());
			return out;
		}

		JsonObject body = new JsonObject();
		body.addProperty("Username", username);
		body.addProperty("Password", password);
		body.addProperty("CompanyId", companyId);
		if (companyBu != null && !companyBu.trim().isEmpty()) body.addProperty("CompanyBu", companyBu.trim());
		body.addProperty("SoftwareId", softwareId);
		if (electronicId != null) body.addProperty("ElectronicId", electronicId);
		if (statusId != null)     body.addProperty("StatusId", statusId);
		if (invoiceYear != null)  body.addProperty("InvoiceYear", invoiceYear);
		if (invoiceNumber != null && !invoiceNumber.isEmpty()) body.addProperty("InvoiceNumber", invoiceNumber);
		if (fromNoMs != null && !fromNoMs.isEmpty()) body.addProperty("From", fromNoMs);
		if (toNoMs   != null && !toNoMs.isEmpty())   body.addProperty("To", toNoMs);
		if (byUpdateDate != null) body.addProperty("ByUpdateDate", byUpdateDate);

		logger.debug("MER queryDocumentProcessStatusInbox request jsonBody: " + JsonLogSanitizer.toSafeJson(gson, body));

		Request request = new Request.Builder()
				.url(url)
				.addHeader("Accept", "application/json")
				.post(RequestBody.create(body.toString(), MediaType.parse("application/json; charset=utf-8")))
				.build();

		try (Response resp = httpClient.newCall(request).execute()) {
			final int code = resp.code();
			final String raw = (resp.body() != null) ? resp.body().string() : null;
			logger.debug("MER queryDocumentProcessStatusInbox ← HTTP " + code + " | resp=" + (raw != null ? raw : "<prazno>"));

			if (code >= 200 && code < 300) {
				// provjeri 200-with-error
				try {
					JsonObject obj = gson.fromJson(raw, JsonObject.class);
					if (obj != null && (obj.has("message") || obj.has("description"))) {
						MerAdvisedError err = MerErrors.parseFromJson(obj);
						logger.error("MER queryDocumentProcessStatusInbox 200-with-error → " + err.formatForLog());
						throw new IOException("MER API logical error on queryDocumentProcessStatusInbox: " + err.formatForLog());
					}
				} catch (JsonSyntaxException ignored) {
					// nije JSON → normalan flow
				}

				java.util.List<InboxDocumentHeader> list;
				if (code == 204 || raw == null || raw.trim().isEmpty()) {
					list = java.util.Collections.emptyList();
				} else {
					Type listType = new TypeToken<java.util.List<InboxDocumentHeader>>(){}.getType();
					list = raw.trim().startsWith("[") ? gson.fromJson(raw, listType) : java.util.Collections.<InboxDocumentHeader>emptyList();
				}
				MerAvailabilityGate.onSuccess(OP_Q_PSTAT_IN);
				QueryDocumentProcessStatusInboxResponse out = new QueryDocumentProcessStatusInboxResponse();
				out.setDocumentHeaders(list);
				return out;
			}

			if (code >= 500) {
				MerSoftFailContext.markSoftFail(MerTriageHelper.Category.SERVER_5XX, code, OP_Q_PSTAT_IN, "HTTP " + code, null);
				MerAvailabilityGate.onFailure(OP_Q_PSTAT_IN);
				QueryDocumentProcessStatusInboxResponse out = new QueryDocumentProcessStatusInboxResponse();
				out.setDocumentHeaders(java.util.Collections.<InboxDocumentHeader>emptyList());
				return out;
			}

			// 4xx → ERROR
			MerAdvisedError err = MerErrors.parseFromRaw(raw);
			logger.error("MER queryDocumentProcessStatusInbox 4xx → " + err.formatForLog());
			throw new IOException("MER API error on queryDocumentProcessStatusInbox: " + err.formatForLog());

		} catch (UnknownHostException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_PSTAT_IN, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_PSTAT_IN);
			QueryDocumentProcessStatusInboxResponse out = new QueryDocumentProcessStatusInboxResponse();
			out.setDocumentHeaders(java.util.Collections.<InboxDocumentHeader>emptyList());
			return out;
		} catch (ConnectException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_PSTAT_IN, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_PSTAT_IN);
			QueryDocumentProcessStatusInboxResponse out = new QueryDocumentProcessStatusInboxResponse();
			out.setDocumentHeaders(java.util.Collections.<InboxDocumentHeader>emptyList());
			return out;
		} catch (SocketTimeoutException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_PSTAT_IN, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_PSTAT_IN);
			QueryDocumentProcessStatusInboxResponse out = new QueryDocumentProcessStatusInboxResponse();
			out.setDocumentHeaders(java.util.Collections.<InboxDocumentHeader>emptyList());
			return out;
		} catch (InterruptedIOException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_PSTAT_IN, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_PSTAT_IN);
			QueryDocumentProcessStatusInboxResponse out = new QueryDocumentProcessStatusInboxResponse();
			out.setDocumentHeaders(java.util.Collections.<InboxDocumentHeader>emptyList());
			return out;
		} catch (javax.net.ssl.SSLException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_PSTAT_IN, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_PSTAT_IN);
			QueryDocumentProcessStatusInboxResponse out = new QueryDocumentProcessStatusInboxResponse();
			out.setDocumentHeaders(java.util.Collections.<InboxDocumentHeader>emptyList());
			return out;
		} finally {
			MerSoftFailContext.clear();
		}
	}
	
	public QueryDocumentProcessStatusOutboxResponse queryDocumentProcessStatusOutbox(
        Long electronicId,
        Integer statusId,
        Integer invoiceYear,
        String invoiceNumber,
        String from,
        String to,
        Boolean byUpdateDate
	) throws Exception {

		HttpUrl url = HttpUrl.parse(baseUrl + "/apis/v2/queryDocumentProcessStatusOutbox");
		if (url == null) throw new IllegalArgumentException("Bad baseUrl for outbox process status");

		MerSoftFailContext.clear();
		if (!MerAvailabilityGate.allow(OP_Q_PSTAT_OUT)) {
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_PSTAT_OUT, "Gate OPEN", null);
			logger.warn("MER gate OPEN, preskačem queryDocumentProcessStatusOutbox.");
			QueryDocumentProcessStatusOutboxResponse out = new QueryDocumentProcessStatusOutboxResponse();
			out.setDocumentHeaders(java.util.Collections.<OutboxDocumentHeader>emptyList());
			return out;
		}

		JsonObject body = new JsonObject();
		body.addProperty("Username", username);
		body.addProperty("Password", password);
		body.addProperty("CompanyId", companyId);
		if (companyBu != null) body.addProperty("CompanyBu", companyBu);
		body.addProperty("SoftwareId", softwareId);
		if (electronicId != null) body.addProperty("ElectronicId", electronicId);
		if (statusId != null)     body.addProperty("StatusId", statusId);
		if (invoiceYear != null)  body.addProperty("InvoiceYear", invoiceYear);
		if (invoiceNumber != null && !invoiceNumber.isEmpty()) body.addProperty("InvoiceNumber", invoiceNumber);
		if (from != null && !from.isEmpty()) body.addProperty("From", from);
		if (to   != null && !to.isEmpty())   body.addProperty("To", to);
		if (byUpdateDate != null) body.addProperty("ByUpdateDate", byUpdateDate);

		Request request = new Request.Builder()
				.url(url)
				.addHeader("Accept", "application/json")
				.post(RequestBody.create(body.toString(), MediaType.parse("application/json; charset=utf-8")))
				.build();

		logger.debug("MER queryDocumentProcessStatusOutbox request body JSON: " + JsonLogSanitizer.toSafeJson(gson, body));

		try (Response resp = httpClient.newCall(request).execute()) {
			final int code = resp.code();
			final String raw = (resp.body() != null) ? resp.body().string() : null;
			logger.debug("MER queryDocumentProcessStatusOutbox ← HTTP " + code + " | resp=" + (raw != null ? raw : "<prazno>"));

			if (code >= 200 && code < 300) {
				try {
					JsonObject obj = gson.fromJson(raw, JsonObject.class);
					if (obj != null && (obj.has("message") || obj.has("description"))) {
						MerAdvisedError err = MerErrors.parseFromJson(obj);
						logger.error("MER queryDocumentProcessStatusOutbox 200-with-error → " + err.formatForLog());
						throw new IOException("MER API logical error on queryDocumentProcessStatusOutbox: " + err.formatForLog());
					}
				} catch (JsonSyntaxException ignored) {
					// nije JSON → normalan flow
				}

				java.util.List<OutboxDocumentHeader> list;
				if (code == 204 || raw == null || raw.trim().isEmpty()) {
					list = java.util.Collections.emptyList();
				} else {
					Type listType = new TypeToken<java.util.List<OutboxDocumentHeader>>(){}.getType();
					list = raw.trim().startsWith("[") ? gson.fromJson(raw, listType) : java.util.Collections.<OutboxDocumentHeader>emptyList();
				}
				MerAvailabilityGate.onSuccess(OP_Q_PSTAT_OUT);
				QueryDocumentProcessStatusOutboxResponse out = new QueryDocumentProcessStatusOutboxResponse();
				out.setDocumentHeaders(list);
				return out;
			}

			if (code >= 500) {
				MerSoftFailContext.markSoftFail(MerTriageHelper.Category.SERVER_5XX, code, OP_Q_PSTAT_OUT, "HTTP " + code, null);
				MerAvailabilityGate.onFailure(OP_Q_PSTAT_OUT);
				QueryDocumentProcessStatusOutboxResponse out = new QueryDocumentProcessStatusOutboxResponse();
				out.setDocumentHeaders(java.util.Collections.<OutboxDocumentHeader>emptyList());
				return out;
			}

			// 4xx → ERROR
			MerAdvisedError err = MerErrors.parseFromRaw(raw);
			logger.error("MER queryDocumentProcessStatusOutbox 4xx → " + err.formatForLog());
			throw new IOException("MER API error on queryDocumentProcessStatusOutbox: " + err.formatForLog());

		} catch (UnknownHostException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_PSTAT_OUT, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_PSTAT_OUT);
			QueryDocumentProcessStatusOutboxResponse out = new QueryDocumentProcessStatusOutboxResponse();
			out.setDocumentHeaders(java.util.Collections.<OutboxDocumentHeader>emptyList());
			return out;
		} catch (ConnectException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_PSTAT_OUT, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_PSTAT_OUT);
			QueryDocumentProcessStatusOutboxResponse out = new QueryDocumentProcessStatusOutboxResponse();
			out.setDocumentHeaders(java.util.Collections.<OutboxDocumentHeader>emptyList());
			return out;
		} catch (SocketTimeoutException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_PSTAT_OUT, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_PSTAT_OUT);
			QueryDocumentProcessStatusOutboxResponse out = new QueryDocumentProcessStatusOutboxResponse();
			out.setDocumentHeaders(java.util.Collections.<OutboxDocumentHeader>emptyList());
			return out;
		} catch (InterruptedIOException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_PSTAT_OUT, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_PSTAT_OUT);
			QueryDocumentProcessStatusOutboxResponse out = new QueryDocumentProcessStatusOutboxResponse();
			out.setDocumentHeaders(java.util.Collections.<OutboxDocumentHeader>emptyList());
			return out;
		} catch (javax.net.ssl.SSLException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_Q_PSTAT_OUT, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_Q_PSTAT_OUT);
			QueryDocumentProcessStatusOutboxResponse out = new QueryDocumentProcessStatusOutboxResponse();
			out.setDocumentHeaders(java.util.Collections.<OutboxDocumentHeader>emptyList());
			return out;
		} finally {
			MerSoftFailContext.clear();
		}
	}
	
	private static String trimToSecond(String iso) {
	    if (iso == null) return null;
	    int dot = iso.indexOf('.');
	    return (dot > 0) ? iso.substring(0, dot) : iso;
	}
	
	public FiscalizationStatusResponse getFiscalizationStatus(long electronicId, int messageType) throws IOException {
		HttpUrl url = HttpUrl.parse(baseUrl + "/apis/v2/fiscalization/status");
		if (url == null) throw new IllegalArgumentException("Bad baseUrl for getFiscalizationStatus");

		MerSoftFailContext.clear();
		if (!MerAvailabilityGate.allow(OP_FISCAL)) {
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_FISCAL, "Gate OPEN", null);
			logger.warn("MER gate OPEN, preskačem getFiscalizationStatus.");
			return null;
		}

		JsonObject body = new JsonObject();
		body.addProperty("username", String.valueOf(username));
		body.addProperty("password", password);
		body.addProperty("companyId", companyId);
		body.addProperty("softwareId", softwareId);
		body.addProperty("electronicId", electronicId);
		body.addProperty("messageType", messageType);

		final String json = gson.toJson(body);
		logger.debug("MER getFiscalizationStatus request body JSON: " + JsonLogSanitizer.toSafeJson(gson, json));

		RequestBody reqBody = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8"));
		Request request = new Request.Builder()
				.url(url)
				.addHeader("Accept", "application/json")
				.post(reqBody)
				.build();

		try (Response resp = httpClient.newCall(request).execute()) {
			final int code = resp.code();
			final String raw = (resp.body() != null) ? resp.body().string() : null;

			logger.debug("MER getFiscalizationStatus HTTP " + code + ", resp=" + (raw != null ? raw : "<prazno>"));

			if (code == 404) {
				logger.info("MER getFiscalizationStatus 404 Not Found (electronicId=" + electronicId
						+ ", messageType=" + messageType + ") — vraćam null (nije greška).");
				return null;
			}

			if (code >= 200 && code < 300) {
				if (raw == null || raw.trim().isEmpty()) {
					throw new IOException("MER API empty response body on getFiscalizationStatus.");
				}

				try {
					JsonObject obj = gson.fromJson(raw, JsonObject.class);
					if (obj != null && (obj.has("message") || obj.has("description"))) {
						MerAdvisedError err = MerErrors.parseFromJson(obj);
						logger.error("MER getFiscalizationStatus 200-with-error → " + err.formatForLog());
						throw new IOException("MER API logical error on getFiscalizationStatus: " + err.formatForLog());
					}
				} catch (JsonSyntaxException ignored) {
					// nije JSON → normalan flow
				}

				MerAvailabilityGate.onSuccess(OP_FISCAL);
				return gson.fromJson(raw, FiscalizationStatusResponse.class);
			}

			if (code >= 500) {
				MerSoftFailContext.markSoftFail(MerTriageHelper.Category.SERVER_5XX, code, OP_FISCAL, "HTTP " + code, null);
				MerAvailabilityGate.onFailure(OP_FISCAL);
				return null;
			}

			// 4xx (osim 404 već obrađen)
			MerAdvisedError err = MerErrors.parseFromRaw(raw);
			logger.error("MER getFiscalizationStatus 4xx → " + err.formatForLog());
			throw new IOException("MER API error on getFiscalizationStatus: " + err.formatForLog());

		} catch (UnknownHostException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_FISCAL, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_FISCAL);
			return null;
		} catch (ConnectException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_FISCAL, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_FISCAL);
			return null;
		} catch (SocketTimeoutException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_FISCAL, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_FISCAL);
			return null;
		} catch (InterruptedIOException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_FISCAL, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_FISCAL);
			return null;
		} catch (javax.net.ssl.SSLException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_FISCAL, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_FISCAL);
			return null;
		} finally {
			MerSoftFailContext.clear();
		}
	}
	
	public MarkPaidResponse markPaid(long electronicId,
                                 String paymentDateIso,
                                 Double amount,
                                 String paymentMethod) throws IOException, EracunServiceUnavailableException {

		JsonObject body = new JsonObject();
		body.addProperty("username", username);
		body.addProperty("password", password);
		body.addProperty("companyId", companyId);
		body.addProperty("softwareId", softwareId);
		body.addProperty("electronicId", electronicId);
		if (paymentDateIso != null && !paymentDateIso.trim().isEmpty()) {
			body.addProperty("paymentDate", paymentDateIso.trim());
		}
		if (amount != null) {
			body.addProperty("paymentAmount", amount);
		}
		if (paymentMethod != null && !paymentMethod.trim().isEmpty()) {
			body.addProperty("paymentMethod", paymentMethod.trim());
		}

		final String url = baseUrl + "/apis/v2/fiscalization/markPaid";

		MerSoftFailContext.clear();
		if (!MerAvailabilityGate.allow(OP_MARKPAID)) {
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_MARKPAID, "Gate OPEN", null);
			logger.warn("MER gate OPEN, preskačem markPaid.");
			throw new EracunServiceUnavailableException("MER markPaid: gate OPEN", null);
		}

		final String json = body.toString();
		logger.debug("MER markPaid request body JSON: " + JsonLogSanitizer.toSafeJson(gson, json));

		Request req = new Request.Builder()
				.url(url)
				.addHeader("Accept", "application/json")
				.post(RequestBody.create(json, MediaType.parse("application/json; charset=utf-8")))
				.build();

		try (Response resp = httpClient.newCall(req).execute()) {
			final int code = resp.code();
			final String raw = (resp.body() != null) ? resp.body().string() : null;

			logger.debug("MER markPaid ← HTTP " + code + " | resp=" + (raw != null ? raw : "<prazno>"));

			// 2xx: success
			if (code >= 200 && code < 300) {
				if (raw == null || raw.trim().isEmpty()) {
					throw new IOException("MER API empty response body on markPaid.");
				}
				try {
					JsonObject obj = gson.fromJson(raw, JsonObject.class);
					if (obj != null && (obj.has("message") || obj.has("description"))) {
						MerAdvisedError err = MerErrors.parseFromJson(obj);
						logger.error("MER markPaid 200-with-error → " + err.formatForLog());
						throw new IOException("MER API logical error on markPaid: " + err.formatForLog());
					}
				} catch (JsonSyntaxException ignored) { /* not JSON → ok */ }

				MerAvailabilityGate.onSuccess(OP_MARKPAID);
				return gson.fromJson(raw, MarkPaidResponse.class);
			}

			// 404: razlikuj prazan od smislenog tijela
			if (code == 404) {
				if (raw == null || raw.trim().isEmpty()) {
					logger.warn("MER markPaid: HTTP 404 with empty body → treating as temporary provider outage (soft-fail).");
					MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, 404, OP_MARKPAID, "HTTP 404 empty body", null);
					MerAvailabilityGate.onFailure(OP_MARKPAID);
					throw new EracunServiceUnavailableException("MER markPaid: HTTP 404 empty body", null);
				}
				// “normalni” 404 → semantički NOT FOUND
				return null;
			}

			// 5xx: outage → soft-fail
			if (code >= 500) {
				MerSoftFailContext.markSoftFail(MerTriageHelper.Category.SERVER_5XX, code, OP_MARKPAID, "HTTP " + code, null);
				MerAvailabilityGate.onFailure(OP_MARKPAID);
				logger.warn("MER markPaid: HTTP " + code + " → SOFT-FAIL");
				throw new EracunServiceUnavailableException("MER markPaid: HTTP " + code, null);
			}

			// ostali 4xx: tvrda greška
			if (code >= 400) {
				MerAdvisedError err = MerErrors.parseFromRaw(raw);
				logger.error("MER markPaid 4xx → " + err.formatForLog());
				throw new IOException("MER API error on markPaid: " + err.formatForLog());
			}

			throw new IOException("MER markPaid: unexpected HTTP " + code);

		} catch (SocketTimeoutException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_MARKPAID, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_MARKPAID);
			throw new EracunServiceUnavailableException("MER markPaid: timeout", ex);

		} catch (InterruptedIOException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_MARKPAID, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_MARKPAID);
			throw new EracunServiceUnavailableException("MER markPaid: interrupted", ex);

		} catch (ConnectException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_MARKPAID, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_MARKPAID);
			throw new EracunServiceUnavailableException("MER markPaid: connect", ex);

		} catch (UnknownHostException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_MARKPAID, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_MARKPAID);
			throw new EracunServiceUnavailableException("MER markPaid: unknown host", ex);

		} catch (javax.net.ssl.SSLException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_MARKPAID, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_MARKPAID);
			throw new EracunServiceUnavailableException("MER markPaid: ssl", ex);

		} finally {
			MerSoftFailContext.clear();
		}
	}
	
	public RejectResponse reject(long electronicId,
                             String rejectionDateIso,
                             String rejectionReasonType,
                             String rejectionDescription) throws IOException {

		MerSoftFailContext.clear();
		if (!MerAvailabilityGate.allow(OP_REJECT)) {
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_REJECT, "Gate OPEN", null);
			logger.warn("MER gate OPEN, preskačem reject.");
			return null; // SOFT-FAIL
		}

		JsonObject body = new JsonObject();
		body.addProperty("username", username);
		body.addProperty("password", password);
		body.addProperty("companyId", companyId);
		body.addProperty("softwareId", softwareId);
		body.addProperty("electronicId", electronicId);

		if (rejectionDateIso != null && !rejectionDateIso.trim().isEmpty()) {
			body.addProperty("rejectionDate", rejectionDateIso.trim());
		}
		if (rejectionReasonType != null && !rejectionReasonType.trim().isEmpty()) {
			body.addProperty("rejectionReasonType", rejectionReasonType.trim());
		}
		if (rejectionDescription != null && !rejectionDescription.trim().isEmpty()) {
			body.addProperty("rejectionReasonDescription", rejectionDescription.trim());
		}

		final String json = body.toString();
		logger.debug("MER reject request body JSON: " + JsonLogSanitizer.toSafeJson(gson, json));

		String url = baseUrl + "/apis/v2/fiscalization/reject";
		Request req = new Request.Builder()
				.url(url)
				.addHeader("Accept", "application/json")
				.post(RequestBody.create(json, MediaType.parse("application/json; charset=utf-8")))
				.build();

		long t0 = System.currentTimeMillis();
		try (Response resp = httpClient.newCall(req).execute()) {
			int code = resp.code();
			String raw = (resp.body() != null) ? resp.body().string() : "";

			long dt = System.currentTimeMillis() - t0;
			logger.info("MER reject ← HTTP " + code + " in " + dt + " ms | eid=" + electronicId);
			if (logger.isDebugEnabled()) {
				logger.debug("MER reject HTTP " + code + ", resp=" + (raw != null ? raw : "<null>"));
			}

			if (code >= 200 && code < 300) {
				if (raw == null || raw.trim().isEmpty()) {
					throw new IOException("MER API empty response body on reject.");
				}

				try {
					JsonObject obj = gson.fromJson(raw, JsonObject.class);
					if (obj != null && (obj.has("message") || obj.has("description"))) {
						MerAdvisedError err = MerErrors.parseFromJson(obj);
						logger.error("MER reject 200-with-error → " + err.formatForLog());
						throw new IOException("MER API logical error on reject: " + err.formatForLog());
					}
				} catch (JsonSyntaxException ignored) {
					// nije JSON → normalan flow
				}

				MerAvailabilityGate.onSuccess(OP_REJECT);
				return gson.fromJson(raw, RejectResponse.class);
			}

			if (code == 404) {
				logger.info("MER reject 404 for eid=" + electronicId + " → returning null");
				return null;
			}

			if (code >= 500) {
				MerSoftFailContext.markSoftFail(MerTriageHelper.Category.SERVER_5XX, code, OP_REJECT, "HTTP " + code, null);
				MerAvailabilityGate.onFailure(OP_REJECT);
				logger.warn("MER reject: HTTP " + code + " → SOFT-FAIL");
				return null;
			}

			// 4xx
			MerAdvisedError err = MerErrors.parseFromRaw(raw);
			logger.error("MER reject 4xx → " + err.formatForLog());
			throw new IOException("MER API error on reject: " + err.formatForLog());

		} catch (UnknownHostException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_REJECT, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_REJECT);
			return null;
		} catch (ConnectException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_REJECT, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_REJECT);
			return null;
		} catch (SocketTimeoutException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_REJECT, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_REJECT);
			return null;
		} catch (InterruptedIOException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_REJECT, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_REJECT);
			return null;
		} catch (SSLException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_REJECT, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_REJECT);
			return null;
		} catch (JsonSyntaxException ex) {
			logger.error(new Functions().logging(ex));
			throw new IOException("MER API invalid JSON on reject", ex);
		} finally {
			MerSoftFailContext.clear();
		}
	}
		
	public CheckIdentifierResponse checkIdentifier(
        MerIdentifierType type,
        String identifierValue
	) throws IOException {

		HttpUrl url = HttpUrl.parse(baseUrl + "/apis/v2/mps/check");
		if (url == null) throw new IllegalArgumentException("Bad baseUrl for checkIdentifier");

		MerSoftFailContext.clear();
		if (!MerAvailabilityGate.allow(OP_CHECKID)) {
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_CHECKID, "Gate OPEN", null);
			logger.warn("MER gate OPEN, preskačem checkIdentifier.");
			return null;
		}

		JsonObject body = new JsonObject();
		body.addProperty("username", String.valueOf(username));
		body.addProperty("password", password);
		body.addProperty("companyId", companyId);
		body.addProperty("softwareId", softwareId);
		body.addProperty("identifierType", type.apiValue());
		body.addProperty("identifierValue", identifierValue);

		final String json = gson.toJson(body);
		logger.debug("MER checkIdentifier request body JSON: " + JsonLogSanitizer.toSafeJson(gson, json));

		Request req = new Request.Builder()
				.url(url)
				.post(RequestBody.create(json, MediaType.parse("application/json; charset=utf-8")))
				.build();

		long t0 = System.currentTimeMillis();
		try (Response resp = httpClient.newCall(req).execute()) {
			int code = resp.code();
			String raw = (resp.body() != null) ? resp.body().string() : null;
			long dt = System.currentTimeMillis() - t0;

			logger.info("MER checkIdentifier ← HTTP " + code + " in " + dt + " ms | type="
					+ type.apiValue() + " value=" + identifierValue);

			if (code == 200) {
				MerAvailabilityGate.onSuccess(OP_CHECKID);
				return new CheckIdentifierResponse(true, code);
			}
			if (code == 404) {
				MerAvailabilityGate.onSuccess(OP_CHECKID);
				return new CheckIdentifierResponse(false, code);
			}
			if (code >= 500) {
				MerSoftFailContext.markSoftFail(MerTriageHelper.Category.SERVER_5XX, code, OP_CHECKID, "HTTP " + code, null);
				MerAvailabilityGate.onFailure(OP_CHECKID);
				return null;
			}

			// Ostali 4xx → trajna greška
			MerAdvisedError err = MerErrors.parseFromRaw(raw);
			logger.error("MER checkIdentifier 4xx → " + err.formatForLog());
			throw new IOException("MER checkIdentifier error: " + err.formatForLog());

		} catch (UnknownHostException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_CHECKID, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_CHECKID);
			return null;
		} catch (ConnectException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_CHECKID, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_CHECKID);
			return null;
		} catch (SocketTimeoutException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_CHECKID, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_CHECKID);
			return null;
		} catch (InterruptedIOException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_CHECKID, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_CHECKID);
			return null;
		} catch (SSLException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_CHECKID, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_CHECKID);
			return null;
		} finally {
			MerSoftFailContext.clear();
		}
	}
	
	public EreportingResponse eReporting(
        String xmlInvoice,
        String deliveryDateIso,
        Boolean isCopy,
        String invoiceType
	) throws IOException {

		JsonObject body = new JsonObject();
		body.addProperty("username", username);
		body.addProperty("password", password);
		body.addProperty("companyId", companyId);
		body.addProperty("softwareId", softwareId);
		if (xmlInvoice != null && !xmlInvoice.trim().isEmpty()) body.addProperty("xmlInvoice", xmlInvoice);
		if (deliveryDateIso != null && !deliveryDateIso.trim().isEmpty()) body.addProperty("deliveryDate", deliveryDateIso.trim());
		if (isCopy != null) body.addProperty("isCopy", isCopy.booleanValue());
		if (invoiceType != null && !invoiceType.trim().isEmpty()) body.addProperty("invoiceType", invoiceType.trim());

		final String url = baseUrl + "/apis/v2/fiscalization/eReporting";

		MerSoftFailContext.clear();
		if (!MerAvailabilityGate.allow(OP_EREPORT)) {
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_EREPORT, "Gate OPEN", null);
			logger.warn("MER gate OPEN, preskačem eReporting.");
			return null;
		}

		final String json = body.toString();
		logger.debug("MER eReporting request body JSON: " + JsonLogSanitizer.toSafeJson(gson, json));

		Request req = new Request.Builder()
				.url(url)
				.addHeader("Accept", "application/json")
				.post(RequestBody.create(json, MediaType.parse("application/json; charset=utf-8")))
				.build();

		try (Response resp = httpClient.newCall(req).execute()) {
			final int code = resp.code();
			final String raw = (resp.body() != null) ? resp.body().string() : null;
			logger.debug("MER eReporting ← HTTP " + code + " | resp=" + (raw != null ? raw : "<prazno>"));

			if (code >= 200 && code < 300) {
				if (raw == null || raw.trim().isEmpty()) {
					throw new IOException("MER API empty response body on eReporting.");
				}

				try {
					JsonObject obj = gson.fromJson(raw, JsonObject.class);
					if (obj != null && (obj.has("message") || obj.has("description"))) {
						MerAdvisedError err = MerErrors.parseFromJson(obj);
						logger.error("MER eReporting 200-with-error → " + err.formatForLog());
						throw new IOException("MER API logical error on eReporting: " + err.formatForLog());
					}
				} catch (JsonSyntaxException ignored) {
					// nije JSON → normalan flow
				}

				MerAvailabilityGate.onSuccess(OP_EREPORT);
				try {
					return gson.fromJson(raw, EreportingResponse.class);
				} catch (JsonSyntaxException ex) {
					logger.error(new Functions().logging(ex));
					throw new IOException("MER API invalid JSON for eReporting: " + raw, ex);
				}
			}

			if (code >= 500) {
				MerSoftFailContext.markSoftFail(MerTriageHelper.Category.SERVER_5XX, code, OP_EREPORT, "HTTP " + code, null);
				MerAvailabilityGate.onFailure(OP_EREPORT);
				logger.warn("MER eReporting: HTTP " + code + " → SOFT-FAIL");
				return null;
			}

			// 4xx → trajna greška
			MerAdvisedError err = MerErrors.parseFromRaw(raw);
			logger.error("MER eReporting 4xx → " + err.formatForLog());
			throw new IOException("MER API error on eReporting: " + err.formatForLog());

		} catch (UnknownHostException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_EREPORT, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_EREPORT);
			return null;
		} catch (ConnectException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_EREPORT, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_EREPORT);
			return null;
		} catch (SocketTimeoutException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_EREPORT, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_EREPORT);
			return null;
		} catch (InterruptedIOException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_EREPORT, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_EREPORT);
			return null;
		} catch (SSLException ex) {
			logger.warn(new Functions().logging(ex));
			MerSoftFailContext.markSoftFail(MerTriageHelper.Category.CONNECTIVITY, null, OP_EREPORT, ex.getMessage(), ex);
			MerAvailabilityGate.onFailure(OP_EREPORT);
			return null;
		} finally {
			MerSoftFailContext.clear();
		}
	}
}