package de.votian.services.service;

import de.votian.services.config.ParameterKeys;
import de.votian.services.entity.Courier;
import de.votian.services.entity.Customer;
import de.votian.services.entity.Employee;
import de.votian.services.entity.Headquarters;
import de.votian.services.entity.User;
import de.votian.services.repository.CourierRepository;
import de.votian.services.repository.CustomerRepository;
import de.votian.services.repository.EmployeeRepository;
import de.votian.services.repository.HeadquartersRepository;
import de.votian.services.repository.UserRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.w3c.dom.Element;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;

@Service
@Transactional
public class LegacyXmlMetaobjectIntegrationService {

    private static final List<String> METAOBJECT_SELECT_QUERIES = List.of(
            "SELECT mo_hash FROM meta_object.metaobject WHERE mo_id_ref_db = ? AND mo_obj_type = ? AND mo_obj_id = ?",
            "SELECT mo_hash FROM metaobject WHERE mo_id_ref_db = ? AND mo_obj_type = ? AND mo_obj_id = ?"
    );
    private static final List<String> METAOBJECT_INSERT_QUERIES = List.of(
            "INSERT INTO meta_object.metaobject (mo_id_ref_db, mo_obj_type, mo_obj_id, mo_hash) VALUES (?, ?, ?, ?)",
            "INSERT INTO metaobject (mo_id_ref_db, mo_obj_type, mo_obj_id, mo_hash) VALUES (?, ?, ?, ?)"
    );

    private final LegacyXmlSystemAuthService authService;
    private final CustomerRepository customerRepository;
    private final CourierRepository courierRepository;
    private final EmployeeRepository employeeRepository;
    private final UserRepository userRepository;
    private final HeadquartersRepository headquartersRepository;
    private final ParameterService parameterService;
    private final JdbcTemplate jdbcTemplate;

    public LegacyXmlMetaobjectIntegrationService(LegacyXmlSystemAuthService authService,
                                                 CustomerRepository customerRepository,
                                                 CourierRepository courierRepository,
                                                 EmployeeRepository employeeRepository,
                                                 UserRepository userRepository,
                                                 HeadquartersRepository headquartersRepository,
                                                 ParameterService parameterService,
                                                 JdbcTemplate jdbcTemplate) {
        this.authService = authService;
        this.customerRepository = customerRepository;
        this.courierRepository = courierRepository;
        this.employeeRepository = employeeRepository;
        this.userRepository = userRepository;
        this.headquartersRepository = headquartersRepository;
        this.parameterService = parameterService;
        this.jdbcTemplate = jdbcTemplate;
    }

    public String handleMetaobjectRequest(String xmlRequest) {
        try {
            Element request = parseRequest(xmlRequest);
            boolean debug = isDebugEnabled(request);
            authenticate(request);

            String operation = upper(text(request, "operation"), "GET");
            if (!"GET".equals(operation) && !"GENERATE".equals(operation)) {
                throw new LegacyXmlRequestException("104", "Die Operation wird aktuell nicht unterstuetzt.");
            }
            String objectType = normalizeObjectType(text(request, "obj_type"));
            MetaobjectContext context = resolveContext(objectType, text(request, "obj_id"), text(request, "obj_eid"));
            String dbInstanceNo = resolveDbInstanceNo();

            String sessionHash = findMetaobjectHash(dbInstanceNo, "usr", context.user().getId())
                    .orElseGet(() -> {
                        String hash = makeMd5Hash(context.headquartersId(), context.businessObjectId(), context.user().getId());
                        if ("GENERATE".equals(operation)) {
                            insertMetaobjectHash(dbInstanceNo, "usr", context.user().getId(), hash);
                        }
                        return hash;
                    });

            AuthHeader authHeader = prepareAuthHeader(context);
            String payload = "<metaobject>" + authHeader.xml(sessionHash) + "</metaobject>";
            return ok(payload, debug ? context.objectType() + ": " + context.objectEid() : "");
        } catch (LegacyXmlRequestException ex) {
            return error(ex, null);
        } catch (Exception ex) {
            return error(new LegacyXmlRequestException("100", "Der Wert entspricht nicht dem geforderten Typ bzw. den Validitaetsbedingungen."), null);
        }
    }

    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 MetaobjectContext resolveContext(String objectType, String objectIdValue, String objectEidValue) {
        if ((objectIdValue == null || objectIdValue.isBlank()) && (objectEidValue == null || objectEidValue.isBlank())) {
            throw new LegacyXmlRequestException("102", "Ein Objekt wurde nicht spezifiziert.");
        }
        return switch (objectType) {
            case "cr" -> resolveCourierContext(objectIdValue, objectEidValue);
            case "cs" -> resolveCustomerContext(objectIdValue, objectEidValue);
            default -> throw new LegacyXmlRequestException("104", "Der Objekttyp wird aktuell nicht unterstuetzt.");
        };
    }

    private MetaobjectContext resolveCustomerContext(String objectIdValue, String objectEidValue) {
        Customer customer = resolveCustomer(objectIdValue, objectEidValue);
        Employee employee = customer.getAdminEmployeeId() != null
                ? employeeRepository.findById(Objects.requireNonNull(customer.getAdminEmployeeId())).orElse(null)
                : null;
        User user = employee != null && employee.getUserId() != null
                ? userRepository.findById(Objects.requireNonNull(employee.getUserId())).orElse(null)
                : null;
        if (user == null) {
            throw new LegacyXmlRequestException("103", "Das Objekt konnte nicht aufgefunden werden.");
        }
        Headquarters headquarters = customer.getHeadquartersId() != null
                ? headquartersRepository.findById(Objects.requireNonNull(customer.getHeadquartersId())).orElse(null)
                : null;
        return new MetaobjectContext(
                "cs",
                customer.getId(),
                customer.getEid(),
                customer.getHeadquartersId(),
                headquarters != null ? headquarters.getMnemonic() : "",
                user
        );
    }

    private MetaobjectContext resolveCourierContext(String objectIdValue, String objectEidValue) {
        Courier courier = resolveCourier(objectIdValue, objectEidValue);
        User user = courier.getUserId() != null
                ? userRepository.findById(Objects.requireNonNull(courier.getUserId())).orElse(null)
                : null;
        if (user == null) {
            throw new LegacyXmlRequestException("103", "Das Objekt konnte nicht aufgefunden werden.");
        }
        Headquarters headquarters = courier.getHeadquartersId() != null
                ? headquartersRepository.findById(Objects.requireNonNull(courier.getHeadquartersId())).orElse(null)
                : null;
        return new MetaobjectContext(
                "cr",
                courier.getId(),
                courier.getEid(),
                courier.getHeadquartersId(),
                headquarters != null ? headquarters.getMnemonic() : "",
                user
        );
    }

    private Customer resolveCustomer(String objectIdValue, String objectEidValue) {
        if (objectIdValue != null && !objectIdValue.isBlank()) {
            try {
                return customerRepository.findById(Objects.requireNonNull(Long.valueOf(objectIdValue.trim())))
                        .orElseThrow(() -> new LegacyXmlRequestException("103", "Das Objekt konnte nicht aufgefunden werden."));
            } catch (NumberFormatException ex) {
                throw new LegacyXmlRequestException("100", "Der Wert entspricht nicht dem geforderten Typ bzw. den Validitaetsbedingungen.");
            }
        }
        return customerRepository.findByEid(objectEidValue.trim())
                .orElseThrow(() -> new LegacyXmlRequestException("103", "Das Objekt konnte nicht aufgefunden werden."));
    }

    private Courier resolveCourier(String objectIdValue, String objectEidValue) {
        if (objectIdValue != null && !objectIdValue.isBlank()) {
            try {
                return courierRepository.findById(Objects.requireNonNull(Long.valueOf(objectIdValue.trim())))
                        .orElseThrow(() -> new LegacyXmlRequestException("103", "Das Objekt konnte nicht aufgefunden werden."));
            } catch (NumberFormatException ex) {
                throw new LegacyXmlRequestException("100", "Der Wert entspricht nicht dem geforderten Typ bzw. den Validitaetsbedingungen.");
            }
        }
        return courierRepository.findByEid(objectEidValue.trim())
                .orElseThrow(() -> new LegacyXmlRequestException("103", "Das Objekt konnte nicht aufgefunden werden."));
    }

    private String resolveDbInstanceNo() {
        String dbInstanceNo = parameterService.getParameterValue(ParameterKeys.GLOBAL_UNIQUE_DB_INSTANCE_NO, 0L);
        if (dbInstanceNo == null || dbInstanceNo.isBlank()) {
            dbInstanceNo = parameterService.getParameterValueWithFallback(ParameterKeys.GLOBAL_UNIQUE_DB_INSTANCE_NO, 0L);
        }
        if (dbInstanceNo == null || dbInstanceNo.isBlank()) {
            throw new LegacyXmlRequestException("104", "Referenz zur globalen metaobject-Instanz fehlt.");
        }
        return dbInstanceNo.trim();
    }

    private Optional<String> findMetaobjectHash(String dbInstanceNo, String objectType, Long objectId) {
        if (objectId == null) {
            return Optional.empty();
        }
        for (String sql : METAOBJECT_SELECT_QUERIES) {
            try {
                List<Map<String, Object>> rows = jdbcTemplate.queryForList(Objects.requireNonNull(sql), Objects.requireNonNull(dbInstanceNo), objectType, objectId);
                if (!rows.isEmpty()) {
                    String hash = stringValue(rows.getFirst().get("mo_hash"));
                    if (!hash.isBlank()) {
                        return Optional.of(hash);
                    }
                }
            } catch (Exception ignored) {
            }
        }
        return Optional.empty();
    }

    private void insertMetaobjectHash(String dbInstanceNo, String objectType, Long objectId, String hash) {
        for (String sql : METAOBJECT_INSERT_QUERIES) {
            try {
                jdbcTemplate.update(Objects.requireNonNull(sql), Objects.requireNonNull(dbInstanceNo), objectType, objectId, hash);
                return;
            } catch (Exception ignored) {
            }
        }
        throw new LegacyXmlRequestException("104", "Referenz zur globalen metaobject-Instanz fehlt.");
    }

    private AuthHeader prepareAuthHeader(MetaobjectContext context) {
        return switch (context.objectType()) {
            case "cr" -> prepareCourierHeader(context);
            case "cs" -> prepareCustomerHeader(context);
            default -> throw new LegacyXmlRequestException("104", "Der Objekttyp wird aktuell nicht unterstuetzt.");
        };
    }

    private AuthHeader prepareCustomerHeader(MetaobjectContext context) {
        User user = context.user();
        String account = stringValue(user.getAccount());
        String password = "";
        if (account.isBlank()) {
            account = context.objectEid();
            password = generatePassword(context.headquartersMnemonic());
            updateUserCredentials(user, account, password);
        }
        return new AuthHeader("customer", context.objectEid(), account, password, true);
    }

    private AuthHeader prepareCourierHeader(MetaobjectContext context) {
        User user = context.user();
        String account = stringValue(user.getAccount());
        if (account.isBlank()) {
            account = context.objectEid();
        }
        String password = generatePassword(context.headquartersMnemonic());
        updateUserCredentials(user, account, password);
        return new AuthHeader("contractor", context.objectEid(), account, password, false);
    }

    private void updateUserCredentials(User user, String account, String password) {
        if (user == null || user.getId() == null) {
            throw new LegacyXmlRequestException("103", "Das Objekt konnte nicht aufgefunden werden.");
        }
        user.setAccount(account);
        userRepository.save(user);
        userRepository.updatePasswordWithMysqlFunction(user.getId(), password);
    }

    private String generatePassword(String headquartersMnemonic) {
        String prefix = headquartersMnemonic == null ? "" : headquartersMnemonic.trim();
        int suffix = ThreadLocalRandom.current().nextInt(10000, 100000);
        return prefix + suffix;
    }

    private String makeMd5Hash(Long headquartersId, Long businessObjectId, Long userId) {
        int random = ThreadLocalRandom.current().nextInt(0, 30001);
        return md5(String.valueOf(random))
                + md5(stringValue(headquartersId))
                + md5(stringValue(businessObjectId))
                + md5(stringValue(userId));
    }

    private String md5(String value) {
        try {
            MessageDigest digest = MessageDigest.getInstance("MD5");
            byte[] bytes = digest.digest(value.getBytes(StandardCharsets.UTF_8));
            return HexFormat.of().formatHex(bytes);
        } catch (NoSuchAlgorithmException ex) {
            throw new IllegalStateException(ex);
        }
    }

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

    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 normalizeObjectType(String value) {
        String normalized = upper(value, "CS").toLowerCase();
        return normalized.isBlank() ? "cs" : normalized;
    }

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

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

    private String error(LegacyXmlRequestException ex, String debug) {
        return "<xml><state>NOT OK</state><errors><error>"
                + tag("err_no", ex.getErrorCode())
                + tagCData("err_desc", ex.getDescription())
                + "</error></errors>"
                + debugTag(debug)
                + "</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 record MetaobjectContext(String objectType,
                                     Long businessObjectId,
                                     String objectEid,
                                     Long headquartersId,
                                     String headquartersMnemonic,
                                     User user) {
    }

    private record AuthHeader(String objectTag,
                              String objectEid,
                              String account,
                              String password,
                              boolean includeCostcenter) {

        private String xml(String sessionHash) {
            StringBuilder xml = new StringBuilder("<auth>")
                    .append(tag(objectTag, objectEid))
                    .append(tag("account", account))
                    .append(tag("password", password))
                    .append(tag("session_id", sessionHash));
            if (includeCostcenter) {
                xml.append(tag("costcenter_name", ""));
            }
            xml.append("</auth>");
            return xml.toString();
        }

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

        private String escape(String value) {
            if (value == null) {
                return "";
            }
            return "<![CDATA[" + value.replace("]]>", "]]]]><![CDATA[>") + "]]>";
        }
    }
}
