package de.votian.services.service;

import de.votian.services.dto.UserSessionDto;
import de.votian.services.security.AccessControlService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;

@Service
@Transactional
public class GenericExportWorkspaceService {

    private static final DateTimeFormatter FILE_NAME_TIMESTAMP = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
    private static final DateTimeFormatter DISPLAY_TIMESTAMP = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final Set<Long> JOB_BASED_CATEGORIES = Set.of(4L, 5L, 6L, 7L, 8L, 9L, 100L, 110L, 115L, 120L, 125L);

    private final JdbcTemplate jdbcTemplate;
    private final AccessControlService accessControlService;
    private final Path exportRoot;

    public GenericExportWorkspaceService(JdbcTemplate jdbcTemplate,
                                         AccessControlService accessControlService,
                                         @Value("${votian.storage.export-workspace-dir:../html/export/download/_ng}") String exportRootDir) {
        this.jdbcTemplate = jdbcTemplate;
        this.accessControlService = accessControlService;
        this.exportRoot = Path.of(exportRootDir).toAbsolutePath().normalize();
    }

    @Transactional(readOnly = true)
    public Bootstrap bootstrap(UserSessionDto user, Long headquartersId) {
        Scope scope = requireScope(user, headquartersId, null);
        List<CategoryOption> categories = jdbcTemplate.query(
                """
                        SELECT expc_id, expc_name, expc_usrtype
                        FROM exportcategory
                        WHERE expc_type = '0'
                          AND (expc_usrtype = ? OR expc_usrtype = ?)
                        ORDER BY expc_id
                        """,
                (rs, rowNum) -> new CategoryOption(
                        rs.getLong("expc_id"),
                        blankToEmpty(rs.getString("expc_name")),
                        blankToEmpty(rs.getString("expc_usrtype"))
                ),
                scope.userTypeFilter(),
                scope.userTypeNameFilter()
        );
        return new Bootstrap(categories, accessControlService.isCustomerUser(user), scope.customerId());
    }

    @Transactional(readOnly = true)
    public List<ProfileRow> findProfiles(UserSessionDto user,
                                         Long headquartersId,
                                         Long customerId,
                                         Long categoryId) {
        Scope scope = requireScope(user, headquartersId, customerId);
        requireCategory(scope, categoryId);
        return jdbcTemplate.query(
                """
                        SELECT expp_id,
                               expp_name,
                               expp_parstr,
                               expp_headline,
                               expp_bolchars,
                               expp_eolchars,
                               expp_bofchars,
                               expp_eofchars,
                               expp_fileextension
                        FROM exportparameters
                        WHERE hq_id = ?
                          AND cs_id = ?
                          AND expc_id = ?
                        ORDER BY expp_name, expp_id
                        """,
                (rs, rowNum) -> new ProfileRow(
                        rs.getLong("expp_id"),
                        blankToEmpty(rs.getString("expp_name")),
                        blankToEmpty(rs.getString("expp_parstr")),
                        "1".equals(blankToEmpty(rs.getString("expp_headline"))),
                        blankToEmpty(rs.getString("expp_bolchars")),
                        blankToEmpty(rs.getString("expp_eolchars")),
                        blankToEmpty(rs.getString("expp_bofchars")),
                        blankToEmpty(rs.getString("expp_eofchars")),
                        blankToEmpty(rs.getString("expp_fileextension"))
                ),
                scope.headquartersId(),
                scope.customerId(),
                categoryId
        );
    }

    public ProfileRow saveProfile(UserSessionDto user,
                                  Long headquartersId,
                                  Long customerId,
                                  Long categoryId,
                                  Long profileId,
                                  String name,
                                  String parameterString,
                                  boolean headline,
                                  String beginOfLineChars,
                                  String endOfLineChars,
                                  String beginOfFileChars,
                                  String endOfFileChars,
                                  String fileExtension) {
        Scope scope = requireScope(user, headquartersId, customerId);
        requireCategory(scope, categoryId);

        String normalizedName = requireText(name, "Profilname");
        String normalizedParameterString = parameterString != null ? parameterString.trim() : "";
        String normalizedFileExtension = normalizeExtension(fileExtension);

        if (profileId != null && profileId > 0) {
            int updated = jdbcTemplate.update(
                    """
                            UPDATE exportparameters
                            SET expp_name = ?,
                                expp_parstr = ?,
                                expp_headline = ?,
                                expp_bolchars = ?,
                                expp_eolchars = ?,
                                expp_bofchars = ?,
                                expp_eofchars = ?,
                                expp_fileextension = ?
                            WHERE expp_id = ?
                              AND hq_id = ?
                              AND cs_id = ?
                              AND expc_id = ?
                            """,
                    normalizedName,
                    normalizedParameterString,
                    headline ? "1" : "0",
                    trimToEmpty(beginOfLineChars),
                    trimToEmpty(endOfLineChars),
                    trimToEmpty(beginOfFileChars),
                    trimToEmpty(endOfFileChars),
                    normalizedFileExtension,
                    profileId,
                    scope.headquartersId(),
                    scope.customerId(),
                    categoryId
            );
            if (updated != 1) {
                throw new IllegalArgumentException("Exportprofil konnte nicht aktualisiert werden.");
            }
            return loadProfile(profileId, scope.headquartersId(), scope.customerId(), categoryId);
        }

        KeyHolder keyHolder = new GeneratedKeyHolder();
        jdbcTemplate.update(connection -> {
            PreparedStatement statement = connection.prepareStatement(
                    """
                            INSERT INTO exportparameters
                                (hq_id, cs_id, expc_id, expp_parstr, expp_headline,
                                 expp_bolchars, expp_eolchars, expp_bofchars, expp_eofchars,
                                 expp_name, expp_fileextension)
                            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                            """,
                    Statement.RETURN_GENERATED_KEYS
            );
            statement.setLong(1, scope.headquartersId());
            statement.setLong(2, scope.customerId());
            statement.setLong(3, categoryId);
            statement.setString(4, normalizedParameterString);
            statement.setString(5, headline ? "1" : "0");
            statement.setString(6, trimToEmpty(beginOfLineChars));
            statement.setString(7, trimToEmpty(endOfLineChars));
            statement.setString(8, trimToEmpty(beginOfFileChars));
            statement.setString(9, trimToEmpty(endOfFileChars));
            statement.setString(10, normalizedName);
            statement.setString(11, normalizedFileExtension);
            return statement;
        }, keyHolder);

        return loadProfile(requireGeneratedId(keyHolder, "Exportprofil"), scope.headquartersId(), scope.customerId(), categoryId);
    }

    public void deleteProfile(UserSessionDto user,
                              Long headquartersId,
                              Long customerId,
                              Long categoryId,
                              Long profileId) {
        Scope scope = requireScope(user, headquartersId, customerId);
        requireCategory(scope, categoryId);
        int updated = jdbcTemplate.update(
                """
                        DELETE FROM exportparameters
                        WHERE expp_id = ?
                          AND hq_id = ?
                          AND cs_id = ?
                          AND expc_id = ?
                        """,
                profileId,
                scope.headquartersId(),
                scope.customerId(),
                categoryId
        );
        if (updated != 1) {
            throw new IllegalArgumentException("Exportprofil konnte nicht geloescht werden.");
        }
    }

    public ExportResult generateExport(UserSessionDto user,
                                       Long headquartersId,
                                       Long customerId,
                                       Long categoryId,
                                       Long profileId,
                                       LocalDate from,
                                       LocalDate to,
                                       Integer statusFilter,
                                       String customerCodeFilter,
                                       String delimiter,
                                       String fileName,
                                       boolean includeHeadline) {
        Scope scope = requireScope(user, headquartersId, customerId);
        CategoryOption category = requireCategory(scope, categoryId);
        ProfileRow profile = profileId != null && profileId > 0
                ? loadProfile(profileId, scope.headquartersId(), scope.customerId(), categoryId)
                : null;

        ExportPayload payload = buildPayload(categoryId, scope.headquartersId(), from, to, statusFilter, customerCodeFilter);
        String resolvedDelimiter = delimiter != null && !delimiter.isBlank() ? delimiter : ";";
        String resolvedFileName = buildFileName(fileName, category.name(), profile != null ? profile.fileExtension() : "");
        byte[] data = renderExport(payload, resolvedDelimiter, includeHeadline || (profile != null && profile.headline()), profile);

        String cryptName = UUID.randomUUID().toString().replace("-", "") + "_" + resolvedFileName;
        writeExportFile(cryptName, data);

        LocalDateTime now = LocalDateTime.now();
        KeyHolder keyHolder = new GeneratedKeyHolder();
        jdbcTemplate.update(connection -> {
            PreparedStatement statement = connection.prepareStatement(
                    """
                            INSERT INTO exportfiles
                                (hq_id, cs_id, expf_name, expf_cryptname, expf_storetime, expf_ftpupload)
                            VALUES (?, ?, ?, ?, ?, ?)
                            """,
                    Statement.RETURN_GENERATED_KEYS
            );
            statement.setLong(1, scope.headquartersId());
            statement.setLong(2, scope.customerId());
            statement.setString(3, resolvedFileName);
            statement.setString(4, cryptName);
            statement.setTimestamp(5, Timestamp.valueOf(now));
            statement.setString(6, "0");
            return statement;
        }, keyHolder);

        Long fileId = requireGeneratedId(keyHolder, "Exportdatei");
        return new ExportResult(
                loadFile(scope, fileId),
                payload.rows().size(),
                payload.description()
        );
    }

    @Transactional(readOnly = true)
    public List<ExportFileRow> listFiles(UserSessionDto user, Long headquartersId, Long customerId) {
        Scope scope = requireScope(user, headquartersId, customerId);
        return jdbcTemplate.query(
                """
                        SELECT ef.expf_id,
                               ef.expf_name,
                               ef.expf_storetime,
                               ef.expf_ftpupload,
                               ef.cs_id,
                               cs.cs_eid
                        FROM exportfiles AS ef
                        LEFT JOIN customer AS cs ON cs.cs_id = ef.cs_id
                        WHERE ef.hq_id = ?
                          AND ef.cs_id = ?
                        ORDER BY ef.expf_storetime DESC, ef.expf_id DESC
                        """,
                (rs, rowNum) -> toExportFileRow(rs.getLong("expf_id"),
                        blankToEmpty(rs.getString("expf_name")),
                        rs.getTimestamp("expf_storetime"),
                        blankToEmpty(rs.getString("expf_ftpupload")),
                        rs.getLong("cs_id"),
                        blankToEmpty(rs.getString("cs_eid"))),
                scope.headquartersId(),
                scope.customerId()
        );
    }

    @Transactional(readOnly = true)
    public byte[] downloadFile(UserSessionDto user, Long headquartersId, Long customerId, Long fileId) {
        Scope scope = requireScope(user, headquartersId, customerId);
        StoredFile storedFile = loadStoredFile(scope, fileId);
        Path path = resolveStoredFile(storedFile.cryptName());
        if (!Files.isRegularFile(path)) {
            throw new IllegalArgumentException("Exportdatei liegt nicht mehr auf dem Server.");
        }
        try {
            return Files.readAllBytes(path);
        } catch (IOException e) {
            throw new IllegalArgumentException("Exportdatei konnte nicht gelesen werden.");
        }
    }

    public void deleteFile(UserSessionDto user, Long headquartersId, Long customerId, Long fileId) {
        Scope scope = requireScope(user, headquartersId, customerId);
        StoredFile storedFile = loadStoredFile(scope, fileId);
        jdbcTemplate.update(
                """
                        DELETE FROM exportfiles
                        WHERE expf_id = ?
                          AND hq_id = ?
                          AND cs_id = ?
                        """,
                fileId,
                scope.headquartersId(),
                scope.customerId()
        );
        try {
            Files.deleteIfExists(resolveStoredFile(storedFile.cryptName()));
        } catch (IOException ignored) {
        }
    }

    private ExportPayload buildPayload(Long categoryId,
                                       Long headquartersId,
                                       LocalDate from,
                                       LocalDate to,
                                       Integer statusFilter,
                                       String customerCodeFilter) {
        if (Objects.equals(categoryId, 1L)) {
            return customerPayload(headquartersId, customerCodeFilter);
        }
        if (Objects.equals(categoryId, 2L)) {
            return courierPayload(headquartersId);
        }
        if (Objects.equals(categoryId, 3L)) {
            return vehiclePayload(headquartersId);
        }
        if (JOB_BASED_CATEGORIES.contains(categoryId)) {
            return operationalPayload(categoryId, headquartersId, from, to, statusFilter, customerCodeFilter);
        }
        throw new IllegalArgumentException("Exportkategorie wird im NG-Workspace noch nicht unterstuetzt.");
    }

    private ExportPayload customerPayload(Long headquartersId, String customerCodeFilter) {
        StringBuilder sql = new StringBuilder(
                """
                        SELECT cs.cs_eid AS eid,
                               cmp.cmp_comp AS firma,
                               cmp.cmp_comp2 AS firma2,
                               ad.ad_street AS strasse,
                               ad.ad_zip AS plz,
                               ad.ad_city AS ort,
                               usr.usr_firstname AS ansprechpartner_vorname,
                               usr.usr_name AS ansprechpartner_name,
                               usr.usr_email AS email
                        FROM customer AS cs
                        JOIN company AS cmp ON cmp.cmp_id = cs.cmp_id
                        LEFT JOIN address AS ad ON ad.ad_id = cmp.ad_id
                        LEFT JOIN employee AS emp ON emp.emp_id = cs.cs_admin
                        LEFT JOIN user AS usr ON usr.usr_id = emp.usr_id
                        WHERE cs.hq_id = ?
                        """
        );
        List<Object> args = new ArrayList<>();
        args.add(headquartersId);
        String normalizedFilter = customerCodeFilter != null ? customerCodeFilter.trim().toLowerCase(Locale.ROOT) : "";
        if (!normalizedFilter.isBlank()) {
            sql.append(" AND (LOWER(cs.cs_eid) LIKE ? OR LOWER(cmp.cmp_comp) LIKE ?)");
            String token = "%" + normalizedFilter + "%";
            args.add(token);
            args.add(token);
        }
        sql.append(" ORDER BY cs.cs_eid, cmp.cmp_comp");
        return new ExportPayload(
                "Kundenstammdaten",
                List.of("eid", "firma", "firma2", "strasse", "plz", "ort", "ansprechpartner_vorname", "ansprechpartner_name", "email"),
                jdbcTemplate.query(Objects.requireNonNull(sql.toString()), (rs, rowNum) -> row(
                        "eid", rs.getString("eid"),
                        "firma", rs.getString("firma"),
                        "firma2", rs.getString("firma2"),
                        "strasse", rs.getString("strasse"),
                        "plz", rs.getString("plz"),
                        "ort", rs.getString("ort"),
                        "ansprechpartner_vorname", rs.getString("ansprechpartner_vorname"),
                        "ansprechpartner_name", rs.getString("ansprechpartner_name"),
                        "email", rs.getString("email")
                ), args.toArray())
        );
    }

    private ExportPayload courierPayload(Long headquartersId) {
        return new ExportPayload(
                "Transporteurstammdaten",
                List.of("eid", "sid", "firma", "nachname", "vorname", "email", "pda", "gruppen"),
                jdbcTemplate.query(
                        """
                                SELECT cr.cr_eid AS eid,
                                       cr.cr_sid AS sid,
                                       cmp.cmp_comp AS firma,
                                       usr.usr_name AS nachname,
                                       usr.usr_firstname AS vorname,
                                       usr.usr_email AS email,
                                       cr.cr_mobile_pda AS pda,
                                       cr.cr_msggrp AS gruppen
                                FROM courier AS cr
                                JOIN company AS cmp ON cmp.cmp_id = cr.cmp_id
                                LEFT JOIN user AS usr ON usr.usr_id = cr.usr_id
                                WHERE cr.hq_id = ?
                                ORDER BY cr.cr_eid, cr.cr_sid
                                """,
                        (rs, rowNum) -> row(
                                "eid", rs.getString("eid"),
                                "sid", rs.getString("sid"),
                                "firma", rs.getString("firma"),
                                "nachname", rs.getString("nachname"),
                                "vorname", rs.getString("vorname"),
                                "email", rs.getString("email"),
                                "pda", rs.getString("pda"),
                                "gruppen", rs.getString("gruppen")
                        ),
                        headquartersId
                )
        );
    }

    private ExportPayload vehiclePayload(Long headquartersId) {
        return new ExportPayload(
                "Fahrzeugstammdaten",
                List.of("fahrzeug_sid", "transporteur_eid", "transporteur_sid", "fahrzeugtyp"),
                jdbcTemplate.query(
                        """
                                SELECT crvh.crvh_sid AS fahrzeug_sid,
                                       cr.cr_eid AS transporteur_eid,
                                       cr.cr_sid AS transporteur_sid,
                                       crvh.vht_id AS fahrzeugtyp
                                FROM couriervehicle AS crvh
                                JOIN courier AS cr ON cr.cr_id = crvh.cr_id
                                WHERE cr.hq_id = ?
                                ORDER BY crvh.crvh_sid, cr.cr_eid
                                """,
                        (rs, rowNum) -> row(
                                "fahrzeug_sid", rs.getString("fahrzeug_sid"),
                                "transporteur_eid", rs.getString("transporteur_eid"),
                                "transporteur_sid", rs.getString("transporteur_sid"),
                                "fahrzeugtyp", rs.getObject("fahrzeugtyp")
                        ),
                        headquartersId
                )
        );
    }

    private ExportPayload operationalPayload(Long categoryId,
                                             Long headquartersId,
                                             LocalDate from,
                                             LocalDate to,
                                             Integer statusFilter,
                                             String customerCodeFilter) {
        StringBuilder sql = new StringBuilder(
                """
                        SELECT jb.jb_id AS auftrag,
                               jb.jb_status AS status,
                               jb.jb_ordertime AS auftragszeit,
                               jb.jb_finishtime AS abschluss,
                               jb.jb_totalprice AS kundenumsatz,
                               jb.jb_cr_price AS kurierpreis,
                               jb.jb_export_time AS exportiert_am,
                               jb.jb_storno AS storno,
                               cs.cs_eid AS kunde_eid,
                               cmp.cmp_comp AS kunde,
                               csc.csc_name AS kostenstelle,
                               cr.cr_sid AS kurier
                        FROM job AS jb
                        LEFT JOIN costcenter AS csc ON csc.csc_id = jb.csc_id_payer
                        LEFT JOIN customer AS cs ON cs.cs_id = csc.cs_id
                        LEFT JOIN company AS cmp ON cmp.cmp_id = cs.cmp_id
                        LEFT JOIN courier AS cr ON cr.cr_id = jb.cr_id
                        WHERE jb.hq_id = ?
                        """
        );
        List<Object> args = new ArrayList<>();
        args.add(headquartersId);
        LocalDate normalizedFrom = from != null ? from : LocalDate.now().withDayOfMonth(1);
        LocalDate normalizedTo = to != null ? to : LocalDate.now();
        sql.append(" AND jb.jb_ordertime >= ? AND jb.jb_ordertime <= ?");
        args.add(Timestamp.valueOf(normalizedFrom.atStartOfDay()));
        args.add(Timestamp.valueOf(normalizedTo.plusDays(1).atStartOfDay().minusNanos(1)));

        if (statusFilter != null) {
            switch (statusFilter) {
                case 1 -> sql.append(" AND (jb.jb_export_time IS NULL OR jb.jb_export_time = '0000-00-00 00:00:00')");
                case 2 -> sql.append(" AND jb.jb_export_time IS NOT NULL");
                default -> {
                }
            }
        }
        String normalizedCustomerFilter = customerCodeFilter != null ? customerCodeFilter.trim().toLowerCase(Locale.ROOT) : "";
        if (!normalizedCustomerFilter.isBlank()) {
            sql.append(" AND (LOWER(cs.cs_eid) LIKE ? OR LOWER(cmp.cmp_comp) LIKE ?)");
            String token = "%" + normalizedCustomerFilter + "%";
            args.add(token);
            args.add(token);
        }
        if (Objects.equals(categoryId, 5L)) {
            sql.append(" AND COALESCE(jb.jb_totalprice, 0) >= 0");
        } else if (Objects.equals(categoryId, 6L)) {
            sql.append(" AND COALESCE(jb.jb_totalprice, 0) < 0");
        } else if (Objects.equals(categoryId, 8L)) {
            sql.append(" AND COALESCE(jb.jb_cr_price, 0) <> 0");
        }
        sql.append(" ORDER BY jb.jb_ordertime, jb.jb_id");

        return new ExportPayload(
                "Bewegungsdaten Kategorie " + categoryId,
                List.of("auftrag", "status", "auftragszeit", "abschluss", "kunde_eid", "kunde", "kostenstelle", "kurier", "kundenumsatz", "kurierpreis", "storno", "exportiert_am"),
                jdbcTemplate.query(Objects.requireNonNull(sql.toString()), (rs, rowNum) -> row(
                        "auftrag", rs.getObject("auftrag"),
                        "status", rs.getObject("status"),
                        "auftragszeit", formatTimestamp(rs.getTimestamp("auftragszeit")),
                        "abschluss", formatTimestamp(rs.getTimestamp("abschluss")),
                        "kunde_eid", rs.getString("kunde_eid"),
                        "kunde", rs.getString("kunde"),
                        "kostenstelle", rs.getString("kostenstelle"),
                        "kurier", rs.getString("kurier"),
                        "kundenumsatz", formatAmount((BigDecimal) rs.getObject("kundenumsatz")),
                        "kurierpreis", formatAmount((BigDecimal) rs.getObject("kurierpreis")),
                        "storno", rs.getString("storno"),
                        "exportiert_am", formatTimestamp(rs.getTimestamp("exportiert_am"))
                ), args.toArray())
        );
    }

    private byte[] renderExport(ExportPayload payload,
                                String delimiter,
                                boolean includeHeadline,
                                ProfileRow profile) {
        StringBuilder builder = new StringBuilder();
        if (profile != null && !profile.beginOfFileChars().isBlank()) {
            builder.append(profile.beginOfFileChars());
        }
        if (includeHeadline) {
            appendRow(builder, payload.columns(), payload.columns(), delimiter, profile);
        }
        for (Map<String, String> row : payload.rows()) {
            appendRow(builder, payload.columns(), row, delimiter, profile);
        }
        if (profile != null && !profile.endOfFileChars().isBlank()) {
            builder.append(profile.endOfFileChars());
        }
        return builder.toString().getBytes(StandardCharsets.UTF_8);
    }

    private void appendRow(StringBuilder builder,
                           List<String> columns,
                           Map<String, String> row,
                           String delimiter,
                           ProfileRow profile) {
        if (profile != null && !profile.beginOfLineChars().isBlank()) {
            builder.append(profile.beginOfLineChars());
        }
        for (int i = 0; i < columns.size(); i++) {
            if (i > 0) {
                builder.append(delimiter);
            }
            String value = row.get(columns.get(i));
            builder.append(escapeDelimited(value, delimiter));
        }
        if (profile != null && !profile.endOfLineChars().isBlank()) {
            builder.append(profile.endOfLineChars());
        }
        builder.append('\n');
    }

    private void appendRow(StringBuilder builder,
                           List<String> columns,
                           List<String> values,
                           String delimiter,
                           ProfileRow profile) {
        if (profile != null && !profile.beginOfLineChars().isBlank()) {
            builder.append(profile.beginOfLineChars());
        }
        for (int i = 0; i < values.size(); i++) {
            if (i > 0) {
                builder.append(delimiter);
            }
            builder.append(escapeDelimited(values.get(i), delimiter));
        }
        if (profile != null && !profile.endOfLineChars().isBlank()) {
            builder.append(profile.endOfLineChars());
        }
        builder.append('\n');
    }

    private String escapeDelimited(String value, String delimiter) {
        String normalized = value != null ? value : "";
        boolean quoted = normalized.contains(delimiter)
                || normalized.contains("\"")
                || normalized.contains("\n")
                || normalized.contains("\r");
        if (!quoted) {
            return normalized;
        }
        return "\"" + normalized.replace("\"", "\"\"") + "\"";
    }

    private Scope requireScope(UserSessionDto user, Long headquartersId, Long customerId) {
        if (!accessControlService.canViewGenericExports(user)) {
            throw new IllegalArgumentException("Datenexport ist fuer den Benutzer nicht freigeschaltet.");
        }
        if (user == null || headquartersId == null || !accessControlService.canAccessHeadquarters(user, headquartersId)) {
            throw new IllegalArgumentException("Niederlassung ist fuer den Datenexport ungueltig.");
        }
        if (accessControlService.isCustomerUser(user)) {
            Long userCustomerId = user.getCustomerId();
            if (userCustomerId == null || userCustomerId <= 0) {
                throw new IllegalArgumentException("Kundenscope fuer den Datenexport ist unvollstaendig.");
            }
            if (customerId != null && !Objects.equals(customerId, userCustomerId)) {
                throw new IllegalArgumentException("Kundenscope fuer den Datenexport ist ungueltig.");
            }
            return new Scope(headquartersId, userCustomerId, "2", "cs");
        }
        return new Scope(headquartersId, 0L, "1", "hq");
    }

    private CategoryOption requireCategory(Scope scope, Long categoryId) {
        if (categoryId == null || categoryId <= 0) {
            throw new IllegalArgumentException("Exportkategorie fehlt.");
        }
        List<CategoryOption> matches = jdbcTemplate.query(
                """
                        SELECT expc_id, expc_name, expc_usrtype
                        FROM exportcategory
                        WHERE expc_id = ?
                          AND (expc_usrtype = ? OR expc_usrtype = ?)
                        """,
                (rs, rowNum) -> new CategoryOption(
                        rs.getLong("expc_id"),
                        blankToEmpty(rs.getString("expc_name")),
                        blankToEmpty(rs.getString("expc_usrtype"))
                ),
                categoryId,
                scope.userTypeFilter(),
                scope.userTypeNameFilter()
        );
        if (matches.isEmpty()) {
            throw new IllegalArgumentException("Exportkategorie wurde nicht gefunden.");
        }
        return matches.get(0);
    }

    private ProfileRow loadProfile(Long profileId, Long headquartersId, Long customerId, Long categoryId) {
        List<ProfileRow> profiles = jdbcTemplate.query(
                """
                        SELECT expp_id,
                               expp_name,
                               expp_parstr,
                               expp_headline,
                               expp_bolchars,
                               expp_eolchars,
                               expp_bofchars,
                               expp_eofchars,
                               expp_fileextension
                        FROM exportparameters
                        WHERE expp_id = ?
                          AND hq_id = ?
                          AND cs_id = ?
                          AND expc_id = ?
                        """,
                (rs, rowNum) -> new ProfileRow(
                        rs.getLong("expp_id"),
                        blankToEmpty(rs.getString("expp_name")),
                        blankToEmpty(rs.getString("expp_parstr")),
                        "1".equals(blankToEmpty(rs.getString("expp_headline"))),
                        blankToEmpty(rs.getString("expp_bolchars")),
                        blankToEmpty(rs.getString("expp_eolchars")),
                        blankToEmpty(rs.getString("expp_bofchars")),
                        blankToEmpty(rs.getString("expp_eofchars")),
                        blankToEmpty(rs.getString("expp_fileextension"))
                ),
                profileId,
                headquartersId,
                customerId,
                categoryId
        );
        if (profiles.isEmpty()) {
            throw new IllegalArgumentException("Exportprofil wurde nicht gefunden.");
        }
        return profiles.get(0);
    }

    private ExportFileRow loadFile(Scope scope, Long fileId) {
        List<ExportFileRow> files = jdbcTemplate.query(
                """
                        SELECT ef.expf_id,
                               ef.expf_name,
                               ef.expf_storetime,
                               ef.expf_ftpupload,
                               ef.cs_id,
                               cs.cs_eid
                        FROM exportfiles AS ef
                        LEFT JOIN customer AS cs ON cs.cs_id = ef.cs_id
                        WHERE ef.expf_id = ?
                          AND ef.hq_id = ?
                          AND ef.cs_id = ?
                        """,
                (rs, rowNum) -> toExportFileRow(
                        rs.getLong("expf_id"),
                        blankToEmpty(rs.getString("expf_name")),
                        rs.getTimestamp("expf_storetime"),
                        blankToEmpty(rs.getString("expf_ftpupload")),
                        rs.getLong("cs_id"),
                        blankToEmpty(rs.getString("cs_eid"))
                ),
                fileId,
                scope.headquartersId(),
                scope.customerId()
        );
        if (files.isEmpty()) {
            throw new IllegalArgumentException("Exportdatei wurde nicht gefunden.");
        }
        return files.get(0);
    }

    private StoredFile loadStoredFile(Scope scope, Long fileId) {
        List<StoredFile> files = jdbcTemplate.query(
                """
                        SELECT expf_id, expf_name, expf_cryptname
                        FROM exportfiles
                        WHERE expf_id = ?
                          AND hq_id = ?
                          AND cs_id = ?
                        """,
                (rs, rowNum) -> new StoredFile(
                        rs.getLong("expf_id"),
                        blankToEmpty(rs.getString("expf_name")),
                        blankToEmpty(rs.getString("expf_cryptname"))
                ),
                fileId,
                scope.headquartersId(),
                scope.customerId()
        );
        if (files.isEmpty()) {
            throw new IllegalArgumentException("Exportdatei wurde nicht gefunden.");
        }
        return files.get(0);
    }

    private ExportFileRow toExportFileRow(Long id,
                                          String fileName,
                                          Timestamp storeTime,
                                          String ftpUploaded,
                                          Long customerId,
                                          String customerCode) {
        return new ExportFileRow(
                id,
                fileName,
                storeTime != null ? DISPLAY_TIMESTAMP.format(storeTime.toLocalDateTime()) : "",
                "1".equals(ftpUploaded),
                customerId,
                customerCode
        );
    }

    private String buildFileName(String explicitFileName, String categoryName, String profileExtension) {
        String candidate = explicitFileName != null ? explicitFileName.trim() : "";
        if (candidate.isBlank()) {
            candidate = FILE_NAME_TIMESTAMP.format(LocalDateTime.now()) + "_" + sanitizeFileName(categoryName);
        }
        candidate = sanitizeFileName(candidate);
        String extension = normalizeExtension(profileExtension);
        if (!extension.isBlank() && !candidate.toLowerCase(Locale.ROOT).endsWith("." + extension.toLowerCase(Locale.ROOT))) {
            candidate = candidate + "." + extension;
        }
        if (!candidate.contains(".")) {
            candidate = candidate + ".csv";
        }
        return candidate;
    }

    private void writeExportFile(String cryptName, byte[] data) {
        try {
            Files.createDirectories(exportRoot);
            Files.write(resolveStoredFile(cryptName), data, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
        } catch (IOException e) {
            throw new IllegalArgumentException("Exportdatei konnte nicht geschrieben werden.");
        }
    }

    private Path resolveStoredFile(String cryptName) {
        return exportRoot.resolve(Path.of(cryptName).getFileName().toString()).normalize();
    }

    private static Map<String, String> row(Object... values) {
        Map<String, String> row = new LinkedHashMap<>();
        for (int i = 0; i < values.length; i += 2) {
            row.put(String.valueOf(values[i]), stringify(values[i + 1]));
        }
        return row;
    }

    private static String stringify(Object value) {
        if (value == null) {
            return "";
        }
        if (value instanceof Number number) {
            return number.toString();
        }
        return String.valueOf(value);
    }

    private static String formatTimestamp(Timestamp timestamp) {
        return timestamp != null ? DISPLAY_TIMESTAMP.format(timestamp.toLocalDateTime()) : "";
    }

    private static String formatAmount(BigDecimal amount) {
        return amount != null ? amount.stripTrailingZeros().toPlainString() : "";
    }

    private static String sanitizeFileName(String fileName) {
        String normalized = fileName != null ? fileName.trim() : "";
        if (normalized.isBlank()) {
            return "export";
        }
        return normalized.replaceAll("[^A-Za-z0-9._-]+", "_");
    }

    private static String requireText(String value, String fieldLabel) {
        String normalized = value != null ? value.trim() : "";
        if (normalized.isBlank()) {
            throw new IllegalArgumentException(fieldLabel + " fehlt.");
        }
        return normalized;
    }

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

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

    private static String normalizeExtension(String value) {
        String normalized = value != null ? value.trim() : "";
        if (normalized.startsWith(".")) {
            normalized = normalized.substring(1);
        }
        return normalized.replaceAll("[^A-Za-z0-9_-]", "");
    }

    private static Long requireGeneratedId(KeyHolder keyHolder, String label) {
        Number key = keyHolder.getKey();
        if (key == null) {
            throw new IllegalArgumentException(label + " konnte nicht angelegt werden.");
        }
        return key.longValue();
    }

    private record Scope(Long headquartersId, Long customerId, String userTypeFilter, String userTypeNameFilter) {
    }

    private record ExportPayload(String description, List<String> columns, List<Map<String, String>> rows) {
    }

    private record StoredFile(Long id, String fileName, String cryptName) {
    }

    public record Bootstrap(List<CategoryOption> categories, boolean customerScoped, Long customerId) {
    }

    public record CategoryOption(Long id, String name, String userType) {
    }

    public record ProfileRow(Long id,
                             String name,
                             String parameterString,
                             boolean headline,
                             String beginOfLineChars,
                             String endOfLineChars,
                             String beginOfFileChars,
                             String endOfFileChars,
                             String fileExtension) {
    }

    public record ExportFileRow(Long id,
                                String fileName,
                                String storedAt,
                                boolean ftpUploaded,
                                Long customerId,
                                String customerCode) {
    }

    public record ExportResult(ExportFileRow file, int rowCount, String description) {
    }
}
