/*
 * Copyright (c) 2025 SimcuTeam. All rights reserved.
 * 遵循 MIT 许可证。
 */

package simcu::simapi.helpers

import std.collection.*
import std.io.*
import stdx.net.http.*
import stdx.net.tls.*
import stdx.net.tls.common.*
import simcu::serialization.*
import simcu::simapi.communications.*
import simcu::simapi.configurations.*
import simcu::simapi.exceptions.*

/**
 * HTTP 客户端：用于调用其他带签名/AES 的 SimApi 服务。
 * 说明：
 *   - 内部使用 stdx.net.http 的 HttpClient（等价 System.Net.Http.HttpClient）
 *   - 返回泛型 T（反序列化响应 body 的 data 字段），不再返回 String
 * @param T 响应 data 的数据类型（任意类，simapi_serialization 反射反序列化）。
 */
public open class SimApiHttpClient {
    public var server: String
    public var appId: String
    public var appKey: String
    public var signName: String = "sign"
    public var timestampName: String = "timestamp"
    public var nonceName: String = "nonce"
    public var appIdName: ?String = Some("appId")
    public var signFields: Array<String> = []

    public init(options!: SimApiOptions = SimApiOptions()) {
        let httpOptions = options.simApiHttpClientOptions
        server = httpOptions.server
        appId = httpOptions.appId
        appKey = httpOptions.appKey
    }

    /**
     * 发起签名请求（GET query 签名 + POST body）。
     * SignQuery<T>：query 串 = SignFields + AppId + timestamp + nonce，整体拼 AppKey 取 MD5 作为 sign。
     * @param url 请求路径（相对路径，自动拼接 server）。
     * @param body 请求体 JSON 字符串（可选）。
     * @param queries 额外查询参数（可选）。
     * @return 响应 data 字段反序列化后的 T。
     */
    public func signQuery<T>(url: String, body!: String = "", queries!: HashMap<String, String> = HashMap<String, String>()): T {
        var queryUrl = StringBuilder()
        for (field in signFields) {
            queryUrl.append("${field}=")
            if (let Some(v) <- queries.get(field)) {
                queryUrl.append(v)
            }
            queryUrl.append("&")
        }
        if (let Some(name) <- appIdName) {
            queryUrl.append("${name}=${appId}&")
        }
        let timestamp = Int64(SimApiUtil.timestampNow)
        let nonce = generateNonce()
        queryUrl.append("${timestampName}=${timestamp}&${nonceName}=${nonce}")
        let signStr = "${queryUrl.toString()}&${appKey}"
        var path = "${server}${url}?${queryUrl.toString()}&${signName}=${SimApiUtil.md5(signStr)}"
        for ((k, v) in queries) {
            if (!signFields.contains(k)) {
                path = "${path}&${k}=${v}"
            }
        }
        return query<T>(path, body)
    }

    /**
     * 发起 AES 加密请求：body 加密后放入 {"data": "..."} 提交。
     * AesQuery<T>：对应 SimApiOneFieldRequest<string> { Data = Encrypt(body, AppKey) }。
     * @param url 请求路径（相对路径，自动拼接 server）。
     * @param body 请求体 JSON 字符串。
     * @return 响应 data 字段反序列化后的 T。
     */
    public func aesQuery<T>(url: String, body: String): T {
        var target = "${server}${url}"
        if (let Some(name) <- appIdName) {
            target = "${target}?${name}=${appId}"
        }
        let encrypted = aesEncrypt(body)
        let req = SimApiUtil.json(Some(SimApiOneFieldRequest<String>(encrypted)))
        return query<T>(target, req)
    }

    /**
     * 发起 AES 加密 + 签名请求。
     * @param url 请求路径（相对路径，自动拼接 server）。
     * @param body 请求体 JSON 字符串。
     * @param queries 额外查询参数（可选）。
     * @return 响应 data 字段反序列化后的 T。
     */
    public func aesSignQuery<T>(url: String, body: String, queries!: HashMap<String, String> = HashMap<String, String>()): T {
        let encrypted = aesEncrypt(body)
        let req = SimApiUtil.json(Some(SimApiOneFieldRequest<String>(encrypted)))
        return signQuery<T>(url, body: req, queries: queries)
    }

    /**
     * 发起 POST 请求并反序列化 SimApiResponse<T>，返回 data 字段。
     * Query<T>：
     *   ErrorWhenFalse(IsSuccessStatusCode) → ReadFromJsonAsync<SimApiResponse<T>> → ErrorWhen(Code != 200) → return Data。
     * 注意：必须 noProxy()，否则会走系统代理（192.168.0.250:8118）导致连接被拒。
     * 反序列化使用 simapi_serialization（Deserialize<T> 免约束）。
     */
    private func query<T>(url: String, body: String): T {
        let client = ClientBuilder().
            noProxy().
            tlsConfig(buildTlsConfig(url)).
            readTimeout(Duration.second * 30).
            build()
        try {
            let request = HttpRequestBuilder().
                post().
                url(url).
                header("Content-Type", "application/json").
                body(body).
                build()
            let response = client.send(request)
            try {
                SimApiError.errorWhenFalse(isSuccess(response.status), code: Int64(response.status),
                    message: "HTTP ERROR: ${response.status}")
                let json = readBodyText(response.body)
                let result = JsonSerializer.deserialize<SimApiResponse<T>>(json)
                SimApiError.errorWhen(result.code != 200, code: result.code, message: result.message)
                return result.data.getOrThrow()
            } finally {
                response.close()
            }
        } finally {
            client.close()
        }
    }

    /// 2xx 视为成功
    private static func isSuccess(status: UInt16): Bool {
        status >= 200 && status < 300
    }

    /// 读取响应体 InputStream 为字符串
    private static func readBodyText(body: InputStream): String {
        var buffer = Array<Byte>(4096, repeat: 0)
        var sb = StringBuilder()
        var read = body.read(buffer)
        while (read > 0) {
            sb.appendFromUtf8(buffer.slice(0, read))
            read = body.read(buffer)
        }
        sb.toString()
    }

    /// 构建 TLS 配置：信任所有证书 + SNI 域名
    private static func buildTlsConfig(url: String): TlsClientConfig {
        var tls = TlsClientConfig()
        tls.verifyMode = CertificateVerifyMode.TrustAll
        let host = extractHost(url)
        if (!host.isEmpty()) {
            tls.serverName = Some(host)
        }
        tls
    }

    private func aesEncrypt(plain: String): String {
        // SimApiAesUtil.Encrypt(plain, AppKey)（AES-256-CBC + PKCS7，Base64(IV + 密文)）
        SimApiAesUtil.encrypt(plain, appKey)
    }

    private static func generateNonce(): String {
        // nonce 直接用 Guid.NewGuid()
        SimApiUtil.newGuid()
    }

    /// 从完整 URL 提取 host（https://host[:port]/path → host）。
    private static func extractHost(fullUrl: String): String {
        match (fullUrl.indexOf("://")) {
            case Some(i) =>
                let rest = fullUrl[i + 3..]
                let slash = rest.indexOf("/") ?? rest.size
                let q = rest.indexOf("?") ?? rest.size
                let end = if (slash < q) { slash } else { q }
                return rest[0..end]
            case None => return ""
        }
    }
}
