import { db } from '../db/client.js';
import {
  PLAN_CHECKLIST_DEFAULT_CONTEXT_CONTENT,
  PLAN_CHECKLIST_DEFAULT_CONTEXT_ID,
  PLAN_CHECKLIST_DEFAULT_CONTEXT_TITLE,
} from './chat-type-defaults.js';

export const APP_CONFIG_TABLE = 'app_configs';
const CHAT_TYPES_CONFIG_KEY = 'chatTypes';
const CHAT_CONTEXT_SETTINGS_CONFIG_KEY = 'chatContextSettings';
const SCOPED_APP_CONFIGS_KEY = 'scopedAppConfigs';
const SCOPED_CONTEXT_CONFIG_BACKUPS_KEY = 'scopedContextConfigBackups';
const SHARED_CHAT_CONTEXT_APP_ORIGIN = 'https://preview.sm-home.cloud';
const CODEX_LIVE_DEFAULT_CHAT_TYPE_ID = 'codex-live-default';
const CODEX_LIVE_DEFAULT_CHAT_TYPE_NAME = '기본처리';
const CODEX_LIVE_DEFAULT_CHAT_TYPE_DESCRIPTION =
  '## 기본 처리\n- 실제 수정 범위와 방식은 채팅 유형 context와 AGENTS.md 규칙을 최우선으로 따릅니다.\n- 최근 대화 내용과 현재 요청은 채팅 유형 context를 해치지 않는 범위에서만 보조 문맥으로 참조합니다.\n- 실패 시에는 현재 세션에서 수정한 소스만 범위를 좁혀 되돌립니다.\n\n## 문맥과 리소스\n- 채팅 유형 정보는 대화방 내용보다 먼저 읽고 적용합니다.\n- 산출물과 첨부 리소스는 `public/.codex_chat/<chat-session-id>/resource/` 기준으로 제공합니다.\n- `[[link-card:...]]`는 외부 공개 링크에만 사용하고, 내부 리소스(`/api/chat/resources/...`, `/.codex_chat/...`)에는 사용하지 않습니다.\n- 내부 문서, 로그, 코드, 테이블 파일은 일반 리소스 URL이나 preview 컴포넌트로 바로 열 수 있는 형식으로 제공합니다.\n- prompt 옵션의 preview에 HTML 문서를 붙일 때는 현재 앱 주소나 `/chat/...` 같은 화면 URL을 넣지 말고, 기본값으로 `preview.type=\"resource\"`와 `/api/chat/resources/.../*.html` 형태의 실제 세션 리소스 URL을 사용합니다.\n- `preview.type=\"html\"`은 완성된 HTML 본문 자체를 `content`로 직접 넣을 때만 사용하고, URL만 전달할 때는 세션 HTML 리소스를 만든 뒤 `resource` preview로 연결합니다.\n\n## 검증\n- 사소한 UI 변경이 있더라도 기본값은 모바일 브라우저 환경에서 캡처하고 검증합니다.\n- 모바일 검증은 `ai-code-app-preview` 컨테이너 기준으로 진행합니다.\n- 외부 접속 확인, 화면 테스트, 최종 검증은 `https://preview.sm-home.cloud/` 기준으로 진행합니다.\n- `https://test.sm-home.cloud/`는 운영 비교나 프록시 점검이 꼭 필요할 때만 보조적으로 확인합니다.\n- 모바일 캡처는 루트 `.env`의 등록 토큰을 `localStorage`에 주입한 상태에서 진행하고 비로그인 기본 화면으로 확인하지 않습니다.\n- 채팅 결과물 중 모바일 캡처와 최종 화면 검증 결과는 링크 카드로 바꾸지 말고 반드시 `[[preview:URL]]` 형식으로 제공합니다.\n- preview 컨테이너만 필요시 재기동 후 검증합니다.\n- preview 외 다른 서버나 컨테이너는 종료하거나 재기동하지 않습니다.\n- 추가 재기동이나 후속 테스트가 필요하면 직접 처리하지 말고 앱알림으로 필요한 대상과 사유를 안내합니다.\n- 캡처 화면으로 요청사항 반영 여부를 다시 확인하고, 부족하면 추가 작업을 이어서 진행합니다.\n\n## 구현 기준\n- 활동로그 아래 표시되는 Plan 체크리스트는 일반 요청에서도 유지하고, 현재 요청 진행 단계에 맞춰 항목과 상태를 자연스럽게 반영합니다.\n- 소스는 덕지덕지 붙지 않게 정리하고, 분리할 부분은 분리합니다.\n- 이전 처리에서 불필요해진 부분은 제거해 clean code 상태를 유지합니다.\n- 답변 스타일과 기본 문맥은 반드시 채팅 유형 정보만 기준으로 적용하세요.\n- Plan 자동화용 자동화 유형 context가 있더라도 Codex Live에서는 별도 요청이 없는 한 참조하지 마세요.';
const CODEX_SUMMARY_CHAT_TYPE_ID = 'codex-summary';
const CODEX_SUMMARY_CHAT_TYPE_NAME = 'Codex 종합';
const CODEX_SUMMARY_LEGACY_DESCRIPTION =
  '## 처리 범위\n- Codex 봇들의 대화와 의견 수렴 과정을 거친 뒤 최종 결과물을 정리해 제공하는 채팅에 사용합니다.\n- Plan 자동화 문맥을 기본값으로 섞지 않고, 현재 채팅방 공통 문맥과 사용자 요청만 기준으로 결과를 정리합니다.\n\n## 응답 방식\n- 중간 대화에서 나온 핵심 의견, 판단 근거, 남은 쟁점을 압축해 정리합니다.\n- 최종 답변에는 필요한 산출물, 검증 결과, 적용 결론을 함께 제공합니다.';
const CODEX_SUMMARY_CHAT_TYPE_DESCRIPTION =
  '## 처리 범위\n- 회의 기록자 1명과 프리토킹 Codex들이 함께 논점을 정리한 뒤 최종 결과를 보고하는 채팅에 사용합니다.\n- 사용자는 최종 결과를 우선 확인하고, 필요할 때만 중간 대화 흐름을 다시 확인합니다.\n- Plan 자동화 문맥을 기본값으로 섞지 않고, 현재 채팅방 공통 문맥과 사용자 요청만 기준으로 결과를 정리합니다.\n\n## 응답 방식\n- 첫 Codex는 회의 기록자 겸 중재자로서 논점, 검증 기준, 확인 포인트를 정리합니다.\n- 이어지는 Codex들은 프리토킹으로 자유롭게 보완·반박·구현 의견을 제시합니다.\n- 마지막에는 회의 기록자가 최종 결론, 검증 결과, 남은 쟁점을 종합해 보고합니다.';
const CODEX_DISPATCHER_CHAT_TYPE_ID = 'codex-dispatcher-workers';
const CODEX_DISPATCHER_CHAT_TYPE_NAME = 'Codex 작업형';
const CODEX_DISPATCHER_CHAT_TYPE_DESCRIPTION =
  '## 처리 범위\n- 중계 지시자 1명과 실작업자 Codex들이 역할을 나눠 실제 작업을 진행하는 채팅에 사용합니다.\n- 필요하면 중계 지시자가 직접 최종 검토를 수행하고, 설정에 따라 별도 검토자를 지정할 수도 있습니다.\n- Plan 자동화 문맥을 기본값으로 섞지 않고, 현재 채팅방 공통 문맥과 사용자 요청만 기준으로 실행합니다.\n\n## 응답 방식\n- 첫 Codex는 중계 지시자로서 작업을 역할·기준·검증 축으로 분해하고 담당을 배분합니다.\n- 이어지는 Codex들은 실작업자로서 구현, 설계, 검증, 반례를 구체적으로 제시합니다.\n- 마지막에는 중계 지시자가 결과물, 검토 결과, 남은 리스크와 후속 액션을 종합 보고합니다.';
const DEFAULT_CHAT_APP_CONFIG = {
  maxContextMessages: 12,
  maxContextChars: 3200,
  codexLiveMaxExecutionSeconds: 600,
  codexLiveIdleTimeoutSeconds: 180,
  receiveRoomNotifications: true,
  restartReservationCompletionDelaySeconds: 10,
} as const;

type ChatPermissionRole = 'guest' | 'token-user';
export type ChatTypeExecutionMode = 'default' | 'summary-free-talking' | 'dispatcher-workers';
export type ChatTypeReviewPolicy = 'self' | 'reviewer';
export type ChatTypeResourceReportPolicy = 'none' | 'if-generated' | 'always';
export type ChatTypeParticipantBinding =
  | 'manual'
  | 'first-moderator-rest-conversation'
  | 'first-moderator-rest-conversation-last-reviewer';

export type ChatTypeExecutionPolicy = {
  mode: ChatTypeExecutionMode;
  participantBinding: ChatTypeParticipantBinding;
  reviewPolicy: ChatTypeReviewPolicy;
  resourceReportPolicy: ChatTypeResourceReportPolicy;
  allowModeratorIntervention: boolean;
  finalSummaryRequired: boolean;
};

type ChatTypeRecord = {
  id: string;
  name: string;
  sortOrder: number;
  description: string;
  executionPolicy: ChatTypeExecutionPolicy;
  permissions: ChatPermissionRole[];
  enabled: boolean;
  updatedAt: string;
};

const LEGACY_PLAN_CHECKLIST_CHAT_TYPE_ID = 'plan-checklist-execution';

export type ChatTypesConfigSnapshot = {
  chatTypes: ChatTypeRecord[];
};

type ChatDefaultContextRecord = {
  id: string;
  title: string;
  sortOrder: number;
  content: string;
  enabled: boolean;
  updatedAt: string;
};

type ChatTypeDefaultContextSelection = {
  chatTypeId: string;
  defaultContextIds: string[];
  updatedAt: string;
};

type ChatRoomCodexParticipant = {
  id: string;
  name: string;
  model: string;
  prompt: string;
  chatTypeId: string | null;
  defaultContextIds: string[];
  role: 'default' | 'moderator' | 'conversation' | 'reviewer';
};

type ChatRoomContextSettings = {
  sessionId: string;
  defaultContextIds: string[];
  customContextTitle: string;
  customContextContent: string;
  codexParticipants: ChatRoomCodexParticipant[];
  updatedAt: string;
};

export type ChatContextSettingsSnapshot = {
  defaultContexts: ChatDefaultContextRecord[];
  chatTypeDefaults: ChatTypeDefaultContextSelection[];
  roomContexts: ChatRoomContextSettings[];
};

type ScopedContextConfigBackupEntry = {
  sourceOrigin: string;
  appDomain: string | null;
  updatedAt: string;
  backedUpAt: string;
  chatTypes?: ChatTypeRecord[];
  chatContextSettings?: ChatContextSettingsSnapshot;
};

async function ensureAppConfigTable() {
  const hasTable = await db.schema.hasTable(APP_CONFIG_TABLE);

  if (!hasTable) {
    await db.schema.createTable(APP_CONFIG_TABLE, (table) => {
      table.increments('id').primary();
      table.jsonb('config_json').notNullable().defaultTo('{}');
      table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
    });
    return;
  }

  const requiredColumns: Array<[string, (table: any) => void]> = [
    ['config_json', (table) => table.jsonb('config_json').notNullable().defaultTo('{}')],
    ['updated_at', (table) => table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
  ];

  for (const [columnName, createColumn] of requiredColumns) {
    const hasColumn = await db.schema.hasColumn(APP_CONFIG_TABLE, columnName);
    if (!hasColumn) {
      await db.schema.alterTable(APP_CONFIG_TABLE, (table) => {
        createColumn(table);
      });
    }
  }
}

function normalizeAppOrigin(appOrigin?: string | null) {
  if (typeof appOrigin !== 'string') {
    return '';
  }

  try {
    const url = new URL(appOrigin.trim());
    return url.origin;
  } catch {
    return '';
  }
}

function normalizeAppDomain(appDomain?: string | null) {
  return typeof appDomain === 'string' ? appDomain.trim().toLowerCase() : '';
}

function deepMergeConfigRecords(
  base: Record<string, unknown>,
  override: Record<string, unknown>,
): Record<string, unknown> {
  const merged = { ...base };

  for (const [key, value] of Object.entries(override)) {
    const current = merged[key];
    const canMergeObject =
      value &&
      current &&
      typeof value === 'object' &&
      typeof current === 'object' &&
      !Array.isArray(value) &&
      !Array.isArray(current);

    merged[key] = canMergeObject
      ? deepMergeConfigRecords(
          normalizeConfigRecord(current),
          normalizeConfigRecord(value),
        )
      : value;
  }

  return merged;
}

function getBaseAppConfigRecord(value: unknown) {
  const normalized = normalizeConfigRecord(value);
  const nextConfig = { ...normalized };
  delete nextConfig[SCOPED_APP_CONFIGS_KEY];
  return nextConfig;
}

function getScopedAppConfigsRecord(value: unknown) {
  return normalizeConfigRecord(normalizeConfigRecord(value)[SCOPED_APP_CONFIGS_KEY]);
}

function getScopedAppConfigEntryRecord(value: unknown) {
  return normalizeConfigRecord(value);
}

function getScopedContextConfigBackupsRecord(value: unknown) {
  return normalizeConfigRecord(
    normalizeConfigRecord(value)[SCOPED_CONTEXT_CONFIG_BACKUPS_KEY],
  ) as Record<string, ScopedContextConfigBackupEntry>;
}

function resolveSharedChatContextAppOrigin() {
  return SHARED_CHAT_CONTEXT_APP_ORIGIN;
}

function hasChatContextSettingsSnapshot(value: ChatContextSettingsSnapshot) {
  return (
    value.defaultContexts.length > 0 ||
    value.chatTypeDefaults.length > 0 ||
    value.roomContexts.length > 0
  );
}

export function stripChatContextSettingsFromScopedAppConfigs(value: unknown) {
  const scopedConfigs = getScopedAppConfigsRecord(value);
  let changed = false;

  const sanitizedScopedConfigs = Object.fromEntries(
    Object.entries(scopedConfigs).map(([origin, entry]) => {
      const normalizedEntry = getScopedAppConfigEntryRecord(entry);
      const normalizedConfig = normalizeConfigRecord(normalizedEntry.config);

      if (!(CHAT_CONTEXT_SETTINGS_CONFIG_KEY in normalizedConfig)) {
        return [origin, normalizedEntry];
      }

      const { [CHAT_CONTEXT_SETTINGS_CONFIG_KEY]: _removed, ...nextConfig } = normalizedConfig;
      changed = true;

      return [
        origin,
        {
          ...normalizedEntry,
          config: nextConfig,
        },
      ];
    }),
  );

  return {
    changed,
    scopedConfigs: sanitizedScopedConfigs,
  };
}

function buildScopedContextConfigBackupEntry(
  origin: string,
  entry: Record<string, unknown>,
  chatTypes: ChatTypeRecord[],
  chatContextSettings: ChatContextSettingsSnapshot,
): ScopedContextConfigBackupEntry | null {
  const normalizedOrigin = normalizeAppOrigin(origin) || origin;
  const hasChatTypes = chatTypes.length > 0;
  const hasChatContextSettings = hasChatContextSettingsSnapshot(chatContextSettings);

  if (!normalizedOrigin || (!hasChatTypes && !hasChatContextSettings)) {
    return null;
  }

  return {
    sourceOrigin: normalizedOrigin,
    appDomain: normalizeAppDomain(typeof entry.appDomain === 'string' ? entry.appDomain : null) || null,
    updatedAt: normalizeText(entry.updatedAt) || new Date().toISOString(),
    backedUpAt: new Date().toISOString(),
    ...(hasChatTypes ? { chatTypes } : null),
    ...(hasChatContextSettings ? { chatContextSettings } : null),
  };
}

export function stripSharedContextDataFromScopedAppConfigs(
  value: unknown,
  canonicalAppOrigin = SHARED_CHAT_CONTEXT_APP_ORIGIN,
): {
  changed: boolean;
  scopedConfigs: Record<string, unknown>;
  backups: Record<string, ScopedContextConfigBackupEntry>;
} {
  const scopedConfigs = getScopedAppConfigsRecord(value);
  const existingBackups = getScopedContextConfigBackupsRecord(value);
  const normalizedCanonicalOrigin = normalizeAppOrigin(canonicalAppOrigin);
  let stripped = false;
  let backupChanged = false;

  const nextBackups = { ...existingBackups };
  const sanitizedScopedConfigs = Object.fromEntries(
    Object.entries(scopedConfigs).map(([origin, entry]) => {
      const normalizedEntry = getScopedAppConfigEntryRecord(entry);
      const normalizedConfig = normalizeConfigRecord(normalizedEntry.config);
      const scopedChatTypes = Array.isArray(normalizedConfig[CHAT_TYPES_CONFIG_KEY])
        ? sanitizeChatTypes(normalizedConfig[CHAT_TYPES_CONFIG_KEY] as unknown[])
        : [];
      const scopedChatContextSettings = sanitizeChatContextSettings(
        normalizedConfig[CHAT_CONTEXT_SETTINGS_CONFIG_KEY],
      );
      const isCanonicalOrigin =
        normalizedCanonicalOrigin && normalizeAppOrigin(origin) === normalizedCanonicalOrigin;

      if (!isCanonicalOrigin) {
        const backupEntry = buildScopedContextConfigBackupEntry(
          origin,
          normalizedEntry,
          scopedChatTypes,
          scopedChatContextSettings,
        );

        if (backupEntry) {
          nextBackups[origin] = backupEntry;
          backupChanged = true;
        }
      }

      if (
        CHAT_TYPES_CONFIG_KEY in normalizedConfig ||
        CHAT_CONTEXT_SETTINGS_CONFIG_KEY in normalizedConfig
      ) {
        stripped = true;
      }

      const {
        [CHAT_TYPES_CONFIG_KEY]: _removedChatTypes,
        [CHAT_CONTEXT_SETTINGS_CONFIG_KEY]: _removedChatContextSettings,
        ...nextConfig
      } = normalizedConfig;

      return [
        origin,
        {
          ...normalizedEntry,
          config: nextConfig,
        },
      ];
    }),
  );

  return {
    changed: stripped || backupChanged,
    scopedConfigs: sanitizedScopedConfigs,
    backups: nextBackups,
  };
}

export function resolveCanonicalChatContextSettingsFromConfig(value: unknown, appOrigin?: string | null) {
  const normalized = normalizeConfigRecord(value);
  const globalSettings = sanitizeChatContextSettings(normalized[CHAT_CONTEXT_SETTINGS_CONFIG_KEY]);

  if (hasChatContextSettingsSnapshot(globalSettings)) {
    return globalSettings;
  }

  const scopedSettings = sanitizeChatContextSettings(
    normalizeConfigRecord(resolveAppConfigByOrigin(normalized, appOrigin))[CHAT_CONTEXT_SETTINGS_CONFIG_KEY],
  );

  return hasChatContextSettingsSnapshot(scopedSettings) ? scopedSettings : globalSettings;
}

export function resolveCanonicalChatTypesFromConfig(value: unknown, appOrigin?: string | null) {
  const normalized = normalizeConfigRecord(value);
  const globalChatTypes = Array.isArray(normalized[CHAT_TYPES_CONFIG_KEY])
    ? sanitizeChatTypes(normalized[CHAT_TYPES_CONFIG_KEY])
    : [];
  const scopedConfig = resolveScopedAppConfig(normalized, appOrigin);
  const scopedChatTypes = Array.isArray(normalizeConfigRecord(scopedConfig)[CHAT_TYPES_CONFIG_KEY])
    ? sanitizeChatTypes(normalizeConfigRecord(scopedConfig)[CHAT_TYPES_CONFIG_KEY] as unknown[])
    : [];

  if (globalChatTypes.length === 0 && scopedChatTypes.length === 0) {
    return null;
  }

  return sanitizeChatTypes([...globalChatTypes, ...scopedChatTypes]);
}

function resolveScopedAppConfig(value: unknown, appOrigin?: string | null) {
  const normalizedAppOrigin = normalizeAppOrigin(appOrigin);

  if (!normalizedAppOrigin) {
    return null;
  }

  const scopedEntry = normalizeConfigRecord(getScopedAppConfigsRecord(value)[normalizedAppOrigin]);
  const scopedConfig = normalizeConfigRecord(scopedEntry.config);
  return Object.keys(scopedConfig).length > 0 ? scopedConfig : null;
}

function mergeScopedAppConfig(
  currentConfig: Record<string, unknown>,
  nextConfig: Record<string, unknown>,
  appOrigin?: string | null,
  appDomain?: string | null,
) {
  const normalizedAppOrigin = normalizeAppOrigin(appOrigin);

  if (!normalizedAppOrigin) {
    return deepMergeConfigRecords(currentConfig, nextConfig);
  }

  const baseConfig = getBaseAppConfigRecord(currentConfig);
  const scopedConfigs = getScopedAppConfigsRecord(currentConfig);
  const currentResolvedScopedConfig =
    resolveScopedAppConfig(currentConfig, normalizedAppOrigin) ?? baseConfig;
  const nextResolvedScopedConfig = deepMergeConfigRecords(currentResolvedScopedConfig, nextConfig);
  const normalizedAppDomain = normalizeAppDomain(appDomain);

  return {
    ...baseConfig,
    [SCOPED_APP_CONFIGS_KEY]: {
      ...scopedConfigs,
      [normalizedAppOrigin]: {
        config: nextResolvedScopedConfig,
        updatedAt: new Date().toISOString(),
        appDomain: normalizedAppDomain || null,
      },
    },
  };
}

export function resolveAppConfigByOrigin(value: unknown, appOrigin?: string | null) {
  const baseConfig = getBaseAppConfigRecord(value);
  const scopedConfig = resolveScopedAppConfig(value, appOrigin);

  if (!scopedConfig) {
    return baseConfig;
  }

  return deepMergeConfigRecords(baseConfig, scopedConfig);
}

export async function getAppConfig(appOrigin?: string | null) {
  await ensureAppConfigTable();

  const row = await db(APP_CONFIG_TABLE).first();

  if (!row) {
    return null;
  }

  if (typeof row.config_json === 'string') {
    try {
      return resolveAppConfigByOrigin(JSON.parse(row.config_json), appOrigin);
    } catch {
      return {};
    }
  }

  return resolveAppConfigByOrigin(row.config_json ?? {}, appOrigin);
}

async function getRawAppConfigRecord() {
  await ensureAppConfigTable();

  const row = await db(APP_CONFIG_TABLE).first();

  if (!row) {
    return {} as Record<string, unknown>;
  }

  if (typeof row.config_json === 'string') {
    try {
      return normalizeConfigRecord(JSON.parse(row.config_json));
    } catch {
      return {} as Record<string, unknown>;
    }
  }

  return normalizeConfigRecord(row.config_json);
}

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

  return value as Record<string, unknown>;
}

function normalizeIntegerInRange(value: unknown, fallback: number, min: number, max: number) {
  if (typeof value !== 'number' || !Number.isFinite(value)) {
    return fallback;
  }

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

function normalizeText(value: unknown) {
  return typeof value === 'string' ? value.trim() : '';
}

function normalizePermissions(value: unknown): ChatPermissionRole[] {
  if (!Array.isArray(value)) {
    return ['token-user'];
  }

  const permissions = Array.from(
    new Set(
      value.filter(
        (item): item is ChatPermissionRole => item === 'guest' || item === 'token-user',
      ),
    ),
  );

  return permissions.length > 0 ? permissions : ['token-user'];
}

function normalizeDefaultContextIds(value: unknown) {
  if (!Array.isArray(value)) {
    return [] as string[];
  }

  return Array.from(new Set(value.map((item) => normalizeText(item)).filter(Boolean)));
}

function normalizeDefaultContextRecord(value: unknown): ChatDefaultContextRecord | null {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return null;
  }

  const record = value as Partial<ChatDefaultContextRecord>;
  const title = normalizeText(record.title);
  const content = normalizeText(record.content);

  if (!title && !content) {
    return null;
  }

  return {
    id: normalizeText(record.id) || `chat-default-context-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
    title: title || '공통 문맥',
    sortOrder: normalizePositiveSortOrder(record.sortOrder),
    content,
    enabled: record.enabled !== false,
    updatedAt: normalizeText(record.updatedAt) || new Date().toISOString(),
  };
}

function sanitizeDefaultContexts(items: unknown) {
  const byId = new Map<string, ChatDefaultContextRecord>();
  const sourceItems = Array.isArray(items) ? items : [];

  sourceItems
    .map((item) => normalizeDefaultContextRecord(item))
    .filter((item): item is ChatDefaultContextRecord => Boolean(item))
    .forEach((item) => {
      const current = byId.get(item.id);

      if (!current || compareUpdatedAt(current, item) <= 0) {
        byId.set(item.id, item);
      }
    });

  return Array.from(byId.values())
    .sort((left, right) => {
      const leftSortOrder = Number.isFinite(left.sortOrder) ? left.sortOrder : null;
      const rightSortOrder = Number.isFinite(right.sortOrder) ? right.sortOrder : null;

      if (leftSortOrder !== null && rightSortOrder !== null && leftSortOrder !== rightSortOrder) {
        return leftSortOrder - rightSortOrder;
      }

      if (leftSortOrder !== null && rightSortOrder === null) {
        return -1;
      }

      if (leftSortOrder === null && rightSortOrder !== null) {
        return 1;
      }

      const titleCompare = left.title.localeCompare(right.title, 'ko-KR');

      if (titleCompare !== 0) {
        return titleCompare;
      }

      return compareUpdatedAt(left, right);
    })
    .map((item, index) => ({
      ...item,
      sortOrder: index + 1,
    }));
}

function sanitizeChatTypeDefaultSelections(items: unknown) {
  const byChatTypeId = new Map<string, ChatTypeDefaultContextSelection>();
  const sourceItems = Array.isArray(items) ? items : [];

  sourceItems.forEach((item) => {
    if (!item || typeof item !== 'object' || Array.isArray(item)) {
      return;
    }

    const record = item as Partial<ChatTypeDefaultContextSelection>;
    const chatTypeId = normalizeText(record.chatTypeId);

    if (!chatTypeId) {
      return;
    }

    const nextRecord: ChatTypeDefaultContextSelection = {
      chatTypeId,
      defaultContextIds: normalizeDefaultContextIds(record.defaultContextIds),
      updatedAt: normalizeText(record.updatedAt) || new Date().toISOString(),
    };
    const current = byChatTypeId.get(chatTypeId);

    if (!current || compareUpdatedAt(current, nextRecord) <= 0) {
      byChatTypeId.set(chatTypeId, nextRecord);
    }
  });

  return Array.from(byChatTypeId.values()).sort((left, right) => left.chatTypeId.localeCompare(right.chatTypeId, 'ko-KR'));
}

function sanitizeRoomContexts(items: unknown) {
  const bySessionId = new Map<string, ChatRoomContextSettings>();
  const sourceItems = Array.isArray(items) ? items : [];

  sourceItems.forEach((item) => {
    if (!item || typeof item !== 'object' || Array.isArray(item)) {
      return;
    }

    const record = item as Partial<ChatRoomContextSettings>;
    const sessionId = normalizeText(record.sessionId);

    if (!sessionId) {
      return;
    }

    const sanitizeCodexParticipants = (participants: unknown) => {
      const sourceParticipants = Array.isArray(participants) ? participants : [];
      return Array.from(
        new Map(
          sourceParticipants
            .map((participant, index) => {
              if (!participant || typeof participant !== 'object' || Array.isArray(participant)) {
                return null;
              }

              const record = participant as Partial<ChatRoomCodexParticipant>;
              const id = normalizeText(record.id) || `codex-participant-${index + 1}`;
              const name = normalizeText(record.name);
              const model = normalizeText(record.model);
              const prompt = normalizeText(record.prompt);
              const chatTypeId = normalizeText(record.chatTypeId) || null;
              const defaultContextIds = normalizeDefaultContextIds(record.defaultContextIds);
              const role =
                normalizeText(record.role) === 'moderator'
                  ? 'moderator'
                  : normalizeText(record.role) === 'conversation'
                    ? 'conversation'
                    : normalizeText(record.role) === 'reviewer'
                      ? 'reviewer'
                    : 'default';

              if (!name || !model) {
                return null;
              }

              return [
                id,
                {
                  id,
                  name,
                  model,
                  prompt,
                  chatTypeId,
                  defaultContextIds,
                  role,
                } satisfies ChatRoomCodexParticipant,
              ] as const;
            })
            .filter((entry): entry is readonly [string, ChatRoomCodexParticipant] => Boolean(entry)),
        ).values(),
      );
    };

    const nextRecord: ChatRoomContextSettings = {
      sessionId,
      defaultContextIds: normalizeDefaultContextIds(record.defaultContextIds),
      customContextTitle: normalizeText(record.customContextTitle),
      customContextContent: normalizeText(record.customContextContent),
      codexParticipants: sanitizeCodexParticipants(record.codexParticipants),
      updatedAt: normalizeText(record.updatedAt) || new Date().toISOString(),
    };
    const hasCustomContext = Boolean(nextRecord.customContextTitle || nextRecord.customContextContent);
    const hasDefaultOverrides = nextRecord.defaultContextIds.length > 0;
    const hasCodexParticipants = nextRecord.codexParticipants.length > 0;

    if (!hasCustomContext && !hasDefaultOverrides && !hasCodexParticipants) {
      return;
    }

    const current = bySessionId.get(sessionId);

    if (!current || compareUpdatedAt(current, nextRecord) <= 0) {
      bySessionId.set(sessionId, nextRecord);
    }
  });

  return Array.from(bySessionId.values()).sort((left, right) => left.sessionId.localeCompare(right.sessionId, 'ko-KR'));
}

function sanitizeChatContextSettings(value: unknown): ChatContextSettingsSnapshot {
  const record = normalizeConfigRecord(value);

  return {
    defaultContexts: sanitizeDefaultContexts(record.defaultContexts),
    chatTypeDefaults: sanitizeChatTypeDefaultSelections(record.chatTypeDefaults),
    roomContexts: sanitizeRoomContexts(record.roomContexts),
  };
}

function normalizeChatTypeRecord(value: unknown): ChatTypeRecord | null {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return null;
  }

  const record = value as Partial<ChatTypeRecord>;
  const name = normalizeText(record.name);

  if (!name) {
    return null;
  }

  return {
    id: normalizeText(record.id) || `chat-type-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
    name,
    sortOrder: normalizePositiveSortOrder(record.sortOrder),
    description: normalizeText(record.description),
    executionPolicy: normalizeChatTypeExecutionPolicy((record as { executionPolicy?: unknown }).executionPolicy),
    permissions: normalizePermissions(record.permissions),
    enabled: record.enabled !== false,
    updatedAt: normalizeText(record.updatedAt) || new Date().toISOString(),
  };
}

export function createDefaultChatTypeExecutionPolicy(
  mode: ChatTypeExecutionMode = 'default',
): ChatTypeExecutionPolicy {
  if (mode === 'summary-free-talking') {
    return {
      mode,
      participantBinding: 'first-moderator-rest-conversation',
      reviewPolicy: 'self',
      resourceReportPolicy: 'if-generated',
      allowModeratorIntervention: false,
      finalSummaryRequired: true,
    };
  }

  if (mode === 'dispatcher-workers') {
    return {
      mode,
      participantBinding: 'first-moderator-rest-conversation',
      reviewPolicy: 'self',
      resourceReportPolicy: 'if-generated',
      allowModeratorIntervention: true,
      finalSummaryRequired: true,
    };
  }

  return {
    mode,
    participantBinding: 'manual',
    reviewPolicy: 'self',
    resourceReportPolicy: 'if-generated',
    allowModeratorIntervention: true,
    finalSummaryRequired: false,
  };
}

function normalizeChatTypeExecutionMode(value: unknown): ChatTypeExecutionMode {
  if (value === 'summary-free-talking' || value === 'dispatcher-workers') {
    return value;
  }

  return 'default';
}

function normalizeChatTypeExecutionPolicy(value: unknown): ChatTypeExecutionPolicy {
  const record = normalizeConfigRecord(value);
  const mode = normalizeChatTypeExecutionMode(record.mode);
  const defaults = createDefaultChatTypeExecutionPolicy(mode);

  return {
    mode,
    participantBinding:
      record.participantBinding === 'first-moderator-rest-conversation' ||
      record.participantBinding === 'first-moderator-rest-conversation-last-reviewer' ||
      record.participantBinding === 'manual'
        ? record.participantBinding
        : defaults.participantBinding,
    reviewPolicy: record.reviewPolicy === 'reviewer' ? 'reviewer' : defaults.reviewPolicy,
    resourceReportPolicy:
      record.resourceReportPolicy === 'none' || record.resourceReportPolicy === 'always'
        ? record.resourceReportPolicy
        : defaults.resourceReportPolicy,
    allowModeratorIntervention:
      typeof record.allowModeratorIntervention === 'boolean'
        ? record.allowModeratorIntervention
        : defaults.allowModeratorIntervention,
    finalSummaryRequired:
      typeof record.finalSummaryRequired === 'boolean' ? record.finalSummaryRequired : defaults.finalSummaryRequired,
  };
}

function normalizePositiveSortOrder(value: unknown) {
  if (typeof value !== 'number' || !Number.isFinite(value)) {
    return Number.NaN;
  }

  const nextValue = Math.trunc(value);
  return nextValue > 0 ? nextValue : Number.NaN;
}

function buildChatTypeSemanticKey(record: Pick<ChatTypeRecord, 'name'>) {
  return record.name.replace(/\s+/g, ' ').toLocaleLowerCase('ko-KR');
}

function isLegacyMigratedChatTypeId(chatTypeId: string) {
  return chatTypeId === LEGACY_PLAN_CHECKLIST_CHAT_TYPE_ID;
}

function compareUpdatedAt(left: { updatedAt: string }, right: { updatedAt: string }) {
  const leftTime = Date.parse(left.updatedAt);
  const rightTime = Date.parse(right.updatedAt);

  if (Number.isFinite(leftTime) && Number.isFinite(rightTime) && leftTime !== rightTime) {
    return leftTime - rightTime;
  }

  return 0;
}

function sanitizeChatTypes(items: unknown[]) {
  const normalized = items
    .map((item) => normalizeChatTypeRecord(item))
    .filter((item): item is ChatTypeRecord => Boolean(item));

  const byId = new Map<string, ChatTypeRecord>();
  const bySemanticKey = new Map<string, ChatTypeRecord>();

  for (const item of normalized) {
    const current = byId.get(item.id);

    if (!current || compareUpdatedAt(current, item) <= 0) {
      byId.set(item.id, item);
    }
  }

  for (const item of byId.values()) {
    const semanticKey = buildChatTypeSemanticKey(item);
    const current = bySemanticKey.get(semanticKey);

    if (!current || compareUpdatedAt(current, item) <= 0) {
      bySemanticKey.set(semanticKey, item);
    }
  }

  return Array.from(bySemanticKey.values())
    .sort((left, right) => {
      const leftSortOrder = Number.isFinite(left.sortOrder) ? left.sortOrder : null;
      const rightSortOrder = Number.isFinite(right.sortOrder) ? right.sortOrder : null;

      if (leftSortOrder !== null && rightSortOrder !== null && leftSortOrder !== rightSortOrder) {
        return leftSortOrder - rightSortOrder;
      }

      if (leftSortOrder !== null && rightSortOrder === null) {
        return -1;
      }

      if (leftSortOrder === null && rightSortOrder !== null) {
        return 1;
      }

      const nameCompare = left.name.localeCompare(right.name, 'ko-KR');

      if (nameCompare !== 0) {
        return nameCompare;
      }

      return compareUpdatedAt(left, right);
    })
    .map((item, index) => ({
      ...item,
      sortOrder: index + 1,
    }));
}

export function sanitizePersistedChatTypes(items: unknown[]) {
  return sanitizeChatTypes(items);
}

function stripLegacyMigratedChatTypes(items: ChatTypeRecord[]) {
  return items.filter((item) => !isLegacyMigratedChatTypeId(item.id));
}

function createBuiltinChatTypeRecord(
  overrides: Partial<ChatTypeRecord> & Pick<ChatTypeRecord, 'id' | 'name' | 'description' | 'executionPolicy'>,
): ChatTypeRecord {
  return {
    id: overrides.id,
    name: overrides.name,
    sortOrder: overrides.sortOrder ?? Number.NaN,
    description: overrides.description,
    executionPolicy: overrides.executionPolicy,
    permissions: overrides.permissions ?? ['token-user'],
    enabled: overrides.enabled ?? true,
    updatedAt: overrides.updatedAt ?? new Date().toISOString(),
  };
}

function buildBuiltinCodexChatTypes() {
  return [
    createBuiltinChatTypeRecord({
      id: CODEX_LIVE_DEFAULT_CHAT_TYPE_ID,
      name: CODEX_LIVE_DEFAULT_CHAT_TYPE_NAME,
      description: CODEX_LIVE_DEFAULT_CHAT_TYPE_DESCRIPTION,
      executionPolicy: createDefaultChatTypeExecutionPolicy('default'),
    }),
    createBuiltinChatTypeRecord({
      id: CODEX_SUMMARY_CHAT_TYPE_ID,
      name: CODEX_SUMMARY_CHAT_TYPE_NAME,
      description: CODEX_SUMMARY_CHAT_TYPE_DESCRIPTION,
      executionPolicy: createDefaultChatTypeExecutionPolicy('summary-free-talking'),
    }),
    createBuiltinChatTypeRecord({
      id: CODEX_DISPATCHER_CHAT_TYPE_ID,
      name: CODEX_DISPATCHER_CHAT_TYPE_NAME,
      description: CODEX_DISPATCHER_CHAT_TYPE_DESCRIPTION,
      executionPolicy: createDefaultChatTypeExecutionPolicy('dispatcher-workers'),
    }),
  ] satisfies ChatTypeRecord[];
}

function shouldUpgradeLegacyCodexSummaryChatType(record: ChatTypeRecord) {
  if (record.id !== CODEX_SUMMARY_CHAT_TYPE_ID) {
    return false;
  }

  return (
    record.executionPolicy.mode === 'default' &&
    (record.description === '' || record.description === CODEX_SUMMARY_LEGACY_DESCRIPTION)
  );
}

export function synchronizeBuiltinCodexChatTypes(items: ChatTypeRecord[]) {
  const builtins = buildBuiltinCodexChatTypes();
  const builtinById = new Map(builtins.map((item) => [item.id, item] as const));
  const merged = items.map((item) => {
    const builtin = builtinById.get(item.id);

    if (!builtin) {
      return item;
    }

    if (item.id === CODEX_SUMMARY_CHAT_TYPE_ID && shouldUpgradeLegacyCodexSummaryChatType(item)) {
      return {
        ...item,
        description: builtin.description,
        executionPolicy: builtin.executionPolicy,
      };
    }

    return item;
  });
  const existingIds = new Set(merged.map((item) => item.id));

  builtins.forEach((builtin) => {
    if (!existingIds.has(builtin.id)) {
      merged.push(builtin);
    }
  });

  return sanitizePersistedChatTypes(merged);
}

function buildPlanChecklistDefaultContext(record?: ChatTypeRecord | null, existing?: ChatDefaultContextRecord | null) {
  const content = normalizeText(record?.description) || normalizeText(existing?.content) || PLAN_CHECKLIST_DEFAULT_CONTEXT_CONTENT;

  return normalizeDefaultContextRecord({
    id: PLAN_CHECKLIST_DEFAULT_CONTEXT_ID,
    title: normalizeText(record?.name) || normalizeText(existing?.title) || PLAN_CHECKLIST_DEFAULT_CONTEXT_TITLE,
    content,
    enabled: existing?.enabled ?? record?.enabled ?? true,
    updatedAt: normalizeText(record?.updatedAt) || normalizeText(existing?.updatedAt) || new Date().toISOString(),
  });
}

export function migrateLegacyChatTypeContexts(
  settings: ChatContextSettingsSnapshot,
  chatTypes: ChatTypeRecord[],
): ChatContextSettingsSnapshot {
  const legacyPlanChecklistChatType = chatTypes.find((item) => item.id === LEGACY_PLAN_CHECKLIST_CHAT_TYPE_ID);

  if (!legacyPlanChecklistChatType) {
    return settings;
  }

  const existingContext =
    settings.defaultContexts.find((item) => item.id === PLAN_CHECKLIST_DEFAULT_CONTEXT_ID) ?? null;
  const migratedContext = buildPlanChecklistDefaultContext(legacyPlanChecklistChatType, existingContext);
  const nextDefaultContexts = migratedContext
    ? sanitizeDefaultContexts([
        ...settings.defaultContexts.filter((item) => item.id !== PLAN_CHECKLIST_DEFAULT_CONTEXT_ID),
        migratedContext,
      ])
    : settings.defaultContexts;
  const nextChatTypeDefaults = sanitizeChatTypeDefaultSelections(
    settings.chatTypeDefaults.filter((item) => item.chatTypeId !== LEGACY_PLAN_CHECKLIST_CHAT_TYPE_ID),
  );

  return {
    defaultContexts: nextDefaultContexts,
    chatTypeDefaults: nextChatTypeDefaults,
    roomContexts: settings.roomContexts,
  };
}

function isSameChatTypeList(left: ChatTypeRecord[], right: ChatTypeRecord[]) {
  if (left.length !== right.length) {
    return false;
  }

  return left.every((item, index) => {
    const target = right[index];
    return (
      target &&
      item.id === target.id &&
      item.name === target.name &&
      item.description === target.description &&
      item.executionPolicy.mode === target.executionPolicy.mode &&
      item.executionPolicy.participantBinding === target.executionPolicy.participantBinding &&
      item.executionPolicy.reviewPolicy === target.executionPolicy.reviewPolicy &&
      item.executionPolicy.resourceReportPolicy === target.executionPolicy.resourceReportPolicy &&
      item.executionPolicy.allowModeratorIntervention === target.executionPolicy.allowModeratorIntervention &&
      item.executionPolicy.finalSummaryRequired === target.executionPolicy.finalSummaryRequired &&
      item.enabled === target.enabled &&
      item.sortOrder === target.sortOrder &&
      item.updatedAt === target.updatedAt &&
      item.permissions.length === target.permissions.length &&
      item.permissions.every((permission, permissionIndex) => permission === target.permissions[permissionIndex])
    );
  });
}

export type AppConfigSnapshot = {
  chat?: {
    maxContextMessages?: number;
    maxContextChars?: number;
    codexLiveMaxExecutionSeconds?: number;
    codexLiveIdleTimeoutSeconds?: number;
    receiveRoomNotifications?: boolean;
    restartReservationCompletionDelaySeconds?: number;
  };
  automation?: {
    autoRefreshEnabled?: boolean;
    autoRefreshIntervalSeconds?: number;
    autoReceiveScheduleType?: 'interval' | 'daily' | 'weekly';
    autoReceiveIntervalSeconds?: number;
    autoReceiveDailyTime?: string;
    autoReceiveWeeklyDay?: 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun';
    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';
  };
};

export function normalizeAppConfigSnapshot(value: unknown): AppConfigSnapshot {
  const normalized = normalizeConfigRecord(value);
  const chat = normalizeConfigRecord(normalized.chat);
  const worklogAutomation = normalizeConfigRecord(normalized.worklogAutomation);

  return {
    ...(normalized as AppConfigSnapshot),
    chat: {
      maxContextMessages: normalizeIntegerInRange(
        chat.maxContextMessages,
        DEFAULT_CHAT_APP_CONFIG.maxContextMessages,
        1,
        50,
      ),
      maxContextChars: normalizeIntegerInRange(
        chat.maxContextChars,
        DEFAULT_CHAT_APP_CONFIG.maxContextChars,
        500,
        20_000,
      ),
      codexLiveMaxExecutionSeconds: normalizeIntegerInRange(
        chat.codexLiveMaxExecutionSeconds,
        DEFAULT_CHAT_APP_CONFIG.codexLiveMaxExecutionSeconds,
        60,
        7200,
      ),
      codexLiveIdleTimeoutSeconds: normalizeIntegerInRange(
        chat.codexLiveIdleTimeoutSeconds,
        DEFAULT_CHAT_APP_CONFIG.codexLiveIdleTimeoutSeconds,
        30,
        3600,
      ),
      receiveRoomNotifications:
        typeof chat.receiveRoomNotifications === 'boolean'
          ? chat.receiveRoomNotifications
          : DEFAULT_CHAT_APP_CONFIG.receiveRoomNotifications,
      restartReservationCompletionDelaySeconds: normalizeIntegerInRange(
        chat.restartReservationCompletionDelaySeconds,
        DEFAULT_CHAT_APP_CONFIG.restartReservationCompletionDelaySeconds,
        1,
        300,
      ),
    },
    worklogAutomation:
      Object.keys(worklogAutomation).length > 0
        ? {
            ...(normalized.worklogAutomation as AppConfigSnapshot['worklogAutomation']),
            repeatRequestEnabled: false,
          }
        : undefined,
  };
}

export async function getAppConfigSnapshot(appOrigin?: string | null): Promise<AppConfigSnapshot> {
  const config = normalizeConfigRecord(await getAppConfig(appOrigin));
  const rawConfig = await getRawAppConfigRecord();
  const sharedChatContextAppOrigin = resolveSharedChatContextAppOrigin();
  const canonicalChatTypes = resolveCanonicalChatTypesFromConfig(rawConfig, sharedChatContextAppOrigin);
  const canonicalChatContextSettings = resolveCanonicalChatContextSettingsFromConfig(
    rawConfig,
    sharedChatContextAppOrigin,
  );
  const { scopedConfigs, backups } = stripSharedContextDataFromScopedAppConfigs(
    rawConfig,
    sharedChatContextAppOrigin,
  );

  return normalizeAppConfigSnapshot({
    ...config,
    ...(canonicalChatTypes ? { [CHAT_TYPES_CONFIG_KEY]: canonicalChatTypes } : null),
    [CHAT_CONTEXT_SETTINGS_CONFIG_KEY]: canonicalChatContextSettings,
    [SCOPED_APP_CONFIGS_KEY]: scopedConfigs,
    [SCOPED_CONTEXT_CONFIG_BACKUPS_KEY]: backups,
  });
}

export async function upsertAppConfig(
  config: Record<string, unknown>,
  appOrigin?: string | null,
  appDomain?: string | null,
) {
  await ensureAppConfigTable();
  const nextConfig = normalizeConfigRecord(config);

  const existing = await db(APP_CONFIG_TABLE).first();

  if (!existing) {
    const initialConfig = mergeScopedAppConfig({}, nextConfig, appOrigin, appDomain);
    const rows = await db(APP_CONFIG_TABLE)
      .insert({
        config_json: initialConfig,
        updated_at: db.fn.now(),
      })
      .returning('*');
    return resolveAppConfigByOrigin(rows[0]?.config_json ?? initialConfig, appOrigin);
  }

  const mergedConfig = mergeScopedAppConfig(
    normalizeConfigRecord(existing.config_json),
    nextConfig,
    appOrigin,
    appDomain,
  );

  const rows = await db(APP_CONFIG_TABLE)
    .update({
      config_json: mergedConfig,
      updated_at: db.fn.now(),
    })
    .returning('*');

  return resolveAppConfigByOrigin(rows[0]?.config_json ?? mergedConfig, appOrigin);
}

async function replaceAppConfig(config: Record<string, unknown>, appOrigin?: string | null) {
  await ensureAppConfigTable();
  const nextConfig = normalizeConfigRecord(config);
  const existing = await db(APP_CONFIG_TABLE).first();

  if (!existing) {
    const rows = await db(APP_CONFIG_TABLE)
      .insert({
        config_json: nextConfig,
        updated_at: db.fn.now(),
      })
      .returning('*');

    return resolveAppConfigByOrigin(rows[0]?.config_json ?? nextConfig, appOrigin);
  }

  const rows = await db(APP_CONFIG_TABLE)
    .update({
      config_json: nextConfig,
      updated_at: db.fn.now(),
    })
    .returning('*');

  return resolveAppConfigByOrigin(rows[0]?.config_json ?? nextConfig, appOrigin);
}

export async function getChatTypesConfig(appOrigin?: string | null): Promise<ChatTypesConfigSnapshot> {
  const rawConfig = await getRawAppConfigRecord();
  const sharedChatContextAppOrigin = resolveSharedChatContextAppOrigin();
  const canonicalChatTypes = resolveCanonicalChatTypesFromConfig(rawConfig, sharedChatContextAppOrigin);
  const migratedChatTypeList = stripLegacyMigratedChatTypes(canonicalChatTypes ?? []);
  const chatTypes = synchronizeBuiltinCodexChatTypes(sanitizePersistedChatTypes(migratedChatTypeList));
  const migratedSettings = migrateLegacyChatTypeContexts(
    resolveCanonicalChatContextSettingsFromConfig(rawConfig, sharedChatContextAppOrigin),
    chatTypes,
  );
  const globalChatTypes = Array.isArray(rawConfig[CHAT_TYPES_CONFIG_KEY])
    ? synchronizeBuiltinCodexChatTypes(
        stripLegacyMigratedChatTypes(sanitizePersistedChatTypes(rawConfig[CHAT_TYPES_CONFIG_KEY] as unknown[])),
      )
    : [];
  const globalSettings = sanitizeChatContextSettings(rawConfig[CHAT_CONTEXT_SETTINGS_CONFIG_KEY]);
  const { changed, scopedConfigs, backups } = stripSharedContextDataFromScopedAppConfigs(
    rawConfig,
    sharedChatContextAppOrigin,
  );

  if (
    changed ||
    !isSameChatTypeList(globalChatTypes, chatTypes) ||
    JSON.stringify(globalSettings) !== JSON.stringify(migratedSettings)
  ) {
    await replaceAppConfig({
      ...rawConfig,
      [CHAT_TYPES_CONFIG_KEY]: chatTypes,
      [CHAT_CONTEXT_SETTINGS_CONFIG_KEY]: migratedSettings,
      [SCOPED_APP_CONFIGS_KEY]: scopedConfigs,
      [SCOPED_CONTEXT_CONFIG_BACKUPS_KEY]: backups,
    });
  }

  return {
    chatTypes,
  };
}

export async function upsertChatTypesConfig(chatTypes: unknown[], appOrigin?: string | null, appDomain?: string | null) {
  const current = await getRawAppConfigRecord();
  const nextChatTypes = synchronizeBuiltinCodexChatTypes(sanitizePersistedChatTypes(chatTypes));
  const { scopedConfigs, backups } = stripSharedContextDataFromScopedAppConfigs(
    current,
    resolveSharedChatContextAppOrigin(),
  );
  const nextConfig = {
    ...current,
    [CHAT_TYPES_CONFIG_KEY]: nextChatTypes,
    [SCOPED_APP_CONFIGS_KEY]: scopedConfigs,
    [SCOPED_CONTEXT_CONFIG_BACKUPS_KEY]: backups,
  };

  void appOrigin;
  void appDomain;
  await replaceAppConfig(nextConfig);
  return {
    chatTypes: nextChatTypes,
  };
}

export async function getChatContextSettingsConfig(appOrigin?: string | null) {
  const rawConfig = await getRawAppConfigRecord();
  const sharedChatContextAppOrigin = resolveSharedChatContextAppOrigin();
  const canonicalChatTypes = resolveCanonicalChatTypesFromConfig(rawConfig, sharedChatContextAppOrigin) ?? [];
  const migratedSettings = migrateLegacyChatTypeContexts(
    resolveCanonicalChatContextSettingsFromConfig(rawConfig, sharedChatContextAppOrigin),
    canonicalChatTypes,
  );
  const migratedChatTypes = stripLegacyMigratedChatTypes(canonicalChatTypes);
  const globalChatTypes = Array.isArray(rawConfig[CHAT_TYPES_CONFIG_KEY])
    ? stripLegacyMigratedChatTypes(sanitizePersistedChatTypes(rawConfig[CHAT_TYPES_CONFIG_KEY] as unknown[]))
    : [];
  const nextCustomChatTypes = sanitizePersistedChatTypes(migratedChatTypes);
  const globalSettings = sanitizeChatContextSettings(rawConfig[CHAT_CONTEXT_SETTINGS_CONFIG_KEY]);
  const { changed, scopedConfigs, backups } = stripSharedContextDataFromScopedAppConfigs(
    rawConfig,
    sharedChatContextAppOrigin,
  );

  if (
    changed ||
    !isSameChatTypeList(globalChatTypes, nextCustomChatTypes) ||
    JSON.stringify(globalSettings) !== JSON.stringify(migratedSettings)
  ) {
    await replaceAppConfig({
      ...rawConfig,
      [CHAT_TYPES_CONFIG_KEY]: nextCustomChatTypes,
      [CHAT_CONTEXT_SETTINGS_CONFIG_KEY]: migratedSettings,
      [SCOPED_APP_CONFIGS_KEY]: scopedConfigs,
      [SCOPED_CONTEXT_CONFIG_BACKUPS_KEY]: backups,
    });
  }

  return migratedSettings;
}

export async function upsertChatContextSettingsConfig(
  settings: unknown,
  appOrigin?: string | null,
  appDomain?: string | null,
) {
  const current = await getRawAppConfigRecord();
  const nextSettings = sanitizeChatContextSettings(settings);
  const { scopedConfigs } = stripChatContextSettingsFromScopedAppConfigs(current);
  const nextConfig = {
    ...current,
    [CHAT_CONTEXT_SETTINGS_CONFIG_KEY]: nextSettings,
    [SCOPED_APP_CONFIGS_KEY]: scopedConfigs,
  };

  void appOrigin;
  void appDomain;
  await replaceAppConfig(nextConfig);
  return nextSettings;
}
