import { getContext, setContext } from 'svelte';
import { browser } from '$app/environment';
import { mode } from 'mode-watcher';
import {
	DEFAULT_PRESET_ID,
	THEME_COOKIE,
	getPreset,
	resolveFonts,
	resolvePalette,
	resolveRadius,
	type ColorTokenKey,
	type FontTokenKey,
	type Mode,
	type ThemeConfig
} from './presets';

/**
 * Live theme state.
 *
 * The server has already inlined the stored theme into the document head, so
 * this class only takes over once the user edits something: it writes the
 * changed custom properties onto <html> and persists the config to a cookie.
 */
export class ThemeState {
	config = $state<ThemeConfig>({ preset: DEFAULT_PRESET_ID });

	/** Which palette the editor is currently writing to. */
	readonly mode = $derived<Mode>(mode.current === 'dark' ? 'dark' : 'light');

	readonly preset = $derived(getPreset(this.config.preset));
	readonly palette = $derived(resolvePalette(this.config, this.mode));
	readonly radius = $derived(resolveRadius(this.config));
	readonly fonts = $derived(resolveFonts(this.config));

	readonly isCustomised = $derived(
		Boolean(
			this.config.radius ||
			this.config.fonts ||
			Object.keys(this.config.light ?? {}).length ||
			Object.keys(this.config.dark ?? {}).length
		)
	);

	/**
	 * True once this session has written inline custom properties onto <html>.
	 * Until then the server-injected stylesheet is the only source of theme, and
	 * it already covers both modes — so we touch nothing and there is no repaint.
	 */
	private touched = false;

	constructor(config: ThemeConfig) {
		this.config = config;
	}

	setPreset(id: string) {
		// Switching preset discards overrides; keeping them across palettes
		// produces colour combinations nobody chose.
		this.config = { preset: id };
		this.apply();
	}

	setColor(token: ColorTokenKey | string, value: string) {
		const mode = this.mode;
		this.config = { ...this.config, [mode]: { ...(this.config[mode] ?? {}), [token]: value } };
		this.apply();
	}

	setRadius(value: string) {
		this.config = { ...this.config, radius: value };
		this.apply();
	}

	setFont(token: FontTokenKey, value: string) {
		this.config = { ...this.config, fonts: { ...(this.config.fonts ?? {}), [token]: value } };
		this.apply();
	}

	reset() {
		this.config = { preset: this.config.preset };
		this.apply();
	}

	/** Push the resolved theme onto the document and persist it. */
	apply() {
		if (!browser) return;
		this.touched = true;

		const root = document.documentElement;
		root.style.setProperty('--radius', this.radius);

		for (const [key, value] of Object.entries(this.fonts)) {
			root.style.setProperty(`--${key}`, value);
		}

		// Inline styles sit on :root and so apply in both modes. Write the
		// palette for the mode being displayed and rewrite it on mode change.
		for (const [key, value] of Object.entries(this.palette)) {
			root.style.setProperty(`--${key}`, value);
		}

		this.persist();
	}

	/**
	 * Re-apply after a light/dark switch, since inline vars are mode-blind.
	 * No-op until something has actually been edited — see `touched`.
	 */
	syncMode() {
		if (!browser || !this.touched) return;
		const palette = resolvePalette(this.config, this.mode);
		for (const [key, value] of Object.entries(palette)) {
			document.documentElement.style.setProperty(`--${key}`, value);
		}
	}

	private persist() {
		if (!browser) return;
		const value = encodeURIComponent(JSON.stringify(this.config));
		// One year, root path, lax — same shape as the auth cookie minus httpOnly,
		// because the editor has to be able to read and rewrite it client-side.
		document.cookie = `${THEME_COOKIE}=${value};path=/;max-age=31536000;samesite=lax`;
	}
}

const THEME_KEY = Symbol('THEME');

export function setThemeState(config: ThemeConfig) {
	return setContext(THEME_KEY, new ThemeState(config));
}

export function getThemeState() {
	return getContext<ReturnType<typeof setThemeState>>(THEME_KEY);
}
