• Settings page component for the Kenmei to AniList sync tool.

    Handles authentication, sync preferences, data management, and cache clearing for the user.

    Returns Element

    export function SettingsPage() {
    const {
    authState,
    login,
    refreshToken,
    logout,
    cancelAuth,
    isLoading,
    error: authError,
    statusMessage,
    setCredentialSource,
    updateCustomCredentials,
    customCredentials,
    } = useAuth();

    const {
    isDebugEnabled,
    toggleDebug,
    storageDebuggerEnabled,
    setStorageDebuggerEnabled,
    logViewerEnabled,
    setLogViewerEnabled,
    stateInspectorEnabled,
    setStateInspectorEnabled,
    } = useDebug();

    const prevCredentialSourceRef = useRef<"default" | "custom">(
    authState.credentialSource,
    );

    const [error, setError] = useState<AppError | null>(null);
    const [cacheCleared, setCacheCleared] = useState(false);
    const [isClearing, setIsClearing] = useState(false);
    const [showStatusMessage, setShowStatusMessage] = useState(true);
    const [cachesToClear, setCachesToClear] = useState({
    auth: false,
    settings: false,
    sync: false,
    import: false,
    review: false,
    manga: false,
    search: false,
    other: false,
    });
    const [useCustomCredentials, setUseCustomCredentials] = useState(
    authState.credentialSource === "custom",
    );
    const [clientId, setClientId] = useState("");
    const [clientSecret, setClientSecret] = useState("");
    const [redirectUri, setRedirectUri] = useState(
    `http://localhost:${DEFAULT_AUTH_PORT}/callback`,
    );
    const [syncConfig, setSyncConfig] = useState<SyncConfig>(getSyncConfig());
    const [matchConfig, setMatchConfig] = useState<MatchConfig>(getMatchConfig());
    const [useCustomThreshold, setUseCustomThreshold] = useState<boolean>(
    typeof syncConfig.autoPauseThreshold === "string" ||
    ![1, 7, 14, 30, 60, 90, 180, 365].includes(
    Number(syncConfig.autoPauseThreshold),
    ),
    );
    // Version status state
    const [versionStatus, setVersionStatus] = useState<AppVersionStatus | null>(
    null,
    );
    // Update Check State
    const [updateChannel, setUpdateChannel] = useState<"stable" | "beta">(
    "stable",
    );
    const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
    const [updateInfo, setUpdateInfo] = useState<null | {
    version: string;
    url: string;
    isBeta: boolean;
    }>(null);
    const [updateError, setUpdateError] = useState<string | null>(null);

    // Handler for opening external links in the default browser
    const handleOpenExternal = (url: string) => (e: React.MouseEvent) => {
    e.preventDefault();
    if (globalThis.electronAPI?.shell?.openExternal) {
    globalThis.electronAPI.shell.openExternal(url);
    } else {
    // Fallback to regular link behavior if not in Electron
    globalThis.open(url, "_blank", "noopener,noreferrer");
    }
    };

    // Track previous credential values to prevent unnecessary updates
    const prevCredentialsRef = useRef({
    id: "",
    secret: "",
    uri: "",
    });

    // Update error state when auth error changes
    useEffect(() => {
    if (authError) {
    setError(createError(ErrorType.AUTHENTICATION, authError));
    } else {
    setError(null);
    }
    }, [authError]);

    // Update credential source when toggle changes, but avoid infinite loop
    useEffect(() => {
    const newSource = useCustomCredentials ? "custom" : "default";
    // Only update if actually changed and not from authState sync
    if (newSource !== prevCredentialSourceRef.current) {
    prevCredentialSourceRef.current = newSource;
    setCredentialSource(newSource);
    }
    }, [useCustomCredentials, setCredentialSource]);

    // Update local state if authState.credentialSource changes externally
    useEffect(() => {
    if (authState.credentialSource !== prevCredentialSourceRef.current) {
    prevCredentialSourceRef.current = authState.credentialSource;
    setUseCustomCredentials(authState.credentialSource === "custom");
    }
    }, [authState.credentialSource]);

    // Update custom credentials when fields change
    useEffect(() => {
    if (useCustomCredentials && clientId && clientSecret && redirectUri) {
    // Only update if values actually changed
    if (
    clientId !== prevCredentialsRef.current.id ||
    clientSecret !== prevCredentialsRef.current.secret ||
    redirectUri !== prevCredentialsRef.current.uri
    ) {
    // Update the ref
    prevCredentialsRef.current = {
    id: clientId,
    secret: clientSecret,
    uri: redirectUri,
    };

    // Update context
    updateCustomCredentials(clientId, clientSecret, redirectUri);
    }
    }
    }, [
    useCustomCredentials,
    clientId,
    clientSecret,
    redirectUri,
    updateCustomCredentials,
    ]);

    // Reset error when auth state changes
    useEffect(() => {
    if (authState.isAuthenticated) {
    setError(null);

    // If we have a status message and authentication is complete,
    // set a timeout to clear the status message
    if (statusMessage && !isLoading) {
    const timer = setTimeout(() => {
    setShowStatusMessage(false);
    }, 3000); // Auto-dismiss after 3 seconds

    return () => clearTimeout(timer);
    }
    } else {
    // Reset the status message visibility when not authenticated
    setShowStatusMessage(true);
    }
    }, [authState.isAuthenticated, statusMessage, isLoading]);

    // Add a timeout to detect stuck loading state
    useEffect(() => {
    let timeoutId: NodeJS.Timeout | null = null;

    if (isLoading) {
    // If loading state persists for more than 20 seconds, trigger a refresh
    timeoutId = setTimeout(() => {
    console.log(
    "Loading state persisted for too long - triggering refresh",
    );
    handleRefreshPage();
    }, 20000);
    }

    return () => {
    if (timeoutId) clearTimeout(timeoutId);
    };
    }, [isLoading]);

    // Add a useEffect to load custom credential settings from localStorage on initial mount
    useEffect(() => {
    try {
    // Load custom credentials toggle state
    const savedUseCustom = localStorage.getItem("useCustomCredentials");
    if (savedUseCustom) {
    setUseCustomCredentials(JSON.parse(savedUseCustom));
    }

    // Load saved custom credentials if they exist
    const savedCustomCreds = localStorage.getItem("customCredentials");
    if (savedCustomCreds) {
    const credentials = JSON.parse(savedCustomCreds);
    setClientId(credentials.clientId || "");
    setClientSecret(credentials.clientSecret || "");
    setRedirectUri(
    credentials.redirectUri ||
    `http://localhost:${DEFAULT_AUTH_PORT}/callback`,
    );

    // Also update context with saved credentials
    if (
    credentials.clientId &&
    credentials.clientSecret &&
    credentials.redirectUri
    ) {
    updateCustomCredentials(
    credentials.clientId,
    credentials.clientSecret,
    credentials.redirectUri,
    );
    }
    }
    } catch (err) {
    console.error("Failed to load saved credential settings:", err);
    }
    }, []);

    // Save custom credentials toggle state whenever it changes
    useEffect(() => {
    localStorage.setItem(
    "useCustomCredentials",
    JSON.stringify(useCustomCredentials),
    );
    }, [useCustomCredentials]);

    // Save custom credentials whenever they change
    useEffect(() => {
    if (clientId || clientSecret || redirectUri) {
    localStorage.setItem(
    "customCredentials",
    JSON.stringify({
    clientId,
    clientSecret,
    redirectUri,
    }),
    );
    }
    }, [clientId, clientSecret, redirectUri]);

    // Initialize fields from customCredentials prop when it changes
    useEffect(() => {
    if (customCredentials) {
    // Use refs to avoid unnecessary state updates
    if (clientId !== customCredentials.clientId) {
    setClientId(customCredentials.clientId);
    }
    if (clientSecret !== customCredentials.clientSecret) {
    setClientSecret(customCredentials.clientSecret);
    }
    if (redirectUri !== customCredentials.redirectUri) {
    setRedirectUri(customCredentials.redirectUri);
    }
    }
    }, [customCredentials]);

    // Version status useEffect
    useEffect(() => {
    let mounted = true;
    getAppVersionStatus().then((status) => {
    if (mounted) setVersionStatus(status);
    });
    return () => {
    mounted = false;
    };
    }, []);

    const handleLogin = async () => {
    try {
    // Create credentials object based on source
    const credentials: APICredentials = useCustomCredentials
    ? {
    source: "custom",
    clientId,
    clientSecret,
    redirectUri,
    }
    : {
    source: "default",
    clientId: DEFAULT_ANILIST_CONFIG.clientId,
    clientSecret: DEFAULT_ANILIST_CONFIG.clientSecret,
    redirectUri: DEFAULT_ANILIST_CONFIG.redirectUri,
    };

    await login(credentials);
    } catch (err: unknown) {
    setError(
    createError(
    ErrorType.AUTHENTICATION,
    err instanceof Error
    ? err.message
    : "Failed to authenticate with AniList. Please try again.",
    ),
    );
    }
    };

    const handleCancelAuth = async () => {
    try {
    await cancelAuth();
    } catch (err) {
    setError(
    createError(
    ErrorType.AUTHENTICATION,
    err instanceof Error
    ? err.message
    : "Failed to cancel authentication. Please try again.",
    ),
    );
    }
    };

    const handleClearCache = async () => {
    setCacheCleared(false);
    setIsClearing(true);
    setError(null);

    const anySelected = Object.values(cachesToClear).some(Boolean);
    if (!anySelected) {
    setIsClearing(false);
    return;
    }

    const getCacheKeysByType = (): Record<string, string[]> => {
    const base: Record<string, string[]> = {
    auth: ["authState", "customCredentials", "useCustomCredentials"],
    search: ["anilist_search_cache"],
    manga: ["anilist_manga_cache"],
    review: ["match_results", "pending_manga", "matching_progress"],
    import: ["kenmei_data", "import_history", "import_stats"],
    sync: ["anilist_sync_history"],
    settings: ["sync_config", "theme"],
    other: ["cache_version"],
    };

    if (!STORAGE_KEYS) return base;

    const pushIfMissing = (arr: string[], value: string) => {
    if (!arr.includes(value)) arr.push(value);
    };

    const rules: { patterns: string[]; target: string[] }[] = [
    { patterns: ["MATCH", "REVIEW"], target: base.review },
    { patterns: ["IMPORT"], target: base.import },
    { patterns: ["CACHE"], target: base.other },
    ];

    for (const [key, value] of Object.entries(STORAGE_KEYS)) {
    if (typeof value !== "string") continue;

    // Apply the first matching rule
    for (const { patterns, target } of rules) {
    if (patterns.some((p) => key.includes(p))) {
    pushIfMissing(target, value);
    break;
    }
    }
    }

    return base;
    };

    const clearExternalCaches = async (services: {
    clearSearchCache?: () => void;
    clearMangaCache?: () => void;
    cacheDebugger?: { resetAllCaches?: () => void };
    }) => {
    try {
    if (
    cachesToClear.search &&
    typeof services.clearSearchCache === "function"
    ) {
    services.clearSearchCache();
    console.log("🧹 Search cache cleared");
    }
    if (
    cachesToClear.manga &&
    typeof services.clearMangaCache === "function"
    ) {
    services.clearMangaCache();
    console.log("🧹 Manga cache cleared");
    }
    if (
    cachesToClear.search &&
    cachesToClear.manga &&
    services.cacheDebugger?.resetAllCaches
    ) {
    services.cacheDebugger.resetAllCaches();
    console.log("🧹 All in-memory caches reset");
    }
    } catch (e) {
    console.warn("Failed to clear external caches", e);
    }
    };

    const clearStorageKeys = (keys: string[]) => {
    for (const cacheKey of keys) {
    try {
    localStorage.removeItem(cacheKey);
    if (
    globalThis.electronStore &&
    typeof globalThis.electronStore.removeItem === "function"
    ) {
    globalThis.electronStore.removeItem(cacheKey);
    console.log(`🧹 Cleared Electron Store cache: ${cacheKey}`);
    }
    console.log(`🧹 Cleared cache: ${cacheKey}`);
    } catch (e) {
    console.warn(`Failed to clear cache: ${cacheKey}`, e);
    }
    }
    };

    const deleteIndexedDB = () => {
    try {
    const req = globalThis.indexedDB?.deleteDatabase("anilist-cache");
    if (!req) return;
    req.onsuccess = () =>
    console.log("🧹 Successfully deleted IndexedDB database");
    req.onerror = () => console.error("Error deleting IndexedDB database");
    } catch (e) {
    console.warn("Failed to clear IndexedDB:", e);
    }
    };

    const showResultSummary = () => {
    const clearedSummary = Object.entries(cachesToClear)
    .filter(([, selected]) => selected)
    .map(([type]) => `✅ Cleared ${type} cache`)
    .join("\n");

    try {
    globalThis.alert(
    "Cache Cleared Successfully!\n\n" +
    clearedSummary +
    "\n\nYou may need to restart the application for all changes to take effect.",
    );
    } catch (e) {
    console.warn("Failed to show alert:", e);
    }
    };

    try {
    // Get all cache clearing functions
    const { clearMangaCache, cacheDebugger } = await import(
    "../api/matching/manga-search-service"
    );
    const { clearSearchCache } = await import("../api/anilist/client");

    await clearExternalCaches({
    clearSearchCache,
    clearMangaCache,
    cacheDebugger,
    });

    const keysByType = getCacheKeysByType();
    const keysToRemove: string[] = [];

    for (const [type, selected] of Object.entries(cachesToClear)) {
    if (!selected) continue;
    const keys = keysByType[type];
    if (Array.isArray(keys)) keysToRemove.push(...keys);
    }

    const uniqueKeys = [...new Set(keysToRemove)];
    console.log("🧹 Clearing the following localStorage keys:", uniqueKeys);

    clearStorageKeys(uniqueKeys);

    if (anySelected) deleteIndexedDB();

    console.log("🧹 Selected caches cleared");
    setCacheCleared(true);
    showResultSummary();
    setTimeout(() => setCacheCleared(false), 5000);
    } catch (err) {
    console.error("Error clearing cache:", err);
    setError(
    createError(
    ErrorType.SYSTEM,
    err instanceof Error
    ? err.message
    : "An unexpected error occurred while clearing cache",
    ),
    );
    } finally {
    setIsClearing(false);
    }
    };

    const dismissError = () => {
    setError(null);
    };

    const handleRefreshPage = () => {
    // Clear error states and status messages
    setError(null);
    globalThis.location.reload();
    };

    const calculateExpiryTime = () => {
    if (!authState.expiresAt) return "unknown";

    const hoursRemaining = Math.round(
    (authState.expiresAt - Date.now()) / 3600000,
    );

    if (hoursRemaining > 24) {
    const days = Math.floor(hoursRemaining / 24);
    const hours = hoursRemaining % 24;
    return `${days}d ${hours}h`;
    }

    return `${hoursRemaining}h`;
    };

    const readLastSyncMetadata = () => {
    if (globalThis.window === undefined || !globalThis.localStorage) {
    return {
    label: "Unavailable",
    hint: "Sync history will appear after your first run.",
    };
    }

    try {
    const historyRaw = globalThis.localStorage.getItem(
    "anilist_sync_history",
    );
    if (!historyRaw) {
    return {
    label: "Never",
    hint: "No syncs have been recorded yet.",
    };
    }

    const history = JSON.parse(historyRaw);
    if (!Array.isArray(history) || history.length === 0) {
    return {
    label: "Never",
    hint: "Run a sync to capture your first history entry.",
    };
    }

    const latest = history[0];
    const timestamp = latest?.timestamp ? new Date(latest.timestamp) : null;
    const formattedDate =
    timestamp && !Number.isNaN(timestamp.valueOf())
    ? timestamp.toLocaleString(undefined, {
    year: "numeric",
    month: "short",
    day: "numeric",
    hour: "2-digit",
    minute: "2-digit",
    })
    : "Recently";

    const summaryBits: string[] = [];
    if (typeof latest?.successfulUpdates === "number") {
    summaryBits.push(`${latest.successfulUpdates} successful`);
    }
    if (
    typeof latest?.failedUpdates === "number" &&
    latest.failedUpdates > 0
    ) {
    summaryBits.push(`${latest.failedUpdates} failed`);
    }
    if (typeof latest?.totalEntries === "number") {
    summaryBits.push(`${latest.totalEntries} total`);
    }

    return {
    label: formattedDate,
    hint:
    summaryBits.length > 0
    ? summaryBits.join(" • ")
    : "Latest sync details captured locally.",
    };
    } catch (err) {
    console.error("Error parsing sync history: ", err);
    return {
    label: "Never",
    hint: "Sync history could not be parsed.",
    };
    }
    };

    // Fetch update info from GitHub
    const handleCheckForUpdates = async () => {
    setIsCheckingUpdate(true);
    setUpdateError(null);
    setUpdateInfo(null);
    try {
    const response = await fetch(
    "https://api.github.com/repos/RLAlpha49/KenmeiToAnilist/releases?per_page=10",
    );
    if (!response.ok) throw new Error("Failed to fetch releases");
    type Release = {
    draft: boolean;
    prerelease: boolean;
    tag_name: string;
    html_url: string;
    body: string;
    };
    const releases: Release[] = await response.json();
    let release: Release | null = null;
    if (updateChannel === "stable") {
    release = releases.find((r) => !r.draft && !r.prerelease) || null;
    } else {
    release =
    releases.find((r) => !r.draft && r.prerelease) ||
    releases.find((r) => !r.draft && !r.prerelease) ||
    null;
    }
    if (!release) throw new Error("No release found for selected channel");
    setUpdateInfo({
    version: release.tag_name,
    url: release.html_url,
    isBeta: !!release.prerelease,
    });
    } catch (e) {
    setUpdateError(e instanceof Error ? e.message : "Unknown error");
    } finally {
    setIsCheckingUpdate(false);
    }
    };

    const defaultCredentialStatus = useMemo(() => {
    const missing: string[] = [];
    const defaultClientId = DEFAULT_ANILIST_CONFIG.clientId?.trim() ?? "";
    const defaultClientSecret =
    DEFAULT_ANILIST_CONFIG.clientSecret?.trim() ?? "";
    if (!defaultClientId) missing.push("Client ID");
    if (!defaultClientSecret) missing.push("Client Secret");
    return {
    hasCredentials: missing.length === 0,
    missing,
    };
    }, []);

    const customCredentialStatus = useMemo(() => {
    const trimmedClientId = clientId.trim();
    const trimmedClientSecret = clientSecret.trim();
    const trimmedRedirectUri = redirectUri.trim();
    const missing: string[] = [];
    if (!trimmedClientId) missing.push("Client ID");
    if (!trimmedClientSecret) missing.push("Client Secret");
    if (!trimmedRedirectUri) missing.push("Redirect URI");
    return {
    complete: missing.length === 0,
    missing,
    };
    }, [clientId, clientSecret, redirectUri]);

    const credentialsBlocked = useCustomCredentials
    ? !customCredentialStatus.complete
    : !defaultCredentialStatus.hasCredentials;
    const disableAuthActions = isLoading || credentialsBlocked;

    const expiresLabel = useMemo(
    () => (authState.isAuthenticated ? calculateExpiryTime() : undefined),
    [authState.expiresAt, authState.isAuthenticated],
    );

    const credentialSourceLabel = useMemo(
    () => (useCustomCredentials ? "Custom credentials" : "Default credentials"),
    [useCustomCredentials],
    );

    const versionLabel = useMemo(() => {
    if (!versionStatus) return undefined;
    if (versionStatus.status === "stable") return "Stable channel";
    if (versionStatus.status === "beta") return "Beta channel";
    if (versionStatus.status === "development") return "Development";
    return undefined;
    }, [versionStatus]);

    const lastSyncMetadata = useMemo(
    () => readLastSyncMetadata(),
    [cacheCleared, authState.isAuthenticated],
    );

    const accountControls = (
    <motion.div
    initial={{ opacity: 0, y: 12 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ duration: 0.35 }}
    className="rounded-3xl border border-slate-200 bg-white/80 p-6 shadow-[inset_0_1px_0_rgba(255,255,255,0.12)] backdrop-blur-md dark:border-white/10 dark:bg-white/5"
    >
    <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
    <div className="space-y-1">
    <h2 className="text-lg font-semibold text-slate-900 dark:text-white">
    AniList authentication
    </h2>
    <p className="text-sm text-slate-600 dark:text-slate-200/80">
    Manage how the app connects to AniList and switch between built-in
    or custom credentials.
    </p>
    </div>
    <div className="flex flex-wrap items-center gap-3">
    <Badge
    className={`border ${authState.isAuthenticated ? "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-400/40 dark:bg-emerald-400/10 dark:text-emerald-100" : "border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-400/40 dark:bg-amber-400/10 dark:text-amber-100"}`}
    >
    {authState.isAuthenticated ? "Session active" : "Session inactive"}
    </Badge>
    <div className="flex items-center gap-2 rounded-full border border-slate-200 bg-white/70 px-3 py-1 dark:border-white/15 dark:bg-white/10">
    <span className="text-xs font-medium text-slate-600 dark:text-slate-100/80">
    Custom keys
    </span>
    <Switch
    checked={useCustomCredentials}
    onCheckedChange={setUseCustomCredentials}
    disabled={authState.isAuthenticated}
    aria-label="Toggle custom AniList credentials"
    />
    </div>
    </div>
    </div>

    {authState.isAuthenticated && (
    <Alert
    variant="destructive"
    className="mt-4 border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-400/40 dark:bg-rose-500/10 dark:text-rose-100"
    >
    <AlertTriangle className="h-4 w-4" />
    <AlertDescription className="text-xs text-rose-600 dark:text-rose-100">
    You must sign out before changing API credentials.
    </AlertDescription>
    </Alert>
    )}

    {!authState.isAuthenticated && (
    <Alert className="mt-4 border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-400/40 dark:bg-amber-500/10 dark:text-amber-50">
    <AlertTriangle className="h-4 w-4" />
    <AlertTitle className="text-amber-700 dark:text-amber-50">
    Not connected
    </AlertTitle>
    <AlertDescription className="text-xs text-amber-600 dark:text-amber-100/80">
    Authenticate with AniList using the hero actions above to enable
    migrations.
    </AlertDescription>
    </Alert>
    )}

    {!useCustomCredentials && !defaultCredentialStatus.hasCredentials && (
    <Alert className="mt-4 border-amber-300 bg-amber-50 text-amber-700 dark:border-amber-400/40 dark:bg-amber-500/10 dark:text-amber-100">
    <AlertTriangle className="h-4 w-4" />
    <AlertTitle className="text-amber-700 dark:text-amber-100">
    Default credentials missing
    </AlertTitle>
    <AlertDescription className="text-xs text-amber-600 dark:text-amber-100/80">
    {`Default credentials missing (${defaultCredentialStatus.missing.join(", ")}). Provide `}
    <code className="font-mono">VITE_ANILIST_CLIENT_ID</code>
    {" and "}
    <code className="font-mono">VITE_ANILIST_CLIENT_SECRET</code>
    {
    " in your environment or switch to custom credentials before signing in."
    }
    </AlertDescription>
    </Alert>
    )}

    {useCustomCredentials && (
    <div className="mt-6 grid gap-4 md:grid-cols-2">
    <div className="grid gap-1.5">
    <label
    htmlFor="client-id"
    className="text-xs font-semibold tracking-wide text-slate-600 uppercase dark:text-slate-200/80"
    >
    Client ID
    </label>
    <input
    id="client-id"
    type="text"
    className="w-full rounded-lg border border-slate-200 bg-white/90 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus-visible:ring-2 focus-visible:ring-indigo-300 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60 dark:border-white/15 dark:bg-slate-950/60 dark:text-white dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-0"
    value={clientId}
    onChange={(e) => setClientId(e.target.value)}
    disabled={authState.isAuthenticated || isLoading}
    placeholder="Your AniList client ID"
    />
    </div>
    <div className="grid gap-1.5">
    <label
    htmlFor="client-secret"
    className="text-xs font-semibold tracking-wide text-slate-600 uppercase dark:text-slate-200/80"
    >
    Client Secret
    </label>
    <input
    id="client-secret"
    type="password"
    className="w-full rounded-lg border border-slate-200 bg-white/90 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus-visible:ring-2 focus-visible:ring-indigo-300 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60 dark:border-white/15 dark:bg-slate-950/60 dark:text-white dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-0"
    value={clientSecret}
    onChange={(e) => setClientSecret(e.target.value)}
    disabled={authState.isAuthenticated || isLoading}
    placeholder="Your AniList client secret"
    />
    </div>
    <div className="grid gap-1.5 md:col-span-2">
    <label
    htmlFor="redirect-uri"
    className="flex items-center gap-1.5 text-xs font-semibold tracking-wide text-slate-600 uppercase dark:text-slate-200/80"
    >
    <Link className="h-3.5 w-3.5" />
    Redirect URI
    </label>
    <input
    id="redirect-uri"
    type="text"
    className="w-full rounded-lg border border-slate-200 bg-white/90 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus-visible:ring-2 focus-visible:ring-indigo-300 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60 dark:border-white/15 dark:bg-slate-950/60 dark:text-white dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-0"
    value={redirectUri}
    onChange={(e) => setRedirectUri(e.target.value)}
    disabled={authState.isAuthenticated || isLoading}
    placeholder={`http://localhost:${DEFAULT_AUTH_PORT}/callback`}
    />
    <p className="text-xs text-slate-600 dark:text-slate-200/70">
    Must match the redirect URI registered in your AniList
    application.
    </p>
    </div>
    <p className="text-xs text-slate-600 md:col-span-2 dark:text-slate-200/70">
    You can create a new client in{" "}
    <a
    href="https://anilist.co/settings/developer"
    target="_blank"
    rel="noopener noreferrer"
    className="text-indigo-600 underline-offset-4 transition hover:text-indigo-500 hover:underline dark:text-indigo-200 dark:hover:text-indigo-100"
    onClick={handleOpenExternal(
    "https://anilist.co/settings/developer",
    )}
    >
    AniList Developer Settings
    </a>
    </p>
    {!customCredentialStatus.complete && (
    <Alert className="border-rose-200 bg-rose-50 text-rose-700 md:col-span-2 dark:border-rose-400/40 dark:bg-rose-500/10 dark:text-rose-100">
    <AlertTriangle className="h-4 w-4" />
    <AlertDescription className="text-xs text-rose-600 dark:text-rose-100">
    {`Custom credentials incomplete. Missing: ${customCredentialStatus.missing.join(", ")}. All fields are required before authenticating.`}
    </AlertDescription>
    </Alert>
    )}
    </div>
    )}

    <div className="mt-6 flex flex-wrap items-center gap-3 text-xs text-slate-600 dark:text-slate-200/80">
    <span className="inline-flex items-center gap-1 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-slate-700 dark:border-white/15 dark:bg-white/10 dark:text-slate-200/80">
    <Clock className="h-3 w-3" />
    {authState.isAuthenticated
    ? `Token expires in ${expiresLabel ?? "unknown"}`
    : "No active session"}
    </span>
    <span className="inline-flex items-center gap-1 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-slate-700 dark:border-white/15 dark:bg-white/10 dark:text-slate-200/80">
    <Key className="h-3 w-3" />
    {credentialSourceLabel}
    </span>
    <span className="inline-flex items-center gap-1 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-slate-700 dark:border-white/15 dark:bg-white/10 dark:text-slate-200/80">
    <ShieldCheck className="h-3 w-3" />
    Stored locally only
    </span>
    </div>
    </motion.div>
    );

    return (
    <motion.div
    className="relative mx-auto max-w-[1200px] space-y-8 px-4 pt-6 pb-12 md:px-6"
    initial={{ opacity: 0 }}
    animate={{ opacity: 1 }}
    transition={{ duration: 0.4 }}
    >
    <SettingsHero
    isAuthenticated={authState.isAuthenticated}
    username={authState.username}
    avatarUrl={authState.avatarUrl}
    statusMessage={
    statusMessage && showStatusMessage && !error ? statusMessage : null
    }
    isLoading={isLoading}
    disableLogin={disableAuthActions}
    onLogin={handleLogin}
    onRefreshToken={refreshToken}
    onLogout={logout}
    onClearStatus={() => setShowStatusMessage(false)}
    onCancelAuth={handleCancelAuth}
    credentialSourceLabel={credentialSourceLabel}
    expiresLabel={expiresLabel}
    versionLabel={versionLabel}
    >
    {accountControls}
    </SettingsHero>

    {error && (
    <motion.div
    className="rounded-3xl border border-rose-400/40 bg-rose-500/10 p-4 backdrop-blur-lg"
    initial={{ opacity: 0, y: -12 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ duration: 0.35 }}
    >
    <ErrorMessage
    message={error.message}
    type={error.type}
    dismiss={dismissError}
    retry={
    error.type === ErrorType.AUTHENTICATION ? handleLogin : undefined
    }
    />
    </motion.div>
    )}

    {cacheCleared && (
    <motion.div
    initial={{ opacity: 0, y: 12 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ duration: 0.3 }}
    className="rounded-3xl border border-emerald-400/40 bg-emerald-500/10 px-4 py-3 text-sm text-emerald-100 backdrop-blur-lg"
    >
    <div className="flex items-center gap-2">
    <CheckCircle className="h-4 w-4" />
    <span>Selected caches cleared successfully.</span>
    </div>
    </motion.div>
    )}

    <motion.div
    initial={{ opacity: 0, y: 20 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ duration: 0.5, delay: 0.2 }}
    className="overflow-hidden rounded-[28px] border border-slate-200 bg-white/80 p-6 shadow-[0_40px_90px_-60px_rgba(15,23,42,0.15)] backdrop-blur-xl dark:border-white/10 dark:bg-slate-950/40 dark:shadow-[0_40px_90px_-60px_rgba(15,23,42,0.9)]"
    >
    <Tabs defaultValue="matching" className="space-y-6">
    <TabsList className="flex w-full flex-col gap-2 rounded-2xl border border-slate-200 bg-white/80 p-5 text-sm text-slate-600 backdrop-blur md:flex-row md:items-center md:justify-start md:gap-3 dark:border-white/10 dark:bg-white/5 dark:text-slate-300">
    <TabsTrigger
    value="matching"
    className="flex flex-1 items-center justify-center gap-1.5 rounded-xl border border-transparent px-5 py-3.5 font-medium text-slate-600 transition hover:border-slate-200 hover:text-slate-900 data-[state=active]:border-slate-200 data-[state=active]:bg-white data-[state=active]:text-slate-900 data-[state=active]:shadow-lg dark:hover:border-white/20 dark:hover:text-white dark:data-[state=active]:border-transparent dark:data-[state=active]:bg-white/20 dark:data-[state=active]:text-white"
    >
    <Search className="h-4 w-4" />
    Matching
    </TabsTrigger>
    <TabsTrigger
    value="sync"
    className="flex flex-1 items-center justify-center gap-1.5 rounded-xl border border-transparent px-5 py-3.5 font-medium text-slate-600 transition hover:border-slate-200 hover:text-slate-900 data-[state=active]:border-slate-200 data-[state=active]:bg-white data-[state=active]:text-slate-900 data-[state=active]:shadow-lg dark:hover:border-white/20 dark:hover:text-white dark:data-[state=active]:border-transparent dark:data-[state=active]:bg-white/20 dark:data-[state=active]:text-white"
    >
    <RefreshCw className="h-4 w-4" />
    Sync
    </TabsTrigger>
    <TabsTrigger
    value="data"
    className="flex flex-1 items-center justify-center gap-1.5 rounded-xl border border-transparent px-5 py-3.5 font-medium text-slate-600 transition hover:border-slate-200 hover:text-slate-900 data-[state=active]:border-slate-200 data-[state=active]:bg-white data-[state=active]:text-slate-900 data-[state=active]:shadow-lg dark:hover:border-white/20 dark:hover:text-white dark:data-[state=active]:border-transparent dark:data-[state=active]:bg-white/20 dark:data-[state=active]:text-white"
    >
    <Database className="h-4 w-4" />
    Data
    </TabsTrigger>
    </TabsList>
    <TabsContent value="matching" className="space-y-6">
    <motion.div variants={itemVariants} initial="hidden" animate="show">
    <SettingsSectionShell
    icon={Search}
    title="Matching preferences"
    description="Configure how your manga is matched with AniList entries during search and sync."
    accent="from-emerald-500/15 via-teal-500/10 to-transparent"
    contentClassName="space-y-5"
    >
    <motion.div
    className="bg-muted/40 rounded-xl border p-4"
    initial={{ opacity: 0, y: 8 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ delay: 0.2, duration: 0.4 }}
    >
    <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
    <div className="space-y-0.5">
    <h3 className="text-sm font-medium">
    Ignore one shots in automatic matching
    </h3>
    <p className="text-muted-foreground text-xs">
    Skip one-shot manga during automatic matching. They will
    still appear in manual searches.
    </p>
    </div>
    <Switch
    id="ignore-one-shots"
    checked={matchConfig.ignoreOneShots}
    onCheckedChange={(checked) => {
    const newConfig = {
    ...matchConfig,
    ignoreOneShots: checked,
    };
    setMatchConfig(newConfig);
    saveMatchConfig(newConfig);
    }}
    />
    </div>
    </motion.div>

    <motion.div
    className="bg-muted/40 rounded-xl border p-4"
    initial={{ opacity: 0, y: 8 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ delay: 0.25, duration: 0.4 }}
    >
    <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
    <div className="space-y-0.5">
    <h3 className="text-sm font-medium">
    Ignore adult content in automatic matching
    </h3>
    <p className="text-muted-foreground text-xs">
    Skip adult content manga during automatic matching. They
    will still appear in manual searches.
    </p>
    </div>
    <Switch
    id="ignore-adult-content"
    checked={matchConfig.ignoreAdultContent}
    onCheckedChange={(checked) => {
    const newConfig = {
    ...matchConfig,
    ignoreAdultContent: checked,
    };
    setMatchConfig(newConfig);
    saveMatchConfig(newConfig);
    }}
    />
    </div>
    </motion.div>

    <motion.div
    className="bg-muted/40 rounded-xl border p-4"
    initial={{ opacity: 0, y: 8 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ delay: 0.3, duration: 0.4 }}
    >
    <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
    <div className="space-y-0.5">
    <h3 className="text-sm font-medium">
    Blur adult content images
    </h3>
    <p className="text-muted-foreground text-xs">
    Blur cover images of adult content manga for privacy.
    Click to reveal the image temporarily.
    </p>
    </div>
    <Switch
    id="blur-adult-content"
    checked={matchConfig.blurAdultContent}
    onCheckedChange={(checked) => {
    const newConfig = {
    ...matchConfig,
    blurAdultContent: checked,
    };
    setMatchConfig(newConfig);
    saveMatchConfig(newConfig);
    }}
    />
    </div>
    </motion.div>

    <motion.div
    className="bg-muted/40 rounded-xl border p-4"
    initial={{ opacity: 0, y: 8 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ delay: 0.35, duration: 0.4 }}
    >
    <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
    <div className="space-y-0.5">
    <div className="flex items-center gap-2">
    <h3 className="text-sm font-medium">
    Enable Comick alternative search
    </h3>
    <Badge
    variant="secondary"
    className="bg-yellow-100 text-xs text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300"
    >
    Disabled
    </Badge>
    </div>
    <p className="text-muted-foreground text-xs">
    Comick fallback search is temporarily disabled as the
    service has been taken down. The API may return once
    Comick fully transitions as a tracking site.
    </p>
    </div>
    <Switch
    id="enable-comick-search"
    checked={false}
    disabled
    onCheckedChange={() => {
    // No-op: Comick fallback is disabled
    }}
    />
    </div>
    </motion.div>

    <motion.div
    className="bg-muted/40 rounded-xl border p-4"
    initial={{ opacity: 0, y: 8 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ delay: 0.4, duration: 0.4 }}
    >
    <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
    <div className="space-y-0.5">
    <h3 className="text-sm font-medium">
    Enable MangaDex alternative search
    </h3>
    <p className="text-muted-foreground text-xs">
    Use MangaDex as a fallback to find manga when AniList
    search returns no results. Will be ignored when rate
    limited and continue searching normally.
    </p>
    </div>
    <Switch
    id="enable-mangadex-search"
    checked={matchConfig.enableMangaDexSearch}
    onCheckedChange={(checked) => {
    const newConfig = {
    ...matchConfig,
    enableMangaDexSearch: checked,
    };
    setMatchConfig(newConfig);
    saveMatchConfig(newConfig);
    }}
    />
    </div>
    </motion.div>

    <motion.div
    initial={{ opacity: 0, y: 8 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ delay: 0.45, duration: 0.4 }}
    >
    <Alert className="border-blue-200 bg-blue-50/50 dark:border-blue-900/50 dark:bg-blue-950/50">
    <InfoIcon className="h-4 w-4 text-blue-600 dark:text-blue-400" />
    <AlertTitle className="text-blue-800 dark:text-blue-200">
    About matching settings
    </AlertTitle>
    <AlertDescription className="text-blue-700 dark:text-blue-300">
    Some settings only affect automatic matching. All manga
    types will still be available when using manual search
    functionality.
    </AlertDescription>
    </Alert>
    </motion.div>
    </SettingsSectionShell>
    </motion.div>
    </TabsContent>

    <TabsContent value="sync" className="space-y-6">
    <motion.div variants={itemVariants} initial="hidden" animate="show">
    <SettingsSectionShell
    icon={RefreshCw}
    title="Sync preferences"
    description="Control how Kenmei data is synchronized to your AniList library."
    accent="from-purple-500/15 via-blue-500/10 to-transparent"
    contentClassName="space-y-5"
    >
    <motion.div
    className="bg-muted/40 rounded-xl border p-4"
    initial={{ opacity: 0, y: 8 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ delay: 0.2, duration: 0.4 }}
    >
    <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
    <div className="space-y-1">
    <h3 className="text-sm font-medium">
    Auto-pause inactive manga
    </h3>
    <p className="text-muted-foreground text-xs">
    Automatically pause manga that haven&apos;t been updated
    recently.
    </p>
    </div>
    <Switch
    id="auto-pause"
    checked={syncConfig.autoPauseInactive}
    onCheckedChange={(checked) => {
    const newConfig = {
    ...syncConfig,
    autoPauseInactive: checked,
    };
    setSyncConfig(newConfig);
    saveSyncConfig(newConfig);
    }}
    />
    </div>

    <div className="mt-4 space-y-4">
    <div className="grid gap-1.5">
    <label
    htmlFor="auto-pause-threshold"
    className="text-xs font-medium"
    >
    Auto-pause threshold
    </label>
    <select
    id="auto-pause-threshold"
    className="border-input bg-background ring-offset-background focus-visible:ring-ring w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
    value={
    useCustomThreshold
    ? "custom"
    : syncConfig.autoPauseThreshold.toString()
    }
    onChange={(e) => {
    const value = e.target.value;
    if (value === "custom") {
    setUseCustomThreshold(true);
    } else {
    setUseCustomThreshold(false);
    const newConfig = {
    ...syncConfig,
    autoPauseThreshold: Number(value),
    };
    setSyncConfig(newConfig);
    saveSyncConfig(newConfig);
    }
    }}
    disabled={!syncConfig.autoPauseInactive}
    >
    <option value="1">1 day</option>
    <option value="7">7 days</option>
    <option value="14">14 days</option>
    <option value="30">30 days</option>
    <option value="60">2 months</option>
    <option value="90">3 months</option>
    <option value="180">6 months</option>
    <option value="365">1 year</option>
    <option value="custom">Custom...</option>
    </select>
    </div>

    {useCustomThreshold && (
    <div className="grid gap-1.5">
    <label
    htmlFor="custom-auto-pause-threshold"
    className="text-xs font-medium"
    >
    Custom threshold (days)
    </label>
    <input
    id="custom-auto-pause-threshold"
    type="number"
    min="1"
    className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
    placeholder="Enter days"
    value={
    syncConfig.customAutoPauseThreshold ||
    syncConfig.autoPauseThreshold
    }
    onChange={(e) => {
    const value = Number.parseInt(e.target.value);
    if (!Number.isNaN(value) && value > 0) {
    const newConfig = {
    ...syncConfig,
    autoPauseThreshold: value,
    customAutoPauseThreshold: value,
    };
    setSyncConfig(newConfig);
    saveSyncConfig(newConfig);
    }
    }}
    disabled={!syncConfig.autoPauseInactive}
    />
    </div>
    )}

    <Alert className="bg-amber-50 text-amber-700 dark:bg-amber-900/20 dark:text-amber-300">
    <AlertTriangle className="h-4 w-4" />
    <AlertDescription className="text-xs">
    Auto-pause applies to manga with status READING.
    </AlertDescription>
    </Alert>
    </div>
    </motion.div>

    <motion.div
    className="bg-muted/40 rounded-xl border p-4"
    initial={{ opacity: 0, y: 8 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ delay: 0.25, duration: 0.4 }}
    >
    <div className="mb-4">
    <h3 className="text-sm font-medium">Status priority</h3>
    <p className="text-muted-foreground text-xs">
    Configure which AniList values override Kenmei data during
    sync.
    </p>
    </div>
    <div className="space-y-3">
    <div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
    <label className="text-sm" htmlFor="preserve-completed">
    Preserve completed status
    </label>
    <Switch
    id="preserve-completed"
    checked={syncConfig.preserveCompletedStatus}
    onCheckedChange={(checked) => {
    const newConfig = {
    ...syncConfig,
    preserveCompletedStatus: checked,
    };
    setSyncConfig(newConfig);
    saveSyncConfig(newConfig);
    }}
    />
    </div>
    <div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
    <label
    className="text-sm"
    htmlFor="prioritize-anilist-status"
    >
    Prioritize AniList status
    </label>
    <Switch
    id="prioritize-anilist-status"
    checked={syncConfig.prioritizeAniListStatus}
    onCheckedChange={(checked) => {
    const newConfig = {
    ...syncConfig,
    prioritizeAniListStatus: checked,
    };
    setSyncConfig(newConfig);
    saveSyncConfig(newConfig);
    }}
    />
    </div>
    <div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
    <label
    className="text-sm"
    htmlFor="prioritize-anilist-progress"
    >
    Prioritize AniList progress
    </label>
    <Switch
    id="prioritize-anilist-progress"
    checked={syncConfig.prioritizeAniListProgress}
    onCheckedChange={(checked) => {
    const newConfig = {
    ...syncConfig,
    prioritizeAniListProgress: checked,
    };
    setSyncConfig(newConfig);
    saveSyncConfig(newConfig);
    }}
    />
    </div>
    <div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
    <label
    className="text-sm"
    htmlFor="prioritize-anilist-score"
    >
    Prioritize AniList score
    </label>
    <Switch
    id="prioritize-anilist-score"
    checked={syncConfig.prioritizeAniListScore}
    onCheckedChange={(checked) => {
    const newConfig = {
    ...syncConfig,
    prioritizeAniListScore: checked,
    };
    setSyncConfig(newConfig);
    saveSyncConfig(newConfig);
    }}
    />
    </div>
    </div>
    </motion.div>

    <motion.div
    className="bg-muted/40 rounded-xl border p-4"
    initial={{ opacity: 0, y: 8 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ delay: 0.3, duration: 0.4 }}
    >
    <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
    <div className="space-y-0.5">
    <h3 className="text-sm font-medium">Privacy settings</h3>
    <p className="text-muted-foreground text-xs">
    Control privacy for synchronized entries.
    </p>
    </div>
    <div className="flex items-center gap-2">
    <label className="text-sm" htmlFor="set-private">
    Set entries as private
    </label>
    <Switch
    id="set-private"
    checked={syncConfig.setPrivate}
    onCheckedChange={(checked) => {
    const newConfig = {
    ...syncConfig,
    setPrivate: checked,
    };
    setSyncConfig(newConfig);
    saveSyncConfig(newConfig);
    }}
    />
    </div>
    </div>
    </motion.div>
    </SettingsSectionShell>
    </motion.div>
    </TabsContent>

    <TabsContent value="data" className="space-y-6">
    <motion.div variants={itemVariants} initial="hidden" animate="show">
    <SettingsSectionShell
    icon={Database}
    title="Data management"
    description="Control cached data stored locally by the application."
    accent="from-sky-500/15 via-cyan-500/10 to-transparent"
    contentClassName="space-y-5"
    >
    <motion.div
    className="bg-muted/40 space-y-4 rounded-xl border p-4"
    initial={{ opacity: 0, y: 8 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ delay: 0.2, duration: 0.4 }}
    >
    <div>
    <h3 className="flex items-center gap-2 text-sm font-medium">
    <Trash2 className="h-4 w-4 text-blue-500" />
    Clear local cache
    </h3>
    <p className="text-muted-foreground text-xs">
    Select which types of cached data to remove.
    </p>
    </div>

    <Separator />

    <div className="grid gap-4 md:grid-cols-2">
    <div className="space-y-2">
    <label
    className="hover:bg-muted flex items-center gap-2 rounded-md p-2"
    htmlFor="auth-cache"
    aria-label="Auth Cache - Authentication state"
    >
    <input
    id="auth-cache"
    type="checkbox"
    className="border-primary text-primary h-4 w-4 rounded"
    checked={cachesToClear.auth}
    onChange={(e) =>
    setCachesToClear({
    ...cachesToClear,
    auth: e.target.checked,
    })
    }
    />
    <div>
    <span className="text-sm font-medium">
    Auth cache
    </span>
    <p className="text-muted-foreground text-xs">
    Authentication state
    </p>
    </div>
    </label>
    <label
    className="hover:bg-muted flex items-center gap-2 rounded-md p-2"
    htmlFor="settings-cache"
    aria-label="Settings Cache - Sync preferences"
    >
    <input
    id="settings-cache"
    type="checkbox"
    className="border-primary text-primary h-4 w-4 rounded"
    checked={cachesToClear.settings}
    onChange={(e) =>
    setCachesToClear({
    ...cachesToClear,
    settings: e.target.checked,
    })
    }
    />
    <div>
    <span className="text-sm font-medium">
    Settings cache
    </span>
    <p className="text-muted-foreground text-xs">
    Sync preferences
    </p>
    </div>
    </label>
    <label
    className="hover:bg-muted flex items-center gap-2 rounded-md p-2"
    htmlFor="sync-cache"
    aria-label="Sync Cache - Sync history"
    >
    <input
    id="sync-cache"
    type="checkbox"
    className="border-primary text-primary h-4 w-4 rounded"
    checked={cachesToClear.sync}
    onChange={(e) =>
    setCachesToClear({
    ...cachesToClear,
    sync: e.target.checked,
    })
    }
    />
    <div>
    <span className="text-sm font-medium">
    Sync cache
    </span>
    <p className="text-muted-foreground text-xs">
    Sync history
    </p>
    </div>
    </label>
    <label
    className="hover:bg-muted flex items-center gap-2 rounded-md p-2"
    htmlFor="import-cache"
    aria-label="Import Cache - Import history"
    >
    <input
    id="import-cache"
    type="checkbox"
    className="border-primary text-primary h-4 w-4 rounded"
    checked={cachesToClear.import}
    onChange={(e) =>
    setCachesToClear({
    ...cachesToClear,
    import: e.target.checked,
    })
    }
    />
    <div>
    <span className="text-sm font-medium">
    Import cache
    </span>
    <p className="text-muted-foreground text-xs">
    Import history
    </p>
    </div>
    </label>
    </div>

    <div className="space-y-2">
    <label
    className="hover:bg-muted flex items-center gap-2 rounded-md p-2"
    htmlFor="review-cache"
    aria-label="Review Cache - Match results"
    >
    <input
    id="review-cache"
    type="checkbox"
    className="border-primary text-primary h-4 w-4 rounded"
    checked={cachesToClear.review}
    onChange={(e) =>
    setCachesToClear({
    ...cachesToClear,
    review: e.target.checked,
    })
    }
    />
    <div>
    <span className="text-sm font-medium">
    Review cache
    </span>
    <p className="text-muted-foreground text-xs">
    Matching results
    </p>
    </div>
    </label>
    <label
    className="hover:bg-muted flex items-center gap-2 rounded-md p-2"
    htmlFor="manga-cache"
    aria-label="Manga Cache - AniList manga data"
    >
    <input
    id="manga-cache"
    type="checkbox"
    className="border-primary text-primary h-4 w-4 rounded"
    checked={cachesToClear.manga}
    onChange={(e) =>
    setCachesToClear({
    ...cachesToClear,
    manga: e.target.checked,
    })
    }
    />
    <div>
    <span className="text-sm font-medium">
    Manga cache
    </span>
    <p className="text-muted-foreground text-xs">
    Manga metadata
    </p>
    </div>
    </label>
    <label
    className="hover:bg-muted flex items-center gap-2 rounded-md p-2"
    htmlFor="search-cache"
    aria-label="Search Cache - Search results"
    >
    <input
    id="search-cache"
    type="checkbox"
    className="border-primary text-primary h-4 w-4 rounded"
    checked={cachesToClear.search}
    onChange={(e) =>
    setCachesToClear({
    ...cachesToClear,
    search: e.target.checked,
    })
    }
    />
    <div>
    <span className="text-sm font-medium">
    Search cache
    </span>
    <p className="text-muted-foreground text-xs">
    Search results
    </p>
    </div>
    </label>
    <label
    className="hover:bg-muted flex items-center gap-2 rounded-md p-2"
    htmlFor="other-caches"
    aria-label="Other Caches - Miscellaneous cache data"
    >
    <input
    id="other-caches"
    type="checkbox"
    className="border-primary text-primary h-4 w-4 rounded"
    checked={cachesToClear.other}
    onChange={(e) =>
    setCachesToClear({
    ...cachesToClear,
    other: e.target.checked,
    })
    }
    />
    <div>
    <span className="text-sm font-medium">
    Other caches
    </span>
    <p className="text-muted-foreground text-xs">
    Miscellaneous application data
    </p>
    </div>
    </label>
    </div>
    </div>

    <div className="flex flex-wrap items-center justify-between gap-2">
    <Button
    variant="link"
    size="sm"
    className="h-auto p-0 text-xs text-blue-600 dark:text-blue-400"
    onClick={() =>
    setCachesToClear({
    auth: true,
    settings: true,
    sync: true,
    import: true,
    review: true,
    manga: true,
    search: true,
    other: true,
    })
    }
    >
    Select all
    </Button>
    <Button
    variant="link"
    size="sm"
    className="h-auto p-0 text-xs text-blue-600 dark:text-blue-400"
    onClick={() =>
    setCachesToClear({
    auth: false,
    settings: false,
    sync: false,
    import: false,
    review: false,
    manga: false,
    search: false,
    other: false,
    })
    }
    >
    Deselect all
    </Button>
    </div>

    {(() => {
    let buttonContent;
    if (isClearing) {
    buttonContent = (
    <>
    <RefreshCw className="mr-2 h-4 w-4 animate-spin" />
    Clearing cache...
    </>
    );
    } else if (cacheCleared) {
    buttonContent = (
    <>
    <CheckCircle className="mr-2 h-4 w-4" />
    Cache cleared successfully
    </>
    );
    } else {
    buttonContent = (
    <>
    <Trash2 className="mr-2 h-4 w-4" />
    Clear selected caches
    </>
    );
    }
    return (
    <Button
    onClick={handleClearCache}
    variant={cacheCleared ? "outline" : "default"}
    disabled={
    isClearing ||
    !Object.values(cachesToClear).some(Boolean)
    }
    className={`w-full disabled:cursor-not-allowed disabled:opacity-60 ${
    cacheCleared
    ? "bg-green-50 text-green-600 hover:bg-green-100 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/40"
    : ""
    }`}
    >
    {buttonContent}
    </Button>
    );
    })()}
    </motion.div>
    </SettingsSectionShell>
    </motion.div>

    <motion.div variants={itemVariants} initial="hidden" animate="show">
    <SettingsSectionShell
    icon={Bug}
    title="Debug tools"
    description="Advanced utilities for troubleshooting and development."
    accent="from-orange-500/15 via-red-500/10 to-transparent"
    contentClassName="space-y-4"
    >
    <motion.div
    className="bg-muted/40 space-y-4 rounded-xl border p-4"
    initial={{ opacity: 0, y: 8 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ delay: 0.2, duration: 0.4 }}
    >
    <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
    <div className="space-y-0.5">
    <h3 className="text-sm font-medium">Debug menu</h3>
    <p className="text-muted-foreground text-xs">
    Enable the in-app debug hub to access advanced
    diagnostics.
    </p>
    </div>
    <div className="flex items-center gap-2">
    <label className="text-sm" htmlFor="debug-enabled">
    Enable debug menu
    </label>
    <Switch
    id="debug-enabled"
    checked={isDebugEnabled}
    onCheckedChange={toggleDebug}
    />
    </div>
    </div>
    {isDebugEnabled ? (
    <div className="space-y-4">
    <div className="rounded-md bg-yellow-50 p-3 dark:bg-yellow-900/20">
    <div className="flex gap-3">
    <AlertTriangle className="mt-0.5 h-4 w-4 text-yellow-400" />
    <p className="text-sm text-yellow-800 dark:text-yellow-200">
    <strong>Heads up:</strong> Debug tools expose
    persistent data and captured logs that may include
    sensitive information. Enable them only on trusted
    devices and disable when finished troubleshooting.
    </p>
    </div>
    </div>
    <div className="border-border/60 bg-background/40 rounded-2xl border border-dashed p-4">
    <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
    <div className="space-y-1">
    <div className="flex items-center gap-2">
    <h4 className="text-sm font-semibold">
    Log viewer
    </h4>
    </div>
    <p className="text-muted-foreground text-xs">
    Inspect captured console output, filter by
    severity, and export logs for support.
    </p>
    </div>
    <div className="flex items-center gap-2">
    <span className="text-muted-foreground text-xs">
    Enable panel
    </span>
    <Switch
    id="log-viewer-enabled"
    checked={logViewerEnabled}
    onCheckedChange={(checked) =>
    setLogViewerEnabled(Boolean(checked))
    }
    />
    </div>
    </div>
    <p className="text-muted-foreground mt-3 text-xs">
    Logs can contain access tokens or other
    personally-identifiable information. Review before
    exporting or sharing.
    </p>
    </div>
    <div className="border-border/60 bg-background/40 rounded-2xl border border-dashed p-4">
    <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
    <div className="space-y-1">
    <div className="flex items-center gap-2">
    <h4 className="text-sm font-semibold">
    Storage debugger
    </h4>
    </div>
    <p className="text-muted-foreground text-xs">
    Inspect and edit Electron Store alongside
    localStorage from the debug command center.
    </p>
    </div>
    <div className="flex items-center gap-2">
    <span className="text-muted-foreground text-xs">
    Enable panel
    </span>
    <Switch
    id="storage-debugger-enabled"
    checked={storageDebuggerEnabled}
    onCheckedChange={(checked) =>
    setStorageDebuggerEnabled(Boolean(checked))
    }
    />
    </div>
    </div>
    </div>
    <div className="border-border/60 bg-background/40 rounded-2xl border border-dashed p-4">
    <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
    <div className="space-y-1">
    <div className="flex items-center gap-2">
    <h4 className="text-sm font-semibold">
    State inspector
    </h4>
    </div>
    <p className="text-muted-foreground text-xs">
    View live auth, sync, and settings state. Edit
    snapshots to simulate runtime scenarios safely.
    </p>
    </div>
    <div className="flex items-center gap-2">
    <span className="text-muted-foreground text-xs">
    Enable panel
    </span>
    <Switch
    id="state-inspector-enabled"
    checked={stateInspectorEnabled}
    onCheckedChange={(checked) =>
    setStateInspectorEnabled(Boolean(checked))
    }
    />
    </div>
    </div>
    <p className="text-muted-foreground mt-3 text-xs">
    Changes applied through the inspector update in-app
    state immediately. Export values before experimenting
    for easy rollback.
    </p>
    </div>
    </div>
    ) : (
    <p className="text-muted-foreground text-xs">
    Turn on the debug menu to manage individual tools.
    </p>
    )}
    </motion.div>
    </SettingsSectionShell>
    </motion.div>
    </TabsContent>
    </Tabs>
    </motion.div>

    {/* Check for Updates Section */}
    <SettingsSectionShell
    icon={RefreshCw}
    title="Check for updates"
    description="Stay current with the latest Kenmei → AniList improvements."
    accent="from-sky-500/15 via-blue-500/10 to-transparent"
    className="mt-6"
    contentClassName="space-y-5"
    >
    <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
    <RadioGroup
    value={updateChannel}
    onValueChange={(v) => setUpdateChannel(v as "stable" | "beta")}
    className="flex flex-row gap-4"
    aria-label="Update Channel"
    >
    <div className="flex items-center gap-2">
    <RadioGroupItem value="stable" id="update-stable" />
    <label htmlFor="update-stable" className="text-sm font-medium">
    Stable
    </label>
    </div>
    <div className="flex items-center gap-2">
    <RadioGroupItem value="beta" id="update-beta" />
    <label htmlFor="update-beta" className="text-sm font-medium">
    Beta/Early Access
    </label>
    </div>
    </RadioGroup>
    <Button
    onClick={handleCheckForUpdates}
    disabled={isCheckingUpdate}
    aria-label="Check for updates"
    className="w-full md:w-auto"
    >
    {isCheckingUpdate ? (
    <>
    <Loader2 className="mr-2 h-4 w-4 animate-spin" />
    Checking...
    </>
    ) : (
    <>
    <RefreshCw className="mr-2 h-4 w-4" />
    Check for Updates
    </>
    )}
    </Button>
    </div>
    {updateError && (
    <div className="rounded-2xl border border-rose-400/40 bg-rose-500/10 px-4 py-2 text-sm text-rose-100">
    {updateError}
    </div>
    )}
    {updateInfo && (
    <div className="rounded-2xl border border-white/10 bg-white/5 p-4">
    <div className="mb-3 flex flex-wrap items-center gap-3">
    <Badge
    className={
    updateInfo.isBeta
    ? "bg-yellow-50 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300"
    : "bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-300"
    }
    >
    {updateInfo.isBeta ? "Beta/Early Access" : "Stable"}
    </Badge>
    <span className="font-mono text-xs text-slate-200">
    Latest: {updateInfo.version}
    </span>
    <button
    type="button"
    aria-label="View release on GitHub"
    className="text-blue-300 underline transition hover:text-blue-200"
    onClick={handleOpenExternal(updateInfo.url)}
    onKeyDown={(e) => {
    if (e.key === "Enter" || e.key === " ") {
    e.preventDefault();
    if (globalThis.electronAPI?.shell?.openExternal) {
    globalThis.electronAPI.shell.openExternal(updateInfo.url);
    } else {
    globalThis.open(
    updateInfo.url,
    "_blank",
    "noopener,noreferrer",
    );
    }
    }
    }}
    >
    View release notes
    </button>
    </div>
    <div className="flex flex-wrap items-center gap-3 text-xs text-slate-200">
    <span className="font-mono">Current: {getAppVersion()}</span>
    {(() => {
    const current = getAppVersion().replace(/^v/, "");
    const latest = updateInfo.version.replace(/^v/, "");
    if (current === latest) {
    return (
    <Badge className="bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300">
    Up to date
    </Badge>
    );
    }
    if (compareVersions(current, latest) < 0) {
    return (
    <Badge className="bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
    Update available
    </Badge>
    );
    }
    return (
    <Badge className="bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
    Development build
    </Badge>
    );
    })()}
    </div>
    </div>
    )}
    </SettingsSectionShell>

    {/* Application Info Section */}
    <SettingsSectionShell
    icon={InfoIcon}
    title="Application insights"
    description="Quick glance."
    accent="from-indigo-500/15 via-purple-500/10 to-transparent"
    className="mt-6"
    contentClassName="space-y-6"
    badge={
    <Badge className="bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-200">
    Version {getAppVersion()}
    </Badge>
    }
    >
    {(() => {
    let channelLabel: string;
    if (versionStatus === null) {
    channelLabel = "Checking channel status";
    } else if (versionStatus.status === "stable") {
    channelLabel = "Stable channel";
    } else if (versionStatus.status === "beta") {
    channelLabel = "Beta channel";
    } else {
    channelLabel = "Development channel";
    }
    return (
    <div className="flex flex-wrap items-center gap-2 text-xs text-slate-600 dark:text-slate-200">
    <span className="inline-flex items-center gap-1 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-slate-700 dark:border-white/15 dark:bg-white/10 dark:text-slate-200">
    <Clock className="h-3.5 w-3.5" />
    {channelLabel}
    </span>
    <span className="inline-flex items-center gap-1 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-slate-700 dark:border-white/15 dark:bg-white/10 dark:text-slate-200">
    <UserCircle className="h-3.5 w-3.5" />
    {authState.isAuthenticated
    ? "Session active"
    : "Session inactive"}
    </span>
    </div>
    );
    })()}

    <div className="grid gap-3 md:grid-cols-3">
    <div className="bg-muted/40 rounded-2xl border p-4">
    <div className="flex items-center gap-2 text-xs font-medium tracking-wide text-slate-900 uppercase dark:text-slate-300">
    <Clock className="h-4 w-4" /> Last synced
    </div>
    <p className="mt-2 text-sm text-white">{lastSyncMetadata.label}</p>
    <p className="text-xs text-slate-900/80 dark:text-slate-300/80">
    {lastSyncMetadata.hint}
    </p>
    </div>
    <div className="bg-muted/40 rounded-2xl border p-4">
    <div className="flex items-center gap-2 text-xs font-medium tracking-wide text-slate-900 uppercase dark:text-slate-300">
    <Key className="h-4 w-4" /> Credentials
    </div>
    <p className="mt-2 text-sm text-white">{credentialSourceLabel}</p>
    <p className="text-xs text-slate-900/80 dark:text-slate-300/80">
    {useCustomCredentials
    ? "Using custom AniList API keys"
    : "Using built-in credentials"}
    </p>
    </div>
    <div className="bg-muted/40 rounded-2xl border p-4">
    <div className="flex items-center gap-2 text-xs font-medium tracking-wide text-slate-900 uppercase dark:text-slate-300">
    <UserCircle className="h-4 w-4" /> Authentication
    </div>
    <p className="mt-2 text-sm text-white">
    {authState.isAuthenticated ? "Connected" : "Not connected"}
    </p>
    <p className="text-xs text-slate-900/80 dark:text-slate-300/80">
    {authState.isAuthenticated
    ? `Expires in ${expiresLabel ?? "unknown"}`
    : "Sign in to unlock sync features"}
    </p>
    </div>
    </div>
    </SettingsSectionShell>
    </motion.div>
    );
    }