package de.votian.services.service;

import de.votian.services.entity.Address;
import de.votian.services.entity.Company;
import de.votian.services.entity.CostCenter;
import de.votian.services.entity.CostCenterAddress;
import de.votian.services.entity.Customer;
import de.votian.services.entity.Employee;
import de.votian.services.entity.Job;
import de.votian.services.entity.User;
import de.votian.services.repository.AddressRepository;
import de.votian.services.repository.CompanyRepository;
import de.votian.services.repository.CostCenterAddressRepository;
import de.votian.services.repository.CostCenterRepository;
import de.votian.services.repository.CustomerRepository;
import de.votian.services.repository.EmployeeRepository;
import de.votian.services.repository.JobRepository;
import de.votian.services.repository.UserRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.w3c.dom.Element;

import java.math.BigDecimal;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

@Service
public class LegacyXmlSystemIntegrationService {

    private final LegacyXmlSystemAuthService authService;
    private final CustomerRepository customerRepository;
    private final CompanyRepository companyRepository;
    private final EmployeeRepository employeeRepository;
    private final UserRepository userRepository;
    private final CostCenterRepository costCenterRepository;
    private final CostCenterAddressRepository costCenterAddressRepository;
    private final AddressRepository addressRepository;
    private final JobRepository jobRepository;
    private final JdbcTemplate jdbcTemplate;

    public LegacyXmlSystemIntegrationService(LegacyXmlSystemAuthService authService,
                                             CustomerRepository customerRepository,
                                             CompanyRepository companyRepository,
                                             EmployeeRepository employeeRepository,
                                             UserRepository userRepository,
                                             CostCenterRepository costCenterRepository,
                                             CostCenterAddressRepository costCenterAddressRepository,
                                             AddressRepository addressRepository,
                                             JobRepository jobRepository,
                                             JdbcTemplate jdbcTemplate) {
        this.authService = authService;
        this.customerRepository = customerRepository;
        this.companyRepository = companyRepository;
        this.employeeRepository = employeeRepository;
        this.userRepository = userRepository;
        this.costCenterRepository = costCenterRepository;
        this.costCenterAddressRepository = costCenterAddressRepository;
        this.addressRepository = addressRepository;
        this.jobRepository = jobRepository;
        this.jdbcTemplate = jdbcTemplate;
    }

    public String handleCustomerRequest(String xmlRequest) {
        try {
            Element request = parseRequest(xmlRequest);
            boolean debug = isDebugEnabled(request);
            authenticate(request);
            String operation = upper(text(request, "operation"), "GET");
            if (!"GET".equals(operation)) {
                throw new LegacyXmlRequestException("104", "Die Operation wird aktuell nicht unterstuetzt.");
            }

            Customer customer = resolveCustomer(request);
            Company company = customer.getCompanyId() != null
                    ? companyRepository.findById(Objects.requireNonNull(customer.getCompanyId())).orElse(null)
                    : null;
            User user = resolveCustomerUser(customer);
            GeoPoint geoPoint = loadCustomerGeo(customer.getId());
            List<CostCenter> costCenters = costCenterRepository.findByCustomerId(customer.getId()).stream()
                    .filter(costCenter -> !"1".equals(costCenter.getIsExtern()))
                    .sorted(Comparator.comparing(CostCenter::getPath, Comparator.nullsLast(String::compareToIgnoreCase))
                            .thenComparing(CostCenter::getName, Comparator.nullsLast(String::compareToIgnoreCase)))
                    .toList();

            StringBuilder payload = new StringBuilder("<customer>");
            payload.append(tag("cs_id", stringValue(customer.getId())))
                    .append(tagCData("cs_eid", customer.getEid()))
                    .append(tagCData("cs_sid", customer.getSid()))
                    .append(tag("hq_id", stringValue(customer.getHeadquartersId())))
                    .append(tag("cs_prov", decimalOrBlank(customer.getMarkup())))
                    .append(tag("cs_discount", decimalOrBlank(customer.getDiscount())))
                    .append(tag("cs_invmode", stringValue(customer.getInvoiceMode())))
                    .append(tagCData("cs_cr_fav_only", customer.getFavoriteCouriersOnly()))
                    .append(tagCData("cs_jbstatusmail", customer.getJobStatusMail()))
                    .append(tagCData("cs_jbstatusmail2", customer.getJobStatusMail2()))
                    .append(tagCData("cs_jbstatusmail3", customer.getJobStatusMail3()))
                    .append(tagCData("cs_jbstatusmail_pdf", customer.getJobStatusMailPdf()))
                    .append(tagCData("cs_filter", customer.getFilter()))
                    .append(tagCData("cs_group", customer.getGroup()))
                    .append(tagCData("cs_tracking", customer.getTracking()))
                    .append(tag("cs_gps_lat", geoPoint != null ? decimalOrBlank(geoPoint.latitude()) : ""))
                    .append(tag("cs_gps_long", geoPoint != null ? decimalOrBlank(geoPoint.longitude()) : ""));

            if (isTruthy(text(request, "cs_last_job"))) {
                payload.append(tag("cs_last_job", stringValue(loadLastJobId(costCenters))));
            }

            payload.append(buildCompanyXml(company))
                    .append(buildUserXml(user))
                    .append(buildCostcentersXml(costCenters))
                    .append(buildCustomerCouriersXml("customercouriers", customer.getId(), "1"))
                    .append(buildCustomerCouriersXml("customercouriers_blocked", customer.getId(), "0"))
                    .append("</customer>");
            return ok(payload.toString(), debug ? "customer: " + customer.getEid() : "");
        } catch (LegacyXmlRequestException ex) {
            return error(ex);
        } catch (Exception ex) {
            return error(new LegacyXmlRequestException("100", "Der Wert entspricht nicht dem geforderten Typ bzw. den Validitaetsbedingungen."));
        }
    }

    private void authenticate(Element request) {
        Element auth = child(request, "auth");
        if (auth == null) {
            throw new LegacyXmlRequestException("101", "Die Authentifizierungsdaten sind nicht in Ordnung.");
        }
        authService.authenticate(text(auth, "session_id"), text(auth, "passphrase"));
    }

    private Customer resolveCustomer(Element request) {
        String csIdValue = text(request, "cs_id");
        String csEid = text(request, "cs_eid");
        Optional<Customer> customer = Optional.empty();
        if (!csIdValue.isBlank()) {
            try {
                customer = customerRepository.findById(Objects.requireNonNull(Long.valueOf(csIdValue)));
            } catch (NumberFormatException ex) {
                throw new LegacyXmlRequestException("100", "Der Wert entspricht nicht dem geforderten Typ bzw. den Validitaetsbedingungen.");
            }
        } else if (!csEid.isBlank()) {
            customer = customerRepository.findByEid(csEid);
        } else {
            throw new LegacyXmlRequestException("102", "Die notwendige ID des Auftraggebers ist nicht gesetzt.");
        }
        return customer.orElseThrow(() -> new LegacyXmlRequestException("103", "Der Auftraggeber wurde nicht im Datenbestand gefunden."));
    }

    private User resolveCustomerUser(Customer customer) {
        if (customer == null || customer.getAdminEmployeeId() == null) {
            return null;
        }
        Employee employee = employeeRepository.findById(Objects.requireNonNull(customer.getAdminEmployeeId())).orElse(null);
        if (employee == null || employee.getUserId() == null) {
            return null;
        }
        return userRepository.findById(Objects.requireNonNull(employee.getUserId())).orElse(null);
    }

    private GeoPoint loadCustomerGeo(Long customerId) {
        if (customerId == null) {
            return null;
        }
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT gc_lat, gc_long FROM geocode WHERE gc_obj_type = 'cs' AND gc_obj_id = ?",
                customerId
        );
        if (rows.isEmpty()) {
            return null;
        }
        Map<String, Object> row = rows.getFirst();
        return new GeoPoint(toBigDecimal(row.get("gc_lat")), toBigDecimal(row.get("gc_long")));
    }

    private Long loadLastJobId(List<CostCenter> costCenters) {
        List<Long> costCenterIds = costCenters.stream().map(CostCenter::getId).toList();
        if (costCenterIds.isEmpty()) {
            return null;
        }
        return jobRepository.findByPayerCostCenters(costCenterIds).stream()
                .map(Job::getId)
                .findFirst()
                .orElse(null);
    }

    private String buildCompanyXml(Company company) {
        Address address = company != null && company.getAddressId() != null
                ? addressRepository.findById(Objects.requireNonNull(company.getAddressId())).orElse(null)
                : null;
        return "<company>"
                + tag("cmp_type", stringValue(company != null ? company.getType() : null))
                + tagCData("cmp_comp", company != null ? company.getComp() : "")
                + tagCData("cmp_comp2", company != null ? company.getComp2() : "")
                + tagCData("cmp_comp3", company != null ? company.getComp3() : "")
                + tagCData("cmp_comp4", company != null ? company.getComp4() : "")
                + tagCData("ad_street", address != null ? address.getStreet() : "")
                + tagCData("ad_hsno", company != null ? company.getHsno() : "")
                + tagCData("ad_zipcode", address != null ? address.getZipcode() : "")
                + tagCData("ad_city", address != null ? address.getCity() : "")
                + tagCData("ad_country", address != null ? address.getCountry() : "")
                + tagCData("cmp_iln", company != null ? company.getIln() : "")
                + tagCData("cmp_tax_idno", company != null ? company.getTaxIdNo() : "")
                + tagCData("cmp_stax_idno", company != null ? company.getStaxIdNo() : "")
                + tagCData("cmp_bank", company != null ? company.getBank() : "")
                + tagCData("cmp_bankno", company != null ? company.getBankNo() : "")
                + tagCData("cmp_bankacc", company != null ? company.getBankAccount() : "")
                + tagCData("cmp_iban", company != null ? company.getIban() : "")
                + tagCData("cmp_swift", company != null ? company.getSwift() : "")
                + tagCData("cmp_logo", company != null ? company.getLogo() : "")
                + tag("cmp_logo_width", stringValue(company != null ? company.getLogoWidth() : null))
                + tag("cmp_logo_height", stringValue(company != null ? company.getLogoHeight() : null))
                + tagCData("cmp_remark", company != null ? company.getRemark() : "")
                + tagCData("cmp_remark2", company != null ? company.getRemark2() : "")
                + tagCData("cmp_match", company != null ? company.getMatch() : "")
                + tagCData("cmp_match2", company != null ? company.getMatch2() : "")
                + tagCData("cmp_newsletter", company != null ? company.getNewsletter() : "")
                + tagCData("cmp_pda_usage", company != null ? company.getPdaUsage() : "")
                + "</company>";
    }

    private String buildUserXml(User user) {
        return "<user>"
                + tagCData("usr_name", user != null ? user.getName() : "")
                + tagCData("usr_firstname", user != null ? user.getFirstname() : "")
                + tag("usr_birthdate", user != null && user.getBirthdate() != null ? user.getBirthdate().toString() : "")
                + tagCData("usr_email", user != null ? user.getEmail() : "")
                + tagCData("usr_inv_email", user != null ? user.getInvoiceEmail() : "")
                + tagCData("usr_phone", user != null ? user.getPhone() : "")
                + tagCData("usr_phone2", user != null ? user.getPhone2() : "")
                + tagCData("usr_fax", user != null ? user.getFax() : "")
                + tagCData("usr_account", user != null ? user.getAccount() : "")
                + "</user>";
    }

    private String buildCostcentersXml(List<CostCenter> costCenters) {
        StringBuilder xml = new StringBuilder("<costcenters>");
        for (CostCenter costCenter : costCenters) {
            xml.append("<costcenter>")
                    .append(tag("csc_id", stringValue(costCenter.getId())))
                    .append(tagCData("csc_name", costCenter.getName()))
                    .append(tag("csc_pre_id", stringValue(costCenter.getParentId())))
                    .append("<costcenteraddresses>");
            for (CostCenterAddress costCenterAddress : costCenterAddressRepository.findByCostCenterId(costCenter.getId())) {
                Address address = costCenterAddress.getAddressId() != null
                        ? addressRepository.findById(Objects.requireNonNull(costCenterAddress.getAddressId())).orElse(null)
                        : null;
                xml.append("<costcenteraddress>")
                        .append(tag("adt_id", stringValue(costCenterAddress.getAddressTypeId())))
                        .append(tagCData("cscad_comp", costCenterAddress.getComp()))
                        .append(tagCData("cscad_comp2", costCenterAddress.getComp2()))
                        .append(tagCData("cscad_comp3", costCenterAddress.getComp3()))
                        .append(tagCData("cscad_comp4", costCenterAddress.getComp4()))
                        .append(tagCData("ad_street", address != null ? address.getStreet() : ""))
                        .append(tagCData("ad_hsno", costCenterAddress.getHsno()))
                        .append(tagCData("ad_floor", costCenterAddress.getFloor()))
                        .append(tagCData("ad_zipcode", address != null ? address.getZipcode() : ""))
                        .append(tagCData("ad_city", address != null ? address.getCity() : ""))
                        .append(tagCData("ad_country", address != null ? address.getCountry() : ""))
                        .append(tagCData("cscad_person", costCenterAddress.getPerson()))
                        .append(tagCData("cscad_phone", costCenterAddress.getPhone()))
                        .append(tagCData("cscad_email", costCenterAddress.getEmail()))
                        .append(tagCData("cscad_remark", costCenterAddress.getRemark()))
                        .append("</costcenteraddress>");
            }
            xml.append("</costcenteraddresses></costcenter>");
        }
        xml.append("</costcenters>");
        return xml.toString();
    }

    private String buildCustomerCouriersXml(String wrapperTag, Long customerId, String relation) {
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT cr.cr_id, cr.cr_eid " +
                        "FROM customercourier cscr " +
                        "JOIN courier cr ON cr.cr_id = cscr.cr_id " +
                        "WHERE cscr.cs_id = ? AND cscr.cscr_relation = ? " +
                        "ORDER BY COALESCE(cscr.cscr_sort, 0), cr.cr_id",
                customerId,
                relation
        );
        if (rows.isEmpty()) {
            return "";
        }
        StringBuilder xml = new StringBuilder("<").append(wrapperTag).append(">");
        for (Map<String, Object> row : rows) {
            xml.append("<customercourier>")
                    .append(tag("cr_id", stringValue(row.get("cr_id"))))
                    .append(tagCData("cr_eid", stringValue(row.get("cr_eid"))))
                    .append("</customercourier>");
        }
        xml.append("</").append(wrapperTag).append(">");
        return xml.toString();
    }

    private Element parseRequest(String xmlRequest) throws Exception {
        return LegacyXmlXmlParsing.parseRequest(xmlRequest, "customer");
    }

    private Element child(Element parent, String tagName) {
        return LegacyXmlXmlParsing.child(parent, tagName);
    }

    private String text(Element parent, String tagName) {
        return LegacyXmlXmlParsing.text(parent, tagName);
    }

    private boolean isDebugEnabled(Element request) {
        String value = upper(text(request, "debug_on"), "");
        return "ON".equals(value) || "YES".equals(value);
    }

    private String upper(String value, String fallback) {
        return value == null || value.isBlank() ? fallback : value.trim().toUpperCase();
    }

    private boolean isTruthy(String value) {
        return "ON".equalsIgnoreCase(value) || "YES".equalsIgnoreCase(value) || "1".equals(value);
    }

    private BigDecimal toBigDecimal(Object value) {
        if (value instanceof BigDecimal decimal) {
            return decimal;
        }
        if (value instanceof Number number) {
            return BigDecimal.valueOf(number.doubleValue());
        }
        if (value instanceof String text && !text.isBlank()) {
            try {
                return new BigDecimal(text.trim());
            } catch (NumberFormatException ex) {
                return null;
            }
        }
        return null;
    }

    private String ok(String payload, String debug) {
        return "<xml><state>OK</state>" + payload + debugTag(debug) + "</xml>";
    }

    private String error(LegacyXmlRequestException ex) {
        return "<xml><state>NOT OK</state><errors><error>"
                + tag("err_no", ex.getErrorCode())
                + tagCData("err_desc", ex.getDescription())
                + "</error></errors></xml>";
    }

    private String debugTag(String debug) {
        return debug == null || debug.isBlank() ? "" : tagCData("debug", debug);
    }

    private String tag(String name, String value) {
        return "<" + name + ">" + escapeXml(value) + "</" + name + ">";
    }

    private String tagCData(String name, String value) {
        return "<" + name + "><![CDATA[" + cdata(value) + "]]></" + name + ">";
    }

    private String cdata(String value) {
        return value == null ? "" : value.replace("]]>", "]]]]><![CDATA[>");
    }

    private String escapeXml(String value) {
        if (value == null) {
            return "";
        }
        return value
                .replace("&", "&amp;")
                .replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace("\"", "&quot;")
                .replace("'", "&apos;");
    }

    private String stringValue(Object value) {
        return value == null ? "" : value.toString();
    }

    private String decimalOrBlank(BigDecimal value) {
        return value == null ? "" : value.toPlainString();
    }

    private record GeoPoint(BigDecimal latitude, BigDecimal longitude) {
    }
}
