package de.votian.services.service;

import de.votian.services.config.ParameterKeys;
import de.votian.services.dto.UserSessionDto;
import de.votian.services.security.AccessControlService;
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.nio.charset.StandardCharsets;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;

@Service
@Transactional
public class ReportWorkspaceService {

    private final JdbcTemplate jdbcTemplate;
    private final AccessControlService accessControlService;
    private final ParameterService parameterService;

    public ReportWorkspaceService(JdbcTemplate jdbcTemplate,
                                  AccessControlService accessControlService,
                                  ParameterService parameterService) {
        this.jdbcTemplate = jdbcTemplate;
        this.accessControlService = accessControlService;
        this.parameterService = parameterService;
    }

    @Transactional(readOnly = true)
    public Bootstrap bootstrap(UserSessionDto user, Long headquartersId) {
        requireWorkspaceAccess(user, headquartersId);
        List<ObjectTypeOption> objectTypes = new ArrayList<>();
        if (accessControlService.canViewCustomerReports(user)) {
            objectTypes.add(new ObjectTypeOption("cs", "Kunden"));
        }
        if (accessControlService.canViewCourierReports(user)) {
            objectTypes.add(new ObjectTypeOption("cr", "Transporteure"));
        }
        if (accessControlService.canViewProspectReports(user)) {
            objectTypes.add(new ObjectTypeOption("pt", "Interessenten"));
        }
        return new Bootstrap(objectTypes, accessControlService.canViewConfidentialReports(user));
    }

    @Transactional(readOnly = true)
    public List<ReportTypeOption> findReportTypes(UserSessionDto user, Long headquartersId, String objectType) {
        requireWorkspaceAccess(user, headquartersId);
        String normalizedObjectType = normalizeObjectType(objectType);
        requireObjectTypeAccess(user, normalizedObjectType);
        return jdbcTemplate.query(
                """
                        SELECT mt_sort, mt_value
                        FROM metatype
                        WHERE mt_type = ?
                        ORDER BY mt_sort
                        """,
                (rs, rowNum) -> new ReportTypeOption(
                        rs.getInt("mt_sort"),
                        blankToEmpty(rs.getString("mt_value"))
                ),
                reportTypeMetaType(normalizedObjectType)
        );
    }

    @Transactional(readOnly = true)
    public List<ObjectOption> searchObjects(UserSessionDto user, Long headquartersId, String objectType, String query) {
        requireWorkspaceAccess(user, headquartersId);
        String normalizedObjectType = normalizeObjectType(objectType);
        requireObjectTypeAccess(user, normalizedObjectType);
        String normalizedQuery = query != null ? query.trim().toLowerCase(Locale.ROOT) : "";

        return switch (normalizedObjectType) {
            case "cs" -> searchCustomers(headquartersId, normalizedQuery);
            case "cr" -> searchCouriers(headquartersId, normalizedQuery);
            case "pt" -> searchProspects(headquartersId, normalizedQuery);
            default -> List.of();
        };
    }

    @Transactional(readOnly = true)
    public List<ReportRow> findReports(UserSessionDto user,
                                       Long headquartersId,
                                       String objectType,
                                       Long objectId,
                                       LocalDate from,
                                       LocalDate to,
                                       Integer reportType,
                                       String search) {
        requireWorkspaceAccess(user, headquartersId);
        String normalizedObjectType = normalizeObjectType(objectType);
        requireObjectTypeAccess(user, normalizedObjectType);

        StringBuilder sql = new StringBuilder(switch (normalizedObjectType) {
            case "cs" -> """
                    SELECT rp.rp_id,
                           rp.rp_objid,
                           rp.usr_id,
                           rp.rp_reporttype,
                           rp.rp_text,
                           rp.rp_confidential,
                           rp.rp_createtime,
                           hq.hq_name,
                           usr.usr_firstname,
                           usr.usr_name,
                           usr.usr_phone,
                           cs.cs_eid AS object_code,
                           cmp.cmp_comp AS object_name_1,
                           cmp.cmp_comp2 AS object_name_2,
                           COALESCE(mt.mt_value, '') AS report_type_label
                    FROM phoenix_group.report_process AS rp
                    JOIN headquarters AS hq ON hq.hq_id = rp.hq_id
                    LEFT JOIN user AS usr ON usr.usr_id = rp.usr_id
                    JOIN customer AS cs ON cs.cs_id = rp.rp_objid
                    JOIN company AS cmp ON cmp.cmp_id = cs.cmp_id
                    LEFT JOIN metatype AS mt
                           ON mt.mt_type = 'report_type'
                          AND mt.mt_sort = rp.rp_reporttype
                    WHERE rp.hq_id = ?
                      AND rp.rp_objtype = 'cs'
                    """;
            case "cr" -> """
                    SELECT rp.rp_id,
                           rp.rp_objid,
                           rp.usr_id,
                           rp.rp_reporttype,
                           rp.rp_text,
                           rp.rp_confidential,
                           rp.rp_createtime,
                           hq.hq_name,
                           usr.usr_firstname,
                           usr.usr_name,
                           usr.usr_phone,
                           cr.cr_eid AS object_code,
                           cmp.cmp_comp AS object_name_1,
                           cmp.cmp_comp2 AS object_name_2,
                           COALESCE(mt.mt_value, '') AS report_type_label
                    FROM phoenix_group.report_process AS rp
                    JOIN headquarters AS hq ON hq.hq_id = rp.hq_id
                    LEFT JOIN user AS usr ON usr.usr_id = rp.usr_id
                    JOIN courier AS cr ON cr.cr_id = rp.rp_objid
                    JOIN company AS cmp ON cmp.cmp_id = cr.cmp_id
                    LEFT JOIN metatype AS mt
                           ON mt.mt_type = 'report_type_2'
                          AND mt.mt_sort = rp.rp_reporttype
                    WHERE rp.hq_id = ?
                      AND rp.rp_objtype = 'cr'
                    """;
            case "pt" -> """
                    SELECT rp.rp_id,
                           rp.rp_objid,
                           rp.usr_id,
                           rp.rp_reporttype,
                           rp.rp_text,
                           rp.rp_confidential,
                           rp.rp_createtime,
                           hq.hq_name,
                           usr.usr_firstname,
                           usr.usr_name,
                           usr.usr_phone,
                           pt.pt_cs_eid AS object_code,
                           pt.pt_cmp_comp AS object_name_1,
                           pt.pt_cmp_comp2 AS object_name_2,
                           COALESCE(mt.mt_value, '') AS report_type_label
                    FROM phoenix_group.report_process AS rp
                    JOIN headquarters AS hq ON hq.hq_id = rp.hq_id
                    LEFT JOIN user AS usr ON usr.usr_id = rp.usr_id
                    JOIN prospect AS pt ON pt.pt_id = rp.rp_objid
                    LEFT JOIN metatype AS mt
                           ON mt.mt_type = 'report_type_3'
                          AND mt.mt_sort = rp.rp_reporttype
                    WHERE rp.hq_id = ?
                      AND rp.rp_objtype = 'pt'
                    """;
            default -> throw new IllegalArgumentException("Objekttyp ungueltig.");
        });
        List<Object> args = new ArrayList<>();
        args.add(headquartersId);

        if (objectId != null && objectId > 0) {
            sql.append(" AND rp.rp_objid = ?");
            args.add(objectId);
        }
        if (from != null) {
            sql.append(" AND rp.rp_createtime >= ?");
            args.add(Timestamp.valueOf(from.atStartOfDay()));
        }
        if (to != null) {
            sql.append(" AND rp.rp_createtime <= ?");
            args.add(Timestamp.valueOf(to.plusDays(1).atStartOfDay().minusNanos(1)));
        }
        if (reportType != null && reportType > 0) {
            sql.append(" AND rp.rp_reporttype = ?");
            args.add(reportType);
        }

        String normalizedSearch = search != null ? search.trim().toLowerCase(Locale.ROOT) : "";
        if (!normalizedSearch.isBlank()) {
            sql.append(reportSearchClause(normalizedObjectType));
            String token = "%" + normalizedSearch + "%";
            args.add(token);
            args.add(token);
            args.add(token);
            args.add(token);
        }
        if (!accessControlService.canViewConfidentialReports(user)) {
            sql.append(" AND (COALESCE(rp.rp_confidential, '0') = '0' OR rp.usr_id = ?)");
            args.add(user != null ? user.getUserId() : 0L);
        }
        sql.append(" ORDER BY rp.rp_createtime DESC, rp.rp_id DESC");

        return jdbcTemplate.query(Objects.requireNonNull(sql.toString()), (rs, rowNum) -> toReportRow(rs, user, normalizedObjectType), args.toArray());
    }

    public ReportRow createReport(UserSessionDto user,
                                  Long headquartersId,
                                  String objectType,
                                  Long objectId,
                                  Integer reportType,
                                  String text,
                                  boolean confidential) {
        requireWorkspaceAccess(user, headquartersId);
        String normalizedObjectType = normalizeObjectType(objectType);
        requireObjectTypeAccess(user, normalizedObjectType);
        requireObjectExists(headquartersId, normalizedObjectType, objectId);

        String normalizedText = requireReportText(text);
        int normalizedReportType = normalizeReportType(reportType);
        if (normalizedReportType > 0) {
            requireReportTypeExists(normalizedObjectType, normalizedReportType);
        }

        KeyHolder keyHolder = new GeneratedKeyHolder();
        LocalDateTime now = LocalDateTime.now();
        jdbcTemplate.update(connection -> {
            PreparedStatement statement = connection.prepareStatement(
                    """
                            INSERT INTO phoenix_group.report_process
                                (usr_id, hq_id, rp_reporttype, rp_text, rp_createtime, rp_objid, rp_objtype, rp_confidential)
                            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                            """,
                    Statement.RETURN_GENERATED_KEYS
            );
            statement.setObject(1, user != null ? user.getUserId() : null);
            statement.setLong(2, headquartersId);
            statement.setInt(3, normalizedReportType);
            statement.setString(4, normalizedText);
            statement.setTimestamp(5, Timestamp.valueOf(now));
            statement.setLong(6, objectId);
            statement.setString(7, normalizedObjectType);
            statement.setString(8, confidential ? "1" : "0");
            return statement;
        }, keyHolder);

        if ("cs".equals(normalizedObjectType)) {
            updateSameDayAppointmentWarning(user, headquartersId, objectId, now.toLocalDate());
        }
        return getReportRow(requireGeneratedId(keyHolder, "Bericht"), user);
    }

    public ReportRow updateReport(UserSessionDto user,
                                  Long headquartersId,
                                  Long reportId,
                                  Integer reportType,
                                  String text,
                                  boolean confidential) {
        requireWorkspaceAccess(user, headquartersId);
        ReportAccessRow existing = loadReportAccessRow(reportId);
        if (!Objects.equals(existing.headquartersId(), headquartersId)) {
            throw new IllegalArgumentException("Bericht gehoert zu einer anderen Niederlassung.");
        }
        requireObjectTypeAccess(user, existing.objectType());
        if (!canEditReport(user, existing.authorUserId())) {
            throw new IllegalArgumentException("Bericht darf nicht bearbeitet werden.");
        }

        String normalizedText = requireReportText(text);
        int normalizedReportType = normalizeReportType(reportType);
        if (normalizedReportType > 0) {
            requireReportTypeExists(existing.objectType(), normalizedReportType);
        }

        jdbcTemplate.update(
                """
                        UPDATE phoenix_group.report_process
                        SET rp_reporttype = ?, rp_text = ?, rp_confidential = ?
                        WHERE rp_id = ?
                        """,
                normalizedReportType,
                normalizedText,
                confidential ? "1" : "0",
                reportId
        );
        return getReportRow(reportId, user);
    }

    public void deleteReport(UserSessionDto user, Long headquartersId, Long reportId) {
        requireWorkspaceAccess(user, headquartersId);
        ReportAccessRow existing = loadReportAccessRow(reportId);
        if (!Objects.equals(existing.headquartersId(), headquartersId)) {
            throw new IllegalArgumentException("Bericht gehoert zu einer anderen Niederlassung.");
        }
        requireObjectTypeAccess(user, existing.objectType());
        if (!Objects.equals(existing.authorUserId(), user != null ? user.getUserId() : null)) {
            throw new IllegalArgumentException("Nur eigene Berichte duerfen geloescht werden.");
        }
        jdbcTemplate.update("DELETE FROM phoenix_group.report_process WHERE rp_id = ?", reportId);
    }

    @Transactional(readOnly = true)
    public List<StatisticRow> statistics(UserSessionDto user,
                                         Long headquartersId,
                                         String objectType,
                                         LocalDate from,
                                         LocalDate to,
                                         boolean splitByReportType,
                                         boolean splitByCustomerType) {
        requireWorkspaceAccess(user, headquartersId);
        String normalizedObjectType = normalizeObjectType(objectType);
        requireObjectTypeAccess(user, normalizedObjectType);

        String reportMetaType = reportTypeMetaType(normalizedObjectType);

        StringBuilder sql = new StringBuilder("""
                SELECT hq.hq_name,
                       COALESCE(usr.usr_firstname, '') AS usr_firstname,
                       COALESCE(usr.usr_name, '') AS usr_name
                """);
        if (splitByReportType) {
            sql.append(", COALESCE(mt.mt_value, '') AS report_type_label");
        } else {
            sql.append(", '' AS report_type_label");
        }
        if (splitByCustomerType && "cs".equals(normalizedObjectType)) {
            sql.append(", COALESCE(mt2.mt_value, '') AS company_type_label");
        } else {
            sql.append(", '' AS company_type_label");
        }
        sql.append(", COUNT(*) AS reports ");
        sql.append("""
                FROM phoenix_group.report_process AS rp
                JOIN headquarters AS hq ON hq.hq_id = rp.hq_id
                LEFT JOIN user AS usr ON usr.usr_id = rp.usr_id
                """);
        if (splitByReportType) {
            sql.append(" LEFT JOIN metatype AS mt ON mt.mt_type = ? AND mt.mt_sort = rp.rp_reporttype ");
        }
        if (splitByCustomerType && "cs".equals(normalizedObjectType)) {
            sql.append("""
                     JOIN customer AS cs ON cs.cs_id = rp.rp_objid
                     JOIN company AS cmp ON cmp.cmp_id = cs.cmp_id
                     LEFT JOIN metatype AS mt2 ON mt2.mt_type = 'customer_type' AND mt2.mt_sort = cmp.cmp_type
                    """);
        }
        sql.append(" WHERE rp.hq_id = ? AND rp.rp_objtype = ? ");

        List<Object> args = new ArrayList<>();
        if (splitByReportType) {
            args.add(reportMetaType);
        }
        args.add(headquartersId);
        args.add(normalizedObjectType);

        if (from != null) {
            sql.append(" AND rp.rp_createtime >= ?");
            args.add(Timestamp.valueOf(from.atStartOfDay()));
        }
        if (to != null) {
            sql.append(" AND rp.rp_createtime <= ?");
            args.add(Timestamp.valueOf(to.plusDays(1).atStartOfDay().minusNanos(1)));
        }
        if (!accessControlService.canViewConfidentialReports(user)) {
            sql.append(" AND (COALESCE(rp.rp_confidential, '0') = '0' OR rp.usr_id = ?)");
            args.add(user != null ? user.getUserId() : 0L);
        }

        sql.append(" GROUP BY hq.hq_name, usr.usr_firstname, usr.usr_name");
        if (splitByReportType) {
            sql.append(", report_type_label");
        }
        if (splitByCustomerType && "cs".equals(normalizedObjectType)) {
            sql.append(", company_type_label");
        }
        sql.append(" ORDER BY hq.hq_name, usr.usr_name, usr.usr_firstname");
        if (splitByReportType) {
            sql.append(", report_type_label");
        }
        if (splitByCustomerType && "cs".equals(normalizedObjectType)) {
            sql.append(", company_type_label");
        }

        return jdbcTemplate.query(Objects.requireNonNull(sql.toString()), (rs, rowNum) -> new StatisticRow(
                blankToEmpty(rs.getString("hq_name")),
                blankToEmpty(rs.getString("usr_firstname")),
                authorLastName(rs.getString("usr_name"), rs.getString("usr_firstname")),
                blankToEmpty(rs.getString("report_type_label")),
                blankToEmpty(rs.getString("company_type_label")),
                rs.getLong("reports")
        ), args.toArray());
    }

    @Transactional(readOnly = true)
    public byte[] exportHistoryCsv(UserSessionDto user,
                                   Long headquartersId,
                                   String objectType,
                                   Long objectId,
                                   LocalDate from,
                                   LocalDate to,
                                   Integer reportType,
                                   String search) {
        List<ReportRow> rows = findReports(user, headquartersId, objectType, objectId, from, to, reportType, search);
        StringBuilder csv = new StringBuilder();
        csv.append("Zeitpunkt;Objekttyp;Objekt;Berichtstyp;Vertraulich;Mitarbeiter;Telefon;Text\n");
        for (ReportRow row : rows) {
            csv.append(csv(row.createdAt() != null ? row.createdAt().toString() : ""))
                    .append(';')
                    .append(csv(objectTypeLabel(row.objectType())))
                    .append(';')
                    .append(csv(row.objectLabel()))
                    .append(';')
                    .append(csv(row.reportTypeLabel()))
                    .append(';')
                    .append(csv(row.confidential() ? "Ja" : "Nein"))
                    .append(';')
                    .append(csv(row.authorName()))
                    .append(';')
                    .append(csv(row.authorPhone()))
                    .append(';')
                    .append(csv(row.text()))
                    .append('\n');
        }
        return csv.toString().getBytes(StandardCharsets.UTF_8);
    }

    @Transactional(readOnly = true)
    public byte[] exportStatisticsCsv(UserSessionDto user,
                                      Long headquartersId,
                                      String objectType,
                                      LocalDate from,
                                      LocalDate to,
                                      boolean splitByReportType,
                                      boolean splitByCustomerType) {
        List<StatisticRow> rows = statistics(user, headquartersId, objectType, from, to, splitByReportType, splitByCustomerType);
        StringBuilder csv = new StringBuilder();
        csv.append("Zentrale;Vorname;Name;Berichtstyp;Kundentyp;Anzahl\n");
        for (StatisticRow row : rows) {
            csv.append(csv(row.headquartersName()))
                    .append(';')
                    .append(csv(row.authorFirstname()))
                    .append(';')
                    .append(csv(row.authorName()))
                    .append(';')
                    .append(csv(row.reportTypeLabel()))
                    .append(';')
                    .append(csv(row.companyTypeLabel()))
                    .append(';')
                    .append(row.reports())
                    .append('\n');
        }
        return csv.toString().getBytes(StandardCharsets.UTF_8);
    }

    private List<ObjectOption> searchCustomers(Long headquartersId, String query) {
        StringBuilder sql = new StringBuilder("""
                SELECT cs.cs_id AS object_id,
                       cs.cs_eid AS object_code,
                       cmp.cmp_comp AS object_name_1,
                       cmp.cmp_comp2 AS object_name_2
                FROM customer AS cs
                JOIN company AS cmp ON cmp.cmp_id = cs.cmp_id
                WHERE cs.hq_id = ?
                  AND COALESCE(cmp.cmp_archived, '0') = '0'
                """);
        List<Object> args = new ArrayList<>();
        args.add(headquartersId);
        if (!query.isBlank()) {
            sql.append("""
                     AND (
                            LOWER(COALESCE(cs.cs_eid, '')) LIKE ?
                         OR LOWER(COALESCE(cmp.cmp_comp, '')) LIKE ?
                         OR LOWER(COALESCE(cmp.cmp_comp2, '')) LIKE ?
                         OR LOWER(COALESCE(cmp.cmp_match, '')) LIKE ?
                     )
                    """);
            String token = "%" + query + "%";
            args.add(token);
            args.add(token);
            args.add(token);
            args.add(token);
        }
        sql.append(" ORDER BY cmp.cmp_comp, cmp.cmp_comp2, cs.cs_id LIMIT 25");
        return jdbcTemplate.query(Objects.requireNonNull(sql.toString()), (rs, rowNum) -> new ObjectOption(
                rs.getLong("object_id"),
                joinParts(rs.getString("object_name_1"), rs.getString("object_name_2")),
                blankToEmpty(rs.getString("object_code"))
        ), args.toArray());
    }

    private List<ObjectOption> searchCouriers(Long headquartersId, String query) {
        StringBuilder sql = new StringBuilder("""
                SELECT cr.cr_id AS object_id,
                       cr.cr_eid AS object_code,
                       cmp.cmp_comp AS object_name_1,
                       cmp.cmp_comp2 AS object_name_2
                FROM courier AS cr
                JOIN company AS cmp ON cmp.cmp_id = cr.cmp_id
                WHERE cr.hq_id = ?
                  AND COALESCE(cmp.cmp_archived, '0') = '0'
                """);
        List<Object> args = new ArrayList<>();
        args.add(headquartersId);
        if (!query.isBlank()) {
            sql.append("""
                     AND (
                            LOWER(COALESCE(cr.cr_eid, '')) LIKE ?
                         OR LOWER(COALESCE(cmp.cmp_comp, '')) LIKE ?
                         OR LOWER(COALESCE(cmp.cmp_comp2, '')) LIKE ?
                         OR LOWER(COALESCE(cmp.cmp_match, '')) LIKE ?
                     )
                    """);
            String token = "%" + query + "%";
            args.add(token);
            args.add(token);
            args.add(token);
            args.add(token);
        }
        sql.append(" ORDER BY cmp.cmp_comp, cmp.cmp_comp2, cr.cr_id LIMIT 25");
        return jdbcTemplate.query(Objects.requireNonNull(sql.toString()), (rs, rowNum) -> new ObjectOption(
                rs.getLong("object_id"),
                joinParts(rs.getString("object_name_1"), rs.getString("object_name_2")),
                blankToEmpty(rs.getString("object_code"))
        ), args.toArray());
    }

    private List<ObjectOption> searchProspects(Long headquartersId, String query) {
        StringBuilder sql = new StringBuilder("""
                SELECT pt.pt_id AS object_id,
                       pt.pt_cs_eid AS object_code,
                       pt.pt_cmp_comp AS object_name_1,
                       pt.pt_cmp_comp2 AS object_name_2
                FROM prospect AS pt
                WHERE pt.hq_id = ?
                """);
        List<Object> args = new ArrayList<>();
        args.add(headquartersId);
        if (!query.isBlank()) {
            sql.append("""
                     AND (
                            LOWER(COALESCE(pt.pt_cs_eid, '')) LIKE ?
                         OR LOWER(COALESCE(pt.pt_cmp_comp, '')) LIKE ?
                         OR LOWER(COALESCE(pt.pt_cmp_comp2, '')) LIKE ?
                         OR LOWER(COALESCE(pt.pt_cmp_match, '')) LIKE ?
                     )
                    """);
            String token = "%" + query + "%";
            args.add(token);
            args.add(token);
            args.add(token);
            args.add(token);
        }
        sql.append(" ORDER BY pt.pt_cmp_comp, pt.pt_cmp_comp2, pt.pt_id LIMIT 25");
        return jdbcTemplate.query(Objects.requireNonNull(sql.toString()), (rs, rowNum) -> new ObjectOption(
                rs.getLong("object_id"),
                joinParts(rs.getString("object_name_1"), rs.getString("object_name_2")),
                blankToEmpty(rs.getString("object_code"))
        ), args.toArray());
    }

    private ReportRow toReportRow(java.sql.ResultSet rs, UserSessionDto user, String objectType) throws java.sql.SQLException {
        Long authorUserId = nullableLong(rs.getObject("usr_id"));
        String authorFirstname = blankToEmpty(rs.getString("usr_firstname"));
        String authorLastname = blankToEmpty(rs.getString("usr_name"));
        String authorName = authorLastName(authorLastname, authorFirstname);
        LocalDateTime createdAt = rs.getTimestamp("rp_createtime") != null
                ? rs.getTimestamp("rp_createtime").toLocalDateTime()
                : null;
        return new ReportRow(
                rs.getLong("rp_id"),
                objectType,
                rs.getLong("rp_objid"),
                buildObjectLabel(rs.getString("object_name_1"), rs.getString("object_name_2"), rs.getString("object_code")),
                blankToEmpty(rs.getString("hq_name")),
                authorUserId,
                authorName,
                blankToEmpty(rs.getString("usr_phone")),
                createdAt,
                nullableInt(rs.getObject("rp_reporttype")),
                blankToEmpty(rs.getString("report_type_label")),
                blankToEmpty(rs.getString("rp_text")),
                "1".equals(blankToEmpty(rs.getString("rp_confidential"))),
                canEditReport(user, authorUserId),
                Objects.equals(authorUserId, user != null ? user.getUserId() : null)
        );
    }

    private ReportRow getReportRow(Long reportId, UserSessionDto user) {
        ReportAccessRow accessRow = loadReportAccessRow(reportId);
        return findReports(
                user,
                accessRow.headquartersId(),
                accessRow.objectType(),
                accessRow.objectId(),
                null,
                null,
                null,
                null
        ).stream()
                .filter(row -> Objects.equals(row.id(), reportId))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Bericht nicht gefunden."));
    }

    private ReportAccessRow loadReportAccessRow(Long reportId) {
        if (reportId == null || reportId <= 0) {
            throw new IllegalArgumentException("Bericht nicht gefunden.");
        }
        ReportAccessRow accessRow = jdbcTemplate.query(
                """
                        SELECT rp_id, hq_id, rp_objtype, rp_objid, usr_id
                        FROM phoenix_group.report_process
                        WHERE rp_id = ?
                        """,
                rs -> rs.next()
                        ? new ReportAccessRow(
                        rs.getLong("rp_id"),
                        rs.getLong("hq_id"),
                        blankToEmpty(rs.getString("rp_objtype")),
                        rs.getLong("rp_objid"),
                        nullableLong(rs.getObject("usr_id"))
                )
                        : null,
                reportId
        );
        if (accessRow == null) {
            throw new IllegalArgumentException("Bericht nicht gefunden.");
        }
        return accessRow;
    }

    private void requireObjectExists(Long headquartersId, String objectType, Long objectId) {
        if (objectId == null || objectId <= 0) {
            throw new IllegalArgumentException("Es wurde kein Objekt ausgewaehlt.");
        }
        String sql = switch (objectType) {
            case "cs" -> "SELECT COUNT(*) FROM customer WHERE cs_id = ? AND hq_id = ?";
            case "cr" -> "SELECT COUNT(*) FROM courier WHERE cr_id = ? AND hq_id = ?";
            case "pt" -> "SELECT COUNT(*) FROM prospect WHERE pt_id = ? AND hq_id = ?";
            default -> throw new IllegalArgumentException("Objekttyp ungueltig.");
        };
        Integer count = jdbcTemplate.queryForObject(sql, Integer.class, objectId, headquartersId);
        if (count == null || count == 0) {
            throw new IllegalArgumentException("Das ausgewaehlte Objekt wurde nicht gefunden.");
        }
    }

    private void requireReportTypeExists(String objectType, Integer reportType) {
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM metatype WHERE mt_type = ? AND mt_sort = ?",
                Integer.class,
                reportTypeMetaType(objectType),
                reportType
        );
        if (count == null || count == 0) {
            throw new IllegalArgumentException("Berichtstyp nicht gefunden.");
        }
    }

    private void requireWorkspaceAccess(UserSessionDto user, Long headquartersId) {
        if (!accessControlService.canViewReports(user) || !accessControlService.canAccessHeadquarters(user, headquartersId)) {
            throw new IllegalArgumentException("Keine Berechtigung fuer den Berichtsarbeitsbereich.");
        }
    }

    private void requireObjectTypeAccess(UserSessionDto user, String objectType) {
        boolean allowed = switch (objectType) {
            case "cs" -> accessControlService.canViewCustomerReports(user);
            case "cr" -> accessControlService.canViewCourierReports(user);
            case "pt" -> accessControlService.canViewProspectReports(user);
            default -> false;
        };
        if (!allowed) {
            throw new IllegalArgumentException("Keine Berechtigung fuer diesen Berichtstyp.");
        }
    }

    private boolean canEditReport(UserSessionDto user, Long authorUserId) {
        return Objects.equals(authorUserId, user != null ? user.getUserId() : null)
                || accessControlService.canManageEmployees(user);
    }

    private void updateSameDayAppointmentWarning(UserSessionDto user,
                                                 Long headquartersId,
                                                 Long customerId,
                                                 LocalDate date) {
        long employeeId = user != null && user.getEmployeeId() != null ? user.getEmployeeId() : 0L;
        String enabled = parameterService.getParameterValueFullFallback(
                employeeId,
                ParameterKeys.MASK_RP_ENABLE_DEACTIVATION_APPOINTMENT_WARNING_OF_SAME_DAY,
                headquartersId != null ? headquartersId : 0L
        );
        if (!"1".equals(enabled)) {
            return;
        }
        jdbcTemplate.update(
                """
                        UPDATE phoenix_group.appointment
                        SET ap_category_4 = '2'
                        WHERE cs_id = ?
                          AND ap_execdate >= ?
                          AND ap_execdate <= ?
                          AND ap_category_4 = '1'
                        """,
                customerId,
                Timestamp.valueOf(date.atStartOfDay()),
                Timestamp.valueOf(date.plusDays(1).atStartOfDay().minusNanos(1))
        );
    }

    private int normalizeReportType(Integer reportType) {
        return reportType != null && reportType > 0 ? reportType : 0;
    }

    private String requireReportText(String text) {
        String normalized = text != null ? text.trim() : "";
        if (normalized.isBlank()) {
            throw new IllegalArgumentException("Berichtstext darf nicht leer sein.");
        }
        return normalized;
    }

    private String normalizeObjectType(String objectType) {
        String normalized = objectType != null ? objectType.trim().toLowerCase(Locale.ROOT) : "";
        if (!List.of("cs", "cr", "pt").contains(normalized)) {
            throw new IllegalArgumentException("Objekttyp ungueltig.");
        }
        return normalized;
    }

    private String reportTypeMetaType(String objectType) {
        return switch (objectType) {
            case "cs" -> "report_type";
            case "cr" -> "report_type_2";
            case "pt" -> "report_type_3";
            default -> throw new IllegalArgumentException("Objekttyp ungueltig.");
        };
    }

    private String reportSearchClause(String objectType) {
        return switch (objectType) {
            case "cs" -> """
                     AND (
                            LOWER(COALESCE(rp.rp_text, '')) LIKE ?
                         OR LOWER(COALESCE(cmp.cmp_comp, '')) LIKE ?
                         OR LOWER(COALESCE(cmp.cmp_comp2, '')) LIKE ?
                         OR LOWER(COALESCE(cs.cs_eid, '')) LIKE ?
                     )
                    """;
            case "cr" -> """
                     AND (
                            LOWER(COALESCE(rp.rp_text, '')) LIKE ?
                         OR LOWER(COALESCE(cmp.cmp_comp, '')) LIKE ?
                         OR LOWER(COALESCE(cmp.cmp_comp2, '')) LIKE ?
                         OR LOWER(COALESCE(cr.cr_eid, '')) LIKE ?
                     )
                    """;
            case "pt" -> """
                     AND (
                            LOWER(COALESCE(rp.rp_text, '')) LIKE ?
                         OR LOWER(COALESCE(pt.pt_cmp_comp, '')) LIKE ?
                         OR LOWER(COALESCE(pt.pt_cmp_comp2, '')) LIKE ?
                         OR LOWER(COALESCE(pt.pt_cs_eid, '')) LIKE ?
                     )
                    """;
            default -> throw new IllegalArgumentException("Objekttyp ungueltig.");
        };
    }

    private String objectTypeLabel(String objectType) {
        return switch (objectType) {
            case "cs" -> "Kunden";
            case "cr" -> "Transporteure";
            case "pt" -> "Interessenten";
            default -> objectType;
        };
    }

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

    private static Long nullableLong(Object value) {
        return value instanceof Number number ? number.longValue() : null;
    }

    private static Integer nullableInt(Object value) {
        return value instanceof Number number ? number.intValue() : null;
    }

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

    private static String joinParts(String first, String second) {
        String left = blankToEmpty(first).trim();
        String right = blankToEmpty(second).trim();
        if (left.isBlank()) {
            return right;
        }
        if (right.isBlank()) {
            return left;
        }
        return left + " " + right;
    }

    private static String buildObjectLabel(String first, String second, String code) {
        String label = joinParts(first, second);
        String normalizedCode = blankToEmpty(code).trim();
        if (normalizedCode.isBlank()) {
            return label;
        }
        if (label.isBlank()) {
            return normalizedCode;
        }
        return label + " [" + normalizedCode + "]";
    }

    private static String authorLastName(String lastName, String firstName) {
        String normalizedLast = blankToEmpty(lastName).trim();
        String normalizedFirst = blankToEmpty(firstName).trim();
        if (normalizedLast.isBlank() && normalizedFirst.isBlank()) {
            return "Nutzer geloescht";
        }
        if (normalizedLast.isBlank()) {
            return normalizedFirst;
        }
        return normalizedLast;
    }

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

    public record Bootstrap(List<ObjectTypeOption> objectTypes, boolean canViewConfidentialReports) {
    }

    public record ObjectTypeOption(String code, String label) {
    }

    public record ReportTypeOption(Integer id, String label) {
    }

    public record ObjectOption(Long id, String label, String secondaryLabel) {
    }

    public record ReportRow(Long id,
                            String objectType,
                            Long objectId,
                            String objectLabel,
                            String headquartersName,
                            Long authorUserId,
                            String authorName,
                            String authorPhone,
                            LocalDateTime createdAt,
                            Integer reportType,
                            String reportTypeLabel,
                            String text,
                            boolean confidential,
                            boolean editable,
                            boolean deletable) {
    }

    public record StatisticRow(String headquartersName,
                               String authorFirstname,
                               String authorName,
                               String reportTypeLabel,
                               String companyTypeLabel,
                               long reports) {
    }

    private record ReportAccessRow(Long id,
                                   Long headquartersId,
                                   String objectType,
                                   Long objectId,
                                   Long authorUserId) {
    }
}
