import { apiFetch } from "../../net/api";
import { p2pStartIncoming, p2pStartOutgoing, p2pCleanup, type P2PSession } from "./p2p";
import { relayReceiveFile, relaySendFile } from "./relay";
import type { FileSource } from "./source";
import { useAppStore, type TransferRecord } from "../../store";

// Orchestrator that picks P2P first, falls back to Relay on ICE timeout
// (plan §M9: 30s after start, if iceConnectionState != connected → /fallback).

const ICE_TIMEOUT_MS = 30_000;

interface OutgoingState
{
    sessionId: string;
    peerName: string;
    src: FileSource;
    p2p: P2PSession | null;
    fallbackTimer: number | null;
    abort: AbortController;
}

const outgoing = new Map<string, OutgoingState>();
const incomingFiles = new Map<string, { name: string; size: number; sender: string }>();

// Sender entry point. Initiates the backend transfer row, kicks off P2P, and
// arms a 30s watchdog that calls /fallback if ICE hasn't reached `connected`.
export async function startOutgoingTransfer(
    receiverName: string,
    src: FileSource,
): Promise<string>
{
    const initR = await apiFetch("/api/transfer/initiate", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
            receiver_name: receiverName,
            file_name: src.name,
            file_size: src.size,
        }),
    });
    if (!initR.ok)
    {
        const text = await initR.text().catch(() => initR.statusText);
        throw new Error(`initiate ${initR.status}: ${text}`);
    }
    const { session_id: sessionId } = (await initR.json()) as { session_id: string };

    useAppStore.getState().upsertTransfer({
        sessionId,
        direction: "outgoing",
        fileName: src.name,
        fileSize: src.size,
        state: "PENDING",
        peerName: receiverName,
        startedAt: Date.now(),
        phase: "waiting_accept",
        bytesTransferred: 0,
        lastProgressAt: Date.now(),
    });

    const abort = new AbortController();
    const state: OutgoingState = {
        sessionId,
        peerName: receiverName,
        src,
        p2p: null,
        fallbackTimer: null,
        abort,
    };
    outgoing.set(sessionId, state);

    // Arm the fallback watchdog *before* any P2P work. p2pStartOutgoing is
    // intentionally synchronous: if createOffer / setLocalDescription hang
    // (e.g. injected stub during fallback testing) the timer must still fire.
    state.fallbackTimer = window.setTimeout(() =>
    {
        const cur = outgoing.get(sessionId);
        if (!cur) { return; }
        const p2pState = cur.p2p?.state;
        if (p2pState !== "connected" && p2pState !== "completed")
        {
            // 把"已经放弃 P2P，等服务端 RELAY_ACTIVE"这个状态显式落到 store，
            // UI 据此告诉用户「还在通，正在切到中继」而不是干等。
            const store = useAppStore.getState();
            const r = store.activeTransfers[sessionId];
            if (r) { store.upsertTransfer({ ...r, phase: "fallback_pending" }); }
            void triggerFallback(sessionId);
        }
    }, ICE_TIMEOUT_MS);

    const p2p = p2pStartOutgoing(sessionId, receiverName, src);
    state.p2p = p2p;

    p2p.onState((s) =>
    {
        if (s === "completed" || s === "connected")
        {
            if (state.fallbackTimer) { clearTimeout(state.fallbackTimer); state.fallbackTimer = null; }
        }
    });

    return sessionId;
}

async function triggerFallback(sessionId: string): Promise<void>
{
    const r = await apiFetch(`/api/transfer/${sessionId}/fallback`, { method: "POST" });
    if (!r.ok && r.status !== 409)
    {
        // 409 = transition not allowed (e.g. already DONE). Anything else is unexpected.
        // eslint-disable-next-line no-console
        console.warn("fallback failed", r.status, await r.text().catch(() => ""));
    }
}

// Called from hub.ts when transfer:incoming fires. Records expected file
// metadata so a Relay fallback can name the downloaded file correctly.
export function rememberIncoming(sessionId: string, fileName: string, fileSize: number, sender: string): void
{
    incomingFiles.set(sessionId, { name: fileName, size: fileSize, sender });
    // Pre-arm the P2P answerer so the sender's offer signal lands in a wired peer.
    p2pStartIncoming(sessionId, sender);
}

// Called from hub.ts on transfer:relay_ready. Decides whether this client is
// the sender or the receiver and runs the appropriate relay direction.
export async function handleRelayReady(sessionId: string): Promise<void>
{
    const out = outgoing.get(sessionId);
    if (out)
    {
        // Sender side: cancel P2P (cleanup data channel + connection),
        // then push the file via the relay endpoint.
        if (out.p2p) { out.p2p.cancel(); }
        p2pCleanup(out.peerName);
        try
        {
            await relaySendFile(sessionId, out.src, (p) =>
            {
                const store = useAppStore.getState();
                const cur = store.activeTransfers[sessionId]
                    ?? currentRecord(sessionId, "outgoing", out.peerName, out.src.name, out.src.size);
                const grew = p.bytes > (cur.bytesTransferred ?? 0);
                store.upsertTransfer({
                    ...cur,
                    state: "RELAY_ACTIVE",
                    phase: "relay_uploading",
                    bytesTransferred: p.bytes,
                    lastProgressAt: grew ? Date.now() : cur.lastProgressAt,
                });
            }, out.abort.signal);
            // Receiver side calls /done from relayReceiveFile's caller; sender
            // just signals X-End on the last chunk and leaves state advancement
            // to the receiver, mirroring the P2P path.
        }
        catch (e)
        {
            await apiFetch(`/api/transfer/${sessionId}/fail`, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ reason: (e as Error).message }),
            });
        }
        finally
        {
            cleanupOutgoing(sessionId);
        }
        return;
    }

    const incoming = incomingFiles.get(sessionId);
    if (!incoming)
    {
        // eslint-disable-next-line no-console
        console.warn("relay_ready for unknown session", sessionId);
        return;
    }

    // Receiver side: cancel P2P answerer, drain the relay stream into a Blob
    // and trigger a download via createObjectURL.
    p2pCleanup(incoming.sender);
    try
    {
        await relayReceiveFile(sessionId, incoming.name, incoming.size, (p) =>
        {
            const store = useAppStore.getState();
            const cur = store.activeTransfers[sessionId];
            if (!cur) { return; }
            const grew = p.bytes > (cur.bytesTransferred ?? 0);
            store.upsertTransfer({
                ...cur,
                state: "RELAY_ACTIVE",
                phase: "relay_downloading",
                bytesTransferred: p.bytes,
                lastProgressAt: grew ? Date.now() : cur.lastProgressAt,
            });
        });
        await apiFetch(`/api/transfer/${sessionId}/done`, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ bytes_transferred: incoming.size }),
        });
    }
    catch (e)
    {
        await apiFetch(`/api/transfer/${sessionId}/fail`, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ reason: (e as Error).message }),
        });
    }
    finally
    {
        incomingFiles.delete(sessionId);
    }
}

function currentRecord(
    sessionId: string,
    direction: "outgoing" | "incoming",
    peerName: string,
    fileName: string,
    fileSize: number,
): TransferRecord
{
    const cur = useAppStore.getState().activeTransfers[sessionId];
    if (cur) { return cur; }
    return {
        sessionId,
        direction,
        peerName,
        fileName,
        fileSize,
        state: "RELAY_ACTIVE",
        startedAt: Date.now(),
    };
}

function cleanupOutgoing(sessionId: string): void
{
    const s = outgoing.get(sessionId);
    if (!s) { return; }
    if (s.fallbackTimer) { clearTimeout(s.fallbackTimer); }
    s.abort.abort();
    outgoing.delete(sessionId);
}

// Hub uses this when transfer:state ends terminally to release any state.
export function cleanupTransfer(sessionId: string): void
{
    cleanupOutgoing(sessionId);
    incomingFiles.delete(sessionId);
}

// UI-triggered (发送方「跳过等待、立即中继」)：发送方不等 30s ICE 超时，直接切到
// 中继。复用 watchdog 的逻辑——先停掉那个定时器以免重复 fire，把记录标成
// fallback_pending 让 UI 显示「正在切到中继」，再请求服务端转 RELAY_ACTIVE。
export async function skipWaitRelay(sessionId: string): Promise<void>
{
    const cur = outgoing.get(sessionId);
    if (cur?.fallbackTimer)
    {
        clearTimeout(cur.fallbackTimer);
        cur.fallbackTimer = null;
    }
    const store = useAppStore.getState();
    const r = store.activeTransfers[sessionId];
    if (r) { store.upsertTransfer({ ...r, phase: "fallback_pending" }); }
    await triggerFallback(sessionId);
}

// UI-triggered (取消进行中 / 等待中的传输)：先就地拆掉本端的 P2P / 中继工作，再
// POST /cancel。服务端转 CANCELLED 后经 hub 的 transfer:state 把记录归到 history。
export async function cancelTransfer(sessionId: string): Promise<void>
{
    const out = outgoing.get(sessionId);
    if (out)
    {
        if (out.p2p) { out.p2p.cancel(); }
        p2pCleanup(out.peerName);
    }
    const inc = incomingFiles.get(sessionId);
    if (inc) { p2pCleanup(inc.sender); }
    cleanupTransfer(sessionId);

    const r = await apiFetch(`/api/transfer/${sessionId}/cancel`, { method: "POST" });
    if (!r.ok && r.status !== 409)
    {
        // 409 = 已是终态（DONE 等），无需处理；其余记一笔。
        // eslint-disable-next-line no-console
        console.warn("cancel failed", sessionId, r.status, await r.text().catch(() => ""));
    }
}
