package hr.com.port.ips.eracun.provider.pondi;

import hr.com.port.ips.eracun.provider.pondi.dto.common.PingResponse;
import hr.com.port.ips.eracun.provider.pondi.dto.ereporting.EReportingRequestsResponse;
import hr.com.port.ips.eracun.provider.pondi.dto.ereporting.EReportingDocumentRejectedRequest;
import hr.com.port.ips.eracun.provider.pondi.dto.ereporting.EReportingDocumentPaidRequest;
import hr.com.port.ips.eracun.provider.pondi.dto.document.DocumentVisualizationResponse;
import hr.com.port.ips.eracun.provider.pondi.dto.document.DocumentValidateResponse;
import hr.com.port.ips.eracun.provider.pondi.dto.document.DocumentValidateRequest;
import hr.com.port.ips.eracun.provider.pondi.dto.document.DocumentStatusResponse;
import hr.com.port.ips.eracun.provider.pondi.dto.document.DocumentSendResponse;
import hr.com.port.ips.eracun.provider.pondi.dto.document.DocumentSendRequest;
import hr.com.port.ips.eracun.provider.pondi.dto.document.DocumentGetResponse;
import hr.com.port.ips.eracun.provider.pondi.dto.document.DocumentChangeStatusRequest;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import hr.com.port.functions.Functions;
import hr.com.port.ips.eracun.provider.EracunProvider;
import hr.com.port.ips.eracun.provider.ProviderCapabilities;
import hr.com.port.ips.eracun.provider.ProviderException;
import hr.com.port.ips.eracun.provider.model.Ack;
import hr.com.port.ips.eracun.provider.model.Ping;
import hr.com.port.ips.eracun.provider.model.doc.DocumentBinary;
import hr.com.port.ips.eracun.provider.model.doc.DocumentHeader;
import hr.com.port.ips.eracun.provider.model.doc.DocumentRef;
import hr.com.port.ips.eracun.provider.model.doc.DocumentSnapshot;
import hr.com.port.ips.eracun.provider.model.ereporting.EReportingDoc;
import hr.com.port.ips.eracun.provider.model.ereporting.EReportingRequests;
import hr.com.port.ips.eracun.provider.model.ereporting.PaymentReport;
import hr.com.port.ips.eracun.provider.model.ereporting.RejectionReport;
import hr.com.port.ips.eracun.provider.model.ids.DocumentKey;
import hr.com.port.ips.eracun.provider.model.ids.RemoteId;
import hr.com.port.ips.eracun.provider.model.paging.ListFilter;
import hr.com.port.ips.eracun.provider.model.paging.Page;
import hr.com.port.ips.eracun.provider.model.status.ProcessStatusChange;
import hr.com.port.ips.eracun.provider.model.validate.ValidationIssue;
import hr.com.port.ips.eracun.provider.model.validate.ValidationResult;
import hr.com.port.ips.eracun.provider.pondi.mapper.PondiModelMapper;
import org.apache.log4j.Logger;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class PondiEracunProvider implements EracunProvider {

    private static final Logger logger = Logger.getLogger(PondiEracunProvider.class);

    private final PondiClient http;
    private final Gson gson;
    private final ProviderCapabilities caps;

    public PondiEracunProvider(String baseUrl, String apiKey) {
        this.http = new PondiClient(baseUrl, apiKey);
        this.gson = new Gson();
        ProviderCapabilities c = new ProviderCapabilities();
        c.supportsPdf = true;
        c.supportsXmlGet = true;
        c.supportsProcessStatusChange = true;
        c.supportsValidation = true;
        c.supportsEReporting = true;
        c.supportsListByModified = true;
        c.supportsBanking = true;
        c.supportsAms = true;
        c.pagingMode = "OFFSET";
        this.caps = c;
    }

    public ProviderCapabilities capabilities() { return caps; }

    public DocumentRef send(String ublXml, String softwareId) throws ProviderException {
        try {
            DocumentSendRequest req = new DocumentSendRequest();
            req.document = ublXml;
            req.softwareId = softwareId;
            String json = http.postJson("/api/v2/document/send", gson.toJson(req));
            DocumentSendResponse r = gson.fromJson(json, DocumentSendResponse.class);
            return PondiModelMapper.toRef(r.id, r.insertedOn);
        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
            throw new ProviderException("PONDI send failed", ex);
        }
    }

    public DocumentBinary getXml(RemoteId id) throws ProviderException {
        try {
            String json = http.get("/api/v2/document/get/" + id.value, null);
            DocumentGetResponse r = gson.fromJson(json, DocumentGetResponse.class);
            return PondiModelMapper.xmlToBinary(r.document);
        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
            throw new ProviderException("PONDI getXml failed", ex);
        }
    }

    public DocumentBinary getPdf(RemoteId id) throws ProviderException {
        try {
            String json = http.get("/api/v2/document/visualization/" + id.value, null);
            DocumentVisualizationResponse r = gson.fromJson(json, DocumentVisualizationResponse.class);
            return PondiModelMapper.pdfToBinary(r.pdf);
        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
            throw new ProviderException("PONDI getPdf failed", ex);
        }
    }

    public DocumentSnapshot get(RemoteId id) throws ProviderException {
        try {
            String json = http.get("/api/v2/document/status/" + id.value, null);
            DocumentStatusResponse s = gson.fromJson(json, DocumentStatusResponse.class);
            return PondiModelMapper.toSnapshot(s);
        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
            throw new ProviderException("PONDI get(status) failed", ex);
        }
    }

    public Ack changeProcessStatus(RemoteId id, ProcessStatusChange change) throws ProviderException {
        try {
            DocumentChangeStatusRequest req = new DocumentChangeStatusRequest();
            if (change.newBusiness == hr.com.port.ips.eracun.provider.model.status.BusinessStatus.APPROVED) {
                req.status = 5;
            } else if (change.newBusiness == hr.com.port.ips.eracun.provider.model.status.BusinessStatus.REJECTED) {
                req.status = 6;
                req.note = change.note;
            } else if (change.newSettlement == hr.com.port.ips.eracun.provider.model.status.SettlementStatus.PAID) {
                req.status = 7;
            } else if (change.newSettlement == hr.com.port.ips.eracun.provider.model.status.SettlementStatus.PARTIALLY_PAID) {
                req.status = 8;
                req.partialPaymentAmount = change.partialAmount == null ? null : change.partialAmount.doubleValue();
            } else {
                Ack a = new Ack();
                a.ok = false;
                a.raw = "No compatible PONDI status for the requested ProcessStatusChange";
                return a;
            }
            req.changedOn = change.changedOn;
            String json = http.postJson("/api/v2/document/changestatus/" + id.value, gson.toJson(req));
            Ack a = new Ack();
            a.ok = true;
            a.raw = json;
            return a;
        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
            throw new ProviderException("PONDI changeProcessStatus failed", ex);
        }
    }

    public ValidationResult validate(String ublXml) throws ProviderException {
        try {
            DocumentValidateRequest req = new DocumentValidateRequest();
            req.document = ublXml;
            String json = http.postJson("/api/v2/document/validate", gson.toJson(req));
            DocumentValidateResponse r = gson.fromJson(json, DocumentValidateResponse.class);
            ValidationResult vr = new ValidationResult();
            vr.issues = new java.util.ArrayList<ValidationIssue>();
            if (r.errors != null) {
                for (String e : r.errors) {
                    ValidationIssue vi = new ValidationIssue();
                    vi.severity = "ERROR";
                    vi.message = e;
                    vr.issues.add(vi);
                }
            }
            return vr;
        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
            throw new ProviderException("PONDI validate failed", ex);
        }
    }

    public Page<DocumentHeader> listIncoming(ListFilter f) throws ProviderException {
        return listGeneric("/api/v2/document/incoming", f);
    }

    public Page<DocumentHeader> listOutgoing(ListFilter f) throws ProviderException {
        return listGeneric("/api/v2/document/outgoing", f);
    }

    private Page<DocumentHeader> listGeneric(String path, ListFilter f) throws ProviderException {
        try {
            Map<String, String> q = new HashMap<String, String>();
            if (f != null) {
                if (f.insertedFrom != null) q.put("insertedFrom", f.insertedFrom);
                if (f.insertedTo != null) q.put("insertedTo", f.insertedTo);
                if (f.modifiedFrom != null) q.put("modifiedFrom", f.modifiedFrom);
                if (f.modifiedTo != null) q.put("modifiedTo", f.modifiedTo);
                if (f.issuedFrom != null) q.put("issuedFrom", f.issuedFrom);
                if (f.issuedTo != null) q.put("issuedTo", f.issuedTo);
                if (f.limit != null) q.put("limit", String.valueOf(f.limit));
                if (f.offset != null) q.put("offset", String.valueOf(f.offset));
                if (f.providerExtensions != null) {
                    java.util.Iterator<java.util.Map.Entry<String,String>> it = f.providerExtensions.entrySet().iterator();
                    while (it.hasNext()) {
                        java.util.Map.Entry<String,String> e = it.next();
                        q.put(e.getKey(), e.getValue());
                    }
                }
            }
            String json = http.get(path, q);
            java.lang.reflect.Type listType = new TypeToken<ArrayList<DocumentStatusResponse>>(){}.getType();
            List<DocumentStatusResponse> src = (List<DocumentStatusResponse>) new Gson().fromJson(json, listType);
            List<DocumentHeader> dst = new ArrayList<DocumentHeader>();
            for (int i = 0; i < src.size(); i++) {
                dst.add(PondiModelMapper.toHeader(src.get(i)));
            }
            Page<DocumentHeader> page = new Page<DocumentHeader>();
            page.items = dst;
            if (f != null && f.limit != null && src.size() == f.limit.intValue() && f.offset != null) {
                page.nextOffset = new Integer(f.offset.intValue() + src.size());
            }
            return page;
        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
            throw new ProviderException("PONDI list failed", ex);
        }
    }

    public Ack reportDocument(EReportingDoc req) throws ProviderException {
        try {
            String body = new Gson().toJson(req).replace("\"xml\":", "\"document\":");
            String json = http.postJson("/api/v2/ereporting/reportdocument", body);
            Ack a = new Ack();
            a.ok = true;
            a.raw = json;
            return a;
        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
            throw new ProviderException("PONDI reportDocument failed", ex);
        }
    }

    public Ack reportPayment(PaymentReport pr) throws ProviderException {
        try {
            EReportingDocumentPaidRequest req = new EReportingDocumentPaidRequest();
            if (pr.id != null) {
                req.paymentDate = pr.paymentDate;
                req.paidAmount = pr.paidAmount == null ? null : pr.paidAmount.doubleValue();
                req.paymentType = pr.paymentType == null ? null : toPaymentTypeInt(pr.paymentType.name());
                String json = http.postJson("/api/v2/ereporting/paid/" + pr.id.value, new Gson().toJson(req));
                Ack a = new Ack(); a.ok = true; a.raw = json; return a;
            } else {
                if (pr.key != null) {
                    req.documentId = pr.key.documentId;
                    req.issueDate = pr.key.issueDate;
                    req.supplierPartyId = pr.key.supplier == null ? null : pr.key.supplier.value;
                    req.customerPartyId = pr.key.customer == null ? null : pr.key.customer.value;
                }
                req.paymentDate = pr.paymentDate;
                req.paidAmount = pr.paidAmount == null ? null : pr.paidAmount.doubleValue();
                req.paymentType = pr.paymentType == null ? null : toPaymentTypeInt(pr.paymentType.name());
                String json = http.postJson("/api/v2/ereporting/paid", new Gson().toJson(req));
                Ack a = new Ack(); a.ok = true; a.raw = json; return a;
            }
        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
            throw new ProviderException("PONDI reportPayment failed", ex);
        }
    }

    public Ack reportRejection(RejectionReport rr) throws ProviderException {
        try {
            EReportingDocumentRejectedRequest req = new EReportingDocumentRejectedRequest();
            if (rr.id != null) {
                req.rejectionDate = rr.rejectionDate;
                req.rejectionType = rr.rejectionType == null ? null : toRejectionTypeInt(rr.rejectionType.name());
                req.rejectionReason = rr.reason;
                String json = http.postJson("/api/v2/ereporting/rejected/" + rr.id.value, new Gson().toJson(req));
                Ack a = new Ack(); a.ok = true; a.raw = json; return a;
            } else {
                if (rr.key != null) {
                    req.documentId = rr.key.documentId;
                    req.issueDate = rr.key.issueDate;
                    req.supplierPartyId = rr.key.supplier == null ? null : rr.key.supplier.value;
                    req.customerPartyId = rr.key.customer == null ? null : rr.key.customer.value;
                }
                req.rejectionDate = rr.rejectionDate;
                req.rejectionType = rr.rejectionType == null ? null : toRejectionTypeInt(rr.rejectionType.name());
                req.rejectionReason = rr.reason;
                String json = http.postJson("/api/v2/ereporting/rejected", new Gson().toJson(req));
                Ack a = new Ack(); a.ok = true; a.raw = json; return a;
            }
        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
            throw new ProviderException("PONDI reportRejection failed", ex);
        }
    }

    public EReportingRequests listEReportingRequests(DocumentKey key) throws ProviderException {
        try {
            Map<String, String> q = new HashMap<String, String>();
            q.put("documentId", key.documentId);
            if (key.issueDate != null && key.issueDate.length() >= 4) {
                q.put("issueYear", key.issueDate.substring(0,4));
            }
            String json = http.get("/api/v2/ereporting/requests", q);
            EReportingRequestsResponse r = gson.fromJson(json, EReportingRequestsResponse.class);
            EReportingRequests out = new EReportingRequests();
            if (r.requests != null) {
                out.items = new EReportingRequests.RequestItem[r.requests.size()];
                for (int i = 0; i < r.requests.size(); i++) {
                    hr.com.port.ips.eracun.provider.pondi.dto.ereporting.EReportingRequest sr = r.requests.get(i);
                    EReportingRequests.RequestItem di = new EReportingRequests.RequestItem();
                    di.createdOn = sr.createdOn;
                    di.description = sr.description;
                    di.success = sr.success;
                    di.finishedOn = sr.finishedOn;
                    di.referenceId = sr.referenceId;
                    di.errorMessage = sr.errorMessage;
                    out.items[i] = di;
                }
            }
            return out;
        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
            throw new ProviderException("PONDI listEReportingRequests failed", ex);
        }
    }

    public Ping ping() throws ProviderException {
        try {
            String json = http.get("/api/v2/ping", null);
            PingResponse pr = gson.fromJson(json, PingResponse.class);
            Ping p = new Ping();
            p.status = pr.status;
            p.message = pr.message;
            return p;
        } catch (Exception ex) {
            logger.error(new Functions().logging(ex));
            throw new ProviderException("PONDI ping failed", ex);
        }
    }

    private Integer toPaymentTypeInt(String name) {
        if ("T".equals(name)) return new Integer(1);
        if ("O".equals(name)) return new Integer(2);
        if ("Z".equals(name)) return new Integer(3);
        return null;
    }

    private Integer toRejectionTypeInt(String name) {
        if ("N".equals(name)) return new Integer(1);
        if ("U".equals(name)) return new Integer(2);
        if ("O".equals(name)) return new Integer(3);
        return null;
    }
}
