// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

/*
 * Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
 * This source file is licensed under the MIT License found in the
 * LICENSE file in the root directory of this source tree.
 */

package simcu::simapi.openapi

import std.io.*
import std.fs.*
import std.unicode.*
import stdx.encoding.base64.*
import soulsoft_web_http.*
import soulsoft_web_hosting.*
import simcu::simapi.configurations.*

/**
 * @brief 提供 OpenAPI UI 静态资源中间件。
 *
 * 资源加载顺序：优先读取应用 wwwroot/{prefix} 目录（允许使用方覆盖），
 * 缺失时回退到 simapi-cj 包内置资源（Base64 内联）。
 *
 * 路由约定：
 *   /{prefix}              → 重定向到 /{prefix}/all.html
 *   /{prefix}/all.html     → 多文档切换页（含顶部栏下拉）
 *   /{prefix}/{id}.html    → 单文档页（无顶部栏，JS 从 URL 提取 id 加载 {id}.json）
 *   /{prefix}/urls         → 文档列表 JSON（由 simapi_extensions 注册）
 *   /{prefix}/{id}.json    → OpenAPI 文档 JSON（由 mapOpenApi 注册）
 */
public class OpenApiUIMiddleware <: IMiddleware {
    private static var _allHtmlCache: ?Array<Byte> = None
    private static var _singleHtmlCache: ?Array<Byte> = None
    private static var _bundleJsCache: ?Array<Byte> = None
    private static var _presetJsCache: ?Array<Byte> = None
    private static var _cssCache: ?Array<Byte> = None
    private let _env: IWebHostEnvironment
    private let _rootPath: PathString
    private let _wwwRootSubDir: String

    /**
     * @brief 创建 OpenAPI UI 中间件实例。
     * @param evn 当前 Web 主机环境。
     * @param options SimApi 全局配置，用于读取文档路由前缀。
     */
    public init(evn: IWebHostEnvironment, options: SimApiOptions) {
        _env = evn
        let prefix = options.simApiDocOptions.urlPrefix.trim().trimStart('/').trimEnd('/')
        _rootPath = PathString(if (prefix.isEmpty()) { "/docs" } else { "/${prefix}" })
        _wwwRootSubDir = if (prefix.isEmpty()) { "docs" } else { prefix }
    }

    /**
     * @brief 处理 OpenAPI UI 相关请求。
     * @param context 当前 HTTP 上下文。
     * @param next 下一个请求委托。
     */
    public func invoke(context: HttpContext, next: RequestDelegate): Unit {
        let remainingPath = if (let Some(path) <- context.request.path.startsWithSegments(
            _rootPath
        )) {
            path
        } else {
            next(context)
            return
        }


        if (let Some(relativePath) <- resolveRelativePath(remainingPath)) {
            if (serveFromWebRoot(context, relativePath) || serveFromEmbedded(context, relativePath)) {
                return
            }
        }

        next(context)
    }

    private func resolveRelativePath(remainingPath: PathString): ?String {
        if (remainingPath == "/" || !remainingPath.hasValue) {
            return None
        }

        let candidate = remainingPath.value.trimStart('/')
        if (!isSafeRelativePath(candidate)) {
            return None
        }
        return Some(candidate)
    }

    private func serveFromWebRoot(context: HttpContext, relativePath: String): Bool {
        let openApiRoot = Path(_env.webRootPath).join(_wwwRootSubDir)
        let path = openApiRoot.join(relativePath).normalize()
        if (!exists(path) || !isPathWithinRoot(path)) {
            return false
        }

        var served = false
        try (fs = File(path, OpenMode.Read)) {
            let data = readToEnd(fs)
            context.response.contentType = contentTypeFor(relativePath)
            context.response.write(data)
            served = true
        } catch (_: Exception) {
            served = false
        }
        return served
    }

    private func serveFromEmbedded(context: HttpContext, relativePath: String): Bool {
        let data = embeddedResource(relativePath)
        if (let Some(bytes) <- data) {
            context.response.contentType = contentTypeFor(relativePath)
            context.response.write(bytes)
            return true
        }
        return false
    }

    private func embeddedResource(relativePath: String): ?Array<Byte> {
        if (relativePath == "all.html") {
            return cachedResource(SwaggerUIResources.allHtml, "allHtml")
        }
        // index.html 不支持（已废弃，使用 all.html 或 {id}.html）
        if (relativePath == "index.html") {
            return None
        }
        // 任意 {id}.html（非 all.html）→ 返回 single.html 模板（JS 从 URL 提取 id）
        if (relativePath.endsWith(".html")) {
            return cachedResource(SwaggerUIResources.singleHtml, "singleHtml")
        }
        if (relativePath == "swagger-ui-bundle.js") {
            return cachedResource(SwaggerUIResources.swaggerUiBundleJs, "bundleJs")
        }
        if (relativePath == "swagger-ui-standalone-preset.js") {
            return cachedResource(SwaggerUIResources.swaggerUiStandalonePresetJs, "presetJs")
        }
        if (relativePath == "swagger-ui.css") {
            return cachedResource(SwaggerUIResources.swaggerUiCss, "css")
        }
        return None
    }

    private func cachedResource(b64: String, key: String): ?Array<Byte> {
        if (let Some(cached) <- OpenApiUIMiddleware.embeddedCache(key)) {
            return Some(cached)
        }
        let decoded = OpenApiUIMiddleware.decodeBase64(b64)
        if (let Some(bytes) <- decoded) {
            OpenApiUIMiddleware.setEmbeddedCache(key, bytes)
        }
        return decoded
    }

    private static func embeddedCache(key: String): ?Array<Byte> {
        if (key == "allHtml") {
            return OpenApiUIMiddleware._allHtmlCache
        }
        if (key == "singleHtml") {
            return OpenApiUIMiddleware._singleHtmlCache
        }
        if (key == "bundleJs") {
            return OpenApiUIMiddleware._bundleJsCache
        }
        if (key == "presetJs") {
            return OpenApiUIMiddleware._presetJsCache
        }
        if (key == "css") {
            return OpenApiUIMiddleware._cssCache
        }
        return None
    }

    private static func setEmbeddedCache(key: String, bytes: Array<Byte>): Unit {
        if (key == "allHtml") {
            OpenApiUIMiddleware._allHtmlCache = Some(bytes)
        } else if (key == "singleHtml") {
            OpenApiUIMiddleware._singleHtmlCache = Some(bytes)
        } else if (key == "bundleJs") {
            OpenApiUIMiddleware._bundleJsCache = Some(bytes)
        } else if (key == "presetJs") {
            OpenApiUIMiddleware._presetJsCache = Some(bytes)
        } else if (key == "css") {
            OpenApiUIMiddleware._cssCache = Some(bytes)
        }
    }

    private static func decodeBase64(b64: String): ?Array<Byte> {
        if (b64.isEmpty()) {
            return None
        }
        try {
            let bytes = fromBase64String(b64).getOrThrow { Exception("内置资源 Base64 解码失败") }
            if (bytes.size == 0) {
                return None
            }
            return Some(bytes)
        } catch (_: Exception) {
            return None
        }
    }

    private func contentTypeFor(relativePath: String): String {
        if (relativePath.endsWith(".html")) {
            return "text/html; charset=utf-8"
        }
        if (relativePath.endsWith(".css")) {
            return "text/css; charset=utf-8"
        }
        if (relativePath.endsWith(".js")) {
            return "application/javascript; charset=utf-8"
        }
        return "application/octet-stream"
    }

    private func isSafeRelativePath(relativePath: String): Bool {
        if (relativePath.isEmpty() || relativePath.startsWith('/') || relativePath.endsWith('/')) {
            return false
        }

        for (segment in relativePath.split('/')) {
            if (segment.isEmpty() || segment == "." || segment == "..") {
                return false
            }
        }

        return true
    }

    private func isPathWithinRoot(path: Path): Bool {
        try {
            let rootPath = canonicalize(Path(_env.webRootPath).join(_wwwRootSubDir))
            let targetPath = canonicalize(path)
            let rootPathString = rootPath.toString()
            let targetPathString = targetPath.toString()
            if (targetPathString == rootPathString) {
                return true
            }

            if (rootPathString == Path.Separator) {
                return targetPathString.startsWith(rootPathString)
            }

            let rootPathPrefix = if (rootPathString.endsWith(Path.Separator)) {
                rootPathString
            } else {
                "${rootPathString}${Path.Separator}"
            }
            return targetPathString.startsWith(rootPathPrefix)
        } catch (_: Exception) {
            return false
        }
    }
}
