/*
 * This file is part of LibEuFin.
 * Copyright (C) 2025 Taler Systems S.A.
 *
 * LibEuFin is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation; either version 3, or
 * (at your option) any later version.
 *
 * LibEuFin is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
 * Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with LibEuFin; see the file COPYING.  If not, see
 * <http://www.gnu.org/licenses/>
 */

package tech.libeufin.common.db

import org.postgresql.jdbc.PgConnection
import org.postgresql.util.PSQLState
import tech.libeufin.common.SERIALIZATION_RETRY
import java.sql.PreparedStatement
import java.sql.ResultSet
import java.sql.SQLException

val SERIALIZATION_ERROR = setOf(
    "40001", // serialization_failure
    "40P01", // deadlock_detected
    "55P03", // lock_not_available
)

/** Executes db logic with automatic retry on serialization errors */
suspend fun <R> retrySerializationError(lambda: suspend () -> R): R {
    repeat(SERIALIZATION_RETRY) {
        try {
            return lambda()
        } catch (e: SQLException) {
            if (!SERIALIZATION_ERROR.contains(e.sqlState)) throw e
        }
    }
    return lambda()
}

fun PgConnection.talerStatement(query: String): TalerStatement = TalerStatement(prepareStatement(query))

/** Run a postgres query using a prepared statement */
inline fun <R> PgConnection.withStatement(query: String, lambda: TalerStatement.() -> R): R =
    talerStatement(query).use { it.lambda() }

/** Run a postgres [transaction] */
fun <R> PgConnection.transaction(transaction: (PgConnection) -> R): R {
    try {
        autoCommit = false
        val result = transaction(this)
        commit()
        autoCommit = true
        return result
    } catch (e: Exception) {
        rollback()
        autoCommit = true
        throw e
    }
}

/** 
 * Execute an update of [table] with a dynamic query generated at runtime.
 * Every [fields] in each row matching [filter] are updated using values from [bind].
 **/
fun PgConnection.dynamicUpdate(
    table: String,
    fields: Sequence<String>,
    filter: String,
    bind: TalerStatement.() -> Unit,
) {
    val sql = fields.joinToString()
    if (sql.isEmpty()) return
    withStatement("UPDATE $table SET $sql $filter") {
        bind()
        executeUpdate()
    }
}