package shine.db.dao;

import shine.db.SqliteDbController;
import shine.db.entities.SignedMessageV2Entry;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public final class SignedMessagesV2DAO {
    private static volatile SignedMessagesV2DAO instance;
    private final SqliteDbController db = SqliteDbController.getInstance();

    private SignedMessagesV2DAO() {}

    public static SignedMessagesV2DAO getInstance() {
        if (instance == null) {
            synchronized (SignedMessagesV2DAO.class) {
                if (instance == null) instance = new SignedMessagesV2DAO();
            }
        }
        return instance;
    }

    public boolean insertIfAbsent(SignedMessageV2Entry e) throws Exception {
        try (Connection c = db.getConnection()) {
            String sql = """
                INSERT OR IGNORE INTO signed_messages_v2 (
                    message_key, base_key, target_login, from_login, to_login,
                    time_ms, nonce, message_type, raw_block, created_at_ms,
                    source_api, origin_session_id, receipt_ref_base_key, receipt_ref_type
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """;
            try (PreparedStatement ps = c.prepareStatement(sql)) {
                ps.setString(1, e.getMessageKey());
                ps.setString(2, e.getBaseKey());
                ps.setString(3, e.getTargetLogin());
                ps.setString(4, e.getFromLogin());
                ps.setString(5, e.getToLogin());
                ps.setLong(6, e.getTimeMs());
                ps.setLong(7, e.getNonce());
                ps.setInt(8, e.getMessageType());
                ps.setBytes(9, e.getRawBlock());
                ps.setLong(10, e.getCreatedAtMs());
                ps.setString(11, e.getSourceApi());
                ps.setString(12, e.getOriginSessionId());
                ps.setString(13, e.getReceiptRefBaseKey());
                if (e.getReceiptRefType() == null) ps.setObject(14, null);
                else ps.setInt(14, e.getReceiptRefType());
                return ps.executeUpdate() > 0;
            }
        }
    }

    /**
     * Атомарная вставка пары блоков: либо вставляются оба, либо не вставляется ни один.
     * Возвращает true только если обе записи добавлены в БД.
     * Если хотя бы одна запись уже существует (или конфликтует по уникальности), возвращает false.
     */
    public boolean insertPairBothOrNothing(SignedMessageV2Entry first, SignedMessageV2Entry second) throws Exception {
        try (Connection c = db.getConnection()) {
            boolean prevAutoCommit = c.getAutoCommit();
            c.setAutoCommit(false);
            try {
                int insertedFirst = insertStrict(c, first);
                int insertedSecond = insertStrict(c, second);
                if (insertedFirst == 1 && insertedSecond == 1) {
                    c.commit();
                    return true;
                }
                c.rollback();
                return false;
            } catch (SQLException sqlEx) {
                try { c.rollback(); } catch (Exception ignored) {}
                if (isConstraintViolation(sqlEx)) {
                    return false;
                }
                throw sqlEx;
            } finally {
                c.setAutoCommit(prevAutoCommit);
            }
        }
    }

    private int insertStrict(Connection c, SignedMessageV2Entry e) throws SQLException {
        String sql = """
            INSERT INTO signed_messages_v2 (
                message_key, base_key, target_login, from_login, to_login,
                time_ms, nonce, message_type, raw_block, created_at_ms,
                source_api, origin_session_id, receipt_ref_base_key, receipt_ref_type
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """;
        try (PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setString(1, e.getMessageKey());
            ps.setString(2, e.getBaseKey());
            ps.setString(3, e.getTargetLogin());
            ps.setString(4, e.getFromLogin());
            ps.setString(5, e.getToLogin());
            ps.setLong(6, e.getTimeMs());
            ps.setLong(7, e.getNonce());
            ps.setInt(8, e.getMessageType());
            ps.setBytes(9, e.getRawBlock());
            ps.setLong(10, e.getCreatedAtMs());
            ps.setString(11, e.getSourceApi());
            ps.setString(12, e.getOriginSessionId());
            ps.setString(13, e.getReceiptRefBaseKey());
            if (e.getReceiptRefType() == null) ps.setObject(14, null);
            else ps.setInt(14, e.getReceiptRefType());
            return ps.executeUpdate();
        }
    }

    private boolean isConstraintViolation(SQLException ex) {
        String msg = String.valueOf(ex.getMessage()).toLowerCase();
        return msg.contains("constraint") || msg.contains("unique") || msg.contains("primary key");
    }

    public SignedMessageV2Entry getByMessageKey(String messageKey) throws Exception {
        try (Connection c = db.getConnection()) {
            String sql = """
                SELECT
                    message_key, base_key, target_login, from_login, to_login,
                    time_ms, nonce, message_type, raw_block, created_at_ms,
                    source_api, origin_session_id, receipt_ref_base_key, receipt_ref_type
                FROM signed_messages_v2
                WHERE message_key = ?
                """;
            try (PreparedStatement ps = c.prepareStatement(sql)) {
                ps.setString(1, messageKey);
                try (ResultSet rs = ps.executeQuery()) {
                    if (!rs.next()) return null;
                    return mapRow(rs);
                }
            }
        }
    }

    public void ensureDeliveryRow(String messageKey, String sessionId, long nowMs) throws Exception {
        try (Connection c = db.getConnection()) {
            String sql = """
                INSERT OR IGNORE INTO signed_message_session_delivery (
                    message_key, session_id, delivered, delivered_at_ms, created_at_ms
                ) VALUES (?, ?, 0, NULL, ?)
                """;
            try (PreparedStatement ps = c.prepareStatement(sql)) {
                ps.setString(1, messageKey);
                ps.setString(2, sessionId);
                ps.setLong(3, nowMs);
                ps.executeUpdate();
            }
        }
    }

    public void markDelivered(String messageKey, String sessionId, long deliveredAtMs) throws Exception {
        try (Connection c = db.getConnection()) {
            String insertSql = """
                INSERT OR IGNORE INTO signed_message_session_delivery (
                    message_key, session_id, delivered, delivered_at_ms, created_at_ms
                ) VALUES (?, ?, 0, NULL, ?)
                """;
            try (PreparedStatement ps = c.prepareStatement(insertSql)) {
                ps.setString(1, messageKey);
                ps.setString(2, sessionId);
                ps.setLong(3, deliveredAtMs);
                ps.executeUpdate();
            }

            String updateSql = """
                UPDATE signed_message_session_delivery
                SET delivered = 1, delivered_at_ms = ?
                WHERE message_key = ? AND session_id = ?
                """;
            try (PreparedStatement ps = c.prepareStatement(updateSql)) {
                ps.setLong(1, deliveredAtMs);
                ps.setString(2, messageKey);
                ps.setString(3, sessionId);
                ps.executeUpdate();
            }
        }
    }

    public List<SignedMessageV2Entry> listPendingForSession(String login, String sessionId) throws Exception {
        try (Connection c = db.getConnection()) {
            String fillSql = """
                INSERT OR IGNORE INTO signed_message_session_delivery (
                    message_key, session_id, delivered, delivered_at_ms, created_at_ms
                )
                SELECT m.message_key, ?, 0, NULL, ?
                FROM signed_messages_v2 m
                WHERE m.target_login = ? COLLATE NOCASE
                """;
            long now = System.currentTimeMillis();
            try (PreparedStatement ps = c.prepareStatement(fillSql)) {
                ps.setString(1, sessionId);
                ps.setLong(2, now);
                ps.setString(3, login);
                ps.executeUpdate();
            }

            String sql = """
                SELECT
                    m.message_key, m.base_key, m.target_login, m.from_login, m.to_login,
                    m.time_ms, m.nonce, m.message_type, m.raw_block, m.created_at_ms,
                    m.source_api, m.origin_session_id, m.receipt_ref_base_key, m.receipt_ref_type
                FROM signed_messages_v2 m
                JOIN signed_message_session_delivery d
                  ON d.message_key = m.message_key
                WHERE d.session_id = ? AND d.delivered = 0
                ORDER BY m.time_ms ASC, m.created_at_ms ASC
                """;
            List<SignedMessageV2Entry> out = new ArrayList<>();
            try (PreparedStatement ps = c.prepareStatement(sql)) {
                ps.setString(1, sessionId);
                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next()) out.add(mapRow(rs));
                }
            }
            return out;
        }
    }

    private SignedMessageV2Entry mapRow(ResultSet rs) throws Exception {
        SignedMessageV2Entry e = new SignedMessageV2Entry();
        e.setMessageKey(rs.getString("message_key"));
        e.setBaseKey(rs.getString("base_key"));
        e.setTargetLogin(rs.getString("target_login"));
        e.setFromLogin(rs.getString("from_login"));
        e.setToLogin(rs.getString("to_login"));
        e.setTimeMs(rs.getLong("time_ms"));
        e.setNonce(rs.getLong("nonce"));
        e.setMessageType(rs.getInt("message_type"));
        e.setRawBlock(rs.getBytes("raw_block"));
        e.setCreatedAtMs(rs.getLong("created_at_ms"));
        e.setSourceApi(rs.getString("source_api"));
        e.setOriginSessionId(rs.getString("origin_session_id"));
        e.setReceiptRefBaseKey(rs.getString("receipt_ref_base_key"));
        int maybeRefType = rs.getInt("receipt_ref_type");
        e.setReceiptRefType(rs.wasNull() ? null : maybeRefType);
        return e;
    }
}
