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

import tech.libeufin.bank.*
import tech.libeufin.common.PageParams
import tech.libeufin.common.asInstant
import tech.libeufin.common.db.*
import tech.libeufin.common.micros
import java.time.Instant

/** Data access logic for auth tokens */
class TokenDAO(private val db: Database) {
    /** Result status of token creation */
    sealed interface TokenCreationResult {
        data object Success: TokenCreationResult
        data object TanRequired: TokenCreationResult
    }

    /** Create new token for [username] */
    suspend fun create(
        username: String,
        content: ByteArray,
        creationTime: Instant,
        expirationTime: Instant,
        scope: TokenScope,
        isRefreshable: Boolean,
        description: String?,
        is2fa: Boolean
    ): TokenCreationResult = db.serializable(
        """
        SELECT out_tan_required FROM create_token(
            ?,?,?,?,?::token_scope_enum,?,?,?
        )
        """
    ) {
        bind(username)
        bind(content)
        bind(creationTime)
        bind(expirationTime)
        bind(scope)
        bind(isRefreshable)
        bind(description)
        bind(is2fa)
        one {
            when {
                it.getBoolean("out_tan_required") -> TokenCreationResult.TanRequired
                else -> TokenCreationResult.Success
            }
        }
    }
    
    /** Get info for [token] */
    suspend fun access(token: ByteArray, accessTime: Instant): BearerToken? = db.serializable(
        """
        UPDATE bearer_tokens
            SET last_access=?
        FROM customers
        WHERE bank_customer=customer_id AND content=? AND deleted_at IS NULL
        RETURNING
            creation_time,
            expiration_time,
            scope,
            is_refreshable
        """
    ) {
        bind(accessTime)
        bind(token) 
        oneOrNull {
            BearerToken(
                creationTime = it.getLong("creation_time").asInstant(),
                expirationTime = it.getLong("expiration_time").asInstant(),
                scope = it.getEnum("scope"),
                isRefreshable = it.getBoolean("is_refreshable")
            )
        }
    }

    /** Get info for [token] and its associated bank account*/
    suspend fun accessInfo(token: ByteArray, accessTime: Instant): Pair<BearerToken, BankInfo>? = db.serializable(
        """
        UPDATE bearer_tokens
            SET last_access=?
        FROM customers
            JOIN bank_accounts ON customer_id=owning_customer_id
        WHERE bank_customer=customer_id AND content=? AND deleted_at IS NULL
        RETURNING
            creation_time,
            expiration_time,
            scope,
            is_refreshable,
            username,
            is_taler_exchange,
            bank_account_id,
            internal_payto,
            name,
            tan_channels,
            email,
            phone
        """
    ) {
        bind(accessTime)
        bind(token) 
        oneOrNull {
            Pair(
                BearerToken(
                    creationTime = it.getLong("creation_time").asInstant(),
                    expirationTime = it.getLong("expiration_time").asInstant(),
                    scope = it.getEnum("scope"),
                    isRefreshable = it.getBoolean("is_refreshable")
                ),
                BankInfo(
                    username = it.getString("username"),
                    payto = it.getBankPayto("internal_payto", "name", db.ctx),
                    bankAccountId = it.getLong("bank_account_id"),
                    isTalerExchange = it.getBoolean("is_taler_exchange"),
                    channels = it.getEnumSet<TanChannel>("tan_channels"),
                    phone = it.getString("phone"),
                    email = it.getString("email")
                )
            )
        }
    }
    
    /** Delete token [token] */
    suspend fun delete(token: ByteArray) = db.serializable(
        "DELETE FROM bearer_tokens WHERE content = ?"
    ) {
        bind(token)
        executeUpdate()
    }

    /** Delete token [id] */
    suspend fun deleteById(id: Long) = db.serializable(
        "DELETE FROM bearer_tokens WHERE bearer_token_id = ?"
    ) {
        bind(id)
        executeUpdateCheck()
    }

    /** Get a page of all tokens of [username] accounts */
    suspend fun page(params: PageParams, username: String, timestamp: Instant): List<TokenInfo>
        = db.page(
            params,
            "bearer_token_id",
            """
            SELECT
              creation_time,
              expiration_time,
              scope,
              is_refreshable,
              description,
              last_access,
              bearer_token_id
              FROM bearer_tokens 
              WHERE 
                expiration_time > ? AND
                bank_customer=(SELECT customer_id FROM customers WHERE deleted_at IS NULL AND username = ?) 
            AND
            """,
            {
                bind(timestamp.micros())
                bind(username)
            }
        ) {
            TokenInfo(
                creation_time = it.getTalerTimestamp("creation_time"),
                expiration = it.getTalerTimestamp("expiration_time"),
                scope = it.getEnum("scope"),
                isRefreshable = it.getBoolean("is_refreshable"),
                description = it.getString("description"),
                last_access = it.getTalerTimestamp("last_access"),
                row_id = it.getLong("bearer_token_id"),
                token_id = it.getLong("bearer_token_id")
            )
        }
}