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

import tech.libeufin.common.*
import tech.libeufin.ebics.EbicsOrder
import java.io.*
import java.nio.file.*
import java.time.*
import java.time.format.DateTimeFormatter
import kotlin.io.*
import kotlin.io.path.*
import io.ktor.client.statement.*

/** Log EBICS transactions steps and payload if [path] is not null */
class EbicsLogger(private val dir: Path?) {

    init {
        if (dir != null) {
            try {
                // Create logging directory if missing
                dir.createDirectories()
            } catch (e: Exception) {
                throw Exception("Failed to init EBICS debug logging directory", e)
            }
            logger.info("Logging to '$dir'")
        }
    }

    /** Create a new [name] EBICS transaction logger */
    fun tx(name: String): TxLogger {
        if (dir == null) return TxLogger(null)
        val utcDateTime = Instant.now().atOffset(ZoneOffset.UTC)
        val txDir = dir
            // yyyy-MM-dd per day directory
            .resolve(utcDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE))
            // HH:mm:ss.SSS-name per transaction directory
            .resolve("${utcDateTime.format(TIME_WITH_MS)}-$name")
        txDir.createDirectories()
        return TxLogger(txDir)
    }

    /** Create a new [order] EBICS transaction logger */
    fun tx(order: EbicsOrder): TxLogger {
        if (dir == null) return TxLogger(null)
        return tx(order.description())
    }

    companion object {
        private val TIME_WITH_MS = DateTimeFormatter.ofPattern("HH:mm:ss.SSS")
    }
}

/** Log EBICS transaction steps and payload */
class TxLogger internal constructor(
    private val dir: Path?
) {
    /** Create a new [name] EBICS transaction step logger*/
    fun step(name: String? = null) = StepLogger(dir, name)

    /** Log a [stream] EBICS transaction payload of [type] */
    fun payload(stream: InputStream, type: String = "xml"): InputStream {
        if (dir == null) return stream
        return payload(stream.readBytes(), type).inputStream()
    }

    /** Log a [content] EBICS transaction payload of [type] */
    fun payload(content: ByteArray, type: String = "xml"): ByteArray {
        if (dir == null) return content
        val type = type.lowercase()
        if (type == "zip") {
            val payloadDir = dir.resolve("payload")
            payloadDir.createDirectory()
            content.inputStream().unzipEach { fileName, xmlContent ->
                xmlContent.use {
                    Files.copy(it, payloadDir.resolve(fileName))
                }
            }
        } else {
            dir.resolve("payload.$type").writeBytes(content, StandardOpenOption.CREATE_NEW)
        }
        return content
    }
}

/** Log EBICS transaction protocol step */
class StepLogger internal constructor(
    private val dir: Path?,
    name: String?
) {
    private val prefix = if (name != null) "$name-" else ""

    /** Log a protocol step [request] */
    fun logRequest(request: ByteArray) {
        dir?.resolve("${prefix}request.xml")?.writeBytes(request, StandardOpenOption.CREATE_NEW)
    }

    /** Log a protocol step failure */
    suspend fun logFailure(res: HttpResponse) {
        if (dir != null) {
            // TODO reduce allocation
            // TODO silent io error
            val bytes = buildString {
                append("${res.version} ${res.status}\n")
                for ((k, vs) in res.headers.entries()) {
                    for (v in vs) {
                        append("${k}: ${v}\n")
                    }
                }
                append('\n')
            }.toByteArray() + res.readRawBytes()
            dir.resolve("${prefix}failure")
               .writeBytes(bytes, StandardOpenOption.CREATE_NEW)
        }
    }

    /** Log a protocol step [response] */
    fun logResponse(response: InputStream): InputStream {
        if (dir == null) return response
        val bytes = response.readBytes()
        dir.resolve("${prefix}response.xml")
           .writeBytes(bytes, StandardOpenOption.CREATE_NEW)
        return bytes.inputStream()
    }
}