export const appearanceThemeModes = ["light", "dark", "system"] as const;
export const appearanceFontFamilies = ["marianne", "system", "rounded", "serif"] as const;
export const appearanceCardStyles = ["soft", "outlined", "elevated"] as const;
export const appearanceTableStyles = ["comfortable", "compact", "striped"] as const;
export const appearanceDisplayDensities = ["comfortable", "compact", "detailed"] as const;
export const appearanceSidebarStyles = ["soft", "contrast", "floating"] as const;
export const appearanceButtonStyles = ["rounded", "pill", "square"] as const;
export const appearanceInputStyles = ["outline", "soft", "filled"] as const;
export const appearanceBackgroundModes = ["cover", "repeat", "fixed"] as const;
export const appearanceBackgroundPresentationModes = ["full", "edge_frame"] as const;
export const appearanceDecorationScopes = [
  "all",
  "home",
  "public",
  "public_directory",
  "public_map",
  "dashboard",
  "dashboard_home",
  "dashboard_profile",
  "dashboard_documents",
  "dashboard_requests",
  "dashboard_new_request",
  "dashboard_reservation_salle",
  "dashboard_material",
  "dashboard_directory",
  "admin",
  "admin_pilotage",
  "admin_logistics",
  "admin_directory",
  "admin_compliance",
] as const;
export const appearanceDecorationLayers = ["background", "card"] as const;
export const appearanceCardDecorationAnchors = ["top_left", "top_right", "bottom_left", "bottom_right"] as const;
export const appearanceBackgroundPositions = [
  "center",
  "top",
  "bottom",
  "left",
  "right",
  "top_left",
  "top_right",
  "bottom_left",
  "bottom_right",
] as const;
export const appearanceBackgroundTargets = [
  "home",
  "public",
  "public_directory",
  "public_map",
  "dashboard",
  "dashboard_home",
  "dashboard_profile",
  "dashboard_documents",
  "dashboard_requests",
  "dashboard_new_request",
  "dashboard_reservation_salle",
  "dashboard_material",
  "dashboard_directory",
  "admin",
  "admin_pilotage",
  "admin_logistics",
  "admin_directory",
  "admin_compliance",
] as const;
export const appearanceAssetFields = ["logoUrl", "faviconUrl", "heroImageUrl", "backgroundImageUrl"] as const;

export type AppearanceThemeMode = (typeof appearanceThemeModes)[number];
export type AppearanceFontFamily = (typeof appearanceFontFamilies)[number];
export type AppearanceCardStyle = (typeof appearanceCardStyles)[number];
export type AppearanceTableStyle = (typeof appearanceTableStyles)[number];
export type AppearanceDisplayDensity = (typeof appearanceDisplayDensities)[number];
export type AppearanceSidebarStyle = (typeof appearanceSidebarStyles)[number];
export type AppearanceButtonStyle = (typeof appearanceButtonStyles)[number];
export type AppearanceInputStyle = (typeof appearanceInputStyles)[number];
export type AppearanceBackgroundMode = (typeof appearanceBackgroundModes)[number];
export type AppearanceBackgroundPresentationMode = (typeof appearanceBackgroundPresentationModes)[number];
export type AppearanceDecorationScope = (typeof appearanceDecorationScopes)[number];
export type AppearanceDecorationLayer = (typeof appearanceDecorationLayers)[number];
export type AppearanceCardDecorationAnchor = (typeof appearanceCardDecorationAnchors)[number];
export type AppearanceBackgroundPosition = (typeof appearanceBackgroundPositions)[number];
export type AppearanceBackgroundTarget = (typeof appearanceBackgroundTargets)[number];
export type AppearanceAssetField = (typeof appearanceAssetFields)[number];

export interface PortalAppearanceDecoration {
  id: string;
  imageUrl: string;
  scope: AppearanceDecorationScope;
  layer: AppearanceDecorationLayer;
  widthPercent: number;
  topPercent: number;
  leftPercent: number;
  opacity: number;
  blurRadius: number;
  cardAnchor: AppearanceCardDecorationAnchor;
  cardInsetPercent: number;
}

export interface PortalAppearance {
  themeMode: AppearanceThemeMode;
  activeSkinId: string;
  portalTitle: string;
  portalTagline: string;
  primaryColor: string;
  secondaryColor: string;
  accentColor: string;
  buttonColor: string;
  alertColor: string;
  fontFamily: AppearanceFontFamily;
  headingFontFamily: AppearanceFontFamily;
  headingScalePercent: number;
  bodyScalePercent: number;
  logoUrl: string;
  faviconUrl: string;
  heroImageUrl: string;
  backgroundImageUrl: string;
  backgroundEnabled: boolean;
  backgroundMode: AppearanceBackgroundMode;
  backgroundPresentationMode: AppearanceBackgroundPresentationMode;
  backgroundOverlayOpacity: number;
  backgroundBlurRadius: number;
  backgroundTargets: AppearanceBackgroundTarget[];
  backgroundTargetImageUrls: Partial<Record<AppearanceBackgroundTarget, string>>;
  backgroundTargetOverlayOpacities: Partial<Record<AppearanceBackgroundTarget, number>>;
  backgroundTargetBlurRadii: Partial<Record<AppearanceBackgroundTarget, number>>;
  backgroundTargetPositions: Partial<Record<AppearanceBackgroundTarget, AppearanceBackgroundPosition>>;
  backgroundTargetPresentationModes: Partial<Record<AppearanceBackgroundTarget, AppearanceBackgroundPresentationMode>>;
  heroImagePositionX: number;
  heroImagePositionY: number;
  heroOverlayOpacity: number;
  heroTitlePositionX: number;
  heroTitlePositionY: number;
  heroTitleWidthPercent: number;
  heroTaglinePositionX: number;
  heroTaglinePositionY: number;
  heroTaglineWidthPercent: number;
  decorativeElements: PortalAppearanceDecoration[];
  cardStyle: AppearanceCardStyle;
  tableStyle: AppearanceTableStyle;
  displayDensity: AppearanceDisplayDensity;
  sidebarStyle: AppearanceSidebarStyle;
  buttonStyle: AppearanceButtonStyle;
  inputStyle: AppearanceInputStyle;
}

export interface AppearanceSkinPreset {
  id: string;
  label: string;
  description: string;
  category: string;
  usage: string[];
  value: Partial<PortalAppearance>;
}

export const APPEARANCE_SETTING_KEY = "appearance";

export const defaultPortalAppearance: PortalAppearance = {
  themeMode: "light",
  activeSkinId: "guyane-institutionnelle",
  portalTitle: "Portail Associations 973",
  portalTagline: "Démarches, annuaire et services du tissu associatif guyanais",
  primaryColor: "#2e7d32",
  secondaryColor: "#1565c0",
  accentColor: "#f9a825",
  buttonColor: "#1565c0",
  alertColor: "#dc2626",
  fontFamily: "marianne",
  headingFontFamily: "marianne",
  headingScalePercent: 100,
  bodyScalePercent: 100,
  logoUrl: "",
  faviconUrl: "",
  heroImageUrl: "",
  backgroundImageUrl: "",
  backgroundEnabled: false,
  backgroundMode: "cover",
  backgroundPresentationMode: "full",
  backgroundOverlayOpacity: 72,
  backgroundBlurRadius: 0,
  backgroundTargets: ["home", "public", "dashboard", "admin"],
  backgroundTargetImageUrls: {},
  backgroundTargetOverlayOpacities: {},
  backgroundTargetBlurRadii: {},
  backgroundTargetPositions: {},
  backgroundTargetPresentationModes: {},
  heroImagePositionX: 50,
  heroImagePositionY: 50,
  heroOverlayOpacity: 74,
  heroTitlePositionX: 0,
  heroTitlePositionY: 0,
  heroTitleWidthPercent: 50,
  heroTaglinePositionX: 0,
  heroTaglinePositionY: 0,
  heroTaglineWidthPercent: 80,
  decorativeElements: [],
  cardStyle: "soft",
  tableStyle: "comfortable",
  displayDensity: "comfortable",
  sidebarStyle: "soft",
  buttonStyle: "rounded",
  inputStyle: "outline",
};

export const appearanceSkinPresets: AppearanceSkinPreset[] = [
  {
    id: "guyane-institutionnelle",
    label: "Guyane institutionnelle",
    description: "Palette ancrée dans le territoire, pensée pour les collectivités, services publics et fédérations.",
    category: "Institutionnel",
    usage: ["collectivités", "mairies", "fédérations", "services publics"],
    value: {
      activeSkinId: "guyane-institutionnelle",
      portalTitle: "Portail Associations 973",
      portalTagline: "L’espace territorial des démarches associatives en Guyane",
      primaryColor: "#2e7d32",
      secondaryColor: "#1565c0",
      accentColor: "#f9a825",
      buttonColor: "#1565c0",
      alertColor: "#c62828",
      fontFamily: "marianne",
      headingFontFamily: "marianne",
      cardStyle: "soft",
      tableStyle: "comfortable",
      displayDensity: "comfortable",
      sidebarStyle: "soft",
      buttonStyle: "rounded",
      inputStyle: "outline",
      backgroundEnabled: false,
      backgroundMode: "cover",
      backgroundPresentationMode: "full",
      backgroundOverlayOpacity: 72,
      backgroundBlurRadius: 0,
      backgroundTargets: ["home", "public", "dashboard", "admin"],
      backgroundTargetImageUrls: {},
      themeMode: "light",
    },
  },
  {
    id: "centre-spatial-guyanais",
    label: "Centre spatial guyanais",
    description: "Ambiance technologique et institutionnelle, inspirée du spatial, de l’innovation et des grands projets.",
    category: "Spatial",
    usage: ["innovation", "recherche", "éducation", "projets scientifiques"],
    value: {
      activeSkinId: "centre-spatial-guyanais",
      portalTitle: "Portail Associations 973",
      portalTagline: "Innovation, projets et coopérations du territoire guyanais",
      primaryColor: "#0b3d91",
      secondaryColor: "#1f5fbf",
      accentColor: "#f6b317",
      buttonColor: "#155eef",
      alertColor: "#d92d20",
      fontFamily: "system",
      headingFontFamily: "rounded",
      cardStyle: "elevated",
      tableStyle: "striped",
      displayDensity: "compact",
      sidebarStyle: "contrast",
      buttonStyle: "pill",
      inputStyle: "soft",
      backgroundEnabled: false,
      backgroundMode: "cover",
      backgroundPresentationMode: "full",
      backgroundOverlayOpacity: 68,
      backgroundBlurRadius: 0,
      backgroundTargets: ["home", "public", "dashboard", "admin"],
      backgroundTargetImageUrls: {},
      themeMode: "light",
    },
  },
  {
    id: "nature-amazonienne",
    label: "Nature amazonienne",
    description: "Univers plus organique, centré sur la biodiversité, l’environnement et les initiatives de terrain.",
    category: "Territoire",
    usage: ["environnement", "tourisme", "biodiversité", "éducation populaire"],
    value: {
      activeSkinId: "nature-amazonienne",
      portalTitle: "Portail Associations 973",
      portalTagline: "Les initiatives guyanaises entre nature, territoire et engagement",
      primaryColor: "#1b5e20",
      secondaryColor: "#4caf50",
      accentColor: "#0277bd",
      buttonColor: "#2e7d32",
      alertColor: "#d84315",
      fontFamily: "serif",
      headingFontFamily: "serif",
      cardStyle: "outlined",
      tableStyle: "comfortable",
      displayDensity: "detailed",
      sidebarStyle: "floating",
      buttonStyle: "rounded",
      inputStyle: "filled",
      backgroundEnabled: false,
      backgroundMode: "cover",
      backgroundPresentationMode: "full",
      backgroundOverlayOpacity: 70,
      backgroundBlurRadius: 0,
      backgroundTargets: ["home", "public", "dashboard", "admin"],
      backgroundTargetImageUrls: {},
      themeMode: "light",
    },
  },
  {
    id: "republicain",
    label: "Républicain",
    description: "Lecture claire, signal institutionnel fort, pour les espaces administratifs et les usages plus officiels.",
    category: "Administration",
    usage: ["préfecture", "administration", "services publics", "collectivités"],
    value: {
      activeSkinId: "republicain",
      portalTitle: "Portail Associations 973",
      portalTagline: "Un portail administratif lisible, fiable et immédiatement identifiable",
      primaryColor: "#0055a4",
      secondaryColor: "#ffffff",
      accentColor: "#ef4135",
      buttonColor: "#0055a4",
      alertColor: "#b42318",
      fontFamily: "marianne",
      headingFontFamily: "marianne",
      cardStyle: "outlined",
      tableStyle: "compact",
      displayDensity: "compact",
      sidebarStyle: "contrast",
      buttonStyle: "square",
      inputStyle: "outline",
      backgroundEnabled: false,
      backgroundMode: "cover",
      backgroundPresentationMode: "full",
      backgroundOverlayOpacity: 78,
      backgroundBlurRadius: 0,
      backgroundTargets: ["home", "public", "dashboard", "admin"],
      backgroundTargetImageUrls: {},
      themeMode: "light",
    },
  },
  {
    id: "institution-immersif",
    label: "Institution immersif",
    description: "Preset prêt à l’emploi pour un fond premium lisible, avec centre neutre et bords illustrés renforcés.",
    category: "Immersif",
    usage: ["dashboard", "portail institutionnel", "parcours connectés", "habillage premium"],
    value: {
      activeSkinId: "institution-immersif",
      portalTitle: "Portail Associations 973",
      portalTagline: "Un portail territorial clair, immersif et immédiatement lisible",
      primaryColor: "#2e7d32",
      secondaryColor: "#1565c0",
      accentColor: "#f9a825",
      buttonColor: "#1565c0",
      alertColor: "#c62828",
      fontFamily: "marianne",
      headingFontFamily: "marianne",
      cardStyle: "soft",
      tableStyle: "comfortable",
      displayDensity: "compact",
      sidebarStyle: "soft",
      buttonStyle: "rounded",
      inputStyle: "outline",
      backgroundEnabled: true,
      backgroundMode: "cover",
      backgroundPresentationMode: "edge_frame",
      backgroundOverlayOpacity: 62,
      backgroundBlurRadius: 0,
      backgroundTargets: ["home", "public", "dashboard", "admin"],
      backgroundTargetImageUrls: {},
      backgroundTargetOverlayOpacities: {
        home: 58,
        public: 64,
        dashboard: 60,
        admin: 68,
      },
      backgroundTargetPositions: {
        home: "center",
        public: "center",
        dashboard: "right",
        admin: "right",
      },
      backgroundTargetPresentationModes: {
        home: "edge_frame",
        public: "edge_frame",
        dashboard: "edge_frame",
        admin: "edge_frame",
      },
      themeMode: "light",
    },
  },
  {
    id: "parcours-guide-immersif",
    label: "Parcours guidé immersif",
    description: "Met en avant les parcours, les formulaires et les cartes avec un habillage plus narratif et très lisible.",
    category: "Immersif",
    usage: ["nouvelle demande", "réservation", "matériel", "parcours guidés"],
    value: {
      activeSkinId: "parcours-guide-immersif",
      portalTitle: "Portail Associations 973",
      portalTagline: "Des démarches plus claires, étape par étape",
      primaryColor: "#2f9e44",
      secondaryColor: "#1d4ed8",
      accentColor: "#f6c453",
      buttonColor: "#2563eb",
      alertColor: "#c2410c",
      fontFamily: "marianne",
      headingFontFamily: "rounded",
      cardStyle: "soft",
      tableStyle: "comfortable",
      displayDensity: "compact",
      sidebarStyle: "floating",
      buttonStyle: "pill",
      inputStyle: "soft",
      backgroundEnabled: true,
      backgroundMode: "cover",
      backgroundPresentationMode: "edge_frame",
      backgroundOverlayOpacity: 56,
      backgroundBlurRadius: 1,
      backgroundTargets: [
        "dashboard",
        "dashboard_home",
        "dashboard_new_request",
        "dashboard_reservation_salle",
        "dashboard_material",
      ],
      backgroundTargetImageUrls: {},
      backgroundTargetOverlayOpacities: {
        dashboard: 58,
        dashboard_home: 54,
        dashboard_new_request: 52,
        dashboard_reservation_salle: 56,
        dashboard_material: 56,
      },
      backgroundTargetPositions: {
        dashboard: "right",
        dashboard_home: "right",
        dashboard_new_request: "right",
        dashboard_reservation_salle: "bottom_right",
        dashboard_material: "bottom_left",
      },
      backgroundTargetPresentationModes: {
        dashboard: "edge_frame",
        dashboard_home: "edge_frame",
        dashboard_new_request: "edge_frame",
        dashboard_reservation_salle: "edge_frame",
        dashboard_material: "edge_frame",
      },
      themeMode: "light",
    },
  },
  {
    id: "pilotage-territorial",
    label: "Pilotage territorial",
    description: "Preset plus sobre et plus dense, pensé pour le bordereau, la logistique et les référentiels admin.",
    category: "Immersif",
    usage: ["bordereau", "logistique", "référentiels", "pilotage admin"],
    value: {
      activeSkinId: "pilotage-territorial",
      portalTitle: "Portail Associations 973",
      portalTagline: "Référentiels, logistique et pilotage du territoire",
      primaryColor: "#256f5c",
      secondaryColor: "#0f4c81",
      accentColor: "#d97706",
      buttonColor: "#0f4c81",
      alertColor: "#b91c1c",
      fontFamily: "marianne",
      headingFontFamily: "marianne",
      cardStyle: "outlined",
      tableStyle: "compact",
      displayDensity: "compact",
      sidebarStyle: "soft",
      buttonStyle: "rounded",
      inputStyle: "outline",
      backgroundEnabled: true,
      backgroundMode: "cover",
      backgroundPresentationMode: "edge_frame",
      backgroundOverlayOpacity: 68,
      backgroundBlurRadius: 0,
      backgroundTargets: ["admin", "admin_pilotage", "admin_logistics", "admin_directory", "admin_compliance"],
      backgroundTargetImageUrls: {},
      backgroundTargetOverlayOpacities: {
        admin: 68,
        admin_pilotage: 70,
        admin_logistics: 66,
        admin_directory: 64,
        admin_compliance: 72,
      },
      backgroundTargetPositions: {
        admin: "right",
        admin_pilotage: "center",
        admin_logistics: "bottom_right",
        admin_directory: "left",
        admin_compliance: "top_right",
      },
      backgroundTargetPresentationModes: {
        admin: "edge_frame",
        admin_pilotage: "edge_frame",
        admin_logistics: "edge_frame",
        admin_directory: "edge_frame",
        admin_compliance: "edge_frame",
      },
      themeMode: "light",
    },
  },
];

const HEX_COLOR_REGEX = /^#(?:[0-9a-fA-F]{3}){1,2}$/;

function normalizeHexColor(value: string | undefined, fallback: string) {
  const trimmed = value?.trim();
  if (!trimmed || !HEX_COLOR_REGEX.test(trimmed)) {
    return fallback;
  }

  if (trimmed.length === 4) {
    return `#${trimmed[1]}${trimmed[1]}${trimmed[2]}${trimmed[2]}${trimmed[3]}${trimmed[3]}`.toLowerCase();
  }

  return trimmed.toLowerCase();
}

function normalizeUrl(value: string | undefined) {
  return value?.trim() || "";
}

function normalizeText(value: string | undefined, fallback: string, maxLength: number) {
  const trimmed = value?.trim();
  if (!trimmed) {
    return fallback;
  }
  return trimmed.slice(0, maxLength);
}

function normalizePercent(value: number | undefined, fallback: number, min = 0, max = 100) {
  if (typeof value !== "number" || Number.isNaN(value)) {
    return fallback;
  }

  return Math.min(max, Math.max(min, Math.round(value)));
}

function normalizeOption<T extends readonly string[]>(value: string | undefined, allowed: T, fallback: T[number]) {
  if (value && allowed.includes(value)) {
    return value as T[number];
  }
  return fallback;
}

function normalizeOptionsArray<T extends readonly string[]>(value: string[] | undefined, allowed: T, fallback: readonly T[number][]) {
  if (!Array.isArray(value)) {
    return [...fallback] as T[number][];
  }

  const filtered = value.filter((entry): entry is T[number] => allowed.includes(entry as T[number]));
  return filtered.length ? Array.from(new Set(filtered)) : [...fallback] as T[number][];
}

function normalizeTargetImageUrls(
  value: Partial<Record<string, string>> | undefined,
  allowed: readonly AppearanceBackgroundTarget[],
) {
  const next: Partial<Record<AppearanceBackgroundTarget, string>> = {};
  if (!value || typeof value !== "object") {
    return next;
  }

  for (const target of allowed) {
    const normalized = normalizeUrl(value[target]);
    if (normalized) {
      next[target] = normalized;
    }
  }

  return next;
}

function normalizeTargetOverlayOpacities(
  value: Partial<Record<string, number>> | undefined,
  allowed: readonly AppearanceBackgroundTarget[],
) {
  const next: Partial<Record<AppearanceBackgroundTarget, number>> = {};
  if (!value || typeof value !== "object") {
    return next;
  }

  for (const target of allowed) {
    const raw = value[target];
    if (typeof raw === "number" && !Number.isNaN(raw)) {
      next[target] = Math.min(100, Math.max(0, Math.round(raw)));
    }
  }

  return next;
}

function normalizeTargetBlurRadii(
  value: Partial<Record<string, number>> | undefined,
  allowed: readonly AppearanceBackgroundTarget[],
) {
  const next: Partial<Record<AppearanceBackgroundTarget, number>> = {};
  if (!value || typeof value !== "object") {
    return next;
  }

  for (const target of allowed) {
    const raw = value[target];
    if (typeof raw === "number" && !Number.isNaN(raw)) {
      next[target] = Math.min(24, Math.max(0, Math.round(raw)));
    }
  }

  return next;
}

function normalizeTargetPositions(
  value: Partial<Record<string, string>> | undefined,
  allowed: readonly AppearanceBackgroundTarget[],
) {
  const next: Partial<Record<AppearanceBackgroundTarget, AppearanceBackgroundPosition>> = {};
  if (!value || typeof value !== "object") {
    return next;
  }

  for (const target of allowed) {
    const raw = value[target];
    if (raw && appearanceBackgroundPositions.includes(raw as AppearanceBackgroundPosition)) {
      next[target] = raw as AppearanceBackgroundPosition;
    }
  }

  return next;
}

function normalizeTargetPresentationModes(
  value: Partial<Record<string, string>> | undefined,
  allowed: readonly AppearanceBackgroundTarget[],
) {
  const next: Partial<Record<AppearanceBackgroundTarget, AppearanceBackgroundPresentationMode>> = {};
  if (!value || typeof value !== "object") {
    return next;
  }

  for (const target of allowed) {
    const raw = value[target];
    if (raw && appearanceBackgroundPresentationModes.includes(raw as AppearanceBackgroundPresentationMode)) {
      next[target] = raw as AppearanceBackgroundPresentationMode;
    }
  }

  return next;
}

function normalizeDecorativeElements(value: unknown): PortalAppearanceDecoration[] {
  if (!Array.isArray(value)) {
    return [];
  }

  return value
    .map((entry, index) => {
      const raw = entry && typeof entry === "object" ? entry as Partial<PortalAppearanceDecoration> : {};
      const imageUrl = normalizeUrl(raw.imageUrl);
      if (!imageUrl) {
        return null;
      }

      return {
        id: normalizeText(raw.id, `decor-${index + 1}`, 80),
        imageUrl,
        scope: normalizeOption(raw.scope, appearanceDecorationScopes, "all"),
        layer: normalizeOption(raw.layer, appearanceDecorationLayers, "background"),
        widthPercent: normalizePercent(raw.widthPercent, 18, 6, 38),
        topPercent: normalizePercent(raw.topPercent, 20, 0, 100),
        leftPercent: normalizePercent(raw.leftPercent, 10, 0, 100),
        opacity: normalizePercent(raw.opacity, 12, 4, 28),
        blurRadius: normalizePercent(raw.blurRadius, 2, 0, 12),
        cardAnchor: normalizeOption(raw.cardAnchor, appearanceCardDecorationAnchors, "top_right"),
        cardInsetPercent: normalizePercent(raw.cardInsetPercent, 6, 0, 20),
      } satisfies PortalAppearanceDecoration;
    })
    .filter((entry): entry is PortalAppearanceDecoration => Boolean(entry))
    .slice(0, 6);
}

export function getAppearanceSkinPreset(presetId: string | undefined) {
  return appearanceSkinPresets.find((preset) => preset.id === presetId) || null;
}

export function sanitizePortalAppearance(value?: Partial<PortalAppearance> | null): PortalAppearance {
  const preset = getAppearanceSkinPreset(value?.activeSkinId) || getAppearanceSkinPreset(defaultPortalAppearance.activeSkinId);
  const presetValue = preset?.value || {};

  const base: PortalAppearance = {
    ...defaultPortalAppearance,
    ...presetValue,
  };
  const normalizedBackgroundImageUrl = normalizeUrl(value?.backgroundImageUrl);
  const normalizedBackgroundTargetImageUrls = normalizeTargetImageUrls(value?.backgroundTargetImageUrls, appearanceBackgroundTargets);
  const normalizedBackgroundTargetOverlayOpacities = normalizeTargetOverlayOpacities(
    value?.backgroundTargetOverlayOpacities,
    appearanceBackgroundTargets
  );
  const normalizedBackgroundTargetBlurRadii = normalizeTargetBlurRadii(
    value?.backgroundTargetBlurRadii,
    appearanceBackgroundTargets
  );
  const normalizedBackgroundTargetPositions = normalizeTargetPositions(
    value?.backgroundTargetPositions,
    appearanceBackgroundTargets
  );
  const normalizedBackgroundTargetPresentationModes = normalizeTargetPresentationModes(
    value?.backgroundTargetPresentationModes,
    appearanceBackgroundTargets
  );
  const normalizedDecorativeElements = normalizeDecorativeElements(value?.decorativeElements);
  const hasAnyBackgroundAsset =
    Boolean(normalizedBackgroundImageUrl) || Object.values(normalizedBackgroundTargetImageUrls).some(Boolean);

  return {
    themeMode: normalizeOption(value?.themeMode, appearanceThemeModes, base.themeMode),
    activeSkinId: normalizeText(value?.activeSkinId, base.activeSkinId, 80),
    portalTitle: normalizeText(value?.portalTitle, base.portalTitle, 120),
    portalTagline: normalizeText(value?.portalTagline, base.portalTagline, 220),
    primaryColor: normalizeHexColor(value?.primaryColor, base.primaryColor),
    secondaryColor: normalizeHexColor(value?.secondaryColor, base.secondaryColor),
    accentColor: normalizeHexColor(value?.accentColor, base.accentColor),
    buttonColor: normalizeHexColor(value?.buttonColor, base.buttonColor),
    alertColor: normalizeHexColor(value?.alertColor, base.alertColor),
    fontFamily: normalizeOption(value?.fontFamily, appearanceFontFamilies, base.fontFamily),
    headingFontFamily: normalizeOption(value?.headingFontFamily, appearanceFontFamilies, base.headingFontFamily),
    headingScalePercent: normalizePercent(value?.headingScalePercent, base.headingScalePercent, 85, 130),
    bodyScalePercent: normalizePercent(value?.bodyScalePercent, base.bodyScalePercent, 90, 115),
    logoUrl: normalizeUrl(value?.logoUrl),
    faviconUrl: normalizeUrl(value?.faviconUrl),
    heroImageUrl: normalizeUrl(value?.heroImageUrl),
    backgroundImageUrl: normalizedBackgroundImageUrl,
    backgroundEnabled: Boolean(value?.backgroundEnabled) && hasAnyBackgroundAsset,
    backgroundMode: normalizeOption(value?.backgroundMode, appearanceBackgroundModes, base.backgroundMode),
    backgroundPresentationMode: normalizeOption(
      value?.backgroundPresentationMode,
      appearanceBackgroundPresentationModes,
      base.backgroundPresentationMode
    ),
    backgroundOverlayOpacity: normalizePercent(value?.backgroundOverlayOpacity, base.backgroundOverlayOpacity, 0, 100),
    backgroundBlurRadius: normalizePercent(value?.backgroundBlurRadius, base.backgroundBlurRadius, 0, 24),
    backgroundTargets: normalizeOptionsArray(value?.backgroundTargets, appearanceBackgroundTargets, base.backgroundTargets),
    backgroundTargetImageUrls: normalizedBackgroundTargetImageUrls,
    backgroundTargetOverlayOpacities: normalizedBackgroundTargetOverlayOpacities,
    backgroundTargetBlurRadii: normalizedBackgroundTargetBlurRadii,
    backgroundTargetPositions: normalizedBackgroundTargetPositions,
    backgroundTargetPresentationModes: normalizedBackgroundTargetPresentationModes,
    heroImagePositionX: normalizePercent(value?.heroImagePositionX, base.heroImagePositionX),
    heroImagePositionY: normalizePercent(value?.heroImagePositionY, base.heroImagePositionY),
    heroOverlayOpacity: normalizePercent(value?.heroOverlayOpacity, base.heroOverlayOpacity),
    heroTitlePositionX: normalizePercent(value?.heroTitlePositionX, base.heroTitlePositionX, -50, 50),
    heroTitlePositionY: normalizePercent(value?.heroTitlePositionY, base.heroTitlePositionY, -50, 50),
    heroTitleWidthPercent: normalizePercent(value?.heroTitleWidthPercent, base.heroTitleWidthPercent, 30, 100),
    heroTaglinePositionX: normalizePercent(value?.heroTaglinePositionX, base.heroTaglinePositionX, -50, 50),
    heroTaglinePositionY: normalizePercent(value?.heroTaglinePositionY, base.heroTaglinePositionY, -50, 50),
    heroTaglineWidthPercent: normalizePercent(value?.heroTaglineWidthPercent, base.heroTaglineWidthPercent, 30, 100),
    decorativeElements: normalizedDecorativeElements,
    cardStyle: normalizeOption(value?.cardStyle, appearanceCardStyles, base.cardStyle),
    tableStyle: normalizeOption(value?.tableStyle, appearanceTableStyles, base.tableStyle),
    displayDensity: normalizeOption(value?.displayDensity, appearanceDisplayDensities, base.displayDensity),
    sidebarStyle: normalizeOption(value?.sidebarStyle, appearanceSidebarStyles, base.sidebarStyle),
    buttonStyle: normalizeOption(value?.buttonStyle, appearanceButtonStyles, base.buttonStyle),
    inputStyle: normalizeOption(value?.inputStyle, appearanceInputStyles, base.inputStyle),
  };
}
