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

import io.ktor.client.HttpClient
import tech.libeufin.common.crypto.CryptoUtil
import tech.libeufin.common.encodeUpHex
import tech.libeufin.common.fmtChunkByTwo
import tech.libeufin.ebics.EbicsKeyMng.Order.*
import java.time.Instant
import kotlin.io.path.Path
import kotlin.io.path.writeBytes
import java.nio.file.FileAlreadyExistsException
import java.nio.file.Path
import java.nio.file.StandardOpenOption

/** Load client private keys at [path] or create new ones if missing */
private fun loadOrGenerateClientKeys(path: Path): ClientPrivateKeysFile {
    // If exists load from disk
    val current = loadClientKeys(path)
    if (current != null) return current
    // Else create new keys
    val newKeys = generateNewKeys()
    persistClientKeys(newKeys, path)
    logger.info("New client private keys created at '$path'")
    return newKeys
}

/**
 * Asks the user to accept the bank public keys.
 *
 * @param bankKeys bank public keys, in format stored on disk.
 * @return true if the user accepted, false otherwise.
 */
fun askUserToAcceptKeys(bankKeys: BankPublicKeysFile, cfg: EbicsSetupConfig): Boolean {
    val encHash = CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_encryption_public_key)
    val authHash = CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_authentication_public_key)
    val authPubKey = cfg.bankAuthPubKey 
    val encPubKey = cfg.bankEncPubKey 
    if (authPubKey != null && encPubKey != null) {
        if (encHash.contentEquals(encPubKey) && authHash.contentEquals(authPubKey)) {
            logger.info("Accepting bank keys matching config hashes")
            return true
        }
        throw Exception(buildString {
            append("Bank keys does not match config hashes\nBank encryption key: ")
            append(encHash.encodeUpHex().fmtChunkByTwo())
            append("\nConfig encryption key: ")
            append(encPubKey.encodeUpHex().fmtChunkByTwo())
            append("\nBank authentication key: ")
            append(authHash.encodeUpHex().fmtChunkByTwo())
            append("\nConfig authentication key: ")
            append(authPubKey.encodeUpHex().fmtChunkByTwo())
        })
    }
    println("The bank has the following keys:")
    println("Encryption key: ${encHash.encodeUpHex().fmtChunkByTwo()}")
    println("Authentication key: ${authHash.encodeUpHex().fmtChunkByTwo()}")
    print("type 'yes, accept' to accept them: ")
    val userResponse: String? = readlnOrNull()
    return userResponse == "yes, accept"
}


/**
 * Mere collector of the PDF generation steps.  Fails the
 * process if a problem occurs.
 *
 * @param privs client private keys.
 * @param cfg configuration handle.
 */
private fun makePdf(privs: ClientPrivateKeysFile, cfg: EbicsHostConfig) {
    val pdf = generateKeysPdf(privs, cfg)
    val path = Path("/tmp/libeufin-ebics-keys.pdf")
    try {
        path.writeBytes(pdf, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)
    } catch (e: Exception) {
        throw Exception("Could not write PDF to '$path'", e)
    }
    println("PDF file with keys created at '$path'")
}


/** Perform an EBICS public key management [order] using [client] and update on disk state */
private suspend fun submitClientKeys(
    keyCfg: EbicsKeysConfig,
    hostCfg: EbicsHostConfig,
    privs: ClientPrivateKeysFile,
    client: HttpClient,
    ebicsLogger: EbicsLogger,
    order: EbicsKeyMng.Order,
    ebics3: Boolean
) {
    require(order != HPB) { "Only INI & HIA are supported for client keys" }
    val resp = keyManagement(hostCfg, privs, client, ebicsLogger, order, ebics3)
    if (resp.technicalCode == EbicsReturnCode.EBICS_INVALID_USER_OR_USER_STATE || resp.technicalCode == EbicsReturnCode.EBICS_INVALID_USER_STATE) {
        throw Exception("$order status code ${resp.technicalCode}: either your IDs are incorrect, or you already have keys registered with this bank")
    }
    val orderData = resp.okOrFail(order.name)
    when (order) {
        INI -> privs.submitted_ini = true
        HIA -> privs.submitted_hia = true
        HPB -> {}
    }
    try {
        persistClientKeys(privs, keyCfg.clientPrivateKeysPath)
    } catch (e: Exception) {
        throw Exception("Could not update the $order state on disk", e)
    }
}

/** Perform an EBICS private key management HPB using [client] */
private suspend fun fetchPrivateKeys(
    cfg: EbicsHostConfig,
    privs: ClientPrivateKeysFile,
    client: HttpClient,
    ebicsLogger: EbicsLogger,
    ebics3: Boolean
): BankPublicKeysFile {
    val order = HPB
    val resp = keyManagement(cfg, privs, client, ebicsLogger, order, ebics3)
    if (resp.technicalCode == EbicsReturnCode.EBICS_AUTHENTICATION_FAILED) {
        throw Exception("$order status code ${resp.technicalCode}: could not download bank keys, send client keys (and/or related PDF document with --generate-registration-pdf) to the bank")
    }
    val orderData = requireNotNull(resp.okOrFail(order.name)) {
        "$order: missing order data"
    }
    val (authPub, encPub) = EbicsKeyMng.parseHpbOrder(orderData)
    return BankPublicKeysFile(
        bank_authentication_public_key = authPub,
        bank_encryption_public_key = encPub,
        accepted = false
    )
}


suspend fun ebicsSetup(
    client: HttpClient,
    ebicsLogger: EbicsLogger,
    keyCfg: EbicsKeysConfig,
    hostCfg: EbicsHostConfig,
    setupCfg: EbicsSetupConfig,
    forceKeysResubmission: Boolean,
    generateRegistrationPdf: Boolean,
    autoAcceptKeys: Boolean,
    ebics3: Boolean
): Pair<ClientPrivateKeysFile, BankPublicKeysFile>{
    val clientKeys = loadOrGenerateClientKeys(keyCfg.clientPrivateKeysPath)
    var bankKeys = loadBankKeys(keyCfg.bankPublicKeysPath)

    // Check EBICS 3 support
    val versions = HEV(client, hostCfg, ebicsLogger)
    logger.debug("HEV: {}", versions)
    if (!versions.contains(VersionNumber(3.0f, "H005")) && !versions.contains(VersionNumber(3.02f, "H005"))) {
        throw Exception("EBICS 3 is not supported by your bank")
    }

    // Privs exist.  Upload their pubs
    val keysNotSub = !clientKeys.submitted_ini
    if (!clientKeys.submitted_ini || forceKeysResubmission)
        submitClientKeys(keyCfg, hostCfg, clientKeys, client, ebicsLogger, INI, ebics3)
    // Eject PDF if the keys were submitted for the first time, or the user asked.
    if (keysNotSub || generateRegistrationPdf) makePdf(clientKeys, hostCfg)
    if (!clientKeys.submitted_hia || forceKeysResubmission)
        submitClientKeys(keyCfg, hostCfg, clientKeys, client, ebicsLogger, HIA, ebics3)
    
    val fetchedBankKeys = fetchPrivateKeys(hostCfg, clientKeys, client, ebicsLogger, ebics3)
    if (bankKeys == null) {
        // Accept bank keys
        logger.info("Bank keys stored at ${keyCfg.bankPublicKeysPath}")
        try {
            persistBankKeys(fetchedBankKeys, keyCfg.bankPublicKeysPath)
        } catch (e: Exception) {
            throw Exception("Could not store bank keys on disk", e)
        }
        bankKeys = fetchedBankKeys
    } else {
        // Check current bank keys
        if (bankKeys.bank_encryption_public_key != fetchedBankKeys.bank_encryption_public_key) {
            throw Exception(buildString {
                append("On disk bank encryption key stored at ")
                append(keyCfg.bankPublicKeysPath)
                append(" doesn't match server key\nDisk:   ")
                append(CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_encryption_public_key).encodeUpHex().fmtChunkByTwo())
                append("\nServer: ")
                append(CryptoUtil.getEbicsPublicKeyHash(fetchedBankKeys.bank_encryption_public_key).encodeUpHex().fmtChunkByTwo())
            })
        } else if (bankKeys.bank_authentication_public_key != fetchedBankKeys.bank_authentication_public_key) {
            throw Exception(buildString {
                append("On disk bank authentication key stored at ")
                append(keyCfg.bankPublicKeysPath)
                append(" doesn't match server key\nDisk:   ")
                append(CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_authentication_public_key).encodeUpHex().fmtChunkByTwo())
                append("\nServer: ")
                append(CryptoUtil.getEbicsPublicKeyHash(fetchedBankKeys.bank_authentication_public_key).encodeUpHex().fmtChunkByTwo())
            })
        }
    }

    if (!bankKeys.accepted) {
        // Finishing the setup by accepting the bank keys.
        if (autoAcceptKeys) bankKeys.accepted = true
        else bankKeys.accepted = askUserToAcceptKeys(bankKeys, setupCfg)

        if (!bankKeys.accepted) {
            throw Exception("Cannot successfully finish the setup without accepting the bank keys")
        }
        try {
            persistBankKeys(bankKeys, keyCfg.bankPublicKeysPath)
        } catch (e: Exception) {
            throw Exception("Could not set bank keys as accepted on disk", e)
        }
    }

    return Pair(clientKeys, bankKeys)
}