// iOS 原生壳的桥接层：仅在 iOS WKWebView「无头引擎」壳中可用。浏览器 / 桌面里
// isIOSShell() 返回 false，所有调用点回退既有 Web 行为，因此本模块对纯 Web 与桌面
// 构建零影响——同一份引擎代码同时部署到浏览器、桌面与 iOS 无头壳。
//
// 方向与桌面（Wails）相反，是本模块存在的根由：
//   - 桌面：React UI 在 WebView 顶层，调 DOWN 到 Go 绑定方法（window.go.main.App.*，
//     直接返回 Promise）。
//   - iOS（arch A，见 ios/PLAN.md §2）：原生 SwiftUI 在顶层，本 WebView 是「无头
//     传输引擎」。JS→原生只能 postMessage 到 WKScriptMessageHandler（无返回值），
//     原生→JS 用 evaluateJavaScript 回调全局函数。故 callNative 以「请求 id + 全局
//     resolver」把单向 postMessage 拼回请求 / 响应语义。
//
// 原生侧须实现的契约（Swift 实现据此对齐）：
//   - 注册名为 "cdropEngine" 的 WKScriptMessageHandler，收 { id, method, payload }。
//   - 处理完一次 callNative 后，evaluateJavaScript 调
//     window.__cdropEngineResolve(id, ok, value)（ok=false 时 value 为错误串）。
//   - 推事件（进度 / 状态 / 传入 offer 等）用 evaluateJavaScript 调
//     window.__cdropEngineEvent(name, payload)。
//   - 接收落盘四方法 beginDownload / appendDownload / finalizeDownload /
//     abortDownload 与桌面 desktop.ts 同形（data 为 base64），落 iOS 沙盒文件。

import type { FileSource } from "../features/transfer/source";

// WKScriptMessageHandler 的 JS 侧投递接口（仅 postMessage）。
interface WebKitMessageHandler
{
    postMessage: (msg: unknown) => void;
}

// 原生注册的引擎消息处理器。其在场即判定为 iOS 无头壳。
function handler(): WebKitMessageHandler | null
{
    const wk = (window as unknown as {
        webkit?: { messageHandlers?: { cdropEngine?: WebKitMessageHandler } };
    }).webkit;
    return wk?.messageHandlers?.cdropEngine ?? null;
}

// isIOSShell 仅要求消息处理器在场——原生注入它即代表无头壳就绪。浏览器 / 桌面里
// 不存在 window.webkit.messageHandlers.cdropEngine，故恒为 false。
export function isIOSShell(): boolean
{
    return typeof window !== "undefined" && handler() !== null;
}

// ── 请求 / 响应关联（callNative） ──────────────────────────────────────────

interface Pending
{
    resolve: (value: unknown) => void;
    reject: (err: Error) => void;
}

const pending = new Map<number, Pending>();
let nextRequestId = 1;

// 安装全局 resolver（幂等）：原生完成一次调用后经 evaluateJavaScript 调它回交结果。
function installResolver(): void
{
    const w = window as unknown as {
        __cdropEngineResolve?: (id: number, ok: boolean, value: unknown) => void;
    };
    if (w.__cdropEngineResolve) { return; }
    w.__cdropEngineResolve = (id, ok, value) =>
    {
        const p = pending.get(id);
        if (!p) { return; }
        pending.delete(id);
        if (ok) { p.resolve(value); }
        else { p.reject(new Error(typeof value === "string" ? value : "native error")); }
    };
}

// callNative 投递一次 { id, method, payload } 给原生，返回待原生回调 resolve 的
// Promise。桥缺失（非 iOS 壳）直接 reject——调用点应先以 isIOSShell() 守卫。
function callNative<T>(method: string, payload?: unknown): Promise<T>
{
    const h = handler();
    if (!h) { return Promise.reject(new Error("ios bridge unavailable")); }
    installResolver();
    const id = nextRequestId;
    nextRequestId += 1;
    return new Promise<T>((resolve, reject) =>
    {
        pending.set(id, { resolve: resolve as (value: unknown) => void, reject });
        h.postMessage({ id, method, payload });
    });
}

// notifyNative 单向投递一条通知给原生（进度 / 状态 / 就绪 / 错误等，无需回执）。原生
// 据消息形状区分：含 id 为 callNative 请求、含 notify 为单向通知。非 iOS 壳为 no-op，
// 故引擎可无条件调用、无需在每个发射点加 isIOSShell() 分支。
export function notifyNative(name: string, payload?: unknown): void
{
    const h = handler();
    if (!h) { return; }
    h.postMessage({ notify: name, payload });
}

// ── 原生 → JS 事件 ─────────────────────────────────────────────────────────

type EventHandler = (payload: unknown) => void;

const eventHandlers = new Map<string, Set<EventHandler>>();

// 安装全局事件入口（幂等）：原生用 evaluateJavaScript 调它派发事件。
function installEventSink(): void
{
    const w = window as unknown as {
        __cdropEngineEvent?: (name: string, payload: unknown) => void;
    };
    if (w.__cdropEngineEvent) { return; }
    w.__cdropEngineEvent = (name, payload) =>
    {
        const set = eventHandlers.get(name);
        if (!set) { return; }
        for (const fn of set) { fn(payload); }
    };
}

// onNativeEvent 订阅一类原生事件，返回取消订阅函数。非 iOS 壳里仍可调用（只是原生
// 永不派发），保持调用点无需分支。
export function onNativeEvent(name: string, fn: EventHandler): () => void
{
    installEventSink();
    let set = eventHandlers.get(name);
    if (!set)
    {
        set = new Set();
        eventHandlers.set(name, set);
    }
    set.add(fn);
    return () => { set?.delete(fn); };
}

// ── 接收落盘桥（与桌面 desktop.ts 同形，落 iOS 沙盒） ──────────────────────

// bytesToBase64 分块走 btoa，避免一次 String.fromCharCode(...大数组) 触发调用栈溢出。
// 与 desktop.ts 同实现；两边各持一份以保持桥模块互不依赖。
function bytesToBase64(bytes: Uint8Array): string
{
    let binary = "";
    const CHUNK = 0x8000;
    for (let i = 0; i < bytes.length; i += CHUNK)
    {
        binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
    }
    return btoa(binary);
}

// base64ToBytes 把原生回传的 base64（发送侧按 Range 读出的文件片）解回字节。
function base64ToBytes(b64: string): Uint8Array<ArrayBuffer>
{
    const binary = atob(b64);
    const out = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i += 1) { out[i] = binary.charCodeAt(i); }
    return out;
}

// saveIncomingFileIOS：整文件（接收端小文件路径）经 base64 过桥交原生写入沙盒下载
// 目录，返回最终绝对路径。桥缺失或写盘失败抛错，由调用方回退。
export async function saveIncomingFileIOS(name: string, blob: Blob): Promise<string>
{
    const buf = await blob.arrayBuffer();
    return callNative<string>("saveDownload", { name, data: bytesToBase64(new Uint8Array(buf)) });
}

// 流式落盘封装：接收端混合 sink 越过内存上限后，把溢出批次交原生写临时文件，finalize
// 时改名落定。与桌面 Begin/Append/Finalize/Abort 同语义（见 incomingSink.ts）。
export async function beginIncomingDownloadIOS(sessionId: string): Promise<void>
{
    await callNative<void>("beginDownload", { sessionId });
}

export async function appendIncomingDownloadIOS(sessionId: string, bytes: Uint8Array): Promise<void>
{
    await callNative<void>("appendDownload", { sessionId, data: bytesToBase64(bytes) });
}

export async function finalizeIncomingDownloadIOS(sessionId: string, name: string): Promise<string>
{
    return callNative<string>("finalizeDownload", { sessionId, name });
}

export async function abortIncomingDownloadIOS(sessionId: string): Promise<void>
{
    if (!handler()) { return; }
    try { await callNative<void>("abortDownload", { sessionId }); }
    catch { /* 取消 / 失败路径，吞掉 */ }
}

// ── 发送侧文件源（经桥按 Range 取片） ────────────────────────────────────────

// 每次桥取片的上限：把单次 evaluateJavaScript 回传的 base64 串控制在 ~683 KiB（512 KiB×4/3），
// 远小于会卡顿的体量。p2p 一次读 4 MiB、relay 一次 1 MiB，bridgeFileSource 内部按此循环拼齐。
const BRIDGE_SLICE_MAX = 512 * 1024;

// readFileSliceIOS：经桥让原生按 [start, end) seek 读暂存的待发文件、回 base64，再解回字节。
// 取代原先「fetch(cdrop-file://) + Range」——引擎在 https origin 下跨 origin fetch 自定义 scheme
// 被 CORS 拦死（Range 头触发预检、scheme handler 无 CORS 头）→ 发送在读文件即失败。改走桥与
// 接收落盘同形，绕开 CORS；整文件仍永不整体进 WebView 内存（按需取片）。
async function readFileSliceIOS(url: string, start: number, end: number): Promise<Uint8Array<ArrayBuffer>>
{
    const b64 = await callNative<string>("readFileSlice", { url, start, end });
    return base64ToBytes(b64);
}

// bridgeFileSource：iOS 无头引擎的发送字节源（取代 rangeSource 的跨 origin fetch）。slice 把请求
// 区间切成 ≤BRIDGE_SLICE_MAX 的子片逐次过桥取回再拼齐，返回严格等于 [start, end) 的副本，故
// p2p / relay 的块读语义完全不变。
export function bridgeFileSource(url: string, name: string, size: number, type: string): FileSource
{
    return {
        name,
        size,
        type: type || "application/octet-stream",
        async slice(start, end)
        {
            const total = end - start;
            if (total <= 0) { return new Uint8Array(0); }
            const out = new Uint8Array(total);
            let pos = 0;
            while (pos < total)
            {
                const s = start + pos;
                const e = Math.min(s + BRIDGE_SLICE_MAX, end);
                const bytes = await readFileSliceIOS(url, s, e);
                if (bytes.byteLength === 0) { break; }   // 防御：原生异常返回空，避免死循环
                out.set(bytes.subarray(0, Math.min(bytes.byteLength, total - pos)), pos);
                pos += bytes.byteLength;
            }
            return out;
        },
    };
}
