import { apiFetch } from "../../net/api";
import { writeNativeClipboard } from "../../net/desktop";
import { useAppStore, type ClipboardState } from "../../store";
import { t } from "../../i18n";

// 字节上限要与后端 CDROP_CLIPBOARD_MAX_BYTES 同步（默认 64 KB）。
// 前端按 UTF-8 字节算长度（不是 string.length，后者是 UTF-16 code unit）。
export const CLIPBOARD_MAX_BYTES = 64 * 1024;

interface PutBody
{
    content: string;
    content_type: "text/plain";
    // origin_ts 是本机复制时刻（ms），服务端据它做 LWW。浏览器拿不到精确复制时间，
    // 用读取/上传瞬间的 Date.now() 近似——比服务端「收到时刻」更贴近真实复制时刻。
    origin_ts: number;
}

// readClipboardText 主路径：readText() 拿浏览器为常规复制场景自动合成的
// text/plain；只有当 readText 返回空且 read() 可用时，才 fallback 到读取
// ClipboardItem，按 text/plain → text/html (DOMParser 提 textContent) 顺序
// 取文本，避免"源应用只塞 text/html 时丢失文本"。
export async function readClipboardText(): Promise<string>
{
    if (!navigator.clipboard)
    {
        throw new Error(t("errors.clipboardUnavailable"));
    }

    try
    {
        const text = await navigator.clipboard.readText();
        if (text) { return text; }
    }
    catch
    {
        // 主路径不可用（权限 / 不支持）→ 降级到 read()
    }

    if (typeof navigator.clipboard.read !== "function")
    {
        return "";
    }

    const items = await navigator.clipboard.read();
    for (const item of items)
    {
        if (item.types.includes("text/plain"))
        {
            const blob = await item.getType("text/plain");
            const text = await blob.text();
            if (text) { return text; }
        }
        if (item.types.includes("text/html"))
        {
            const blob = await item.getType("text/html");
            const html = await blob.text();
            const text = htmlToPlainText(html);
            if (text) { return text; }
        }
    }
    return "";
}

// htmlToPlainText 用 DOMParser 解析 HTML（不执行脚本），按块级元素补换行后
// 取 textContent。XSS 风险为零——我们丢弃所有标签，只保留文本节点内容。
export function htmlToPlainText(html: string): string
{
    const doc = new DOMParser().parseFromString(html, "text/html");
    doc.querySelectorAll("br").forEach((br) => br.replaceWith("\n"));
    doc
        .querySelectorAll("p, div, li, h1, h2, h3, h4, h5, h6, tr, blockquote, pre")
        .forEach((b) => b.append("\n"));
    return ( doc.body.textContent ?? "" ).replace(/\n{3,}/g, "\n\n").trim();
}

export function utf8ByteLength(text: string): number
{
    return new TextEncoder().encode(text).length;
}

// uploadClipboard 把一段纯文本作为剪贴板写入推到云端（PUT /api/clipboard）。
// 字节超限 / 空字符串 / 网络错误统一抛 Error，调用点显示给用户。
export async function uploadClipboard(content: string): Promise<void>
{
    if (utf8ByteLength(content) > CLIPBOARD_MAX_BYTES)
    {
        throw new Error(t("errors.clipboardOverflow", { max: CLIPBOARD_MAX_BYTES }));
    }
    const body: PutBody = { content, content_type: "text/plain", origin_ts: Date.now() };
    const r = await apiFetch("/api/clipboard", {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
    });
    // 409 = LWW 落败：别的设备有更晚的复制赢了。不是错误——胜出内容会经 SSE 到达，
    // 这里静默返回即可，不打扰用户。
    if (r.status === 409) { return; }
    if (!r.ok)
    {
        const text = await r.text().catch(() => r.statusText);
        throw new Error(`PUT /api/clipboard ${r.status}: ${text}`);
    }

    // 立即把自己的写入合入本地 store（用服务端回的权威 version / updated_at），
    // 上传后 UI 立刻反映"云端最新"；同步窗口结束后 SSE 回声内容相同、被自回声挡掉。
    const accepted = await r.json().catch(() => null) as { version?: number; updated_at?: number } | null;
    const state = useAppStore.getState();
    state.setClipboard({
        content,
        contentType: "text/plain",
        sourceDevice: state.selfDeviceName ?? "",
        updatedAt: accepted?.updated_at ?? Math.floor(Date.now() / 1000),
        version: accepted?.version ?? 0,
    });
}

// fetchClipboard 用 If-None-Match 拉一次最新；304 表示与本地内存一致，不动 store。
// ETag 直接从 store.clipboard.updatedAt 派生（与"本地内存"等价），刷新后内存清零
// → 不发 If-None-Match → 总是拉到最新内容。后端在尚未写入时返回 200 + 空 State
// + ETag "0"，所以这里不需要再处理 404。
export async function fetchClipboard(): Promise<void>
{
    const headers = new Headers();
    const cur = useAppStore.getState().clipboard;
    if (cur && cur.version > 0)
    {
        // ETag 是版本号（与后端 etagFor(version) 对齐）；版本未变即 304、省下整份内容。
        headers.set("If-None-Match", `"${cur.version}"`);
    }

    const r = await apiFetch("/api/clipboard", { headers });
    if (r.status === 304) { return; }
    if (!r.ok)
    {
        const text = await r.text().catch(() => r.statusText);
        throw new Error(`GET /api/clipboard ${r.status}: ${text}`);
    }
    const data = await r.json() as {
        content: string;
        content_type: string;
        source_device: string;
        updated_at: number;
        version: number;
    };
    applyRemoteClipboard({
        content: data.content,
        contentType: data.content_type,
        sourceDevice: data.source_device,
        updatedAt: data.updated_at,
        version: data.version,
    });
}

// applyRemoteClipboard 由 SSE clipboard:update 事件 / fetchClipboard 调用。
// 自回声（source_device == 本机）时不覆盖——本机 PUT 后已即时写过 store。
export function applyRemoteClipboard(state: ClipboardState): void
{
    const selfDevice = useAppStore.getState().selfDeviceName;
    const cur = useAppStore.getState().clipboard;
    if (selfDevice && state.sourceDevice === selfDevice && cur?.updatedAt === state.updatedAt)
    {
        return;
    }
    useAppStore.getState().setClipboard(state);

    // 桌面端：把来自其他设备的更新写回本机原生剪贴板（下行同步）。自身来源的回声
    // 不写（Go 侧 Monitor 亦会再挡一层）；浏览器里 writeNativeClipboard 为 no-op。
    if (!selfDevice || state.sourceDevice !== selfDevice)
    {
        writeNativeClipboard(state.content, state.sourceDevice);
    }
}

