import { connectSSE, type SseEvent } from "../../net/sse";
import { useAppStore, type DeviceInfo, type TransferRecord } from "../../store";
import { apiFetch } from "../../net/api";
import { notifyNativeIfBackground } from "../../net/desktop";
import { t } from "../../i18n";
import { p2pHandleSignal, p2pCleanup } from "./p2p";
import { rememberIncoming, handleRelayReady, cleanupTransfer } from "./transfer";
import { applyRemoteClipboard } from "../clipboard/clipboard";

const HUB_URL = "/api/hub/events";

// startHub opens an SSE channel and dispatches frames into the Zustand store.
// Pass an AbortSignal to cancel cleanly (e.g. on logout or React unmount).
//
// Reconnect: the loop NEVER gives up on its own — a flaky or briefly-offline
// network must heal without user action (it also backs the desktop client, which
// must tolerate sleep/Wi-Fi drops without erroring out). Backoff grows 1s → 3s →
// 9s → 18s and caps at 30s, so we keep retrying forever at a sane cadence until
// the signal aborts (logout / unmount). After a few straight failures we set
// sseReconnecting=true so the UI can show a "reconnecting…" banner; it clears the
// moment the stream reopens.
const RECONNECT_DELAYS_MS = [1000, 3000, 9000];
const MAX_RECONNECT_DELAY_MS = 30000;
const RECONNECTING_AFTER_FAILURES = 3;

export async function startHub(signal: AbortSignal): Promise<void>
{
    useAppStore.setState({ sseConnected: false, sseReconnecting: false });
    let consecutiveFailures = 0;

    while (!signal.aborted)
    {
        let cleanEnd = false;
        try
        {
            await connectSSE(HUB_URL, dispatch, signal, onOpen);
            // Stream ended cleanly (server closed it, or a proxy recycled it).
            // Do NOT flip the indicator to "connecting" for the brief reconnect
            // gap: on some platforms (notably Windows WebView2 behind the asset
            // proxy) the stream gets recycled frequently, which would otherwise
            // show "connecting" almost all the time even while events keep
            // flowing. Keep sseConnected as-is and reconnect promptly.
            consecutiveFailures = 0;
            cleanEnd = true;
        }
        catch (e)
        {
            if (signal.aborted) { return; }
            if ((e as { name?: string }).name === "AbortError") { return; }
            consecutiveFailures++;
            // A real failure: drop to "connecting" and back off. Once we've failed
            // a few times in a row, surface the "reconnecting" banner — but keep
            // looping; we never stop trying on our own.
            const patch: Record<string, boolean> = { sseConnected: false };
            if (consecutiveFailures >= RECONNECTING_AFTER_FAILURES) { patch.sseReconnecting = true; }
            useAppStore.setState(patch);
            // eslint-disable-next-line no-console
            console.warn("hub: SSE attempt failed", consecutiveFailures, e);
        }

        const delay = cleanEnd ? RECONNECT_DELAYS_MS[0] : backoffDelay(consecutiveFailures);
        try { await sleep(delay, signal); }
        catch { return; }
    }
}

// backoffDelay grows with the failure count and caps at MAX_RECONNECT_DELAY_MS,
// so the loop keeps retrying indefinitely at a bounded cadence (never gives up).
function backoffDelay(failures: number): number
{
    if (failures <= RECONNECT_DELAYS_MS.length)
    {
        return RECONNECT_DELAYS_MS[failures - 1] ?? RECONNECT_DELAYS_MS[0];
    }
    const last = RECONNECT_DELAYS_MS[RECONNECT_DELAYS_MS.length - 1];
    const grown = last * Math.pow(2, failures - RECONNECT_DELAYS_MS.length);
    return Math.min(grown, MAX_RECONNECT_DELAY_MS);
}

function sleep(ms: number, signal: AbortSignal): Promise<void>
{
    return new Promise((resolve, reject) =>
    {
        const t = setTimeout(resolve, ms);
        const onAbort = () =>
        {
            clearTimeout(t);
            reject(new Error("aborted"));
        };
        if (signal.aborted) { onAbort(); return; }
        signal.addEventListener("abort", onAbort, { once: true });
    });
}

// onOpen marks the channel connected the moment the stream opens (see
// connectSSE) — independent of whether any frame has arrived yet. Reopening also
// clears any lingering "reconnecting" banner.
function onOpen(): void
{
    markConnected();
}

function markConnected(): void
{
    const s = useAppStore.getState();
    if (!s.sseConnected || s.sseReconnecting)
    {
        useAppStore.setState({ sseConnected: true, sseReconnecting: false });
    }
}

function dispatch(ev: SseEvent): void
{
    markConnected();

    switch (ev.type)
    {
        case "presence":
            handlePresence(ev.data);
            return;
        case "transfer:incoming":
            void handleIncoming(ev.data);
            return;
        case "transfer:state":
            handleState(ev.data);
            return;
        case "transfer:relay_ready":
        {
            const p = ev.data as { session_id?: string };
            if (p.session_id) { void handleRelayReady(p.session_id); }
            return;
        }
        case "signal":
            handleSignal(ev.data);
            return;
        case "message":
            handleIncomingMessage(ev.data);
            return;
        case "clipboard:update":
            handleClipboardUpdate(ev.data);
            return;
        case "ping":
            return;
        default:
            // eslint-disable-next-line no-console
            console.warn("hub: unknown event", ev.type, ev.data);
    }
}

function handleIncomingMessage(data: unknown): void
{
    const p = data as { from?: string; text?: string; sent_at?: number };
    if (!p.from || typeof p.text !== "string") { return; }
    useAppStore.getState().addMessage({
        id: crypto.randomUUID(),
        direction: "incoming",
        peerName: p.from,
        text: p.text,
        sentAt: typeof p.sent_at === "number" ? p.sent_at : Math.floor(Date.now() / 1000),
    });
    // 桌面端窗口在后台时弹原生通知（标题=发送方设备名，正文=消息）。
    notifyNativeIfBackground(p.from, p.text);
}

function handleClipboardUpdate(data: unknown): void
{
    const p = data as {
        content?: string;
        content_type?: string;
        source_device?: string;
        updated_at?: number;
        version?: number;
    };
    if (typeof p.content !== "string" || typeof p.updated_at !== "number") { return; }
    applyRemoteClipboard({
        content: p.content,
        contentType: p.content_type ?? "text/plain",
        sourceDevice: p.source_device ?? "",
        updatedAt: p.updated_at,
        version: typeof p.version === "number" ? p.version : 0,
    });
}

function handlePresence(data: unknown): void
{
    if (typeof data !== "object" || data === null) { return; }
    const items = (data as { devices?: unknown }).devices;
    if (!Array.isArray(items)) { return; }

    const devices: DeviceInfo[] = [];
    for (const raw of items)
    {
        if (typeof raw !== "object" || raw === null) { continue; }
        const r = raw as Record<string, unknown>;
        if (typeof r.name !== "string" || typeof r.type !== "string") { continue; }
        devices.push({
            name: r.name,
            type: r.type,
            online: Boolean(r.online),
            lastSeen: typeof r.last_seen === "number" ? r.last_seen : 0,
        });
    }

    const store = useAppStore.getState();
    const prevOnline = new Map(store.devices.map((d) => [d.name, d.online] as const));
    store.setDevices(devices);

    // Detect peers that just transitioned to offline; if any active transfer
    // is targeting them, ask the backend to fail it. completeTransfer (driven
    // by transfer:state FAILED) will remove the entry, so we don't double-fire.
    for (const d of devices)
    {
        if (!d.online && prevOnline.get(d.name))
        {
            for (const t of Object.values(store.activeTransfers))
            {
                if (t.peerName === d.name)
                {
                    void apiFetch(`/api/transfer/${t.sessionId}/fail`, {
                        method: "POST",
                        headers: { "Content-Type": "application/json" },
                        body: JSON.stringify({ reason: "peer_offline" }),
                    });
                }
            }
        }
    }
}

interface IncomingPayload
{
    session?: {
        id?: string;
        sender_name?: string;
        file_name?: string;
        file_size?: number;
    };
    auto_accept?: boolean;
}

async function handleIncoming(data: unknown): Promise<void>
{
    const p = data as IncomingPayload;
    const s = p.session;
    if (!s?.id || !s.sender_name || !s.file_name || typeof s.file_size !== "number")
    {
        return;
    }
    const rec: TransferRecord = {
        sessionId: s.id,
        direction: "incoming",
        fileName: s.file_name,
        fileSize: s.file_size,
        state: "PENDING",
        peerName: s.sender_name,
        startedAt: Date.now(),
        phase: "waiting_accept",
        bytesTransferred: 0,
        lastProgressAt: Date.now(),
    };
    useAppStore.getState().upsertTransfer(rec);

    // 桌面端窗口在后台时弹原生通知「收到文件」（前台时由传输卡片呈现）。
    notifyNativeIfBackground(
        t("notify.incoming.title"),
        t("notify.incoming.body", { sender: s.sender_name, file: s.file_name }),
    );

    // brief §2 写明 "> 100 MB 二次确认"，但 MVP 没有该 UI，且 auto_accept=false
    // 时跳过 rememberIncoming + /accept 会让 sender 等不到任何 ICE 候选 → 30s
    // 后 fallback 到 relay → ring buffer 64 MiB 写满 → 整个传输死锁。所以这里
    // 一律 pre-arm + accept；后端 auto_accept 字段保留给未来 UI 阻塞 prompt。
    void p.auto_accept;
    rememberIncoming(s.id, s.file_name, s.file_size, s.sender_name);
    try
    {
        const r = await apiFetch(`/api/transfer/${s.id}/accept`, { method: "POST" });
        if (!r.ok)
        {
            // eslint-disable-next-line no-console
            console.warn("auto-accept HTTP", r.status, await r.text().catch(() => ""));
        }
    }
    catch (e)
    {
        // eslint-disable-next-line no-console
        console.warn("auto-accept failed", e);
    }
}

function handleState(data: unknown): void
{
    const p = data as { session_id?: string; state?: string; mode?: string };
    if (!p.session_id || !p.state) { return; }

    const store = useAppStore.getState();
    const cur = store.activeTransfers[p.session_id];

    const terminal = p.state === "DONE" || p.state === "FAILED" || p.state === "CANCELLED";
    if (terminal)
    {
        if (cur) { p2pCleanup(cur.peerName); }
        cleanupTransfer(p.session_id);
        // Preserve mode so history can show "DONE via relay" etc.
        if (cur && p.mode && cur.mode !== p.mode)
        {
            store.upsertTransfer({ ...cur, mode: p.mode });
        }
        store.completeTransfer(p.session_id, p.state);
        // 桌面端窗口在后台时，对完成 / 失败弹原生通知（取消不提醒，与推送事件选择一致）。
        if (p.state === "DONE" || p.state === "FAILED")
        {
            notifyNativeIfBackground(
                t(p.state === "DONE" ? "notify.transfer.doneTitle" : "notify.transfer.failedTitle"),
                cur?.fileName ?? "",
            );
        }
        return;
    }
    if (cur)
    {
        store.upsertTransfer({
            ...cur,
            state: p.state,
            mode: p.mode ?? cur.mode,
        });
    }
}

interface SignalEvent
{
    from?: string;
    payload?: { type: string; sdp?: RTCSessionDescriptionInit; candidate?: RTCIceCandidateInit };
}

function handleSignal(data: unknown): void
{
    const ev = data as SignalEvent;
    if (!ev.from || !ev.payload?.type)
    {
        return;
    }
    const t = ev.payload.type;
    if (t !== "offer" && t !== "answer" && t !== "ice") { return; }
    p2pHandleSignal(ev.from, ev.payload as { type: "offer" | "answer" | "ice" } & typeof ev.payload);
}
