// cdrop 无头传输引擎入口（arch A，见 ios/PLAN.md §2 / §6）。
//
// 不加载 React / 路由 / UI——只复用 web 的传输引擎（P2P + relay + 信令 + 会话），由原生
// iOS 壳经桥（net/ios.ts）驱动。原生在加载本 bundle 前经 WKUserScript 注入
// window.__CDROP_BOOT__（session / device_name / api_base / device_type:"ios"），store 在
// import 时即从中水合（见 store/helpers.ts）。本页须从 https 安全源加载——WebRTC 的
// RTCPeerConnection 在非安全 origin 下可能被禁（见 ios/PLAN.md R-iOS-3）。
//
// 命令（原生 → 引擎，经 onNativeEvent）：
//   sendFile { target, url, name, size, type? } —— 取回 url 指向的待发文件并发起传输
//   cancelTransfer { sessionId }
//   switchToRelay { sessionId } —— 手动切中继
//   session { access_token, refresh_token?, user } —— 原生重登 / 续期后推新令牌对
//   shutdown —— 断开 hub、停 ICE 刷新
// 通知（引擎 → 原生，经 notifyNative）：
//   ready { device } / presence { devices[] } / transfers { active[] } / transferDone {...}
//   / sendStarted {...} / error { stage, message }
//   sessionRotated { access_token, refresh_token } —— 引擎自刷致 broker 轮换后，回报原生
//     更新 Keychain（重启免用失效旧 refresh）
//   authExpired {} —— 续期被 401 拒（refresh 过期 / 吊销），原生清 Keychain 回登录页

import { refreshSessionScope } from "../features/auth/auth";
import { fetchClipboard, uploadClipboard } from "../features/clipboard/clipboard";
import { sendMessage } from "../features/messaging/messaging";
import { apiFetch } from "../net/api";
import { startHub } from "../features/transfer/hub";
import { refreshICEServers, stopICEServerRefresh } from "../features/transfer/iceServers";
import { cancelTransfer, skipWaitRelay, startOutgoingTransfer } from "../features/transfer/transfer";
import { bridgeFileSource, isIOSShell, notifyNative, onNativeEvent } from "../net/ios";
import { qrApprove, qrDeny, qrRequest } from "../net/qr";
import { useAppStore } from "../store";
import type { TransferRecord } from "../store/types";

interface SendFilePayload
{
    target: string;
    url: string;
    name: string;
    size: number;
    type?: string;
}

// 推给原生的精简传输视图：去掉 iceStats 大对象，只留原生 UI 需要的字段 + 一条压缩的
// ICE 摘要（连接态 + 选中候选对），供详情页诊断 P2P 是否走 TURN 中继（pair 含 "relay"
// 即中继路径，解释慢速）。
function toWire(r: TransferRecord)
{
    const ice = r.iceStats;
    return {
        sessionId:        r.sessionId,
        direction:        r.direction,
        fileName:         r.fileName,
        fileSize:         r.fileSize,
        state:            r.state,
        mode:             r.mode,
        peerName:         r.peerName,
        phase:            r.phase,
        bytesTransferred: r.bytesTransferred,
        bytesPerSec:      r.bytesPerSec,
        ice: ice
            ? {
                conn:   ice.connection,
                local:  ice.selectedPair?.local,
                remote: ice.selectedPair?.remote,
            }
            : undefined,
    };
}

// hub 的生命周期句柄；shutdown 命令据此断开 SSE。
const hubCtrl = new AbortController();

// 活跃传输快照推送节流：接收端每个 64KB chunk 都会更新 store（emitProgress），若每次都
// 过桥推原生，会(1)让速度数字 ~10Hz 闪烁失去参考价值、(2)主线程被 postMessage/序列化抖
// 动堆满拖慢 dc.onmessage（见 p2p.ts 收方看门狗注释）→ 反噬 P2P 吞吐。故合并到 ~3Hz
// 尾沿推送：进度条仍流畅、速度可读，主线程负载大幅下降。终态另由 transferDone 即时推。
const TRANSFERS_MIN_INTERVAL_MS = 350;
let transfersTimer: ReturnType<typeof setTimeout> | null = null;
let pendingActive: unknown[] | null = null;
let lastTransfersAt = 0;

function pushTransfersThrottled(active: unknown[]): void
{
    pendingActive = active;
    if (transfersTimer !== null) { return; }
    const wait = Math.max(0, TRANSFERS_MIN_INTERVAL_MS - (Date.now() - lastTransfersAt));
    transfersTimer = setTimeout(() =>
    {
        transfersTimer = null;
        lastTransfersAt = Date.now();
        notifyNative("transfers", { active: pendingActive });
        pendingActive = null;
    }, wait);
}

// 订阅 store：activeTransfers 引用变化推快照、history 头部新增推完成事件、devices 引用
// 变化推在线列表。原生据 sessionId / name 幂等更新自己的 UI。setDevices / setActive 每次
// 都换数组 / 对象引用，故 !== 即可判变更。
function subscribeStore(): void
{
    const st0 = useAppStore.getState();
    let prevActive = st0.activeTransfers;
    let prevDoneId = st0.history[0]?.sessionId;
    let prevDevices = st0.devices;
    let prevMsgId = st0.messages[0]?.id;
    let prevSse = st0.sseConnected;
    let prevAuthed = st0.accessToken !== null;
    let prevRefresh = st0.refreshToken;
    let presenceCount = 0;

    // 把 SSE 连接态 + 收到的 presence 计数推给原生，供设置页诊断「设备空」到底卡在哪：
    // SSE 未连接＝鉴权/连接问题；已连接但 presence=0＝后端没发；presence>0 但列表空＝原生侧
    // （已用本地隔离证伪——原生侧 OK）。
    const pushHubState = (s: ReturnType<typeof useAppStore.getState>): void =>
    {
        notifyNative("hubState", {
            connected: s.sseConnected,
            reconnecting: s.sseReconnecting,
            presenceCount,
            devices: s.devices.length,
        });
    };

    useAppStore.subscribe((s) =>
    {
        if (s.activeTransfers !== prevActive)
        {
            prevActive = s.activeTransfers;
            pushTransfersThrottled(Object.values(s.activeTransfers).map(toWire));
        }
        const head = s.history[0];
        if (head && head.sessionId !== prevDoneId)
        {
            prevDoneId = head.sessionId;
            notifyNative("transferDone", toWire(head));
        }
        if (s.devices !== prevDevices)
        {
            prevDevices = s.devices;
            presenceCount += 1;
            notifyNative("presence", { devices: s.devices });
            pushHubState(s);
        }
        // 设备间文本消息：messages 最新在前（addMessage 前插），每次新增就把新的头条作为「增量」
        // 推原生（而非整列）。原生持久化记录、按 id 去重前插——单条增量避免引擎重启后空列表整组
        // 覆盖掉原生已持久化的历史消息（重启清空问题的根因）。messages 由 hub.ts handleIncomingMessage
        // （入）与 sendMessage（出）维护。
        const headMsg = s.messages[0];
        if (headMsg && headMsg.id !== prevMsgId)
        {
            prevMsgId = headMsg.id;
            notifyNative("message", { message: headMsg });
        }
        if (s.sseConnected !== prevSse)
        {
            prevSse = s.sseConnected;
            pushHubState(s);
        }
        // 引擎自刷（apiFetch 401 → refreshTokens 用注入的 refresh_token 经 /api/auth/refresh
        // 续期）后 broker 轮换了 refresh_token。把新令牌对回报原生更新 Keychain，使重启后不再
        // 用已轮换失效的旧 refresh。
        if (s.refreshToken !== prevRefresh && s.refreshToken)
        {
            prevRefresh = s.refreshToken;
            notifyNative("sessionRotated", {
                access_token: s.accessToken,
                refresh_token: s.refreshToken,
            });
        }
        // 续期被服务端以 401 拒绝（refresh_token 已过期 / 被吊销）后 forceLogout 清空登录态
        // → 通知原生清 Keychain 回登录页，避免「貌似已登录但拉不到任何数据」的死态。仅在确证
        // 失效（非瞬时）时触发。
        const authed = s.accessToken !== null;
        if (prevAuthed && !authed)
        {
            notifyNative("authExpired", {});
        }
        prevAuthed = authed;
    });
}

// handleSendFile：原生把待发文件经 payload.url（cdrop-file://<id>）暴露。R-iOS-4：用
// bridgeFileSource 经桥按块取片（原生据 [start,end) seek 文件、回 base64），整文件永不整体进
// WebView 内存——大文件不再有 jetsam 风险，传输层 p2p / relay 透明复用。改走桥而非跨 origin
// fetch，绕开自定义 scheme 的 CORS 拦截（否则发送在读文件即失败）。
async function handleSendFile(p: SendFilePayload): Promise<void>
{
    try
    {
        const src = bridgeFileSource(p.url, p.name, p.size, p.type ?? "");
        const sessionId = await startOutgoingTransfer(p.target, src);
        notifyNative("sendStarted", { sessionId, name: p.name });
    }
    catch (e)
    {
        notifyNative("error", { stage: "send", message: e instanceof Error ? e.message : String(e) });
    }
}

// bindCommands：把原生命令接到引擎函数。
function bindCommands(): void
{
    onNativeEvent("sendFile", (payload) =>
    {
        void handleSendFile(payload as SendFilePayload);
    });
    onNativeEvent("cancelTransfer", (payload) =>
    {
        void cancelTransfer((payload as { sessionId: string }).sessionId);
    });
    onNativeEvent("switchToRelay", (payload) =>
    {
        void skipWaitRelay((payload as { sessionId: string }).sessionId);
    });
    // 原生 Keychain 续期后推来的新会话令牌 → 写回 store，让 apiFetch 用新令牌（iOS 无
    // 浏览器 cookie / Go 进程，token 续期由原生侧持有 refresh_token 后驱动）。
    onNativeEvent("session", (payload) =>
    {
        const p = payload as {
            access_token?: string;
            refresh_token?: string;
            user?: { id: string; name: string; avatar?: string };
        };
        if (p.access_token && p.user)
        {
            useAppStore.getState().setAuth({
                accessToken: p.access_token,
                refreshToken: p.refresh_token ?? null,
                user: p.user,
            });
        }
    });
    // 剪贴板上行：原生读 UIPasteboard 后把文本送来，经云剪贴板上传（PUT /api/clipboard）。
    onNativeEvent("clipboardUpload", (payload) =>
    {
        const content = (payload as { content?: string }).content ?? "";
        if (!content) { return; }
        void uploadClipboard(content)
            .then(() => notifyNative("clipboardUploaded", {}))
            .catch((e) => notifyNative("error", { stage: "clipboard", message: String(e) }));
    });
    // 剪贴板下行：拉一次云剪贴板，把最新内容推回原生写入 UIPasteboard。
    onNativeEvent("clipboardPull", () =>
    {
        void fetchClipboard()
            .then(() =>
            {
                const c = useAppStore.getState().clipboard;
                notifyNative("clipboard", { content: c?.content ?? "", sourceDevice: c?.sourceDevice ?? "" });
            })
            .catch((e) => notifyNative("error", { stage: "clipboard", message: String(e) }));
    });
    // 设备管理：移除 / 吊销一台设备（DELETE /api/devices/{device_id}）。按稳定 device_id（路由
    // 参数即 device_id），原生从 presence 的 device_id 传来——之前误传设备名致后端按 device_id
    // 找不到而「移除失败」。需完整会话；403（step-up）回错给原生提示「请在网页端完成」。
    onNativeEvent("revokeDevice", (payload) =>
    {
        const p = payload as { device_id?: string; name?: string };
        const id = p.device_id ?? "";
        if (!id) { return; }
        void revokeDevice(id);
    });
    // 设备间文本消息（发送）：原生把 { to=对端设备名, text } 送来，经 POST /api/message 转发（hub
    // SendTo 按名解析到对端 live 连接）。发送成功后 store.addMessage 会触发上面的 message 订阅回推。
    onNativeEvent("sendMessage", (payload) =>
    {
        const p = payload as { to?: string; text?: string };
        if (!p.to || !p.text) { return; }
        void sendMessage(p.to, p.text)
            .catch((e) => notifyNative("error", { stage: "message", message: e instanceof Error ? e.message : String(e) }));
    });
    // 本机即时改名（设备名与令牌解耦）：原生送来 { device_id=本机, name }，经 PATCH /api/devices/
    // {device_id} 只改名字、不换 token、不重连——后端按 device_id 改名 + 重广播 presence。成功后
    // 更新引擎 selfDeviceName（后续 X-Device-Name 用新名），并回报原生更新本地名 / Keychain。
    onNativeEvent("renameSelf", (payload) =>
    {
        const p = payload as { device_id?: string; name?: string };
        if (!p.device_id || !p.name) { return; }
        void renameSelf(p.device_id, p.name);
    });
    // 扫码登录他端（#8）：原生（已登录）扫到另一台设备的登录二维码后，送来 { request_id, code }。
    // 先拉请求信息回报原生供安全确认，再据原生指令批准 / 拒绝（qr.ts，走 apiFetch 带本机 Bearer）。
    onNativeEvent("fetchLoginRequest", (payload) =>
    {
        const p = payload as { request_id?: string; code?: string };
        if (!p.request_id || !p.code) { return; }
        void fetchLoginRequest(p.request_id, p.code);
    });
    onNativeEvent("approveLogin", (payload) =>
    {
        const p = payload as { request_id?: string; code?: string };
        if (!p.request_id || !p.code) { return; }
        void approveLogin(p.request_id, p.code);
    });
    onNativeEvent("denyLogin", (payload) =>
    {
        const p = payload as { request_id?: string; code?: string };
        if (!p.request_id || !p.code) { return; }
        void denyLogin(p.request_id, p.code);
    });
    // APNs 设备令牌登记：原生（AppDelegate）拿到令牌经桥送来，引擎用新鲜会话 token 上报后端。
    onNativeEvent("registerPush", (payload) =>
    {
        const p = payload as { token?: string; locale?: string };
        if (!p.token) { return; }
        void registerPush(p.token, p.locale ?? "");
    });
    // 控件专用设备会话代铸：主 app 让引擎（持完整会话）给控制中心控件铸一条独立委派会话
    // （独立 device_id），回交原生存 App Group 供控件自用 / 自刷，不与引擎抢 refresh 轮换。
    onNativeEvent("provisionWidgetSession", (payload) =>
    {
        const p = payload as { device_id?: string; device_name?: string };
        if (!p.device_id) { return; }
        void provisionWidgetSession(p.device_id, p.device_name ?? "");
    });
    onNativeEvent("shutdown", () =>
    {
        hubCtrl.abort();
        stopICEServerRefresh();
    });
}

// registerPush：把原生送来的 APNs 设备令牌上报后端（POST /api/push/apns/register）。走引擎
// 而非原生直连，复用 apiFetch 的新鲜会话 token + 自动续期（同剪贴板 / 吊销范式）。后端未配
// APNs（无 .p8）时返回 503，best-effort 忽略即可。
async function registerPush(token: string, locale: string): Promise<void>
{
    try
    {
        await apiFetch("/api/push/apns/register", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ device_token: token, locale }),
        });
    }
    catch (e)
    {
        notifyNative("error", { stage: "push", message: e instanceof Error ? e.message : String(e) });
    }
}

// provisionWidgetSession：经 /api/auth/device-session 给控件代铸一条独立委派会话（device_type
// ios），把 access / refresh 回交原生（存 App Group）。控件据此独立调 /api/clipboard 并自刷。
async function provisionWidgetSession(deviceId: string, deviceName: string): Promise<void>
{
    try
    {
        const r = await apiFetch("/api/auth/device-session", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ device_id: deviceId, device_name: deviceName, device_type: "ios" }),
        });
        if (!r.ok) { return; }
        const d = (await r.json()) as { access_token: string; refresh_token: string; user_id: string };
        notifyNative("widgetSession", {
            access_token: d.access_token,
            refresh_token: d.refresh_token,
            user_id: d.user_id,
        });
    }
    catch (e)
    {
        notifyNative("error", { stage: "widget", message: e instanceof Error ? e.message : String(e) });
    }
}

async function revokeDevice(deviceID: string): Promise<void>
{
    try
    {
        const r = await apiFetch(`/api/devices/${encodeURIComponent(deviceID)}`, { method: "DELETE" });
        if (r.status === 403)
        {
            notifyNative("error", { stage: "revoke", message: "step_up_required" });
            return;
        }
        if (!r.ok)
        {
            notifyNative("error", { stage: "revoke", message: `HTTP ${r.status}` });
            return;
        }
        notifyNative("deviceRevoked", { device_id: deviceID });
    }
    catch (e)
    {
        notifyNative("error", { stage: "revoke", message: String(e) });
    }
}

// renameSelf renames THIS device by its stable device_id, decoupled from the session token:
// PATCH /api/devices/{device_id} only changes the name (no re-mint), then the engine adopts the
// new name as X-Device-Name and reports it back so the native shell persists it.
async function renameSelf(deviceID: string, name: string): Promise<void>
{
    try
    {
        const r = await apiFetch(`/api/devices/${encodeURIComponent(deviceID)}`, {
            method: "PATCH",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ name }),
        });
        if (!r.ok)
        {
            notifyNative("error", { stage: "rename", message: `HTTP ${r.status}` });
            return;
        }
        useAppStore.getState().setSelfDeviceName(name);
        notifyNative("renamed", { name });
    }
    catch (e)
    {
        notifyNative("error", { stage: "rename", message: e instanceof Error ? e.message : String(e) });
    }
}

// 扫码登录他端（#8）：本机持完整会话，代另一台设备批准其登录请求。三步映射到 qr.ts：
//   fetchLoginRequest → qrRequest 拉待批准设备信息，回报原生渲染确认（不盲批）；
//   approveLogin → qrApprove 授予完整会话（full + persist，原生只接受 full）；
//   denyLogin → qrDeny 拒绝。任一失败回报 loginFailed（码失效 / 已处理 / 网络），原生提示并清待批准态。
async function fetchLoginRequest(requestId: string, code: string): Promise<void>
{
    try
    {
        const info = await qrRequest(requestId, code);
        notifyNative("loginRequest", {
            request_id: requestId,
            code,
            device_name: info.deviceName,
            device_type: info.deviceType,
            request_ip: info.requestIp,
        });
    }
    catch
    {
        notifyNative("loginFailed", {});
    }
}

async function approveLogin(requestId: string, code: string): Promise<void>
{
    try
    {
        await qrApprove(requestId, code, "full", "persist");
        notifyNative("loginApproved", {});
    }
    catch
    {
        notifyNative("loginFailed", {});
    }
}

async function denyLogin(requestId: string, code: string): Promise<void>
{
    try
    {
        await qrDeny(requestId, code);
        notifyNative("loginDenied", {});
    }
    catch
    {
        notifyNative("loginFailed", {});
    }
}

// healIOSIdentity corrects the display identity after a QR login. collectQRSession returns the
// subject UUID as the name (cdrop holds no accounts table; qr.go), so the injected session shows
// "你好, <uuid>". Once the token is live at the edge, /api/me carries the real X-Auth-Name: read
// it, update the store, and push it to the native shell (which persists it to the Keychain so the
// real name survives a relaunch). Best-effort; a failure just leaves the prior name.
async function healIOSIdentity(): Promise<void>
{
    try
    {
        const r = await apiFetch("/api/me");
        if (!r.ok) { return; }
        const me = (await r.json()) as { user_id?: string; name?: string; avatar?: string };
        if (!me?.user_id) { return; }
        const realName = me.name && me.name !== me.user_id ? me.name : "";
        if (!realName) { return; }
        const cur = useAppStore.getState();
        if (!cur.user) { return; }
        if (cur.user.name === realName && cur.user.avatar === (me.avatar || cur.user.avatar)) { return; }
        useAppStore.getState().setAuth({
            accessToken: cur.accessToken,
            user: { id: cur.user.id, name: realName, avatar: me.avatar || cur.user.avatar },
        });
        notifyNative("identityUpdated", { user_id: me.user_id, name: realName, avatar: me.avatar ?? "" });
    }
    catch { /* best-effort identity heal */ }
}

// installLogBridge：把 console.warn / error 过桥给原生（设置页显示「最近日志」）。SSE 失败
// 等都走 console.warn，这样不接 Web Inspector 也能在真机看到失败原因（401 / 网络 / 静默挂起）。
function installLogBridge(): void
{
    const safeStr = (a: unknown): string =>
        a instanceof Error
            ? `${a.name}: ${a.message}`
            : typeof a === "object" && a !== null
                ? (() => { try { return JSON.stringify(a); } catch { return String(a); } })()
                : String(a);
    const wrap = (level: string, orig: (...a: unknown[]) => void) =>
        (...args: unknown[]): void =>
        {
            try { notifyNative("log", { level, msg: args.map(safeStr).join(" ") }); }
            catch { /* 永不让日志桥拖垮引擎 */ }
            orig(...args);
        };
    console.warn = wrap("warn", console.warn.bind(console));
    console.error = wrap("error", console.error.bind(console));
}

// boot：原生壳内启动引擎。store 已于 import 时从 __CDROP_BOOT__ 水合，故此处同步读取
// 注入的登录态；缺失即报错（原生须在加载本 bundle 前注入）。
function boot(): void
{
    if (!isIOSShell())
    {
        // 非 iOS 壳（误加载 engine.html）——保持惰性，不触发任何网络 / 引擎行为。
        // eslint-disable-next-line no-console
        console.warn("cdrop engine: not running inside the iOS shell, idle");
        return;
    }

    const { user, selfDeviceName } = useAppStore.getState();
    if (!user || !selfDeviceName)
    {
        notifyNative("error", {
            stage: "boot",
            message: "missing injected session / device_name (window.__CDROP_BOOT__)",
        });
        return;
    }

    installLogBridge();   // 把引擎 console.warn/error 过桥给原生，设置页显示，便于诊断 SSE 失败原因
    bindCommands();
    subscribeStore();

    void startHub(hubCtrl.signal);   // SSE 信令循环：presence / 传入 offer / 状态 / 信令
    void refreshICEServers();        // 预取 TURN / STUN，首次 WebRTC 即可用
    void refreshSessionScope();      // 校正 /api/me 的权限级别
    void healIOSIdentity();          // 用 /api/me 的真名替换 QR 登录注入的 UUID 名（#12）

    // 启动即推一次当前快照：subscribe 只在「之后」的变更触发，初始态（多为空，但桌面壳
    // 复用同入口时可能已有）须主动补发，免原生 UI 等到下一次变更才填。
    const init = useAppStore.getState();
    notifyNative("ready", { device: selfDeviceName });
    notifyNative("presence", { devices: init.devices });
    notifyNative("transfers", { active: Object.values(init.activeTransfers).map(toWire) });

    // 诊断隔离（仅 CDROP_DEBUG_SESSION 下、prod 用户无此 flag）：经真 postMessage 桥推一
    // 条假 presence，验证「桥 → 原生解析 → UI 渲染」整条链路是否工作（不依赖真 SSE/鉴权，
    // 因 debug token 必 401 拿不到真 presence）。若原生「设备」据此显示出来 → 链路 OK，空
    // 列表的真因在上游（SSE 没连 / 后端空）；若仍不显示 → 原生侧 bug。
    const boot = (window as unknown as { __CDROP_BOOT__?: { debug?: boolean } }).__CDROP_BOOT__;
    if (boot?.debug)
    {
        const nowSec = Math.floor(Date.now() / 1000);
        notifyNative("presence", { devices: [
            { name: "Debug-MacBook", type: "macos", online: true, lastSeen: nowSec },
            { name: "Debug-PC", type: "windows", online: false, lastSeen: nowSec - 600 },
        ] });
    }
}

boot();
