/*
 * 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 org.slf4j.Logger
import org.slf4j.LoggerFactory
import tech.libeufin.common.SERIALIZATION_RETRY
import java.sql.PreparedStatement
import java.sql.ResultSet
import java.sql.SQLException

internal val logger: Logger = LoggerFactory.getLogger("libeufin-db")

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

/** Run a postgres query using a prepared statement */
inline fun <R> PgConnection.withStatement(query: String, lambda: PreparedStatement.() -> R): R {
    val stmt = prepareStatement(query)
    return stmt.use {
        val res = stmt.lambda()
        // Log warnings
        var warning = stmt.getWarnings()
        while (warning != null) {
            logger.warning(warning.message)
            warning = warning.getNextWarning()
        }
        stmt.clearWarnings()
        res
    }
}

/** 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
    }
}

/** Read one row or null if none */
fun <T> PreparedStatement.oneOrNull(lambda: (ResultSet) -> T): T? {
    executeQuery().use {
        return if (it.next()) lambda(it) else null
    }
}

/** Read one row or throw if none */
fun <T> PreparedStatement.one(lambda: (ResultSet) -> T): T =
    requireNotNull(oneOrNull(lambda)) { "Missing result to database query" }

/** Read one row or throw [err] in case or unique violation error */
fun <T> PreparedStatement.oneUniqueViolation(err: T, lambda: (ResultSet) -> T): T {
    return try {
        one(lambda)
    } catch (e: SQLException) {
        if (e.sqlState == PSQLState.UNIQUE_VIOLATION.state) return err
        throw e // rethrowing, not to hide other types of errors.
    }
}

/** Read all rows */
fun <T> PreparedStatement.all(lambda: (ResultSet) -> T): List<T> {
    executeQuery().use {
        val ret = mutableListOf<T>()
        while (it.next()) {
            ret.add(lambda(it))
        }
        return ret
    }
}

/** Execute a query checking it return a least one row  */
fun PreparedStatement.executeQueryCheck(): Boolean {
    executeQuery().use {
        return it.next()
    }
}

/** Execute an update checking it update at least one row */
fun PreparedStatement.executeUpdateCheck(): Boolean {
    executeUpdate()
    return updateCount > 0
}

/** Execute an update checking if fail because of unique violation error */
fun PreparedStatement.executeUpdateViolation(): Boolean {
    return try {
        executeUpdateCheck()
    } catch (e: SQLException) {
        logger.debug(e.message)
        if (e.sqlState == PSQLState.UNIQUE_VIOLATION.state) return false
        throw e // rethrowing, not to hide other types of errors.
    }
}

/** Execute an update checking if fail because of unique violation error and resetting state */
fun PreparedStatement.executeProcedureViolation(): Boolean {
    val savepoint = connection.setSavepoint()
    return try {
        executeUpdate()
        connection.releaseSavepoint(savepoint)
        true
    } catch (e: SQLException) {
        connection.rollback(savepoint)
        if (e.sqlState == PSQLState.UNIQUE_VIOLATION.state) return false
        throw e // rethrowing, not to hide other types of errors.
    }
}

/** 
 * 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: Sequence<Any?>,
) {
    val sql = fields.joinToString()
    if (sql.isEmpty()) return
    withStatement("UPDATE $table SET $sql $filter") {
        for ((idx, value) in bind.withIndex()) {
            setObject(idx + 1, value)
        }
        executeUpdate()
    }
}