package de.votian.services.service;

import de.votian.services.config.ParameterKeys;
import de.votian.services.dto.EmployeeWarehouseConfigDto;
import de.votian.services.dto.EmployeeWarehouseConfigRequest;
import de.votian.services.dto.EmployeeWarehouseRootDto;
import de.votian.services.dto.EmployeeWarehouseStockOptionDto;
import de.votian.services.dto.EmployeeWarehouseSubstockAssignmentDto;
import de.votian.services.entity.Employee;
import de.votian.services.entity.User;
import de.votian.services.repository.EmployeeRepository;
import de.votian.services.repository.UserRepository;
import jakarta.persistence.EntityManager;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

@Service
@Transactional(readOnly = true)
public class EmployeeWarehouseConfigService {

    private final EmployeeRepository employeeRepository;
    private final UserRepository userRepository;
    private final ParameterService parameterService;
    private final EntityManager entityManager;

    public EmployeeWarehouseConfigService(EmployeeRepository employeeRepository,
                                          UserRepository userRepository,
                                          ParameterService parameterService,
                                          EntityManager entityManager) {
        this.employeeRepository = employeeRepository;
        this.userRepository = userRepository;
        this.parameterService = parameterService;
        this.entityManager = entityManager;
    }

    public EmployeeWarehouseConfigDto loadTemplate(Long headquartersId) {
        if (headquartersId == null || headquartersId <= 0) {
            throw badRequest("Keine Niederlassung fuer die Lagerkonfiguration verfuegbar.");
        }
        return new EmployeeWarehouseConfigDto(
                loadRoots(headquartersId),
                List.of(),
                null,
                false,
                false,
                true,
                List.of()
        );
    }

    public EmployeeWarehouseConfigDto loadConfig(Long employeeId) {
        Employee employee = employee(employeeId);
        User user = user(employee.getUserId());
        Long headquartersId = headquartersId(employee, user);

        List<EmployeeWarehouseRootDto> roots = loadRoots(headquartersId);
        Set<Long> validRootIds = roots.stream()
                .map(EmployeeWarehouseRootDto::id)
                .collect(Collectors.toCollection(LinkedHashSet::new));

        Long defaultRootStockId = defaultRootStockId(user.getId());
        Set<Long> selectedRootIds = new LinkedHashSet<>(parseIds(
                parameterService.getParameterValue(employeeId, ParameterKeys.MASK_STK_ROOT_ACCESS, headquartersId)
        ));
        if (defaultRootStockId != null) {
            selectedRootIds.add(defaultRootStockId);
        }
        selectedRootIds.retainAll(validRootIds);
        if (defaultRootStockId != null && !selectedRootIds.contains(defaultRootStockId)) {
            defaultRootStockId = null;
        }

        Map<Long, Set<Long>> substockAssignments = parseSubstockAssignments(
                parameterService.getParameterValue(employeeId, ParameterKeys.MASK_STK_SUBSTOCK_ACCESS, headquartersId)
        );
        List<EmployeeWarehouseSubstockAssignmentDto> assignments = new ArrayList<>();
        for (EmployeeWarehouseRootDto root : roots) {
            Set<Long> validSubstockIds = root.substocks().stream()
                    .map(EmployeeWarehouseStockOptionDto::id)
                    .collect(Collectors.toCollection(LinkedHashSet::new));
            Set<Long> assigned = new LinkedHashSet<>(substockAssignments.getOrDefault(root.id(), Set.of()));
            assigned.retainAll(validSubstockIds);
            if (!assigned.isEmpty()) {
                assignments.add(new EmployeeWarehouseSubstockAssignmentDto(root.id(), new ArrayList<>(assigned)));
            }
        }

        return new EmployeeWarehouseConfigDto(
                roots,
                new ArrayList<>(selectedRootIds),
                defaultRootStockId,
                "1".equals(parameterService.getParameterValue(employeeId, ParameterKeys.MASK_STK_READONLY, headquartersId)),
                "1".equals(parameterService.getParameterValue(employeeId, ParameterKeys.MASK_STK_ARTICLE_ACCESS, headquartersId)),
                "1".equals(parameterService.getParameterValue(employeeId,
                        ParameterKeys.MASK_STK_READONLY_WHERE_DEFINED_WRITEACCESS,
                        headquartersId)),
                assignments
        );
    }

    @Transactional
    public void saveConfig(Long employeeId, EmployeeWarehouseConfigRequest request) {
        Employee employee = employee(employeeId);
        User user = user(employee.getUserId());
        Long headquartersId = headquartersId(employee, user);

        List<EmployeeWarehouseRootDto> roots = loadRoots(headquartersId);
        Map<Long, EmployeeWarehouseRootDto> rootsById = roots.stream()
                .collect(Collectors.toMap(EmployeeWarehouseRootDto::id, root -> root, (left, right) -> left, LinkedHashMap::new));

        Set<Long> selectedRootIds = sanitizeRootIds(request != null ? request.selectedRootIds() : null, rootsById.keySet());
        Long defaultRootStockId = request != null ? request.defaultRootStockId() : null;
        if (defaultRootStockId != null) {
            if (!rootsById.containsKey(defaultRootStockId)) {
                throw badRequest("Das ausgewaehlte Standard-Hauptlager ist ungueltig.");
            }
            selectedRootIds.add(defaultRootStockId);
        }

        Map<Long, Set<Long>> requestedAssignments = sanitizeSubstockAssignments(
                request != null ? request.substockAssignments() : null,
                rootsById,
                selectedRootIds
        );

        parameterService.setParameterValue(
                ParameterKeys.MASK_STK_ROOT_ACCESS,
                joinIds(selectedRootIds),
                headquartersId,
                employeeId
        );
        parameterService.setParameterValue(
                ParameterKeys.MASK_STK_READONLY,
                request != null && request.readOnly() ? "1" : "0",
                headquartersId,
                employeeId
        );
        parameterService.setParameterValue(
                ParameterKeys.MASK_STK_ARTICLE_ACCESS,
                request != null && request.articleAccess() ? "1" : "0",
                headquartersId,
                employeeId
        );
        parameterService.setParameterValue(
                ParameterKeys.MASK_STK_READONLY_WHERE_DEFINED_WRITEACCESS,
                request == null || request.restrictVisibilityToAssignedSubstocks() ? "1" : "0",
                headquartersId,
                employeeId
        );
        parameterService.setParameterValue(
                ParameterKeys.MASK_STK_SUBSTOCK_ACCESS,
                joinAssignments(requestedAssignments),
                headquartersId,
                employeeId
        );

        updateDefaultRootStock(user.getId(), defaultRootStockId);
    }

    private List<EmployeeWarehouseRootDto> loadRoots(Long headquartersId) {
        @SuppressWarnings("unchecked")
        List<Object[]> rows = entityManager.createNativeQuery(
                        "SELECT stk_id, stk_name " +
                                "FROM stock " +
                                "WHERE hq_id = :hqId AND stk_pre_id = 0 AND COALESCE(stk_path, '') = '' " +
                                "ORDER BY stk_name")
                .setParameter("hqId", headquartersId)
                .getResultList();

        List<EmployeeWarehouseRootDto> roots = new ArrayList<>();
        for (Object[] row : rows) {
            Long rootId = toLong(row[0]);
            if (rootId == null) {
                continue;
            }
            roots.add(new EmployeeWarehouseRootDto(
                    rootId,
                    asString(row[1]),
                    loadSubstocks(headquartersId, rootId)
            ));
        }
        return roots;
    }

    private List<EmployeeWarehouseStockOptionDto> loadSubstocks(Long headquartersId, Long rootStockId) {
        @SuppressWarnings("unchecked")
        List<Object[]> rows = entityManager.createNativeQuery(
                        "SELECT s.stk_id, s.stk_pre_id, s.stk_name, COALESCE(s.stk_path, '') " +
                                "FROM stock s " +
                                "WHERE s.hq_id = :hqId AND (s.stk_id = :rootId OR COALESCE(s.stk_path, '') LIKE :path) " +
                                "ORDER BY COALESCE(s.stk_path, ''), s.stk_name")
                .setParameter("hqId", headquartersId)
                .setParameter("rootId", rootStockId)
                .setParameter("path", "%//" + rootStockId + "//%")
                .getResultList();

        List<EmployeeWarehouseStockOptionDto> substocks = new ArrayList<>();
        for (Object[] row : rows) {
            Long stockId = toLong(row[0]);
            if (stockId == null || stockId.equals(rootStockId)) {
                continue;
            }
            substocks.add(new EmployeeWarehouseStockOptionDto(
                    stockId,
                    toLong(row[1]),
                    levelFromPath(asString(row[3])),
                    asString(row[2])
            ));
        }
        return substocks;
    }

    private Map<Long, Set<Long>> sanitizeSubstockAssignments(List<EmployeeWarehouseSubstockAssignmentDto> assignments,
                                                             Map<Long, EmployeeWarehouseRootDto> rootsById,
                                                             Set<Long> selectedRootIds) {
        if (assignments == null || assignments.isEmpty()) {
            return Map.of();
        }

        Map<Long, Set<Long>> result = new LinkedHashMap<>();
        for (EmployeeWarehouseSubstockAssignmentDto assignment : assignments) {
            if (assignment == null || assignment.rootStockId() == null) {
                continue;
            }
            if (!selectedRootIds.contains(assignment.rootStockId())) {
                continue;
            }
            EmployeeWarehouseRootDto root = rootsById.get(assignment.rootStockId());
            if (root == null) {
                continue;
            }

            Set<Long> validSubstockIds = root.substocks().stream()
                    .map(EmployeeWarehouseStockOptionDto::id)
                    .collect(Collectors.toCollection(LinkedHashSet::new));
            Set<Long> stockIds = sanitizeRootIds(assignment.stockIds(), validSubstockIds);
            if (!stockIds.isEmpty()) {
                result.put(assignment.rootStockId(), stockIds);
            }
        }
        return result;
    }

    private Map<Long, Set<Long>> parseSubstockAssignments(String value) {
        if (value == null || value.isBlank()) {
            return Map.of();
        }

        Map<Long, Set<Long>> result = new LinkedHashMap<>();
        for (String tuple : value.split("\\|")) {
            if (tuple == null || tuple.isBlank()) {
                continue;
            }
            List<Long> ids = parseIds(tuple);
            if (ids.isEmpty()) {
                continue;
            }
            Long rootId = ids.get(0);
            Set<Long> stockIds = ids.stream()
                    .skip(1)
                    .collect(Collectors.toCollection(LinkedHashSet::new));
            if (!stockIds.isEmpty()) {
                result.put(rootId, stockIds);
            }
        }
        return result;
    }

    private Set<Long> sanitizeRootIds(List<Long> requestedIds, Collection<Long> validIds) {
        Set<Long> valid = validIds == null
                ? Set.of()
                : validIds.stream()
                .filter(id -> id != null && id > 0)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (requestedIds == null || requestedIds.isEmpty() || valid.isEmpty()) {
            return new LinkedHashSet<>();
        }

        Set<Long> sanitized = new LinkedHashSet<>();
        for (Long id : requestedIds) {
            if (id != null && valid.contains(id)) {
                sanitized.add(id);
            }
        }
        return sanitized;
    }

    private void updateDefaultRootStock(Long userId, Long defaultRootStockId) {
        entityManager.createNativeQuery("DELETE FROM stockuser WHERE usr_id = :userId")
                .setParameter("userId", userId)
                .executeUpdate();
        if (defaultRootStockId != null) {
            entityManager.createNativeQuery("INSERT INTO stockuser (stk_id, usr_id) VALUES (:stockId, :userId)")
                    .setParameter("stockId", defaultRootStockId)
                    .setParameter("userId", userId)
                    .executeUpdate();
        }
    }

    private Long defaultRootStockId(Long userId) {
        if (userId == null) {
            return null;
        }
        @SuppressWarnings("unchecked")
        List<Number> rows = entityManager.createNativeQuery(
                        "SELECT stk_id FROM stockuser WHERE usr_id = :userId ORDER BY stk_id LIMIT 1")
                .setParameter("userId", userId)
                .getResultList();
        if (rows.isEmpty()) {
            return null;
        }
        return rows.get(0).longValue();
    }

    private List<Long> parseIds(String value) {
        if (value == null || value.isBlank()) {
            return List.of();
        }
        List<Long> ids = new ArrayList<>();
        for (String token : value.split(",")) {
            if (token == null || token.isBlank()) {
                continue;
            }
            try {
                ids.add(Long.parseLong(token.trim()));
            } catch (NumberFormatException ignored) {
                // Ignore malformed legacy fragments.
            }
        }
        return ids;
    }

    private String joinIds(Collection<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return "";
        }
        return ids.stream()
                .filter(id -> id != null && id > 0)
                .map(String::valueOf)
                .collect(Collectors.joining(","));
    }

    private String joinAssignments(Map<Long, Set<Long>> assignments) {
        if (assignments == null || assignments.isEmpty()) {
            return "";
        }
        List<String> parts = new ArrayList<>();
        for (Map.Entry<Long, Set<Long>> entry : assignments.entrySet()) {
            if (entry.getKey() == null || entry.getValue() == null || entry.getValue().isEmpty()) {
                continue;
            }
            List<Long> ids = new ArrayList<>();
            ids.add(entry.getKey());
            ids.addAll(entry.getValue());
            parts.add(joinIds(ids));
        }
        return String.join("|", parts);
    }

    private int levelFromPath(String path) {
        if (path == null || path.isBlank()) {
            return 0;
        }
        int level = 0;
        for (String token : path.split("//")) {
            if (!token.isBlank()) {
                level++;
            }
        }
        return level;
    }

    private Employee employee(Long employeeId) {
        if (employeeId == null) {
            throw badRequest("Kein Mitarbeiter ausgewaehlt.");
        }
        return employeeRepository.findById(employeeId)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Mitarbeiter nicht gefunden."));
    }

    private User user(Long userId) {
        return userRepository.findById(Objects.requireNonNull(userId))
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Benutzer nicht gefunden."));
    }

    private Long headquartersId(Employee employee, User user) {
        Long headquartersId = employee.getHeadquartersId() != null
                ? employee.getHeadquartersId()
                : user.getHeadquartersId();
        if (headquartersId == null || headquartersId <= 0) {
            throw badRequest("Der Mitarbeiter ist keiner Niederlassung zugeordnet.");
        }
        return headquartersId;
    }

    private Long toLong(Object value) {
        if (value instanceof Number number) {
            return number.longValue();
        }
        if (value == null) {
            return null;
        }
        try {
            return Long.parseLong(value.toString());
        } catch (NumberFormatException ignored) {
            return null;
        }
    }

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

    private ResponseStatusException badRequest(String message) {
        return new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
    }
}
