import localforage from "localforage";
import browser from "webextension-polyfill";
import {
  type CustomTheme,
  getForcedDarkMode,
  type LoadedCustomTheme,
  shouldForceThemeAppearance,
} from "@/types/CustomThemes";
import { BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY } from "@/seqta/utils/cloudSettingsSync";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import debounce from "@/seqta/utils/debounce";
import { themeUpdates } from "@/interface/hooks/ThemeUpdates";
import { getApiBase } from "@/seqta/utils/DevApiBase";
import { isAllowedFetchUrl } from "@/seqta/utils/allowedFetchUrl";
import { updateAllColors } from "@/seqta/ui/colors/Manager";
import {
  clearCustomThemeAdaptiveCssVariables,
  setCustomThemeAdaptiveCssVariables,
} from "@/seqta/ui/colors/customThemeAdaptiveBindings";
import {
  clearThemeInPage,
  syncThemeToPage,
  type ThemePageSyncInput,
} from "@/seqta/utils/patchThemeImagesPageContext";
import { verboseDebug, verboseInfo } from "@/utils/verboseLog";
import {
  clearThemeRuntime,
  injectThemeDom,
  runThemeScript,
  type ThemeDomSpec,
  type ThemeScriptSpec,
  validateThemeDom,
  validateThemeScript,
} from "./theme-runtime";
import {
  base64ToBlob,
  blobToBase64Data,
  stripBase64Prefix,
} from "./themeImageUrl";

type ThemeContent = {
  id: string;
  name: string;
  coverImage?: string; // base64, optional
  description: string;
  defaultColour?: string;
  CanChangeColour?: boolean;
  CustomCSS?: string;
  hideThemeName?: boolean;
  forceTheme?: boolean;
  forceDark?: boolean;
  adaptiveCssVariables?: string[];
  images?: { id: string; variableName: string; data: string }[]; // data: base64
  themeScript?: ThemeScriptSpec;
  themeDom?: ThemeDomSpec;
};

export type InstallThemeMeta = {
  fromStore: boolean;
  /** Server list `updated_at` (Unix seconds); set when installing from store. */
  serverUpdatedAtSec?: number;
  forceTheme?: boolean;
  adaptiveCssVariables?: string[];
  images?: { id: string; variableName: string; data: string }[]; // data: base64
};

export class ThemeManager {
  private static instance: ThemeManager;
  private currentTheme: CustomTheme | null = null;
  private lastSyncedImageKey: string | null = null;
  private originalPreviewColor: string | null = null;
  private originalPreviewTheme: boolean | null = null;
  private lastTransitionPoint: { x: number; y: number } = { x: 0, y: 0 };
  private storeUpdateCheckRunning = false;

  private constructor() {
    verboseDebug("[ThemeManager] Initializing...");
  }

  public static getInstance(): ThemeManager {
    if (!ThemeManager.instance) {
      ThemeManager.instance = new ThemeManager();
    }
    return ThemeManager.instance;
  }

  /**
   * Get the currently active theme
   */
  public getCurrentTheme(): CustomTheme | null {
    return this.currentTheme;
  }

  /**
   * Get a theme by ID from storage
   */
  public async getTheme(themeId: string): Promise<CustomTheme | null> {
    verboseDebug("[ThemeManager] Getting theme:", themeId);
    try {
      const theme = (await localforage.getItem(themeId)) as CustomTheme;
      return theme;
    } catch (error) {
      console.error("[ThemeManager] Error getting theme:", error);
      return null;
    }
  }

  /**
   * Get the ID of the currently selected theme
   */
  public getSelectedThemeId(): string {
    return settingsState.selectedTheme;
  }

  /**
   * Update the last transition point based on a click or event
   */
  public setTransitionPoint(x: number, y: number): void {
    this.lastTransitionPoint = { x, y };
  }

  /**
   * Apply a view transition animation
   */
  private async applyViewTransition(callback: () => void): Promise<void> {
    if (
      !document.startViewTransition || 
      !settingsState.animations || 
      window.matchMedia("(prefers-reduced-motion: reduce)").matches
    ) {
      // Just run the callback without animation if transitions not supported
      callback();
      return;
    }
    
    // Use last known transition point or fallback to center
    const x = this.lastTransitionPoint.x || window.innerWidth / 2;
    const y = this.lastTransitionPoint.y || window.innerHeight / 2;
    const right = window.innerWidth - x;
    const bottom = window.innerHeight - y;
    
    const maxRadius = Math.hypot(
      Math.max(x, right),
      Math.max(y, bottom),
    );
    
    await document.startViewTransition(() => {
      callback();
    }).ready;
    
    try {
      document.documentElement.animate(
        {
          clipPath: [
            `circle(0px at ${x}px ${y}px)`,
            `circle(${maxRadius}px at ${x}px ${y}px)`,
          ],
        },
        {
          duration: 400,
          easing: 'ease-in-out',
          pseudoElement: '::view-transition-new(root)',
        }
      );
    } catch (error) {
      console.error("[ThemeManager] View Transition animation error:", error);
    }
  }

  /**
   * Disable the current theme without deleting it
   */
  public async disableTheme(): Promise<void> {
    verboseDebug("[ThemeManager] Disabling current theme");
    try {
      if (!this.currentTheme) {
        verboseDebug("[ThemeManager] No theme to disable");
        return;
      }

      await this.removeTheme(this.currentTheme);
      this.currentTheme = null;
      settingsState.selectedTheme = "";
      verboseDebug("[ThemeManager] Theme disabled successfully");
    } catch (error) {
      console.error("[ThemeManager] Error disabling theme:", error);
    }
  }

  /**
   * After cloud restore, IndexedDB/theme storage is only reachable from page context (not MV3 SW).
   * Background sets BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY; we fetch the store JSON here before setTheme().
   * The resolved id matches cloud sync **`themeId` / `selectedTheme`**: it may be a standard theme uuid or a
   * flavour (slave) variant id — **`downloadAndInstallStoreTheme`** is the same code path as the theme store installer.
   */
  public async prepareThemeAfterCloudSync(): Promise<void> {
    try {
      const snap = await browser.storage.local.get(BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY);
      const pending = snap[BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY];
      if (pending === undefined) return;

      await browser.storage.local.remove(BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY);

      if (typeof pending !== "string") return;
      const id = pending.trim();
      if (!id) return;

      const existing = (await localforage.getItem(id)) as CustomTheme | null;
      if (existing) return;

      await this.downloadAndInstallStoreTheme({ id, name: id });
    } catch (e) {
      console.warn("[ThemeManager] prepareThemeAfterCloudSync:", e);
    }
  }

  /**
   * Initialize the theme system and restore previous state
   */
  public async initialize(): Promise<void> {
    verboseDebug("[ThemeManager] Starting initialization");
    try {
      const neumorphicThemeId = "9a9786d1-b5fc-4a91-8c7a-f8bf7f7679ad";
      const migrationCSS = "#title {\nbackground: transparent !important;\n}";
      
      const theme = (await localforage.getItem(neumorphicThemeId)) as CustomTheme | null;
      if (theme && theme.CustomCSS && !theme.CustomCSS.includes("#title {\nbackground: transparent !important;\n}")) {
        theme.CustomCSS = theme.CustomCSS + "\n" + migrationCSS;
        await localforage.setItem(neumorphicThemeId, theme);
      }

      const themeCreatorOpen = localStorage.getItem("themeCreatorOpen");
      if (themeCreatorOpen === "true") {
        verboseDebug(
          "[ThemeManager] Theme creator was open, clearing preview state",
        );
        this.clearPreview();
        localStorage.removeItem("themeCreatorOpen");
      }

      if (settingsState.selectedTheme) {
        verboseDebug(
          "[ThemeManager] Found selected theme, restoring:",
          settingsState.selectedTheme,
        );
        await this.setTheme(settingsState.selectedTheme, false);
      }
    } catch (error) {
      console.error("[ThemeManager] Error during initialization:", error);
    } finally {
      void this.checkStoreThemeUpdates();
    }
  }

  /**
   * Clean up theme system resources
   */
  public async cleanup(): Promise<void> {
    verboseDebug("[ThemeManager] Cleaning up resources");
    try {
      if (this.currentTheme) {
        await this.removeTheme(this.currentTheme, false);
      }
    } catch (error) {
      console.error("[ThemeManager] Error during cleanup:", error);
    }
  }

  /**
   * Set and apply a theme by ID
   */
  public async setTheme(themeId: string, applyViewTransition: boolean = true): Promise<void> {
    verboseDebug("[ThemeManager] Setting theme:", themeId);
    try {
      const theme = (await localforage.getItem(themeId)) as CustomTheme;
      if (!theme) {
        console.error("[ThemeManager] Theme not found:", themeId);
        return;
      }

      // Store original settings before applying new theme
      if (!settingsState.selectedTheme) {
        verboseDebug("[ThemeManager] Storing original settings");
        settingsState.originalSelectedColor = settingsState.selectedColor;

        if (shouldForceThemeAppearance(theme)) {
          settingsState.originalDarkMode = settingsState.DarkMode;
        }
      }

      // Use view transition for the theme change
      if (applyViewTransition) {
        await this.applyViewTransition(async () => {
          // Remove current theme if exists
          if (this.currentTheme) {
            verboseDebug("[ThemeManager] Removing current theme");
            await this.removeThemeWithoutTransition(this.currentTheme);
          }

          // Apply new theme
          await this.applyTheme(theme);
          this.currentTheme = theme;
          settingsState.selectedTheme = themeId;
        });
      } else {
        // Remove current theme if exists
        if (this.currentTheme) {
          verboseDebug("[ThemeManager] Removing current theme");
          await this.removeThemeWithoutTransition(this.currentTheme);
        }

        // Apply new theme
        await this.applyTheme(theme);
        this.currentTheme = theme;
        settingsState.selectedTheme = themeId;
      }
      void updateAllColors();
    } catch (error) {
      console.error("[ThemeManager] Error setting theme:", error);
    }
  }

  /**
   * Apply theme components (CSS, images, settings)
   */
  private async applyTheme(theme: CustomTheme): Promise<void> {
    verboseDebug("[ThemeManager] Applying theme:", theme.name);
    try {
      // Run the theme script BEFORE injecting CustomCSS so any state the
      // script publishes (e.g. `data-city-state` and `--city-sky-color` for
      // Noir City) is already on <html> when the new CSS rules paint.
      // Otherwise the CSS lands with var() unresolved and the page flashes
      // its previous state before snapping to the right colour.
      runThemeScript(theme.themeScript);

      // Custom CSS + images must be applied in page context (Firefox).
      verboseDebug("[ThemeManager] Applying theme styles in page context");
      await syncThemeToPage({
        customCss: theme.CustomCSS || "",
        images: theme.CustomImages ?? [],
      });
      if (theme.CustomImages?.length) {
        this.lastSyncedImageKey = this.imageSyncKey(theme.CustomImages);
      } else {
        this.lastSyncedImageKey = null;
      }

      // Apply theme settings
      if (shouldForceThemeAppearance(theme)) {
        const dark = getForcedDarkMode(theme);
        verboseDebug("[ThemeManager] Setting dark mode:", dark);
        settingsState.DarkMode = dark;
      }

      // Use the stored selected color if available, otherwise use the default
      if (theme.selectedColor) {
        verboseDebug(
          "[ThemeManager] Restoring saved color:",
          theme.selectedColor,
        );
        settingsState.selectedColor = theme.selectedColor;
      } else if (theme.defaultColour) {
        verboseDebug(
          "[ThemeManager] Using default color:",
          theme.defaultColour,
        );
        settingsState.selectedColor = theme.defaultColour;
      }

      setCustomThemeAdaptiveCssVariables(theme.adaptiveCssVariables ?? []);

      injectThemeDom(theme.themeDom);
    } catch (error) {
      console.error("[ThemeManager] Error applying theme:", error);
    }
  }

  /**
   * Remove theme and restore original settings with view transition
   */
  private async removeTheme(
    theme: CustomTheme,
    clearSelectedTheme: boolean = true,
  ): Promise<void> {
    verboseDebug("[ThemeManager] Removing theme with transition:", theme.name);
    try {
      await this.applyViewTransition(async () => {
        await this.removeThemeWithoutTransition(theme, clearSelectedTheme);
      });
    } catch (error) {
      console.error("[ThemeManager] Error removing theme with transition:", error);
    }
  }
  
  /**
   * Remove theme without applying view transition animation
   */
  private async removeThemeWithoutTransition(
    theme: CustomTheme,
    clearSelectedTheme: boolean = true,
  ): Promise<void> {
    verboseDebug("[ThemeManager] Removing theme:", theme.name);
    try {
      clearThemeRuntime();

      verboseDebug("[ThemeManager] Removing theme page styles");
      clearThemeInPage();
      this.lastSyncedImageKey = null;

      if (this.currentTheme) {
        // Store the current color with the theme before removing it
        const selectedColor = settingsState.selectedColor;
        const markUserEditedForColor =
          this.currentTheme.userEdited !== true &&
          this.currentTheme.installedFromStore !== false &&
          selectedColor &&
          this.currentTheme.defaultColour &&
          selectedColor.trim().toLowerCase() !==
            this.currentTheme.defaultColour.trim().toLowerCase();
        await localforage.setItem(this.currentTheme.id, {
          ...this.currentTheme,
          selectedColor,
          ...(markUserEditedForColor ? { userEdited: true } : {}),
        });
      }

      // Restore original settings
      if (settingsState.originalSelectedColor) {
        verboseDebug(
          "[ThemeManager] Restoring original color:",
          settingsState.originalSelectedColor,
        );
        settingsState.selectedColor = settingsState.originalSelectedColor;
      }

      if (settingsState.originalDarkMode !== undefined) {
        verboseDebug(
          "[ThemeManager] Restoring original dark mode:",
          settingsState.originalDarkMode,
        );
        settingsState.DarkMode = settingsState.originalDarkMode;
        delete settingsState.originalDarkMode;
      }

      this.currentTheme = null;
      if (clearSelectedTheme) {
        settingsState.selectedTheme = "";
      }
      clearCustomThemeAdaptiveCssVariables();
    } catch (error) {
      console.error("[ThemeManager] Error removing theme:", error);
    }
  }

  /**
   * Stable key so preview updates can skip re-encoding image blobs when only CSS changed.
   */
  private imageSyncKey(
    images: Array<{ id: string; variableName: string; blob: Blob }>,
  ): string {
    return images
      .map((image) => `${image.id}:${image.variableName}:${image.blob.size}`)
      .join("|");
  }

  /**
   * Get list of available themes
   */
  public async getAvailableThemes(): Promise<CustomTheme[]> {
    verboseDebug("[ThemeManager] Getting available themes");
    try {
      const themeIds = (await localforage.getItem("customThemes")) as
        | string[]
        | null;
      if (!themeIds) {
        return [];
      }

      const themes = await Promise.all(
        themeIds.map(async (id) => {
          return (await localforage.getItem(id)) as CustomTheme;
        }),
      );

      return themes.filter((theme) => theme !== null);
    } catch (error) {
      console.error("[ThemeManager] Error getting available themes:", error);
      return [];
    }
  }

  /**
   * Save or update a theme
   */
  public async saveTheme(theme: LoadedCustomTheme): Promise<void> {
    verboseDebug("[ThemeManager] Saving theme:", theme.name);
    try {
      const existing = (await localforage.getItem(theme.id)) as CustomTheme | null;
      let toSave = theme;
      if (existing?.userEdited === true && theme.userEdited !== false) {
        toSave = { ...theme, userEdited: true };
      }

      await localforage.setItem(toSave.id, toSave);
      const themeIds = (await localforage.getItem("customThemes")) as
        | string[]
        | null;

      if (themeIds) {
        if (!themeIds.includes(toSave.id)) {
          themeIds.push(toSave.id);
          await localforage.setItem("customThemes", themeIds);
        }
      } else {
        await localforage.setItem("customThemes", [toSave.id]);
      }
    } catch (error) {
      console.error("[ThemeManager] Error saving theme:", error);
    }
  }

  /**
   * Delete a theme
   */
  public async deleteTheme(themeId: string): Promise<void> {
    verboseDebug("[ThemeManager] Deleting theme:", themeId);
    try {
      const theme = (await localforage.getItem(themeId)) as CustomTheme;
      if (theme) {
        if (this.currentTheme?.id === themeId) {
          await this.removeTheme(theme);
        }
        await localforage.removeItem(themeId);

        const themeIds = (await localforage.getItem("customThemes")) as
          | string[]
          | null;
        if (themeIds) {
          const updatedThemeIds = themeIds.filter((id) => id !== themeId);
          await localforage.setItem("customThemes", updatedThemeIds);
        }
      }
    } catch (error) {
      console.error("[ThemeManager] Error deleting theme:", error);
    }
  }

  /** Use a getter so dev-mode session-only base URL overrides take effect immediately. */
  private get THEME_API_BASE(): string {
    return `${getApiBase()}/api`;
  }
  private readonly GITHUB_THEMES_BASE = 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/themes';

  /**
   * Fetch JSON from a URL via background script (avoids CORS when running inside SEQTA page)
   */
  private async fetchFromUrl(url: string): Promise<any> {
    const result = (await browser.runtime.sendMessage({
      type: 'fetchFromUrl',
      url,
    })) as { data?: unknown; error?: string };
    if (result?.error) throw new Error(result.error);
    return result?.data;
  }

  /**
   * Download and install a theme from the store.
   * Uses API first (increments download_count), falls back to GitHub if unreachable.
   */
  public async downloadTheme(themeContent: {
    id: string;
    name: string;
    description?: string;
    coverImage?: string;
    theme_json_url?: string;
    updated_at?: number;
  }): Promise<void> {
    try {
      await this.downloadAndInstallStoreTheme(themeContent);
    } catch (error) {
      console.error("[ThemeManager] Error downloading theme:", error);
    }
  }

  /**
   * Fetch theme.json from the store and install (throws on failure).
   */
  private async downloadAndInstallStoreTheme(themeContent: {
    id: string;
    name: string;
    description?: string;
    coverImage?: string;
    theme_json_url?: string;
    updated_at?: number;
  }): Promise<void> {
    verboseDebug("[ThemeManager] Downloading theme:", themeContent.name);
    if (!themeContent.id) {
      throw new Error("Missing theme id");
    }

    let themeData: ThemeContent;

    try {
      const downloadData = (await this.fetchFromUrl(
        `${this.THEME_API_BASE}/themes/${themeContent.id}/download`,
      )) as { success?: boolean; data?: { theme_json_url: string } };
      if (!downloadData?.success || !downloadData?.data?.theme_json_url) {
        throw new Error("Failed to get theme download URL");
      }
      const themeJsonUrl = downloadData.data.theme_json_url;
      if (!isAllowedFetchUrl(themeJsonUrl)) {
        throw new Error("Theme download URL not allowed");
      }
      themeData = (await this.fetchFromUrl(
        themeJsonUrl,
      )) as ThemeContent;
    } catch (apiError) {
      console.warn("[ThemeManager] API failed, trying GitHub fallback:", apiError);
      const githubUrl = `${this.GITHUB_THEMES_BASE}/${themeContent.id}/theme.json`;
      themeData = (await this.fetchFromUrl(githubUrl)) as ThemeContent;
    }

    await this.installTheme(themeData, {
      fromStore: true,
      serverUpdatedAtSec: themeContent.updated_at,
    });
  }

  /**
   * Install a theme from theme data
   */
  public async installTheme(
    themeData: ThemeContent,
    meta?: InstallThemeMeta,
  ): Promise<void> {
    verboseDebug("[ThemeManager] Installing theme:", themeData.name);
    try {
      // Validate required fields
      if (!themeData.id || !themeData.name) {
        throw new Error("Theme is missing required fields (id or name)");
      }

      // Validate optional runtime hooks. Reject the install if either is
      // malformed so a tampered theme cannot smuggle in unsafe values.
      if (!validateThemeScript(themeData.themeScript)) {
        throw new Error("Theme has invalid themeScript");
      }
      if (!validateThemeDom(themeData.themeDom)) {
        throw new Error("Theme has invalid themeDom");
      }

      const fromStore = meta?.fromStore ?? false;
      const serverUpdatedAtSec = meta?.serverUpdatedAtSec;

      // Handle cover image (optional)
      let coverImageBlob = null;
      if (themeData.coverImage) {
        try {
          coverImageBlob = base64ToBlob(
            stripBase64Prefix(themeData.coverImage),
            "image/png",
          );
        } catch (e) {
          console.warn("[ThemeManager] Failed to process cover image:", e);
          // Continue without cover image
        }
      }

      // Handle images (optional)
      const images =
        themeData.images
          ?.map((image) => {
            try {
              if (!image.id || !image.variableName || !image.data) {
                console.warn("[ThemeManager] Skipping invalid image:", image);
                return null;
              }
              return {
                ...image,
                blob: base64ToBlob(stripBase64Prefix(image.data), "image/png"),
              };
            } catch (e) {
              console.warn("[ThemeManager] Failed to process image:", e);
              return null;
            }
          })
          .filter((img) => img !== null) ?? [];

      const theme: LoadedCustomTheme = {
        id: themeData.id,
        name: themeData.name,
        description: themeData.description || "",
        webURL: themeData.id,
        coverImage: coverImageBlob,
        CustomImages: images,
        CustomCSS: themeData.CustomCSS || "",
        defaultColour: themeData.defaultColour || "rgba(0, 123, 255, 1)",
        CanChangeColour: themeData.CanChangeColour ?? true,
        allowBackgrounds: true,
        isEditable: false,
        hideThemeName: themeData.hideThemeName ?? false,
        forceDark: themeData.forceDark,
        installedFromStore: fromStore,
        userEdited: fromStore ? false : undefined,
        storeSyncedAtSec:
          fromStore && serverUpdatedAtSec != null
            ? serverUpdatedAtSec
            : undefined,
        forceTheme:
          themeData.forceTheme !== undefined
            ? themeData.forceTheme
            : themeData.forceDark !== undefined
              ? true
              : undefined,
        adaptiveCssVariables: themeData.adaptiveCssVariables,
        themeScript: themeData.themeScript,
        themeDom: themeData.themeDom,
      };

      await this.saveTheme(theme);
    } catch (error) {
      console.error("[ThemeManager] Error installing theme:", error);
      throw error; // Re-throw to handle in UI
    }
  }

  /**
   * Compare installed store themes to GET /api/themes and refresh when the server is newer.
   * Skips themes with userEdited: true (theme creator / popup save, or custom accent vs default).
   */
  private static STORE_CHECK_KEY = "bsplus_lastStoreThemeCheck";
  private static STORE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours

  public async checkStoreThemeUpdates(): Promise<void> {
    if (this.storeUpdateCheckRunning) return;

    const lastCheck = Number(localStorage.getItem(ThemeManager.STORE_CHECK_KEY) || 0);
    if (Date.now() - lastCheck < ThemeManager.STORE_CHECK_INTERVAL_MS) return;

    this.storeUpdateCheckRunning = true;
    localStorage.setItem(ThemeManager.STORE_CHECK_KEY, String(Date.now()));
    try {
      const res = (await browser.runtime.sendMessage({
        type: "fetchThemes",
      })) as {
        success?: boolean;
        data?: { themes?: Array<{ id: string; updated_at?: number }> };
      };

      if (!res?.success || !res.data?.themes?.length) return;

      const serverById = new Map<string, number>();
      for (const t of res.data.themes) {
        if (
          typeof t.id === "string" &&
          typeof t.updated_at === "number"
        ) {
          serverById.set(t.id, t.updated_at);
        }
      }
      if (serverById.size === 0) return;

      const themeIds = (await localforage.getItem("customThemes")) as
        | string[]
        | null;
      if (!themeIds?.length) return;

      for (const id of themeIds) {
        const theme = (await localforage.getItem(id)) as CustomTheme | null;
        if (!theme || theme.userEdited === true) {
          continue;
        }

        // File imports explicitly set installedFromStore: false — never auto-sync.
        if (theme.installedFromStore === false) {
          continue;
        }

        const serverUpdated = serverById.get(id);
        if (serverUpdated == null) {
          continue;
        }

        if (
          theme.installedFromStore === true &&
          theme.storeSyncedAtSec != null
        ) {
          if (serverUpdated <= theme.storeSyncedAtSec) {
            continue;
          }
        }
        // Else: legacy installs from before store metadata (installedFromStore/storeSyncedAtSec
        // unset) or incomplete rows — still listed on the server, so sync to latest once.

        const wasSelected = settingsState.selectedTheme === id;
        try {
          if (wasSelected) {
            await this.disableTheme();
          }
          await this.downloadAndInstallStoreTheme({
            id: theme.id,
            name: theme.name,
            updated_at: serverUpdated,
          });
          verboseInfo(
            "[ThemeManager] Theme auto-updated from store:",
            theme.name,
          );
          if (wasSelected) {
            await this.setTheme(id, false);
          }
          themeUpdates.triggerUpdate();
        } catch (err) {
          console.error("[ThemeManager] Store theme auto-update failed:", id, err);
          if (wasSelected) {
            try {
              await this.setTheme(id, false);
            } catch (restoreErr) {
              console.error(
                "[ThemeManager] Failed to restore theme after update error:",
                restoreErr,
              );
            }
          }
        }
      }
    } catch (e) {
      console.error("[ThemeManager] checkStoreThemeUpdates error:", e);
    } finally {
      this.storeUpdateCheckRunning = false;
    }
  }

  /**
   * Share a theme by exporting it
   */
  public async shareTheme(themeId: string): Promise<void> {
    verboseDebug("[ThemeManager] Sharing theme:", themeId);
    try {
      const theme = (await localforage.getItem(themeId)) as LoadedCustomTheme;
      if (!theme) {
        console.error("[ThemeManager] Theme not found");
        return;
      }

      // Extract only the fields we want to share
      const {
        CustomImages = [],
        coverImage,
        webURL,
        isEditable,
        selectedColor,
        allowBackgrounds,
        installedFromStore,
        storeSyncedAtSec,
        userEdited,
        ...themeBasics
      } = theme;

      // Convert images to base64
      const finalImages = await Promise.all(
        CustomImages.map(async (image) => ({
          id: image.id,
          variableName: image.variableName,
          data: await blobToBase64Data(image.blob),
        })),
      );

      // Convert cover image to base64
      const coverImageBase64 = coverImage
        ? await blobToBase64Data(coverImage)
        : null;

      // Create shareable theme data with only necessary fields
      const shareableTheme = {
        ...themeBasics,
        images: finalImages,
        coverImage: coverImageBase64,
      };

      // Save theme file
      this.saveThemeFile(shareableTheme, theme.name || "Unnamed_Theme");
    } catch (error) {
      console.error("[ThemeManager] Error sharing theme:", error);
    }
  }

  /**
   * Preview a theme without applying it
   */
  public async previewTheme(theme: LoadedCustomTheme): Promise<void> {
    verboseDebug("[ThemeManager] Previewing theme:", theme.name);
    try {
      const { CustomCSS, CustomImages, defaultColour } = theme;

      // Store original settings only if this is a new theme
      if (!theme.webURL) {
        if (this.originalPreviewColor === null) {
          this.originalPreviewColor = settingsState.selectedColor;
          localStorage.setItem(
            "originalPreviewColor",
            settingsState.selectedColor,
          );
        }
        if (this.originalPreviewTheme === null) {
          this.originalPreviewTheme = settingsState.DarkMode;
        }
      }

      // Apply custom CSS + images in page context (preview stylesheet)
      await syncThemeToPage({
        previewCss: CustomCSS,
        images: CustomImages,
      });
      this.lastSyncedImageKey = this.imageSyncKey(CustomImages);

      // Apply theme settings
      if (shouldForceThemeAppearance(theme)) {
        settingsState.DarkMode = getForcedDarkMode(theme);
      }

      if (defaultColour) {
        settingsState.selectedColor = defaultColour;
      }

      setCustomThemeAdaptiveCssVariables(theme.adaptiveCssVariables ?? []);
      void updateAllColors();
    } catch (error) {
      console.error("[ThemeManager] Error previewing theme:", error);
    }
  }

  /**
   * Update the preview of a theme in real-time (for theme creator)
   */
  public async updatePreview(theme: Partial<LoadedCustomTheme>): Promise<void> {
    verboseDebug("[ThemeManager] Updating theme preview");
    try {
      // Only store original settings if this is a new theme (not editing)
      // We can tell it's a new theme if it has no webURL (which is set when a theme is saved/loaded)
      if (!theme.webURL) {
        if (this.originalPreviewColor === null) {
          this.originalPreviewColor = settingsState.selectedColor;
        }
        if (this.originalPreviewTheme === null) {
          this.originalPreviewTheme = settingsState.DarkMode;
        }
      }

      const syncInput: ThemePageSyncInput = {};

      if (theme.CustomCSS !== undefined) {
        syncInput.previewCss = theme.CustomCSS;
      }

      if (theme.CustomImages) {
        const imageKey = this.imageSyncKey(theme.CustomImages);
        if (imageKey !== this.lastSyncedImageKey) {
          syncInput.images = theme.CustomImages;
          this.lastSyncedImageKey = imageKey;
        }
      }

      if (Object.keys(syncInput).length > 0) {
        await syncThemeToPage(syncInput);
      }

      // Always apply dark mode setting when theme forces appearance
      if (shouldForceThemeAppearance(theme as CustomTheme)) {
        settingsState.DarkMode = getForcedDarkMode(theme as CustomTheme);
      }

      // Only apply color if this is a new theme
      if (!theme.webURL && theme.defaultColour) {
        settingsState.selectedColor = theme.defaultColour;
      }

      setCustomThemeAdaptiveCssVariables(theme.adaptiveCssVariables ?? []);
      void updateAllColors();
    } catch (error) {
      console.error("[ThemeManager] Error updating theme preview:", error);
    }
  }

  /**
   * Update the preview of a theme (debounced)
   * @param theme - The theme to update the preview of
   */
  public updatePreviewDebounced = debounce(
    (theme: Partial<LoadedCustomTheme>): void => {
      this.updatePreview(theme);
    },
    2,
  );

  /**
   * Clear theme preview
   */
  public clearPreview(): void {
    verboseDebug("[ThemeManager] Clearing theme preview");
    try {
      void syncThemeToPage({ clearPreview: true, images: [] });
      this.lastSyncedImageKey = null;

      clearCustomThemeAdaptiveCssVariables();

      // Restore original settings
      const storedColor = localStorage.getItem("originalPreviewColor");

      if (storedColor) {
        settingsState.selectedColor = storedColor;
        localStorage.removeItem("originalPreviewColor");
      } else if (this.originalPreviewColor !== null) {
        verboseDebug(
          "[ThemeManager] Restoring color from memory:",
          this.originalPreviewColor,
        );
        settingsState.selectedColor = this.originalPreviewColor;
        verboseDebug(
          "[ThemeManager] Color after restore:",
          settingsState.selectedColor,
        );
      } else {
        verboseDebug("[ThemeManager] No color to restore found");
      }
      this.originalPreviewColor = null;

      if (this.originalPreviewTheme !== null) {
        verboseDebug(
          "[ThemeManager] Restoring dark mode:",
          this.originalPreviewTheme,
        );
        settingsState.DarkMode = this.originalPreviewTheme;
        this.originalPreviewTheme = null;
      }

      void updateAllColors();
    } catch (error) {
      console.error("[ThemeManager] Error clearing preview:", error);
    }
  }

  private saveThemeFile(data: object, fileName: string): void {
    try {
      const fileData = JSON.stringify(data, null, 2);
      const blob = new Blob([fileData], { type: "application/json" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = `${fileName}.theme.json`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    } catch (err) {
      console.error("[ThemeManager] Error saving theme file:", err);
    }
  }
}
