/*
 * This file is part of LibEuFin.
 * Copyright (C) 2023-2024 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.bank.db

import tech.libeufin.bank.Operation
import tech.libeufin.bank.TanChannel
import tech.libeufin.common.db.*
import tech.libeufin.common.internalServerError
import tech.libeufin.common.micros
import java.time.Duration
import java.time.Instant
import java.util.concurrent.TimeUnit

/** Data access logic for tan challenged */
class TanDAO(private val db: Database) {
    /** Create new TAN challenge */
    suspend fun new(
        username: String, 
        op: Operation,
        body: String, 
        code: String,
        timestamp: Instant,
        retryCounter: Int,
        validityPeriod: Duration,
        channel: TanChannel? = null,
        info: String? = null
    ): Long = db.serializable(
        "SELECT tan_challenge_create(?,?::op_enum,?,?,?,?,?,?::tan_enum,?)"
    ) {
        setString(1, body)
        setString(2, op.name)
        setString(3, code)
        setLong(4, timestamp.micros())
        setLong(5, TimeUnit.MICROSECONDS.convert(validityPeriod))
        setInt(6, retryCounter)
        setString(7, username)
        setString(8, channel?.name)
        setString(9, info)
        oneOrNull {
            it.getLong(1)
        } ?: throw internalServerError("TAN challenge returned nothing.")
    }

    /** Result of TAN challenge transmission */
    sealed interface TanSendResult {
        data class Success(val tanInfo: String, val tanChannel: TanChannel, val tanCode: String?): TanSendResult
        data object NotFound: TanSendResult
        data object AuthRequired: TanSendResult
        data object TooMany: TanSendResult
    }

    /** Request TAN challenge transmission */
    suspend fun send(
        id: Long, 
        username: String, 
        code: String,
        timestamp: Instant,
        retryCounter: Int,
        validityPeriod: Duration,
        isAuth: Boolean,
        maxActive: Int
    ) = db.serializable(
        """
        SELECT out_no_op, out_tan_code, out_tan_channel, out_tan_info, out_auth_required, out_too_many
        FROM tan_challenge_send(?,?,?,?,?,?,?,?)
        """
    ) {
        setLong(1, id)
        setString(2, username)
        setString(3, code)
        setLong(4, timestamp.micros())
        setLong(5, TimeUnit.MICROSECONDS.convert(validityPeriod))
        setInt(6, retryCounter)
        setBoolean(7, isAuth)
        setInt(8, maxActive)
        one {
            when {
                it.getBoolean("out_no_op") -> TanSendResult.NotFound
                it.getBoolean("out_auth_required") -> TanSendResult.AuthRequired
                it.getBoolean("out_too_many") -> TanSendResult.TooMany
                else -> TanSendResult.Success(
                    tanInfo = it.getString("out_tan_info"),
                    tanChannel = it.getEnum("out_tan_channel"),
                    tanCode = it.getString("out_tan_code")
                )
            }
        }
    }

    /** Mark TAN challenge transmission */
    suspend fun markSent(
        id: Long,
        timestamp: Instant,
        retransmissionPeriod: Duration
    ) = db.serializable(
        "SELECT tan_challenge_mark_sent(?,?,?)"
    ) {
        setLong(1, id)
        setLong(2, timestamp.micros())
        setLong(3, TimeUnit.MICROSECONDS.convert(retransmissionPeriod))
        executeQuery()
    }

    /** Result of TAN challenge solution */
    sealed interface TanSolveResult {
        data class Success(val body: String, val op: Operation, val channel: TanChannel?, val info: String?): TanSolveResult
        data object NotFound: TanSolveResult
        data object NoRetry: TanSolveResult
        data object Expired: TanSolveResult
        data object BadCode: TanSolveResult
        data object AuthRequired: TanSolveResult
    }

    /** Solve TAN challenge */
    suspend fun solve(
        id: Long,
        username: String,
        code: String,
        timestamp: Instant,
        isAuth: Boolean,
    ) = db.serializable(
        """
        SELECT 
            out_ok, out_no_op, out_no_retry, out_expired,
            out_body, out_op, out_channel, out_info,
            out_auth_required
        FROM tan_challenge_try(?,?,?,?,?)
        """
    ) {
        setLong(1, id)
        setString(2, username)
        setString(3, code)
        setLong(4, timestamp.micros())
        setBoolean(5, isAuth)
        one {
            when {
                it.getBoolean("out_ok") -> TanSolveResult.Success(
                    body = it.getString("out_body"),
                    op = it.getEnum("out_op"),
                    channel = it.getOptEnum<TanChannel>("out_channel"),
                    info = it.getString("out_info")
                )
                it.getBoolean("out_auth_required") -> TanSolveResult.AuthRequired
                it.getBoolean("out_no_op") -> TanSolveResult.NotFound
                it.getBoolean("out_no_retry") -> TanSolveResult.NoRetry
                it.getBoolean("out_expired") -> TanSolveResult.Expired
                else -> TanSolveResult.BadCode
            }
        }
    }

    data class Challenge (
        val body: String,
        val channel: TanChannel?,
        val info: String?
    )

    /** Get a solved TAN challenge [id] for account [username] and [op] */
    suspend fun challenge(
        id: Long,
        username: String,
        op: Operation
    ) = db.serializable(
        """
        SELECT body, tan_challenges.tan_channel, tan_info
        FROM tan_challenges
            JOIN customers ON customer=customer_id
        WHERE challenge_id=? AND op=?::op_enum AND username=? AND deleted_at IS NULL
        """
    ) {
        setLong(1, id)
        setString(2, op.name)
        setString(3, username)
        oneOrNull { 
            Challenge(
                body = it.getString("body"),
                channel = it.getOptEnum<TanChannel>("tan_channel"),
                info = it.getString("tan_info")
            )
        }
    }
}