import { atom } from 'jotai';
import { isSupportedTargetLang } from '../utils/translation/langUtils';

const STORAGE_KEY = 'settings';
export type DateFormat =
  | 'D MMM YYYY'
  | 'DD/MM/YYYY'
  | 'MM/DD/YYYY'
  | 'YYYY/MM/DD'
  | 'YYYY-MM-DD'
  | '';
export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500';
// Call mic noise suppression tier:
//  - 'off'     : no suppression
//  - 'browser' : WebRTC built-in suppression (Element Call noiseSuppression param)
//  - 'ml'      : client-side RNNoise ML suppression (Lotus denoise shim)
export type NoiseSuppressionMode = 'off' | 'browser' | 'ml';
// Self-hostable, build-bundled ML models. DeepFilterNet 3 is included via
// deepfilternet3-noise-filter with its df_bg.wasm + ONNX model VENDORED and
// self-hosted (its cdnUrl is overridden), so it no longer depends on an external
// CDN. Its wasm is single-threaded, so no COOP/COEP cross-origin isolation is
// required (see LOTUS_DENOISE_ENGINEERING_REVIEW.md).
export type DenoiseModelId = 'rnnoise' | 'speex' | 'dtln' | 'deepfilternet';
// Incoming-call ringtone. 'classic' is the bundled call.ogg clip; 'chime' /
// 'soft' / 'retro' are synthesized in-browser (see utils/ringtones.ts);
// 'none' is silent (visual-only incoming-call UI).
export type RingtoneId = 'classic' | 'chime' | 'soft' | 'retro' | 'none';
// [P5-31] Granular call quality caps. 'auto' = don't cap (the EC fork keeps its
// default encoding). Numbers are kbps (audio/screenshare bitrate) or fps
// (screenshare framerate); converted to the fork's bits/sec + fps payload in
// utils/callQuality.ts and applied via the io.lotus.set_quality widget action.
export type CallAudioBitrate = 'auto' | '32' | '64' | '96' | '128' | '256';
export type ScreenshareBitrate = 'auto' | '500' | '1500' | '3000' | '8000';
export type ScreenshareFramerate = 'auto' | '15' | '30' | '60';
export type ChatBackground =
  | 'none'
  | 'blueprint'
  | 'carbon'
  | 'stars'
  | 'topographic'
  | 'herringbone'
  | 'crosshatch'
  | 'chevron'
  | 'polka'
  | 'triangles'
  | 'plaid'
  | 'tactical'
  | 'circuit'
  | 'hexgrid'
  | 'waves'
  | 'neon'
  | 'aurora'
  | 'anim-rain'
  | 'anim-stars'
  | 'anim-pulse'
  | 'anim-aurora'
  | 'anim-fireflies';
export enum MessageLayout {
  Modern = 0,
  Compact = 1,
  Bubble = 2,
}

/**
 * Keys of the toggleable composer toolbar buttons. Also used as the identity
 * of each button when persisting/restoring a custom drag-and-drop order.
 */
export const COMPOSER_TOOLBAR_BUTTON_KEYS = [
  'showFormat',
  'showEmoji',
  'showSticker',
  'showGif',
  'showLocation',
  'showPoll',
  'showVoice',
  'showSchedule',
] as const;

export type ComposerToolbarButtonKey = (typeof COMPOSER_TOOLBAR_BUTTON_KEYS)[number];

/**
 * The fixed order the composer toolbar rendered before reordering existed.
 * Used as the fallback for users without a saved order, and to append any
 * new/unknown button keys, so existing users see no change.
 */
export const DEFAULT_COMPOSER_TOOLBAR_ORDER: ComposerToolbarButtonKey[] = [
  'showFormat',
  'showSticker',
  'showEmoji',
  'showGif',
  'showLocation',
  'showPoll',
  'showVoice',
  'showSchedule',
];

export interface ComposerToolbarSettings {
  showFormat: boolean;
  showEmoji: boolean;
  showSticker: boolean;
  showGif: boolean;
  showLocation: boolean;
  showPoll: boolean;
  showVoice: boolean;
  showSchedule: boolean;
  order: ComposerToolbarButtonKey[];
}

export const DEFAULT_COMPOSER_TOOLBAR: ComposerToolbarSettings = {
  showFormat: true,
  showEmoji: true,
  showSticker: true,
  showGif: true,
  showLocation: true,
  showPoll: true,
  showVoice: true,
  showSchedule: true,
  order: DEFAULT_COMPOSER_TOOLBAR_ORDER,
};

/**
 * Returns a complete, de-duplicated composer toolbar order:
 * - drops unknown/duplicate keys from the saved order
 * - appends any missing keys (new buttons or existing users with no saved
 *   order) at the end in their canonical default position
 * so a button can never disappear from the toolbar.
 */
export const normalizeComposerToolbarOrder = (
  order: ComposerToolbarButtonKey[] | undefined,
): ComposerToolbarButtonKey[] => {
  const known = new Set<ComposerToolbarButtonKey>(COMPOSER_TOOLBAR_BUTTON_KEYS);
  const seen = new Set<ComposerToolbarButtonKey>();
  const result: ComposerToolbarButtonKey[] = [];

  (order ?? []).forEach((key) => {
    if (known.has(key) && !seen.has(key)) {
      seen.add(key);
      result.push(key);
    }
  });
  // Append missing keys in their canonical default position…
  DEFAULT_COMPOSER_TOOLBAR_ORDER.forEach((key) => {
    if (!seen.has(key)) {
      seen.add(key);
      result.push(key);
    }
  });
  // …then any known key not covered by the default order (safety net so a new
  // button added to COMPOSER_TOOLBAR_BUTTON_KEYS but forgotten in the default
  // order can still render/reorder rather than being permanently dropped).
  COMPOSER_TOOLBAR_BUTTON_KEYS.forEach((key) => {
    if (!seen.has(key)) {
      seen.add(key);
      result.push(key);
    }
  });

  return result;
};

export interface Settings {
  themeId?: string;
  useSystemTheme: boolean;
  lightThemeId?: string;
  darkThemeId?: string;
  monochromeMode?: boolean;
  isMarkdown: boolean;
  editorToolbar: boolean;
  twitterEmoji: boolean;
  pageZoom: number;
  hideActivity: boolean;
  hidePresence: boolean;
  privateReadReceipts: boolean;
  presenceStatus: 'auto' | 'online' | 'idle' | 'dnd' | 'invisible';

  isPeopleDrawer: boolean;
  memberSortFilterIndex: number;
  enterForNewline: boolean;
  messageLayout: MessageLayout;
  messageSpacing: MessageSpacing;
  hideMembershipEvents: boolean;
  hideNickAvatarEvents: boolean;
  mediaAutoLoad: boolean;
  urlPreview: boolean;
  encUrlPreview: boolean;
  inlineMediaEmbeds: boolean;
  showHiddenEvents: boolean;
  // [MSC1763] Opt-in: permanently redact your OWN messages once a room's
  // retention window passes (default off — nothing auto-deletes by surprise).
  enforceRetentionLocally: boolean;
  legacyUsernameColor: boolean;

  showNotifications: boolean;
  isNotificationSounds: boolean;
  messageSoundId: 'notification' | 'invite' | 'call' | 'none';
  inviteSoundId: 'notification' | 'invite' | 'call' | 'none';

  quietHoursEnabled: boolean;
  quietHoursStart: string; // "HH:MM" 24h
  quietHoursEnd: string; // "HH:MM" 24h

  homeRoomSort: 'recent' | 'alpha' | 'unread';

  hour24Clock: boolean;
  dateFormatString: string;

  developerTools: boolean;
  lotusTerminal: boolean;

  chatBackground: ChatBackground;
  perMessageProfiles: boolean;

  cameraOnJoin: boolean;
  callNoiseSuppression: NoiseSuppressionMode;
  callDenoiseModel: DenoiseModelId;
  callDenoiseNativeNS: boolean;
  callDenoiseGate: boolean;
  callDenoiseGateThreshold: number;
  pttMode: boolean;
  pttKey: string;

  nightLightEnabled: boolean;
  nightLightOpacity: number;
  nightLightSchedule: boolean;
  nightLightStart: string;
  nightLightEnd: string;

  glassmorphismSidebar: boolean;

  deafenKey: string;

  warnOnUnverifiedDevices: boolean;

  pauseAnimations: boolean;

  composerToolbarButtons: ComposerToolbarSettings;

  mentionHighlightColor: string;
  customAccentColor: string;
  fontFamily: 'system' | 'inter' | 'jetbrains-mono' | 'fira-code';

  afkAutoMute: boolean;
  afkTimeoutMinutes: number;

  callJoinLeaveSound: 'off' | 'chime' | 'soft' | 'retro';
  ringtoneId: RingtoneId;
  ringtoneVolume: number; // 0–100

  // [P5-31] Call quality controls
  callAudioBitrate: CallAudioBitrate;
  screenshareBitrate: ScreenshareBitrate;
  screenshareFramerate: ScreenshareFramerate;
  // [P5-15] In-call soundboard
  soundboardEnabled: boolean;
  soundboardVolume: number; // 0–100

  seasonalThemeOverride:
    | 'auto'
    | 'off'
    | 'halloween'
    | 'christmas'
    | 'newyear'
    | 'autumn'
    | 'aprilfools'
    | 'lunar'
    | 'valentines'
    | 'stpatricks'
    | 'earthday'
    | 'deepspace'
    | 'arcade';

  // On-device message translation
  translateTargetLang: string; // BCP-47 base code, default 'en'
  autoTranslate: boolean; // auto-translate incoming foreign messages (opt-in)
}

const defaultSettings: Settings = {
  themeId: undefined,
  useSystemTheme: true,
  lightThemeId: undefined,
  darkThemeId: undefined,
  monochromeMode: false,
  isMarkdown: true,
  editorToolbar: false,
  twitterEmoji: false,
  pageZoom: 100,
  hideActivity: false,
  hidePresence: false,
  privateReadReceipts: false,
  presenceStatus: 'auto',

  isPeopleDrawer: true,
  memberSortFilterIndex: 0,
  enterForNewline: false,
  messageLayout: 0,
  messageSpacing: '400',
  hideMembershipEvents: false,
  hideNickAvatarEvents: true,
  mediaAutoLoad: true,
  urlPreview: true,
  encUrlPreview: true,
  inlineMediaEmbeds: true,
  showHiddenEvents: false,
  enforceRetentionLocally: false,
  legacyUsernameColor: false,

  showNotifications: true,
  isNotificationSounds: true,
  messageSoundId: 'notification',
  inviteSoundId: 'invite',

  quietHoursEnabled: false,
  quietHoursStart: '23:00',
  quietHoursEnd: '08:00',

  homeRoomSort: 'recent',

  hour24Clock: false,
  dateFormatString: 'D MMM YYYY',

  developerTools: false,
  lotusTerminal: false,

  chatBackground: 'none',
  perMessageProfiles: false,

  cameraOnJoin: false,
  // Tier default stays browser-native (known-good; best-perceived in testing so
  // far). If a user opts into the ML tier, default to the highest-quality model.
  callNoiseSuppression: 'browser',
  callDenoiseModel: 'deepfilternet',
  // "Series suppression" (stack the browser's native NS before the ML model) is
  // off by default — best practice is a single NS stage; it's an opt-in test aid.
  callDenoiseNativeNS: false,
  callDenoiseGate: false,
  callDenoiseGateThreshold: -45,
  pttMode: false,
  pttKey: 'Space',

  nightLightSchedule: false,
  nightLightStart: '21:00',
  nightLightEnd: '07:00',
  nightLightEnabled: false,
  nightLightOpacity: 30,

  glassmorphismSidebar: false,

  deafenKey: 'KeyM',

  warnOnUnverifiedDevices: false,

  pauseAnimations: false,

  composerToolbarButtons: DEFAULT_COMPOSER_TOOLBAR,

  mentionHighlightColor: '',
  customAccentColor: '',
  fontFamily: 'inter',

  afkAutoMute: false,
  afkTimeoutMinutes: 10,

  callJoinLeaveSound: 'chime',
  ringtoneId: 'classic',
  ringtoneVolume: 70,

  callAudioBitrate: 'auto',
  screenshareBitrate: 'auto',
  screenshareFramerate: 'auto',
  soundboardEnabled: true,
  soundboardVolume: 80,

  seasonalThemeOverride: 'auto',

  translateTargetLang: 'en',
  autoTranslate: false,
};

export const getSettings = (): Settings => {
  try {
    const settings = localStorage.getItem(STORAGE_KEY);
    if (settings === null) return defaultSettings;
    const saved = JSON.parse(settings) as Partial<Settings>;
    return {
      ...defaultSettings,
      ...saved,
      // Migrate legacy boolean callNoiseSuppression -> 3-way mode:
      // true => browser-native, false => off. New string values pass through.
      callNoiseSuppression:
        typeof saved.callNoiseSuppression === 'boolean'
          ? saved.callNoiseSuppression
            ? 'browser'
            : 'off'
          : (saved.callNoiseSuppression ?? defaultSettings.callNoiseSuppression),
      // Coerce any retired/unknown persisted model back to the default working
      // model; only whitelisted ids pass through.
      callDenoiseModel:
        saved.callDenoiseModel === 'rnnoise' ||
        saved.callDenoiseModel === 'speex' ||
        saved.callDenoiseModel === 'dtln' ||
        saved.callDenoiseModel === 'deepfilternet'
          ? saved.callDenoiseModel
          : defaultSettings.callDenoiseModel,
      // Coerce any unknown persisted ringtone id back to the default.
      ringtoneId:
        saved.ringtoneId === 'classic' ||
        saved.ringtoneId === 'chime' ||
        saved.ringtoneId === 'soft' ||
        saved.ringtoneId === 'retro' ||
        saved.ringtoneId === 'none'
          ? saved.ringtoneId
          : defaultSettings.ringtoneId,
      // Coerce persisted target language to a curated, supported code; anything
      // else (missing/wrong type/unknown) falls back to the default so the hook
      // never targets a language the engine can't produce.
      translateTargetLang:
        typeof saved.translateTargetLang === 'string' &&
        isSupportedTargetLang(saved.translateTargetLang)
          ? saved.translateTargetLang
          : defaultSettings.translateTargetLang,
      autoTranslate:
        typeof saved.autoTranslate === 'boolean'
          ? saved.autoTranslate
          : defaultSettings.autoTranslate,
      composerToolbarButtons: {
        ...DEFAULT_COMPOSER_TOOLBAR,
        ...(saved.composerToolbarButtons ?? {}),
        order: normalizeComposerToolbarOrder(saved.composerToolbarButtons?.order),
      },
    };
  } catch {
    // We may be here precisely because localStorage access throws (blocked
    // storage / private mode / sandboxed context). Removing the key must not be
    // allowed to re-throw — getSettings() runs at module load, so an uncaught
    // error here would crash the whole app on startup.
    try {
      localStorage.removeItem(STORAGE_KEY);
    } catch {
      /* localStorage unavailable — nothing to clean up */
    }
    return defaultSettings;
  }
};

export const setSettings = (settings: Settings) => {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
  } catch {
    /* quota */
  }
};

const baseSettings = atom<Settings>(getSettings());
export const settingsAtom = atom<Settings, [Settings], undefined>(
  (get) => get(baseSettings),
  (get, set, update) => {
    set(baseSettings, update);
    setSettings(update);
  },
);
