/*
 * 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.common

import io.ktor.http.*
import kotlin.math.min
import java.util.*

fun Parameters.expect(name: String): String 
    = get(name) ?: throw badRequest("Missing '$name' parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING)
fun Parameters.int(name: String): Int? 
    = get(name)?.run { toIntOrNull() ?: throw paramsMalformed("Param '$name' not a number") }
fun Parameters.expectInt(name: String): Int 
    = int(name) ?: throw badRequest("Missing '$name' number parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING)
fun Parameters.long(name: String): Long? 
    = get(name)?.run { toLongOrNull() ?: throw paramsMalformed("Param '$name' not a number") }
fun Parameters.expectLong(name: String): Long 
    = long(name) ?: throw badRequest("Missing '$name' number parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING)
fun Parameters.uuid(name: String): UUID?
    = get(name)?.run {
        try {
            UUID.fromString(this)
        } catch (e: Exception) {
            throw paramsMalformed("Param '$name' not an UUID")
        }
    } 
fun Parameters.expectUuid(name: String): UUID 
    = uuid(name) ?: throw badRequest("Missing '$name' UUID parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING)
fun Parameters.payto(name: String): Payto?
    = get(name)?.run {
        try {
            Payto.parse(this)
        } catch (e: Exception) {
            throw paramsMalformed("Param '$name' not a valid payto")
        }
    } 
fun Parameters.expectPayto(name: String): Payto 
    = payto(name) ?: throw badRequest("Missing '$name' payto parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING)
fun Parameters.amount(name: String): TalerAmount? 
    = get(name)?.run { 
        try {
            TalerAmount(this)
        } catch (e: Exception) {
            throw paramsMalformed("Param '$name' not a taler amount")
        }
    }

data class PageParams(
    val limit: Int, val offset: Long
) {
    companion object {
        fun extract(params: Parameters): PageParams {
            val legacy_limit_value = params.int("delta")
            val new_limit_value = params.int("limit")
            if (legacy_limit_value != null && new_limit_value != null && legacy_limit_value != new_limit_value)
                throw paramsMalformed("Param 'limit' cannot be used with param 'delta'")

            val legacy_offset_value = params.long("start")
            val new_offset_value = params.long("offset")
            if (legacy_offset_value != null && new_offset_value != null && legacy_offset_value != new_offset_value)
                throw paramsMalformed("Param 'offset' cannot be used with param 'start'")
            
            val limit: Int = new_limit_value ?: legacy_limit_value ?: -20
            if (limit == 0) throw paramsMalformed("Param 'limit' must be non-zero")
            else if (limit > MAX_PAGE_SIZE) throw paramsMalformed("Param 'limit' must be <= ${MAX_PAGE_SIZE}")
            // The sign of 'limit' only selects the direction, so the negative
            // side needs the same bound.  Note that abs(Int.MIN_VALUE) silently
            // returns Int.MIN_VALUE, so an unbounded negative limit would reach
            // SQL as a negative LIMIT.
            else if (limit < -MAX_PAGE_SIZE) throw paramsMalformed("Param 'limit' must be >= ${-MAX_PAGE_SIZE}")
            val offset: Long = new_offset_value ?: legacy_offset_value ?: if (limit >= 0) 0L else Long.MAX_VALUE
            if (offset < 0) throw paramsMalformed("Param 'offset' must be a positive number")

            return PageParams(limit, offset)
        }
    }
}

data class TransferParams(
    val page: PageParams, val status: TransferStatusState?
) {
    companion object {
        private val names = TransferStatusState.entries.map { it.name }
        private val names_fmt = names.joinToString()
        fun extract(params: Parameters): TransferParams {
            val status = params["status"]?.let {
                if (!names.contains(it)) {
                    throw paramsMalformed("Param 'status' must be one of $names_fmt")
                }
                TransferStatusState.valueOf(it)
            }
            return TransferParams(PageParams.extract(params), status)
        }
    }
}

data class PollingParams(
    val timeout_ms: Long
) {
    companion object {
        fun extract(params: Parameters): PollingParams {
            val legacy_value = params.long("long_poll_ms")
            val new_value = params.long("timeout_ms")
            if (legacy_value != null && new_value != null && legacy_value != new_value)
                throw paramsMalformed("Param 'timeout_ms' cannot be used with param 'long_poll_ms'")
            val timeout_ms: Long = min(new_value ?: legacy_value ?: 0, MAX_TIMEOUT_MS)
            if (timeout_ms < 0) throw paramsMalformed("Param 'timeout_ms' must be a positive number")
            return PollingParams(timeout_ms)
        }
    }
}

data class HistoryParams(
    val page: PageParams, val polling: PollingParams
) {
    companion object {
        fun extract(params: Parameters): HistoryParams {
            return HistoryParams(PageParams.extract(params), PollingParams.extract(params))
        }
    }
}

data class AccountCheckParams(
    val account: Payto
) {
    companion object {
        fun extract(params: Parameters): AccountCheckParams {
            val account = params.expectPayto("account")
            return AccountCheckParams(account)
        }
    }
}