// Wails 桌面壳的桥接层：仅在桌面 WebView 中可用。浏览器里 isDesktop() 返回
// false，所有调用点都回退到既有的 Web 行为，因此本模块对纯 Web 构建零影响
// ——同一份代码同时部署到浏览器与桌面。
import { useAppStore, type User } from "../store";

// 桌面会话视图：不含 refresh_token——长寿命密钥只在 Go 进程，绝不进 JS（凭据策略
// 见 desktop/platform/session.go）。登录 emit、刷新返回、启动注入都用这个形状。
interface DesktopSession
{
    access_token: string;
    expires_in: number;
    user: { id: string; name: string; avatar?: string };
}

// Wails v2 注入的全局运行时：事件订阅在此。
interface WailsRuntime
{
    EventsOn: (event: string, cb: (...data: unknown[]) => void) => () => void;
}

// 桌面本地配置（~/Library/Application Support/cdrop/config.json）。字段与 Go
// platform.DesktopConfig 的 json tag 一致。
export interface DesktopConfig
{
    clipboard_sync_enabled: boolean;
    launch_at_login: boolean;
    download_dir: string; // 空＝系统下载目录（Go 侧 ResolveDownloadDir 解析）
}

// Go 侧 App 结构体导出的方法（window.go.main.App.*）。
interface DesktopBridge
{
    StartLogin: () => void;
    Refresh: () => Promise<DesktopSession>;
    DeviceName: () => Promise<string>;
    ApplyRemoteClipboard: (content: string, sourceDevice: string) => Promise<void>;
    GetSettings: () => Promise<DesktopConfig>;
    SaveSettings: (cfg: DesktopConfig) => Promise<void>;
    ClearSession: () => Promise<void>;
    SetDeviceName: (name: string) => Promise<void>;
    // data 为 base64：Wails 把 JS 字符串作为 JSON 字符串编码，Go 端 []byte 参数按
    // base64 解码。返回实际写入的绝对路径。
    SaveDownload: (name: string, data: string) => Promise<string>;
    // 流式落盘（接收端混合 sink 越过内存上限后走这条）：Begin 开临时文件、Append
    // 追加批次（base64）、Finalize 改名落定返回绝对路径、Abort 取消时丢弃临时文件。
    BeginDownload: (sessionId: string) => Promise<void>;
    AppendDownload: (sessionId: string, data: string) => Promise<void>;
    FinalizeDownload: (sessionId: string, name: string) => Promise<string>;
    AbortDownload: (sessionId: string) => Promise<void>;
    ChooseDownloadDir: (title: string) => Promise<string>;
    EffectiveDownloadDir: () => Promise<string>;
    // 弹原生系统通知（macOS UNUserNotificationCenter / Windows toast）。标题、正文
    // 已由前端本地化。
    ShowNotification: (title: string, body: string) => Promise<void>;
}

// 桌面剪贴板自动同步开关，默认开；由设置页通过 setClipboardSyncEnabled 调整。
// 同时门控上行（原生复制 → 上传）与下行（云端更新 → 写回原生），关闭即两端静默。
let clipboardSyncEnabled = true;

export function setClipboardSyncEnabled(enabled: boolean): void
{
    clipboardSyncEnabled = enabled;
}

export function isClipboardSyncEnabled(): boolean
{
    return clipboardSyncEnabled;
}

function runtime(): WailsRuntime | null
{
    const rt = (window as unknown as { runtime?: WailsRuntime }).runtime;
    return rt ?? null;
}

function bridge(): DesktopBridge | null
{
    const go = (window as unknown as { go?: { main?: { App?: DesktopBridge } } }).go;
    return go?.main?.App ?? null;
}

// isDesktop 同时要求运行时与绑定都在场，避免半初始化状态下误判。
export function isDesktop(): boolean
{
    return typeof window !== "undefined" && runtime() !== null && bridge() !== null;
}

function toUser(u: DesktopSession["user"]): User
{
    return { id: u.id, name: u.name, avatar: u.avatar || undefined };
}

// startDesktopLogin 触发 Go 侧 loopback PKCE 流程，等待 oauth:success / oauth:error
// 事件；成功后把 token + user 写进 store，并以主机名补齐设备名（跳过 /setup）。
export async function startDesktopLogin(): Promise<void>
{
    const rt = runtime();
    const app = bridge();
    if (!rt || !app) { throw new Error("desktop bridge unavailable"); }

    await new Promise<void>((resolve, reject) =>
    {
        let offOk: (() => void) | null = null;
        let offErr: (() => void) | null = null;
        const cleanup = () =>
        {
            if (offOk) { offOk(); }
            if (offErr) { offErr(); }
        };
        offOk = rt.EventsOn("oauth:success", (...data) =>
        {
            cleanup();
            const res = data[0] as DesktopSession;
            // 不带 refreshToken：桌面的 refresh_token 只在 Go，JS 永不持有。
            useAppStore.getState().setAuth({
                accessToken: res.access_token,
                user: toUser(res.user),
            });
            resolve();
        });
        offErr = rt.EventsOn("oauth:error", (...data) =>
        {
            cleanup();
            const detail = (data[0] as { detail?: string })?.detail ?? "登录失败";
            reject(new Error(detail));
        });
        app.StartLogin();
    });

    await ensureDeviceName();
}

// ensureDeviceName 在桌面端用主机名补齐 selfDeviceName（仅当尚未命名），让路由
// 守卫不再跳转 /setup。失败静默——用户仍可在设置里手动命名。
export async function ensureDeviceName(): Promise<void>
{
    const app = bridge();
    if (!app) { return; }
    if (useAppStore.getState().selfDeviceName) { return; }
    try
    {
        const name = await app.DeviceName();
        if (name) { useAppStore.getState().setSelfDeviceName(name); }
    }
    catch { /* 保留为空，回退到 /setup 手动命名 */ }
}

// notifyNativeIfBackground：桌面端窗口非前台时，弹一条原生系统通知。窗口在前台
// （可见且聚焦）时为 no-op——此刻应用内 UI 已呈现该事件，符合「页面打开用应用内
// 提示，否则系统通知」。浏览器里 isDesktop()=false，始终 no-op（Web 的后台通知走
// 服务端 Web Push）。title / body 须由调用方先用 t() 本地化。
export function notifyNativeIfBackground(title: string, body: string): void
{
    const app = bridge();
    if (!isDesktop() || !app) { return; }
    const foreground = typeof document !== "undefined"
        && document.visibilityState === "visible"
        && document.hasFocus();
    if (foreground) { return; }
    void app.ShowNotification(title, body).catch(() => { /* 通知失败不影响主流程 */ });
}

// initDesktopMenuBridge 把 Go 菜单栏发来的事件接到前端（如「设置」→ 路由跳转）。
// 仅桌面端生效；浏览器里 isDesktop()=false 直接返回，对 Web 零影响。
export function initDesktopMenuBridge(onSettings: () => void): void
{
    const rt = runtime();
    if (!isDesktop() || !rt) { return; }
    rt.EventsOn("menu:settings", () => onSettings());
}

// onNativeClipboard 订阅 Go 的本机剪贴板复制事件（上行）。回调里上层决定是否
// 上传（既有 uploadClipboard 链路：鉴权 + 去重 + store）。同步关闭时不回调。
// 返回取消订阅函数；浏览器里为 no-op。
export function onNativeClipboard(handler: (text: string) => void): () => void
{
    const rt = runtime();
    if (!isDesktop() || !rt) { return () => { /* no-op */ }; }
    return rt.EventsOn("clipboard:native", (...data) =>
    {
        if (!clipboardSyncEnabled) { return; }
        const text = typeof data[0] === "string" ? data[0] : "";
        if (text) { handler(text); }
    });
}

// writeNativeClipboard 把来自其他设备的云端更新写回本机原生剪贴板（下行）。
// 仅桌面 + 同步开启 + 非空时触发；自身来源由上层与 Go 侧 Monitor 双重过滤。
export function writeNativeClipboard(content: string, sourceDevice: string): void
{
    const app = bridge();
    if (!isDesktop() || !app || !clipboardSyncEnabled || !content) { return; }
    void app.ApplyRemoteClipboard(content, sourceDevice);
}

// loadDesktopSettings 读取本地配置并把剪贴板同步开关应用到本模块的门控。
// 返回配置供设置页初始化；浏览器 / 失败时返回 null。应在应用启动时调用一次。
export async function loadDesktopSettings(): Promise<DesktopConfig | null>
{
    const app = bridge();
    if (!app) { return null; }
    try
    {
        const cfg = await app.GetSettings();
        setClipboardSyncEnabled(cfg.clipboard_sync_enabled);
        return cfg;
    }
    catch
    {
        return null;
    }
}

// saveDesktopSettings 持久化配置并立即应用同步开关（launch-at-login 的副作用在
// Go 侧落地）。抛错交由设置页提示。
export async function saveDesktopSettings(cfg: DesktopConfig): Promise<void>
{
    const app = bridge();
    if (!app) { return; }
    setClipboardSyncEnabled(cfg.clipboard_sync_enabled);
    await app.SaveSettings(cfg);
}

// clearDesktopSession 登出时让 Go 删除持久化的 session 文件，使下次启动不再注入
// 已登录态（fire-and-forget）。浏览器里 bridge() 为 null，直接返回。
export function clearDesktopSession(): void
{
    const app = bridge();
    if (!app) { return; }
    void app.ClearSession();
}

// persistDesktopDeviceName 让 Go 持久化重命名后的设备名（写入本地配置），使其跨
// 重启存活并随启动注入；浏览器为 no-op。fire-and-forget。
export function persistDesktopDeviceName(name: string): void
{
    const app = bridge();
    if (!app) { return; }
    void app.SetDeviceName(name);
}

// desktopRefresh 触发 Go 内部刷新（无参——refresh_token 只在 Go，JS 不传也不持有）。
// Go 用自己的 refresh_token 换新 access_token（public-client，不带 secret），返回
// 不含 refresh_token 的会话视图；失败返回 null 让上层照旧清登录态。
export async function desktopRefresh(): Promise<DesktopSession | null>
{
    const app = bridge();
    if (!app) { return null; }
    try
    {
        return await app.Refresh();
    }
    catch
    {
        return null;
    }
}

// bytesToBase64 分块走 btoa，避免一次 String.fromCharCode(...大数组) 触发调用栈溢出。
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);
}

// saveIncomingFileDesktop 把接收到的文件交给 Go 写入配置的下载目录（绕开 WebView
// 默认下载，使路径可配置）。返回实际写入的绝对路径；桥接缺失或写盘失败时抛错，
// 由调用方回退到浏览器下载。整文件经 base64 过桥——大小折中见落盘机制选型。
export async function saveIncomingFileDesktop(name: string, blob: Blob): Promise<string>
{
    const app = bridge();
    if (!app) { throw new Error("desktop bridge unavailable"); }
    const buf = await blob.arrayBuffer();
    const b64 = bytesToBase64(new Uint8Array(buf));
    return app.SaveDownload(name, b64);
}

// 流式落盘封装：接收端混合 sink 越过内存上限后，把溢出批次交给 Go 写入临时文件，
// finalize 时改名落定。begin / finalize 失败抛错（由 sink 走失败路径）；append 在
// 批次边界调用，base64 过桥同 SaveDownload；abort 吞错（取消路径不应再抛）。
export async function beginIncomingDownloadDesktop(sessionId: string): Promise<void>
{
    const app = bridge();
    if (!app) { throw new Error("desktop bridge unavailable"); }
    await app.BeginDownload(sessionId);
}

export async function appendIncomingDownloadDesktop(sessionId: string, bytes: Uint8Array): Promise<void>
{
    const app = bridge();
    if (!app) { throw new Error("desktop bridge unavailable"); }
    await app.AppendDownload(sessionId, bytesToBase64(bytes));
}

export async function finalizeIncomingDownloadDesktop(sessionId: string, name: string): Promise<string>
{
    const app = bridge();
    if (!app) { throw new Error("desktop bridge unavailable"); }
    return app.FinalizeDownload(sessionId, name);
}

export async function abortIncomingDownloadDesktop(sessionId: string): Promise<void>
{
    const app = bridge();
    if (!app) { return; }
    try { await app.AbortDownload(sessionId); }
    catch { /* 取消 / 失败路径，吞掉 */ }
}

// chooseDownloadDir 打开原生目录选择框（标题本地化由调用方传入），返回所选目录；
// 用户取消返回空串。浏览器里 bridge() 为 null，返回空串。
export async function chooseDownloadDir(title: string): Promise<string>
{
    const app = bridge();
    if (!app) { return ""; }
    try
    {
        return await app.ChooseDownloadDir(title);
    }
    catch
    {
        return "";
    }
}

// effectiveDownloadDir 返回当前实际落盘目录（配置覆盖或系统默认），供设置页显示。
export async function effectiveDownloadDir(): Promise<string>
{
    const app = bridge();
    if (!app) { return ""; }
    try
    {
        return await app.EffectiveDownloadDir();
    }
    catch
    {
        return "";
    }
}
