package de.votian.services.service;

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.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.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;

@Service
@Transactional
public class CommunicationWorkspaceService {

    private static final DateTimeFormatter DISPLAY_TIMESTAMP = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

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

    public CommunicationWorkspaceService(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);
        cleanupExpiredTickerEntries();
        Integer unread = jdbcTemplate.queryForObject(
                """
                        SELECT COUNT(*)
                        FROM phoenix_log.messageforum
                        WHERE hq_id = ?
                          AND mf_createtime >= ?
                          AND mf_usr_type = '3'
                          AND COALESCE(mf_state, 0) < 2
                        """,
                Integer.class,
                headquartersId,
                Timestamp.valueOf(LocalDate.now().atStartOfDay())
        );
        return new Bootstrap(
                accessControlService.canManageTicker(user),
                accessControlService.canManageDeviceMessages(user),
                accessControlService.canManageCourierMessageGroups(user),
                accessControlService.canManageNewsletter(user),
                unread != null ? unread : 0
        );
    }

    @Transactional(readOnly = true)
    public List<TickerRow> findTickers(UserSessionDto user, Long headquartersId) {
        requireWorkspaceAccess(user, headquartersId);
        cleanupExpiredTickerEntries();

        Long employeeId = user != null ? user.getEmployeeId() : null;
        List<TickerRow> rows = jdbcTemplate.query(
                """
                        SELECT tif.tif_id,
                               tif.hq_id,
                               tif.emp_id,
                               tif.emp_id_creator,
                               tif.tif_createtime,
                               tif.tif_starttime,
                               tif.tif_endtime,
                               tif.tif_text,
                               tif.tif_unerasable,
                               hq.hq_name AS target_hq_name,
                               usr_target.usr_firstname AS target_firstname,
                               usr_target.usr_name AS target_name,
                               usr_creator.usr_firstname AS creator_firstname,
                               usr_creator.usr_name AS creator_name
                        FROM phoenix_group.tickerforum AS tif
                        LEFT JOIN headquarters AS hq ON hq.hq_id = tif.hq_id
                        LEFT JOIN employee AS emp_target ON emp_target.emp_id = tif.emp_id
                        LEFT JOIN user AS usr_target ON usr_target.usr_id = emp_target.usr_id
                        LEFT JOIN employee AS emp_creator ON emp_creator.emp_id = tif.emp_id_creator
                        LEFT JOIN user AS usr_creator ON usr_creator.usr_id = emp_creator.usr_id
                        WHERE COALESCE(tif.tif_unerasable, '0') <> '1'
                          AND (
                                tif.emp_id_creator = ?
                                OR tif.emp_id = ?
                                OR ((tif.emp_id IS NULL OR tif.emp_id = 0) AND tif.hq_id = ?)
                                OR ((tif.emp_id IS NULL OR tif.emp_id = 0) AND (tif.hq_id IS NULL OR tif.hq_id = 0))
                              )
                        ORDER BY tif.tif_createtime DESC, tif.tif_id DESC
                        LIMIT 300
                        """,
                (rs, rowNum) -> new TickerRow(
                        rs.getLong("tif_id"),
                        formatTimestamp(rs.getTimestamp("tif_createtime")),
                        formatTimestamp(rs.getTimestamp("tif_starttime")),
                        formatTimestamp(rs.getTimestamp("tif_endtime")),
                        blankToEmpty(rs.getString("tif_text")),
                        blankToEmpty(rs.getString("creator_firstname")) + " " + blankToEmpty(rs.getString("creator_name")),
                        blankToEmpty(rs.getString("target_hq_name")),
                        blankToEmpty(rs.getString("target_firstname")) + " " + blankToEmpty(rs.getString("target_name")),
                        "1".equals(blankToEmpty(rs.getString("tif_unerasable"))),
                        canDeleteTicker(user, headquartersId, rs.getLong("tif_id"), rs.getLong("emp_id_creator"))
                ),
                employeeId != null ? employeeId : 0L,
                employeeId != null ? employeeId : 0L,
                headquartersId
        );
        return rows.stream()
                .map(row -> new TickerRow(
                        row.id(),
                        row.createdAt(),
                        row.startAt(),
                        row.endAt(),
                        row.text(),
                        row.creatorLabel().trim(),
                        row.targetHeadquartersLabel().trim(),
                        row.targetEmployeeLabel().trim(),
                        row.unerasable(),
                        row.deletable()
                ))
                .toList();
    }

    public TickerRow createTicker(UserSessionDto user,
                                  Long headquartersId,
                                  Long targetHeadquartersId,
                                  Long targetEmployeeId,
                                  LocalDateTime startAt,
                                  LocalDateTime endAt,
                                  String text,
                                  boolean unerasable) {
        requireWorkspaceAccess(user, headquartersId);
        if (!accessControlService.canManageTicker(user)) {
            throw new IllegalArgumentException("Mitteilungen duerfen fuer den Benutzer nicht gepflegt werden.");
        }
        Long employeeId = requireEmployeeId(user);
        String normalizedText = requireLimitedText(
                text,
                maxLength("NEWSTICKER_MAX_BODY_LENGTH", headquartersId, 1000),
                "Mitteilungstext"
        );
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime normalizedStart = startAt != null ? startAt : now;
        LocalDateTime normalizedEnd = endAt != null ? endAt : now.plusDays(1);
        if (normalizedEnd.isBefore(normalizedStart)) {
            throw new IllegalArgumentException("Ende der Mitteilung liegt vor dem Start.");
        }

        if (targetEmployeeId != null && targetEmployeeId > 0) {
            targetHeadquartersId = 0L;
        }
        Long normalizedTargetHeadquartersId = targetHeadquartersId != null ? targetHeadquartersId : 0L;
        Long normalizedTargetEmployeeId = targetEmployeeId != null ? targetEmployeeId : 0L;

        KeyHolder keyHolder = new GeneratedKeyHolder();
        jdbcTemplate.update(connection -> {
            PreparedStatement statement = connection.prepareStatement(
                    """
                            INSERT INTO phoenix_group.tickerforum
                                (hq_id, emp_id, emp_id_creator, tif_createtime, tif_starttime, tif_endtime, tif_subject, tif_text, tif_unerasable)
                            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
                            """,
                    Statement.RETURN_GENERATED_KEYS
            );
            statement.setLong(1, normalizedTargetHeadquartersId);
            statement.setLong(2, normalizedTargetEmployeeId);
            statement.setLong(3, employeeId);
            statement.setTimestamp(4, Timestamp.valueOf(now));
            statement.setTimestamp(5, Timestamp.valueOf(normalizedStart));
            statement.setTimestamp(6, Timestamp.valueOf(normalizedEnd));
            statement.setString(7, "");
            statement.setString(8, normalizedText);
            statement.setString(9, unerasable ? "1" : "0");
            return statement;
        }, keyHolder);
        return findTicker(headquartersId, requireGeneratedId(keyHolder, "Mitteilung"), user);
    }

    public void deleteTicker(UserSessionDto user, Long headquartersId, Long tickerId) {
        requireWorkspaceAccess(user, headquartersId);
        TickerAccess tickerAccess = loadTickerAccess(tickerId);
        if (!canDeleteTicker(user, headquartersId, tickerId, tickerAccess.creatorEmployeeId())) {
            throw new IllegalArgumentException("Mitteilung darf nicht geloescht werden.");
        }
        jdbcTemplate.update("DELETE FROM phoenix_group.tickerforum WHERE tif_id = ?", tickerId);
    }

    @Transactional(readOnly = true)
    public List<MessageGroupRow> findMessageGroups(UserSessionDto user, Long headquartersId) {
        requireWorkspaceAccess(user, headquartersId);
        return jdbcTemplate.query(
                """
                        SELECT msggrp.msggrp_id,
                               msggrp.msggrp_name,
                               COUNT(cr.cr_id) AS courier_count
                        FROM messagegroup AS msggrp
                        LEFT JOIN courier AS cr
                               ON cr.hq_id = msggrp.hq_id
                              AND cr.cr_msggrp LIKE CONCAT('%,', msggrp.msggrp_id, ',%')
                        WHERE msggrp.hq_id = ?
                        GROUP BY msggrp.msggrp_id, msggrp.msggrp_name
                        ORDER BY msggrp.msggrp_name, msggrp.msggrp_id
                        """,
                (rs, rowNum) -> new MessageGroupRow(
                        rs.getLong("msggrp_id"),
                        blankToEmpty(rs.getString("msggrp_name")),
                        rs.getInt("courier_count")
                ),
                headquartersId
        );
    }

    public MessageGroupRow saveMessageGroup(UserSessionDto user, Long headquartersId, Long groupId, String name) {
        requireWorkspaceAccess(user, headquartersId);
        if (!accessControlService.canManageCourierMessageGroups(user)) {
            throw new IllegalArgumentException("Nachrichtengruppen duerfen fuer den Benutzer nicht gepflegt werden.");
        }
        String normalizedName = requireLimitedText(name, 120, "Gruppenname");
        if (groupId != null && groupId > 0) {
            int updated = jdbcTemplate.update(
                    "UPDATE messagegroup SET msggrp_name = ? WHERE msggrp_id = ? AND hq_id = ?",
                    normalizedName,
                    groupId,
                    headquartersId
            );
            if (updated != 1) {
                throw new IllegalArgumentException("Nachrichtengruppe konnte nicht aktualisiert werden.");
            }
            return findMessageGroups(user, headquartersId).stream()
                    .filter(row -> row.id().equals(groupId))
                    .findFirst()
                    .orElseThrow(() -> new IllegalArgumentException("Nachrichtengruppe wurde nicht gefunden."));
        }

        KeyHolder keyHolder = new GeneratedKeyHolder();
        jdbcTemplate.update(connection -> {
            PreparedStatement statement = connection.prepareStatement(
                    "INSERT INTO messagegroup (hq_id, msggrp_name) VALUES (?, ?)",
                    Statement.RETURN_GENERATED_KEYS
            );
            statement.setLong(1, headquartersId);
            statement.setString(2, normalizedName);
            return statement;
        }, keyHolder);

        Long id = requireGeneratedId(keyHolder, "Nachrichtengruppe");
        return findMessageGroups(user, headquartersId).stream()
                .filter(row -> row.id().equals(id))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Nachrichtengruppe wurde nicht gefunden."));
    }

    public void deleteMessageGroup(UserSessionDto user, Long headquartersId, Long groupId) {
        requireWorkspaceAccess(user, headquartersId);
        if (!accessControlService.canManageCourierMessageGroups(user)) {
            throw new IllegalArgumentException("Nachrichtengruppen duerfen fuer den Benutzer nicht gepflegt werden.");
        }
        List<CourierGroupState> states = jdbcTemplate.query(
                "SELECT cr_id, cr_msggrp FROM courier WHERE hq_id = ? AND cr_msggrp LIKE ?",
                (rs, rowNum) -> new CourierGroupState(rs.getLong("cr_id"), blankToEmpty(rs.getString("cr_msggrp"))),
                headquartersId,
                "%," + groupId + ",%"
        );
        for (CourierGroupState state : states) {
            List<Long> existingIds = parseTokenIds(state.rawGroups());
            if (existingIds.contains(groupId)) {
                Set<Long> remaining = new LinkedHashSet<>(existingIds);
                remaining.remove(groupId);
                jdbcTemplate.update(
                        "UPDATE courier SET cr_msggrp = ? WHERE cr_id = ? AND hq_id = ?",
                        storeTokenIds(remaining.stream().toList()),
                        state.courierId(),
                        headquartersId
                );
            }
        }
        jdbcTemplate.update("DELETE FROM messagegroup WHERE msggrp_id = ? AND hq_id = ?", groupId, headquartersId);
    }

    @Transactional(readOnly = true)
    public List<CourierOption> findCouriers(UserSessionDto user, Long headquartersId, String query) {
        requireWorkspaceAccess(user, headquartersId);
        String normalizedQuery = query != null ? query.trim().toLowerCase(Locale.ROOT) : "";
        StringBuilder sql = new StringBuilder(
                """
                        SELECT cr.cr_id,
                               cr.cr_eid,
                               cr.cr_sid,
                               cr.cr_msggrp,
                               cmp.cmp_comp,
                               usr.usr_firstname,
                               usr.usr_name
                        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 = ?
                        """
        );
        List<Object> args = new ArrayList<>();
        args.add(headquartersId);
        if (!normalizedQuery.isBlank()) {
            sql.append(" AND (LOWER(cr.cr_eid) LIKE ? OR LOWER(cr.cr_sid) LIKE ? OR LOWER(cmp.cmp_comp) LIKE ? OR LOWER(usr.usr_name) LIKE ?)");
            String token = "%" + normalizedQuery + "%";
            args.add(token);
            args.add(token);
            args.add(token);
            args.add(token);
        }
        sql.append(" ORDER BY cr.cr_eid, cr.cr_sid LIMIT 200");
        return jdbcTemplate.query(Objects.requireNonNull(sql.toString()), (rs, rowNum) -> new CourierOption(
                rs.getLong("cr_id"),
                blankToEmpty(rs.getString("cr_eid")),
                blankToEmpty(rs.getString("cr_sid")),
                blankToEmpty(rs.getString("cmp_comp")),
                (blankToEmpty(rs.getString("usr_firstname")) + " " + blankToEmpty(rs.getString("usr_name"))).trim(),
                parseTokenIds(rs.getString("cr_msggrp"))
        ), args.toArray());
    }

    public CourierOption updateCourierGroups(UserSessionDto user,
                                             Long headquartersId,
                                             Long courierId,
                                             List<Long> groupIds) {
        requireWorkspaceAccess(user, headquartersId);
        if (!accessControlService.canManageCourierMessageGroups(user)) {
            throw new IllegalArgumentException("Kuriergruppen duerfen fuer den Benutzer nicht gepflegt werden.");
        }
        jdbcTemplate.update(
                "UPDATE courier SET cr_msggrp = ? WHERE cr_id = ? AND hq_id = ?",
                storeTokenIds(groupIds),
                courierId,
                headquartersId
        );
        return findCouriers(user, headquartersId, "").stream()
                .filter(row -> row.id().equals(courierId))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Kurier wurde nicht gefunden."));
    }

    @Transactional(readOnly = true)
    public List<GroupAssignmentRow> findGroupAssignments(UserSessionDto user, Long headquartersId) {
        requireWorkspaceAccess(user, headquartersId);
        return jdbcTemplate.query(
                """
                        SELECT msggrp.msggrp_id,
                               msggrp.msggrp_name,
                               cr.cr_id,
                               cr.cr_eid,
                               usr.usr_firstname,
                               usr.usr_name
                        FROM messagegroup AS msggrp
                        JOIN courier AS cr
                          ON cr.hq_id = msggrp.hq_id
                         AND cr.cr_msggrp LIKE CONCAT('%,', msggrp.msggrp_id, ',%')
                        LEFT JOIN user AS usr ON usr.usr_id = cr.usr_id
                        WHERE msggrp.hq_id = ?
                        ORDER BY msggrp.msggrp_name, cr.cr_eid
                        """,
                (rs, rowNum) -> new GroupAssignmentRow(
                        rs.getLong("msggrp_id"),
                        blankToEmpty(rs.getString("msggrp_name")),
                        rs.getLong("cr_id"),
                        blankToEmpty(rs.getString("cr_eid")),
                        (blankToEmpty(rs.getString("usr_firstname")) + " " + blankToEmpty(rs.getString("usr_name"))).trim()
                ),
                headquartersId
        );
    }

    @Transactional(readOnly = true)
    public List<EmployeeOption> findEmployees(UserSessionDto user, Long headquartersId, String query) {
        requireWorkspaceAccess(user, headquartersId);
        String normalizedQuery = query != null ? query.trim().toLowerCase(Locale.ROOT) : "";
        StringBuilder sql = new StringBuilder(
                """
                        SELECT emp.emp_id, usr.usr_firstname, usr.usr_name
                        FROM employee AS emp
                        JOIN user AS usr ON usr.usr_id = emp.usr_id
                        WHERE usr.hq_id = ?
                        """
        );
        List<Object> args = new ArrayList<>();
        args.add(headquartersId);
        if (!normalizedQuery.isBlank()) {
            sql.append(" AND (LOWER(usr.usr_firstname) LIKE ? OR LOWER(usr.usr_name) LIKE ?)");
            String token = "%" + normalizedQuery + "%";
            args.add(token);
            args.add(token);
        }
        sql.append(" ORDER BY usr.usr_firstname, usr.usr_name LIMIT 100");
        return jdbcTemplate.query(Objects.requireNonNull(sql.toString()), (rs, rowNum) -> new EmployeeOption(
                rs.getLong("emp_id"),
                (blankToEmpty(rs.getString("usr_firstname")) + " " + blankToEmpty(rs.getString("usr_name"))).trim()
        ), args.toArray());
    }

    @Transactional(readOnly = true)
    public List<NewsletterRecipientRow> findNewsletterRecipients(UserSessionDto user,
                                                                 Long headquartersId,
                                                                 String typeFilter,
                                                                 String statusFilter,
                                                                 String search) {
        requireNewsletterAccess(user, headquartersId);

        String normalizedType = trimToEmpty(typeFilter).toLowerCase(Locale.ROOT);
        String normalizedStatus = trimToEmpty(statusFilter).toLowerCase(Locale.ROOT);
        String normalizedSearch = trimToEmpty(search).toLowerCase(Locale.ROOT);

        StringBuilder sql = new StringBuilder(
                """
                        SELECT audience.recipient_type,
                               audience.recipient_id,
                               audience.company_id,
                               audience.recipient_code,
                               audience.secondary_code,
                               audience.company_name,
                               audience.contact_name,
                               audience.email,
                               audience.newsletter_state,
                               audience.dsgvo_state
                        FROM (
                            SELECT 'cs' AS recipient_type,
                                   cs.cs_id AS recipient_id,
                                   cmp.cmp_id AS company_id,
                                   COALESCE(cs.cs_eid, '') AS recipient_code,
                                   '' AS secondary_code,
                                   COALESCE(cmp.cmp_comp, '') AS company_name,
                                   TRIM(CONCAT(COALESCE(usr.usr_firstname, ''), ' ', COALESCE(usr.usr_name, ''))) AS contact_name,
                                   COALESCE(usr.usr_email, '') AS email,
                                   COALESCE(cmp.cmp_newsletter, '0') AS newsletter_state,
                                   COALESCE(cmp.cmp_dsgvo, '1') AS dsgvo_state
                            FROM customer AS cs
                            JOIN company AS cmp ON cmp.cmp_id = cs.cmp_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 = ?
                              AND COALESCE(cmp.cmp_archived, '0') = '0'
                            UNION ALL
                            SELECT 'cr' AS recipient_type,
                                   cr.cr_id AS recipient_id,
                                   cmp.cmp_id AS company_id,
                                   COALESCE(cr.cr_eid, '') AS recipient_code,
                                   COALESCE(cr.cr_sid, '') AS secondary_code,
                                   COALESCE(cmp.cmp_comp, '') AS company_name,
                                   TRIM(CONCAT(COALESCE(usr.usr_firstname, ''), ' ', COALESCE(usr.usr_name, ''))) AS contact_name,
                                   COALESCE(usr.usr_email, '') AS email,
                                   COALESCE(cmp.cmp_newsletter, '0') AS newsletter_state,
                                   COALESCE(cmp.cmp_dsgvo, '1') AS dsgvo_state
                            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 = ?
                              AND COALESCE(cmp.cmp_archived, '0') = '0'
                        ) AS audience
                        WHERE 1 = 1
                        """
        );
        List<Object> args = new ArrayList<>();
        args.add(headquartersId);
        args.add(headquartersId);

        if ("cs".equals(normalizedType) || "cr".equals(normalizedType)) {
            sql.append(" AND audience.recipient_type = ?");
            args.add(normalizedType);
        }

        switch (normalizedStatus) {
            case "ready" -> sql.append(" AND audience.newsletter_state = '1' AND audience.dsgvo_state = '1' AND audience.email <> ''");
            case "active" -> sql.append(" AND audience.newsletter_state = '1'");
            case "inactive" -> sql.append(" AND audience.newsletter_state <> '1'");
            case "pendingconsent" -> sql.append(" AND audience.dsgvo_state <> '1'");
            case "missingemail" -> sql.append(" AND audience.email = ''");
            default -> {
            }
        }

        if (!normalizedSearch.isBlank()) {
            sql.append(" AND (LOWER(audience.recipient_code) LIKE ? OR LOWER(audience.secondary_code) LIKE ? OR LOWER(audience.company_name) LIKE ? OR LOWER(audience.contact_name) LIKE ? OR LOWER(audience.email) LIKE ?)");
            String token = "%" + normalizedSearch + "%";
            args.add(token);
            args.add(token);
            args.add(token);
            args.add(token);
            args.add(token);
        }

        sql.append(" ORDER BY audience.company_name, audience.recipient_type, audience.recipient_code, audience.secondary_code");

        return jdbcTemplate.query(Objects.requireNonNull(sql.toString()), (rs, rowNum) -> {
            String recipientType = blankToEmpty(rs.getString("recipient_type"));
            boolean newsletterActive = "1".equals(blankToEmpty(rs.getString("newsletter_state")));
            boolean dsgvoConfirmed = "1".equals(blankToEmpty(rs.getString("dsgvo_state")));
            String email = blankToEmpty(rs.getString("email"));
            return new NewsletterRecipientRow(
                    recipientType,
                    rs.getLong("recipient_id"),
                    rs.getLong("company_id"),
                    newsletterRecipientTypeLabel(recipientType),
                    blankToEmpty(rs.getString("recipient_code")),
                    blankToEmpty(rs.getString("secondary_code")),
                    blankToEmpty(rs.getString("company_name")),
                    blankToEmpty(rs.getString("contact_name")),
                    email,
                    newsletterActive,
                    dsgvoConfirmed,
                    newsletterActive && dsgvoConfirmed && !email.isBlank()
            );
        }, args.toArray());
    }

    public NewsletterRecipientRow updateNewsletterRecipient(UserSessionDto user,
                                                            Long headquartersId,
                                                            String recipientType,
                                                            Long recipientId,
                                                            boolean newsletterActive,
                                                            boolean dsgvoConfirmed) {
        requireNewsletterAccess(user, headquartersId);
        NewsletterTarget target = resolveNewsletterTarget(headquartersId, recipientType, recipientId);
        jdbcTemplate.update(
                """
                        UPDATE company
                        SET cmp_newsletter = ?,
                            cmp_dsgvo = ?,
                            cmp_modify_status = '2'
                        WHERE cmp_id = ?
                        """,
                newsletterActive ? "1" : "0",
                dsgvoConfirmed ? "1" : "0",
                target.companyId()
        );
        return loadNewsletterRecipient(user, headquartersId, target.recipientType(), target.recipientId());
    }

    @Transactional(readOnly = true)
    public List<MessageRow> findMessages(UserSessionDto user,
                                         Long headquartersId,
                                         LocalDate from,
                                         LocalDate to,
                                         List<Integer> states,
                                         String typeFilter,
                                         String search) {
        requireWorkspaceAccess(user, headquartersId);
        StringBuilder sql = new StringBuilder(
                """
                        SELECT mf.mf_id,
                               mf.mf_type,
                               mf.mf_createtime,
                               mf.mf_acknowledge,
                               mf.usr_id_sender,
                               mf.usr_id_receiver,
                               mf.usr_id_first_reader,
                               mf.usr_id_answerer,
                               mf.mf_usr_type,
                               mf.mf_state,
                               mf.mf_prio,
                               mf.mf_subject,
                               mf.mf_body,
                               mf.mf_pre_id,
                               usr_sender.usr_firstname AS sender_firstname,
                               usr_sender.usr_name AS sender_name,
                               cr_sender.cr_sid AS sender_sid,
                               usr_receiver.usr_firstname AS receiver_firstname,
                               usr_receiver.usr_name AS receiver_name,
                               cr_receiver.cr_sid AS receiver_sid
                        FROM phoenix_log.messageforum AS mf
                        LEFT JOIN user AS usr_sender ON usr_sender.usr_id = mf.usr_id_sender
                        LEFT JOIN courier AS cr_sender ON cr_sender.usr_id = usr_sender.usr_id
                        LEFT JOIN user AS usr_receiver ON usr_receiver.usr_id = mf.usr_id_receiver
                        LEFT JOIN courier AS cr_receiver ON cr_receiver.usr_id = usr_receiver.usr_id
                        WHERE mf.hq_id = ?
                        """
        );
        List<Object> args = new ArrayList<>();
        args.add(headquartersId);

        LocalDate normalizedFrom = from != null ? from : LocalDate.now();
        LocalDate normalizedTo = to != null ? to : LocalDate.now();
        sql.append(" AND mf.mf_createtime >= ? AND mf.mf_createtime <= ?");
        args.add(Timestamp.valueOf(normalizedFrom.atStartOfDay()));
        args.add(Timestamp.valueOf(normalizedTo.plusDays(1).atStartOfDay().minusNanos(1)));

        if (states != null && !states.isEmpty()) {
            sql.append(" AND mf.mf_state IN (");
            for (int i = 0; i < states.size(); i++) {
                if (i > 0) {
                    sql.append(", ");
                }
                sql.append("?");
                args.add(states.get(i));
            }
            sql.append(")");
        }
        if ("direct".equalsIgnoreCase(typeFilter)) {
            sql.append(" AND COALESCE(mf.mf_type, '0') <> '1'");
        } else if ("group".equalsIgnoreCase(typeFilter)) {
            sql.append(" AND COALESCE(mf.mf_type, '0') = '1'");
        }

        String normalizedSearch = search != null ? search.trim().toLowerCase(Locale.ROOT) : "";
        if (!normalizedSearch.isBlank()) {
            sql.append(" AND (LOWER(COALESCE(mf.mf_subject, '')) LIKE ? OR LOWER(COALESCE(mf.mf_body, '')) LIKE ?)");
            String token = "%" + normalizedSearch + "%";
            args.add(token);
            args.add(token);
        }
        sql.append(" ORDER BY mf.mf_createtime DESC, mf.mf_id DESC LIMIT 1000");

        return jdbcTemplate.query(Objects.requireNonNull(sql.toString()), (rs, rowNum) -> {
            int state = rs.getInt("mf_state");
            String senderUserType = blankToEmpty(rs.getString("mf_usr_type"));
            return new MessageRow(
                    rs.getLong("mf_id"),
                    formatTimestamp(rs.getTimestamp("mf_createtime")),
                    state,
                    blankToEmpty(rs.getString("mf_type")),
                    senderLabel(
                            rs.getString("sender_firstname"),
                            rs.getString("sender_name"),
                            rs.getString("sender_sid")
                    ),
                    senderLabel(
                            rs.getString("receiver_firstname"),
                            rs.getString("receiver_name"),
                            rs.getString("receiver_sid")
                    ),
                    blankToEmpty(rs.getString("mf_subject")),
                    blankToEmpty(rs.getString("mf_body")),
                    rs.getLong("usr_id_sender"),
                    rs.getLong("usr_id_receiver"),
                    rs.getLong("mf_pre_id"),
                    state < 2 && "3".equals(senderUserType),
                    state < 3 && "3".equals(senderUserType)
            );
        }, args.toArray());
    }

    public MessageRow markMessageRead(UserSessionDto user, Long headquartersId, Long messageId) {
        requireWorkspaceAccess(user, headquartersId);
        if (!accessControlService.canManageDeviceMessages(user)) {
            throw new IllegalArgumentException("Nachrichten duerfen fuer den Benutzer nicht bearbeitet werden.");
        }
        Long userId = requireUserId(user);
        MessageAccessRow message = loadMessageAccess(messageId);
        if (!Objects.equals(message.headquartersId(), headquartersId)) {
            throw new IllegalArgumentException("Nachricht gehoert zu einer anderen Niederlassung.");
        }
        if (message.state() < 2) {
            jdbcTemplate.update(
                    """
                            UPDATE phoenix_log.messageforum
                            SET mf_state = 2,
                                mf_acknowledge = ?,
                                usr_id_first_reader = ?
                            WHERE mf_id = ?
                              AND COALESCE(mf_state, 0) < 2
                            """,
                    Timestamp.valueOf(LocalDateTime.now()),
                    userId,
                    messageId
            );
        }
        return loadMessageRow(headquartersId, messageId);
    }

    public MessageRow answerMessage(UserSessionDto user,
                                    Long headquartersId,
                                    Long messageId,
                                    String subject,
                                    String body) {
        requireWorkspaceAccess(user, headquartersId);
        if (!accessControlService.canManageDeviceMessages(user)) {
            throw new IllegalArgumentException("Nachrichten duerfen fuer den Benutzer nicht beantwortet werden.");
        }
        Long userId = requireUserId(user);
        MessageAccessRow message = loadMessageAccess(messageId);
        if (!Objects.equals(message.headquartersId(), headquartersId)) {
            throw new IllegalArgumentException("Nachricht gehoert zu einer anderen Niederlassung.");
        }
        if (!"3".equals(message.userType())) {
            throw new IllegalArgumentException("Nur Fahrernachrichten koennen beantwortet werden.");
        }
        if (message.state() < 3) {
            jdbcTemplate.update(
                    """
                            UPDATE phoenix_log.messageforum
                            SET mf_state = 3,
                                usr_id_answerer = ?,
                                mf_acknowledge = ?
                            WHERE mf_id = ?
                              AND COALESCE(mf_state, 0) < 3
                            """,
                    userId,
                    Timestamp.valueOf(LocalDateTime.now()),
                    messageId
            );
        }
        sendToReceivers(headquartersId, userId, List.of(message.senderUserId()), requireLimitedText(subject, 80, "Betreff"),
                requireLimitedText(body, maxLength("MESSAGE_MAX_BODY_LENGTH", headquartersId, 250), "Text"), "0", messageId);
        return loadMessageRow(headquartersId, messageId);
    }

    public SendResult sendMessage(UserSessionDto user,
                                  Long headquartersId,
                                  List<Long> groupIds,
                                  String courierSid,
                                  String subject,
                                  String body,
                                  boolean allActiveCouriers) {
        requireWorkspaceAccess(user, headquartersId);
        if (!accessControlService.canManageDeviceMessages(user)) {
            throw new IllegalArgumentException("Nachrichten duerfen fuer den Benutzer nicht versendet werden.");
        }
        Long userId = requireUserId(user);
        String normalizedSubject = trimToEmpty(subject);
        String normalizedBody = requireLimitedText(body, maxLength("MESSAGE_MAX_BODY_LENGTH", headquartersId, 250), "Text");

        List<Long> receiverUserIds;
        String type;
        if (allActiveCouriers) {
            receiverUserIds = activeCourierUserIds(headquartersId);
            type = "1";
        } else if (courierSid != null && !courierSid.isBlank()) {
            receiverUserIds = directCourierUserIds(headquartersId, courierSid);
            type = "0";
        } else {
            receiverUserIds = groupedCourierUserIds(headquartersId, groupIds);
            type = "1";
        }

        if (receiverUserIds.isEmpty()) {
            throw new IllegalArgumentException("Es wurden keine Empfaenger fuer den Versand gefunden.");
        }
        sendToReceivers(headquartersId, userId, receiverUserIds, normalizedSubject, normalizedBody, type, 0L);
        return new SendResult(receiverUserIds.size());
    }

    private void sendToReceivers(Long headquartersId,
                                 Long senderUserId,
                                 List<Long> receiverUserIds,
                                 String subject,
                                 String body,
                                 String type,
                                 Long predecessorId) {
        LocalDateTime now = LocalDateTime.now();
        for (Long receiverUserId : receiverUserIds.stream().distinct().toList()) {
            jdbcTemplate.update(
                    """
                            INSERT INTO phoenix_log.messageforum
                                (mf_type, mf_createtime, usr_id_sender, hq_id, usr_id_receiver, mf_usr_type, mf_state, mf_prio, mf_subject, mf_body, mf_pre_id)
                            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                            """,
                    type,
                    Timestamp.valueOf(now),
                    senderUserId,
                    headquartersId,
                    receiverUserId,
                    "1",
                    0,
                    0,
                    subject,
                    body,
                    predecessorId != null ? predecessorId : 0L
            );
        }
    }

    private List<Long> groupedCourierUserIds(Long headquartersId, List<Long> groupIds) {
        List<Long> normalizedGroupIds = groupIds != null ? groupIds.stream().filter(Objects::nonNull).filter(id -> id > 0).distinct().toList() : List.of();
        if (normalizedGroupIds.isEmpty()) {
            return List.of();
        }
        StringBuilder sql = new StringBuilder(
                """
                        SELECT DISTINCT cr.usr_id
                        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'
                          AND COALESCE(cmp.cmp_authenticated, '1') = '1'
                          AND (
                        """
        );
        List<Object> args = new ArrayList<>();
        args.add(headquartersId);
        for (int i = 0; i < normalizedGroupIds.size(); i++) {
            if (i > 0) {
                sql.append(" OR ");
            }
            sql.append("cr.cr_msggrp LIKE ?");
            args.add("%," + normalizedGroupIds.get(i) + ",%");
        }
        sql.append(") ORDER BY cr.usr_id");
        return jdbcTemplate.query(Objects.requireNonNull(sql.toString()), (rs, rowNum) -> rs.getLong(1), args.toArray());
    }

    private List<Long> directCourierUserIds(Long headquartersId, String courierSid) {
        return jdbcTemplate.query(
                """
                        SELECT cr.usr_id
                        FROM courier AS cr
                        WHERE cr.hq_id = ?
                          AND cr.cr_sid = ?
                        """,
                (rs, rowNum) -> rs.getLong("usr_id"),
                headquartersId,
                courierSid.trim()
        );
    }

    private List<Long> activeCourierUserIds(Long headquartersId) {
        int days = maxLength("MESSAGE_MIN_DAYS_BEFORE", headquartersId, 7);
        LocalDateTime minimumAvailableTime = LocalDateTime.now().minusDays(days);
        return jdbcTemplate.query(
                """
                        SELECT DISTINCT cr.usr_id
                        FROM courier AS cr
                        JOIN company AS cmp ON cmp.cmp_id = cr.cmp_id
                        WHERE cr.cr_availabletime > ?
                          AND COALESCE(cmp.cmp_archived, '0') = '0'
                          AND COALESCE(cmp.cmp_authenticated, '1') = '1'
                        ORDER BY cr.usr_id
                        """,
                (rs, rowNum) -> rs.getLong("usr_id"),
                Timestamp.valueOf(minimumAvailableTime)
        );
    }

    private boolean canDeleteTicker(UserSessionDto user, Long headquartersId, Long tickerId, Long creatorEmployeeId) {
        if (!accessControlService.canManageTicker(user)) {
            return false;
        }
        Long employeeId = user != null ? user.getEmployeeId() : null;
        return employeeId != null
                && (employeeId.equals(creatorEmployeeId) || isRootAdmin(headquartersId, employeeId));
    }

    private TickerRow findTicker(Long headquartersId, Long tickerId, UserSessionDto user) {
        return findTickers(user, headquartersId).stream()
                .filter(row -> row.id().equals(tickerId))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Mitteilung wurde nicht gefunden."));
    }

    private TickerAccess loadTickerAccess(Long tickerId) {
        List<TickerAccess> rows = jdbcTemplate.query(
                "SELECT tif_id, emp_id_creator FROM phoenix_group.tickerforum WHERE tif_id = ?",
                (rs, rowNum) -> new TickerAccess(rs.getLong("tif_id"), rs.getLong("emp_id_creator")),
                tickerId
        );
        if (rows.isEmpty()) {
            throw new IllegalArgumentException("Mitteilung wurde nicht gefunden.");
        }
        return rows.get(0);
    }

    private MessageAccessRow loadMessageAccess(Long messageId) {
        List<MessageAccessRow> rows = jdbcTemplate.query(
                """
                        SELECT mf_id, hq_id, usr_id_sender, mf_state, mf_usr_type
                        FROM phoenix_log.messageforum
                        WHERE mf_id = ?
                        """,
                (rs, rowNum) -> new MessageAccessRow(
                        rs.getLong("mf_id"),
                        rs.getLong("hq_id"),
                        rs.getLong("usr_id_sender"),
                        rs.getInt("mf_state"),
                        blankToEmpty(rs.getString("mf_usr_type"))
                ),
                messageId
        );
        if (rows.isEmpty()) {
            throw new IllegalArgumentException("Nachricht wurde nicht gefunden.");
        }
        return rows.get(0);
    }

    private MessageRow loadMessageRow(Long headquartersId, Long messageId) {
        List<MessageRow> rows = jdbcTemplate.query(
                """
                        SELECT mf.mf_id,
                               mf.mf_type,
                               mf.mf_createtime,
                               mf.mf_usr_type,
                               mf.mf_state,
                               mf.mf_subject,
                               mf.mf_body,
                               usr_sender.usr_firstname AS sender_firstname,
                               usr_sender.usr_name AS sender_name,
                               cr_sender.cr_sid AS sender_sid,
                               usr_receiver.usr_firstname AS receiver_firstname,
                               usr_receiver.usr_name AS receiver_name,
                               cr_receiver.cr_sid AS receiver_sid
                        FROM phoenix_log.messageforum AS mf
                        LEFT JOIN user AS usr_sender ON usr_sender.usr_id = mf.usr_id_sender
                        LEFT JOIN courier AS cr_sender ON cr_sender.usr_id = usr_sender.usr_id
                        LEFT JOIN user AS usr_receiver ON usr_receiver.usr_id = mf.usr_id_receiver
                        LEFT JOIN courier AS cr_receiver ON cr_receiver.usr_id = usr_receiver.usr_id
                        WHERE mf.hq_id = ?
                          AND mf.mf_id = ?
                        """,
                (rs, rowNum) -> {
                    int state = rs.getInt("mf_state");
                    String senderUserType = blankToEmpty(rs.getString("mf_usr_type"));
                    return new MessageRow(
                            rs.getLong("mf_id"),
                            formatTimestamp(rs.getTimestamp("mf_createtime")),
                            state,
                            blankToEmpty(rs.getString("mf_type")),
                            senderLabel(rs.getString("sender_firstname"), rs.getString("sender_name"), rs.getString("sender_sid")),
                            senderLabel(rs.getString("receiver_firstname"), rs.getString("receiver_name"), rs.getString("receiver_sid")),
                            blankToEmpty(rs.getString("mf_subject")),
                            blankToEmpty(rs.getString("mf_body")),
                            0L,
                            0L,
                            0L,
                            state < 2 && "3".equals(senderUserType),
                            state < 3 && "3".equals(senderUserType)
                    );
                },
                headquartersId,
                messageId
        );
        if (rows.isEmpty()) {
            throw new IllegalArgumentException("Nachricht wurde nicht gefunden.");
        }
        return rows.get(0);
    }

    private void cleanupExpiredTickerEntries() {
        jdbcTemplate.update(
                "DELETE FROM phoenix_group.tickerforum WHERE tif_endtime < ? AND COALESCE(tif_unerasable, '0') <> '1'",
                Timestamp.valueOf(LocalDateTime.now())
        );
    }

    private boolean isRootAdmin(Long headquartersId, Long employeeId) {
        Long rootAdminEmployeeId = jdbcTemplate.queryForObject(
                "SELECT hq_admin FROM headquarters WHERE hq_id = ?",
                Long.class,
                headquartersId
        );
        return Objects.equals(rootAdminEmployeeId, employeeId);
    }

    private void requireWorkspaceAccess(UserSessionDto user, Long headquartersId) {
        if (!accessControlService.canViewCommunication(user)) {
            throw new IllegalArgumentException("Kommunikationsworkspace ist fuer den Benutzer nicht freigeschaltet.");
        }
        if (headquartersId == null || !accessControlService.canAccessHeadquarters(user, headquartersId)) {
            throw new IllegalArgumentException("Niederlassung ist fuer den Kommunikationsworkspace ungueltig.");
        }
    }

    private void requireNewsletterAccess(UserSessionDto user, Long headquartersId) {
        requireWorkspaceAccess(user, headquartersId);
        if (!accessControlService.canManageNewsletter(user)) {
            throw new IllegalArgumentException("Newsletter duerfen fuer den Benutzer nicht gepflegt werden.");
        }
    }

    private Long requireEmployeeId(UserSessionDto user) {
        if (user == null || user.getEmployeeId() == null || user.getEmployeeId() <= 0) {
            throw new IllegalArgumentException("Mitarbeiterkontext ist unvollstaendig.");
        }
        return user.getEmployeeId();
    }

    private Long requireUserId(UserSessionDto user) {
        if (user == null || user.getUserId() == null || user.getUserId() <= 0) {
            throw new IllegalArgumentException("Benutzerkontext ist unvollstaendig.");
        }
        return user.getUserId();
    }

    private int maxLength(String parameterKey, Long headquartersId, int fallback) {
        String value = parameterService.getParameterValueWithFallback(parameterKey, headquartersId != null ? headquartersId : 0L);
        if (value == null || value.isBlank()) {
            return fallback;
        }
        try {
            return Integer.parseInt(value.trim());
        } catch (NumberFormatException ignored) {
            return fallback;
        }
    }

    private String requireLimitedText(String value, int maxLength, String label) {
        String normalized = value != null ? value.trim() : "";
        if (normalized.isBlank()) {
            throw new IllegalArgumentException(label + " fehlt.");
        }
        if (normalized.length() > maxLength) {
            return normalized.substring(0, maxLength);
        }
        return normalized;
    }

    private static String senderLabel(String firstname, String lastname, String sid) {
        String label = (blankToEmpty(firstname) + " " + blankToEmpty(lastname)).trim();
        if (!blankToEmpty(sid).isBlank()) {
            label = label.isBlank() ? sid : label + " [" + sid + "]";
        }
        return label;
    }

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

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

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

    private NewsletterTarget resolveNewsletterTarget(Long headquartersId, String recipientType, Long recipientId) {
        if (recipientId == null || recipientId <= 0) {
            throw new IllegalArgumentException("Newsletter-Empfaenger ist ungueltig.");
        }
        String normalizedType = trimToEmpty(recipientType).toLowerCase(Locale.ROOT);
        List<NewsletterTarget> rows;
        if ("cs".equals(normalizedType)) {
            rows = jdbcTemplate.query(
                    """
                            SELECT cs.cs_id AS recipient_id, cs.cmp_id AS company_id
                            FROM customer AS cs
                            WHERE cs.hq_id = ?
                              AND cs.cs_id = ?
                            """,
                    (rs, rowNum) -> new NewsletterTarget("cs", rs.getLong("recipient_id"), rs.getLong("company_id")),
                    headquartersId,
                    recipientId
            );
        } else if ("cr".equals(normalizedType)) {
            rows = jdbcTemplate.query(
                    """
                            SELECT cr.cr_id AS recipient_id, cr.cmp_id AS company_id
                            FROM courier AS cr
                            WHERE cr.hq_id = ?
                              AND cr.cr_id = ?
                            """,
                    (rs, rowNum) -> new NewsletterTarget("cr", rs.getLong("recipient_id"), rs.getLong("company_id")),
                    headquartersId,
                    recipientId
            );
        } else {
            throw new IllegalArgumentException("Newsletter-Empfaengertyp ist ungueltig.");
        }
        if (rows.isEmpty()) {
            throw new IllegalArgumentException("Newsletter-Empfaenger wurde nicht gefunden.");
        }
        return rows.get(0);
    }

    private NewsletterRecipientRow loadNewsletterRecipient(UserSessionDto user,
                                                           Long headquartersId,
                                                           String recipientType,
                                                           Long recipientId) {
        return findNewsletterRecipients(user, headquartersId, recipientType, "all", "").stream()
                .filter(row -> row.recipientType().equals(recipientType))
                .filter(row -> Objects.equals(row.recipientId(), recipientId))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Newsletter-Empfaenger wurde nicht gefunden."));
    }

    private static String newsletterRecipientTypeLabel(String recipientType) {
        return switch (trimToEmpty(recipientType).toLowerCase(Locale.ROOT)) {
            case "cs" -> "Kunde";
            case "cr" -> "Kurier";
            default -> recipientType;
        };
    }

    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();
    }

    public List<Long> parseTokenIds(String value) {
        if (value == null || value.isBlank()) {
            return List.of();
        }
        String[] tokens = value.replace(" ", "").split(",");
        Set<Long> ids = new LinkedHashSet<>();
        for (String token : tokens) {
            if (token == null || token.isBlank()) {
                continue;
            }
            try {
                ids.add(Long.parseLong(token));
            } catch (NumberFormatException ignored) {
            }
        }
        return new ArrayList<>(ids);
    }

    public String storeTokenIds(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return "";
        }
        StringBuilder builder = new StringBuilder(",");
        ids.stream()
                .filter(Objects::nonNull)
                .filter(id -> id > 0)
                .distinct()
                .sorted()
                .forEach(id -> builder.append(id).append(','));
        return builder.length() > 1 ? builder.toString() : "";
    }

    private record TickerAccess(Long id, Long creatorEmployeeId) {
    }

    private record MessageAccessRow(Long id, Long headquartersId, Long senderUserId, int state, String userType) {
    }

    private record CourierGroupState(Long courierId, String rawGroups) {
    }

    private record NewsletterTarget(String recipientType, Long recipientId, Long companyId) {
    }

    public record Bootstrap(boolean canManageTicker,
                            boolean canManageMessages,
                            boolean canManageGroups,
                            boolean canManageNewsletter,
                            int unreadCourierMessagesToday) {
    }

    public record TickerRow(Long id,
                            String createdAt,
                            String startAt,
                            String endAt,
                            String text,
                            String creatorLabel,
                            String targetHeadquartersLabel,
                            String targetEmployeeLabel,
                            boolean unerasable,
                            boolean deletable) {
    }

    public record MessageGroupRow(Long id, String name, int courierCount) {
    }

    public record CourierOption(Long id,
                                String code,
                                String sid,
                                String company,
                                String contactName,
                                List<Long> groupIds) {
    }

    public record GroupAssignmentRow(Long groupId,
                                     String groupName,
                                     Long courierId,
                                     String courierCode,
                                     String courierName) {
    }

    public record EmployeeOption(Long id, String label) {
    }

    public record NewsletterRecipientRow(String recipientType,
                                         Long recipientId,
                                         Long companyId,
                                         String recipientTypeLabel,
                                         String recipientCode,
                                         String secondaryCode,
                                         String companyName,
                                         String contactName,
                                         String email,
                                         boolean newsletterActive,
                                         boolean dsgvoConfirmed,
                                         boolean deliverable) {
    }

    public record MessageRow(Long id,
                             String createdAt,
                             int state,
                             String type,
                             String senderLabel,
                             String receiverLabel,
                             String subject,
                             String body,
                             Long senderUserId,
                             Long receiverUserId,
                             Long predecessorId,
                             boolean readable,
                             boolean answerable) {
    }

    public record SendResult(int recipients) {
    }
}
