// 接收端的字节落地槽，用来避免把整个文件累积到 JS 堆。
//
// 三条路径，按运行环境选：
//   - 浏览器（含 iOS PWA）：流式写 Origin Private File System（OPFS），内存只占
//     一个 chunk。iOS Safari 单 tab 内存预算紧，超过即被系统节流 / 暂停 JS → SCTP
//     不再 ack → sender bufferedAmount 撑满死锁（实测 24.4 MB 卡住的根因），故必须
//     流式。Safari 16+ / Chrome 102+ / Firefox 111+ 都已支持 createWritable。
//   - 桌面（Wails WKWebView）：混合有界内存槽。wails:// 自定义 scheme 无持久 WebView
//     存储，OPFS 在此降级、createWritable().write() 慢且回压不畅，会把接收 promise
//     链堵住、拖垮主线程到 dc.onmessage 饿死 → 接收方 SCTP rwnd=0 → 发送方
//     bufferedAmount 卡死在 16 MiB → 传输死锁（实测 PWA→Mac 约 15 MB 卡住）。故桌面
//     不碰 OPFS：小文件（暂存 < CAP）纯内存累积、close 时整文件过 Go 写盘；越过内存
//     上限（CAP=256 MB）的大文件改为流式 spill 到 Go 临时文件（每 64 MB 一批），内存
//     恒定有界。桌面 RAM 充裕，整体不会撑爆堆，又彻底避开 OPFS 那条饿死链路。
//   - 老浏览器 / OPFS 不可用：退回纯内存累积。
//
// close() 返回 SinkResult：浏览器 / 桌面小文件返回 Blob（走 downloadBlob——浏览器
// 原生下载 / 桌面整文件过 Go 写盘）；桌面已 spill 的大文件返回已落盘绝对路径（文件
// 已在 Go 侧写好，跳过 Blob）。

import { t } from "../../i18n";
import {
    abortIncomingDownloadDesktop,
    appendIncomingDownloadDesktop,
    beginIncomingDownloadDesktop,
    finalizeIncomingDownloadDesktop,
    isDesktop,
    saveIncomingFileDesktop,
} from "../../net/desktop";
import { toast } from "../../ui/feedback";

// close() 两态结果：blob＝整文件在内存，交给 downloadBlob；saved＝桌面大文件已流式
// 落盘，path 是最终绝对路径（无需再过 Blob）。
export type SinkResult =
    | { kind: "blob"; blob: Blob }
    | { kind: "saved"; path: string };

export interface IncomingSink
{
    /** 顺序写一个 chunk；返回的 Promise 只有完全落盘后才 resolve，自然 backpressure */
    write(chunk: ArrayBuffer): Promise<void>;
    /** 收到 done 帧后调用：finalize 并返回最终结果（Blob 或已落盘路径） */
    close(): Promise<SinkResult>;
    /** 失败 / 取消路径上调用，确保 OPFS / 流式临时文件被删 */
    cancel(): Promise<void>;
}

// 桌面混合 sink 的内存上限与 spill 批大小：暂存达 CAP 即转流式落盘，之后每满一批刷。
const DESKTOP_MEMORY_CAP = 256 * 1024 * 1024;
const DESKTOP_FLUSH_BATCH = 64 * 1024 * 1024;

/**
 * 按环境选 sink：桌面走混合有界内存槽（见文件头注释），浏览器优先 OPFS、失败回退
 * 内存。sessionId 用作 OPFS 临时文件名 / 桌面流式落盘 key（确保并发会话不撞文件）；
 * fileName 供桌面 spill 落盘后命名（仅大文件路径用到）。
 */
export async function openIncomingSink(sessionId: string, fileName: string): Promise<IncomingSink>
{
    if (isDesktop())
    {
        return openHybridDesktopSink(sessionId, fileName);
    }
    // 注意：直接 navigator.storage?.getDirectory 在 TS 下永远 truthy（optional
    // chaining 取的是函数引用，不会调用）。要真探测必须 try-catch 调用。
    if (typeof navigator !== "undefined" && navigator.storage)
    {
        try { return await openOPFSSink(sessionId); }
        catch (e)
        {
            // eslint-disable-next-line no-console
            console.warn("OPFS unavailable, falling back to in-memory sink", e);
        }
    }
    return openMemorySink();
}

/**
 * 桌面混合有界内存槽。小文件（暂存 < CAP）纯内存累积，close 时返回 Blob 交给
 * downloadBlob（= 既有桌面整文件过 Go 写盘路径）；一旦暂存越过 CAP，转流式落盘——
 * 把已暂存的全部按 ≤批大小刷给 Go、释放内存，之后每满一批再刷，内存恒定有界。
 *
 * write() 由调用方（p2p / relay 的 receiveQueue promise 链）严格串行调用，故内部
 * 无需再加锁。
 */
function openHybridDesktopSink(sessionId: string, fileName: string): IncomingSink
{
    let pending: ArrayBuffer[] = [];
    let pendingBytes = 0;
    let spilled = false;
    let closed = false;

    // 从 pending 头部取出至多 DESKTOP_FLUSH_BATCH 字节拼成一块 append 给 Go。
    // drainAll=true 把剩余全部刷完（spill 起始 / close）；否则只在满一批时刷。
    const flush = async (drainAll: boolean): Promise<void> =>
    {
        while (pendingBytes >= DESKTOP_FLUSH_BATCH || (drainAll && pendingBytes > 0))
        {
            let take = 0;
            const batch: ArrayBuffer[] = [];
            while (pending.length > 0
                && (take === 0 || take + pending[0].byteLength <= DESKTOP_FLUSH_BATCH))
            {
                const c = pending.shift() as ArrayBuffer;
                batch.push(c);
                take += c.byteLength;
            }
            pendingBytes -= take;
            await appendIncomingDownloadDesktop(sessionId, concatChunks(batch, take));
        }
    };

    return {
        async write(chunk)
        {
            if (closed) { return; }
            // 复制一份：底层 ArrayBuffer 来自 dc.onmessage 的 event.data，异步链上
            // 保留独立副本避免被回收 / 复用。
            pending.push(chunk.slice(0));
            pendingBytes += chunk.byteLength;

            if (!spilled)
            {
                if (pendingBytes >= DESKTOP_MEMORY_CAP)
                {
                    // 越过内存上限：开始流式落盘，把已暂存的全部刷给 Go，转滚动批。
                    spilled = true;
                    await beginIncomingDownloadDesktop(sessionId);
                    await flush(true);
                }
                return;
            }
            await flush(false);
        },
        async close()
        {
            if (closed) { throw new Error("sink already closed"); }
            closed = true;
            if (!spilled)
            {
                // 小文件：从未落盘，整文件还在内存 → Blob 交给 downloadBlob。
                return { kind: "blob", blob: new Blob(pending) };
            }
            await flush(true);
            const path = await finalizeIncomingDownloadDesktop(sessionId, fileName);
            return { kind: "saved", path };
        },
        async cancel()
        {
            closed = true;
            pending = [];
            pendingBytes = 0;
            if (spilled) { await abortIncomingDownloadDesktop(sessionId); }
        },
    };
}

// concatChunks 把若干 ArrayBuffer 拼成一个 Uint8Array（供 base64 过桥）。
function concatChunks(parts: ArrayBuffer[], totalBytes: number): Uint8Array
{
    const out = new Uint8Array(totalBytes);
    let off = 0;
    for (const p of parts)
    {
        out.set(new Uint8Array(p), off);
        off += p.byteLength;
    }
    return out;
}

async function openOPFSSink(sessionId: string): Promise<IncomingSink>
{
    const dir = await navigator.storage.getDirectory();
    const fileName = `cdrop-${sessionId}`;
    const handle = await dir.getFileHandle(fileName, { create: true });
    // FileSystemWritableFileStream：异步写、支持流式追加（每次 write 都是
    // 顺序写到当前位置）。Safari 16+ / Chrome 102+ / Firefox 111+。
    const writable = await handle.createWritable();
    let closed = false;

    const cleanup = async () =>
    {
        try { await dir.removeEntry(fileName); } catch { /* 已删 / 不存在 */ }
    };

    return {
        async write(chunk)
        {
            if (closed) { return; }
            await writable.write(chunk);
        },
        async close()
        {
            if (closed) { throw new Error("sink already closed"); }
            closed = true;
            await writable.close();
            const file = await handle.getFile();
            // 文件在 OPFS 里仍占空间；download 触发后再删（finalizeIncoming 里调）
            return {
                kind: "blob",
                blob: new Blob([file], { type: file.type || "application/octet-stream" }),
            };
        },
        async cancel()
        {
            if (closed) { await cleanup(); return; }
            closed = true;
            try { await writable.close(); } catch { /* writable 已无效 */ }
            await cleanup();
        },
    };
}

function openMemorySink(): IncomingSink
{
    const chunks: BlobPart[] = [];
    let closed = false;
    return {
        async write(chunk)
        {
            if (closed) { return; }
            // 复制一份保险：底层 ArrayBuffer 来自 dc.onmessage 的 event.data，
            // 同步处理时引用安全；异步链上保留独立副本避免被回收。
            chunks.push(chunk.slice(0));
        },
        async close()
        {
            closed = true;
            return { kind: "blob", blob: new Blob(chunks) };
        },
        async cancel()
        {
            closed = true;
            chunks.length = 0;
        },
    };
}

// deliverIncoming 把 sink.close() 的结果落到用户：已落盘（桌面大文件）只提示路径，
// 文件已在 Go 侧写好；Blob（浏览器 / 桌面小文件）交给 downloadBlob（浏览器原生下载
// / 桌面整文件过 Go 写盘）。p2p / relay 的接收完成路径都走这里。
export function deliverIncoming(result: SinkResult, fileName: string, sessionId: string): void
{
    if (result.kind === "saved")
    {
        toast.ok(t("transfer.savedTo", { path: result.path }));
        return;
    }
    downloadBlob(result.blob, fileName, sessionId);
}

/**
 * 触发浏览器下载并把 OPFS 临时文件 + ObjectURL 都安排清理。
 * 60s 给浏览器把 blob 落到用户磁盘的窗口，过短可能导致下载读到一半源被删。
 */
export function downloadBlob(blob: Blob, fileName: string, sessionId?: string): void
{
    // 桌面端把字节交给 Go 写入用户配置的下载目录（路径可配置）；浏览器端走原生下载。
    if (isDesktop())
    {
        void saveIncomingFileDesktop(fileName, blob)
            .then((savedPath) =>
            {
                toast.ok(t("transfer.savedTo", { path: savedPath }));
                if (sessionId) { void cleanupOPFS(sessionId); }
            })
            .catch((e) =>
            {
                // 桌面 WebView 没有可用的浏览器下载回退（WKWebView 不处理 a[download]
                // 的 blob），所以写盘失败必须显式报错，绝不能静默丢文件。Go 侧已在配置
                // 目录不可写时自动改写系统下载目录，能走到这里说明连默认目录也失败了。
                // eslint-disable-next-line no-console
                console.error("desktop save failed", e);
                toast.error(t("transfer.saveFailed", { error: e instanceof Error ? e.message : String(e) }));
                if (sessionId) { void cleanupOPFS(sessionId); }
            });
        return;
    }
    browserDownload(blob, fileName, sessionId);
}

function browserDownload(blob: Blob, fileName: string, sessionId?: string): void
{
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = fileName;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    setTimeout(() =>
    {
        URL.revokeObjectURL(url);
        if (sessionId) { void cleanupOPFS(sessionId); }
    }, 60_000);
}

async function cleanupOPFS(sessionId: string): Promise<void>
{
    if (typeof navigator === "undefined" || !navigator.storage?.getDirectory) { return; }
    try
    {
        const dir = await navigator.storage.getDirectory();
        await dir.removeEntry(`cdrop-${sessionId}`);
    }
    catch { /* 已经清掉或没创建过 */ }
}
