import { useSyncExternalStore } from 'react';
import { normalizeCodexModel } from './codexModelOptions';
import { appendClientIdHeader } from './clientIdentity';
import { getAutomationNotificationPreferenceTarget } from './notificationIdentity';
import { isPreviewRuntime } from './previewRuntime';

export const APP_CONFIG_STORAGE_KEY = 'work-server.app-config';
const APP_CONFIG_EVENT = 'work-server:app-config';
const APP_CONFIG_API_PATH = '/app-config';
const AUTOMATION_NOTIFICATION_PREFERENCE_API_PATH = '/notifications/preferences/automation';
const APP_CONFIG_REQUEST_TIMEOUT_MS = 8000;
let cachedConfig: AppConfig | null = null;
let cachedRawConfig: string | null = null;

export type AutomationScheduleType = 'interval' | 'daily' | 'weekly';
export type WeeklyScheduleDay = 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun';
export type CodexModelScope = 'all' | 'room';
export type PlanCostTimeUnit = 'hour' | 'minute' | 'second';

export type AppConfig = {
  chat: {
    defaultModelScope: CodexModelScope;
    defaultModelGlobal: string;
    defaultModelByRoom: Record<string, string>;
    maxContextMessages: number;
    maxContextChars: number;
    codexLiveMaxExecutionSeconds: number;
    codexLiveIdleTimeoutSeconds: number;
    receiveRoomNotifications: boolean;
    guidePwaInstallForNotifications: boolean;
    guideWebPushPermission: boolean;
    guideWebPushRegistration: boolean;
    restartReservationCompletionDelaySeconds: number;
  };
  automation: {
    autoRefreshEnabled: boolean;
    autoRefreshIntervalSeconds: number;
    autoReceiveScheduleType: AutomationScheduleType;
    autoReceiveIntervalSeconds: number;
    autoReceiveDailyTime: string;
    autoReceiveWeeklyDay: WeeklyScheduleDay;
    autoReceiveWeeklyTime: string;
    notifyOnAutomationStart: boolean;
    notifyOnAutomationProgress: boolean;
    notifyOnAutomationCompletion: boolean;
    notifyOnAutomationRelease: boolean;
    notifyOnAutomationMain: boolean;
    notifyOnAutomationFailure: boolean;
    notifyOnAutomationRestart: boolean;
    notifyOnAutomationIssueResolved: boolean;
  };
  worklogAutomation: {
    autoCreateDailyWorklog: boolean;
    dailyCreateTime: string;
    repeatRequestEnabled: boolean;
    repeatIntervalMinutes: number;
    includeScreenshots: boolean;
    includeChangedFiles: boolean;
    includeCommandLogs: boolean;
    template: 'simple' | 'detailed';
  };
  planDefaults: {
    jangsingProcessingRequired: boolean;
    autoDeployToMain: boolean;
    openEditorAfterCreate: boolean;
  };
  planCost: {
    baseCostPerMillionTokens: number;
    retryCostMultiplierPercent: number;
    hourlyCostMultiplierPercent: number;
    timeCostUnit: PlanCostTimeUnit;
    attentionCostThresholdMultiplier: number;
    warningCostThresholdMultiplier: number;
    highCostThresholdMultiplier: number;
  };
  gestureShortcuts: {
    openSearch: string;
    openWindowSearch: string;
  };
};

export const DEFAULT_APP_CONFIG: AppConfig = {
  chat: {
    defaultModelScope: 'all',
    defaultModelGlobal: 'gpt-5.4',
    defaultModelByRoom: {},
    maxContextMessages: 12,
    maxContextChars: 3200,
    codexLiveMaxExecutionSeconds: 600,
    codexLiveIdleTimeoutSeconds: 180,
    receiveRoomNotifications: true,
    guidePwaInstallForNotifications: true,
    guideWebPushPermission: true,
    guideWebPushRegistration: true,
    restartReservationCompletionDelaySeconds: 10,
  },
  automation: {
    autoRefreshEnabled: true,
    autoRefreshIntervalSeconds: 5,
    autoReceiveScheduleType: 'interval',
    autoReceiveIntervalSeconds: 30,
    autoReceiveDailyTime: '09:00',
    autoReceiveWeeklyDay: 'mon',
    autoReceiveWeeklyTime: '09:00',
    notifyOnAutomationStart: true,
    notifyOnAutomationProgress: true,
    notifyOnAutomationCompletion: true,
    notifyOnAutomationRelease: true,
    notifyOnAutomationMain: true,
    notifyOnAutomationFailure: true,
    notifyOnAutomationRestart: true,
    notifyOnAutomationIssueResolved: true,
  },
  worklogAutomation: {
    autoCreateDailyWorklog: false,
    dailyCreateTime: '18:00',
    repeatRequestEnabled: false,
    repeatIntervalMinutes: 60,
    includeScreenshots: true,
    includeChangedFiles: true,
    includeCommandLogs: true,
    template: 'detailed',
  },
  planDefaults: {
    jangsingProcessingRequired: true,
    autoDeployToMain: true,
    openEditorAfterCreate: true,
  },
  planCost: {
    baseCostPerMillionTokens: 10000,
    retryCostMultiplierPercent: 15,
    hourlyCostMultiplierPercent: 0,
    timeCostUnit: 'hour',
    attentionCostThresholdMultiplier: 1,
    warningCostThresholdMultiplier: 2,
    highCostThresholdMultiplier: 4,
  },
  gestureShortcuts: {
    openSearch: 'Mod+K',
    openWindowSearch: 'Mod+Shift+K',
  },
};

const WEEKLY_DAY_LABELS: Record<WeeklyScheduleDay, string> = {
  mon: '월요일',
  tue: '화요일',
  wed: '수요일',
  thu: '목요일',
  fri: '금요일',
  sat: '토요일',
  sun: '일요일',
};

const AUTOMATION_NOTIFICATION_KEYS = [
  'notifyOnAutomationStart',
  'notifyOnAutomationProgress',
  'notifyOnAutomationCompletion',
  'notifyOnAutomationRelease',
  'notifyOnAutomationMain',
  'notifyOnAutomationFailure',
  'notifyOnAutomationRestart',
  'notifyOnAutomationIssueResolved',
] as const;

type AutomationNotificationSettings = Pick<AppConfig['automation'], (typeof AUTOMATION_NOTIFICATION_KEYS)[number]>;

function clampIntervalSeconds(value: number, fallback: number) {
  if (!Number.isFinite(value)) {
    return fallback;
  }

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

function normalizeTimeValue(value: string, fallback: string) {
  if (/^\d{2}:\d{2}$/.test(value)) {
    return value;
  }

  return fallback;
}

function normalizeScheduleType(value: string | undefined): AutomationScheduleType {
  if (value === 'daily' || value === 'weekly') {
    return value;
  }

  return 'interval';
}

function normalizeWeeklyDay(value: string | undefined): WeeklyScheduleDay {
  if (value && value in WEEKLY_DAY_LABELS) {
    return value as WeeklyScheduleDay;
  }

  return DEFAULT_APP_CONFIG.automation.autoReceiveWeeklyDay;
}

function normalizeWorklogTemplate(value: string | undefined): AppConfig['worklogAutomation']['template'] {
  if (value === 'simple') {
    return 'simple';
  }

  return 'detailed';
}

function normalizeShortcutValue(value: string | undefined, fallback: string) {
  const trimmed = value?.trim();

  if (!trimmed) {
    return fallback;
  }

  return trimmed
    .split('+')
    .map((token) => token.trim())
    .filter(Boolean)
    .join('+');
}

function normalizePlanCostValue(value: number | undefined, fallback: number) {
  if (value === undefined || !Number.isFinite(value)) {
    return fallback;
  }

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

function normalizePlanCostMultiplierValue(value: number | undefined, fallback: number) {
  if (value === undefined || !Number.isFinite(value)) {
    return fallback;
  }

  return Math.min(100, Math.max(0.1, Math.round(value * 10) / 10));
}

function normalizePlanCostPercentValue(value: number | undefined, fallback: number) {
  if (value === undefined || !Number.isFinite(value)) {
    return fallback;
  }

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

function normalizePlanCostTimeUnit(value: string | undefined): PlanCostTimeUnit {
  if (value === 'minute' || value === 'second') {
    return value;
  }

  return 'hour';
}

function normalizeChatContextMessageLimit(value: number | undefined, fallback: number) {
  if (value === undefined || !Number.isFinite(value)) {
    return fallback;
  }

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

function normalizeChatContextCharLimit(value: number | undefined, fallback: number) {
  if (value === undefined || !Number.isFinite(value)) {
    return fallback;
  }

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

function normalizeCodexLiveMaxExecutionSeconds(value: number | undefined, fallback: number) {
  if (value === undefined || !Number.isFinite(value)) {
    return fallback;
  }

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

function normalizeCodexLiveIdleTimeoutSeconds(value: number | undefined, fallback: number) {
  if (value === undefined || !Number.isFinite(value)) {
    return fallback;
  }

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

function normalizeRestartReservationCompletionDelaySeconds(value: number | undefined, fallback: number) {
  if (value === undefined || !Number.isFinite(value)) {
    return fallback;
  }

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

function normalizeCodexModelScope(value: string | undefined): AppConfig['chat']['defaultModelScope'] {
  if (value === 'room') {
    return 'room';
  }

  return 'all';
}

function normalizeChatDefaultModelByRoom(value: unknown): Record<string, string> {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return {};
  }

  return Object.entries(value).reduce<Record<string, string>>((accumulator, [sessionId, modelValue]) => {
    const normalizedSessionId = sessionId.trim();
    const modelText = typeof modelValue === 'string' ? modelValue : '';

    if (!normalizedSessionId || !modelText.trim()) {
      return accumulator;
    }

    accumulator[normalizedSessionId] = normalizeCodexModel(modelText);
    return accumulator;
  }, {});
}

function normalizeBooleanValue(value: boolean | undefined, fallback: boolean) {
  if (typeof value !== 'boolean') {
    return fallback;
  }

  return value;
}

function normalizeConfig(raw?: Partial<AppConfig>): AppConfig {
  const chat = raw?.chat;
  const automation = raw?.automation;
  const worklogAutomation = raw?.worklogAutomation;
  const planDefaults = raw?.planDefaults;
  const planCost = raw?.planCost;
  const gestureShortcuts = raw?.gestureShortcuts;

  return {
    chat: {
      defaultModelScope: normalizeCodexModelScope(chat?.defaultModelScope),
      defaultModelGlobal: normalizeCodexModel(chat?.defaultModelGlobal ?? DEFAULT_APP_CONFIG.chat.defaultModelGlobal),
      defaultModelByRoom: normalizeChatDefaultModelByRoom(chat?.defaultModelByRoom),
      maxContextMessages: normalizeChatContextMessageLimit(
        chat?.maxContextMessages,
        DEFAULT_APP_CONFIG.chat.maxContextMessages,
      ),
      maxContextChars: normalizeChatContextCharLimit(chat?.maxContextChars, DEFAULT_APP_CONFIG.chat.maxContextChars),
      codexLiveMaxExecutionSeconds: normalizeCodexLiveMaxExecutionSeconds(
        chat?.codexLiveMaxExecutionSeconds,
        DEFAULT_APP_CONFIG.chat.codexLiveMaxExecutionSeconds,
      ),
      codexLiveIdleTimeoutSeconds: normalizeCodexLiveIdleTimeoutSeconds(
        chat?.codexLiveIdleTimeoutSeconds,
        DEFAULT_APP_CONFIG.chat.codexLiveIdleTimeoutSeconds,
      ),
      receiveRoomNotifications: normalizeBooleanValue(
        chat?.receiveRoomNotifications,
        DEFAULT_APP_CONFIG.chat.receiveRoomNotifications,
      ),
      guidePwaInstallForNotifications: normalizeBooleanValue(
        chat?.guidePwaInstallForNotifications,
        DEFAULT_APP_CONFIG.chat.guidePwaInstallForNotifications,
      ),
      guideWebPushPermission: normalizeBooleanValue(
        chat?.guideWebPushPermission,
        DEFAULT_APP_CONFIG.chat.guideWebPushPermission,
      ),
      guideWebPushRegistration: normalizeBooleanValue(
        chat?.guideWebPushRegistration,
        DEFAULT_APP_CONFIG.chat.guideWebPushRegistration,
      ),
      restartReservationCompletionDelaySeconds: normalizeRestartReservationCompletionDelaySeconds(
        chat?.restartReservationCompletionDelaySeconds,
        DEFAULT_APP_CONFIG.chat.restartReservationCompletionDelaySeconds,
      ),
    },
    automation: {
      autoRefreshEnabled: automation?.autoRefreshEnabled ?? DEFAULT_APP_CONFIG.automation.autoRefreshEnabled,
      autoRefreshIntervalSeconds: clampIntervalSeconds(
        automation?.autoRefreshIntervalSeconds ?? DEFAULT_APP_CONFIG.automation.autoRefreshIntervalSeconds,
        DEFAULT_APP_CONFIG.automation.autoRefreshIntervalSeconds,
      ),
      autoReceiveScheduleType: normalizeScheduleType(automation?.autoReceiveScheduleType),
      autoReceiveIntervalSeconds: clampIntervalSeconds(
        automation?.autoReceiveIntervalSeconds ?? DEFAULT_APP_CONFIG.automation.autoReceiveIntervalSeconds,
        DEFAULT_APP_CONFIG.automation.autoReceiveIntervalSeconds,
      ),
      autoReceiveDailyTime: normalizeTimeValue(
        automation?.autoReceiveDailyTime ?? DEFAULT_APP_CONFIG.automation.autoReceiveDailyTime,
        DEFAULT_APP_CONFIG.automation.autoReceiveDailyTime,
      ),
      autoReceiveWeeklyDay: normalizeWeeklyDay(automation?.autoReceiveWeeklyDay),
      autoReceiveWeeklyTime: normalizeTimeValue(
        automation?.autoReceiveWeeklyTime ?? DEFAULT_APP_CONFIG.automation.autoReceiveWeeklyTime,
        DEFAULT_APP_CONFIG.automation.autoReceiveWeeklyTime,
      ),
      notifyOnAutomationStart: automation?.notifyOnAutomationStart ?? DEFAULT_APP_CONFIG.automation.notifyOnAutomationStart,
      notifyOnAutomationProgress:
        automation?.notifyOnAutomationProgress ?? DEFAULT_APP_CONFIG.automation.notifyOnAutomationProgress,
      notifyOnAutomationCompletion:
        automation?.notifyOnAutomationCompletion ?? DEFAULT_APP_CONFIG.automation.notifyOnAutomationCompletion,
      notifyOnAutomationRelease:
        automation?.notifyOnAutomationRelease ?? DEFAULT_APP_CONFIG.automation.notifyOnAutomationRelease,
      notifyOnAutomationMain: automation?.notifyOnAutomationMain ?? DEFAULT_APP_CONFIG.automation.notifyOnAutomationMain,
      notifyOnAutomationFailure:
        automation?.notifyOnAutomationFailure ?? DEFAULT_APP_CONFIG.automation.notifyOnAutomationFailure,
      notifyOnAutomationRestart:
        automation?.notifyOnAutomationRestart ?? DEFAULT_APP_CONFIG.automation.notifyOnAutomationRestart,
      notifyOnAutomationIssueResolved:
        automation?.notifyOnAutomationIssueResolved ?? DEFAULT_APP_CONFIG.automation.notifyOnAutomationIssueResolved,
    },
    worklogAutomation: {
      autoCreateDailyWorklog:
        worklogAutomation?.autoCreateDailyWorklog ?? DEFAULT_APP_CONFIG.worklogAutomation.autoCreateDailyWorklog,
      dailyCreateTime: normalizeTimeValue(
        worklogAutomation?.dailyCreateTime ?? DEFAULT_APP_CONFIG.worklogAutomation.dailyCreateTime,
        DEFAULT_APP_CONFIG.worklogAutomation.dailyCreateTime,
      ),
      repeatRequestEnabled: false,
      repeatIntervalMinutes: DEFAULT_APP_CONFIG.worklogAutomation.repeatIntervalMinutes,
      includeScreenshots:
        worklogAutomation?.includeScreenshots ?? DEFAULT_APP_CONFIG.worklogAutomation.includeScreenshots,
      includeChangedFiles:
        worklogAutomation?.includeChangedFiles ?? DEFAULT_APP_CONFIG.worklogAutomation.includeChangedFiles,
      includeCommandLogs:
        worklogAutomation?.includeCommandLogs ?? DEFAULT_APP_CONFIG.worklogAutomation.includeCommandLogs,
      template: normalizeWorklogTemplate(worklogAutomation?.template),
    },
    planDefaults: {
      jangsingProcessingRequired:
        planDefaults?.jangsingProcessingRequired ?? DEFAULT_APP_CONFIG.planDefaults.jangsingProcessingRequired,
      autoDeployToMain: planDefaults?.autoDeployToMain ?? DEFAULT_APP_CONFIG.planDefaults.autoDeployToMain,
      openEditorAfterCreate: planDefaults?.openEditorAfterCreate ?? DEFAULT_APP_CONFIG.planDefaults.openEditorAfterCreate,
    },
    planCost: {
      baseCostPerMillionTokens: normalizePlanCostValue(
        planCost?.baseCostPerMillionTokens,
        DEFAULT_APP_CONFIG.planCost.baseCostPerMillionTokens,
      ),
      retryCostMultiplierPercent: normalizePlanCostPercentValue(
        planCost?.retryCostMultiplierPercent,
        DEFAULT_APP_CONFIG.planCost.retryCostMultiplierPercent,
      ),
      hourlyCostMultiplierPercent: normalizePlanCostPercentValue(
        planCost?.hourlyCostMultiplierPercent,
        DEFAULT_APP_CONFIG.planCost.hourlyCostMultiplierPercent,
      ),
      timeCostUnit: normalizePlanCostTimeUnit(planCost?.timeCostUnit),
      attentionCostThresholdMultiplier: normalizePlanCostMultiplierValue(
        planCost?.attentionCostThresholdMultiplier,
        DEFAULT_APP_CONFIG.planCost.attentionCostThresholdMultiplier,
      ),
      warningCostThresholdMultiplier: normalizePlanCostMultiplierValue(
        planCost?.warningCostThresholdMultiplier,
        DEFAULT_APP_CONFIG.planCost.warningCostThresholdMultiplier,
      ),
      highCostThresholdMultiplier: normalizePlanCostMultiplierValue(
        planCost?.highCostThresholdMultiplier,
        DEFAULT_APP_CONFIG.planCost.highCostThresholdMultiplier,
      ),
    },
    gestureShortcuts: {
      openSearch: normalizeShortcutValue(
        gestureShortcuts?.openSearch,
        DEFAULT_APP_CONFIG.gestureShortcuts.openSearch,
      ),
      openWindowSearch: normalizeShortcutValue(
        gestureShortcuts?.openWindowSearch,
        DEFAULT_APP_CONFIG.gestureShortcuts.openWindowSearch,
      ),
    },
  };
}

function pickAutomationNotificationSettings(automation: AppConfig['automation']): AutomationNotificationSettings {
  return AUTOMATION_NOTIFICATION_KEYS.reduce((picked, key) => {
    picked[key] = automation[key];
    return picked;
  }, {} as AutomationNotificationSettings);
}

function mergeAutomationNotificationSettings(
  config: AppConfig,
  automation?: Partial<AutomationNotificationSettings> | null,
) {
  if (!automation) {
    return config;
  }

  return normalizeConfig({
    ...config,
    automation: {
      ...config.automation,
      ...automation,
    },
  });
}

function getCurrentAppOrigin() {
  if (typeof window === 'undefined') {
    return '';
  }

  return window.location.origin;
}

function getCurrentAppDomain() {
  if (typeof window === 'undefined') {
    return '';
  }

  return window.location.hostname;
}

function emitConfigChange() {
  if (typeof window === 'undefined') {
    return;
  }

  window.dispatchEvent(new Event(APP_CONFIG_EVENT));
}

function resolveAppConfigApiBaseUrl() {
  if (import.meta.env.VITE_WORK_SERVER_URL) {
    return import.meta.env.VITE_WORK_SERVER_URL;
  }

  return '/api';
}

function resolveAppConfigFallbackBaseUrl() {
  if (typeof window === 'undefined') {
    return null;
  }

  const hostname = window.location.hostname;
  const isLocalWorkServerHost =
    hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '0.0.0.0';

  if (!isLocalWorkServerHost) {
    return null;
  }

  const fallbackUrl = new URL(window.location.origin);
  fallbackUrl.port = '3100';
  fallbackUrl.pathname = '/api';
  fallbackUrl.search = '';
  fallbackUrl.hash = '';

  return fallbackUrl.toString().replace(/\/+$/, '');
}

const APP_CONFIG_API_BASE_URL = resolveAppConfigApiBaseUrl();
const APP_CONFIG_FALLBACK_BASE_URL = resolveAppConfigFallbackBaseUrl();

type AppConfigRequestOptions = {
  shareToken?: string | null;
  skipAutomationNotifications?: boolean;
};

class AppConfigApiError extends Error {
  status: number;

  constructor(message: string, status: number) {
    super(message);
    this.name = 'AppConfigApiError';
    this.status = status;
  }
}

async function requestAppConfigOnce<T>(
  baseUrl: string,
  path: string,
  init?: RequestInit,
  options?: AppConfigRequestOptions,
): Promise<T> {
  const headers = appendClientIdHeader(init?.headers);
  const hasBody = init?.body !== undefined && init.body !== null;
  const method = init?.method?.toUpperCase() ?? 'GET';
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), APP_CONFIG_REQUEST_TIMEOUT_MS);
  const shareToken = options?.shareToken?.trim() ?? '';

  if (hasBody && !headers.has('Content-Type')) {
    headers.set('Content-Type', 'application/json');
  }

  const appOrigin = getCurrentAppOrigin();
  const appDomain = getCurrentAppDomain();

  if (appOrigin && !headers.has('X-App-Origin')) {
    headers.set('X-App-Origin', appOrigin);
  }

  if (appDomain && !headers.has('X-App-Domain')) {
    headers.set('X-App-Domain', appDomain);
  }

  if (shareToken && !headers.has('X-Chat-Share-Token')) {
    headers.set('X-Chat-Share-Token', shareToken);
  }

  let response: Response;

  try {
    response = await fetch(`${baseUrl}${path}`, {
      ...init,
      headers,
      signal: controller.signal,
      cache: init?.cache ?? (method === 'GET' ? 'no-store' : undefined),
    });
  } catch (error) {
    clearTimeout(timeoutId);
    if (error instanceof DOMException && error.name === 'AbortError') {
      throw new AppConfigApiError('서버 응답이 지연됩니다.', 408);
    }
    throw error;
  }

  clearTimeout(timeoutId);

  if (!response.ok) {
    const text = await response.text();

    try {
      const payload = JSON.parse(text) as { message?: string };
      throw new AppConfigApiError(payload.message || '요청 처리에 실패했습니다.', response.status);
    } catch {
      throw new AppConfigApiError(text || '요청 처리에 실패했습니다.', response.status);
    }
  }

  const contentType = response.headers.get('content-type') ?? '';
  if (!contentType.toLowerCase().includes('application/json')) {
    const text = await response.text();
    throw new AppConfigApiError(text ? '서버 응답이 JSON이 아닙니다.' : '서버 응답을 확인할 수 없습니다.', 502);
  }

  return response.json() as Promise<T>;
}

async function requestAppConfig<T>(path: string, init?: RequestInit, options?: AppConfigRequestOptions): Promise<T> {
  try {
    return await requestAppConfigOnce<T>(APP_CONFIG_API_BASE_URL, path, init, options);
  } catch (error) {
    const shouldRetryWithFallback =
      APP_CONFIG_FALLBACK_BASE_URL &&
      APP_CONFIG_FALLBACK_BASE_URL !== APP_CONFIG_API_BASE_URL &&
      (error instanceof AppConfigApiError
        ? error.status === 404 || error.status === 408 || error.status === 502
        : error instanceof Error && /404|not found|Failed to fetch|Load failed|NetworkError/i.test(error.message));

    if (!shouldRetryWithFallback) {
      throw error;
    }

    return requestAppConfigOnce<T>(APP_CONFIG_FALLBACK_BASE_URL, path, init, options);
  }
}

export async function fetchAppConfigFromServer(options?: AppConfigRequestOptions) {
  try {
    const response = await requestAppConfig<{ ok: boolean; config: Partial<AppConfig> }>(APP_CONFIG_API_PATH, undefined, options);
    const config = normalizeConfig(response.config);
    const preference = options?.skipAutomationNotifications ? null : await fetchAutomationNotificationPreferenceFromServer();
    return mergeAutomationNotificationSettings(config, preference);
  } catch {
    return null;
  }
}

async function fetchAutomationNotificationPreferenceFromServer() {
  try {
    const target = getAutomationNotificationPreferenceTarget();
    const query = target ? `?${new URLSearchParams(target).toString()}` : '';
    const response = await requestAppConfig<{
      ok: boolean;
      automation?: Partial<AutomationNotificationSettings> | null;
    }>(`${AUTOMATION_NOTIFICATION_PREFERENCE_API_PATH}${query}`);
    return response.automation ?? null;
  } catch {
    return null;
  }
}

export async function saveAppConfigToServer(config: AppConfig, options?: AppConfigRequestOptions) {
  const response = await requestAppConfig<{ ok: boolean; config: Partial<AppConfig> }>(APP_CONFIG_API_PATH, {
    method: 'PUT',
    body: JSON.stringify({ config }),
  }, options);

  return normalizeConfig(response.config);
}

export async function saveAutomationNotificationPreferenceToServer(config: AppConfig) {
  const target = getAutomationNotificationPreferenceTarget();
  const response = await requestAppConfig<{
    ok: boolean;
    automation: Partial<AutomationNotificationSettings>;
  }>(AUTOMATION_NOTIFICATION_PREFERENCE_API_PATH, {
    method: 'PUT',
    body: JSON.stringify({
      ...(target ?? {}),
      automation: pickAutomationNotificationSettings(config.automation),
    }),
  });

  return mergeAutomationNotificationSettings(config, response.automation);
}

export async function syncAppConfigFromServer(options?: AppConfigRequestOptions) {
  const config = await fetchAppConfigFromServer(options);

  if (!config) {
    return false;
  }

  setStoredAppConfig(config);
  return true;
}

export function getStoredAppConfig(): AppConfig {
  if (typeof window === 'undefined') {
    return DEFAULT_APP_CONFIG;
  }

  try {
    const storage = isPreviewRuntime() ? window.sessionStorage : window.localStorage;
    const raw = storage.getItem(APP_CONFIG_STORAGE_KEY);

    if (!raw) {
      cachedConfig = DEFAULT_APP_CONFIG;
      cachedRawConfig = null;
      return DEFAULT_APP_CONFIG;
    }

    if (raw === cachedRawConfig && cachedConfig) {
      return cachedConfig;
    }

    const normalized = normalizeConfig(JSON.parse(raw) as Partial<AppConfig>);
    cachedConfig = normalized;
    cachedRawConfig = raw;
    return normalized;
  } catch {
    cachedConfig = DEFAULT_APP_CONFIG;
    cachedRawConfig = null;
    return DEFAULT_APP_CONFIG;
  }
}

export function setStoredAppConfig(config: AppConfig) {
  if (typeof window === 'undefined') {
    return;
  }

  const normalized = normalizeConfig(config);
  const raw = JSON.stringify(normalized);
  cachedConfig = normalized;
  cachedRawConfig = raw;
  try {
    const storage = isPreviewRuntime() ? window.sessionStorage : window.localStorage;
    storage.setItem(APP_CONFIG_STORAGE_KEY, raw);
  } catch {
    // Ignore storage failures and keep the in-memory config for the current session.
  }
  emitConfigChange();
}

export function updateStoredAppConfig(updater: (current: AppConfig) => AppConfig) {
  const current = getStoredAppConfig();
  const next = updater(current);
  setStoredAppConfig(next);
}

function subscribeToAppConfig(callback: () => void) {
  if (typeof window === 'undefined') {
    return () => undefined;
  }

  const handleChange = () => {
    callback();
  };

  window.addEventListener(APP_CONFIG_EVENT, handleChange);
  window.addEventListener('storage', handleChange);

  return () => {
    window.removeEventListener(APP_CONFIG_EVENT, handleChange);
    window.removeEventListener('storage', handleChange);
  };
}

export function useAppConfig() {
  return useSyncExternalStore(subscribeToAppConfig, getStoredAppConfig, () => DEFAULT_APP_CONFIG);
}

export function describeAutoReceiveSchedule(config: AppConfig) {
  const automation = config.automation;

  if (automation.autoReceiveScheduleType === 'daily') {
    return `매일 ${automation.autoReceiveDailyTime}`;
  }

  if (automation.autoReceiveScheduleType === 'weekly') {
    return `매주 ${WEEKLY_DAY_LABELS[automation.autoReceiveWeeklyDay]} ${automation.autoReceiveWeeklyTime}`;
  }

  return `${automation.autoReceiveIntervalSeconds}초마다`;
}

export function getWeeklyScheduleOptions() {
  return Object.entries(WEEKLY_DAY_LABELS).map(([value, label]) => ({
    value: value as WeeklyScheduleDay,
    label,
  }));
}
