import type { FastifyBaseLogger } from 'fastify';
import path from 'node:path';
import { env } from '../config/env.js';
import { db } from '../db/client.js';
import { getAppConfigSnapshot } from './app-config-service.js';
import { listBoardPosts, type BoardPostItem, type BoardPostRequestItem } from './board-service.js';
import { getActiveChatService } from './chat-service.js';
import { chatRuntimeService, type ChatRuntimeJobItem } from './chat-runtime-service.js';
import { createNotificationMessage, deleteOlderNotificationMessagesBySource } from './notification-message-service.js';
import { resolveMainProjectRoot } from './main-project-root-service.js';
import {
  listServerCommands,
  restartServerCommand,
  type ServerCommandSnapshot,
  type ServerCommandKey,
} from './server-command-service.js';
import { ensureVisitorHistoryTables, listVisitorClients } from './visitor-history-service.js';
import { syncMainProjectBranchForReservedRestart } from './git-service.js';
import { isCurrentWorkServerSlotActive } from './work-server-slot-service.js';

const SERVER_RESTART_RESERVATION_TABLE = 'server_restart_reservations';
const SERVER_RESTART_RESERVATION_ROW_ID = 1;
const SERVER_RESTART_RESERVATION_POLL_INTERVAL_MS = 10_000;
const ACTIVE_CLIENT_WINDOW_MS = 3 * 60 * 1000;
const TEST_TO_WORK_SERVER_DELAY_MS = 5_000;
const RESTART_VERIFICATION_CLOCK_SKEW_MS = 5_000;
const SERVER_RESTART_RESERVATION_NOTIFICATION_SOURCE = 'server-restart-reservation';
const RESERVED_RESTART_AUTO_FIX_SESSION_ID = 'server-restart-reservation';
const RESERVED_RESTART_AUTO_FIX_MAX_EXECUTION_SECONDS = 600;
const RESERVED_RESTART_AUTO_FIX_IDLE_TIMEOUT_SECONDS = 180;
const RESERVED_RESTART_MAIN_COMMIT_MESSAGE = 'chore: sync main before reserved restart';

type RestartReservationStatus = 'idle' | 'waiting' | 'ready' | 'executing' | 'recovering' | 'completed' | 'cancelled' | 'failed';
type RestartReservationTarget = 'all' | 'test' | 'work-server';
type RestartReservationExecutionPhase =
  | 'idle'
  | 'commit-main-worktree'
  | 'restart-test'
  | 'restart-work-server'
  | 'verify-runtime';

type RestartReservationWorkloadSummary = {
  codexRunningCount: number;
  codexQueuedCount: number;
  automationRunningCount: number;
  automationQueuedCount: number;
};

type RestartReservationWorkItem = {
  kind: 'codex' | 'automation';
  status: 'running' | 'queued' | 'waiting';
  title: string;
  detail: string | null;
  requestId: string | null;
  sessionId: string | null;
};

type RestartReservationAutoFixStatus = 'idle' | 'queued' | 'running' | 'completed' | 'failed';

type RestartReservationAutoFix = {
  enabled: boolean;
  targetKey: 'test' | 'work-server' | null;
  requestId: string | null;
  sessionId: string | null;
  status: RestartReservationAutoFixStatus;
  summary: string | null;
  detail: string | null;
  requestedAt: string | null;
  startedAt: string | null;
  completedAt: string | null;
  failedAt: string | null;
};

type RestartReservationRow = {
  id: number;
  enabled: boolean;
  target: RestartReservationTarget;
  status: RestartReservationStatus;
  requested_at: string | null;
  requested_by_client_id: string | null;
  last_checked_at: string | null;
  waiting_reason: string | null;
  workload_summary_json: RestartReservationWorkloadSummary | string | null;
  started_at: string | null;
  completed_at: string | null;
  cancelled_at: string | null;
  last_error: string | null;
  active_client_count: number | null;
  notified_active_clients_at: string | null;
  app_origin: string | null;
  auto_execute_at: string | null;
  auto_execute_delay_seconds: number | null;
  execution_phase: RestartReservationExecutionPhase | string | null;
  updated_at: string | null;
  auto_fix_json: RestartReservationAutoFix | string | null;
};

export type ServerRestartReservationSnapshot = {
  enabled: boolean;
  target: RestartReservationTarget;
  status: RestartReservationStatus;
  requestedAt: string | null;
  requestedByClientId: string | null;
  lastCheckedAt: string | null;
  nextCheckAt: string | null;
  waitingReason: string | null;
  workloadSummary: RestartReservationWorkloadSummary;
  startedAt: string | null;
  completedAt: string | null;
  cancelledAt: string | null;
  lastError: string | null;
  activeClientCount: number;
  notifiedActiveClientsAt: string | null;
  appOrigin: string | null;
  autoExecuteAt: string | null;
  autoExecuteDelaySeconds: number;
  executionPhase: RestartReservationExecutionPhase;
  updatedAt: string | null;
  workItems: RestartReservationWorkItem[];
  autoFix: RestartReservationAutoFix;
};

function normalizeExecutionPhase(value: unknown): RestartReservationExecutionPhase {
  return value === 'commit-main-worktree'
    || value === 'restart-test'
    || value === 'restart-work-server'
    || value === 'verify-runtime'
    ? value
    : 'idle';
}

function normalizeReservationTarget(value: unknown): RestartReservationTarget {
  return value === 'test' || value === 'work-server' ? value : 'all';
}

function getReservationTargetKeys(target: RestartReservationTarget): Array<'test' | 'work-server'> {
  if (target === 'test') {
    return ['test'];
  }

  if (target === 'work-server') {
    return ['work-server'];
  }

  return ['test', 'work-server'];
}

function getReservationTargetLabel(target: RestartReservationTarget) {
  if (target === 'test') {
    return 'TEST 서버';
  }

  if (target === 'work-server') {
    return 'WORK 서버';
  }

  return 'TEST / WORK 서버';
}

function getDefaultWorkloadSummary(): RestartReservationWorkloadSummary {
  return {
    codexRunningCount: 0,
    codexQueuedCount: 0,
    automationRunningCount: 0,
    automationQueuedCount: 0,
  };
}

function getDefaultAutoFixState(): RestartReservationAutoFix {
  return {
    enabled: false,
    targetKey: null,
    requestId: null,
    sessionId: null,
    status: 'idle',
    summary: null,
    detail: null,
    requestedAt: null,
    startedAt: null,
    completedAt: null,
    failedAt: null,
  };
}

function hasAcceptedAutomationRequest(requestItem: Pick<BoardPostRequestItem, 'planItemId' | 'automationReceivedAt' | 'workflowState'>) {
  return Boolean(requestItem.planItemId) || Boolean(requestItem.automationReceivedAt) || requestItem.workflowState !== 'pending';
}

export function summarizeRestartReservationAutomationWork(
  posts: Array<Pick<BoardPostItem, 'requestItems'>>,
) {
  return posts.reduce(
    (summary, item) => {
      for (const requestItem of item.requestItems) {
        if (requestItem.status === 'in_progress') {
          summary.running += 1;
          continue;
        }

        if (
          (requestItem.status === 'queued' || requestItem.status === 'waiting')
          && hasAcceptedAutomationRequest(requestItem)
        ) {
          summary.queued += 1;
        }
      }

      return summary;
    },
    { running: 0, queued: 0 },
  );
}

export function summarizeRestartReservationCodexWork(
  runtimeItems: {
    running: Array<Pick<ChatRuntimeJobItem, 'sessionId'>>;
    queued: Array<Pick<ChatRuntimeJobItem, 'sessionId'>>;
  },
  options?: {
    sessionClientIds?: Map<string, string | null>;
  },
) {
  void options?.sessionClientIds;

  return {
    running: runtimeItems.running.length,
    queued: runtimeItems.queued.length,
  };
}

export async function getRestartReservationWorkloadSummary() {
  const runtimeSnapshot = chatRuntimeService.getSnapshot();
  const runtimeSummary = summarizeRestartReservationCodexWork(
    {
      running: runtimeSnapshot.running,
      queued: runtimeSnapshot.queued,
    },
    { sessionClientIds: getActiveChatService()?.getSessionClientIdMap() ?? new Map<string, string | null>() },
  );
  const automationSummary = await countPendingAutomationWork();

  return {
    codexRunningCount: runtimeSummary.running,
    codexQueuedCount: runtimeSummary.queued,
    automationRunningCount: automationSummary.running,
    automationQueuedCount: automationSummary.queued,
  } satisfies RestartReservationWorkloadSummary;
}

function buildNextCheckAt(row: RestartReservationRow | null | undefined) {
  if (!row?.enabled || row.status !== 'waiting') {
    return null;
  }

  const baseTimestamp = row.last_checked_at ?? row.requested_at ?? row.updated_at;

  if (!baseTimestamp) {
    return null;
  }

  const baseTime = Date.parse(baseTimestamp);

  if (!Number.isFinite(baseTime)) {
    return null;
  }

  return new Date(baseTime + SERVER_RESTART_RESERVATION_POLL_INTERVAL_MS).toISOString();
}

function parseAutoFixState(rawValue: RestartReservationRow['auto_fix_json']): RestartReservationAutoFix {
  if (!rawValue) {
    return getDefaultAutoFixState();
  }

  if (typeof rawValue === 'string') {
    try {
      return parseAutoFixState(JSON.parse(rawValue) as RestartReservationAutoFix);
    } catch {
      return getDefaultAutoFixState();
    }
  }

  const value = rawValue as Partial<RestartReservationAutoFix>;

  return {
    enabled: value.enabled === true,
    targetKey: value.targetKey === 'test' || value.targetKey === 'work-server' ? value.targetKey : null,
    requestId: typeof value.requestId === 'string' ? value.requestId : null,
    sessionId: typeof value.sessionId === 'string' ? value.sessionId : null,
    status:
      value.status === 'queued'
      || value.status === 'running'
      || value.status === 'completed'
      || value.status === 'failed'
        ? value.status
        : 'idle',
    summary: typeof value.summary === 'string' ? value.summary : null,
    detail: typeof value.detail === 'string' ? value.detail : null,
    requestedAt: typeof value.requestedAt === 'string' ? value.requestedAt : null,
    startedAt: typeof value.startedAt === 'string' ? value.startedAt : null,
    completedAt: typeof value.completedAt === 'string' ? value.completedAt : null,
    failedAt: typeof value.failedAt === 'string' ? value.failedAt : null,
  };
}

async function syncReservedRestartMainProject(logger: FastifyBaseLogger) {
  const mainProjectRoot = resolveMainProjectRoot();
  const result = await syncMainProjectBranchForReservedRestart(
    mainProjectRoot,
    env.PLAN_MAIN_BRANCH,
    RESERVED_RESTART_MAIN_COMMIT_MESSAGE,
  );

  logger.info(
    {
      branchName: result.branchName,
      committed: result.committed,
      commitMessage: result.commitMessage,
      head: result.head,
      syncMode: result.syncMode,
      repoPath: mainProjectRoot,
    },
    'Reserved restart synced main project branch',
  );

  return result;
}

function mapReservationRow(
  row: RestartReservationRow | null | undefined,
  options?: {
    workItems?: RestartReservationWorkItem[];
  },
): ServerRestartReservationSnapshot {
  const rawSummary = row?.workload_summary_json;
  let workloadSummary = getDefaultWorkloadSummary();

  if (typeof rawSummary === 'string') {
    try {
      const parsed = JSON.parse(rawSummary) as Partial<RestartReservationWorkloadSummary>;
      workloadSummary = {
        codexRunningCount: Number(parsed.codexRunningCount ?? 0),
        codexQueuedCount: Number(parsed.codexQueuedCount ?? 0),
        automationRunningCount: Number(parsed.automationRunningCount ?? 0),
        automationQueuedCount: Number(parsed.automationQueuedCount ?? 0),
      };
    } catch {
      workloadSummary = getDefaultWorkloadSummary();
    }
  } else if (rawSummary && typeof rawSummary === 'object') {
    workloadSummary = {
      codexRunningCount: Number(rawSummary.codexRunningCount ?? 0),
      codexQueuedCount: Number(rawSummary.codexQueuedCount ?? 0),
      automationRunningCount: Number(rawSummary.automationRunningCount ?? 0),
      automationQueuedCount: Number(rawSummary.automationQueuedCount ?? 0),
    };
  }

  const autoFix = parseAutoFixState(row?.auto_fix_json ?? null);

  return {
    enabled: Boolean(row?.enabled),
    target: normalizeReservationTarget(row?.target),
    status: row?.status ?? 'idle',
    requestedAt: row?.requested_at ?? null,
    requestedByClientId: row?.requested_by_client_id ?? null,
    lastCheckedAt: row?.last_checked_at ?? null,
    nextCheckAt: buildNextCheckAt(row),
    waitingReason: row?.waiting_reason ?? null,
    workloadSummary,
    startedAt: row?.started_at ?? null,
    completedAt: row?.completed_at ?? null,
    cancelledAt: row?.cancelled_at ?? null,
    lastError: row?.last_error ?? null,
    activeClientCount: Number(row?.active_client_count ?? 0),
    notifiedActiveClientsAt: row?.notified_active_clients_at ?? null,
    appOrigin: row?.app_origin ?? null,
    autoExecuteAt: row?.auto_execute_at ?? null,
    autoExecuteDelaySeconds: Math.max(1, Number(row?.auto_execute_delay_seconds ?? 10)),
    executionPhase: normalizeExecutionPhase(row?.execution_phase),
    updatedAt: row?.updated_at ?? null,
    workItems: options?.workItems ?? [],
    autoFix,
  };
}

async function ensureServerRestartReservationTable() {
  const hasTable = await db.schema.hasTable(SERVER_RESTART_RESERVATION_TABLE);

  if (!hasTable) {
    await db.schema.createTable(SERVER_RESTART_RESERVATION_TABLE, (table) => {
      table.integer('id').primary();
      table.boolean('enabled').notNullable().defaultTo(false);
      table.string('target', 20).notNullable().defaultTo('all');
      table.string('status', 20).notNullable().defaultTo('idle');
      table.timestamp('requested_at', { useTz: true }).nullable();
      table.string('requested_by_client_id', 120).nullable();
      table.timestamp('last_checked_at', { useTz: true }).nullable();
      table.text('waiting_reason').nullable();
      table.jsonb('workload_summary_json').notNullable().defaultTo('{}');
      table.timestamp('started_at', { useTz: true }).nullable();
      table.timestamp('completed_at', { useTz: true }).nullable();
      table.timestamp('cancelled_at', { useTz: true }).nullable();
      table.text('last_error').nullable();
      table.integer('active_client_count').notNullable().defaultTo(0);
      table.timestamp('notified_active_clients_at', { useTz: true }).nullable();
      table.string('app_origin', 255).nullable();
      table.timestamp('auto_execute_at', { useTz: true }).nullable();
      table.integer('auto_execute_delay_seconds').notNullable().defaultTo(10);
      table.string('execution_phase', 40).notNullable().defaultTo('idle');
      table.jsonb('auto_fix_json').notNullable().defaultTo('{}');
      table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
    });
  }

  const requiredColumns: Array<[string, (table: any) => void]> = [
    ['app_origin', (table) => table.string('app_origin', 255).nullable()],
    ['auto_execute_at', (table) => table.timestamp('auto_execute_at', { useTz: true }).nullable()],
    ['auto_execute_delay_seconds', (table) => table.integer('auto_execute_delay_seconds').notNullable().defaultTo(10)],
    ['execution_phase', (table) => table.string('execution_phase', 40).notNullable().defaultTo('idle')],
    ['auto_fix_json', (table) => table.jsonb('auto_fix_json').notNullable().defaultTo('{}')],
  ];

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

  const existing = await db(SERVER_RESTART_RESERVATION_TABLE).where({ id: SERVER_RESTART_RESERVATION_ROW_ID }).first();

  if (!existing) {
    await db(SERVER_RESTART_RESERVATION_TABLE).insert({
      id: SERVER_RESTART_RESERVATION_ROW_ID,
      enabled: false,
      target: 'all',
      status: 'idle',
      workload_summary_json: getDefaultWorkloadSummary(),
      active_client_count: 0,
      execution_phase: 'idle',
      auto_fix_json: getDefaultAutoFixState(),
      updated_at: db.fn.now(),
    });
  }
}

async function readReservationRow() {
  await ensureServerRestartReservationTable();
  return (await db(SERVER_RESTART_RESERVATION_TABLE)
    .where({ id: SERVER_RESTART_RESERVATION_ROW_ID })
    .first()) as RestartReservationRow | undefined;
}

async function updateReservationRow(patch: Record<string, unknown>) {
  await ensureServerRestartReservationTable();
  await db(SERVER_RESTART_RESERVATION_TABLE)
    .where({ id: SERVER_RESTART_RESERVATION_ROW_ID })
    .update({
      ...patch,
      updated_at: db.fn.now(),
    });
  return readReservationRow();
}

async function countPendingAutomationWork() {
  return summarizeRestartReservationAutomationWork(await listBoardPosts());
}

async function listRestartReservationWorkItems(): Promise<RestartReservationWorkItem[]> {
  const runtimeSnapshot = chatRuntimeService.getSnapshot();
  const codexRunningItems = runtimeSnapshot.running.slice(0, 4).map((item) => ({
    kind: 'codex' as const,
    status: 'running' as const,
    title: item.summary || 'Codex Live 요청',
    detail: item.startedAt ? `실행 시작 ${item.startedAt}` : null,
    requestId: item.requestId,
    sessionId: item.sessionId,
  }));
  const codexQueuedItems = runtimeSnapshot.queued.slice(0, 4).map((item) => ({
    kind: 'codex' as const,
    status: 'queued' as const,
    title: item.summary || 'Codex Live 요청',
    detail: item.enqueuedAt ? `대기 등록 ${item.enqueuedAt}` : null,
    requestId: item.requestId,
    sessionId: item.sessionId,
  }));

  const boardPosts = await listBoardPosts();
  const automationItems = boardPosts.flatMap((post) =>
    post.requestItems
      .filter((requestItem) => requestItem.status === 'in_progress' || requestItem.status === 'queued' || requestItem.status === 'waiting')
      .slice(0, 4)
      .map((requestItem) => ({
        kind: 'automation' as const,
        status:
          requestItem.status === 'in_progress'
            ? 'running' as const
            : requestItem.status === 'queued'
              ? 'queued' as const
              : 'waiting' as const,
        title: requestItem.title.trim() || post.title.trim() || `자동화 요청 #${requestItem.id}`,
        detail: [post.title.trim() || null, requestItem.statusLabel.trim() || null].filter(Boolean).join(' · ') || null,
        requestId: requestItem.planItemId ? String(requestItem.planItemId) : null,
        sessionId: null,
      })),
  );

  return [...codexRunningItems, ...codexQueuedItems, ...automationItems.slice(0, 6)];
}

function normalizeRunnerUrl(value: string) {
  return value.trim().replace(/\/+$/, '');
}

function buildCommandRunnerApiCandidates(requestPath: string) {
  const configuredHealthUrl = env.SERVER_COMMAND_RUNNER_URL?.trim() || 'http://host.docker.internal:3211/health';

  let parsedUrl: URL;

  try {
    parsedUrl = new URL(configuredHealthUrl);
  } catch {
    return [];
  }

  const hostVariants =
    parsedUrl.hostname === 'host.docker.internal'
      ? ['host.docker.internal', '127.0.0.1', 'localhost']
      : parsedUrl.hostname === '127.0.0.1' || parsedUrl.hostname === 'localhost'
        ? [parsedUrl.hostname, parsedUrl.hostname === '127.0.0.1' ? 'localhost' : '127.0.0.1', 'host.docker.internal']
        : [parsedUrl.hostname];

  const deduped: string[] = [];

  for (const hostname of hostVariants) {
    const candidate = new URL(parsedUrl.toString());
    candidate.hostname = hostname;
    candidate.pathname = requestPath.startsWith('/') ? requestPath : `/${requestPath}`;
    candidate.search = '';
    candidate.hash = '';
    const serialized = normalizeRunnerUrl(candidate.toString());

    if (!deduped.includes(serialized)) {
      deduped.push(serialized);
    }
  }

  return deduped;
}

async function requestCommandRunner(requestPath: string, init?: RequestInit) {
  const headers = new Headers(init?.headers);

  if (init?.body != null && !headers.has('Content-Type')) {
    headers.set('Content-Type', 'application/json');
  }

  if (env.SERVER_COMMAND_RUNNER_ACCESS_TOKEN?.trim() && !headers.has('X-Access-Token')) {
    headers.set('X-Access-Token', env.SERVER_COMMAND_RUNNER_ACCESS_TOKEN.trim());
  }

  let lastError: Error | null = null;

  for (const url of buildCommandRunnerApiCandidates(requestPath)) {
    try {
      return await fetch(url, {
        ...init,
        headers,
      });
    } catch (error) {
      lastError = error instanceof Error ? error : new Error(String(error));
    }
  }

  throw lastError ?? new Error('command-runner에 연결하지 못했습니다.');
}

function buildWaitingReason(target: RestartReservationTarget, summary: RestartReservationWorkloadSummary) {
  if (target === 'work-server') {
    return null;
  }

  const reasons: string[] = [];

  const codexPending = summary.codexRunningCount + summary.codexQueuedCount;
  const automationPending = summary.automationRunningCount + summary.automationQueuedCount;

  if (codexPending > 0) {
    reasons.push(`Codex Live ${codexPending}건`);
  }

  if (automationPending > 0) {
    reasons.push(`자동화 ${automationPending}건`);
  }

  return reasons.length > 0 ? `${reasons.join(', ')} 진행 중이라 재기동을 대기합니다.` : null;
}

function isRestartBuildFailure(error: unknown) {
  const message = error instanceof Error ? error.message : String(error ?? '');
  return /(build|vite|tsc|typescript|npm run build|pnpm build|rollup|esbuild|compilation|compile|exit:\d+)/i.test(message);
}

function buildReservedRestartAutoFixPrompt(args: {
  targetKey: 'test' | 'work-server';
  failureMessage: string;
}) {
  const repoPath = resolveMainProjectRoot();
  const targetLabel = args.targetKey === 'test' ? 'TEST 앱' : 'WORK-SERVER';

  return [
    `당신은 ${repoPath} 저장소에서 ${targetLabel} 재기동 빌드 실패를 자동 복구하는 Codex 실행기입니다.`,
    '반드시 저장소 루트의 AGENTS.md를 먼저 읽고 그 규칙을 따르세요.',
    '목표는 현재 로컬 main 기준으로 재기동을 막는 빌드 오류를 직접 수정하는 것입니다.',
    '필요한 범위만 수정하고, 불필요한 Git 작업은 하지 마세요.',
    `현재 실패 대상: ${targetLabel}`,
    '실패 로그:',
    args.failureMessage,
    '작업 지시:',
    '1. 빌드 실패 원인을 확인합니다.',
    '2. 현재 저장소에서 직접 수정합니다.',
    '3. 대상 서버 재기동을 막는 빌드 오류가 해결되었는지 관련 빌드/검증 명령으로 확인합니다.',
    '4. 최종 답변은 한국어로 간결하게 작성합니다.',
  ].join('\n');
}

async function updateReservationAutoFixState(patch: Partial<RestartReservationAutoFix>) {
  const row = await readReservationRow();
  const current = parseAutoFixState(row?.auto_fix_json ?? null);
  const nextState: RestartReservationAutoFix = {
    ...current,
    ...patch,
  };

  return updateReservationRow({
    auto_fix_json: nextState,
  });
}

async function runReservedRestartAutoFix(
  logger: FastifyBaseLogger,
  args: {
    targetKey: 'test' | 'work-server';
    failureMessage: string;
  },
) {
  const repoPath = resolveMainProjectRoot();
  const requestId = `server-restart-fix-${args.targetKey}-${Date.now().toString(36)}`;
  const sessionId = RESERVED_RESTART_AUTO_FIX_SESSION_ID;
  const prompt = buildReservedRestartAutoFixPrompt(args);

  await updateReservationAutoFixState({
    enabled: true,
    targetKey: args.targetKey,
    requestId,
    sessionId,
    status: 'queued',
    summary: `${args.targetKey.toUpperCase()} 빌드 오류 자동 개선 요청을 준비 중입니다.`,
    detail: args.failureMessage,
    requestedAt: new Date().toISOString(),
    startedAt: null,
    completedAt: null,
    failedAt: null,
  });

  const response = await requestCommandRunner('/api/codex-live/execute', {
    method: 'POST',
    body: JSON.stringify({
      requestId,
      sessionId,
      repoPath,
      prompt,
      resourceDir: path.join(
        repoPath,
        'public',
        '.codex_chat',
        sessionId,
        'resource',
      ),
      uploadDir: path.join(
        repoPath,
        'public',
        '.codex_chat',
        sessionId,
        'resource',
        'uploads',
      ),
      maxExecutionSeconds: RESERVED_RESTART_AUTO_FIX_MAX_EXECUTION_SECONDS,
      idleTimeoutSeconds: RESERVED_RESTART_AUTO_FIX_IDLE_TIMEOUT_SECONDS,
    }),
  });

  if (!response.ok || !response.body) {
    const message = (await response.text().catch(() => '')) || 'Codex 자동 개선 요청을 시작하지 못했습니다.';
    await updateReservationAutoFixState({
      enabled: true,
      targetKey: args.targetKey,
      requestId,
      sessionId,
      status: 'failed',
      summary: `${args.targetKey.toUpperCase()} 자동 개선 요청 시작 실패`,
      detail: message,
      failedAt: new Date().toISOString(),
    });
    throw new Error(message);
  }

  const decoder = new TextDecoder();
  const reader = response.body.getReader();
  let buffer = '';
  let completedText = '';
  let remoteError = '';

  while (true) {
    const { value, done } = await reader.read();

    if (done) {
      break;
    }

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop() ?? '';

    for (const rawLine of lines) {
      const line = rawLine.trim();

      if (!line) {
        continue;
      }

      let event: Record<string, unknown>;

      try {
        event = JSON.parse(line) as Record<string, unknown>;
      } catch {
        continue;
      }

      const type = typeof event.type === 'string' ? event.type : '';

      if (type === 'started') {
        await updateReservationAutoFixState({
          enabled: true,
          targetKey: args.targetKey,
          requestId,
          sessionId,
          status: 'running',
          summary: `${args.targetKey.toUpperCase()} 빌드 오류를 Codex가 분석 중입니다.`,
          startedAt: new Date().toISOString(),
        });
        continue;
      }

      if (type === 'activity' || type === 'stdout' || type === 'stderr') {
        const lineText = String(event.line ?? '').trim();

        if (lineText) {
          await updateReservationAutoFixState({
            enabled: true,
            targetKey: args.targetKey,
            requestId,
            sessionId,
            status: 'running',
            summary: `${args.targetKey.toUpperCase()} 빌드 오류 자동 개선 진행 중`,
            detail: lineText,
          });
        }
        continue;
      }

      if (type === 'completed') {
        completedText = String(event.text ?? '').trim();
        continue;
      }

      if (type === 'error') {
        remoteError = String(event.message ?? '').trim();
      }
    }
  }

  if (remoteError) {
    await updateReservationAutoFixState({
      enabled: true,
      targetKey: args.targetKey,
      requestId,
      sessionId,
      status: 'failed',
      summary: `${args.targetKey.toUpperCase()} 자동 개선 실패`,
      detail: remoteError,
      failedAt: new Date().toISOString(),
    });
    throw new Error(remoteError);
  }

  await updateReservationAutoFixState({
    enabled: true,
    targetKey: args.targetKey,
    requestId,
    sessionId,
    status: 'completed',
    summary: `${args.targetKey.toUpperCase()} 빌드 오류 자동 개선을 마쳤습니다.`,
    detail: completedText || 'Codex 자동 개선이 완료되었습니다. 재기동을 다시 시도합니다.',
    completedAt: new Date().toISOString(),
  });

  logger.info({ requestId, targetKey: args.targetKey }, 'Reserved restart auto fix completed');

  return {
    requestId,
    sessionId,
    completedText,
  };
}

async function listActiveClients() {
  await ensureVisitorHistoryTables();
  const visitors = await listVisitorClients(50);
  const cutoffTime = Date.now() - ACTIVE_CLIENT_WINDOW_MS;

  return visitors.filter((item) => {
    const lastVisitedAt = Date.parse(item.lastVisitedAt);
    return Number.isFinite(lastVisitedAt) && lastVisitedAt >= cutoffTime;
  });
}

function waitForDuration(durationMs: number) {
  return new Promise<void>((resolve) => {
    setTimeout(resolve, durationMs);
  });
}

function resolveAutoExecuteDelaySeconds(rawDelaySeconds: number | null | undefined) {
  if (!Number.isFinite(rawDelaySeconds)) {
    return 10;
  }

  return Math.min(300, Math.max(1, Math.round(rawDelaySeconds ?? 10)));
}

function buildAutoExecuteAt(baseTimestamp: string | null | undefined, autoExecuteDelaySeconds: number) {
  const baseTime = Date.parse(baseTimestamp ?? '');

  if (!Number.isFinite(baseTime)) {
    return null;
  }

  return new Date(baseTime + autoExecuteDelaySeconds * 1000).toISOString();
}

async function resolveReservationAutoExecuteDelaySeconds(row: RestartReservationRow) {
  if (Number.isFinite(row.auto_execute_delay_seconds)) {
    return resolveAutoExecuteDelaySeconds(row.auto_execute_delay_seconds);
  }

  const appConfig = await getAppConfigSnapshot(row.app_origin ?? undefined);
  return resolveAutoExecuteDelaySeconds(appConfig.chat?.restartReservationCompletionDelaySeconds ?? 10);
}

function isReservationAutoExecuteDue(row: RestartReservationRow) {
  const autoExecuteTime = Date.parse(row.auto_execute_at ?? '');

  if (!Number.isFinite(autoExecuteTime)) {
    return false;
  }

  return Date.now() >= autoExecuteTime;
}

function isReservationCheckDue(row: RestartReservationRow) {
  const baseTimestamp = row.last_checked_at ?? row.requested_at ?? row.updated_at;

  if (!baseTimestamp) {
    return true;
  }

  const baseTime = Date.parse(baseTimestamp);

  if (!Number.isFinite(baseTime)) {
    return true;
  }

  return Date.now() - baseTime >= SERVER_RESTART_RESERVATION_POLL_INTERVAL_MS;
}

function hasRestartStartedAfterReservation(
  serverStartedAt: string | null | undefined,
  reservationStartedAt: string | null | undefined,
) {
  const serverStartedTime = Date.parse(serverStartedAt ?? '');
  const reservationStartedTime = Date.parse(reservationStartedAt ?? '');

  if (!Number.isFinite(serverStartedTime) || !Number.isFinite(reservationStartedTime)) {
    return false;
  }

  return serverStartedTime + RESTART_VERIFICATION_CLOCK_SKEW_MS >= reservationStartedTime;
}

function hasRuntimeMarkerAfterReservation(
  runtimeMarkerAt: string | null | undefined,
  reservationStartedAt: string | null | undefined,
) {
  const runtimeMarkerTime = Date.parse(runtimeMarkerAt ?? '');
  const reservationStartedTime = Date.parse(reservationStartedAt ?? '');

  if (!Number.isFinite(runtimeMarkerTime) || !Number.isFinite(reservationStartedTime)) {
    return false;
  }

  return runtimeMarkerTime + RESTART_VERIFICATION_CLOCK_SKEW_MS >= reservationStartedTime;
}

function getWorkServerRuntimeMarkerAt(
  server: Pick<ServerCommandSnapshot, 'runningBuiltAt' | 'runningVersion'>,
) {
  if (server.runningBuiltAt?.trim()) {
    return server.runningBuiltAt;
  }

  const versionText = server.runningVersion?.trim() ?? '';
  const versionMarker = versionText.includes('@') ? versionText.split('@').at(-1)?.trim() ?? '' : '';
  return versionMarker || null;
}

export function hasReservedRestartVerification(
  key: 'test' | 'work-server',
  server: Pick<
    ServerCommandSnapshot,
    'availability' | 'startedAt' | 'runningBuiltAt' | 'runningVersion' | 'buildRequired' | 'updateAvailable'
  > | null | undefined,
  reservationStartedAt: string | null | undefined,
) {
  if (!server || server.availability !== 'online' || !hasRestartStartedAfterReservation(server.startedAt, reservationStartedAt)) {
    return false;
  }

  if (key === 'test') {
    return hasRuntimeMarkerAfterReservation(server.runningBuiltAt, reservationStartedAt);
  }

  return hasRuntimeMarkerAfterReservation(getWorkServerRuntimeMarkerAt(server), reservationStartedAt);
}

async function finalizeReservedRestart(row: RestartReservationRow) {
  const statuses = await listServerCommands();
  const testServer = statuses.find((item) => item.key === 'test') ?? null;
  const workServer = statuses.find((item) => item.key === 'work-server') ?? null;
  const target = normalizeReservationTarget(row.target);
  const targetKeys = getReservationTargetKeys(target);
  const verificationResults = {
    test: targetKeys.includes('test') ? hasReservedRestartVerification('test', testServer, row.started_at) : true,
    'work-server': targetKeys.includes('work-server')
      ? hasReservedRestartVerification('work-server', workServer, row.started_at)
      : true,
  };

  if (!verificationResults.test || !verificationResults['work-server']) {
    const waitingTargets = [
      !verificationResults.test ? 'TEST 서버' : null,
      !verificationResults['work-server'] ? 'WORK 서버' : null,
    ].filter((value): value is string => Boolean(value));

    await updateReservationRow({
      enabled: true,
      status: 'executing',
      execution_phase: 'verify-runtime',
      last_checked_at: db.fn.now(),
      waiting_reason: `${waitingTargets.join(' / ')} 새 런타임과 정상 기동을 확인하는 중입니다.`,
      last_error: null,
    });
    return mapReservationRow(row);
  }

  const nextRow = await updateReservationRow({
    enabled: false,
    status: 'completed',
    completed_at: db.fn.now(),
    waiting_reason: null,
    workload_summary_json: getDefaultWorkloadSummary(),
    last_error: null,
    last_checked_at: db.fn.now(),
    auto_execute_at: null,
    execution_phase: 'idle',
  });

  return mapReservationRow(nextRow);
}

async function restartReservedTargetWithRecovery(
  logger: FastifyBaseLogger,
  targetKey: 'test' | 'work-server',
  startMessage: string,
) {
  await updateReservationRow({
    enabled: true,
    status: 'executing',
    execution_phase: targetKey === 'test' ? 'restart-test' : 'restart-work-server',
    waiting_reason: startMessage,
    last_checked_at: db.fn.now(),
  });

  try {
    await restartServerCommand(targetKey);
    return;
  } catch (error) {
    const message = error instanceof Error ? error.message : '재기동에 실패했습니다.';

    if (!isRestartBuildFailure(error)) {
      throw error;
    }

    logger.warn({ err: error, targetKey }, 'Reserved restart build failure detected, requesting Codex auto fix');
    await updateReservationRow({
      enabled: true,
      status: 'recovering',
      waiting_reason: `${targetKey.toUpperCase()} 재기동 빌드 실패를 감지해 Codex 자동 개선을 시작합니다.`,
      last_checked_at: db.fn.now(),
      last_error: message,
    });

    await runReservedRestartAutoFix(logger, {
      targetKey,
      failureMessage: message,
    });

    await updateReservationRow({
      enabled: true,
      status: 'executing',
      execution_phase: targetKey === 'test' ? 'restart-test' : 'restart-work-server',
      waiting_reason: `${targetKey.toUpperCase()} 빌드 오류를 수정해 재기동을 다시 시도합니다.`,
      last_checked_at: db.fn.now(),
      last_error: null,
    });

    await restartServerCommand(targetKey);
  }
}

async function finalizeSingleServerRestart(targetKey: 'test' | 'work-server') {
  const nextRow = await updateReservationRow({
    enabled: false,
    target: targetKey,
    status: 'completed',
    completed_at: db.fn.now(),
    waiting_reason: null,
    workload_summary_json: getDefaultWorkloadSummary(),
    last_error: null,
    last_checked_at: db.fn.now(),
    auto_execute_at: null,
    execution_phase: 'idle',
  });

  return mapReservationRow(nextRow);
}

let immediateRecoveryPromise: Promise<void> | null = null;

export async function requestImmediateRestartRecovery(
  logger: FastifyBaseLogger,
  targetKey: 'test' | 'work-server',
  failureMessage: string,
) {
  await updateReservationRow({
    enabled: true,
    target: targetKey,
    status: 'recovering',
    requested_at: db.fn.now(),
    requested_by_client_id: null,
    waiting_reason: `${targetKey.toUpperCase()} 재기동 빌드 실패를 감지해 Codex 자동 개선을 시작합니다.`,
    workload_summary_json: getDefaultWorkloadSummary(),
    started_at: db.fn.now(),
    completed_at: null,
    cancelled_at: null,
    last_error: failureMessage,
    active_client_count: 0,
    notified_active_clients_at: null,
    app_origin: null,
    auto_execute_at: null,
    auto_execute_delay_seconds: 10,
    last_checked_at: db.fn.now(),
    execution_phase: targetKey === 'test' ? 'restart-test' : 'restart-work-server',
    auto_fix_json: getDefaultAutoFixState(),
  });

  if (immediateRecoveryPromise) {
    return getServerRestartReservation();
  }

  immediateRecoveryPromise = (async () => {
    try {
      await restartReservedTargetWithRecovery(
        logger,
        targetKey,
        `${targetKey.toUpperCase()} 빌드 오류를 자동 수정한 뒤 재기동을 다시 시도합니다.`,
      );
      await finalizeSingleServerRestart(targetKey);
    } catch (error) {
      const message = error instanceof Error ? error.message : '즉시 재기동 자동 복구에 실패했습니다.';
      logger.error({ err: error, targetKey }, 'Immediate restart recovery failed');
      await updateReservationRow({
        enabled: false,
        target: targetKey,
        status: 'failed',
        waiting_reason: null,
        last_error: message,
        last_checked_at: db.fn.now(),
        execution_phase: 'idle',
      }).catch(() => undefined);
    } finally {
      immediateRecoveryPromise = null;
    }
  })();

  return getServerRestartReservation();
}

async function executeReservedRestart(logger: FastifyBaseLogger, row: RestartReservationRow) {
  const target = normalizeReservationTarget(row.target);
  const targetLabel = getReservationTargetLabel(target);
  const targetKeys = getReservationTargetKeys(target);
  const activeClients = await listActiveClients();
  await updateReservationRow({
    enabled: true,
    status: 'executing',
    started_at: row.started_at ?? db.fn.now(),
    last_checked_at: db.fn.now(),
    execution_phase: 'commit-main-worktree',
    waiting_reason: `${targetLabel} 무중단 재기동을 위해 main 작업트리 상태를 확인합니다.`,
    active_client_count: activeClients.length,
    last_error: null,
  });

  if (activeClients.length > 0) {
    await createNotificationMessage({
      title: '예약된 재기동 시작',
      body: `활성 클라이언트 ${activeClients.length}건이 감지되어 예약된 ${targetLabel} 무중단 재기동을 시작합니다. 잠시 후 화면이 새로고침될 수 있습니다.`,
      category: 'system',
      source: SERVER_RESTART_RESERVATION_NOTIFICATION_SOURCE,
      priority: 'high',
      metadata: {
        previewText: `${targetLabel} 재기동 시작 · 활성 클라이언트 ${activeClients.length}건`,
        linkUrl: '/?topMenu=plans',
        linkLabel: '작업 화면 열기',
      },
    });
    await deleteOlderNotificationMessagesBySource(SERVER_RESTART_RESERVATION_NOTIFICATION_SOURCE, 1);

    await updateReservationRow({
      notified_active_clients_at: db.fn.now(),
      active_client_count: activeClients.length,
    });
  }

  logger.info(
    {
      requestedAt: row.requested_at,
      requestedByClientId: row.requested_by_client_id,
      activeClientCount: activeClients.length,
    },
    'Executing reserved restart',
  );

  const syncResult = await syncReservedRestartMainProject(logger);

  await updateReservationRow({
    enabled: true,
    status: 'executing',
    execution_phase: targetKeys.includes('test') ? 'restart-test' : 'restart-work-server',
    waiting_reason: syncResult.committed
      ? `main 변경을 정리한 뒤 ${targetLabel} 재기동을 시작합니다.`
      : `main 작업트리 상태를 확인한 뒤 ${targetLabel} 재기동을 시작합니다.`,
    last_checked_at: db.fn.now(),
  });

  if (targetKeys.includes('test')) {
    await restartReservedTargetWithRecovery(logger, 'test', '예약된 자동 실행 시간이 되어 TEST 서버 재기동을 시작합니다.');
  }

  if (targetKeys.includes('test') && targetKeys.includes('work-server')) {
    await waitForDuration(TEST_TO_WORK_SERVER_DELAY_MS);
  }

  if (targetKeys.includes('work-server')) {
    await updateReservationRow({
      enabled: true,
      status: 'executing',
      execution_phase: 'restart-work-server',
      waiting_reason: target === 'work-server'
        ? 'WORK 서버 무중단 재기동 후 정상 기동을 확인하는 중입니다.'
        : 'WORK 서버 재기동 후 정상 기동을 확인하는 중입니다.',
      last_checked_at: db.fn.now(),
    });

    await restartReservedTargetWithRecovery(logger, 'work-server', '예약된 흐름에 따라 WORK 서버 재기동을 시작합니다.');
  }

  await updateReservationRow({
    enabled: true,
    status: 'executing',
    execution_phase: 'verify-runtime',
    waiting_reason: `${targetLabel} 새 런타임과 정상 기동을 확인하는 중입니다.`,
    last_checked_at: db.fn.now(),
  });
}

export async function getServerRestartReservation() {
  const row = await readReservationRow();
  const autoFix = parseAutoFixState(row?.auto_fix_json ?? null);
  const shouldExposeWorkItems =
    Boolean(row?.enabled)
    || row?.status === 'waiting'
    || row?.status === 'ready'
    || row?.status === 'executing'
    || row?.status === 'recovering'
    || autoFix.enabled;
  const workItems = shouldExposeWorkItems ? await listRestartReservationWorkItems() : [];
  return mapReservationRow(row, { workItems });
}

export async function scheduleServerRestartReservation(options?: {
  target?: RestartReservationTarget | null;
  clientId?: string | null;
  appOrigin?: string | null;
  autoExecuteDelaySeconds?: number | null;
}) {
  const autoExecuteDelaySeconds = resolveAutoExecuteDelaySeconds(options?.autoExecuteDelaySeconds);
  const target = normalizeReservationTarget(options?.target);
  const row = await updateReservationRow({
    enabled: true,
    target,
    status: 'waiting',
    requested_at: db.fn.now(),
    requested_by_client_id: options?.clientId?.trim() || null,
    last_checked_at: null,
    waiting_reason: target === 'work-server'
      ? 'WORK 서버 무중단 재기동 가능 여부를 확인하는 중입니다.'
      : '진행 중인 Codex Live / 자동화 작업을 확인하는 중입니다.',
    workload_summary_json: getDefaultWorkloadSummary(),
    started_at: null,
    completed_at: null,
    cancelled_at: null,
    last_error: null,
    active_client_count: 0,
    notified_active_clients_at: null,
    app_origin: options?.appOrigin?.trim() || null,
    auto_execute_at: null,
    auto_execute_delay_seconds: autoExecuteDelaySeconds,
    execution_phase: 'idle',
    auto_fix_json: getDefaultAutoFixState(),
  });

  return mapReservationRow(row);
}

export async function cancelServerRestartReservation() {
  const row = await updateReservationRow({
    enabled: false,
    status: 'cancelled',
    cancelled_at: db.fn.now(),
    waiting_reason: null,
    workload_summary_json: getDefaultWorkloadSummary(),
    active_client_count: 0,
    last_error: null,
    auto_execute_at: null,
    execution_phase: 'idle',
    auto_fix_json: getDefaultAutoFixState(),
  });

  return mapReservationRow(row);
}

export async function confirmServerRestartReservation(logger: FastifyBaseLogger) {
  const row = await readReservationRow();

  if (!row?.enabled) {
    return mapReservationRow(row);
  }

  if (row.status === 'executing' || row.status === 'completed') {
    return mapReservationRow(row);
  }

  if (row.status !== 'ready') {
    const error = new Error('재기동 예약이 아직 실행 준비 상태가 아닙니다.');
    (error as Error & { statusCode?: number }).statusCode = 409;
    throw error;
  }

  const nextRow = await updateReservationRow({
    enabled: true,
    status: 'executing',
    started_at: db.fn.now(),
    last_checked_at: db.fn.now(),
    waiting_reason: `${getReservationTargetLabel(normalizeReservationTarget(row.target))} 재기동 준비를 시작합니다.`,
    last_error: null,
    auto_execute_at: null,
    execution_phase: 'commit-main-worktree',
    auto_fix_json: getDefaultAutoFixState(),
  });

  if (!nextRow) {
    throw new Error('재기동 예약 상태를 갱신하지 못했습니다.');
  }

  void executeReservedRestart(logger, nextRow).catch(async (error) => {
    const message = error instanceof Error ? error.message : '예약된 재기동 처리에 실패했습니다.';
    logger.error({ err: error }, 'Confirmed reserved restart failed');
    await updateReservationRow({
      enabled: false,
      status: 'failed',
      last_error: message,
      waiting_reason: null,
      execution_phase: 'idle',
    }).catch(() => undefined);
  });

  return mapReservationRow(nextRow);
}

export class ServerRestartReservationWorker {
  private timer: ReturnType<typeof setInterval> | null = null;
  private running = false;

  constructor(private readonly logger: FastifyBaseLogger) {}

  start() {
    if (this.timer) {
      return;
    }

    void this.tick();
    this.timer = setInterval(() => {
      void this.tick();
    }, SERVER_RESTART_RESERVATION_POLL_INTERVAL_MS);
  }

  async stop() {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  }

  private async tick() {
    if (this.running) {
      return;
    }

    this.running = true;

    try {
      if (!(await isCurrentWorkServerSlotActive())) {
        return;
      }

      const row = await readReservationRow();

      if (!row?.enabled) {
        return;
      }

      if (row.status === 'recovering') {
        return;
      }

      if (row.status === 'executing' && row.started_at) {
        await finalizeReservedRestart(row);
        return;
      }

      if (!isReservationCheckDue(row)) {
        return;
      }

      const workloadSummary = await getRestartReservationWorkloadSummary();
      const waitingReason = buildWaitingReason(normalizeReservationTarget(row.target), workloadSummary);

      if (!waitingReason && row.status === 'ready' && isReservationAutoExecuteDue(row)) {
        await confirmServerRestartReservation(this.logger);
        return;
      }

      const autoExecuteDelaySeconds = await resolveReservationAutoExecuteDelaySeconds(row);
      const targetLabel = getReservationTargetLabel(normalizeReservationTarget(row.target));
      const autoExecuteAt = buildAutoExecuteAt(
        row.status === 'ready' && row.auto_execute_at ? row.auto_execute_at : new Date().toISOString(),
        autoExecuteDelaySeconds,
      );

      await updateReservationRow({
        status: waitingReason ? 'waiting' : 'ready',
        last_checked_at: db.fn.now(),
        waiting_reason: waitingReason
          ?? `진행 중인 작업이 모두 끝났습니다. ${autoExecuteDelaySeconds}초 뒤 ${targetLabel} 무중단 재기동을 자동 시작합니다.`,
        workload_summary_json: workloadSummary,
        auto_execute_at: waitingReason
          ? null
          : row.status === 'ready' && row.auto_execute_at
            ? row.auto_execute_at
            : autoExecuteAt,
        auto_execute_delay_seconds: autoExecuteDelaySeconds,
        execution_phase: 'idle',
      });

      if (waitingReason) {
        return;
      }
    } catch (error) {
      const message = error instanceof Error ? error.message : '예약된 재기동 처리에 실패했습니다.';
      this.logger.error({ err: error }, 'Reserved restart processing failed');
      await updateReservationRow({
        enabled: false,
        status: 'failed',
        last_error: message,
        waiting_reason: null,
        execution_phase: 'idle',
      }).catch(() => undefined);
    } finally {
      this.running = false;
    }
  }
}
