import { useCallback, useEffect, useState } from "react";
import {
    Anchor,
    Container,
    Group,
    Stack,
    Text,
    Title,
} from "@mantine/core";
import { createFileRoute, Link, redirect, useNavigate } from "@tanstack/react-router";
import { AlertTriangle, Bell, LogOut, ShieldAlert, Trash2 } from "lucide-react";
import { logout, startStepUpReauth, syncWebDeviceName } from "../features/auth/auth";
import { DesktopSettings } from "../features/desktop/DesktopSettings";
import {
    consumePendingRevoke, consumeStepUpCode, stashPendingRevoke,
} from "../features/qr/stepUp";
import { isDesktop, persistDesktopDeviceName } from "../net/desktop";
import { apiFetch } from "../net/api";
import {
    currentPushState,
    disablePush,
    enablePush,
    pushSupported,
    type PushState,
} from "../net/push";
import {
    listSessions, postStepUp, revokeSession, StepUpRequiredError,
    type AuthSession,
} from "../net/sessions";
import { useAppStore } from "../store";
import { t } from "../i18n";
import { formatRelative, isAsciiDeviceName } from "../utils/format";
import { Badge, Button, Callout, DynText, Panel, TextField } from "../ui/primitives";
import { StateDot } from "../ui/glyphs";
import { toast } from "../ui/feedback";

export const Route = createFileRoute("/settings")({
    component: SettingsPage,
    beforeLoad: () =>
    {
        const { user, selfDeviceName } = useAppStore.getState();
        if (!user) { throw redirect({ to: "/login", search: { dev_user: undefined } }); }
        if (!selfDeviceName) { throw redirect({ to: "/setup" }); }
    },
});

function SettingsPage()
{
    const navigate = useNavigate();
    const user = useAppStore((s) => s.user);
    const sessionScope = useAppStore((s) => s.sessionScope);
    const isGuest = sessionScope === "guest";
    const selfDeviceName = useAppStore((s) => s.selfDeviceName);
    const setSelfDeviceName = useAppStore((s) => s.setSelfDeviceName);

    const [ pendingName, setPendingName ] = useState(selfDeviceName ?? "");
    const [ renaming, setRenaming ] = useState(false);

    const trimmedName = pendingName.trim();
    const canSaveName = !!trimmedName && trimmedName !== selfDeviceName;

    const handleRename = async () =>
    {
        if (!selfDeviceName) { return; }
        if (!trimmedName) { toast.warn(t("settings.deviceName.empty")); return; }
        if (!isAsciiDeviceName(trimmedName))
        {
            toast.warn(t("settings.deviceName.asciiOnly"));
            return;
        }
        if (trimmedName === selfDeviceName)
        {
            toast.warn(t("settings.deviceName.unchanged"));
            return;
        }

        setRenaming(true);
        try
        {
            // 先在后端把旧设备记录删掉。Hub.Kick 会强制关闭旧的 SSE，
            // 紧接着 root effect 检测到 selfDeviceName 变化会以新名重新注册。
            // 404（旧记录已被 sweeper 清理）也按成功处理。
            const r = await apiFetch(
                `/api/devices/${encodeURIComponent(selfDeviceName)}`,
                { method: "DELETE" },
            );
            if (!r.ok && r.status !== 404)
            {
                const text = await r.text().catch(() => r.statusText);
                throw new Error(`${r.status}: ${text}`);
            }
            setSelfDeviceName(trimmedName);
            persistDesktopDeviceName(trimmedName); // 桌面：持久化到 Go 配置，跨重启存活
            syncWebDeviceName(trimmedName);        // 浏览器：持久化到服务端 session，抗 PWA 存储清除
            toast.ok(t("settings.deviceName.success"));
        }
        catch (e)
        {
            toast.error(t("settings.error.delete", {
                message: e instanceof Error ? e.message : String(e),
            }));
        }
        finally
        {
            setRenaming(false);
        }
    };

    const handleUnregisterSelf = async () =>
    {
        if (!selfDeviceName) { return; }
        if (!window.confirm(t("settings.unregister.currentConfirm"))) { return; }

        try
        {
            await apiFetch(
                `/api/devices/${encodeURIComponent(selfDeviceName)}`,
                { method: "DELETE" },
            );
        }
        catch
        {
            // 即便 DELETE 失败也按用户意图清理本地并跳转登录。
        }
        finally
        {
            setSelfDeviceName(null);
            logout();
            navigate({ to: "/login", search: { dev_user: undefined } });
        }
    };

    const handleSignOut = () =>
    {
        logout();
        navigate({ to: "/login", search: { dev_user: undefined } });
    };

    return (
        <Container size="sm" py="lg">
            <Stack gap="lg">
                <Group justify="space-between" align="center" wrap="nowrap">
                    <Group gap="xs" align="center" wrap="nowrap">
                        <Title order={2}>{t("settings.title")}</Title>
                        {isGuest && <Badge tone="warn">{t("settings.guest.badge")}</Badge>}
                    </Group>
                    <Anchor component={Link} to="/" size="sm">
                        {t("nav.backToHome")}
                    </Anchor>
                </Group>

                {/* 受限访客标识：说明这台设备不能管理账号 / 授权设备，要完整权限请在
                    主设备上重新登录。放在最显眼处（标题下第一块）。 */}
                {isGuest && (
                    <Callout
                        tone="warn"
                        icon={<ShieldAlert size={16} />}
                        title={t("settings.guest.title")}
                    >
                        {t("settings.guest.body")}
                    </Callout>
                )}

                <Panel title={t("settings.currentDevice.title")}>
                    <Stack gap="sm">
                        <TextField
                            label={t("settings.deviceName.field")}
                            value={pendingName}
                            onChange={(e) => setPendingName(e.currentTarget.value)}
                            onKeyDown={(e) =>
                            {
                                if (e.key === "Enter" && canSaveName)
                                {
                                    e.preventDefault();
                                    void handleRename();
                                }
                            }}
                        />
                        <Group>
                            <Button
                                variant="primary"
                                onClick={handleRename}
                                disabled={!canSaveName}
                                loading={renaming}
                            >
                                {t("settings.deviceName.save")}
                            </Button>
                        </Group>
                        <div style={{ height: 1, background: "var(--divider)" }} />
                        <Text size="sm" c="dimmed" className="justify" data-jz-level="paragraph">
                            {t("settings.unregister.currentHint")}
                        </Text>
                        <Group>
                            <Button
                                variant="danger"
                                leftIcon={<Trash2 size={14} />}
                                onClick={handleUnregisterSelf}
                            >
                                {t("settings.unregister.current")}
                            </Button>
                        </Group>
                    </Stack>
                </Panel>

                {isDesktop() && <DesktopSettings />}

                {pushSupported() && <PushPanel />}

                <SessionsPanel
                    isGuest={isGuest}
                    onSignedOutSelf={handleSignOut}
                />

                <Panel title={t("settings.account.title")}>
                    <Stack gap="sm">
                        {user && (
                            <Stack gap={2}>
                                <Text size="sm" fw={500} truncate>{user.name}</Text>
                                {user.id !== user.name && (
                                    <Text size="xs" c="dimmed" truncate>{user.id}</Text>
                                )}
                            </Stack>
                        )}
                        <Group>
                            <Button
                                variant="ghost"
                                leftIcon={<LogOut size={14} />}
                                onClick={handleSignOut}
                            >
                                {t("settings.account.signOut")}
                            </Button>
                        </Group>
                    </Stack>
                </Panel>
            </Stack>
        </Container>
    );
}

// PushPanel：浏览器 / PWA 的 Web Push 开关。仅 pushSupported() 为真时渲染（桌面壳
// 与 dev 均为 false——桌面走原生通知、dev 无 SW）。订阅权威态在浏览器 pushManager，
// 挂载时用 currentPushState 读取一次。
function PushPanel()
{
    const [ state, setState ] = useState<PushState | null>(null);
    const [ busy, setBusy ] = useState(false);

    useEffect(() =>
    {
        let alive = true;
        void currentPushState().then((s) => { if (alive) { setState(s); } });
        return () => { alive = false; };
    }, []);

    const handleEnable = async () =>
    {
        setBusy(true);
        try
        {
            const s = await enablePush();
            setState(s);
            if (s.subscribed) { toast.ok(t("settings.push.enableOk")); }
            else if (s.permission === "denied") { toast.warn(t("settings.push.denied")); }
            else { toast.error(t("settings.push.failed")); }
        }
        finally { setBusy(false); }
    };

    const handleDisable = async () =>
    {
        setBusy(true);
        try
        {
            const s = await disablePush();
            setState(s);
            toast.ok(t("settings.push.disableOk"));
        }
        finally { setBusy(false); }
    };

    const subscribed = state?.subscribed ?? false;
    const denied = state?.permission === "denied";

    return (
        <Panel title={t("settings.push.title")}>
            <Stack gap="sm">
                <Text size="sm" c="dimmed" className="justify" data-jz-level="paragraph">{t("settings.push.hint")}</Text>
                {denied && !subscribed && (
                    <Text size="sm" className="justify" data-jz-level="paragraph" style={{ color: "var(--state-error)" }}>
                        {t("settings.push.deniedHint")}
                    </Text>
                )}
                <Group>
                    {subscribed ? (
                        <Button
                            variant="ghost"
                            leftIcon={<Bell size={14} />}
                            loading={busy}
                            onClick={handleDisable}
                        >
                            {t("settings.push.disable")}
                        </Button>
                    ) : (
                        <Button
                            variant="primary"
                            leftIcon={<Bell size={14} />}
                            loading={busy}
                            disabled={denied}
                            onClick={handleEnable}
                        >
                            {t("settings.push.enable")}
                        </Button>
                    )}
                    {subscribed && (
                        <Badge tone="online">{t("settings.push.enabled")}</Badge>
                    )}
                </Group>
            </Stack>
        </Panel>
    );
}

// sortSessions 把会话排成「在线在上、未活跃（offline）在下」，组内按最近活跃时间降序
// （活跃越近越靠前）。不改变原数组，返回新数组。
function sortSessions(list: AuthSession[]): AuthSession[]
{
    return [ ...list ].sort((a, b) =>
    {
        if (a.online !== b.online) { return a.online ? -1 : 1; }
        return b.lastUsedAt - a.lastUsedAt;
    });
}

// SessionsPanel：登录会话管理——浏览器 / 扫码登录的设备会话。每条显示在线状态点 +
// 设备名 + 权限级别 Badge + 「本机」标记 + 最近活跃，并提供「登出」（真正吊销该登录
// 的访问令牌）。在线会话排在上、未活跃（offline）的排在下；离线会话仍可登出。
//
// step-up：DELETE 返回 403 step_up_required 时，把待登出的 sessionId 暂存 → 发起
// 一次 prompt=login 再认证（returnTo=/settings）→ 整页跳走 → 回来后由本组件 mount
// effect 取出暂存的 {code, verifier} + sessionId，POST /auth/stepup 记录窗口，再
// 重试那条 DELETE。step-up 窗口内（后端 5 分钟）后续登出不再 403、直接成功。
//
// 受限访客（isGuest）：无权管理登录会话、后端 GET 会 403，故根本不发该请求，直接展示
// 一句克制的说明 Callout（不出现任何加载 / 失败态）。
function SessionsPanel(props: { isGuest: boolean; onSignedOutSelf: () => void })
{
    const { isGuest, onSignedOutSelf } = props;
    const [ sessions, setSessions ] = useState<AuthSession[] | null>(null);
    const [ loadError, setLoadError ] = useState(false);
    const [ busyId, setBusyId ] = useState<string | null>(null);
    const [ stepUpRejected, setStepUpRejected ] = useState(false);

    // reload 拉取并写入列表，同时把结果回传给调用方（step-up 重试路径需在 revoke
    // 前从这份列表里查出 current，不能在 revoke 之后再拉——若登出的正是本机会话，
    // 令牌已失效、那次 listSessions 会 401）。在线会话排前、未活跃的排后，组内按最近
    // 活跃时间降序。失败回 null。受限访客不会走到这里（调用方在 guest 分支直接返回）。
    const reload = useCallback(async (): Promise<AuthSession[] | null> =>
    {
        setLoadError(false);
        try
        {
            const list = sortSessions(await listSessions());
            setSessions(list);
            return list;
        }
        catch { setLoadError(true); setSessions(null); return null; }
    }, []);

    // doRevoke 真正执行一条登出。current（本机）会话登出后清本地 auth、回登录页
    // （等价退出登录）；其余刷新列表。403 step_up_required 由调用方分流，不入此处。
    const finishRevoke = useCallback((sess: AuthSession) =>
    {
        if (sess.current) { onSignedOutSelf(); return; }
        toast.ok(t("settings.sessions.revoked"));
        void reload();
    }, [ onSignedOutSelf, reload ]);

    // 进页面拉一次。返回时若检测到 step-up 往返暂存（{code, verifier} + 待登出 id），
    // 先 POST /auth/stepup 记录窗口，再重试那条 DELETE——一次性串起整页跳转两端。
    // 受限访客无权管理会话、后端 GET 会 403，故根本不发请求、直接走说明态。
    useEffect(() =>
    {
        if (isGuest) { return; }
        let cancelled = false;

        const run = async () =>
        {
            // step-up 往返恢复：取出暂存（一次性）。无暂存则普通进页只拉列表。
            const stash = consumeStepUpCode();
            const pendingId = consumePendingRevoke();
            const list = await reload();
            if (cancelled) { return; }

            if (stash && pendingId)
            {
                // 在 revoke 前就从这份列表里查出待登出的那条（尤其 current 标记）：
                // 若登出的正是本机会话，revoke 后令牌即失效，不能再 listSessions。
                const target = list?.find((x) => x.id === pendingId);
                setBusyId(pendingId);
                try
                {
                    await postStepUp(stash.code, stash.verifier);
                    await revokeSession(pendingId);
                    if (cancelled) { return; }
                    if (target) { finishRevoke(target); }
                    else { toast.ok(t("settings.sessions.revoked")); await reload(); }
                }
                catch (e)
                {
                    if (cancelled) { return; }
                    if (e instanceof StepUpRequiredError) { setStepUpRejected(true); }
                    else
                    {
                        toast.error(t("settings.error.delete", {
                            message: e instanceof Error ? e.message : String(e),
                        }));
                    }
                    void reload();
                }
                finally { if (!cancelled) { setBusyId(null); } }
            }
        };

        void run();
        return () => { cancelled = true; };
    }, [ isGuest, reload, finishRevoke ]);

    const handleRevoke = async (sess: AuthSession) =>
    {
        const confirmMsg = sess.current
            ? t("settings.sessions.confirmCurrent")
            : t("settings.sessions.confirmOther", { name: sess.deviceName });
        if (!window.confirm(confirmMsg)) { return; }

        setBusyId(sess.id);
        setStepUpRejected(false);
        try
        {
            await revokeSession(sess.id);
            finishRevoke(sess);
        }
        catch (e)
        {
            if (e instanceof StepUpRequiredError)
            {
                // 需再认证：暂存待登出 id，发起 prompt=login 往返，回到 /settings 后
                // 由 mount effect 接力重试。成功则整页跳走、本组件卸载。
                stashPendingRevoke(sess.id);
                try
                {
                    await startStepUpReauth("/settings");
                    return;   // 跳转去 provider，不再 setBusyId。
                }
                catch (reauthErr)
                {
                    toast.error(t("settings.error.delete", {
                        message: reauthErr instanceof Error ? reauthErr.message : String(reauthErr),
                    }));
                }
            }
            else
            {
                toast.error(t("settings.error.delete", {
                    message: e instanceof Error ? e.message : String(e),
                }));
            }
        }
        finally { setBusyId(null); }
    };

    // 受限访客：不展示列表 / 加载 / 失败态，只给一句克制的说明——管理登录会话需在主设备
    // 上完整登录。后端 GET 本就 403，前面 effect 也已跳过请求，这里纯文案分支。
    if (isGuest)
    {
        return (
            <Panel title={t("settings.sessions.title")}>
                <Callout tone="warn" icon={<ShieldAlert size={16} />}>
                    {t("settings.sessions.guestNote")}
                </Callout>
            </Panel>
        );
    }

    return (
        <Panel title={t("settings.sessions.title")}>
            <Stack gap="sm">
                <Text size="sm" c="dimmed" className="justify" data-jz-level="paragraph">{t("settings.sessions.hint")}</Text>
                {stepUpRejected && (
                    <Callout tone="warn" icon={<AlertTriangle size={16} />}>
                        {t("settings.sessions.stepUpRejected")}
                    </Callout>
                )}
                {loadError ? (
                    <Group>
                        <Text size="sm" style={{ color: "var(--state-error)" }}>
                            {t("settings.sessions.loadError")}
                        </Text>
                        <Button size="sm" variant="ghost" onClick={() => void reload()}>
                            {t("settings.sessions.retry")}
                        </Button>
                    </Group>
                ) : sessions === null ? (
                    // 仍在加载（首屏闪一帧）：不渲染占位文案，避免「暂无」误闪。
                    null
                ) : sessions.length === 0 ? (
                    <Text size="sm" c="dimmed">{t("settings.sessions.empty")}</Text>
                ) : (
                    <Stack gap="xs">
                        {sessions.map((sess) => (
                            <SessionRow
                                key={sess.id}
                                session={sess}
                                busy={busyId === sess.id}
                                // 在线、离线会话都可登出（离线亦走 step-up，流程一致）。
                                onRevoke={() => handleRevoke(sess)}
                            />
                        ))}
                    </Stack>
                )}
            </Stack>
        </Panel>
    );
}

// 会话种类标签：网页 / 扫码会话用 settings.sessions.kind.*；原生客户端
// （macos / windows / linux / ios）复用设备类型标签（「macOS 客户端」等）。
function sessionKindLabel(kind: AuthSession["kind"]): string
{
    if (kind === "oidc" || kind === "self" || kind === "guest")
    {
        return t(`settings.sessions.kind.${kind}` as const);
    }
    return t(`deviceType.${kind}` as const);
}

function SessionRow(props: { session: AuthSession; busy: boolean; onRevoke?: () => void })
{
    const { session, busy, onRevoke } = props;
    const lastUsedText = session.lastUsedAt
        ? t("settings.sessions.lastUsed", { time: formatRelative(session.lastUsedAt) })
        : t("settings.sessions.neverUsed");
    const kindLabel = sessionKindLabel(session.kind);
    const onlineLabel = t(session.online ? "settings.online" : "settings.offline");
    return (
        <Panel variant="sunken" padding="tight">
            <Group justify="space-between" wrap="nowrap" align="center">
                <Stack gap={2} style={{ minWidth: 0, flex: 1 }}>
                    <Group gap={6} wrap="nowrap">
                        <StateDot
                            tone={session.online ? "online" : "offline"}
                            title={onlineLabel}
                        />
                        <Text fw={500} size="sm" truncate>
                            {session.deviceName || kindLabel}
                        </Text>
                        <Badge tone={session.scope === "guest" ? "warn" : "accent"}>
                            {session.scope === "guest"
                                ? t("settings.sessions.scopeGuest")
                                : t("settings.sessions.scopeFull")}
                        </Badge>
                        {session.current && (
                            <Badge tone="online">{t("settings.sessions.current")}</Badge>
                        )}
                    </Group>
                    <Text size="xs" c="dimmed" truncate>
                        <DynText>{onlineLabel}</DynText> · {kindLabel} · <DynText>{lastUsedText}</DynText>
                    </Text>
                </Stack>
                {onRevoke && (
                    <Button
                        size="sm"
                        variant="ghost"
                        loading={busy}
                        leftIcon={<LogOut size={13} />}
                        onClick={onRevoke}
                        style={{ color: "var(--state-error)" }}
                    >
                        {busy
                            ? t("settings.sessions.signingOut")
                            : session.current
                                ? t("settings.sessions.signOutCurrent")
                                : t("settings.sessions.signOut")}
                    </Button>
                )}
            </Group>
        </Panel>
    );
}

