package de.votian.services.service;

import de.votian.services.config.ParameterKeys;
import de.votian.services.dto.UserSessionDto;
import de.votian.services.dto.WarehouseArticleOptionDto;
import de.votian.services.dto.WarehouseBootstrapDto;
import de.votian.services.dto.WarehouseInventoryRowDto;
import de.votian.services.dto.WarehouseJournalEntryDto;
import de.votian.services.dto.WarehouseMoveRequest;
import de.votian.services.dto.WarehouseRootStockDto;
import de.votian.services.dto.WarehouseSerialItemDto;
import de.votian.services.dto.WarehouseStockNodeDto;
import de.votian.services.security.AccessControlService;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

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

    private static final DateTimeFormatter JOURNAL_TIMESTAMP = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final int JOURNAL_LIMIT = 500;
    private static final int DATA_FIELD_COUNT = 25;

    private final EntityManager entityManager;
    private final ParameterService parameterService;
    private final AccessControlService accessControlService;

    public WarehouseService(EntityManager entityManager,
                            ParameterService parameterService,
                            AccessControlService accessControlService) {
        this.entityManager = entityManager;
        this.parameterService = parameterService;
        this.accessControlService = accessControlService;
    }

    public WarehouseBootstrapDto loadBootstrap(UserSessionDto user) {
        assertWarehouseAccess(user);
        List<WarehouseRootStockDto> roots = loadAccessibleRoots(user);
        Long defaultRootStockId = determineDefaultRootStockId(user, roots);
        return new WarehouseBootstrapDto(
                roots,
                defaultRootStockId,
                canMove(user),
                canManageArticles(user)
        );
    }

    public List<WarehouseStockNodeDto> loadStocks(UserSessionDto user, Long rootStockId) {
        assertWarehouseAccess(user);
        if (!accessibleRootIds(user).contains(rootStockId)) {
            throw forbidden("Kein Zugriff auf das gewaehlte Hauptlager.");
        }

        List<StockNode> tree = loadRootTree(rootStockId);
        if (tree.isEmpty()) {
            return List.of();
        }

        Map<Long, Integer> usedCapacityByStock = loadUsedCapacity(tree.stream().map(StockNode::id).toList());
        Map<Long, Integer> reservedCapacityByStock = loadReservedCapacity(tree.stream().map(StockNode::id).toList());

        List<WarehouseStockNodeDto> result = new ArrayList<>();
        for (StockNode node : tree) {
            int maxQuantity = node.maxQuantity();
            int usedCapacity = usedCapacityByStock.getOrDefault(node.id(), 0);
            int reservedCapacity = reservedCapacityByStock.getOrDefault(node.id(), 0);
            int freeCapacity = Math.max(0, maxQuantity - usedCapacity - reservedCapacity);
            result.add(new WarehouseStockNodeDto(
                    node.id(),
                    node.parentId(),
                    levelFromPath(node.path()),
                    node.name(),
                    node.visible(),
                    node.planning(),
                    maxQuantity,
                    usedCapacity,
                    reservedCapacity,
                    freeCapacity,
                    node.barcode(),
                    buildAddressLabel(node),
                    canWriteStock(user, node.id())
            ));
        }
        return result;
    }

    public List<WarehouseArticleOptionDto> loadArticleOptions(UserSessionDto user, Long rootStockId) {
        assertWarehouseAccess(user);
        if (rootStockId != null && !accessibleRootIds(user).contains(rootStockId)) {
            throw forbidden("Kein Zugriff auf das gewaehlte Hauptlager.");
        }

        @SuppressWarnings("unchecked")
        List<Object[]> rows = entityManager.createNativeQuery(
                        "SELECT at.at_id, at.at_eid, at.at_name, COALESCE(at.at_match, ''), COALESCE(at.at_serialno, 0) " +
                                "FROM article at " +
                                "WHERE at.hq_id = :hqId AND COALESCE(at.at_visible, 1) = 1 " +
                                "ORDER BY at.at_name, at.at_eid")
                .setParameter("hqId", user.getHeadquartersId())
                .getResultList();

        List<WarehouseArticleOptionDto> result = new ArrayList<>();
        for (Object[] row : rows) {
            result.add(new WarehouseArticleOptionDto(
                    toLong(row[0]),
                    asString(row[1]),
                    asString(row[2]),
                    asString(row[3]),
                    intValue(row[4]) == 1
            ));
        }
        return result;
    }

    public List<WarehouseInventoryRowDto> loadInventory(UserSessionDto user,
                                                        Long stockId,
                                                        boolean includeSubstocks,
                                                        String search) {
        assertWarehouseAccess(user);
        StockNode selectedStock = loadStockNode(stockId);
        Long rootStockId = rootStockId(selectedStock);
        if (!accessibleRootIds(user).contains(rootStockId)) {
            throw forbidden("Kein Zugriff auf den gewaehlten Lagerbereich.");
        }

        List<StockNode> tree = loadRootTree(rootStockId);
        List<Long> scopedStockIds = scopedDisplayStockIds(user, tree, stockId, includeSubstocks);
        if (scopedStockIds.isEmpty()) {
            return List.of();
        }

        StringBuilder sql = new StringBuilder(
                "SELECT a.at_id, a.at_eid, a.at_name, COALESCE(a.at_match, ''), COALESCE(a.at_barcode, ''), " +
                        "       COALESCE(a.at_serialno, 0), COALESCE(SUM(sa.stkat_itemquantity), 0), " +
                        "       COALESCE(SUM(sa.stkat_quantity), 0), COUNT(DISTINCT sa.stk_id) " +
                        "FROM stockarticle sa " +
                        "JOIN article a ON a.at_id = sa.at_id " +
                        "WHERE sa.stk_id IN (").append(joinIds(scopedStockIds)).append(")");
        if (search != null && !search.isBlank()) {
            sql.append(" AND (LOWER(COALESCE(a.at_eid, '')) LIKE :search")
                    .append(" OR LOWER(COALESCE(a.at_name, '')) LIKE :search")
                    .append(" OR LOWER(COALESCE(a.at_match, '')) LIKE :search")
                    .append(" OR LOWER(COALESCE(a.at_barcode, '')) LIKE :search)");
        }
        sql.append(" GROUP BY a.at_id, a.at_eid, a.at_name, a.at_match, a.at_barcode, a.at_serialno")
                .append(" ORDER BY a.at_name, a.at_eid");

        Query query = entityManager.createNativeQuery(sql.toString());
        if (search != null && !search.isBlank()) {
            query.setParameter("search", like(search));
        }

        @SuppressWarnings("unchecked")
        List<Object[]> rows = query.getResultList();
        List<WarehouseInventoryRowDto> result = new ArrayList<>();
        for (Object[] row : rows) {
            result.add(new WarehouseInventoryRowDto(
                    toLong(row[0]),
                    asString(row[1]),
                    asString(row[2]),
                    asString(row[3]),
                    asString(row[4]),
                    intValue(row[5]) == 1,
                    intValue(row[6]),
                    intValue(row[7]),
                    intValue(row[8])
            ));
        }
        return result;
    }

    public List<WarehouseSerialItemDto> loadSerialItems(UserSessionDto user,
                                                        Long stockId,
                                                        boolean includeSubstocks,
                                                        Long articleId,
                                                        String search) {
        assertWarehouseAccess(user);
        if (articleId == null) {
            return List.of();
        }

        StockNode selectedStock = loadStockNode(stockId);
        Long rootStockId = rootStockId(selectedStock);
        if (!accessibleRootIds(user).contains(rootStockId)) {
            throw forbidden("Kein Zugriff auf den gewaehlten Lagerbereich.");
        }

        List<StockNode> tree = loadRootTree(rootStockId);
        List<Long> scopedStockIds = scopedDisplayStockIds(user, tree, stockId, includeSubstocks);
        if (scopedStockIds.isEmpty()) {
            return List.of();
        }

        StringBuilder sql = new StringBuilder(
                "SELECT ai.ati_id, ai.stk_id, s.stk_name, ai.ati_serialno, " +
                        buildDataFieldSelection("ai") +
                        " FROM articleitem ai " +
                        "JOIN stock s ON s.stk_id = ai.stk_id " +
                        "WHERE ai.at_id = :articleId AND ai.stk_id IN (" + joinIds(scopedStockIds) + ")");
        if (search != null && !search.isBlank()) {
            sql.append(" AND (LOWER(COALESCE(ai.ati_serialno, '')) LIKE :search");
            for (int index = 1; index <= DATA_FIELD_COUNT; index++) {
                sql.append(" OR LOWER(COALESCE(ai.ati_data_").append(pad(index)).append(", '')) LIKE :search");
            }
            sql.append(")");
        }
        sql.append(" ORDER BY s.stk_name, ai.ati_serialno");

        Query query = entityManager.createNativeQuery(sql.toString())
                .setParameter("articleId", articleId);
        if (search != null && !search.isBlank()) {
            query.setParameter("search", like(search));
        }

        @SuppressWarnings("unchecked")
        List<Object[]> rows = query.getResultList();
        List<WarehouseSerialItemDto> result = new ArrayList<>();
        for (Object[] row : rows) {
            result.add(new WarehouseSerialItemDto(
                    toLong(row[0]),
                    toLong(row[1]),
                    asString(row[2]),
                    asString(row[3]),
                    extractDataFields(row, 4)
            ));
        }
        return result;
    }

    public List<WarehouseJournalEntryDto> loadJournal(UserSessionDto user,
                                                      Long rootStockId,
                                                      Long stockFromId,
                                                      Long stockToId,
                                                      Long articleId,
                                                      LocalDate from,
                                                      LocalDate to,
                                                      String search) {
        assertWarehouseAccess(user);
        if (!accessibleRootIds(user).contains(rootStockId)) {
            throw forbidden("Kein Zugriff auf das gewaehlte Hauptlager.");
        }

        List<StockNode> tree = loadRootTree(rootStockId);
        if (tree.isEmpty()) {
            return List.of();
        }

        List<Long> rootTreeStockIds = tree.stream().map(StockNode::id).toList();
        List<Long> visibleStockIds = displayVisibleStockIds(user, tree);
        if (visibleStockIds.isEmpty()) {
            visibleStockIds = rootTreeStockIds;
        }

        StringBuilder sql = new StringBuilder(
                "SELECT sm.stkmv_id, COALESCE(sm.stkmv_datetime2, sm.stkmv_timestamp), " +
                        "       COALESCE(sf.stk_name, 'Eingangslieferung'), COALESCE(st.stk_name, 'Ausgangslager'), " +
                        "       COALESCE(a.at_eid, ''), COALESCE(a.at_name, ''), COALESCE(sm.stkmv_itemquantity, 0), " +
                        "       COALESCE(sm.stkmv_serialno, ''), COALESCE(sm.stkmv_tan, ''), COALESCE(sm.stkmv_remark, ''), sm.jb_id " +
                        "FROM stockmove sm " +
                        "LEFT JOIN stock sf ON sf.stk_id = sm.stk_id_from " +
                        "LEFT JOIN stock st ON st.stk_id = sm.stk_id_to " +
                        "LEFT JOIN article a ON a.at_id = sm.at_id " +
                        "WHERE ((sm.stk_id_from IN (" + joinIds(rootTreeStockIds) + ")) OR (sm.stk_id_to IN (" + joinIds(rootTreeStockIds) + ")))");

        if (stockFromId != null) {
            sql.append(" AND sm.stk_id_from = :stockFromId");
        }
        if (stockToId != null) {
            sql.append(" AND sm.stk_id_to = :stockToId");
        }
        if (articleId != null) {
            sql.append(" AND sm.at_id = :articleId");
        }
        if (from != null) {
            sql.append(" AND COALESCE(sm.stkmv_datetime2, sm.stkmv_timestamp) >= :fromTime");
        }
        if (to != null) {
            sql.append(" AND COALESCE(sm.stkmv_datetime2, sm.stkmv_timestamp) <= :toTime");
        }
        if (search != null && !search.isBlank()) {
            sql.append(" AND (LOWER(COALESCE(a.at_eid, '')) LIKE :search")
                    .append(" OR LOWER(COALESCE(a.at_name, '')) LIKE :search")
                    .append(" OR LOWER(COALESCE(sm.stkmv_serialno, '')) LIKE :search")
                    .append(" OR LOWER(COALESCE(sm.stkmv_remark, '')) LIKE :search)")
            ;
        }
        if (visibleStockIds.size() != rootTreeStockIds.size()) {
            sql.append(" AND ((sm.stk_id_from IN (").append(joinIds(visibleStockIds)).append("))")
                    .append(" OR (sm.stk_id_to IN (").append(joinIds(visibleStockIds)).append(")))");
        }
        sql.append(" ORDER BY COALESCE(sm.stkmv_datetime2, sm.stkmv_timestamp) DESC LIMIT ").append(JOURNAL_LIMIT);

        Query query = entityManager.createNativeQuery(sql.toString());
        if (stockFromId != null) {
            query.setParameter("stockFromId", stockFromId);
        }
        if (stockToId != null) {
            query.setParameter("stockToId", stockToId);
        }
        if (articleId != null) {
            query.setParameter("articleId", articleId);
        }
        if (from != null) {
            query.setParameter("fromTime", from.atStartOfDay());
        }
        if (to != null) {
            query.setParameter("toTime", to.plusDays(1).atStartOfDay().minusSeconds(1));
        }
        if (search != null && !search.isBlank()) {
            query.setParameter("search", like(search));
        }

        @SuppressWarnings("unchecked")
        List<Object[]> rows = query.getResultList();
        List<WarehouseJournalEntryDto> result = new ArrayList<>();
        for (Object[] row : rows) {
            result.add(new WarehouseJournalEntryDto(
                    toLong(row[0]),
                    formatTimestamp(row[1]),
                    asString(row[2]),
                    asString(row[3]),
                    asString(row[4]),
                    asString(row[5]),
                    intValue(row[6]),
                    asString(row[7]),
                    asString(row[8]),
                    asString(row[9]),
                    toLong(row[10])
            ));
        }
        return result;
    }

    @Transactional
    public void moveArticle(UserSessionDto user, WarehouseMoveRequest request) {
        assertWarehouseAccess(user);
        if (!canMove(user)) {
            throw forbidden("Fuer den angemeldeten Benutzer sind Lagerbuchungen gesperrt.");
        }
        if (request == null || request.articleId() == null) {
            throw badRequest("Bitte zuerst einen Artikel waehlen.");
        }

        long sourceStockId = request.sourceStockId() != null ? request.sourceStockId() : 0L;
        long targetStockId = request.targetStockId() != null ? request.targetStockId() : 0L;
        if (sourceStockId == 0 && targetStockId == 0) {
            throw badRequest("Eingang und Ausgang duerfen nicht gleichzeitig virtuell sein.");
        }
        if (sourceStockId == targetStockId) {
            throw badRequest("Quelle und Ziel duerfen nicht identisch sein.");
        }

        ArticleDescriptor article = loadArticle(request.articleId());
        if (article == null) {
            throw badRequest("Der ausgewaehlte Artikel ist nicht bekannt.");
        }

        int itemQuantity = request.itemQuantity() != null ? request.itemQuantity() : 0;
        if (itemQuantity <= 0) {
            throw badRequest("Die Bewegungsmenge muss groesser als 0 sein.");
        }
        String serialNo = safeTrim(request.serialNo());
        if (article.serialTracked() && serialNo.isBlank()) {
            throw badRequest("Fuer seriennummerngepflegte Artikel ist eine Seriennummer erforderlich.");
        }
        if (article.serialTracked() && itemQuantity != 1) {
            throw badRequest("Seriennummerngepflegte Artikel duerfen nur mit Menge 1 bewegt werden.");
        }

        StockNode sourceStock = sourceStockId > 0 ? loadStockNode(sourceStockId) : null;
        StockNode targetStock = targetStockId > 0 ? loadStockNode(targetStockId) : null;

        if (sourceStock != null && !canWriteStock(user, sourceStock.id())) {
            throw forbidden("Fuer das Quelllager besteht keine Schreibberechtigung.");
        }
        if (targetStock != null && !canWriteStock(user, targetStock.id())) {
            throw forbidden("Fuer das Ziellager besteht keine Schreibberechtigung.");
        }

        List<String> serialData = blankDataFields();
        Long sourceArticleItemId = null;
        if (sourceStock != null) {
            int sourceQuantity = loadDirectArticleQuantity(sourceStock.id(), article.id());
            if (sourceQuantity < itemQuantity) {
                throw badRequest("Die angeforderte Bewegungsmenge ist im Quelllager nicht verfuegbar.");
            }
            if (article.serialTracked()) {
                ArticleItemDescriptor articleItem = loadArticleItem(article.id(), sourceStock.id(), serialNo);
                if (articleItem == null) {
                    throw badRequest("Die angegebene Seriennummer befindet sich nicht im Bestand des Quelllagers.");
                }
                sourceArticleItemId = articleItem.id();
                serialData = articleItem.dataFields();
            }
        }

        if (targetStock != null) {
            if (article.serialTracked() && serialExists(article.id(), serialNo, sourceArticleItemId)) {
                throw badRequest("Die angegebene Seriennummer existiert bereits in einem anderen Lagerbestand.");
            }
            if (targetStock.planning() && targetStock.maxQuantity() > 0) {
                int freeItemCapacity = freeItemCapacity(targetStock, article);
                if (freeItemCapacity < itemQuantity) {
                    throw badRequest("Die Bewegungsmenge passt kapazitativ nicht auf das Ziellager.");
                }
            }
        }

        LocalDateTime bookingTime = LocalDateTime.now();
        insertStockMove(user, article, sourceStockId, targetStockId, itemQuantity, serialNo, safeTrim(request.tan()),
                safeTrim(request.remark()), request.jobId(), bookingTime, serialData);

        if (sourceStock != null) {
            updateStockArticle(sourceStock.id(), article, loadDirectArticleQuantity(sourceStock.id(), article.id()) - itemQuantity);
            if (article.serialTracked() && sourceArticleItemId != null) {
                deleteArticleItem(sourceArticleItemId);
            }
        }
        if (targetStock != null) {
            updateStockArticle(targetStock.id(), article, loadDirectArticleQuantity(targetStock.id(), article.id()) + itemQuantity);
            if (article.serialTracked()) {
                insertArticleItem(article.id(), targetStock.id(), serialNo, serialData);
            }
        }
    }

    private void assertWarehouseAccess(UserSessionDto user) {
        if (user == null || !accessControlService.canViewWarehouse(user)) {
            throw forbidden("Kein Zugriff auf das Lagermodul.");
        }
    }

    private boolean canMove(UserSessionDto user) {
        if (user == null) {
            return false;
        }
        if (accessControlService.isHeadquartersUser(user)) {
            return accessControlService.canViewWarehouse(user);
        }
        return !isWarehouseReadOnly(user);
    }

    private boolean canManageArticles(UserSessionDto user) {
        if (user == null) {
            return false;
        }
        if (accessControlService.isHeadquartersUser(user)) {
            return accessControlService.canViewWarehouse(user);
        }
        return "1".equals(parameterValue(user, ParameterKeys.MASK_STK_ARTICLE_ACCESS));
    }

    private boolean isWarehouseReadOnly(UserSessionDto user) {
        return "1".equals(parameterValue(user, ParameterKeys.MASK_STK_READONLY));
    }

    private String parameterValue(UserSessionDto user, String key) {
        if (user == null || user.getEmployeeId() == null || user.getHeadquartersId() == null) {
            return "";
        }
        return parameterService.getParameterValue(user.getEmployeeId(), key, user.getHeadquartersId());
    }

    private List<WarehouseRootStockDto> loadAccessibleRoots(UserSessionDto user) {
        List<Long> accessibleRootIds = accessibleRootIds(user);
        if (accessibleRootIds.isEmpty()) {
            return List.of();
        }

        StringBuilder sql = new StringBuilder(
                "SELECT stk_id, stk_name, COALESCE(stk_barcode, ''), COALESCE(stk_planning, 0), COALESCE(stk_maxquantity, 0) " +
                        "FROM stock WHERE stk_id IN (")
                .append(joinIds(accessibleRootIds))
                .append(") ORDER BY stk_name");

        @SuppressWarnings("unchecked")
        List<Object[]> rows = entityManager.createNativeQuery(sql.toString()).getResultList();
        List<WarehouseRootStockDto> result = new ArrayList<>();
        for (Object[] row : rows) {
            result.add(new WarehouseRootStockDto(
                    toLong(row[0]),
                    asString(row[1]),
                    asString(row[2]),
                    intValue(row[3]) == 1,
                    intValue(row[4])
            ));
        }
        return result;
    }

    private List<Long> accessibleRootIds(UserSessionDto user) {
        if (user == null || user.getHeadquartersId() == null) {
            return List.of();
        }

        if (accessControlService.isHeadquartersUser(user)) {
            @SuppressWarnings("unchecked")
            List<Number> rows = entityManager.createNativeQuery(
                            "SELECT stk_id FROM stock WHERE hq_id = :hqId AND stk_pre_id = 0 AND COALESCE(stk_path, '') = '' ORDER BY stk_name")
                    .setParameter("hqId", user.getHeadquartersId())
                    .getResultList();
            return rows.stream().map(Number::longValue).toList();
        }

        List<Long> ids = parseIds(parameterValue(user, ParameterKeys.MASK_STK_ROOT_ACCESS));
        if (!ids.isEmpty()) {
            return ids;
        }

        Long defaultRootStockId = defaultRootStockId(user);
        if (defaultRootStockId != null) {
            return List.of(defaultRootStockId);
        }
        return List.of();
    }

    private Long determineDefaultRootStockId(UserSessionDto user, List<WarehouseRootStockDto> roots) {
        Long preferred = defaultRootStockId(user);
        if (preferred != null) {
            for (WarehouseRootStockDto root : roots) {
                if (root.id().equals(preferred)) {
                    return preferred;
                }
            }
        }
        return roots.isEmpty() ? null : roots.get(0).id();
    }

    private Long defaultRootStockId(UserSessionDto user) {
        if (user == null || user.getUserId() == 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", user.getUserId())
                .getResultList();
        if (rows.isEmpty()) {
            return null;
        }
        return rows.get(0).longValue();
    }

    private List<StockNode> loadRootTree(Long rootStockId) {
        @SuppressWarnings("unchecked")
        List<Object[]> rows = entityManager.createNativeQuery(
                        "SELECT s.stk_id, s.stk_pre_id, s.stk_name, COALESCE(s.stk_path, ''), COALESCE(s.stk_visible, 1), " +
                                "       COALESCE(s.stk_planning, 0), COALESCE(s.stk_maxquantity, 0), COALESCE(s.stk_barcode, ''), " +
                                "       COALESCE(a.ad_street, ''), COALESCE(s.stk_hsno, ''), COALESCE(a.ad_zipcode, ''), COALESCE(a.ad_city, '') " +
                                "FROM stock s " +
                                "LEFT JOIN address a ON a.ad_id = s.ad_id " +
                                "WHERE s.stk_id = :rootId OR COALESCE(s.stk_path, '') LIKE :path " +
                                "ORDER BY COALESCE(s.stk_path, ''), s.stk_name")
                .setParameter("rootId", rootStockId)
                .setParameter("path", "%//" + rootStockId + "//%")
                .getResultList();

        List<StockNode> result = new ArrayList<>();
        for (Object[] row : rows) {
            result.add(new StockNode(
                    toLong(row[0]),
                    toLong(row[1]),
                    asString(row[2]),
                    asString(row[3]),
                    intValue(row[4]) == 1,
                    intValue(row[5]) == 1,
                    intValue(row[6]),
                    asString(row[7]),
                    asString(row[8]),
                    asString(row[9]),
                    asString(row[10]),
                    asString(row[11])
            ));
        }
        return result;
    }

    private StockNode loadStockNode(Long stockId) {
        List<StockNode> rows = loadStocksByIds(List.of(stockId));
        if (rows.isEmpty()) {
            throw badRequest("Der ausgewaehlte Lagerort ist nicht bekannt.");
        }
        return rows.get(0);
    }

    private List<StockNode> loadStocksByIds(Collection<Long> stockIds) {
        if (stockIds == null || stockIds.isEmpty()) {
            return List.of();
        }
        @SuppressWarnings("unchecked")
        List<Object[]> rows = entityManager.createNativeQuery(
                        "SELECT s.stk_id, s.stk_pre_id, s.stk_name, COALESCE(s.stk_path, ''), COALESCE(s.stk_visible, 1), " +
                                "       COALESCE(s.stk_planning, 0), COALESCE(s.stk_maxquantity, 0), COALESCE(s.stk_barcode, ''), " +
                                "       COALESCE(a.ad_street, ''), COALESCE(s.stk_hsno, ''), COALESCE(a.ad_zipcode, ''), COALESCE(a.ad_city, '') " +
                                "FROM stock s " +
                                "LEFT JOIN address a ON a.ad_id = s.ad_id " +
                                "WHERE s.stk_id IN (" + joinIds(stockIds) + ")")
                .getResultList();

        List<StockNode> result = new ArrayList<>();
        for (Object[] row : rows) {
            result.add(new StockNode(
                    toLong(row[0]),
                    toLong(row[1]),
                    asString(row[2]),
                    asString(row[3]),
                    intValue(row[4]) == 1,
                    intValue(row[5]) == 1,
                    intValue(row[6]),
                    asString(row[7]),
                    asString(row[8]),
                    asString(row[9]),
                    asString(row[10]),
                    asString(row[11])
            ));
        }
        return result;
    }

    private Map<Long, Integer> loadUsedCapacity(List<Long> stockIds) {
        if (stockIds.isEmpty()) {
            return Map.of();
        }
        @SuppressWarnings("unchecked")
        List<Object[]> rows = entityManager.createNativeQuery(
                        "SELECT stk_id, COALESCE(SUM(stkat_quantity), 0) " +
                                "FROM stockarticle WHERE stk_id IN (" + joinIds(stockIds) + ") GROUP BY stk_id")
                .getResultList();
        Map<Long, Integer> result = new HashMap<>();
        for (Object[] row : rows) {
            result.put(toLong(row[0]), intValue(row[1]));
        }
        return result;
    }

    private Map<Long, Integer> loadReservedCapacity(List<Long> stockIds) {
        if (stockIds.isEmpty()) {
            return Map.of();
        }
        @SuppressWarnings("unchecked")
        List<Object[]> rows = entityManager.createNativeQuery(
                        "SELECT stk_pre_id, COALESCE(SUM(stk_maxquantity), 0) " +
                                "FROM stock WHERE stk_pre_id IN (" + joinIds(stockIds) + ") GROUP BY stk_pre_id")
                .getResultList();
        Map<Long, Integer> result = new HashMap<>();
        for (Object[] row : rows) {
            result.put(toLong(row[0]), intValue(row[1]));
        }
        return result;
    }

    private List<Long> scopedDisplayStockIds(UserSessionDto user,
                                             List<StockNode> rootTree,
                                             Long stockId,
                                             boolean includeSubstocks) {
        Map<Long, StockNode> nodesById = rootTree.stream().collect(Collectors.toMap(StockNode::id, node -> node));
        List<Long> candidateIds = rootTree.stream()
                .filter(node -> includeSubstocks
                        ? isDescendantOrSelf(node.id(), node.path(), stockId)
                        : Objects.equals(node.id(), stockId))
                .map(StockNode::id)
                .toList();
        if (candidateIds.isEmpty()) {
            return List.of();
        }

        Set<Long> visibleStockIds = new LinkedHashSet<>(displayVisibleStockIds(user, rootTree));
        if (visibleStockIds.isEmpty()) {
            return candidateIds;
        }

        return candidateIds.stream()
                .filter(visibleStockIds::contains)
                .filter(nodesById::containsKey)
                .toList();
    }

    private List<Long> displayVisibleStockIds(UserSessionDto user, List<StockNode> rootTree) {
        if (!accessControlService.isWarehouseUser(user)) {
            return rootTree.stream().map(StockNode::id).toList();
        }

        if (!"1".equals(parameterValue(user, ParameterKeys.MASK_STK_READONLY_WHERE_DEFINED_WRITEACCESS))) {
            return rootTree.stream().map(StockNode::id).toList();
        }

        Long rootId = rootStockId(rootTree.get(0));
        List<Long> restrictedRoots = restrictedWriteSubstocks(user).getOrDefault(rootId, List.of());
        if (restrictedRoots.isEmpty()) {
            return rootTree.stream().map(StockNode::id).toList();
        }

        return rootTree.stream()
                .filter(node -> restrictedRoots.stream().anyMatch(allowedId -> isDescendantOrSelf(node.id(), node.path(), allowedId)))
                .map(StockNode::id)
                .toList();
    }

    private boolean canWriteStock(UserSessionDto user, Long stockId) {
        if (stockId == null || stockId <= 0) {
            return canMove(user);
        }
        if (!canMove(user)) {
            return false;
        }
        if (accessControlService.isHeadquartersUser(user)) {
            return accessControlService.canViewWarehouse(user);
        }

        StockNode stock = loadStockNode(stockId);
        Long rootId = rootStockId(stock);
        if (!accessibleRootIds(user).contains(rootId)) {
            return false;
        }

        List<Long> restrictions = restrictedWriteSubstocks(user).get(rootId);
        if (restrictions == null || restrictions.isEmpty()) {
            return true;
        }
        return restrictions.stream().anyMatch(allowedId -> isDescendantOrSelf(stock.id(), stock.path(), allowedId));
    }

    private Map<Long, List<Long>> restrictedWriteSubstocks(UserSessionDto user) {
        if (user == null || user.getEmployeeId() == null || !accessControlService.isWarehouseUser(user)) {
            return Map.of();
        }
        return parseSubstockAccess(parameterValue(user, ParameterKeys.MASK_STK_SUBSTOCK_ACCESS));
    }

    private Long rootStockId(StockNode stock) {
        if (stock == null) {
            return null;
        }
        if (stock.path() == null || stock.path().isBlank()) {
            return stock.id();
        }
        List<Long> ids = parsePathIds(stock.path());
        return ids.isEmpty() ? stock.id() : ids.get(0);
    }

    private int loadDirectArticleQuantity(Long stockId, Long articleId) {
        Number value = (Number) entityManager.createNativeQuery(
                        "SELECT COALESCE(SUM(stkat_itemquantity), 0) FROM stockarticle WHERE stk_id = :stockId AND at_id = :articleId")
                .setParameter("stockId", stockId)
                .setParameter("articleId", articleId)
                .getSingleResult();
        return value != null ? value.intValue() : 0;
    }

    private int freeItemCapacity(StockNode stock, ArticleDescriptor article) {
        int usedCapacity = loadUsedCapacity(List.of(stock.id())).getOrDefault(stock.id(), 0);
        int reservedCapacity = loadReservedCapacity(List.of(stock.id())).getOrDefault(stock.id(), 0);
        int freeAreaCapacity = Math.max(0, stock.maxQuantity() - usedCapacity - reservedCapacity);
        return (int) Math.ceil((freeAreaCapacity * (double) article.stkItemQuantity()) / article.stkAreaQuantity());
    }

    private ArticleDescriptor loadArticle(Long articleId) {
        @SuppressWarnings("unchecked")
        List<Object[]> rows = entityManager.createNativeQuery(
                        "SELECT at_id, at_name, COALESCE(at_serialno, 0), COALESCE(at_stk_itemquantity, 1), " +
                                "COALESCE(at_stk_areaquantity, 1), COALESCE(at_bundlequantity, 1), COALESCE(at_bundlecode, '') " +
                                "FROM article WHERE at_id = :articleId")
                .setParameter("articleId", articleId)
                .getResultList();
        if (rows.isEmpty()) {
            return null;
        }
        Object[] row = rows.get(0);
        return new ArticleDescriptor(
                toLong(row[0]),
                asString(row[1]),
                intValue(row[2]) == 1,
                Math.max(1, intValue(row[3])),
                Math.max(1, intValue(row[4])),
                Math.max(1, intValue(row[5])),
                asString(row[6])
        );
    }

    private ArticleItemDescriptor loadArticleItem(Long articleId, Long stockId, String serialNo) {
        @SuppressWarnings("unchecked")
        List<Object[]> rows = entityManager.createNativeQuery(
                        "SELECT ati_id, " + buildDataFieldSelection("ai") +
                                " FROM articleitem ai WHERE ai.at_id = :articleId AND ai.stk_id = :stockId AND ai.ati_serialno = :serialNo")
                .setParameter("articleId", articleId)
                .setParameter("stockId", stockId)
                .setParameter("serialNo", serialNo)
                .getResultList();
        if (rows.isEmpty()) {
            return null;
        }
        Object[] row = rows.get(0);
        return new ArticleItemDescriptor(toLong(row[0]), extractDataFields(row, 1));
    }

    private boolean serialExists(Long articleId, String serialNo, Long ignoredItemId) {
        StringBuilder sql = new StringBuilder(
                "SELECT COUNT(*) FROM articleitem WHERE at_id = :articleId AND ati_serialno = :serialNo");
        if (ignoredItemId != null) {
            sql.append(" AND ati_id <> :ignoredItemId");
        }
        Query query = entityManager.createNativeQuery(sql.toString())
                .setParameter("articleId", articleId)
                .setParameter("serialNo", serialNo);
        if (ignoredItemId != null) {
            query.setParameter("ignoredItemId", ignoredItemId);
        }
        Number result = (Number) query.getSingleResult();
        return result != null && result.intValue() > 0;
    }

    private void insertStockMove(UserSessionDto user,
                                 ArticleDescriptor article,
                                 long sourceStockId,
                                 long targetStockId,
                                 int itemQuantity,
                                 String serialNo,
                                 String tan,
                                 String remark,
                                 Long jobId,
                                 LocalDateTime bookingTime,
                                 List<String> dataFields) {
        String data = dataFields.stream().anyMatch(value -> value != null && !value.isBlank())
                ? String.join("<br>", dataFields)
                : "";
        entityManager.createNativeQuery(
                        "INSERT INTO stockmove (" +
                                "stkmv_timestamp, stk_id_from, stk_id_to, at_id, stkmv_itemquantity, stkmv_bundlequantity, " +
                                "stkmv_bundlecode, stkmv_tan, stkmv_remark, stkmv_serialno, stkmv_data, at_id_bundle, " +
                                "stkmv_datetime, stkmv_datetime2, jb_id, usr_id" +
                                ") VALUES (" +
                                ":timestamp, :stockFromId, :stockToId, :articleId, :itemQuantity, :bundleQuantity, " +
                                ":bundleCode, :tan, :remark, :serialNo, :data, 0, :bookingTime, :bookingTime, :jobId, :userId)")
                .setParameter("timestamp", bookingTime)
                .setParameter("stockFromId", sourceStockId)
                .setParameter("stockToId", targetStockId)
                .setParameter("articleId", article.id())
                .setParameter("itemQuantity", itemQuantity)
                .setParameter("bundleQuantity", article.bundleQuantity())
                .setParameter("bundleCode", article.bundleCode())
                .setParameter("tan", tan)
                .setParameter("remark", remark)
                .setParameter("serialNo", serialNo)
                .setParameter("data", data)
                .setParameter("bookingTime", bookingTime)
                .setParameter("jobId", jobId != null ? jobId : 0L)
                .setParameter("userId", user.getUserId() != null ? user.getUserId() : 0L)
                .executeUpdate();
    }

    private void updateStockArticle(Long stockId, ArticleDescriptor article, int newItemQuantity) {
        if (newItemQuantity <= 0) {
            entityManager.createNativeQuery("DELETE FROM stockarticle WHERE stk_id = :stockId AND at_id = :articleId")
                    .setParameter("stockId", stockId)
                    .setParameter("articleId", article.id())
                    .executeUpdate();
            return;
        }

        int areaQuantity = (int) Math.ceil((newItemQuantity * (double) article.stkAreaQuantity()) / article.stkItemQuantity());
        Number existing = (Number) entityManager.createNativeQuery(
                        "SELECT COUNT(*) FROM stockarticle WHERE stk_id = :stockId AND at_id = :articleId")
                .setParameter("stockId", stockId)
                .setParameter("articleId", article.id())
                .getSingleResult();

        if (existing != null && existing.intValue() > 0) {
            entityManager.createNativeQuery(
                            "UPDATE stockarticle SET stkat_quantity = :areaQuantity, stkat_itemquantity = :itemQuantity " +
                                    "WHERE stk_id = :stockId AND at_id = :articleId")
                    .setParameter("areaQuantity", areaQuantity)
                    .setParameter("itemQuantity", newItemQuantity)
                    .setParameter("stockId", stockId)
                    .setParameter("articleId", article.id())
                    .executeUpdate();
        } else {
            entityManager.createNativeQuery(
                            "INSERT INTO stockarticle (stk_id, at_id, stkat_quantity, stkat_itemquantity) " +
                                    "VALUES (:stockId, :articleId, :areaQuantity, :itemQuantity)")
                    .setParameter("stockId", stockId)
                    .setParameter("articleId", article.id())
                    .setParameter("areaQuantity", areaQuantity)
                    .setParameter("itemQuantity", newItemQuantity)
                    .executeUpdate();
        }
    }

    private void deleteArticleItem(Long itemId) {
        entityManager.createNativeQuery("DELETE FROM articleitem WHERE ati_id = :itemId")
                .setParameter("itemId", itemId)
                .executeUpdate();
    }

    private void insertArticleItem(Long articleId, Long stockId, String serialNo, List<String> dataFields) {
        StringBuilder sql = new StringBuilder("INSERT INTO articleitem (at_id, stk_id, ati_serialno");
        for (int index = 1; index <= DATA_FIELD_COUNT; index++) {
            sql.append(", ati_data_").append(pad(index));
        }
        sql.append(") VALUES (:articleId, :stockId, :serialNo");
        for (int index = 1; index <= DATA_FIELD_COUNT; index++) {
            sql.append(", :data").append(index);
        }
        sql.append(")");

        Query query = entityManager.createNativeQuery(sql.toString())
                .setParameter("articleId", articleId)
                .setParameter("stockId", stockId)
                .setParameter("serialNo", serialNo);
        for (int index = 1; index <= DATA_FIELD_COUNT; index++) {
            query.setParameter("data" + index, safeIndex(dataFields, index - 1));
        }
        query.executeUpdate();
    }

    private static String buildDataFieldSelection(String alias) {
        List<String> fields = new ArrayList<>();
        for (int index = 1; index <= DATA_FIELD_COUNT; index++) {
            fields.add("COALESCE(" + alias + ".ati_data_" + pad(index) + ", '')");
        }
        return String.join(", ", fields);
    }

    private static List<String> extractDataFields(Object[] row, int startIndex) {
        List<String> result = new ArrayList<>();
        for (int index = startIndex; index < startIndex + DATA_FIELD_COUNT; index++) {
            result.add(asString(row[index]));
        }
        return result;
    }

    private static List<String> blankDataFields() {
        return Collections.nCopies(DATA_FIELD_COUNT, "");
    }

    private static String safeIndex(List<String> values, int index) {
        if (values == null || index < 0 || index >= values.size()) {
            return "";
        }
        return safeTrim(values.get(index));
    }

    private static String safeTrim(String value) {
        return value != null ? value.trim() : "";
    }

    private static int levelFromPath(String path) {
        return parsePathIds(path).size();
    }

    private static List<Long> parsePathIds(String path) {
        if (path == null || path.isBlank()) {
            return List.of();
        }
        List<Long> result = new ArrayList<>();
        for (String part : path.split("/")) {
            if (part == null || part.isBlank()) {
                continue;
            }
            try {
                result.add(Long.parseLong(part));
            } catch (NumberFormatException ignored) {
            }
        }
        return result;
    }

    private static boolean isDescendantOrSelf(Long stockId, String path, Long candidateAncestorId) {
        if (stockId == null || candidateAncestorId == null) {
            return false;
        }
        return stockId.equals(candidateAncestorId)
                || (path != null && path.contains("//" + candidateAncestorId + "//"));
    }

    private static Map<Long, List<Long>> parseSubstockAccess(String rawValue) {
        if (rawValue == null || rawValue.isBlank()) {
            return Map.of();
        }
        Map<Long, List<Long>> result = new LinkedHashMap<>();
        String normalized = rawValue.replace("|", "--");
        for (String tuple : normalized.split("--")) {
            if (tuple == null || tuple.isBlank()) {
                continue;
            }
            List<Long> ids = parseIds(tuple);
            if (ids.size() <= 1) {
                continue;
            }
            result.put(ids.get(0), ids.subList(1, ids.size()));
        }
        return result;
    }

    private static List<Long> parseIds(String rawValue) {
        if (rawValue == null || rawValue.isBlank()) {
            return List.of();
        }
        List<Long> result = new ArrayList<>();
        for (String value : rawValue.split(",")) {
            if (value == null || value.isBlank()) {
                continue;
            }
            try {
                result.add(Long.parseLong(value.trim()));
            } catch (NumberFormatException ignored) {
            }
        }
        return result;
    }

    private static String joinIds(Collection<Long> ids) {
        return ids.stream()
                .filter(Objects::nonNull)
                .map(String::valueOf)
                .collect(Collectors.joining(","));
    }

    private static String like(String value) {
        return "%" + value.toLowerCase(Locale.ROOT).trim() + "%";
    }

    private static String buildAddressLabel(StockNode node) {
        List<String> parts = new ArrayList<>();
        String street = safeTrim(node.street());
        String houseNumber = safeTrim(node.houseNumber());
        if (!street.isBlank()) {
            parts.add(street + (!houseNumber.isBlank() ? " " + houseNumber : ""));
        }
        String zipcode = safeTrim(node.zipcode());
        String city = safeTrim(node.city());
        if (!zipcode.isBlank() || !city.isBlank()) {
            parts.add((zipcode + " " + city).trim());
        }
        return String.join(", ", parts);
    }

    private static String pad(int value) {
        return value < 10 ? "0" + value : String.valueOf(value);
    }

    private static String formatTimestamp(Object value) {
        if (value instanceof LocalDateTime dateTime) {
            return dateTime.format(JOURNAL_TIMESTAMP);
        }
        return asString(value);
    }

    private static int intValue(Object value) {
        if (value instanceof Integer integer) {
            return integer;
        }
        if (value instanceof Long longValue) {
            return longValue.intValue();
        }
        if (value instanceof BigDecimal decimal) {
            return decimal.intValue();
        }
        if (value instanceof Number number) {
            return number.intValue();
        }
        if (value == null) {
            return 0;
        }
        try {
            return Integer.parseInt(value.toString());
        } catch (NumberFormatException ignored) {
            return 0;
        }
    }

    private static Long toLong(Object value) {
        if (value instanceof Long longValue) {
            return longValue;
        }
        if (value instanceof Integer integer) {
            return integer.longValue();
        }
        if (value instanceof BigDecimal decimal) {
            return decimal.longValue();
        }
        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 static String asString(Object value) {
        return value != null ? value.toString() : "";
    }

    private static ResponseStatusException forbidden(String message) {
        return new ResponseStatusException(HttpStatus.FORBIDDEN, message);
    }

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

    private record StockNode(
            Long id,
            Long parentId,
            String name,
            String path,
            boolean visible,
            boolean planning,
            int maxQuantity,
            String barcode,
            String street,
            String houseNumber,
            String zipcode,
            String city
    ) {
    }

    private record ArticleDescriptor(
            Long id,
            String name,
            boolean serialTracked,
            int stkItemQuantity,
            int stkAreaQuantity,
            int bundleQuantity,
            String bundleCode
    ) {
    }

    private record ArticleItemDescriptor(Long id, List<String> dataFields) {
    }
}
