/* Copyright 2025-2026 Eugene Petrenko (mcp@jonnyzzz.com); Copyright 2025-2026 JetBrains. Use of this source code is governed by the Apache 2.0 license. */
package com.jonnyzzz.mcpSteroid.ideDownloader

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonArray
import org.slf4j.LoggerFactory
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URI
import java.net.URLEncoder
import java.nio.charset.StandardCharsets

private val ideReleaseLookupLog = LoggerFactory.getLogger("com.jonnyzzz.mcpSteroid.ideDownloader.IdeReleaseLookup")

data class IdeArchiveResolution(
    val product: IdeProduct,
    val channel: IdeChannel,
    val version: String,
    val build: String,
    val url: String,
    val downloadKey: String,
    val releaseDate: String? = null,
    val checksumUrl: String? = null,
    val expectedSha256: String? = null,
)

/**
 * Returns the JetBrains products-API download key for the given OS / architecture combo.
 *
 * @see <a href="https://data.services.jetbrains.com/products">JetBrains Products API</a>
 */
fun resolveDownloadKey(
    os: HostOs,
    architecture: HostArchitecture,
): String = when (os) {
    HostOs.LINUX -> if (architecture.isArmArch) "linuxARM64" else "linux"
    HostOs.MAC -> if (architecture.isArmArch) "macM1" else "mac"
    HostOs.WINDOWS -> if (architecture.isArmArch) "windowsARM64" else "windows"
}

/**
 * Resolves the download URL for the latest IDE archive from the public products API.
 *
 * @param product the IDE product to look up
 * @param channel the release channel (stable or EAP)
 * @param os the target operating system (default: auto-detected)
 * @param architecture the host architecture for platform-specific archive selection
 * @return the direct download URL for the archive
 */
fun resolveArchiveUrl(
    product: IdeProduct,
    channel: IdeChannel,
    os: HostOs = resolveHostOs(),
    architecture: HostArchitecture = resolveHostArchitecture(),
): String {
    return resolveArchive(product, channel, os, architecture).url
}

fun resolveArchive(
    product: IdeProduct,
    channel: IdeChannel,
    os: HostOs = resolveHostOs(),
    architecture: HostArchitecture = resolveHostArchitecture(),
    version: String? = null,
): IdeArchiveResolution = resolveArchiveWithUrlReader(
    product = product,
    channel = channel,
    os = os,
    architecture = architecture,
    version = version,
    productsApiReader = { url -> readUrlText(url) },
    androidStudioReader = { url -> readUrlText(url, accept = "text/html,*/*") },
)

internal fun resolveArchiveWithUrlReader(
    product: IdeProduct,
    channel: IdeChannel,
    os: HostOs = resolveHostOs(),
    architecture: HostArchitecture = resolveHostArchitecture(),
    version: String? = null,
    urlReader: (String) -> String,
): IdeArchiveResolution = resolveArchiveWithUrlReader(
    product = product,
    channel = channel,
    os = os,
    architecture = architecture,
    version = version,
    productsApiReader = urlReader,
    androidStudioReader = urlReader,
)

private fun resolveArchiveWithUrlReader(
    product: IdeProduct,
    channel: IdeChannel,
    os: HostOs,
    architecture: HostArchitecture,
    version: String?,
    productsApiReader: (String) -> String,
    androidStudioReader: (String) -> String,
): IdeArchiveResolution {
    // Android Studio is a Google product and lives on a different feed.
    if (product === IdeProduct.AndroidStudio) {
        return resolveAndroidStudioArchiveWithUrlReader(channel, os, architecture, version, androidStudioReader)
    }

    val releaseType = URLEncoder.encode(channel.apiValue, StandardCharsets.UTF_8)
    val url = "https://data.services.jetbrains.com/products?code=${product.code}&release.type=$releaseType"

    logFetchingProductsInfo(url)
    val payload = productsApiReader(url)

    return resolveArchiveFromProductsApiPayload(
        product = product,
        channel = channel,
        os = os,
        architecture = architecture,
        version = version,
        productsApiUrl = url,
        payload = payload,
    )
}

internal fun resolveArchiveFromProductsApiPayload(
    product: IdeProduct,
    channel: IdeChannel,
    os: HostOs,
    architecture: HostArchitecture,
    version: String? = null,
    productsApiUrl: String,
    payload: String,
): IdeArchiveResolution {
    val json = Json { ignoreUnknownKeys = true }
    val products = json.parseToJsonElement(payload).jsonArray

    val matchingProduct = products
        .filterIsInstance<JsonObject>()
        .firstOrNull { obj -> (obj["code"] as? JsonPrimitive)?.content == product.code }
        ?: error("Products response does not contain '${product.code}' entry")

    val releases = (matchingProduct["releases"] as? JsonArray) ?: JsonArray(emptyList())
    val downloadKey = resolveDownloadKey(os, architecture)
    val wantedVersion = version?.takeIf { it.isNotBlank() }
    val skippedWrongFilename = mutableListOf<String>()

    for (release in releases.filterIsInstance<JsonObject>()) {
        val type = (release["type"] as? JsonPrimitive)?.content
        val releaseVersion = (release["version"] as? JsonPrimitive)?.content
        val build = (release["build"] as? JsonPrimitive)?.content
        val releaseDate = (release["date"] as? JsonPrimitive)?.content?.takeIf { it.isNotBlank() }
        if (!type.equals(channel.apiValue, ignoreCase = true)) continue
        if (releaseVersion.isNullOrBlank() || build.isNullOrBlank()) continue
        if (wantedVersion != null && wantedVersion != releaseVersion && wantedVersion != build) continue

        val downloads = release["downloads"] as? JsonObject ?: continue
        val platformDownload = downloads[downloadKey] as? JsonObject ?: continue
        val link = (platformDownload["link"] as? JsonPrimitive)?.content ?: continue
        if (link.isBlank()) continue
        val checksumLink = (platformDownload["checksumLink"] as? JsonPrimitive)?.content?.takeIf { it.isNotBlank() }
        val filename = downloadFilenameFromUrl(link)
        if (!product.acceptsDownloadFilename(filename)) {
            skippedWrongFilename += "$releaseVersion -> $filename"
            continue
        }
        return IdeArchiveResolution(
            product = product,
            channel = channel,
            version = releaseVersion,
            build = build,
            url = link,
            downloadKey = downloadKey,
            releaseDate = releaseDate,
            checksumUrl = checksumLink,
        )
    }

    error(resolveArchiveFailureMessage(product, channel, wantedVersion, downloadKey, productsApiUrl, skippedWrongFilename))
}

internal fun IdeProduct.acceptsDownloadFilename(filename: String): Boolean {
    val tokens = urlFilenameTokens
    return tokens.isEmpty() || tokens.any { token -> filename.contains(token) }
}

internal fun downloadFilenameFromUrl(link: String): String =
    URI(link).path.substringAfterLast('/').takeIf { it.isNotBlank() } ?: link.substringAfterLast('/')

private fun resolveArchiveFailureMessage(
    product: IdeProduct,
    channel: IdeChannel,
    wantedVersion: String?,
    downloadKey: String,
    productsApiUrl: String,
    skippedWrongFilename: List<String>,
): String {
    val versionMessage = if (wantedVersion == null) "latest" else "version '$wantedVersion'"
    val tokens = product.urlFilenameTokens
    if (tokens.isEmpty()) {
        return "Unable to resolve $versionMessage '${channel.apiValue}' release for product '${product.code}' " +
            "(tried download key $downloadKey) from $productsApiUrl"
    }
    val tokensText = tokens.joinToString { "`$it`" }
    val skippedText = skippedWrongFilename
        .take(5)
        .joinToString(prefix = " Skipped mismatched filenames: ")
        .takeIf { skippedWrongFilename.isNotEmpty() }
        .orEmpty()
    return "No release in the '${channel.apiValue}' channel of code=${product.code} serves a download URL whose filename " +
        "contains any of: $tokensText (tried download key $downloadKey) from $productsApiUrl. " +
        "Latest matched: <none>.$skippedText JetBrains may not have published this edition for the most recent " +
        "version; try --version with an older known-good build."
}

internal fun logFetchingProductsInfo(url: String) {
    ideReleaseLookupLog.debug("[IDE-DOWNLOAD] Fetching products info from {}", url)
}

internal fun readUrlText(url: String, accept: String = "application/json"): String {
    val connection = (URI(url).toURL().openConnection() as HttpURLConnection).apply {
        requestMethod = "GET"
        connectTimeout = 15_000
        readTimeout = 15_000
        setRequestProperty("Accept", accept)
    }
    try {
        val statusCode = connection.responseCode
        val body = (if (statusCode in 200..299) connection.inputStream else connection.errorStream)
            ?.bufferedReader()
            ?.use { it.readText() }
            .orEmpty()
        if (statusCode !in 200..299) {
            throw IOException("Failed to fetch from $url. HTTP $statusCode\n$body")
        }
        return body
    } finally {
        connection.disconnect()
    }
}
