import { createContext, useContext, useState, useEffect, useCallback, useRef, type ReactNode } from 'react';
import { writeUiPref } from './uiPref';
import { GetUIPref } from '../../wailsjs/go/main/App';

// Theme system. Each choice maps to a `data-theme` value on <html> that the
// CSS variables in style.css key off of. 'auto' follows the OS light/dark
// preference. The choice is persisted (localStorage + portable UI pref, so it
// travels with the data/ folder like the language).
export type ThemeChoice = 'auto' | 'light-warm' | 'light-cool' | 'light-sage' | 'light-nordic' | 'sahara'
  | 'dim-slate' | 'dark-warm' | 'dark-graphite' | 'dark-indigo' | 'dark-teal' | 'dark-plum' | 'dxhunter' | 'dxhunter-orange' | 'high-contrast';

// Selectable, concrete themes (excludes 'auto') in display order: lights first,
// then darks, with high-contrast last — it is an accessibility choice, not a
// taste one, and listing it among the moods buries it.
export const CONCRETE_THEMES: Exclude<ThemeChoice, 'auto'>[] = [
  'light-warm', 'light-cool', 'light-sage', 'light-nordic', 'sahara',
  'dim-slate', 'dark-warm', 'dark-graphite', 'dark-indigo', 'dark-teal', 'dark-plum', 'dxhunter', 'dxhunter-orange',
  'high-contrast',
];

export const LS_KEY = 'opslog.theme';
// A fresh install starts DARK. A shack is usually a dim room and the screen is
// looked at for hours; every other logger defaults the same way. Graphite
// specifically, because that is what 'auto' already resolves to for a dark
// system — so the two paths agree instead of landing on different darks.
const DEFAULT: ThemeChoice = 'dark-graphite';
const ALL: ThemeChoice[] = ['auto', ...CONCRETE_THEMES];

function systemDark(): boolean {
  try { return window.matchMedia('(prefers-color-scheme: dark)').matches; } catch { return false; }
}

// Resolve 'auto' to a concrete data-theme value (graphite dark / warm light).
function resolve(choice: ThemeChoice): string {
  if (choice === 'auto') return systemDark() ? 'dark-graphite' : 'light-warm';
  return choice;
}

function readStored(): ThemeChoice {
  try {
    const v = localStorage.getItem(LS_KEY) as ThemeChoice | null;
    if (v && ALL.includes(v)) return v;
  } catch { /* private mode */ }
  return DEFAULT;
}

// Stamp the resolved theme onto <html data-theme>.
export function applyThemeToDom(choice: ThemeChoice): void {
  try { document.documentElement.setAttribute('data-theme', resolve(choice)); } catch { /* no DOM */ }
}

// Call before the first render (main.tsx) so there is no flash of the default
// palette while React boots.
export function initTheme(): void { applyThemeToDom(readStored()); }

type Ctx = { theme: ThemeChoice; setTheme: (t: ThemeChoice) => void };
const ThemeCtx = createContext<Ctx>({ theme: DEFAULT, setTheme: () => {} });
export function useTheme(): Ctx { return useContext(ThemeCtx); }

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setThemeState] = useState<ThemeChoice>(() => readStored());
  // Set once the operator changes the theme by hand, so the self-heal below
  // never clobbers a fresh choice with a value it read a moment earlier.
  const userPicked = useRef(false);

  const setTheme = useCallback((t: ThemeChoice) => {
    userPicked.current = true;
    setThemeState(t);
    applyThemeToDom(t);
    writeUiPref(LS_KEY, t);
  }, []);

  // Self-heal the persisted theme. The synchronous boot read (localStorage) can
  // miss it when the WebView cleared its storage, OR when syncPortablePrefs ran
  // while the backend was still starting (settings store not wired yet → GetUIPref
  // returned "" with no error, so nothing was restored) — the "restart lands on
  // the light theme sometimes" bug. Re-read the portable pref from the DB once the
  // backend is up and apply it, retrying briefly to ride out a slow startup.
  useEffect(() => {
    let cancelled = false;
    let tries = 0;
    const load = () => {
      tries += 1;
      GetUIPref(LS_KEY).then((raw) => {
        // Backend ANSWERED (settings store ready). Apply a valid stored value; an
        // empty/invalid one means the theme is genuinely unset → keep the default.
        // Either way we're done — do NOT retry (retrying only matters while the
        // backend is still starting, which now surfaces as a rejected promise).
        if (cancelled || userPicked.current) return;
        const v = raw as ThemeChoice;
        if (v && ALL.includes(v)) {
          try { localStorage.setItem(LS_KEY, v); } catch { /* quota */ }
          applyThemeToDom(v); // idempotent — safe to call unconditionally
          setThemeState(v);
        }
      }).catch(() => {
        // Settings store not ready yet — a brief startup window before the backend
        // has opened the local DB and built the store (the frontend can query it
        // first). Keep retrying well past the old 2.4s cap so a dark theme isn't
        // lost to the light default after an update cleared localStorage.
        if (!cancelled && !userPicked.current && tries < 120) window.setTimeout(load, 300);
      });
    };
    load();
    return () => { cancelled = true; };
  }, []);

  // While in 'auto', re-resolve when the OS light/dark preference flips.
  useEffect(() => {
    if (theme !== 'auto') return;
    const mq = window.matchMedia('(prefers-color-scheme: dark)');
    const onChange = () => applyThemeToDom('auto');
    mq.addEventListener('change', onChange);
    return () => mq.removeEventListener('change', onChange);
  }, [theme]);

  // Keep the DOM attribute in sync with state.
  useEffect(() => { applyThemeToDom(theme); }, [theme]);

  return <ThemeCtx.Provider value={{ theme, setTheme }}>{children}</ThemeCtx.Provider>;
}
