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

import org.bouncycastle.math.ec.rfc8032.Ed25519
import java.math.BigInteger
import java.security.MessageDigest

sealed interface IncomingSubject {
    data class Reserve(val reserve_pub: EddsaPublicKey): IncomingSubject
    data class Kyc(val account_pub: EddsaPublicKey): IncomingSubject
    data class Map(val auth_pub: EddsaPublicKey): IncomingSubject
    data object AdminBalanceAdjust: IncomingSubject

    val type: IncomingType get() = when (this) {
        is Reserve -> IncomingType.reserve
        is Kyc -> IncomingType.kyc
        is Map -> IncomingType.map
        AdminBalanceAdjust -> throw IllegalStateException("Admin balance adjust")
    }
    val key: EddsaPublicKey get() = when (this) {
        is Reserve -> this.reserve_pub
        is Kyc -> this.account_pub
        is Map -> this.auth_pub
        AdminBalanceAdjust -> throw IllegalStateException("Admin balance adjust")
    }
}

/** Base32 quality by proximity to spec and error probability */
private enum class Base32Quality {
    /// Both mixed casing and mixed characters, that's weird
    Mixed,
    /// Standard but use lowercase, maybe the client shown lowercase in the UI
    Standard,
    /// Uppercase but mixed characters, its common when making typos
    Upper,
    /// Both uppercase and use the standard alphabet as it should
    UpperStandard;

    companion object {
        fun measure(s: String): Base32Quality {
            var uppercase = true;
            var standard = true;
            for (char in s) {
                uppercase = uppercase && char.isUpperCase()
                standard = standard && Base32Crockford.ALPHABET.contains(char)
            }
            return if (uppercase && standard) {
                Base32Quality.UpperStandard
            } else if (uppercase && !standard) {
                Base32Quality.Upper
            } else if (!uppercase && standard) {
                Base32Quality.Standard
            } else {
                Base32Quality.Mixed
            }
        }
    }
}

private data class Candidate(val subject: IncomingSubject, val quality: Base32Quality)

private const val ADMIN_BALANCE_ADJUST = "ADMINBALANCEADJUST"
private const val KEY_SIZE = 52;
private const val PREFIX_SIZE = KEY_SIZE + 3;
private val ALPHA_NUMBERIC_PATTERN = Regex("[0-9a-zA-Z]*")

/**
 * Extract the public key from an unstructured incoming transfer subject.
 *
 * When a user enters the transfer object in an unstructured way, for ex in
 * their banking UI, they may mistakenly enter separators such as ' \n-+' and
 * make typos.
 * To parse them while ignoring user errors, we reconstruct valid keys from key
 * parts, resolving ambiguities where possible.
 **/
fun parseIncomingSubject(subject: String?): IncomingSubject {
    if (subject == null || subject.isEmpty()) {
        throw Exception("missing subject")
    }

    /** Parse an incoming subject */
    fun parseSingle(str: String): Candidate? {
        // Check key type
        val (type, raw) = when (str.length) {
            ADMIN_BALANCE_ADJUST.length -> if (str.equals(ADMIN_BALANCE_ADJUST, ignoreCase = true)) {
                return Candidate(IncomingSubject.AdminBalanceAdjust, Base32Quality.UpperStandard)
            } else {
                return null
            }
            KEY_SIZE -> Pair(IncomingType.reserve, str)
            PREFIX_SIZE -> if (str.startsWith("KYC")) {
                Pair(IncomingType.kyc, str.substring(3))
            } else if (str.startsWith("MAP")) {
                Pair(IncomingType.map, str.substring(3))
            } else {
                return null
            }
            else -> return null
        }

        // Check key validity
        val key = try {
            EddsaPublicKey(raw)
        } catch (e: Exception) {
            return null
        }
        if (!Ed25519.validatePublicKeyFull(key.raw, 0)) {
            return null
        }

        val quality = Base32Quality.measure(raw);

        val subject = when (type) {
            IncomingType.map -> IncomingSubject.Map(key)
            IncomingType.kyc -> IncomingSubject.Kyc(key)
            IncomingType.reserve -> IncomingSubject.Reserve(key)
        }
        return Candidate(subject, quality)
    }

    // Find and concatenate valid parts of a keys
    val parts = mutableListOf(0)
    val concatenated = StringBuilder()
    for (match in ALPHA_NUMBERIC_PATTERN.findAll(subject.replace("%20", " "))) {
        concatenated.append(match.value);
        parts.add(concatenated.length);
    }

    // Find best candidates
    var best: Candidate? = null
    // For each part as a starting point
    for ((i, start) in parts.withIndex()) {
        // Use progressively longer concatenation
        for (end in parts.subList(i, parts.size)) {
            val range = start until end
            // Until they are to long to be a key
            if (range.count() > PREFIX_SIZE) {
                break;
            }
            // Parse the concatenated parts
            val slice = concatenated.substring(range)
            parseSingle(slice)?.let { other ->
                if (best != null) {
                    if (best.subject is IncomingSubject.AdminBalanceAdjust) {
                        if (other.subject !is IncomingSubject.AdminBalanceAdjust) {
                            throw Exception("found multiple subject kind")
                        }
                    } else if (other.quality > best.quality // We prefer high quality keys
                        || ( // We prefer prefixed keys over reserve keys
                            best.subject.type == IncomingType.reserve &&
                            (other.subject.type == IncomingType.kyc || other.subject.type == IncomingType.map)
                        ))
                    {
                        best = other
                    } else if (best.subject.key != other.subject.key // If keys are different
                        && best.quality == other.quality // Of same quality
                        && !( // And prefixing is different
                            (best.subject.type == IncomingType.kyc || best.subject.type == IncomingType.map) &&
                            other.subject.type == IncomingType.reserve
                        ))
                    {
                        throw Exception("found multiple reserve public key")
                    }
                } else {
                    best = other
                }
            }
        }
    }

    return best?.subject ?: throw Exception("missing reserve public key")
}

/** Extract the reserve public key from an incoming Taler transaction subject */
fun parseOutgoingSubject(subject: String): Triple<ShortHashCode, BaseURL, String?>  {
    var iterator = subject.splitToSequence(' ').iterator();
    val first = iterator.next()
    if (!iterator.hasNext()) throw Exception("malformed outgoing subject")
    val second = iterator.next()
    if (iterator.hasNext()) {
        val third = iterator.next()
        return Triple(EddsaPublicKey(second), BaseURL.parse(third), first)
    } else {
        return Triple(EddsaPublicKey(first), BaseURL.parse(second), null)
    }
}

/** Format an outgoing subject */
fun fmtOutgoingSubject(wtid: ShortHashCode, url: BaseURL, metadata: String? = null): String = buildString {
    if (metadata != null) {
        append(metadata)
        append(" ")
    }
    append(wtid)
    append(" ")
    append(url)
}

/** Format an incoming subject */
fun fmtIncomingSubject(type: IncomingType, key: EddsaPublicKey): String = buildString {
    append("Taler ")
    when (type) {
        IncomingType.kyc -> append("KYC:")
        IncomingType.map -> append("MAP:")
        IncomingType.reserve -> Unit
    }
    append(key)
}

/** Encode a public key as a QR-Bill reference */
fun subjectFmtQrBill(key: EddsaPublicKey): String {
    // High-Entropy Hash (SHA-256) to ensure even distribution
    val digest = MessageDigest.getInstance("SHA-256")
    val hashInt = BigInteger(1, digest.digest(key.raw))
    
    // Modulo 10^26 to fit the Swiss QR data field
    val divisor = BigInteger.TEN.pow(26)
    val referenceBase = hashInt.remainder(divisor).toString().padStart(26, '0')

    // Modulo 10 Recursive calculation
    val lookupTable = intArrayOf(0, 9, 4, 6, 8, 2, 7, 1, 3, 5)
    var carry = 0
    
    for (char in referenceBase) {
        val digit = char.digitToInt()
        carry = lookupTable[(carry + digit) % 10]
    }
    
    val checksum = (10 - carry) % 10
    
    return referenceBase + checksum
}

/** Check if a subject is a valid QR-Bill reference */
fun subjectIsQrBill(reference: String): Boolean {
    // Quick length and numeric check
    if (reference.length != 27 || !reference.all { it.isDigit() }) {
        return false
    }

    // Modulo 10 Recursive check
    val lookupTable = intArrayOf(0, 9, 4, 6, 8, 2, 7, 1, 3, 5)
    var carry = 0
    
    for (char in reference) {
        val digit = char.digitToInt()
        carry = lookupTable[(carry + digit) % 10]
    }
    
    // If the check digit was correct, the final carry will be 0
    return carry == 0
}