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

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.api.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.util.*
import tech.libeufin.common.setupSecurityProperties
import tech.libeufin.common.BaseURL
import tech.libeufin.ebics.httpClient
import java.security.Key
import java.time.*
import java.time.format.DateTimeFormatter
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import kotlinx.coroutines.runBlocking
import java.util.Base64

data class AzureStorageConfig(
    var accountName: String = "ACCOUNT_NAME",
    var accountKey: String = "ACCOUNT_KEY"
)

const val API_VERSION = "2025-11-05"

val LINEAR_WHITESPACE = Regex("\\s+")

/**
 * Ktor Client Plugin for Azure Storage Shared Key Authorization.
 */
val AzureSharedKeyAuth = createClientPlugin("AzureSharedKeyAuth", ::AzureStorageConfig) {
    val config = pluginConfig
    val keyBytes = Base64.getDecoder().decode(config.accountKey)
    val signingKey: Key = SecretKeySpec(keyBytes, "HmacSHA256")
    
    // Intercepts the request before it is sent
    onRequest { req, _ ->
        // 1. Set required headers (x-ms-date and x-ms-version)
        val dateHeaderValue = Instant.now().atZone(ZoneOffset.ofHours(0)).format(DateTimeFormatter.RFC_1123_DATE_TIME)
        
        req.headers.apply {
            // Azure uses x-ms-date instead of the standard Date header for signing
            set("x-ms-date", dateHeaderValue) 
            set("x-ms-version", API_VERSION)
        }

        // 2. Build the StringToSign
        val stringToSign = createStringToSign(req, config.accountName)

        // 3. Calculate the HMAC-SHA256 signature
        val signature = run {
            val mac = Mac.getInstance("HmacSHA256")
            mac.init(signingKey)
            val hash = mac.doFinal(stringToSign.toByteArray(Charsets.UTF_8))
            Base64.getEncoder().encodeToString(hash)
        }

        // 4. Add the Authorization header
        val authHeader = "SharedKey ${config.accountName}:$signature"
        req.headers.set(HttpHeaders.Authorization, authHeader)
    }
}

/**
 * Constructs the StringToSign based on the Azure Storage Shared Key specification.
 */
private fun createStringToSign(
    req: HttpRequestBuilder,
    accountName: String
): String = buildString {
    val h = req.headers;

    fun add(value: Any?) {
        append(value ?: "")
        append('\n')
    }

    // 1. VERB
    add(req.method.value) 
    // 2. Content-Encoding
    add(h[HttpHeaders.ContentEncoding])
    // 3. Content-Language
    add(h[HttpHeaders.ContentLanguage])
    // 4. Content-Length (empty string if zero for modern versions)
    val length = req.contentLength()
    add(if (length != null && length != 0L) "$length" else null)
    // 5. Content-MD5
    add(h["Content-MD5"])
    // 6. Content-Type
    add(h[HttpHeaders.ContentType])
    // 7. Date
    add("") // Must be empty as x-ms-date is used)
    // 8. If-Modified-Since
    add(h[HttpHeaders.IfModifiedSince])
    // 9. If-Match
    add(h[HttpHeaders.IfMatch])
    // 10. If-None-Match
    add(h[HttpHeaders.IfNoneMatch])
    // 11. If-Unmodified-Since
    add(h[HttpHeaders.IfUnmodifiedSince])
    // 12. Range
    add(h[HttpHeaders.Range])
    // 13. CanonicalizedHeaders
    // This includes all x-ms- headers, converted to lowercase, sorted, and concatenated.
    for (entry in h.entries().sortedBy { it.key }) {
        val k = entry.key
        if (k.startsWith("x-ms-")) {
            append(k)
            append(':')
            append(entry.value.joinToString(" ").replace(LINEAR_WHITESPACE, " ").trim())
            append('\n')
        }

    }
    // 14. CanonicalizedResource
    // This includes the account name, the path, and canonicalized query parameters.
    append('/')
    append(accountName)
    append(req.url.encodedPath.trimEnd('/'))
    for (entry in req.url.encodedParameters.entries().sortedBy { it.key }) {
        append('\n')
        append(entry.key)
        append(':')
        append(entry.value.sorted().joinToString(","))
    }
}

data class AzureError(val status: HttpStatusCode, val code: String?): Exception("${status.value} ${code}")

class AzureBlogStorage(
    name: String,
    key: String,
    url: BaseURL,
    client: HttpClient
) {
    private val client = client.config {
        defaultRequest {
            url(url.toString())
        }
        install(AzureSharedKeyAuth) {
            accountName = name
            accountKey = key
        }

        install(HttpCallValidator) {
            validateResponse { res ->
                if (res.status.value >= 300) {
                    throw AzureError(res.status, res.headers["x-ms-error-code"])
                }
            }
        }
    }

    suspend fun createContainer(name: String) {
        client.put("$name?restype=container")
    }

    suspend fun getContainterMetadata(container: String) {
        client.get("$container?restype=container&comp=metadata")
    }

    suspend fun putBlob(container: String, name: String, content: ByteArray, contentType: ContentType) {
        client.put("$container/$name") {
            setBody(content)
            contentType(contentType)
            headers.set(HttpHeaders.ContentLength, "${content.size}")
            headers.set("x-ms-blob-type", "BlockBlob")
        }
    }
}