package shine.db.dao;

import shine.db.SqliteDbController;
import shine.db.entities.EspPairingRequestEntry;

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 EspPairingRequestsDAO {

    private static volatile EspPairingRequestsDAO instance;
    private final SqliteDbController db = SqliteDbController.getInstance();

    private EspPairingRequestsDAO() { }

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

    public void insert(EspPairingRequestEntry entry) throws SQLException {
        try (Connection c = db.getConnection()) {
            insert(c, entry);
        }
    }

    public void insert(Connection c, EspPairingRequestEntry entry) throws SQLException {
        String sql = """
            INSERT INTO esp_pairing_requests (
                pairing_id,
                login,
                requester_session_key,
                requester_session_type,
                requester_client_platform,
                payload_type,
                status,
                short_code,
                fingerprint_b58,
                encrypted_payload,
                reject_reason,
                approved_by_session_id,
                created_at_ms,
                expires_at_ms,
                updated_at_ms,
                delivered_to_homeserver
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """;
        try (PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setString(1, entry.getPairingId());
            ps.setString(2, entry.getLogin());
            ps.setString(3, entry.getRequesterSessionKey());
            ps.setInt(4, entry.getRequesterSessionType());
            ps.setString(5, entry.getRequesterClientPlatform());
            ps.setInt(6, entry.getPayloadType());
            ps.setString(7, entry.getStatus());
            ps.setString(8, entry.getShortCode());
            ps.setString(9, entry.getFingerprintB58());
            ps.setString(10, entry.getEncryptedPayload());
            ps.setString(11, entry.getRejectReason());
            ps.setString(12, entry.getApprovedBySessionId());
            ps.setLong(13, entry.getCreatedAtMs());
            ps.setLong(14, entry.getExpiresAtMs());
            ps.setLong(15, entry.getUpdatedAtMs());
            ps.setInt(16, entry.isDeliveredToHomeserver() ? 1 : 0);
            ps.executeUpdate();
        }
    }

    public EspPairingRequestEntry getByPairingId(String pairingId) throws SQLException {
        try (Connection c = db.getConnection()) {
            return getByPairingId(c, pairingId);
        }
    }

    public EspPairingRequestEntry getByPairingId(Connection c, String pairingId) throws SQLException {
        String sql = """
            SELECT pairing_id, login, requester_session_key, requester_session_type, requester_client_platform,
                   payload_type, status, short_code, fingerprint_b58, encrypted_payload, reject_reason,
                   approved_by_session_id, created_at_ms, expires_at_ms, updated_at_ms, delivered_to_homeserver
            FROM esp_pairing_requests
            WHERE pairing_id = ?
            LIMIT 1
            """;
        try (PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setString(1, pairingId);
            try (ResultSet rs = ps.executeQuery()) {
                if (!rs.next()) return null;
                return mapRow(rs);
            }
        }
    }

    public List<EspPairingRequestEntry> listActiveByLogin(String login, long nowMs) throws SQLException {
        try (Connection c = db.getConnection()) {
            return listActiveByLogin(c, login, nowMs);
        }
    }

    public List<EspPairingRequestEntry> listActiveByLogin(Connection c, String login, long nowMs) throws SQLException {
        String sql = """
            SELECT pairing_id, login, requester_session_key, requester_session_type, requester_client_platform,
                   payload_type, status, short_code, fingerprint_b58, encrypted_payload, reject_reason,
                   approved_by_session_id, created_at_ms, expires_at_ms, updated_at_ms, delivered_to_homeserver
            FROM esp_pairing_requests
            WHERE login = ? COLLATE NOCASE
              AND expires_at_ms > ?
              AND status IN ('created', 'approved', 'rejected')
            ORDER BY created_at_ms DESC
            """;
        List<EspPairingRequestEntry> list = new ArrayList<>();
        try (PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setString(1, login);
            ps.setLong(2, nowMs);
            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) list.add(mapRow(rs));
            }
        }
        return list;
    }

    public int countRecentByLoginAndStatuses(String login, long sinceMs, String... statuses) throws SQLException {
        try (Connection c = db.getConnection()) {
            StringBuilder sql = new StringBuilder("""
                SELECT COUNT(*)
                FROM esp_pairing_requests
                WHERE login = ? COLLATE NOCASE
                  AND created_at_ms >= ?
                  AND status IN (
                """);
            for (int i = 0; i < statuses.length; i++) {
                if (i > 0) sql.append(", ");
                sql.append("?");
            }
            sql.append(")");
            try (PreparedStatement ps = c.prepareStatement(sql.toString())) {
                ps.setString(1, login);
                ps.setLong(2, sinceMs);
                for (int i = 0; i < statuses.length; i++) {
                    ps.setString(3 + i, statuses[i]);
                }
                try (ResultSet rs = ps.executeQuery()) {
                    return rs.next() ? rs.getInt(1) : 0;
                }
            }
        }
    }

    public void updateDeliveryFlag(String pairingId, boolean delivered, long updatedAtMs) throws SQLException {
        updateSimple(pairingId, """
            UPDATE esp_pairing_requests
            SET delivered_to_homeserver = ?, updated_at_ms = ?
            WHERE pairing_id = ?
            """, ps -> {
            ps.setInt(1, delivered ? 1 : 0);
            ps.setLong(2, updatedAtMs);
            ps.setString(3, pairingId);
        });
    }

    public void markApproved(String pairingId, String encryptedPayload, String approvedBySessionId, long updatedAtMs) throws SQLException {
        updateSimple(pairingId, """
            UPDATE esp_pairing_requests
            SET status = 'approved',
                encrypted_payload = ?,
                approved_by_session_id = ?,
                reject_reason = NULL,
                updated_at_ms = ?
            WHERE pairing_id = ?
            """, ps -> {
            ps.setString(1, encryptedPayload);
            ps.setString(2, approvedBySessionId);
            ps.setLong(3, updatedAtMs);
            ps.setString(4, pairingId);
        });
    }

    public void markRejected(String pairingId, String rejectReason, String approvedBySessionId, long updatedAtMs) throws SQLException {
        updateSimple(pairingId, """
            UPDATE esp_pairing_requests
            SET status = 'rejected',
                reject_reason = ?,
                approved_by_session_id = ?,
                encrypted_payload = NULL,
                updated_at_ms = ?
            WHERE pairing_id = ?
            """, ps -> {
            ps.setString(1, rejectReason);
            ps.setString(2, approvedBySessionId);
            ps.setLong(3, updatedAtMs);
            ps.setString(4, pairingId);
        });
    }

    public int expirePending(long nowMs) throws SQLException {
        try (Connection c = db.getConnection();
             PreparedStatement ps = c.prepareStatement("""
                 UPDATE esp_pairing_requests
                 SET status = 'expired',
                     updated_at_ms = ?
                 WHERE status = 'created'
                   AND expires_at_ms <= ?
                 """)) {
            ps.setLong(1, nowMs);
            ps.setLong(2, nowMs);
            return ps.executeUpdate();
        }
    }

    private interface PreparedStatementSetter {
        void accept(PreparedStatement ps) throws SQLException;
    }

    private void updateSimple(String pairingId, String sql, PreparedStatementSetter setter) throws SQLException {
        try (Connection c = db.getConnection();
             PreparedStatement ps = c.prepareStatement(sql)) {
            setter.accept(ps);
            ps.executeUpdate();
        }
    }

    private static EspPairingRequestEntry mapRow(ResultSet rs) throws SQLException {
        EspPairingRequestEntry entry = new EspPairingRequestEntry();
        entry.setPairingId(rs.getString("pairing_id"));
        entry.setLogin(rs.getString("login"));
        entry.setRequesterSessionKey(rs.getString("requester_session_key"));
        entry.setRequesterSessionType(rs.getInt("requester_session_type"));
        entry.setRequesterClientPlatform(rs.getString("requester_client_platform"));
        entry.setPayloadType(rs.getInt("payload_type"));
        entry.setStatus(rs.getString("status"));
        entry.setShortCode(rs.getString("short_code"));
        entry.setFingerprintB58(rs.getString("fingerprint_b58"));
        entry.setEncryptedPayload(rs.getString("encrypted_payload"));
        entry.setRejectReason(rs.getString("reject_reason"));
        entry.setApprovedBySessionId(rs.getString("approved_by_session_id"));
        entry.setCreatedAtMs(rs.getLong("created_at_ms"));
        entry.setExpiresAtMs(rs.getLong("expires_at_ms"));
        entry.setUpdatedAtMs(rs.getLong("updated_at_ms"));
        entry.setDeliveredToHomeserver(rs.getInt("delivered_to_homeserver") != 0);
        return entry;
    }
}
