import { cloneDeep, merge } from "lodash-es";

import { vuelessConfig } from "./ui";
import { isCSR, getStored, getCookie, setCookie, deleteCookie, toNumber } from "./helper";

import {
  PX_IN_REM,
  AUTO_MODE_KEY,
  COLOR_MODE_KEY,
  LIGHT_MODE_CLASS,
  DARK_MODE_CLASS,
  GRAYSCALE_COLOR,
  DEFAULT_PRIMARY_COLOR,
  DEFAULT_NEUTRAL_COLOR,
  OUTLINE,
  DEFAULT_OUTLINE,
  OUTLINE_DECREMENT,
  OUTLINE_INCREMENT,
  ROUNDING,
  DEFAULT_ROUNDING,
  ROUNDING_DECREMENT,
  ROUNDING_INCREMENT,
  NEUTRAL_COLOR,
  PRIMARY_COLOR,
  COLOR_SHADES,
  DEFAULT_LIGHT_THEME,
  DEFAULT_DARK_THEME,
  PRIMARY_COLORS,
  NEUTRAL_COLORS,
  TEXT,
  DEFAULT_TEXT,
  TEXT_INCREMENT,
  TEXT_DECREMENT,
  DISABLED_OPACITY,
  DEFAULT_DISABLED_OPACITY,
  LETTER_SPACING,
  DEFAULT_LETTER_SPACING,
  THEME_TOKENS,
  LIGHT_THEME,
  DARK_THEME,
  SPACING,
  DEFAULT_SPACING,
} from "../constants";

import type {
  NeutralColors,
  PrimaryColors,
  ThemeConfig,
  ColorShades,
  ThemeConfigText,
  ThemeConfigOutline,
  ThemeConfigRounding,
  MergedThemeConfig,
  VuelessCssVariables,
} from "../types";
import { ColorMode } from "../types";

declare interface SetColorMode {
  colorMode: `${ColorMode}`;
  isColorModeAuto: boolean;
}

/* Creates a media query that checks if the user's system color scheme is set to the dark. */
const prefersColorSchemeDark = isCSR && window.matchMedia("(prefers-color-scheme: dark)");

function toggleColorModeClass() {
  if (!prefersColorSchemeDark) return;

  const isDarkMode = prefersColorSchemeDark.matches;
  const colorMode = isDarkMode ? ColorMode.Dark : ColorMode.Light;

  setCookie(COLOR_MODE_KEY, colorMode);
  setCookie(AUTO_MODE_KEY, String(Number(true)));

  document.documentElement.classList.toggle(DARK_MODE_CLASS, isDarkMode);
  document.documentElement.classList.toggle(LIGHT_MODE_CLASS, !isDarkMode);

  /* Dispatching custom event for the useDarkMode composable. */
  window.dispatchEvent(new CustomEvent("darkModeChange", { detail: isDarkMode }));
}

/**
 * Sets the client-side rendering (CSR) color mode by applying the specified mode,
 * configuring the appropriate event listeners, setting CSS classes, and saving the mode
 * in cookies and local storage.
 *
 * @param {`${ColorMode}`} mode - The desired color mode (dark | light | auto).
 * @return {Object} An object containing:
 * - `colorMode` {string}: The applied color mode (e.g., "light", "dark").
 * - `isColorModeAuto` {boolean}: Indicates whether the color mode is set to auto.
 */
function setCSRColorMode(mode: `${ColorMode}`): SetColorMode {
  const colorMode = mode || getCookie(COLOR_MODE_KEY) || vuelessConfig.colorMode || ColorMode.Light;
  const isCachedAutoMode = !!Number(getCookie(AUTO_MODE_KEY) ?? 0);

  const isAutoMode = colorMode === ColorMode.Auto;
  const isSystemDarkMode = isAutoMode && prefersColorSchemeDark && prefersColorSchemeDark?.matches;
  const isDarkMode = colorMode === ColorMode.Dark || isSystemDarkMode;

  /* Removing system color mode change event listener. */
  if (prefersColorSchemeDark) {
    prefersColorSchemeDark.removeEventListener("change", toggleColorModeClass);
  }

  /* Adding system color mode change event listener. */
  if ((isAutoMode || isCachedAutoMode) && prefersColorSchemeDark) {
    prefersColorSchemeDark.addEventListener("change", toggleColorModeClass);
  }

  /* Adding color mode classes. */
  document.documentElement.classList.toggle(DARK_MODE_CLASS, isDarkMode);
  document.documentElement.classList.toggle(LIGHT_MODE_CLASS, !isDarkMode);

  /* Dispatching custom event for the useDarkMode composable. */
  window.dispatchEvent(new CustomEvent("darkModeChange", { detail: isDarkMode }));

  /* Saving color mode value into cookies (server) and local storage (client). */
  let currentColorMode = colorMode;

  if (isAutoMode) {
    currentColorMode = isDarkMode ? ColorMode.Dark : ColorMode.Light;
  }

  /* Define color mode cookies to be used in both CSR and SSR */
  if (mode || getCookie(AUTO_MODE_KEY) === undefined) {
    setCookie(COLOR_MODE_KEY, currentColorMode);
    setCookie(AUTO_MODE_KEY, String(Number(isAutoMode)));

    if (mode !== ColorMode.Auto && prefersColorSchemeDark) {
      prefersColorSchemeDark.removeEventListener("change", toggleColorModeClass);
    }
  }

  return {
    colorMode: currentColorMode,
    isColorModeAuto: isAutoMode || isCachedAutoMode,
  };
}

/**
 * Gets server-side rendering (SSR) color mode.
 *
 * @param {`${ColorMode}`} mode - The desired color mode (dark | light | auto).
 * @param {boolean} isColorModeAuto - Indicates whether the color mode is set to auto.
 * @return {Object} An object containing:
 * - `colorMode` {string}: The applied color mode (e.g., "light", "dark").
 * - `isColorModeAuto` {boolean}: Indicates whether the color mode is set to auto.
 */
function getSSRColorMode(mode: `${ColorMode}`, isColorModeAuto: boolean = false): SetColorMode {
  const currentColorMode = mode || vuelessConfig.colorMode || ColorMode.Light;
  const isAutoMode = currentColorMode === ColorMode.Auto;

  return {
    colorMode: currentColorMode,
    isColorModeAuto: isAutoMode || isColorModeAuto,
  };
}

/**
 * Retrieves CSS variable value to be easily used in the JavaScript code.
 * @return string.
 */
export function cssVar(name: string) {
  return (isCSR && getComputedStyle(document.documentElement).getPropertyValue(name)) || undefined;
}

/**
 * Resets all theme data by clearing cookies and localStorage.
 * This removes all stored theme preferences including color mode, colors, text sizes,
 * outline sizes, rounding values, letter spacing, and disabled opacity.
 */
export function resetTheme() {
  if (!isCSR) return;

  const themeKeys = [
    AUTO_MODE_KEY,
    COLOR_MODE_KEY,
    `vl-${PRIMARY_COLOR}`,
    `vl-${NEUTRAL_COLOR}`,
    `vl-${TEXT}-xs`,
    `vl-${TEXT}-sm`,
    `vl-${TEXT}-md`,
    `vl-${TEXT}-lg`,
    `vl-${OUTLINE}-sm`,
    `vl-${OUTLINE}-md`,
    `vl-${OUTLINE}-lg`,
    `vl-${ROUNDING}-sm`,
    `vl-${ROUNDING}-md`,
    `vl-${ROUNDING}-lg`,
    `vl-${SPACING}`,
    `vl-${LETTER_SPACING}`,
    `vl-${DISABLED_OPACITY}`,
    `vl-${LIGHT_THEME}`,
    `vl-${DARK_THEME}`,
  ];

  themeKeys.forEach((key) => {
    localStorage.removeItem(key);
    deleteCookie(key);
  });
}

/**
 * Retrieves the current theme configuration.
 * @return ThemeConfig - current theme configuration
 */
export function getTheme(config?: ThemeConfig): MergedThemeConfig {
  const { colorMode, isColorModeAuto } = isCSR
    ? setCSRColorMode(config?.colorMode as ColorMode)
    : getSSRColorMode(config?.colorMode as ColorMode, config?.isColorModeAuto);

  const primary = getPrimaryColor(config?.primary);
  const neutral = getNeutralColor(config?.neutral);

  const text = getText(config?.text);
  const outline = getOutlines(config?.outline);
  const rounding = getRoundings(config?.rounding);
  const spacing = getSpacing(config?.spacing);
  const letterSpacing = getLetterSpacing(config?.letterSpacing);
  const disabledOpacity = getDisabledOpacity(config?.disabledOpacity);

  const lightTheme = getLightTheme(config?.lightTheme);
  const darkTheme = getDarkTheme(config?.darkTheme);

  return {
    colorMode,
    isColorModeAuto,
    primary,
    neutral,
    text,
    outline,
    rounding,
    spacing,
    letterSpacing,
    disabledOpacity,
    lightTheme,
    darkTheme,
  };
}

/**
 * Applying theme settings.
 * Changes and reset Vueless CSS variables.
 * @return string - CSS variables
 */
export function setTheme(config: ThemeConfig = {}) {
  isCSR
    ? setCSRColorMode(config.colorMode as ColorMode)
    : getSSRColorMode(config.colorMode as ColorMode);

  const text = getText(config.text);
  const outline = getOutlines(config.outline);
  const rounding = getRoundings(config.rounding);
  const spacing = getSpacing(config.spacing);
  const letterSpacing = getLetterSpacing(config.letterSpacing);
  const disabledOpacity = getDisabledOpacity(config.disabledOpacity);

  let primary = getPrimaryColor(config.primary);
  const neutral = getNeutralColor(config.neutral);

  const lightTheme = getLightTheme(config.lightTheme);
  const darkTheme = getDarkTheme(config.darkTheme);

  /* Redeclare primary color if grayscale color set as default */
  if (primary === GRAYSCALE_COLOR) {
    primary = neutral;

    ["", "lifted", "accented"].forEach((shade) => {
      const primaryShade: keyof VuelessCssVariables = shade
        ? `--vl-primary-${shade}`
        : "--vl-primary";

      const grayscaleShade: keyof VuelessCssVariables = shade
        ? `--vl-grayscale-${shade}`
        : "--vl-grayscale";

      /* Dark theme: if the primary color shade is not defined in global or runtime config, use the grayscale color. */

      const hasGlobalDarkPrimaryColor = hasPrimaryColor(
        vuelessConfig.darkTheme,
        DEFAULT_DARK_THEME,
        primaryShade,
      );

      const hasRuntimeDarkPrimaryColor = hasPrimaryColor(
        config.darkTheme,
        DEFAULT_DARK_THEME,
        primaryShade,
      );

      if (!hasGlobalDarkPrimaryColor && !hasRuntimeDarkPrimaryColor) {
        darkTheme[primaryShade] = darkTheme[grayscaleShade];
      }

      /* Light theme: if the primary color shade is not defined in global or runtime config, use the grayscale color. */

      const hasGlobalLightPrimaryColor = hasPrimaryColor(
        vuelessConfig.lightTheme,
        DEFAULT_LIGHT_THEME,
        primaryShade,
      );

      const hasRuntimeLightPrimaryColor = hasPrimaryColor(
        config.lightTheme,
        DEFAULT_LIGHT_THEME,
        primaryShade,
      );

      if (!hasGlobalLightPrimaryColor && !hasRuntimeLightPrimaryColor) {
        lightTheme[primaryShade] = lightTheme[grayscaleShade];
      }
    });
  }

  return setRootCSSVariables({
    primary,
    neutral,
    text,
    outline,
    rounding,
    spacing,
    letterSpacing,
    disabledOpacity,
    lightTheme,
    darkTheme,
  });
}

/**
 * Normalizes the provided theme configuration object into a structured format.
 *
 * @param {object} theme - The theme configuration object to normalize.
 * @return {MergedThemeConfig}
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function normalizeThemeConfig(theme: any): MergedThemeConfig {
  return {
    colorMode: String(theme.colorMode ?? "") as ColorMode,
    isColorModeAuto: !!toNumber(theme.isColorModeAuto),
    primary: theme.primary || null,
    neutral: theme.neutral || null,
    text: {
      xs: toNumber(theme.text?.xs),
      sm: toNumber(theme.text?.sm),
      md: toNumber(theme.text?.md),
      lg: toNumber(theme.text?.lg),
    },
    outline: {
      sm: toNumber(theme.outline?.sm),
      md: toNumber(theme.outline?.md),
      lg: toNumber(theme.outline?.lg),
    },
    rounding: {
      sm: toNumber(theme.rounding?.sm),
      md: toNumber(theme.rounding?.md),
      lg: toNumber(theme.rounding?.lg),
    },
    spacing: toNumber(theme.spacing),
    letterSpacing: toNumber(theme.letterSpacing),
    disabledOpacity: toNumber(theme.disabledOpacity),
  };
}

function isColorShadesObject(color: unknown): color is Partial<ColorShades> {
  return typeof color === "object" && color !== null && !Array.isArray(color);
}

function validateColorShades(color: Partial<ColorShades>, colorType: string) {
  const validShades = COLOR_SHADES;
  const providedShades = Object.keys(color);

  const invalidShades = providedShades.filter((shade) => !validShades.includes(Number(shade)));
  const missingShades = validShades.filter((shade) => !(String(shade) in color));

  if (invalidShades.length > 0) {
    const invalidShadesStr = invalidShades.join(", ");
    const validShadesStr = validShades.join(", ");

    // eslint-disable-next-line no-console
    console.warn(
      `[vueless] Invalid shade keys found in ${colorType} color object: ${invalidShadesStr}. ` +
        `Valid shades are: ${validShadesStr}.`,
    );
  }

  if (missingShades.length > 0) {
    const missingShadesStr = missingShades.join(", ");
    const validShadesStr = validShades.join(", ");

    // eslint-disable-next-line no-console
    console.warn(
      `[vueless] Missing shade keys in ${colorType} color object: ${missingShadesStr}. ` +
        `All shades (${validShadesStr}) should be defined.`,
    );
  }
}

/**
 * Determines if the provided color mode configuration has a primary color
 * that differs from the default color mode configuration.
 * @return {boolean}
 */
function hasPrimaryColor(
  colorModeConfig: Partial<VuelessCssVariables> | undefined,
  defaultColorModeConfig: Partial<VuelessCssVariables>,
  primaryShade: keyof VuelessCssVariables,
) {
  const shade = colorModeConfig?.[primaryShade];
  const defaultShade = defaultColorModeConfig?.[primaryShade];

  return shade && shade !== defaultShade;
}

/**
 * Retrieve primary color value and save them to cookie and localStorage.
 * @return string | Partial<ColorShades> - primary color.
 */
function getPrimaryColor(primary?: PrimaryColors) {
  const storageKey = `vl-${PRIMARY_COLOR}`;

  const storedValue = getStored(storageKey);
  let parsedStoredValue: PrimaryColors | undefined;

  if (storedValue) {
    try {
      parsedStoredValue = JSON.parse(storedValue);
    } catch {
      parsedStoredValue = storedValue;
    }
  }

  let primaryColor: PrimaryColors =
    primary ?? parsedStoredValue ?? vuelessConfig.primary ?? DEFAULT_PRIMARY_COLOR;

  if (isColorShadesObject(primaryColor)) {
    validateColorShades(primaryColor, PRIMARY_COLOR);

    if (isCSR && primary) {
      setCookie(storageKey, JSON.stringify(primaryColor));
      localStorage.setItem(storageKey, JSON.stringify(primaryColor));
    }

    return primaryColor;
  }

  const isPrimaryColor =
    PRIMARY_COLORS.some((color) => color === primaryColor) || primaryColor === GRAYSCALE_COLOR;

  if (!isPrimaryColor) {
    // eslint-disable-next-line no-console
    console.warn(`The primary color '${primaryColor}' is missing in your palette.`);

    primaryColor = DEFAULT_PRIMARY_COLOR;
  }

  if (isCSR && primary) {
    setCookie(storageKey, String(primaryColor));
    localStorage.setItem(storageKey, String(primaryColor));
  }

  return primaryColor;
}

/**
 * Retrieve neutral color value and save them to cookie and localStorage.
 * @return string | Partial<ColorShades> - neutral color.
 */
function getNeutralColor(neutral?: NeutralColors) {
  const storageKey = `vl-${NEUTRAL_COLOR}`;

  const storedValue = getStored(storageKey);
  let parsedStoredValue: NeutralColors | undefined;

  if (storedValue) {
    try {
      parsedStoredValue = JSON.parse(storedValue);
    } catch {
      parsedStoredValue = storedValue;
    }
  }

  let neutralColor: NeutralColors =
    neutral ?? parsedStoredValue ?? vuelessConfig.neutral ?? DEFAULT_NEUTRAL_COLOR;

  if (isColorShadesObject(neutralColor)) {
    validateColorShades(neutralColor, NEUTRAL_COLOR);

    if (isCSR && neutral) {
      setCookie(storageKey, JSON.stringify(neutralColor));
      localStorage.setItem(storageKey, JSON.stringify(neutralColor));
    }

    return neutralColor;
  }

  const isNeutralColor = NEUTRAL_COLORS.some((color) => color === neutralColor);

  if (!isNeutralColor) {
    // eslint-disable-next-line no-console
    console.warn(`The neutral color '${neutralColor}' is missing in your palette.`);

    neutralColor = DEFAULT_NEUTRAL_COLOR;
  }

  if (isCSR && neutral) {
    setCookie(storageKey, String(neutralColor));
    localStorage.setItem(storageKey, String(neutralColor));
  }

  return neutralColor;
}

/**
 * Calculate font-size values and save them to cookie and localStorage.
 * @return object - xs, sm, md, lg font-size values.
 */
function getText(text?: ThemeConfig["text"]) {
  const storageKey = {
    xs: `vl-${TEXT}-xs`,
    sm: `vl-${TEXT}-sm`,
    md: `vl-${TEXT}-md`,
    lg: `vl-${TEXT}-lg`,
  };

  const runtimeText = primitiveToObject(text) as ThemeConfigText;
  const globalText = primitiveToObject(vuelessConfig.text) as ThemeConfigText;
  const storedText = {
    xs: getStored(storageKey.xs),
    sm: getStored(storageKey.sm),
    md: getStored(storageKey.md),
    lg: getStored(storageKey.lg),
  };

  const textMd = Math.max(0, Number(runtimeText.md ?? globalText.md ?? DEFAULT_TEXT));
  const textXs = Math.max(0, textMd - TEXT_DECREMENT * 2);
  const textSm = Math.max(0, textMd - TEXT_DECREMENT);
  const textLg = Math.max(0, textMd + TEXT_INCREMENT);

  const definedText = {
    xs: Math.max(0, Number(runtimeText.xs ?? getStored(storageKey.xs) ?? globalText.xs ?? 0)),
    sm: Math.max(0, Number(runtimeText.sm ?? getStored(storageKey.sm) ?? globalText.sm ?? 0)),
    md: Math.max(0, Number(runtimeText.md ?? getStored(storageKey.md) ?? globalText.md ?? 0)),
    lg: Math.max(0, Number(runtimeText.lg ?? getStored(storageKey.lg) ?? globalText.lg ?? 0)),
  };

  /* eslint-disable prettier/prettier,vue/max-len */
  const mergedText = {
    xs: runtimeText.xs === undefined && globalText.xs === undefined && (storedText.xs === undefined || typeof text === "number") ? textXs : definedText.xs,
    sm: runtimeText.sm === undefined && globalText.sm === undefined && (storedText.sm === undefined || typeof text === "number") ? textSm : definedText.sm,
    md: runtimeText.md === undefined && globalText.md === undefined && storedText.md === undefined ? textMd : definedText.md,
    lg: runtimeText.lg === undefined && globalText.lg === undefined && (storedText.lg === undefined || typeof text === "number") ? textLg : definedText.lg,
  };
  /* eslint-enable prettier/prettier,vue/max-len */

  if (isCSR && text !== undefined) {
    setCookie(storageKey.xs, String(mergedText.xs));
    setCookie(storageKey.sm, String(mergedText.sm));
    setCookie(storageKey.md, String(mergedText.md));
    setCookie(storageKey.lg, String(mergedText.lg));

    localStorage.setItem(storageKey.xs, String(mergedText.xs));
    localStorage.setItem(storageKey.sm, String(mergedText.sm));
    localStorage.setItem(storageKey.md, String(mergedText.md));
    localStorage.setItem(storageKey.lg, String(mergedText.lg));
  }

  return mergedText;
}

/**
 * Calculate outline values and save them to cookie and localStorage.
 * @return object - sm, md, lg outline values.
 */
function getOutlines(outline?: ThemeConfig["outline"]) {
  const storageKey = {
    sm: `vl-${OUTLINE}-sm`,
    md: `vl-${OUTLINE}-md`,
    lg: `vl-${OUTLINE}-lg`,
  };

  const runtimeOutline = primitiveToObject(outline) as ThemeConfigOutline;
  const globalOutline = primitiveToObject(vuelessConfig.outline) as ThemeConfigOutline;
  const storedOutline = {
    sm: getStored(storageKey.sm),
    md: getStored(storageKey.md),
    lg: getStored(storageKey.lg),
  };

  const outlineMd = Math.max(0, Number(runtimeOutline.md ?? globalOutline.md ?? DEFAULT_OUTLINE));
  const outlineSm = Math.max(0, outlineMd - OUTLINE_DECREMENT);
  let outlineLg = Math.max(0, outlineMd + OUTLINE_INCREMENT);

  if (outlineMd === 0) {
    outlineLg = 0;
  }

  const definedOutline = {
    sm: Math.max(0, Number(runtimeOutline.sm ?? getStored(storageKey.sm) ?? globalOutline.sm ?? 0)),
    md: Math.max(0, Number(runtimeOutline.md ?? getStored(storageKey.md) ?? globalOutline.md ?? 0)),
    lg: Math.max(0, Number(runtimeOutline.lg ?? getStored(storageKey.lg) ?? globalOutline.lg ?? 0)),
  };

  /* eslint-disable prettier/prettier,vue/max-len */
  const mergedOutline = {
    sm: runtimeOutline.sm === undefined && globalOutline.sm === undefined && (storedOutline.sm === undefined || typeof outline === "number") ? outlineSm : definedOutline.sm,
    md: runtimeOutline.md === undefined && globalOutline.md === undefined && storedOutline.md === undefined ? outlineMd : definedOutline.md,
    lg: runtimeOutline.lg === undefined && globalOutline.lg === undefined && (storedOutline.lg === undefined || typeof outline === "number") ? outlineLg : definedOutline.lg,
  };
  /* eslint-enable prettier/prettier,vue/max-len */

  if (isCSR && outline !== undefined) {
    setCookie(storageKey.sm, String(mergedOutline.sm));
    setCookie(storageKey.md, String(mergedOutline.md));
    setCookie(storageKey.lg, String(mergedOutline.lg));

    localStorage.setItem(storageKey.sm, String(mergedOutline.sm));
    localStorage.setItem(storageKey.md, String(mergedOutline.md));
    localStorage.setItem(storageKey.lg, String(mergedOutline.lg));
  }

  return mergedOutline;
}

/**
 * Calculate rounding values and save them to cookie and localStorage.
 * @return object - sm, md, lg rounding values.
 */
function getRoundings(rounding?: ThemeConfig["rounding"]) {
  const storageKey = {
    sm: `vl-${ROUNDING}-sm`,
    md: `vl-${ROUNDING}-md`,
    lg: `vl-${ROUNDING}-lg`,
  };

  const runtimeRounding = primitiveToObject(rounding) as ThemeConfigRounding;
  const globalRounding = primitiveToObject(vuelessConfig.rounding) as ThemeConfigRounding;
  const storedRounding = {
    sm: getStored(storageKey.sm),
    md: getStored(storageKey.md),
    lg: getStored(storageKey.lg),
  };

  // eslint-disable-next-line prettier/prettier
  const roundingMd = Math.max(0, Number(runtimeRounding.md ?? globalRounding.md ?? DEFAULT_ROUNDING));
  let roundingSm = Math.max(0, roundingMd - ROUNDING_DECREMENT);
  let roundingLg = Math.max(0, roundingMd + ROUNDING_INCREMENT);

  if (roundingMd === ROUNDING_INCREMENT) {
    roundingSm = ROUNDING_DECREMENT;
  }

  if (roundingMd === ROUNDING_DECREMENT) {
    roundingSm = ROUNDING_INCREMENT - ROUNDING_DECREMENT;
  }

  if (roundingMd === 0) {
    roundingLg = ROUNDING_INCREMENT - ROUNDING_DECREMENT;
  }

  /* eslint-disable prettier/prettier,vue/max-len */
  const definedRounding = {
    sm: Math.max(0, Number(runtimeRounding.sm ?? getStored(storageKey.sm) ?? globalRounding.sm ?? 0)),
    md: Math.max(0, Number(runtimeRounding.md ?? getStored(storageKey.md) ?? globalRounding.md ?? 0)),
    lg: Math.max(0, Number(runtimeRounding.lg ?? getStored(storageKey.lg) ?? globalRounding.lg ?? 0)),
  };

  const mergedRounding = {
    sm: runtimeRounding.sm === undefined && globalRounding.sm === undefined && (storedRounding.sm === undefined || typeof rounding === "number") ? roundingSm : definedRounding.sm,
    md: runtimeRounding.md === undefined && globalRounding.md === undefined && storedRounding.md === undefined ? roundingMd : definedRounding.md,
    lg: runtimeRounding.lg === undefined && globalRounding.lg === undefined && (storedRounding.lg === undefined || typeof rounding === "number") ? roundingLg : definedRounding.lg,
  };
  /* eslint-enable prettier/prettier,vue/max-len */

  if (isCSR && rounding !== undefined) {
    setCookie(storageKey.sm, String(mergedRounding.sm));
    setCookie(storageKey.md, String(mergedRounding.md));
    setCookie(storageKey.lg, String(mergedRounding.lg));

    localStorage.setItem(storageKey.sm, String(mergedRounding.sm));
    localStorage.setItem(storageKey.md, String(mergedRounding.md));
    localStorage.setItem(storageKey.lg, String(mergedRounding.lg));
  }

  return mergedRounding;
}

/**
 * Retrieve spacing value and save them to cookie and localStorage.
 * @return number - spacing value.
 */
function getSpacing(spacing?: ThemeConfig["spacing"]) {
  const storageKey = `vl-${SPACING}`;

  const storedSpacing = spacing ?? getStored(storageKey) ?? vuelessConfig.spacing;
  const mergedSpacing = Number(storedSpacing ?? DEFAULT_SPACING);

  if (isCSR && spacing !== undefined) {
    setCookie(storageKey, String(mergedSpacing));
    localStorage.setItem(storageKey, String(mergedSpacing));
  }

  return mergedSpacing;
}

/**
 * Retrieve letter spacing value and save them to cookie and localStorage.
 * @return number - letter spacing value.
 */
function getLetterSpacing(letterSpacing?: ThemeConfig["letterSpacing"]) {
  const storageKey = `vl-${LETTER_SPACING}`;

  const spacing = letterSpacing ?? getStored(storageKey) ?? vuelessConfig.letterSpacing;
  const mergedSpacing = Number(spacing ?? DEFAULT_LETTER_SPACING);

  if (isCSR && letterSpacing !== undefined) {
    setCookie(storageKey, String(mergedSpacing));
    localStorage.setItem(storageKey, String(mergedSpacing));
  }

  return mergedSpacing;
}

/**
 * Retrieve disabled opacity value and save them to cookie and localStorage.
 * @return number - opacity value.
 */
function getDisabledOpacity(disabledOpacity?: ThemeConfig["disabledOpacity"]) {
  const storageKey = `vl-${DISABLED_OPACITY}`;

  const opacity = disabledOpacity ?? getStored(storageKey) ?? vuelessConfig.disabledOpacity;
  const mergedOpacity = Math.max(0, Number(opacity ?? DEFAULT_DISABLED_OPACITY));

  if (isCSR && disabledOpacity !== undefined) {
    setCookie(storageKey, String(mergedOpacity));
    localStorage.setItem(storageKey, String(mergedOpacity));
  }

  return mergedOpacity;
}

/**
 * Retrieve light theme configuration and save them to cookie and localStorage.
 * @return Partial<VuelessCssVariables> - light theme configuration.
 */
function getLightTheme(lightTheme?: Partial<VuelessCssVariables>) {
  const storageKey = `vl-${LIGHT_THEME}`;

  const storedLightTheme: Partial<VuelessCssVariables> = JSON.parse(getStored(storageKey) ?? "{}");

  const mergedLightTheme = merge(
    {},
    DEFAULT_LIGHT_THEME,
    vuelessConfig.lightTheme,
    storedLightTheme,
    lightTheme,
  );

  if (isCSR && lightTheme !== undefined) {
    const themeString = JSON.stringify(mergedLightTheme);

    localStorage.setItem(storageKey, themeString);
  }

  return mergedLightTheme;
}

/**
 * Retrieve dark theme configuration and save them to cookie and localStorage.
 * @return Partial<VuelessCssVariables> - dark theme configuration.
 */
function getDarkTheme(darkTheme?: Partial<VuelessCssVariables>) {
  const storageKey = `vl-${DARK_THEME}`;

  const storedDarkTheme: Partial<VuelessCssVariables> = JSON.parse(getStored(storageKey) ?? "{}");

  const mergedDarkTheme = merge(
    {},
    DEFAULT_DARK_THEME,
    vuelessConfig.darkTheme,
    storedDarkTheme,
    darkTheme,
  );

  if (isCSR && darkTheme !== undefined) {
    const themeString = JSON.stringify(mergedDarkTheme);

    localStorage.setItem(storageKey, themeString);
  }

  return mergedDarkTheme;
}

/**
 * Generate and apply Vueless CSS variables.
 * @return string - Vueless CSS variables string.
 */
export function setRootCSSVariables(vars: MergedThemeConfig) {
  let darkVariables: Partial<VuelessCssVariables> = {};

  let variables: Partial<VuelessCssVariables> = {
    "--vl-text-xs": `${Number(vars.text?.xs ?? 0) / PX_IN_REM}rem`,
    "--vl-text-sm": `${Number(vars.text?.sm ?? 0) / PX_IN_REM}rem`,
    "--vl-text-md": `${Number(vars.text?.md ?? 0) / PX_IN_REM}rem`,
    "--vl-text-lg": `${Number(vars.text?.lg ?? 0) / PX_IN_REM}rem`,
    "--vl-outline-sm": `${vars.outline.sm}px`,
    "--vl-outline-md": `${vars.outline.md}px`,
    "--vl-outline-lg": `${vars.outline.lg}px`,
    "--vl-rounding-sm": `${Number(vars.rounding.sm ?? 0) / PX_IN_REM}rem`,
    "--vl-rounding-md": `${Number(vars.rounding.md ?? 0) / PX_IN_REM}rem`,
    "--vl-rounding-lg": `${Number(vars.rounding.lg ?? 0) / PX_IN_REM}rem`,
    "--vl-spacing": `${Number(vars.spacing ?? 0) / PX_IN_REM}rem`,
    "--vl-letter-spacing": `${vars.letterSpacing}em`,
    "--vl-disabled-opacity": `${vars.disabledOpacity}%`,
  };

  if (isColorShadesObject(vars.primary)) {
    for (const shade of COLOR_SHADES) {
      const shadeValue = vars.primary[shade as keyof ColorShades];

      if (shadeValue) {
        variables[`--vl-${PRIMARY_COLOR}-${shade}` as keyof VuelessCssVariables] = shadeValue;
      }
    }
  } else {
    for (const shade of COLOR_SHADES) {
      variables[`--vl-${PRIMARY_COLOR}-${shade}` as keyof VuelessCssVariables] =
        `var(--color-${vars.primary}-${shade})`;
    }
  }

  if (isColorShadesObject(vars.neutral)) {
    for (const shade of COLOR_SHADES) {
      const shadeValue = vars.neutral[shade as keyof ColorShades];

      if (shadeValue) {
        variables[`--vl-${NEUTRAL_COLOR}-${shade}` as keyof VuelessCssVariables] = shadeValue;
      }
    }
  } else {
    for (const shade of COLOR_SHADES) {
      variables[`--vl-${NEUTRAL_COLOR}-${shade}` as keyof VuelessCssVariables] =
        `var(--color-${vars.neutral}-${shade})`;
    }
  }

  const [light, dark] = generateCSSColorVariables(vars.lightTheme ?? {}, vars.darkTheme ?? {});

  variables = { ...variables, ...light };
  darkVariables = { ...darkVariables, ...dark };

  return setCSSVariables(variables, darkVariables);
}

/**
 * Generate CSS color variables.
 * @return string - Vueless color CSS variables.
 */
function generateCSSColorVariables(
  lightTheme: Partial<VuelessCssVariables>,
  darkTheme: Partial<VuelessCssVariables>,
) {
  const variables: Partial<VuelessCssVariables> = {};
  const darkVariables: Partial<VuelessCssVariables> = {};

  Object.entries(lightTheme).forEach(([vuelessVariable, lightColor]) => {
    const variable = vuelessVariable as keyof VuelessCssVariables;

    variables[variable] = lightColor?.startsWith("--") ? `var(${lightColor})` : lightColor;
  });

  Object.entries(darkTheme).forEach(([vuelessVariable, darkColor]) => {
    const variable = vuelessVariable as keyof VuelessCssVariables;

    darkVariables[variable] = darkColor?.startsWith("--") ? `var(${darkColor})` : darkColor;
  });

  return [variables, darkVariables];
}

/**
 * Converts CSS variables object into strings and apply them.
 * @return string - Vueless CSS variables.
 */
function setCSSVariables(
  variables: Partial<VuelessCssVariables>,
  darkVariables: Partial<VuelessCssVariables>,
) {
  const variablesString = Object.entries(variables)
    .map(([key, value]) => `${key}: ${value};`)
    .join(" ");

  const darkVariablesString = Object.entries(darkVariables)
    .map(([key, value]) => `${key}: ${value};`)
    .join(" ");

  const rootVariables = `
    :host, :root {${variablesString}}
    .${DARK_MODE_CLASS} {${darkVariablesString}}
  `;

  if (isCSR) {
    let style = document.getElementById(THEME_TOKENS) as HTMLStyleElement | null;

    if (!style) {
      style = document.createElement("style");
      style.id = THEME_TOKENS;

      const firstStyleOrLink = document.querySelector("link[rel='stylesheet'], style");

      firstStyleOrLink && firstStyleOrLink.parentNode
        ? firstStyleOrLink.parentNode.insertBefore(style, firstStyleOrLink)
        : document.head.appendChild(style);
    }

    style.innerHTML = rootVariables;
  }

  return rootVariables;
}

/**
 * Converts a primitive value into an object with the primitive value assigned to a key "md".
 * If the provided value is already an object, it returns a deeply cloned copy of that object.
 */
function primitiveToObject(value: unknown): object {
  return typeof value === "object" ? cloneDeep(value as object) : { md: value };
}
