package cjTauri

import std.collection.*
import std.sort.*
import stdx.encoding.json.*

/**
 * 插件(对标 Tauri v2 的 plugin,设计见 docs/RFC-插件体系.md)
 *
 * 一个插件 = 一组命令(全名 `<插件名>:<短名>`)+ 可选事件 + 可选前端 JS 片段。
 * 权限仍由应用的 capabilities 目录里的 json 决定:插件只声明「我提供什么」,**不自动放行**,
 * 默认最小权限不被破坏(RFC §5.3 的取舍 B)。
 *
 * 装配(必须在 run() / runUrl() 之前——页面加载前命令表要定稿):
 *   TauriApp().plugin(FsPlugin())
 *
 * 命令全名与内置的 system:* 同形,所以 IpcHub 分发与 Capability 校验零改动。
 * 接口带默认实现,插件作者通常只写 name() 与 commands()。
 */
public interface Plugin {
    /** 插件名:作为命令/事件的命名空间(如 "fs"),不得为空、不得含 ':' */
    func name(): String

    /** 命令短名 → 处理器;框架注册为 "<插件名>:<短名>" */
    func commands(): HashMap<String, CommandHandler>

    /** 可能推送的事件短名;框架登记为 "<插件名>:<短名>",用于缺权限提示。默认无事件 */
    func events(): ArrayList<String> {
        return ArrayList<String>()
    }

    /** 前端 JS 片段:定义 window.__CJ_TAURI__.<插件名>;没有就返回空串 */
    func jsShim(): String {
        return ""
    }

    /** 初始化:记录 app 引用、做一次性准备。默认空实现 */
    func setup(app: TauriApp): Unit { let _ = app }

    /**
     * 命名权限集(v1.1):短集名 → 成员(写短名会自动补 "<插件名>:",写全名则原样保留)。
     * 应用在 capabilities 的 permissions 里按 "<插件名>:<短集名>" 引用,一次放行一组。
     * 默认无。**集声明不等于放行**——清单不引用它,一条也不会生效。
     */
    func permissions(): HashMap<String, ArrayList<String>> {
        return HashMap<String, ArrayList<String>>()
    }

    /**
     * 本插件是否吃「路径 scope」这层校验(RFC-003 第 4 期,默认 false)。
     *
     * 声明 true 意味着:命令里带路径的参数一律经 `IpcContext.resolveScopedPath(插件名, 路径)` 取值
     * (越界即拒),且清单没为它写 scopes 时框架会在启动时提示「按旧行为全量放行」。
     * 为什么不由框架猜「哪个参数是路径」:只有插件自己知道参数语义,猜出来的白名单等于没有白名单。
     */
    func scopeAware(): Bool {
        return false
    }

    /**
     * 自描述(批次 3,RFC §4.6.7):供 sync/check 三方比对的静态数据。
     * 默认实现 = 全宿主、无能力依赖(多数插件无需覆盖);
     * 有平台约束的插件必须覆盖(如 SqlitePlugin 只支持 webkitgtk)。
     *
     * v1 曾结论「不加接口方法」(数据可从 commands()/events()/permissions() 推出);
     * 批次 3 改自描述双通道——supportedHosts / requiredCapabilities 是命令表推不出来的
     * 语义信息,只能由插件自报。
     */
    func describe(): PluginManifest {
        return PluginManifest(ArrayList<String>(), ArrayList<String>())
    }
}

/**
 * 插件自描述(批次 3,RFC §4.6.7):供 sync/check 三方比对的静态数据。
 *
 * - supportedHosts:插件实际支持的 hostKind 列表;空 = 全宿主。
 *   不在列表的宿主上,插件必须以桩如实失败「端不支持」(P-2),不得静默缺失;
 * - requiredCapabilities:插件依赖的能力清单 identifier;空 = 无依赖。
 *   check 拿它与 capabilities 目录的 identifier 交叉比对(缺声明 = 失败)。
 */
public class PluginManifest {
    public let supportedHosts: ArrayList<String>
    public let requiredCapabilities: ArrayList<String>

    public init(supportedHosts: ArrayList<String>, requiredCapabilities: ArrayList<String>) {
        this.supportedHosts = supportedHosts
        this.requiredCapabilities = requiredCapabilities
    }
}

/**
 * 插件名是否合法:非空且不含全/半角冒号。
 * 冒号是命名空间分隔符,混进来会让 "<插件名>:<短名>" 无法解析、权限也写不清。
 */
public func isValidPluginName(name: String): Bool {
    if (name.size == 0) {
        return false
    }
    return !name.contains(":") && !name.contains(":")
}

/** 某插件的命令全名列表(`<插件名>:<短名>`) */
public func pluginCommandNames(p: Plugin): ArrayList<String> {
    let names = ArrayList<String>()
    for ((short, _) in p.commands()) {
        names.add("${p.name()}:${short}")
    }
    return names
}

/** 某插件的事件全名列表(`<插件名>:<短名>`) */
public func pluginEventNames(p: Plugin): ArrayList<String> {
    let names = ArrayList<String>()
    for (short in p.events()) {
        names.add("${p.name()}:${short}")
    }
    return names
}

/** 把插件的命令按 "<插件名>:<短名>" 注册进 hub(TauriApp.prepare 与测试共用) */
public func registerPluginCommands(hub: IpcHub, p: Plugin): Unit {
    for ((short, handler) in p.commands()) {
        hub.register("${p.name()}:${short}", handler)
    }
}

/** 某插件的命名权限集:`<插件名>:<短集名>` → 展开后的成员全名列表 */
public func pluginPermissionSets(p: Plugin): HashMap<String, ArrayList<String>> {
    let out = HashMap<String, ArrayList<String>>()
    for ((shortSet, members) in p.permissions()) {
        let expanded = ArrayList<String>()
        for (m in members) {
            if (m.contains(":")) {
                expanded.add(m)
            } else {
                expanded.add("${p.name()}:${m}")
            }
        }
        out.add("${p.name()}:${shortSet}", expanded)
    }
    return out
}

/** 把插件的命名权限集注册进能力注册中心(TauriApp.prepare 与测试共用) */
public func registerPluginPermissionSets(reg: CapabilityRegistry, p: Plugin): Unit {
    for ((setName, members) in pluginPermissionSets(p)) {
        reg.addPermissionSet(setName, members)
    }
}

/** 人类可读的命名权限集清单(启动日志用);一个集都没有时返回空串 */
public func describePermissionSets(plugins: ArrayList<Plugin>): String {
    var text = ""
    for (p in plugins) {
        for ((setName, members) in pluginPermissionSets(p)) {
            var list = ""
            for (m in members) {
                if (list.size > 0) {
                    list += ", "
                }
                list += m
            }
            text += "  ${setName}  ${list}\n"
        }
    }
    return text
}

/**
 * 收集各插件的 jsShim,作为「预执行脚本」逐条返回(一个插件一段,顺序 = 装配顺序)
 *
 * v2 方案(RFC §5.4):不再把 shim 拼成 script 标签塞进 HTML——那段字符串操作对畸形 HTML 不健壮,
 * 而且 runUrl 的页面 HTML 根本不在本进程、拼不进去。改为交给宿主的 addInitScript,两平台都以
 * document-start 注入,HTML / URL 两种来源一视同仁。
 * 一个插件一段(而不是拼成一整段):某个插件的 shim 写坏了不会带崩其它插件的命名空间。
 */
public func pluginInitScripts(plugins: ArrayList<Plugin>): ArrayList<String> {
    let out = ArrayList<String>()
    for (p in plugins) {
        let shim = p.jsShim()
        if (shim.size > 0) {
            out.add(shim)
        }
    }
    return out
}

/** 人类可读的插件清单(启动日志用,对齐 RFC §5.5 的打印形态) */
public func describePlugins(plugins: ArrayList<Plugin>): String {
    var text = ""
    for (p in plugins) {
        var cmds = ""
        for (n in pluginCommandNames(p)) {
            if (cmds.size > 0) {
                cmds += ", "
            }
            cmds += n
        }
        text += "  ${p.name()}  ${cmds}"
        let evts = pluginEventNames(p)
        if (evts.size > 0) {
            text += "  events: "
            var first = true
            for (n in evts) {
                if (!first) {
                    text += ", "
                }
                text += n
                first = false
            }
        }
        text += "\n"
    }
    return text
}

/**
 * 机器可读的插件清单(RFC §8 第 7 条的答案 C:给工具/CI 用,**不动 Plugin 接口**)
 *
 * 形态 `[{name, commands, events, permissions, hasShim}, ...]`:
 *   - `commands` / `events`:全名数组(`<插件名>:<短名>`,与 IPC 注册名、能力清单里写的名字一致)
 *   - `permissions`:集全名 → 成员全名数组(**框架展开后**的样子,也就是清单里能引用的名字)
 *   - `hasShim`:是否带前端 JS 片段(工具据此判断要不要等 `__CJ_TAURI__.<插件名>`)
 * 数组与权限键都按字典序排:HashMap 迭代顺序不定,工具的输出 / diff 需要稳定。
 *
 * 数据来自 describe()(批次 3 自描述双通道):命令/事件/权限集之外,携带 supportedHosts /
 * requiredCapabilities 两个命令表推不出来的语义项(RFC §4.6.7)。
 */
public func describePluginsJson(plugins: ArrayList<Plugin>): JsonValue {
    let arr = ArrayList<JsonValue>()
    for (p in plugins) {
        var obj = JsonObject()
        let d = p.describe()
        obj.put("name", JsonString(p.name()))
        obj.put("commands", jsonStringArray(pluginCommandNames(p)))
        obj.put("events", jsonStringArray(pluginEventNames(p)))
        obj.put("permissions", jsonPermissionSets(p))
        obj.put("hasShim", JsonBool(p.jsShim().size > 0))
        obj.put("supportedHosts", jsonStringArray(d.supportedHosts))
        obj.put("requiredCapabilities", jsonStringArray(d.requiredCapabilities))
        arr.add(obj)
    }
    return JsonArray(arr)
}

/** 字符串数组 → JSON 数组,元素按字典序(HashMap 来的一律先排序,保证输出稳定) */
func jsonStringArray(items: ArrayList<String>): JsonValue {
    let sorted = sortedStrings(items)
    let arr = ArrayList<JsonValue>()
    for (s in sorted) {
        arr.add(JsonString(s))
    }
    return JsonArray(arr)
}

/** 命名权限集 → JSON 对象(键 = 集全名,值 = 成员全名数组);键也按字典序 */
func jsonPermissionSets(p: Plugin): JsonValue {
    let sets = pluginPermissionSets(p)
    let keys = ArrayList<String>()
    for ((setName, _) in sets) {
        keys.add(setName)
    }
    var obj = JsonObject()
    for (setName in sortedStrings(keys)) {
        match (sets.get(setName)) {
            case Some(members) => obj.put(setName, jsonStringArray(members))
            case None => ()
        }
    }
    return obj
}

/** 复制一份并按字典序排序(不改动入参;元素量级只有每插件的几条命令,开销可忽略) */
func sortedStrings(items: ArrayList<String>): ArrayList<String> {
    let out = ArrayList<String>()
    for (s in items) {
        out.add(s)
    }
    sort(out)
    return out
}

/**
 * 未授权提示:插件声明了、但应用的能力清单没放行时,说清缺哪条、加到哪去。
 * 只提示、不改权限——放行与否仍由 capabilities 决定(RFC §5.3 的取舍 B)。
 * 调用时机:能力清单加载完成之后、页面加载之前(TauriApp.prepare)。
 */
public func warnUnauthorizedPlugins(plugins: ArrayList<Plugin>, caps: CapabilityRegistry): Unit {
    for (p in plugins) {
        for (name in pluginCommandNames(p)) {
            if (!caps.canInvoke(name)) {
                eprintln("[cj-tauri] 插件 ${p.name()} 的命令 ${name} 尚未授权,前端调用会被拒绝")
                eprintln("[cj-tauri]   请在 capabilities 目录的 json 里把 \"${name}\" 加进 commands")
            }
        }
        for (name in pluginEventNames(p)) {
            if (!caps.canEmit(name)) {
                eprintln("[cj-tauri] 插件 ${p.name()} 的事件 ${name} 尚未授权,推送会被丢弃")
                eprintln("[cj-tauri]   请在 capabilities 目录的 json 里把 \"${name}\" 加进 events")
            }
        }
    }
}

/**
 * 提示「吃 scope 的插件没有被声明 scope」——旧清单缺省行为:全量放行(RFC-003 第 4 期)。
 *
 * 只在「该插件至少有一条命令被授权」时提示:没用到它的应用不必被这行日志打扰。
 * 声明了 scope 的插件(caps.hasScopeForPlugin 为真)不提示。
 */
public func warnUnscopedPlugins(plugins: ArrayList<Plugin>, caps: CapabilityRegistry): Unit {
    for (p in plugins) {
        if (!p.scopeAware()) {
            continue
        }
        var authorized = false
        for (name in pluginCommandNames(p)) {
            if (caps.canInvoke(name)) {
                authorized = true
                break
            }
        }
        if (!authorized) {
            continue
        }
        if (caps.hasScopeForPlugin(p.name())) {
            continue
        }
        eprintln("[cj-tauri] 插件 ${p.name()} 的路径参数未声明 scope:按旧行为全量放行")
        eprintln("[cj-tauri]   建议在 capabilities 的 json 里加 \"scopes\": { \"${p.name()}\": { \"allow\": [\"<放行路径>\"] } }")
    }
}