/*
 * 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.server.application.*
import org.slf4j.Logger
import java.io.ByteArrayOutputStream
import java.io.FilterInputStream
import java.io.InputStream
import java.math.BigInteger
import java.security.SecureRandom
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.zip.DeflaterInputStream
import java.util.zip.InflaterInputStream
import java.util.zip.ZipInputStream
import kotlin.random.Random

/* ----- String ----- */

/** Decode a base64 encoded string */
fun String.decodeBase64(): ByteArray = Base64.getDecoder().decode(this)
/** Encode a string as base64 */
fun String.encodeBase64(): String = toByteArray().encodeBase64()
/** Decode a hexadecimal uppercase encoded string */
fun String.decodeUpHex(): ByteArray = HexFormat.of().withUpperCase().parseHex(this)

fun String.splitOnce(pat: String): Pair<String, String>? {
    val split = splitToSequence(pat, limit=2).iterator()
    val first = split.next()
    if (!split.hasNext()) return null
    return Pair(first, split.next())
}

/** Format a string with a space every two characters */
fun String.fmtChunkByTwo() = buildString {
    this@fmtChunkByTwo.forEachIndexed { pos, c ->
        if (pos != 0 && pos % 2 == 0) append(' ')
        append(c)
    }
}

/* ----- Date & Time ----- */

/** Converting YYYY-MM-DD to Instant */
fun dateToInstant(date: String): Instant = 
    LocalDate.parse(date, DateTimeFormatter.ISO_DATE).atStartOfDay().toInstant(ZoneOffset.UTC)

/** Converting YYYY-MM-DDTHH:MM:SS to Instant */
fun dateTimeToInstant(date: String): Instant = 
    LocalDateTime.parse(date, DateTimeFormatter.ISO_DATE_TIME).toInstant(ZoneOffset.UTC)

private val DATE_TIME_PATH = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HHmmss")

/** Converting Instant to YYYY-MM-DDTHHMMSS */
fun Instant.toDateTimeFilePath(): String =
    this.atOffset(ZoneOffset.UTC).format(DATE_TIME_PATH)

/* ----- BigInteger -----*/

fun BigInteger.encodeHex(): String = this.toByteArray().encodeHex()
fun BigInteger.encodeBase64(): String = this.toByteArray().encodeBase64()

/* ----- Random ----- */

/** Thread local cryptographically strong random number generator */
val SECURE_RNG = ThreadLocal.withInitial { SecureRandom() }

/* ----- ByteArray ----- */

fun ByteArray.rand(rng: Random = Random): ByteArray {
    rng.nextBytes(this)
    return this
}
fun ByteArray.secureRand(): ByteArray {
    SECURE_RNG.get().nextBytes(this)
    return this
}
fun ByteArray.encodeHex(): String = HexFormat.of().formatHex(this)
fun ByteArray.encodeUpHex(): String = HexFormat.of().withUpperCase().formatHex(this)
fun ByteArray.encodeBase64(): String = Base64.getEncoder().encodeToString(this)
fun ByteArray.asUtf8(): String = this.toString(Charsets.UTF_8)
fun ByteArrayOutputStream.asUtf8(): String = this.toString(Charsets.UTF_8)

/* ----- InputStream ----- */

/** Unzip an input stream and run [lambda] over each entry */
inline fun InputStream.unzipEach(lambda: (String, InputStream) -> Unit) {
    ZipInputStream(this).use { zip ->
        while (true) {
            val entry = zip.getNextEntry() ?: break
            val entryStream = object: FilterInputStream(zip) {
                override fun close() {
                    zip.closeEntry()
                }
            }
            lambda(entry.name, entryStream)
        }
    }
}

/** Decode a base64 encoded input stream */
fun InputStream.decodeBase64(): InputStream 
    = Base64.getDecoder().wrap(this)

/** Encode an input stream as base64 */
fun InputStream.encodeBase64(): String {
    val w = ByteArrayOutputStream()
    val encoded = Base64.getEncoder().wrap(w)
    transferTo(encoded)
    encoded.close()
    return w.asUtf8()
}

/** Deflate an input stream */
fun InputStream.deflate(): DeflaterInputStream 
    = DeflaterInputStream(this)

/** Inflate an input stream */
fun InputStream.inflate(): InflaterInputStream 
    = InflaterInputStream(this)

/** Read an input stream as UTF8 text */
fun InputStream.readText(): String 
    = this.reader().readText()

/* ----- Throwable ----- */

fun Throwable.fmt(): String = buildString {
    append(message ?: this@fmt::class.simpleName)
    var cause = cause
    while (cause != null) {
        append(": ")
        append(cause.message ?: cause::class.simpleName)
        cause = cause.cause
    }
}

fun Throwable.fmtLog(logger: Logger) {
    logger.error(this.fmt())
    logger.trace("", this)
}

/* ----- Logger ----- */

inline fun Logger.debug(lambda: () -> String) {
    if (isDebugEnabled) debug(lambda())
}

inline fun Logger.trace(lambda: () -> String) {
    if (isTraceEnabled) trace(lambda())
}

/* ----- KTOR ----- */

fun ApplicationCall.uuidPath(name: String): UUID {
    val value = parameters[name]!!
    try {
        return UUID.fromString(value)
    } catch (e: Exception) {
        throw badRequest("UUID uri component malformed: ${e.message}", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) // TODO better error ?
    }
}

fun ApplicationCall.longPath(name: String): Long {
    val value = parameters[name]!!
    try {
        return value.toLong()
    } catch (e: Exception) {
        throw badRequest("Long uri component malformed: ${e.message}", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) // TODO better error ?
    }
}

/* ----- Payto ----- */

fun ibanPayto(iban: String, name: String? = null): IbanPayto {
    return Payto.parse(IbanPayto.build(iban, null, name)).expectIban()
}