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

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.parameters.arguments.*
import com.github.ajalt.clikt.parameters.groups.provideDelegate 
import com.github.ajalt.clikt.parameters.options.*
import com.github.ajalt.clikt.parameters.types.*
import tech.libeufin.bank.*
import tech.libeufin.bank.auth.*
import tech.libeufin.common.*
import java.time.*
import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit

class CreateToken : TalerCmd("create-token") {
    override fun help(context: Context) = "Create authentication token for a user"

    private val username by option(
        "-u", "--user", "--username",
        metavar = "<username>",
        help = "Account username"
    ).required()
    private val scope by option("--scope", "-s", help = "Scope for the token").enum<TokenScope>().required()
    private val duration by option("--duration", "-d", metavar = "<forever|micros>", help = "Custom token validity duration").convert {
        if (it == "forever") {
            ChronoUnit.FOREVER.duration
        } else {
            val dUs = it.toLongOrNull() ?: throw Exception("Expected forever or a number in micros")
            when {
                dUs < 0 -> throw Exception("Negative duration specified")
                dUs > RelativeTime.MAX_SAFE_INTEGER -> throw Exception("Duration value exceed cap (2^53-1)")
                else -> Duration.of(dUs, ChronoUnit.MICROS)
            } 
        }
    }.default(TOKEN_DEFAULT_DURATION)
    private val description by option("--description", help = "Optional token description")
    private val refreshable by option("--refreshable", help = "Make the token refreshable into a new token").flag()
    private val currentToken by option("--current-token", help = "Current token to reuse if still valid").convert {
        Base32Crockford.decode(it.removePrefix(TOKEN_PREFIX))
    }

    override fun run() = cliCmd(logger) {
        bankConfig(config).withDb { db, cfg ->
            val now = Instant.now()

            val token = currentToken?.let { db.token.access(it, now) }

            if (token != null && token.expirationTime.isBefore(now) && validScope(scope.logical(), token.scope)) {
                println("$TOKEN_PREFIX$currentToken")
            } else {
                val expirationTimestamp = 
                    if (duration == ChronoUnit.FOREVER.duration) {
                        Instant.MAX
                    } else {
                        try {
                            now.plus(duration)
                        } catch (e: Exception) {
                            throw Exception("Bad token duration: ${e.message}")
                        }
                    }
                val token = Base32Crockford32B.secureRand()
                db.token.create(
                    username = username,
                    content = token.raw,
                    creationTime = now,
                    expirationTime = expirationTimestamp,
                    scope = scope,
                    isRefreshable = refreshable,
                    description = description,
                    is2fa = true
                )
                println("$TOKEN_PREFIX$token")
            }
        }
    }
}