"use client"

/**
 * useAdvancedDisplaySettings
 * =================================================================
 * Single source of truth for the "Advanced display settings" panel that
 * logged-in users see on the /web grid and map-grid embeds.
 *
 * Cascade (highest wins):
 *   user override (panel) > URL param > theme default > code default
 *
 * Persistence is per embed *instance*: localStorage is keyed by a hash of
 * the embed config (embedType + significant URL params + optional
 * `embedInstance`), so a host page with N iframes gets up to N independent
 * override scopes. Identical configs share state by design.
 *
 * -----------------------------------------------------------------
 * HOW TO ADD A NEW TOGGLE
 * -----------------------------------------------------------------
 * 1. Add the field to `AdvancedDisplaySettings` below with a comment
 *    describing what it does and which content/embed scope it applies to.
 * 2. Add an entry to `SETTING_DEFINITIONS`. The Filters Advanced panel
 *    renders controls automatically from this array — no UI changes needed.
 * 3. Wire ONE consumer (pick the layer that actually changes behavior):
 *      - Data-layer (alters the API request): extend the `filters` type in
 *        web/src/hooks/useVibeMapData.ts and add the conditional at the
 *        relevant hardcoded site(s). Add the field to the React-Query key.
 *      - Render-layer (client-side filter at render time): read
 *        `advanced.values.<key>` in the embed client and pass it down to
 *        the component that performs the filter (e.g. EventsGrid for
 *        `hideNoImageItems`).
 *      - Map-layer (alters MapLayout behavior): read `advanced.values.<key>`
 *        in MapGridClient.tsx and pass to MapLayout props (e.g. the
 *        `showOutOfBoundary` pattern).
 * 4. If the toggle has a theme default, add it to the embed client's
 *    `themeDefaults` object passed to this hook. Pick whatever theme path
 *    matches the toggle's semantic domain — there is no forced single
 *    namespace.
 * 5. If the toggle is map-only (or grid-only), reflect that in
 *    `appliesToEmbed`. The panel filters definitions automatically.
 * 6. Document the URL param in
 *    web/src/app/(embed)/embed/grid/URL_PARAMS.md and
 *    web/src/app/(embed)/embed/map-grid/URL_PARAMS.md (whichever applies).
 *
 * Special-case override logic (e.g. forcing `showOutOfBoundary` to false
 * when the active list boundary is a MultiPolygon) belongs at the consumer
 * site, not in this hook. The hook stays purely declarative.
 * =================================================================
 */

import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { DEFAULT_PLACES_SORT, PLACES_SORT_OPTIONS, type PlacesSortBy } from "ui-components/map"
import {
	DEFAULT_RADIUS_M,
	MIN_RADIUS_M,
	MAX_RADIUS_M,
	RADIUS_PRESET_MILES,
	METERS_PER_MILE,
	formatRadiusMiles,
} from "hooks-shared/map"

// -----------------------------------------------------------------
// Settings shape
// -----------------------------------------------------------------

/**
 * "Show unapproved?" 3-way toggle. Drives the `is_approved` API param:
 *   - "no"   → `is_approved=true`  (default — only approved)
 *   - "yes"  → param omitted        (both approved + unapproved)
 *   - "only" → `is_approved=false` (audit — only unapproved)
 */
export type UnapprovedMode = "no" | "yes" | "only"

export const UNAPPROVED_MODE_OPTIONS = [
	{ label: "No", value: "no" },
	{ label: "Yes", value: "yes" },
	{ label: "Only", value: "only" },
] as const

export type AdvancedDisplaySettings = {
	// Data-layer toggles (alter the API request)
	unapprovedMode: UnapprovedMode // default "no" — events + places
	showClosed: boolean // default false — places only. ON → API is_closed=true (only closed); OFF → is_closed=false (only open)
	showOnlyChains: boolean // default false — places only. ON → API is_chain=true (only chains); OFF → is_chain=false
	showOnlyDuplicates: boolean // default false — places only. ON → API is_duplicate=true (only dupes); OFF → is_duplicate=false
	showOnlyNonPublic: boolean // default false — places only. ON → API is_public=false (only non-public); OFF → is_public=true
	completenessScore: number // default 0 — 0..100, → API completeness_score__gte
	/**
	 * Places-only client-pick of the API `ordering` param. Stored value is
	 * the literal ordering string (e.g. "-score_combined"), so consumers
	 * pass it straight through to the data hook. Hidden for events.
	 */
	sortBy: PlacesSortBy
	/**
	 * When true, the data hook switches from polygon boundary search to a
	 * radius search around the active boundary's centerpoint. Pair with
	 * `radius` (meters). The map auto-draws the radius circle.
	 */
	geoMode: boolean
	/**
	 * Radius (meters) used when `geoMode` is true. Defaults to 5 miles.
	 */
	radius: number

	/**
	 * Number of columns in the grid (applies to /embed/grid and the grid
	 * side of /embed/map-grid). Default 3. Theme can override via
	 * `theme.layout.gridColumns`. URL via `?gridColumns=`.
	 */
	gridColumns: number

	// Render/layout-layer toggles (client-side filtering or rendering)
	hideNoImageItems: boolean // default false — drops items with no image at render
	/**
	 * Inverse of `hideNoImageItems`: when on, drops items that DO have an
	 * image so the user sees only items missing media (image-audit mode).
	 * Mutually exclusive with `hideNoImageItems` via `visibleWhen` in
	 * SETTING_DEFINITIONS so both can't be on simultaneously.
	 */
	hideWithImageItems: boolean
	showOutOfBoundary: boolean // default false — shows markers/items outside the boundary polygon

	// Audit-only client-side filters (admin reveals). Each one is a
	// "SHOW ONLY items missing X" inverse of a hide. When on, drops every
	// item that has the relevant data so the admin sees only the ones
	// that need to be filled in.
	showOnlyMissingVibes: boolean
	showOnlyMissingCategories: boolean
	showOnlyMissingDescription: boolean
	/**
	 * Stale-data filter. 0 = off. When > 0, keeps only items whose
	 * `updated_at` is at least N days old.
	 */
	staleDays: number
	/** When true, keeps only items where `is_flagged` is truthy. */
	flaggedOnly: boolean
	/**
	 * Places-only. When true, keeps only items whose `name` contains the
	 * word "closed" (case-insensitive, whole word) — surfaces places that
	 * have been manually flagged as closed in the title (e.g.
	 * "Restaurant (Closed)") but never had the `is_closed` field set.
	 */
	showOnlyClosedInTitle: boolean
}

export const DEFAULTS: AdvancedDisplaySettings = {
	unapprovedMode: "no",
	gridColumns: 3,
	showClosed: false,
	showOnlyChains: false,
	showOnlyDuplicates: false,
	showOnlyNonPublic: false,
	completenessScore: 0,
	sortBy: DEFAULT_PLACES_SORT,
	geoMode: false,
	radius: DEFAULT_RADIUS_M,
	hideNoImageItems: false,
	hideWithImageItems: false,
	showOutOfBoundary: false,
	showOnlyMissingVibes: false,
	showOnlyMissingCategories: false,
	showOnlyMissingDescription: false,
	staleDays: 0,
	flaggedOnly: false,
	showOnlyClosedInTitle: false,
}

// -----------------------------------------------------------------
// Setting definitions (data-driven panel)
// -----------------------------------------------------------------

export type EmbedType = "grid" | "map-grid"
export type ContentScope = "events" | "places" | "both"

type SettingDefinitionBase = {
	key: keyof AdvancedDisplaySettings
	label: string
	helpText: string
	/**
	 * Optional section label. Adjacent definitions sharing a group
	 * render under one section heading in the Advanced popover. Mirrors
	 * the shared `AdvancedSettingDefinition` shape so Filters.tsx reads
	 * it directly.
	 */
	group?: string
	appliesToContent: ContentScope
	appliesToEmbed: ReadonlyArray<EmbedType>
	urlParam: string
	/**
	 * When false, this setting is admin-only and cannot be controlled via URL —
	 * `pickAdvancedFromUrl` will skip it. Used for toggles that should never
	 * leak to logged-out viewers via a stray query string. Default: false.
	 *
	 * Set true ONLY for settings that already have a public URL surface
	 * (e.g. hideNoImageItems, showMarkersOutOfBoundary).
	 */
	urlControllable?: boolean
}

/**
 * Optional extensions all controls can carry. Lined up with the shared
 * `AdvancedSettingDefinition` shape in `ui-components/types` so the
 * Filters Advanced panel renderer reads them automatically.
 */
type ControlExtras = {
	/** Hide the row when this predicate returns false. Used to gate the
	 *  radius slider on geoMode being on. */
	visibleWhen?: (settings: Record<string, unknown>) => boolean
	/** Slider-only: custom formatter for the value chip (e.g. "1.5 mi"). */
	formatValue?: (value: number) => string
	/** Slider-only: quick-pick presets rendered as pill buttons. */
	presets?: ReadonlyArray<{ label: string; value: number }>
	/**
	 * Slider-only: when `true`, render ONLY the preset pills (no slider
	 * track, no value chip). Useful for discrete numeric picks. Requires
	 * `presets`.
	 */
	presetsOnly?: boolean
}

export type SwitchDefinition = SettingDefinitionBase & ControlExtras & {
	control: "switch"
	defaultValue: boolean
}

export type SliderDefinition = SettingDefinitionBase & ControlExtras & {
	control: "slider"
	defaultValue: number
	min: number
	max: number
	step: number
}

export type SelectDefinition = SettingDefinitionBase & ControlExtras & {
	control: "select"
	defaultValue: string
	options: ReadonlyArray<{ label: string; value: string }>
}

export type SettingDefinition = SwitchDefinition | SliderDefinition | SelectDefinition

// Order = render order in the Advanced popover. Adjacent defs sharing
// a `group` render under one section heading.
//
// Information architecture: short, single-concern groups first
// (Sort / Search area / Display / Reveal), then the heavy "Audit" group
// last. The popover packs everything into 2 columns; placing Audit
// last lets the short groups pack tightly into column 1 while Audit
// stands alone in column 2 — minimum dead space, max balance.
//
// Within "Audit" the switches sit together at the top (4 yes/no
// audit filters), with the two slider-with-presets controls below.
// Never intersperse sliders and switches in one group — the eye has
// to re-orient on every row.
//
//   "Sort"             → sortBy
//   "Search area"      → geoMode / radius / showOutOfBoundary
//   "Display"          → hideNoImageItems / hideWithImageItems
//   "Reveal"           → unapprovedMode / showClosed /
//                        showOnlyClosedInTitle / showOnlyChains /
//                        showOnlyDuplicates / showOnlyNonPublic
//   "Audit"            → flaggedOnly / showOnlyMissingVibes /
//                        showOnlyMissingCategories /
//                        showOnlyMissingDescription /
//                        staleDays / completenessScore
export const SETTING_DEFINITIONS: ReadonlyArray<SettingDefinition> = [
	// ─── Sort ────────────────────────────────────────────────────────
	{
		key: "sortBy",
		control: "select",
		defaultValue: DEFAULT_PLACES_SORT,
		options: PLACES_SORT_OPTIONS as ReadonlyArray<{ label: string; value: string }>,
		appliesToContent: "places",
		appliesToEmbed: ["grid", "map-grid"],
		urlParam: "ordering",
		urlControllable: true, // pre-existing public URL param
		group: "Sort",
		label: "Sort",
		helpText: "Order results — defaults to combined score (highest first).",
	},

	// ─── Search area ─────────────────────────────────────────────────
	{
		key: "geoMode",
		control: "switch",
		defaultValue: false,
		appliesToContent: "both",
		appliesToEmbed: ["grid", "map-grid"],
		urlParam: "geoMode",
		// admin-only — the URL surface for radius search is the
		// pre-existing `?radius=` + `?lat=` + `?lng=` trio, not a separate
		// `?geoMode=` flag. Keep this opt-in via the panel only.
		group: "Search area",
		label: "Radius search",
		helpText: "Search a circle around the boundary centerpoint instead of the polygon. Lets results outside the boundary surface.",
	},
	{
		key: "radius",
		control: "slider",
		defaultValue: DEFAULT_RADIUS_M,
		min: MIN_RADIUS_M,
		max: MAX_RADIUS_M,
		step: 100,
		appliesToContent: "both",
		appliesToEmbed: ["grid", "map-grid"],
		urlParam: "radius",
		urlControllable: true, // pre-existing public URL param
		group: "Search area",
		label: "Radius",
		helpText: "Search radius around the boundary centerpoint.",
		formatValue: formatRadiusMiles,
		presets: RADIUS_PRESET_MILES.map((mi) => ({
			label: `${mi} mi`,
			value: Math.round(mi * METERS_PER_MILE),
		})),
		visibleWhen: (settings) => settings.geoMode === true,
	},
	{
		key: "showOutOfBoundary",
		control: "switch",
		defaultValue: false,
		appliesToContent: "both",
		appliesToEmbed: ["map-grid"],
		urlParam: "showMarkersOutOfBoundary",
		urlControllable: true, // pre-existing public URL param
		group: "Search area",
		label: "Show items outside boundary",
		helpText: "Show markers/cards that fall outside the configured boundary polygon. Auto-enabled when radius search is on.",
	},

	// ─── Display ─────────────────────────────────────────────────────
	{
		key: "gridColumns",
		control: "slider",
		defaultValue: 3,
		min: 2,
		max: 5,
		step: 1,
		appliesToContent: "both",
		appliesToEmbed: ["grid", "map-grid"],
		urlParam: "gridColumns",
		urlControllable: true, // pre-existing public URL param
		group: "Display",
		label: "Columns",
		helpText: "Number of columns in the grid. Default comes from the theme.",
		// Discrete numeric choice — render the pills only, skip the slider.
		presetsOnly: true,
		presets: [
			{ label: "2", value: 2 },
			{ label: "3", value: 3 },
			{ label: "4", value: 4 },
			{ label: "5", value: 5 },
		],
	},
	{
		key: "hideNoImageItems",
		control: "switch",
		defaultValue: false,
		appliesToContent: "both",
		appliesToEmbed: ["grid", "map-grid"],
		urlParam: "hideNoImageItems",
		urlControllable: true, // pre-existing public URL param
		group: "Display",
		label: "Hide items without images",
		helpText: "Hide every card that has no display image.",
		// Mutual exclusion is enforced at the action level in the embed
		// clients (turning one on auto-turns the other off) rather than
		// via `visibleWhen`. Hiding the row was confusing when a theme
		// default left one toggled-on and the user couldn't find the
		// inverse to flip it.
	},
	{
		key: "hideWithImageItems",
		control: "switch",
		defaultValue: false,
		appliesToContent: "both",
		appliesToEmbed: ["grid", "map-grid"],
		urlParam: "hideWithImageItems",
		urlControllable: true,
		group: "Display",
		label: "Hide items with images",
		helpText: "Inverse — show only cards that still need a display image.",
	},

	// ─── Reveal (normally hidden by the API) ─────────────────────────
	{
		key: "unapprovedMode",
		control: "select",
		defaultValue: "no",
		options: UNAPPROVED_MODE_OPTIONS as ReadonlyArray<{ label: string; value: string }>,
		appliesToContent: "both",
		appliesToEmbed: ["grid", "map-grid"],
		urlParam: "showUnapproved",
		group: "Reveal",
		label: "Show unapproved",
		helpText: "No = only approved (default). Yes = both approved + unapproved. Only = audit mode, only unapproved items.",
	},
	{
		key: "showClosed",
		control: "switch",
		defaultValue: false,
		appliesToContent: "places",
		appliesToEmbed: ["grid", "map-grid"],
		urlParam: "showClosed",
		group: "Reveal",
		label: "Only closed places",
		helpText: "Audit mode — show ONLY places flagged as permanently closed (is_closed=true). Default request returns only open places.",
	},
	{
		key: "showOnlyClosedInTitle",
		control: "switch",
		defaultValue: false,
		appliesToContent: "places",
		appliesToEmbed: ["grid", "map-grid"],
		urlParam: "showOnlyClosedInTitle",
		group: "Reveal",
		label: "Only \"closed\" in name",
		helpText: "Show only places whose name contains the word \"closed\" (e.g. \"Restaurant (Closed)\") — surfaces places marked closed in the title but never flagged via is_closed.",
	},
	{
		key: "showOnlyChains",
		control: "switch",
		defaultValue: false,
		appliesToContent: "places",
		appliesToEmbed: ["grid", "map-grid"],
		urlParam: "showOnlyChains",
		group: "Reveal",
		label: "Only chains",
		helpText: "Audit mode — show ONLY chain/franchise locations (is_chain=true). Default request excludes chains.",
	},
	{
		key: "showOnlyDuplicates",
		control: "switch",
		defaultValue: false,
		appliesToContent: "places",
		appliesToEmbed: ["grid", "map-grid"],
		urlParam: "showOnlyDuplicates",
		group: "Reveal",
		label: "Only duplicates",
		helpText: "Audit mode — show ONLY places flagged as duplicates (is_duplicate=true). Default request excludes duplicates.",
	},
	{
		key: "showOnlyNonPublic",
		control: "switch",
		defaultValue: false,
		appliesToContent: "places",
		appliesToEmbed: ["grid", "map-grid"],
		urlParam: "showOnlyNonPublic",
		group: "Reveal",
		label: "Only non-public",
		helpText: "Audit mode — show ONLY non-public places (is_public=false). Default request returns only publicly visible places.",
	},

	// ─── Audit ───────────────────────────────────────────────────────
	// Switches first (yes/no audit filters), sliders last (range
	// thresholds). Never mix the two within one group — sliders are
	// tall and break the scanning rhythm a column of switches sets up.
	{
		key: "flaggedOnly",
		control: "switch",
		defaultValue: false,
		appliesToContent: "both",
		appliesToEmbed: ["grid", "map-grid"],
		urlParam: "flaggedOnly",
		group: "Audit",
		label: "Flagged only",
		helpText: "Show only items that have been flagged for review.",
	},
	{
		key: "showOnlyMissingVibes",
		control: "switch",
		defaultValue: false,
		appliesToContent: "both",
		appliesToEmbed: ["grid", "map-grid"],
		urlParam: "showOnlyMissingVibes",
		group: "Audit",
		label: "Show only: missing vibes",
		helpText: "Show only items that have no vibes assigned — useful for taxonomy audits.",
	},
	{
		key: "showOnlyMissingCategories",
		control: "switch",
		defaultValue: false,
		appliesToContent: "both",
		appliesToEmbed: ["grid", "map-grid"],
		urlParam: "showOnlyMissingCategories",
		group: "Audit",
		label: "Show only: missing categories",
		helpText: "Show only items that have no categories assigned.",
	},
	{
		key: "showOnlyMissingDescription",
		control: "switch",
		defaultValue: false,
		appliesToContent: "both",
		appliesToEmbed: ["grid", "map-grid"],
		urlParam: "showOnlyMissingDescription",
		group: "Audit",
		label: "Show only: missing description",
		helpText: "Show only items that have a blank description.",
	},
	{
		key: "staleDays",
		control: "slider",
		defaultValue: 0,
		min: 0,
		max: 365,
		step: 1,
		appliesToContent: "both",
		appliesToEmbed: ["grid", "map-grid"],
		urlParam: "staleDays",
		group: "Audit",
		label: "Not updated in",
		helpText: "Show only items whose last update is at least this many days old. 0 = off.",
		formatValue: (v: number) => (v === 0 ? "Any" : `${v}+ days`),
		presets: [
			{ label: "Any", value: 0 },
			{ label: "7d", value: 7 },
			{ label: "30d", value: 30 },
			{ label: "60d", value: 60 },
			{ label: "90d", value: 90 },
			{ label: "180d", value: 180 },
			{ label: "1yr", value: 365 },
		],
	},
	{
		key: "completenessScore",
		control: "slider",
		defaultValue: 0,
		min: 0,
		max: 100,
		step: 5,
		appliesToContent: "both",
		appliesToEmbed: ["grid", "map-grid"],
		urlParam: "completenessScore",
		// admin-only — no URL surface
		group: "Audit",
		label: "Min completeness score",
		helpText: "Only show items whose completeness score is at least this value.",
	},
]

// Whitelist used by `pickAdvancedFromUrl` and any other URL-param coercion call site.
export const ADVANCED_URL_PARAMS: ReadonlyArray<string> = SETTING_DEFINITIONS.map((d) => d.urlParam)

// Map URL param name → setting key. Useful for parsers and reverse lookups.
export const URL_PARAM_TO_KEY: Readonly<Record<string, keyof AdvancedDisplaySettings>> = SETTING_DEFINITIONS.reduce(
	(acc, def) => {
		acc[def.urlParam] = def.key
		return acc
	},
	{} as Record<string, keyof AdvancedDisplaySettings>,
)

// -----------------------------------------------------------------
// Hook signature
// -----------------------------------------------------------------

export type AdvancedSettingSource = "default" | "theme" | "url" | "user"

export type UseAdvancedDisplaySettingsArgs = {
	embedType: EmbedType
	/**
	 * The embed's parsed URL/search params object. Used to derive a stable
	 * per-instance localStorage key. Pass the raw `params` object you
	 * already have — only a small whitelist of identity-relevant keys is read.
	 */
	instanceParams: Record<string, unknown>
	/** = isLoggedIn. When false, `set` is a no-op and storage is skipped. */
	enabled: boolean
	/**
	 * URL-derived overrides, already coerced. Build with `pickAdvancedFromUrl`
	 * from web/src/utils/embedUtils.ts.
	 */
	urlParams: Partial<AdvancedDisplaySettings>
	/**
	 * Theme-derived defaults. The embed client builds this explicitly by
	 * reading whatever theme paths matter for each setting (e.g.
	 * `theme.cards.hideNoImageItems`, `theme.map.showMarkersOutOfBoundary`).
	 * No forced namespace.
	 */
	themeDefaults: Partial<AdvancedDisplaySettings>
}

export type UseAdvancedDisplaySettingsResult = {
	values: AdvancedDisplaySettings
	sources: Record<keyof AdvancedDisplaySettings, AdvancedSettingSource>
	set: <K extends keyof AdvancedDisplaySettings>(key: K, value: AdvancedDisplaySettings[K]) => void
	reset: (key?: keyof AdvancedDisplaySettings) => void
	isUserOverridden: (key: keyof AdvancedDisplaySettings) => boolean
}

// -----------------------------------------------------------------
// Internals
// -----------------------------------------------------------------

const STORAGE_PREFIX = "vibemap.embed.advancedDisplay.v1"

// Identity keys that distinguish one embed instance from another. Two
// iframes with the same values for every key share a localStorage scope;
// any difference yields a different key. Hosts can force separation with
// `?embedInstance=foo`.
const INSTANCE_KEY_FIELDS = [
	"themeName",
	"type",
	"listId",
	"list_id",
	"cityId",
	"city",
	"boundary",
	"embedInstance",
] as const

function djb2Short(input: string): string {
	let h = 5381
	for (let i = 0; i < input.length; i++) h = ((h << 5) + h + input.charCodeAt(i)) | 0
	return (h >>> 0).toString(36)
}

function deriveInstanceKey(embedType: EmbedType, instanceParams: Record<string, unknown>): string {
	const parts: string[] = [embedType]
	for (const f of INSTANCE_KEY_FIELDS) {
		const v = instanceParams?.[f]
		parts.push(v == null ? "" : String(v))
	}
	return djb2Short(parts.join("|"))
}

function readStorage(storageKey: string): Partial<AdvancedDisplaySettings> {
	if (typeof window === "undefined") return {}
	try {
		const raw = window.localStorage.getItem(storageKey)
		if (!raw) return {}
		const parsed = JSON.parse(raw)
		return parsed && typeof parsed === "object" ? (parsed as Partial<AdvancedDisplaySettings>) : {}
	} catch {
		return {}
	}
}

function writeStorage(storageKey: string, value: Partial<AdvancedDisplaySettings>): void {
	if (typeof window === "undefined") return
	try {
		if (Object.keys(value).length === 0) {
			window.localStorage.removeItem(storageKey)
		} else {
			window.localStorage.setItem(storageKey, JSON.stringify(value))
		}
	} catch {
		/* quota / private mode — accept the loss */
	}
}

// -----------------------------------------------------------------
// Hook
// -----------------------------------------------------------------

export function useAdvancedDisplaySettings(args: UseAdvancedDisplaySettingsArgs): UseAdvancedDisplaySettingsResult {
	const { embedType, instanceParams, enabled, urlParams, themeDefaults } = args

	const storageKey = useMemo(
		() => `${STORAGE_PREFIX}.${deriveInstanceKey(embedType, instanceParams)}`,
		// `instanceParams` is allowed to be a fresh object reference each render;
		// the derivation is content-based so repeated calls with equal values yield
		// the same string. We still memoize on `instanceParams` to avoid recomputing
		// when references happen to be stable.
		// eslint-disable-next-line react-hooks/exhaustive-deps
		[embedType, ...INSTANCE_KEY_FIELDS.map((f) => instanceParams?.[f])],
	)

	// Lazy init reads from localStorage once on mount (client only). When
	// `enabled=false`, we skip the read so logged-out viewers can't pollute
	// (or read) admin overrides.
	const [overrides, setOverrides] = useState<Partial<AdvancedDisplaySettings>>(() =>
		enabled ? readStorage(storageKey) : {},
	)

	// If `enabled` flips from false → true within a session (rare; auth state
	// change without remount), hydrate overrides from storage at that point.
	const lastEnabledRef = useRef(enabled)
	useEffect(() => {
		if (enabled && !lastEnabledRef.current) {
			setOverrides(readStorage(storageKey))
		}
		lastEnabledRef.current = enabled
	}, [enabled, storageKey])

	// If the storage key changes (e.g. instanceParams change without remount,
	// which shouldn't happen in normal embed usage but is defensive), re-load.
	const lastStorageKeyRef = useRef(storageKey)
	useEffect(() => {
		if (lastStorageKeyRef.current !== storageKey) {
			setOverrides(enabled ? readStorage(storageKey) : {})
			lastStorageKeyRef.current = storageKey
		}
	}, [storageKey, enabled])

	const set = useCallback(
		<K extends keyof AdvancedDisplaySettings>(key: K, value: AdvancedDisplaySettings[K]) => {
			if (!enabled) return
			setOverrides((prev) => {
				const next = { ...prev, [key]: value }
				writeStorage(storageKey, next)
				return next
			})
		},
		[enabled, storageKey],
	)

	const reset = useCallback(
		(key?: keyof AdvancedDisplaySettings) => {
			if (!enabled) return
			setOverrides((prev) => {
				if (!key) {
					writeStorage(storageKey, {})
					return {}
				}
				const next = { ...prev }
				delete next[key]
				writeStorage(storageKey, next)
				return next
			})
		},
		[enabled, storageKey],
	)

	const { values, sources } = useMemo(() => {
		const v = { ...DEFAULTS } as AdvancedDisplaySettings
		const s = {} as Record<keyof AdvancedDisplaySettings, AdvancedSettingSource>
		// For "select" controls, only accept values that exist in the current
		// option list. Defends against stale values persisted in
		// localStorage / supplied via URL after the option set was trimmed
		// (e.g. when an API mapping turns out to be broken and we drop the
		// offending sort).
		const isValidForDef = (def: typeof SETTING_DEFINITIONS[number], val: unknown): boolean => {
			if (def.control !== "select") return true
			return def.options.some((opt) => opt.value === val)
		}
		for (const def of SETTING_DEFINITIONS) {
			const k = def.key as keyof AdvancedDisplaySettings
			const userVal = overrides[k]
			const urlVal = urlParams[k]
			const themeVal = themeDefaults[k]
			if (userVal !== undefined && isValidForDef(def, userVal)) {
				;(v as any)[k] = userVal
				s[k] = "user"
			} else if (urlVal !== undefined && isValidForDef(def, urlVal)) {
				;(v as any)[k] = urlVal
				s[k] = "url"
			} else if (themeVal !== undefined && isValidForDef(def, themeVal)) {
				;(v as any)[k] = themeVal
				s[k] = "theme"
			} else {
				s[k] = "default"
			}
		}
		return { values: v, sources: s }
	}, [overrides, urlParams, themeDefaults])

	const isUserOverridden = useCallback(
		(key: keyof AdvancedDisplaySettings) => sources[key] === "user",
		[sources],
	)

	return { values, sources, set, reset, isUserOverridden }
}
