import { z } from 'zod';
import { db } from '../db/client.js';
import { resolveAutomationType, resolveStoredAutomationTypeId } from './automation-type-config-service.js';
import { createBoardPost, receiveBoardPostAutomation } from './board-service.js';
import {
  buildManagedScheduleServiceMetadata,
  hasManagedScheduleServicePackage,
  prepareManagedScheduleServiceDirectory,
  removeManagedScheduleServiceArtifacts,
  runManagedScheduleService,
} from './managed-schedule-service.js';
import {
  ensureSchedulePromptSnapshot,
  parseAutomationContextIds,
  stringifyAutomationContextIds,
} from './automation-context-service.js';
import {
  createCompletedPlanExecutionLogItem,
  createPlanSourceWorkHistory,
  createPlanActionHistory,
  ensurePlanTable,
  planAutomationTypeSchema,
  PLAN_TABLE,
} from './plan-service.js';
import { getKstNowParts } from './worklog-automation-utils.js';

export const PLAN_SCHEDULED_TASK_TABLE = 'plan_scheduled_tasks';
const PLAN_SCHEDULE_REGISTRATION_ADVISORY_LOCK_NAMESPACE = 741_205_262;
const scheduleModes = ['interval', 'daily'] as const;
const repeatIntervalUnits = ['second', 'minute', 'hour', 'day', 'week', 'month'] as const;
const scheduleExecutionModes = ['codex', 'managed-service'] as const;
const DEFAULT_DAILY_RUN_TIME = '09:00';
const TIME_OF_DAY_PATTERN = /^([01]\d|2[0-3]):[0-5]\d$/;
const DATE_KEY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
const MAX_REPEAT_INTERVAL_SECONDS = 31_536_000;
const DEFAULT_REPEAT_INTERVAL_SECONDS = 60 * 60;

const scheduleDateRangeSchema = z
  .object({
    startDate: z.string().regex(DATE_KEY_PATTERN),
    endDate: z.string().regex(DATE_KEY_PATTERN),
  })
  .superRefine((value, context) => {
    if (value.startDate > value.endDate) {
      context.addIssue({
        code: z.ZodIssueCode.custom,
        message: '시작일은 종료일보다 늦을 수 없습니다.',
        path: ['endDate'],
      });
    }
  });

const scheduleWeekdaySchema = z.number().int().min(0).max(6);
const scheduleTimeWindowSchema = z
  .object({
    startTime: z.string().regex(TIME_OF_DAY_PATTERN).nullable().optional().default(null),
    endTime: z.string().regex(TIME_OF_DAY_PATTERN).nullable().optional().default(null),
  })
  .refine((value) => Boolean(value.startTime || value.endTime), {
    message: '시작시간 또는 종료시간 중 하나는 입력해야 합니다.',
  });

export const createPlanScheduledTaskSchema = z.object({
  workId: z.string().trim().optional().default('반복작업'),
  note: z.string().default(''),
  automationType: z.preprocess((value) => value, planAutomationTypeSchema.default('none')),
  automationContextIds: z.array(z.string().trim().min(1).max(160)).max(20).optional().default([]),
  releaseTarget: z.string().trim().min(1).default('release'),
  jangsingProcessingRequired: z.boolean().default(true),
  autoDeployToMain: z.boolean().default(true),
  suppressWebPush: z.boolean().default(false),
  enabled: z.boolean().default(true),
  immediateRunEnabled: z.boolean().default(true),
  refreshContextSnapshotOnNextRun: z.boolean().default(false),
  executionMode: z.enum(scheduleExecutionModes).default('codex'),
  recreateManagedServiceOnNextSave: z.boolean().default(false),
  scheduleMode: z.enum(scheduleModes).default('interval'),
  repeatIntervalValue: z.coerce.number().int().min(1).max(MAX_REPEAT_INTERVAL_SECONDS).default(60),
  repeatIntervalUnit: z.enum(repeatIntervalUnits).default('minute'),
  repeatIntervalMinutes: z.coerce.number().int().min(1).max(525600).optional(),
  repeatIntervalSeconds: z.coerce.number().int().min(1).max(MAX_REPEAT_INTERVAL_SECONDS).optional(),
  dailyRunTime: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).default(DEFAULT_DAILY_RUN_TIME),
  scheduleWeekdays: z.array(scheduleWeekdaySchema).max(7).optional().default([]),
  scheduleDateRanges: z.array(scheduleDateRangeSchema).max(40).optional().default([]),
  repeatWindows: z.array(scheduleTimeWindowSchema).max(24).optional().default([]),
  repeatWindowStartTime: z.string().regex(TIME_OF_DAY_PATTERN).nullable().optional().default(null),
  repeatWindowEndTime: z.string().regex(TIME_OF_DAY_PATTERN).nullable().optional().default(null),
});

export const updatePlanScheduledTaskSchema = z.object({
  workId: z.string().trim().optional(),
  note: z.string().optional(),
  automationType: z.preprocess((value) => value, planAutomationTypeSchema.optional()),
  automationContextIds: z.array(z.string().trim().min(1).max(160)).max(20).optional(),
  releaseTarget: z.string().trim().min(1).optional(),
  jangsingProcessingRequired: z.boolean().optional(),
  autoDeployToMain: z.boolean().optional(),
  suppressWebPush: z.boolean().optional(),
  enabled: z.boolean().optional(),
  immediateRunEnabled: z.boolean().optional(),
  refreshContextSnapshotOnNextRun: z.boolean().optional(),
  executionMode: z.enum(scheduleExecutionModes).optional(),
  recreateManagedServiceOnNextSave: z.boolean().optional(),
  scheduleMode: z.enum(scheduleModes).optional(),
  repeatIntervalValue: z.coerce.number().int().min(1).max(MAX_REPEAT_INTERVAL_SECONDS).optional(),
  repeatIntervalUnit: z.enum(repeatIntervalUnits).optional(),
  repeatIntervalMinutes: z.coerce.number().int().min(1).max(525600).optional(),
  repeatIntervalSeconds: z.coerce.number().int().min(1).max(MAX_REPEAT_INTERVAL_SECONDS).optional(),
  dailyRunTime: z.string().regex(TIME_OF_DAY_PATTERN).optional(),
  scheduleWeekdays: z.array(scheduleWeekdaySchema).max(7).optional(),
  scheduleDateRanges: z.array(scheduleDateRangeSchema).max(40).optional(),
  repeatWindows: z.array(scheduleTimeWindowSchema).max(24).optional(),
  repeatWindowStartTime: z.string().regex(TIME_OF_DAY_PATTERN).nullable().optional(),
  repeatWindowEndTime: z.string().regex(TIME_OF_DAY_PATTERN).nullable().optional(),
});

function normalizeScheduledWorkId(value?: string | null) {
  const workId = String(value ?? '').trim();
  const normalized = workId.replace(/^\[|\]$/g, '').replace(/\s+/g, '').toLowerCase();

  if (!workId || normalized === '작업id' || normalized === 'workid' || normalized === 'undefined' || normalized === 'null') {
    return '반복작업';
  }

  return workId;
}

function normalizeRepeatIntervalMinutes(value: unknown) {
  const numericValue = Number(value);

  if (!Number.isFinite(numericValue)) {
    return 60;
  }

  return Math.min(525600, Math.max(1, Math.round(numericValue)));
}

function normalizeRepeatIntervalValue(value: unknown) {
  const numericValue = Number(value);

  if (!Number.isFinite(numericValue)) {
    return 60;
  }

  return Math.min(MAX_REPEAT_INTERVAL_SECONDS, Math.max(1, Math.round(numericValue)));
}

function normalizeRepeatIntervalSeconds(value: unknown) {
  const numericValue = Number(value);

  if (!Number.isFinite(numericValue)) {
    return DEFAULT_REPEAT_INTERVAL_SECONDS;
  }

  return Math.min(MAX_REPEAT_INTERVAL_SECONDS, Math.max(1, Math.round(numericValue)));
}

function normalizeRepeatIntervalUnit(value: unknown): (typeof repeatIntervalUnits)[number] {
  return repeatIntervalUnits.includes(value as (typeof repeatIntervalUnits)[number])
    ? (value as (typeof repeatIntervalUnits)[number])
    : 'minute';
}

function normalizeScheduleMode(value: unknown): (typeof scheduleModes)[number] {
  return scheduleModes.includes(value as (typeof scheduleModes)[number])
    ? (value as (typeof scheduleModes)[number])
    : 'interval';
}

function normalizeDailyRunTime(value: unknown) {
  return typeof value === 'string' && TIME_OF_DAY_PATTERN.test(value)
    ? value
    : DEFAULT_DAILY_RUN_TIME;
}

function normalizeOptionalTimeOfDay(value: unknown) {
  return typeof value === 'string' && TIME_OF_DAY_PATTERN.test(value) ? value : null;
}

function normalizeDateKey(value: unknown) {
  const trimmedValue = typeof value === 'string' ? value.trim() : '';
  return DATE_KEY_PATTERN.test(trimmedValue) ? trimmedValue : null;
}

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

  const normalizedRanges = value
    .map((item) =>
      scheduleDateRangeSchema.safeParse({
        startDate: normalizeDateKey((item as { startDate?: unknown })?.startDate),
        endDate: normalizeDateKey((item as { endDate?: unknown })?.endDate),
      }),
    )
    .filter((result) => result.success)
    .map((result) => result.data)
    .sort((left, right) =>
      left.startDate === right.startDate
        ? left.endDate.localeCompare(right.endDate)
        : left.startDate.localeCompare(right.startDate),
    );

  const uniqueRanges = [];
  const seen = new Set<string>();

  for (const range of normalizedRanges) {
    const key = `${range.startDate}:${range.endDate}`;

    if (seen.has(key)) {
      continue;
    }

    seen.add(key);
    uniqueRanges.push(range);
  }

  return uniqueRanges;
}

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

  return Array.from(
    new Set(
      value
        .map((item) => Number(item))
        .filter((item) => Number.isInteger(item) && item >= 0 && item <= 6),
    ),
  ).sort((left, right) => left - right);
}

function parseScheduleWeekdays(value: unknown) {
  if (typeof value !== 'string' || !value.trim()) {
    return [];
  }

  try {
    return normalizeScheduleWeekdays(JSON.parse(value));
  } catch {
    return [];
  }
}

function stringifyScheduleWeekdays(value: unknown) {
  return JSON.stringify(normalizeScheduleWeekdays(value));
}

function parseScheduleDateRanges(value: unknown) {
  if (typeof value !== 'string' || !value.trim()) {
    return [];
  }

  try {
    return normalizeScheduleDateRanges(JSON.parse(value));
  } catch {
    return [];
  }
}

function stringifyScheduleDateRanges(value: unknown) {
  return JSON.stringify(normalizeScheduleDateRanges(value));
}

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

  const normalizedWindows = value
    .map((item) =>
      scheduleTimeWindowSchema.safeParse({
        startTime: normalizeOptionalTimeOfDay((item as { startTime?: unknown })?.startTime),
        endTime: normalizeOptionalTimeOfDay((item as { endTime?: unknown })?.endTime),
      }),
    )
    .filter((result) => result.success)
    .map((result) => result.data)
    .sort((left, right) => {
      const leftKey = `${left.startTime ?? ''}:${left.endTime ?? ''}`;
      const rightKey = `${right.startTime ?? ''}:${right.endTime ?? ''}`;
      return leftKey.localeCompare(rightKey);
    });

  const uniqueWindows = [];
  const seen = new Set<string>();

  for (const window of normalizedWindows) {
    const key = `${window.startTime ?? ''}:${window.endTime ?? ''}`;

    if (seen.has(key)) {
      continue;
    }

    seen.add(key);
    uniqueWindows.push(window);
  }

  return uniqueWindows;
}

function parseScheduleTimeWindows(value: unknown) {
  if (typeof value !== 'string' || !value.trim()) {
    return [];
  }

  try {
    return normalizeScheduleTimeWindows(JSON.parse(value));
  } catch {
    return [];
  }
}

function stringifyScheduleTimeWindows(value: unknown) {
  return JSON.stringify(normalizeScheduleTimeWindows(value));
}

function toMinutesOfDay(value: string) {
  const [hours, minutes] = value.split(':').map((part) => Number(part));
  return hours * 60 + minutes;
}

function resolveScheduleTimeWindows(row: Record<string, unknown>) {
  const storedWindows = parseScheduleTimeWindows(row.repeat_windows_json);

  if (storedWindows.length) {
    return storedWindows;
  }

  const startTime = normalizeOptionalTimeOfDay(row.repeat_window_start_time);
  const endTime = normalizeOptionalTimeOfDay(row.repeat_window_end_time);

  return startTime || endTime
    ? [
        {
          startTime,
          endTime,
        },
      ]
    : [];
}

function buildKstDateTime(dateKey: string, timeOfDay: string) {
  return new Date(`${dateKey}T${timeOfDay}:00+09:00`);
}

function resolveActiveRepeatWindow(
  row: Record<string, unknown>,
  now: Date,
): {
  startTime: string | null;
  endTime: string | null;
  anchorDateKey: string;
} | null {
  const nowParts = getKstNowParts(now);

  for (const window of resolveScheduleTimeWindows(row)) {
    const startMinutesOfDay = window.startTime ? toMinutesOfDay(window.startTime) : null;
    const endMinutesOfDay = window.endTime ? toMinutesOfDay(window.endTime) : null;
    const crossesMidnight =
      startMinutesOfDay !== null && endMinutesOfDay !== null && startMinutesOfDay > endMinutesOfDay;
    const isActive =
      startMinutesOfDay !== null && endMinutesOfDay !== null
        ? crossesMidnight
          ? nowParts.minutesOfDay >= startMinutesOfDay || nowParts.minutesOfDay <= endMinutesOfDay
          : nowParts.minutesOfDay >= startMinutesOfDay && nowParts.minutesOfDay <= endMinutesOfDay
        : startMinutesOfDay !== null
        ? nowParts.minutesOfDay >= startMinutesOfDay
        : endMinutesOfDay !== null
        ? nowParts.minutesOfDay <= endMinutesOfDay
        : false;

    if (!isActive) {
      continue;
    }

    const anchorDateKey =
      crossesMidnight && endMinutesOfDay !== null && nowParts.minutesOfDay <= endMinutesOfDay
        ? shiftKstDateKey(nowParts.dateKey, -1)
        : nowParts.dateKey;

    return {
      startTime: window.startTime,
      endTime: window.endTime,
      anchorDateKey,
    };
  }

  return null;
}

function shiftKstDateKey(dateKey: string, offsetDays: number) {
  const baseDate = buildKstDateTime(dateKey, '00:00');

  if (Number.isNaN(baseDate.getTime())) {
    return dateKey;
  }

  baseDate.setUTCDate(baseDate.getUTCDate() + offsetDays);
  return getKstDateKey(baseDate) ?? dateKey;
}

function isDateWithinScheduleDateRanges(row: Record<string, unknown>, now: Date) {
  const scheduleDateRanges = parseScheduleDateRanges(row.schedule_date_ranges_json);

  if (!scheduleDateRanges.length) {
    return true;
  }

  const dateKey = getKstDateKey(now);

  if (!dateKey) {
    return false;
  }

  return scheduleDateRanges.some((range) => range.startDate <= dateKey && dateKey <= range.endDate);
}

function getKstWeekday(now: Date) {
  const dateKey = getKstDateKey(now);

  if (!dateKey) {
    return null;
  }

  const [year, month, day] = dateKey.split('-').map((value) => Number(value));
  const date = new Date(Date.UTC(year, month - 1, day));

  if (Number.isNaN(date.getTime())) {
    return null;
  }

  return date.getUTCDay();
}

function isWithinScheduleWeekdays(row: Record<string, unknown>, now: Date) {
  const scheduleWeekdays = parseScheduleWeekdays(row.schedule_weekdays_json);

  if (!scheduleWeekdays.length) {
    return true;
  }

  const weekday = getKstWeekday(now);
  return weekday !== null && scheduleWeekdays.includes(weekday);
}

function normalizeScheduleExecutionMode(value: unknown): (typeof scheduleExecutionModes)[number] {
  return scheduleExecutionModes.includes(value as (typeof scheduleExecutionModes)[number])
    ? (value as (typeof scheduleExecutionModes)[number])
    : 'codex';
}

function toRepeatIntervalMinutes(value: unknown, unit: unknown) {
  return Math.max(1, Math.ceil(toRepeatIntervalSeconds(value, unit) / 60));
}

function toRepeatIntervalSeconds(value: unknown, unit: unknown) {
  const repeatIntervalValue = normalizeRepeatIntervalValue(value);
  const repeatIntervalUnit = normalizeRepeatIntervalUnit(unit);

  if (repeatIntervalUnit === 'second') {
    return repeatIntervalValue;
  }

  if (repeatIntervalUnit === 'day') {
    return repeatIntervalValue * 24 * 60 * 60;
  }

  if (repeatIntervalUnit === 'week') {
    return repeatIntervalValue * 7 * 24 * 60 * 60;
  }

  if (repeatIntervalUnit === 'month') {
    return repeatIntervalValue * 30 * 24 * 60 * 60;
  }

  if (repeatIntervalUnit === 'hour') {
    return repeatIntervalValue * 60 * 60;
  }

  return repeatIntervalValue * 60;
}

function resolveStoredRepeatIntervalValue(row: Record<string, unknown>) {
  return normalizeRepeatIntervalValue(row.repeat_interval_value ?? row.repeat_interval_minutes ?? 60);
}

function resolveStoredRepeatIntervalUnit(row: Record<string, unknown>) {
  return normalizeRepeatIntervalUnit(row.repeat_interval_unit);
}

function resolveStoredRepeatIntervalSeconds(row: Record<string, unknown>) {
  return toRepeatIntervalSeconds(resolveStoredRepeatIntervalValue(row), resolveStoredRepeatIntervalUnit(row));
}

function resolveStoredRepeatIntervalMinutes(row: Record<string, unknown>) {
  return toRepeatIntervalMinutes(resolveStoredRepeatIntervalValue(row), resolveStoredRepeatIntervalUnit(row));
}

function normalizeBoolean(value: unknown, fallback: boolean) {
  if (typeof value === 'boolean') {
    return value;
  }

  if (value === 0 || value === '0' || value === 'false') {
    return false;
  }

  if (value === 1 || value === '1' || value === 'true') {
    return true;
  }

  return fallback;
}

function readBooleanLikeValue(value: unknown) {
  if (typeof value === 'boolean') {
    return value;
  }

  if (typeof value === 'number') {
    return value !== 0;
  }

  if (typeof value === 'string') {
    const normalized = value.trim().toLowerCase();
    return normalized === 'true' || normalized === 't' || normalized === '1';
  }

  return false;
}

async function tryAcquirePlanScheduleRegistrationLock(scheduleId: number) {
  const result = (await db.raw('select pg_try_advisory_lock(?, ?) as locked', [
    PLAN_SCHEDULE_REGISTRATION_ADVISORY_LOCK_NAMESPACE,
    scheduleId,
  ])) as { rows?: Array<{ locked?: unknown }> };

  return readBooleanLikeValue(result.rows?.[0]?.locked);
}

async function releasePlanScheduleRegistrationLock(scheduleId: number) {
  await db.raw('select pg_advisory_unlock(?, ?)', [
    PLAN_SCHEDULE_REGISTRATION_ADVISORY_LOCK_NAMESPACE,
    scheduleId,
  ]);
}

function buildManagedServiceFailureSummary(result: {
  title?: string;
  skipped?: boolean;
  itemCount?: number;
  ios?: { ok?: boolean; skipped?: boolean; reason?: string; sentCount?: number; failedCount?: number };
  web?: { ok?: boolean; skipped?: boolean; reason?: string; sentCount?: number; failedCount?: number };
}) {
  const summaryParts = [
    result.title ? `title=${result.title}` : null,
    `itemCount=${Number(result.itemCount ?? 0)}`,
    `skipped=${result.skipped ? 'true' : 'false'}`,
    `webOk=${result.web?.ok ? 'true' : 'false'}`,
    `webSkipped=${result.web?.skipped ? 'true' : 'false'}`,
    `webSent=${Number(result.web?.sentCount ?? 0)}`,
    `webFailed=${Number(result.web?.failedCount ?? 0)}`,
    result.web?.reason ? `webReason=${result.web.reason}` : null,
    `iosOk=${result.ios?.ok ? 'true' : 'false'}`,
    `iosSkipped=${result.ios?.skipped ? 'true' : 'false'}`,
    result.ios?.reason ? `iosReason=${result.ios.reason}` : null,
  ].filter((value): value is string => Boolean(value));

  return summaryParts.join(', ');
}

export function buildScheduledBoardPostTitle(row: Record<string, unknown>) {
  const workId = normalizeScheduledWorkId(String(row.work_id ?? '반복작업'));
  const note = String(row.note ?? '')
    .split('\n')
    .map((line) => line.trim())
    .find(Boolean);
  const title = note || workId;

  return title.length > 200 ? `${title.slice(0, 197).trimEnd()}...` : title;
}

export function buildScheduledPlanWorkIdBase(row: Record<string, unknown>) {
  const workId = normalizeScheduledWorkId(String(row.work_id ?? '반복작업'));

  if (normalizeScheduleExecutionMode(row.execution_mode) !== 'managed-service') {
    return workId;
  }

  const scheduleId = Number(row.id);

  if (!Number.isInteger(scheduleId) || scheduleId <= 0) {
    return workId;
  }

  return normalizeScheduledWorkId(`schedule-${scheduleId}-${workId}`);
}

export function shouldCreatePlanForScheduleExecution(row: Record<string, unknown>) {
  return normalizeScheduleExecutionMode(row.execution_mode) === 'managed-service';
}

function getKstDateKey(value: unknown) {
  if (!value) {
    return null;
  }

  const date = value instanceof Date ? value : new Date(String(value));

  if (Number.isNaN(date.getTime())) {
    return null;
  }

  return getKstNowParts(date).dateKey;
}

function isDailyScheduleDue(row: Record<string, unknown>, now: Date) {
  const nowParts = getKstNowParts(now);
  const [hours, minutes] = normalizeDailyRunTime(row.daily_run_time).split(':').map((value) => Number(value));
  const scheduledMinutesOfDay = hours * 60 + minutes;
  return nowParts.minutesOfDay >= scheduledMinutesOfDay && getKstDateKey(row.last_registered_at) !== nowParts.dateKey;
}

function isIntervalScheduleDue(row: Record<string, unknown>, now: Date) {
  const lastRegisteredAt = row.last_registered_at ? new Date(String(row.last_registered_at)) : null;
  const repeatIntervalSeconds = resolveStoredRepeatIntervalSeconds(row);

  if (!lastRegisteredAt || Number.isNaN(lastRegisteredAt.getTime())) {
    const activeWindow = resolveActiveRepeatWindow(row, now);

    if (activeWindow?.startTime) {
      const startAt = buildKstDateTime(activeWindow.anchorDateKey, activeWindow.startTime);

      if (!Number.isNaN(startAt.getTime())) {
        if (normalizeBoolean(row.immediate_run_enabled, true)) {
          return now.getTime() >= startAt.getTime();
        }

        return now.getTime() >= startAt.getTime() + repeatIntervalSeconds * 1000;
      }
    }
  }

  const intervalBaseAt = lastRegisteredAt && !Number.isNaN(lastRegisteredAt.getTime())
    ? lastRegisteredAt
    : new Date(String(row.created_at ?? now.toISOString()));

  if (Number.isNaN(intervalBaseAt.getTime())) {
    return true;
  }

  return intervalBaseAt.getTime() <= now.getTime() - repeatIntervalSeconds * 1000;
}

function isWithinRepeatWindow(row: Record<string, unknown>, now: Date) {
  const timeWindows = resolveScheduleTimeWindows(row);

  if (!timeWindows.length) {
    return true;
  }

  return resolveActiveRepeatWindow(row, now) !== null;
}

function isScheduleDue(row: Record<string, unknown>, now: Date) {
  if (!isDateWithinScheduleDateRanges(row, now)) {
    return false;
  }

  const scheduleMode = normalizeScheduleMode(row.schedule_mode);

  if (scheduleMode === 'daily' && !isWithinScheduleWeekdays(row, now)) {
    return false;
  }

  if (scheduleMode === 'interval' && !isWithinScheduleWeekdays(row, now)) {
    return false;
  }

  if (scheduleMode === 'interval' && !isWithinRepeatWindow(row, now)) {
    return false;
  }

  if (!row.last_registered_at && normalizeBoolean(row.immediate_run_enabled, true)) {
    return true;
  }

  return scheduleMode === 'daily' ? isDailyScheduleDue(row, now) : isIntervalScheduleDue(row, now);
}

export function isPlanScheduledTaskDue(row: Record<string, unknown>, now = new Date()) {
  return isScheduleDue(row, now);
}

export function mapPlanScheduledTaskRow(row: Record<string, unknown>) {
  const repeatIntervalValue = resolveStoredRepeatIntervalValue(row);
  const repeatIntervalUnit = resolveStoredRepeatIntervalUnit(row);

  return {
    id: row.id,
    workId: row.work_id,
    note: row.note,
    automationType: resolveStoredAutomationTypeId(row),
    automationContextIds: parseAutomationContextIds(row.automation_context_ids_json),
    releaseTarget: row.release_target,
    jangsingProcessingRequired: Boolean(row.jangsing_processing_required ?? true),
    autoDeployToMain: Boolean(row.auto_deploy_to_main ?? true),
    suppressWebPush: Boolean(row.suppress_web_push ?? false),
    enabled: Boolean(row.enabled ?? true),
    immediateRunEnabled: normalizeBoolean(row.immediate_run_enabled, true),
    refreshContextSnapshotOnNextRun: normalizeBoolean(row.context_snapshot_refresh_requested, false),
    executionMode: normalizeScheduleExecutionMode(row.execution_mode),
    managedServiceKey: typeof row.managed_service_key === 'string' ? row.managed_service_key : null,
    managedServicePackageName: typeof row.managed_service_package_name === 'string' ? row.managed_service_package_name : null,
    managedServiceDirectory: typeof row.managed_service_directory === 'string' ? row.managed_service_directory : null,
    managedServiceManifestPath: typeof row.managed_service_manifest_path === 'string' ? row.managed_service_manifest_path : null,
    managedServiceGeneratedAt: row.managed_service_generated_at ?? null,
    managedServiceGenerationPlanItemId:
      row.managed_service_generation_plan_item_id === null || row.managed_service_generation_plan_item_id === undefined
        ? null
        : Number(row.managed_service_generation_plan_item_id),
    managedServiceGenerationBoardPostId:
      row.managed_service_generation_board_post_id === null || row.managed_service_generation_board_post_id === undefined
        ? null
        : Number(row.managed_service_generation_board_post_id),
    recreateManagedServiceOnNextSave: normalizeBoolean(row.managed_service_recreate_requested, false),
    scheduleMode: normalizeScheduleMode(row.schedule_mode),
    repeatIntervalValue,
    repeatIntervalUnit,
    repeatIntervalSeconds: resolveStoredRepeatIntervalSeconds(row),
    repeatIntervalMinutes: resolveStoredRepeatIntervalMinutes(row),
    dailyRunTime: normalizeDailyRunTime(row.daily_run_time),
    scheduleWeekdays: parseScheduleWeekdays(row.schedule_weekdays_json),
    scheduleDateRanges: parseScheduleDateRanges(row.schedule_date_ranges_json),
    repeatWindows: resolveScheduleTimeWindows(row),
    repeatWindowStartTime: normalizeOptionalTimeOfDay(row.repeat_window_start_time),
    repeatWindowEndTime: normalizeOptionalTimeOfDay(row.repeat_window_end_time),
    lastRegisteredAt: row.last_registered_at,
    createdAt: row.created_at,
    updatedAt: row.updated_at,
  };
}

async function removeManagedServicePackage(scheduleId: number) {
  await removeManagedScheduleServiceArtifacts(scheduleId);
}

async function ensureManagedServicePackage(options: {
  scheduleId: number;
  workId: string;
  note: string;
  releaseTarget: string;
  automationType: string;
}) {
  const metadata = buildManagedScheduleServiceMetadata(options.scheduleId, options.workId);
  await prepareManagedScheduleServiceDirectory(options.scheduleId);
  return metadata;
}

function buildScheduledManagedServicePlanWorkIdBase(row: Record<string, unknown>) {
  return buildScheduledPlanWorkIdBase(row);
}

function isManagedServiceGenerationPlanPending(row: Record<string, unknown> | null | undefined) {
  const status = String(row?.status ?? '').trim();
  const workerStatus = String(row?.worker_status ?? '').trim();

  if (!status) {
    return false;
  }

  if (['완료', '릴리즈완료', '작업완료'].includes(status)) {
    return false;
  }

  if (['작업취소', '브랜치실패', '자동작업실패', 'release반영실패', 'main반영실패'].includes(workerStatus)) {
    return false;
  }

  return true;
}

function buildManagedServiceGenerationPlanNote(options: {
  row: Record<string, unknown>;
  scheduleSnapshot: {
    requestPath: string;
    contextPath: string;
    manifestPath: string;
  };
  reason: 'requested' | 'missing';
}) {
  const scheduleId = Number(options.row.id);
  const metadata = buildManagedScheduleServiceMetadata(scheduleId, String(options.row.work_id ?? '반복작업'));
  const requestedReason = options.reason === 'requested' ? '사용자가 패키지 재처리를 요청함' : '실행 대상 서비스 패키지가 누락됨';

  return [
    '## 스케줄 서비스 패키지 생성 지시',
    `- 대상 스케줄 PK: ${scheduleId}`,
    `- 작업 ID base: ${buildScheduledManagedServicePlanWorkIdBase(options.row)}`,
    `- 생성 사유: ${requestedReason}`,
    `- 패키지 루트: ${metadata.relativeDirectory}`,
    `- 서비스 키: ${metadata.serviceKey}`,
    `- 패키지명: ${metadata.packageName}`,
    '',
    '반드시 아래 파일을 위 패키지 루트에 직접 생성하거나 갱신하세요.',
    `- ${metadata.readmePath}`,
    `- ${metadata.sourcePath}`,
    `- ${metadata.runtimePath}`,
    `- ${metadata.manifestPath}`,
    '',
    '생성 규칙:',
    '- service.ts 와 service.mjs 는 둘 다 유지합니다.',
    '- service.mjs 는 스케줄 실행 시 직접 import 되어 `run(runtime)` 를 호출합니다.',
    '- README.md 에 서비스 목적, 실행 방식, 의존 경로, 검증 방법을 적습니다.',
    '- service-manifest.json 에 schedulePk, workId, serviceKey, packageName, relativeDirectory, sourcePath, runtimePath, readmePath, createdAt 를 기록합니다.',
    '- managed-service 같은 하위 임시 디렉터리를 다시 만들지 말고, 위 루트 경로만 사용합니다.',
    '- 실제 서비스 로직 요구사항은 아래 request/context 문서를 읽고 구현하세요. 본 자동화 메모 안에 원문 요구사항을 다시 복사하지 마세요.',
    '- generic placeholder 같은 빈 구현으로 남기지 마세요.',
    '',
    '참고 문서:',
    `- 요청 정리: ${options.scheduleSnapshot.requestPath}`,
    `- 컨텍스트 정리: ${options.scheduleSnapshot.contextPath}`,
    `- 스냅샷 manifest: ${options.scheduleSnapshot.manifestPath}`,
    '',
    '검증 기준:',
    '- 생성 직후 위 네 파일이 모두 존재해야 합니다.',
    '- service.mjs 가 현재 저장소 코드 기준으로 바로 실행 가능한 상태여야 합니다.',
  ]
    .filter(Boolean)
    .join('\n')
    .trim();
}

async function queueManagedServiceGenerationPlan(options: {
  row: Record<string, unknown>;
  scheduleSnapshot: {
    requestPath: string;
    contextPath: string;
    manifestPath: string;
  };
  automationContextIds: string[];
  reason: 'requested' | 'missing';
}) {
  const boardPost = await createBoardPost({
    title: `[스케줄 서비스 생성] ${buildScheduledBoardPostTitle(options.row)}`,
    content: buildManagedServiceGenerationPlanNote({
      row: options.row,
      scheduleSnapshot: options.scheduleSnapshot,
      reason: options.reason,
    }),
    attachments: [],
    automationType: String(options.row.automation_type_id ?? options.row.automation_type ?? 'none'),
    automationContextIds: options.automationContextIds,
    requestExecutionMode: 'all_at_once',
    requestItems: [],
  });
  const automationReceipt = await receiveBoardPostAutomation(Number(boardPost.id), {
    planWorkIdBase: buildScheduledManagedServicePlanWorkIdBase(options.row),
    planWorkIdSuffixLabel: 'service',
    suppressWebPush: Boolean(options.row.suppress_web_push ?? false),
  });

  const createdPlanItemId = automationReceipt?.planItemIds[0] ?? null;

  if (!createdPlanItemId) {
    throw new Error(`Plan 스케줄 #${options.row.id} 서비스 패키지 자동 접수에 실패했습니다.`);
  }

  const updatedRows = await db(PLAN_SCHEDULED_TASK_TABLE)
    .where({ id: options.row.id })
    .update({
      managed_service_generation_board_post_id: Number(boardPost.id),
      managed_service_generation_plan_item_id: Number(createdPlanItemId),
      managed_service_recreate_requested: false,
      updated_at: db.fn.now(),
    })
    .returning('*');
  const createdPlan = await db(PLAN_TABLE).where({ id: createdPlanItemId }).first();

  return {
    row: updatedRows[0] ?? options.row,
    createdPlan: createdPlan ?? null,
    createdBoardPost: boardPost,
  };
}

async function ensureManagedServiceExecutionReady(options: {
  row: Record<string, unknown>;
  scheduleSnapshot: {
    requestPath: string;
    contextPath: string;
    manifestPath: string;
  };
  automationContextIds: string[];
}) {
  const { row, scheduleSnapshot, automationContextIds } = options;

  if (normalizeScheduleExecutionMode(row.execution_mode) !== 'managed-service') {
    return {
      row,
      ready: true,
      generationTriggered: false,
      reason: null as 'requested' | 'missing' | null,
      createdPlan: null as Record<string, unknown> | null,
      createdBoardPosts: [] as Array<Record<string, unknown>>,
    };
  }

  const recreateRequested = normalizeBoolean(row.managed_service_recreate_requested, false);
  const packageExists = await hasManagedScheduleServicePackage(
    typeof row.managed_service_directory === 'string' ? row.managed_service_directory : null,
  );

  if (packageExists && !recreateRequested) {
    return {
      row,
      ready: true,
      generationTriggered: false,
      reason: null as 'requested' | 'missing' | null,
      createdPlan: null as Record<string, unknown> | null,
      createdBoardPosts: [] as Array<Record<string, unknown>>,
    };
  }

  const existingPlanId = Number(row.managed_service_generation_plan_item_id ?? 0);
  const existingPlan = existingPlanId > 0 ? await db(PLAN_TABLE).where({ id: existingPlanId }).first() : null;

  if (packageExists && existingPlan && !recreateRequested) {
    const updatedRows = await db(PLAN_SCHEDULED_TASK_TABLE)
      .where({ id: row.id })
      .update({
        managed_service_recreate_requested: false,
        managed_service_generated_at: db.fn.now(),
        updated_at: db.fn.now(),
      })
      .returning('*');

    return {
      row: updatedRows[0] ?? row,
      ready: true,
      generationTriggered: false,
      reason: null as 'requested' | 'missing' | null,
      createdPlan: null as Record<string, unknown> | null,
      createdBoardPosts: [] as Array<Record<string, unknown>>,
    };
  }

  if (existingPlan && isManagedServiceGenerationPlanPending(existingPlan)) {
    return {
      row,
      ready: false,
      generationTriggered: false,
      reason: recreateRequested ? ('requested' as const) : ('missing' as const),
      createdPlan: existingPlan,
      createdBoardPosts: [],
    };
  }

  await removeManagedServicePackage(Number(row.id));
  await ensureManagedServicePackage({
    scheduleId: Number(row.id),
    workId: String(row.work_id ?? '반복작업'),
    note: String(row.note ?? ''),
    releaseTarget: String(row.release_target ?? 'release'),
    automationType: String(row.automation_type_id ?? row.automation_type ?? 'none'),
  });
  const queued = await queueManagedServiceGenerationPlan({
    row,
    scheduleSnapshot,
    automationContextIds,
    reason: recreateRequested ? 'requested' : 'missing',
  });

  return {
    row: queued.row,
    ready: false,
    generationTriggered: true,
    reason: recreateRequested ? ('requested' as const) : ('missing' as const),
    createdPlan: queued.createdPlan,
    createdBoardPosts: [queued.createdBoardPost],
  };
}

async function ensurePlanScheduledTaskColumn(
  columnName: string,
  addColumn: (table: any) => void,
) {
  const hasColumn = await db.schema.hasColumn(PLAN_SCHEDULED_TASK_TABLE, columnName);

  if (hasColumn) {
    return;
  }

  await db.schema.alterTable(PLAN_SCHEDULED_TASK_TABLE, (table) => {
    addColumn(table);
  });
}

export async function ensurePlanScheduledTaskTable() {
  const exists = await db.schema.hasTable(PLAN_SCHEDULED_TASK_TABLE);

  if (!exists) {
    await db.schema.createTable(PLAN_SCHEDULED_TASK_TABLE, (table) => {
      table.increments('id').primary();
      table.string('work_id', 120).notNullable().defaultTo('반복작업');
      table.text('note').notNullable().defaultTo('');
      table.string('automation_type', 40).notNullable().defaultTo('none');
      table.string('automation_type_id', 120).nullable();
      table.text('automation_context_ids_json').notNullable().defaultTo('[]');
      table.string('release_target', 120).notNullable().defaultTo('release');
      table.boolean('jangsing_processing_required').notNullable().defaultTo(true);
      table.boolean('auto_deploy_to_main').notNullable().defaultTo(true);
      table.boolean('suppress_web_push').notNullable().defaultTo(false);
      table.boolean('enabled').notNullable().defaultTo(true);
      table.boolean('immediate_run_enabled').notNullable().defaultTo(true);
      table.boolean('context_snapshot_refresh_requested').notNullable().defaultTo(false);
      table.string('execution_mode', 40).notNullable().defaultTo('codex');
      table.string('managed_service_key', 160).nullable();
      table.string('managed_service_package_name', 200).nullable();
      table.text('managed_service_directory').nullable();
      table.text('managed_service_manifest_path').nullable();
      table.timestamp('managed_service_generated_at', { useTz: true }).nullable();
      table.integer('managed_service_generation_plan_item_id').nullable();
      table.integer('managed_service_generation_board_post_id').nullable();
      table.boolean('managed_service_recreate_requested').notNullable().defaultTo(false);
      table.string('schedule_mode', 20).notNullable().defaultTo('interval');
      table.integer('repeat_interval_value').notNullable().defaultTo(60);
      table.string('repeat_interval_unit', 20).notNullable().defaultTo('minute');
      table.integer('repeat_interval_minutes').notNullable().defaultTo(60);
      table.integer('repeat_interval_seconds').notNullable().defaultTo(DEFAULT_REPEAT_INTERVAL_SECONDS);
      table.string('daily_run_time', 5).notNullable().defaultTo(DEFAULT_DAILY_RUN_TIME);
      table.text('schedule_weekdays_json').notNullable().defaultTo('[]');
      table.text('schedule_date_ranges_json').notNullable().defaultTo('[]');
      table.text('repeat_windows_json').notNullable().defaultTo('[]');
      table.string('repeat_window_start_time', 5).nullable();
      table.string('repeat_window_end_time', 5).nullable();
      table.timestamp('last_registered_at', { useTz: true }).nullable();
      table.timestamp('context_snapshot_generated_at', { useTz: true }).nullable();
      table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
      table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
    });
    return;
  }

  await ensurePlanScheduledTaskColumn('release_target', (table) => {
    table.string('release_target', 120).notNullable().defaultTo('release');
  });
  await ensurePlanScheduledTaskColumn('automation_type', (table) => {
    table.string('automation_type', 40).notNullable().defaultTo('none');
  });
  await ensurePlanScheduledTaskColumn('automation_type_id', (table) => {
    table.string('automation_type_id', 120).nullable();
  });
  await ensurePlanScheduledTaskColumn('automation_context_ids_json', (table) => {
    table.text('automation_context_ids_json').notNullable().defaultTo('[]');
  });
  await ensurePlanScheduledTaskColumn('jangsing_processing_required', (table) => {
    table.boolean('jangsing_processing_required').notNullable().defaultTo(true);
  });
  await ensurePlanScheduledTaskColumn('auto_deploy_to_main', (table) => {
    table.boolean('auto_deploy_to_main').notNullable().defaultTo(true);
  });
  await ensurePlanScheduledTaskColumn('suppress_web_push', (table) => {
    table.boolean('suppress_web_push').notNullable().defaultTo(false);
  });
  await ensurePlanScheduledTaskColumn('enabled', (table) => {
    table.boolean('enabled').notNullable().defaultTo(true);
  });
  await ensurePlanScheduledTaskColumn('immediate_run_enabled', (table) => {
    table.boolean('immediate_run_enabled').notNullable().defaultTo(true);
  });
  await ensurePlanScheduledTaskColumn('context_snapshot_refresh_requested', (table) => {
    table.boolean('context_snapshot_refresh_requested').notNullable().defaultTo(false);
  });
  await ensurePlanScheduledTaskColumn('execution_mode', (table) => {
    table.string('execution_mode', 40).notNullable().defaultTo('codex');
  });
  await ensurePlanScheduledTaskColumn('managed_service_key', (table) => {
    table.string('managed_service_key', 160).nullable();
  });
  await ensurePlanScheduledTaskColumn('managed_service_package_name', (table) => {
    table.string('managed_service_package_name', 200).nullable();
  });
  await ensurePlanScheduledTaskColumn('managed_service_directory', (table) => {
    table.text('managed_service_directory').nullable();
  });
  await ensurePlanScheduledTaskColumn('managed_service_manifest_path', (table) => {
    table.text('managed_service_manifest_path').nullable();
  });
  await ensurePlanScheduledTaskColumn('managed_service_generated_at', (table) => {
    table.timestamp('managed_service_generated_at', { useTz: true }).nullable();
  });
  await ensurePlanScheduledTaskColumn('managed_service_generation_plan_item_id', (table) => {
    table.integer('managed_service_generation_plan_item_id').nullable();
  });
  await ensurePlanScheduledTaskColumn('managed_service_generation_board_post_id', (table) => {
    table.integer('managed_service_generation_board_post_id').nullable();
  });
  await ensurePlanScheduledTaskColumn('managed_service_recreate_requested', (table) => {
    table.boolean('managed_service_recreate_requested').notNullable().defaultTo(false);
  });
  await ensurePlanScheduledTaskColumn('schedule_mode', (table) => {
    table.string('schedule_mode', 20).notNullable().defaultTo('interval');
  });
  await ensurePlanScheduledTaskColumn('repeat_interval_value', (table) => {
    table.integer('repeat_interval_value').notNullable().defaultTo(60);
  });
  await ensurePlanScheduledTaskColumn('repeat_interval_unit', (table) => {
    table.string('repeat_interval_unit', 20).notNullable().defaultTo('minute');
  });
  await ensurePlanScheduledTaskColumn('repeat_interval_minutes', (table) => {
    table.integer('repeat_interval_minutes').notNullable().defaultTo(60);
  });
  await ensurePlanScheduledTaskColumn('repeat_interval_seconds', (table) => {
    table.integer('repeat_interval_seconds').notNullable().defaultTo(DEFAULT_REPEAT_INTERVAL_SECONDS);
  });
  await ensurePlanScheduledTaskColumn('daily_run_time', (table) => {
    table.string('daily_run_time', 5).notNullable().defaultTo(DEFAULT_DAILY_RUN_TIME);
  });
  await ensurePlanScheduledTaskColumn('schedule_weekdays_json', (table) => {
    table.text('schedule_weekdays_json').notNullable().defaultTo('[]');
  });
  await ensurePlanScheduledTaskColumn('schedule_date_ranges_json', (table) => {
    table.text('schedule_date_ranges_json').notNullable().defaultTo('[]');
  });
  await ensurePlanScheduledTaskColumn('repeat_windows_json', (table) => {
    table.text('repeat_windows_json').notNullable().defaultTo('[]');
  });
  await ensurePlanScheduledTaskColumn('repeat_window_start_time', (table) => {
    table.string('repeat_window_start_time', 5).nullable();
  });
  await ensurePlanScheduledTaskColumn('repeat_window_end_time', (table) => {
    table.string('repeat_window_end_time', 5).nullable();
  });
  await ensurePlanScheduledTaskColumn('last_registered_at', (table) => {
    table.timestamp('last_registered_at', { useTz: true }).nullable();
  });
  await ensurePlanScheduledTaskColumn('context_snapshot_generated_at', (table) => {
    table.timestamp('context_snapshot_generated_at', { useTz: true }).nullable();
  });
  await ensurePlanScheduledTaskColumn('created_at', (table) => {
    table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
  });
  await ensurePlanScheduledTaskColumn('updated_at', (table) => {
    table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
  });

  await db(PLAN_SCHEDULED_TASK_TABLE)
    .where({ automation_type: 'plan_registration' })
    .update({ automation_type: 'plan' });
  await db(PLAN_SCHEDULED_TASK_TABLE)
    .where({ automation_type: 'general_development' })
    .update({ automation_type: 'auto_worker' });
  await db(PLAN_SCHEDULED_TASK_TABLE)
    .whereNull('automation_type_id')
    .update({
      automation_type_id: db.raw('automation_type'),
    });

  await db(PLAN_SCHEDULED_TASK_TABLE)
    .where({ repeat_interval_unit: 'minute' })
    .update({
      repeat_interval_value: db.raw('repeat_interval_minutes'),
    });
  await db(PLAN_SCHEDULED_TASK_TABLE)
    .whereNull('repeat_interval_seconds')
    .update({
      repeat_interval_seconds: db.raw('repeat_interval_minutes * 60'),
    });
  await db(PLAN_SCHEDULED_TASK_TABLE)
    .whereNull('suppress_web_push')
    .update({
      suppress_web_push: false,
    });

  const existingWindowRows = await db(PLAN_SCHEDULED_TASK_TABLE).select(
    'id',
    'repeat_windows_json',
    'repeat_window_start_time',
    'repeat_window_end_time',
  );

  for (const row of existingWindowRows) {
    const normalizedWindows = resolveScheduleTimeWindows(row);
    const currentWindows = parseScheduleTimeWindows(row.repeat_windows_json);

    if (JSON.stringify(currentWindows) === JSON.stringify(normalizedWindows)) {
      continue;
    }

    await db(PLAN_SCHEDULED_TASK_TABLE)
      .where({ id: row.id })
      .update({
        repeat_windows_json: stringifyScheduleTimeWindows(normalizedWindows),
      });
  }

  const existingRows = await db(PLAN_SCHEDULED_TASK_TABLE).select(
    'id',
    'repeat_interval_value',
    'repeat_interval_unit',
    'repeat_interval_minutes',
    'repeat_interval_seconds',
  );

  for (const row of existingRows) {
    const repeatIntervalSeconds = resolveStoredRepeatIntervalSeconds(row);
    const repeatIntervalMinutes = resolveStoredRepeatIntervalMinutes(row);
    const currentSeconds = normalizeRepeatIntervalSeconds(row.repeat_interval_seconds ?? repeatIntervalSeconds);
    const currentMinutes = normalizeRepeatIntervalMinutes(row.repeat_interval_minutes ?? repeatIntervalMinutes);

    if (currentSeconds === repeatIntervalSeconds && currentMinutes === repeatIntervalMinutes) {
      continue;
    }

    await db(PLAN_SCHEDULED_TASK_TABLE)
      .where({ id: row.id })
      .update({
        repeat_interval_seconds: repeatIntervalSeconds,
        repeat_interval_minutes: repeatIntervalMinutes,
      });
  }
}

export async function listPlanScheduledTasks() {
  await ensurePlanScheduledTaskTable();

  const rows = await db(PLAN_SCHEDULED_TASK_TABLE).select('*').orderBy('id', 'desc');
  return Promise.all(rows.map((row) => syncManagedServiceGenerationCompletionForScheduleRow(row)));
}

export async function getPlanScheduledTaskById(id: number) {
  await ensurePlanScheduledTaskTable();

  const row = await db(PLAN_SCHEDULED_TASK_TABLE).where({ id }).first();
  return row ? syncManagedServiceGenerationCompletionForScheduleRow(row) : null;
}

async function syncManagedServiceGenerationCompletionForScheduleRow(row: Record<string, unknown>) {
  if (normalizeScheduleExecutionMode(row.execution_mode) !== 'managed-service') {
    return row;
  }

  const generationPlanItemId = Number(row.managed_service_generation_plan_item_id ?? 0);

  if (generationPlanItemId <= 0) {
    return row;
  }

  const packageExists = await hasManagedScheduleServicePackage(
    typeof row.managed_service_directory === 'string' ? row.managed_service_directory : null,
  );

  if (!packageExists) {
    return row;
  }

  const generationPlan = await db(PLAN_TABLE)
    .select('id', 'status', 'worker_status')
    .where({ id: generationPlanItemId })
    .first();
  const generationPlanStatus = String(generationPlan?.status ?? '').trim();
  const generationPlanWorkerStatus = String(generationPlan?.worker_status ?? '').trim();
  const isCompletedPlan =
    Boolean(generationPlan)
    && ['완료', '작업완료', '릴리즈완료'].includes(generationPlanStatus)
    && !['브랜치실패', '자동작업실패', 'release반영실패', 'main반영실패'].includes(generationPlanWorkerStatus);

  if (!isCompletedPlan) {
    return row;
  }

  if (row.managed_service_generated_at && !normalizeBoolean(row.managed_service_recreate_requested, false)) {
    return row;
  }

  const updatedRows = await db(PLAN_SCHEDULED_TASK_TABLE)
    .where({ id: row.id })
    .update({
      managed_service_generated_at: db.fn.now(),
      managed_service_recreate_requested: false,
      updated_at: db.fn.now(),
    })
    .returning('*');

  return updatedRows[0] ?? row;
}

export async function syncManagedServiceGenerationCompletion(planItemId: number) {
  await ensurePlanScheduledTaskTable();

  const rows = await db(PLAN_SCHEDULED_TASK_TABLE)
    .select('*')
    .where({ managed_service_generation_plan_item_id: planItemId });

  const syncedRows = await Promise.all(rows.map((row) => syncManagedServiceGenerationCompletionForScheduleRow(row)));
  return syncedRows.filter((row) => Boolean(row.managed_service_generated_at));
}

export async function createPlanScheduledTask(payload: z.infer<typeof createPlanScheduledTaskSchema>) {
  await ensurePlanScheduledTaskTable();
  const scheduleMode = normalizeScheduleMode(payload.scheduleMode);
  const repeatIntervalValue = normalizeRepeatIntervalValue(payload.repeatIntervalValue);
  const repeatIntervalUnit = normalizeRepeatIntervalUnit(payload.repeatIntervalUnit);
  const automationType = await resolveAutomationType(payload.automationType);
  const executionMode = normalizeScheduleExecutionMode(payload.executionMode);
  const repeatWindows = normalizeScheduleTimeWindows(payload.repeatWindows);
  const firstRepeatWindow = repeatWindows[0] ?? null;
  const shouldAcknowledgeManagedServiceRefreshOnNextRun =
    executionMode === 'managed-service' && Boolean(payload.recreateManagedServiceOnNextSave);

  const rows = await db(PLAN_SCHEDULED_TASK_TABLE)
    .insert({
      work_id: normalizeScheduledWorkId(payload.workId),
      note: payload.note,
      automation_type: automationType.behaviorType,
      automation_type_id: automationType.id,
      automation_context_ids_json: stringifyAutomationContextIds(payload.automationContextIds),
      release_target: payload.releaseTarget,
      jangsing_processing_required: payload.jangsingProcessingRequired,
      auto_deploy_to_main: payload.autoDeployToMain,
      suppress_web_push: payload.suppressWebPush,
      enabled: payload.enabled,
      immediate_run_enabled: payload.immediateRunEnabled,
      context_snapshot_refresh_requested: payload.refreshContextSnapshotOnNextRun,
      execution_mode: executionMode,
      managed_service_recreate_requested: shouldAcknowledgeManagedServiceRefreshOnNextRun,
      schedule_mode: scheduleMode,
      repeat_interval_value: repeatIntervalValue,
      repeat_interval_unit: repeatIntervalUnit,
      repeat_interval_seconds: toRepeatIntervalSeconds(repeatIntervalValue, repeatIntervalUnit),
      repeat_interval_minutes: toRepeatIntervalMinutes(repeatIntervalValue, repeatIntervalUnit),
      daily_run_time: normalizeDailyRunTime(payload.dailyRunTime),
      schedule_weekdays_json: stringifyScheduleWeekdays(payload.scheduleWeekdays),
      schedule_date_ranges_json: stringifyScheduleDateRanges(payload.scheduleDateRanges),
      repeat_windows_json: stringifyScheduleTimeWindows(repeatWindows),
      repeat_window_start_time: firstRepeatWindow?.startTime ?? null,
      repeat_window_end_time: firstRepeatWindow?.endTime ?? null,
      updated_at: db.fn.now(),
    })
    .returning('*');
  let row = rows[0];

  if (executionMode === 'managed-service' && row?.id) {
    const managedService = await ensureManagedServicePackage({
      scheduleId: Number(row.id),
      workId: String(row.work_id ?? payload.workId ?? '반복작업'),
      note: String(row.note ?? payload.note ?? ''),
      releaseTarget: String(row.release_target ?? payload.releaseTarget ?? 'release'),
      automationType: String(row.automation_type_id ?? row.automation_type ?? automationType.id),
    });

    const updatedRows = await db(PLAN_SCHEDULED_TASK_TABLE)
      .where({ id: row.id })
      .update({
        managed_service_key: managedService.serviceKey,
        managed_service_package_name: managedService.packageName,
        managed_service_directory: managedService.relativeDirectory,
        managed_service_manifest_path: managedService.manifestPath,
        managed_service_generated_at: null,
        managed_service_generation_plan_item_id: null,
        managed_service_generation_board_post_id: null,
        managed_service_recreate_requested: shouldAcknowledgeManagedServiceRefreshOnNextRun,
        updated_at: db.fn.now(),
      })
      .returning('*');
    row = updatedRows[0] ?? row;
  }

  return row;
}

export async function updatePlanScheduledTask(id: number, payload: z.infer<typeof updatePlanScheduledTaskSchema>) {
  await ensurePlanScheduledTaskTable();

  const currentRow = await db(PLAN_SCHEDULED_TASK_TABLE).where({ id }).first();

  if (!currentRow) {
    return null;
  }
  const scheduleMode = normalizeScheduleMode(payload.scheduleMode ?? currentRow.schedule_mode);
  const repeatIntervalValue = normalizeRepeatIntervalValue(
    payload.repeatIntervalValue ?? currentRow.repeat_interval_value ?? currentRow.repeat_interval_minutes ?? 60,
  );
  const repeatIntervalUnit = normalizeRepeatIntervalUnit(payload.repeatIntervalUnit ?? currentRow.repeat_interval_unit);
  const executionMode = normalizeScheduleExecutionMode(payload.executionMode ?? currentRow.execution_mode);
  const automationType = await resolveAutomationType(
    payload.automationType ?? currentRow.automation_type_id ?? currentRow.automation_type,
  );
  const repeatWindows = normalizeScheduleTimeWindows(
    payload.repeatWindows ?? resolveScheduleTimeWindows(currentRow),
  );
  const firstRepeatWindow = repeatWindows[0] ?? null;
  const shouldRecreateManagedService =
    executionMode === 'managed-service' &&
    (normalizeBoolean(payload.recreateManagedServiceOnNextSave, false)
      || executionMode !== normalizeScheduleExecutionMode(currentRow.execution_mode)
      || (payload.workId !== undefined && normalizeScheduledWorkId(payload.workId) !== String(currentRow.work_id ?? '')));

  const rows = await db(PLAN_SCHEDULED_TASK_TABLE)
    .where({ id })
    .update({
      work_id: payload.workId === undefined ? currentRow.work_id : normalizeScheduledWorkId(payload.workId),
      note: payload.note ?? currentRow.note,
      automation_type: automationType.behaviorType,
      automation_type_id: automationType.id,
      automation_context_ids_json: stringifyAutomationContextIds(
        payload.automationContextIds ?? parseAutomationContextIds(currentRow.automation_context_ids_json),
      ),
      release_target: payload.releaseTarget ?? currentRow.release_target ?? 'release',
      jangsing_processing_required:
        payload.jangsingProcessingRequired ?? currentRow.jangsing_processing_required ?? true,
      auto_deploy_to_main: payload.autoDeployToMain ?? currentRow.auto_deploy_to_main ?? true,
      suppress_web_push: payload.suppressWebPush ?? currentRow.suppress_web_push ?? false,
      enabled: payload.enabled ?? currentRow.enabled ?? true,
      immediate_run_enabled: payload.immediateRunEnabled ?? currentRow.immediate_run_enabled ?? true,
      context_snapshot_refresh_requested:
        payload.refreshContextSnapshotOnNextRun ?? currentRow.context_snapshot_refresh_requested ?? false,
      execution_mode: executionMode,
      managed_service_recreate_requested: shouldRecreateManagedService,
      managed_service_generation_plan_item_id:
        shouldRecreateManagedService || executionMode !== normalizeScheduleExecutionMode(currentRow.execution_mode)
          ? null
          : currentRow.managed_service_generation_plan_item_id ?? null,
      managed_service_generation_board_post_id:
        shouldRecreateManagedService || executionMode !== normalizeScheduleExecutionMode(currentRow.execution_mode)
          ? null
          : currentRow.managed_service_generation_board_post_id ?? null,
      schedule_mode: scheduleMode,
      repeat_interval_value: repeatIntervalValue,
      repeat_interval_unit: repeatIntervalUnit,
      repeat_interval_seconds: toRepeatIntervalSeconds(repeatIntervalValue, repeatIntervalUnit),
      repeat_interval_minutes: toRepeatIntervalMinutes(repeatIntervalValue, repeatIntervalUnit),
      daily_run_time: normalizeDailyRunTime(payload.dailyRunTime ?? currentRow.daily_run_time),
      schedule_weekdays_json: stringifyScheduleWeekdays(
        payload.scheduleWeekdays ?? parseScheduleWeekdays(currentRow.schedule_weekdays_json),
      ),
      schedule_date_ranges_json: stringifyScheduleDateRanges(
        payload.scheduleDateRanges ?? parseScheduleDateRanges(currentRow.schedule_date_ranges_json),
      ),
      repeat_windows_json: stringifyScheduleTimeWindows(repeatWindows),
      repeat_window_start_time: firstRepeatWindow?.startTime ?? null,
      repeat_window_end_time: firstRepeatWindow?.endTime ?? null,
      context_snapshot_generated_at:
        payload.enabled !== undefined && Boolean(payload.enabled) !== Boolean(currentRow.enabled ?? true)
          ? null
          : currentRow.context_snapshot_generated_at ?? null,
      updated_at: db.fn.now(),
    })
    .returning('*');
  let row = rows[0] ?? null;

  if (!row) {
    return null;
  }

  if (executionMode !== 'managed-service') {
    await removeManagedServicePackage(Number(row.id));
    const updatedRows = await db(PLAN_SCHEDULED_TASK_TABLE)
      .where({ id: row.id })
      .update({
        managed_service_key: null,
        managed_service_package_name: null,
        managed_service_directory: null,
        managed_service_manifest_path: null,
        managed_service_generated_at: null,
        managed_service_generation_plan_item_id: null,
        managed_service_generation_board_post_id: null,
        managed_service_recreate_requested: false,
        updated_at: db.fn.now(),
      })
      .returning('*');
    return updatedRows[0] ?? row;
  }

  if (
    shouldRecreateManagedService
    || !String(row.managed_service_directory ?? '').trim()
    || !String(row.managed_service_key ?? '').trim()
  ) {
    await removeManagedServicePackage(Number(row.id));
    const managedService = await ensureManagedServicePackage({
      scheduleId: Number(row.id),
      workId: String(row.work_id ?? currentRow.work_id ?? '반복작업'),
      note: String(row.note ?? currentRow.note ?? ''),
      releaseTarget: String(row.release_target ?? currentRow.release_target ?? 'release'),
      automationType: String(row.automation_type_id ?? row.automation_type ?? automationType.id),
    });
    const updatedRows = await db(PLAN_SCHEDULED_TASK_TABLE)
      .where({ id: row.id })
      .update({
        managed_service_key: managedService.serviceKey,
        managed_service_package_name: managedService.packageName,
        managed_service_directory: managedService.relativeDirectory,
        managed_service_manifest_path: managedService.manifestPath,
        managed_service_generated_at: null,
        managed_service_generation_plan_item_id: null,
        managed_service_generation_board_post_id: null,
        managed_service_recreate_requested: shouldRecreateManagedService,
        updated_at: db.fn.now(),
      })
      .returning('*');
    row = updatedRows[0] ?? row;
  }

  return row;
}

export async function deletePlanScheduledTask(id: number) {
  await ensurePlanScheduledTaskTable();

  const currentRow = await db(PLAN_SCHEDULED_TASK_TABLE).where({ id }).first();

  if (!currentRow) {
    return null;
  }

  await removeManagedServicePackage(id);
  await db(PLAN_SCHEDULED_TASK_TABLE).where({ id }).delete();

  return currentRow;
}

export async function registerDuePlanScheduledTasks(now = new Date()) {
  await ensurePlanTable();
  await ensurePlanScheduledTaskTable();

  const rows = await db(PLAN_SCHEDULED_TASK_TABLE).where({ enabled: true }).orderBy('id', 'asc');
  const registered = [];

  for (const row of rows.filter((item) => isScheduleDue(item, now))) {
    const registration = await registerPlanScheduledTaskRow(row, now);

    if (registration.createdPlan || registration.createdBoardPosts.length > 0) {
      registered.push(registration);
    }
  }

  return registered;
}

async function registerPlanScheduledTaskRow(row: Record<string, unknown>, now: Date) {
  const scheduleId = Number(row.id);

  if (!Number.isInteger(scheduleId) || scheduleId <= 0) {
    throw new Error('유효하지 않은 스케줄 ID입니다.');
  }

  if (!(await tryAcquirePlanScheduleRegistrationLock(scheduleId))) {
    return {
      createdPlan: null,
      createdBoardPosts: [],
    };
  }

  try {
    const executionMode = normalizeScheduleExecutionMode(row.execution_mode);
    const automationContextIds = parseAutomationContextIds(row.automation_context_ids_json);
    const shouldRefreshSnapshot =
      !row.context_snapshot_generated_at || normalizeBoolean(row.context_snapshot_refresh_requested, false);
    const scheduleSnapshot = shouldRefreshSnapshot
      ? await ensureSchedulePromptSnapshot({
          scheduleId: Number(row.id),
          workId: buildScheduledPlanWorkIdBase(row),
          note: String(row.note ?? ''),
          forceRefresh: true,
        })
      : {
          directory: `.auto_codex/schedule/${row.id}`,
          requestPath: `.auto_codex/schedule/${row.id}/request.md`,
          contextPath: `.auto_codex/schedule/${row.id}/context.md`,
          manifestPath: `.auto_codex/schedule/${row.id}/manifest.json`,
        };
    const managedServiceReady = await ensureManagedServiceExecutionReady({
      row,
      scheduleSnapshot,
      automationContextIds,
    });
    const effectiveRow = managedServiceReady.row;
    const scheduleNote = [
      String(effectiveRow.note ?? '').trim(),
      '',
      '## 스케줄 전용 참조 문서',
      `- ${scheduleSnapshot.requestPath}`,
      `- ${scheduleSnapshot.contextPath}`,
      '',
      '위 경로의 Markdown 문서를 먼저 읽고 처리하세요. 원본 소스/문서 재탐색은 꼭 필요한 경우에만 제한적으로 수행하세요.',
      executionMode === 'managed-service'
        ? [
            '',
            '## 스케줄 관리 서비스',
            `- 서비스 키: ${String(effectiveRow.managed_service_key ?? `schedule-${scheduleId}-service`)}`,
            `- 서비스 경로: ${String(effectiveRow.managed_service_directory ?? `.auto_codex/schedule/${scheduleId}`)}`,
            managedServiceReady.ready
              ? '- 현재 생성된 스케줄 전용 서비스 파일을 직접 실행합니다.'
              : `- 현재 서비스 패키지 생성 Plan을 ${managedServiceReady.generationTriggered ? '자동 접수했으며' : '이미 접수해 두었으며'} 생성 완료 전까지 실제 서비스 실행은 보류합니다.`,
            managedServiceReady.reason
              ? `- 생성 사유: ${managedServiceReady.reason === 'missing' ? '패키지 누락 감지' : '패키지 재생성 요청'}`
              : null,
            '- 서비스 구현은 Codex CLI가 `.auto_codex/schedule/{id}` 아래 파일을 생성한 결과물을 사용합니다.',
          ].join('\n')
        : null,
    ]
      .filter((value): value is string => Boolean(value))
      .join('\n')
      .trim();

    if (executionMode === 'managed-service') {
      if (!managedServiceReady.ready) {
        return {
          createdPlan: managedServiceReady.createdPlan,
          createdBoardPosts: managedServiceReady.createdBoardPosts,
        };
      }

      const managedServiceDirectory = String(
        effectiveRow.managed_service_directory ?? `.auto_codex/schedule/${scheduleId}`,
      );
      const managedServiceResult = await runManagedScheduleService(managedServiceDirectory);
      if (!managedServiceResult.ok) {
        throw new Error(
          `스케줄 서비스 실행 실패: ${buildManagedServiceFailureSummary(managedServiceResult)}`,
        );
      }

      const createdPlan = await createCompletedPlanExecutionLogItem({
        workId: buildScheduledPlanWorkIdBase(effectiveRow),
        note: scheduleNote,
        automationType: String(effectiveRow.automation_type_id ?? effectiveRow.automation_type ?? 'none'),
        automationContextIds,
        releaseTarget: String(effectiveRow.release_target ?? 'release'),
        jangsingProcessingRequired: Boolean(effectiveRow.jangsing_processing_required ?? true),
        autoDeployToMain: Boolean(effectiveRow.auto_deploy_to_main ?? true),
        suppressWebPush: Boolean(effectiveRow.suppress_web_push ?? false),
        repeatRequestEnabled: false,
        repeatIntervalMinutes: 60,
      });
      const managedServiceChangedFiles = [
        `${managedServiceDirectory}/README.md`,
        `${managedServiceDirectory}/service.ts`,
        `${managedServiceDirectory}/service.mjs`,
        `${managedServiceDirectory}/service-manifest.json`,
      ];

      await createPlanSourceWorkHistory(Number(createdPlan.id), {
        summary: [
          `스케줄 서비스 실행: schedule #${scheduleId}`,
          `서비스 키: ${String(effectiveRow.managed_service_key ?? `schedule-${scheduleId}-service`)}`,
          `결과: ${
            managedServiceResult.skipped
              ? `스킵 (${managedServiceResult.web.reason ?? managedServiceResult.ios.reason ?? '사유 없음'})`
              : `${managedServiceResult.itemCount}건 전송 시도`
          }`,
        ].join('\n'),
        branchName: String(createdPlan.releaseTarget ?? createdPlan.assignedBranch ?? 'main'),
        commitHash: null,
        changedFiles: managedServiceChangedFiles,
        commandLog: [
          `schedule-managed-service run scheduleId=${String(effectiveRow.id)}`,
          `servicePath=${managedServiceDirectory}/service.mjs`,
          `itemCount=${managedServiceResult.itemCount}`,
          `webSent=${managedServiceResult.web.sentCount}`,
          `webFailed=${managedServiceResult.web.failedCount}`,
          `skipped=${managedServiceResult.skipped ? 'true' : 'false'}`,
          `reason=${managedServiceResult.web.reason ?? managedServiceResult.ios.reason ?? ''}`,
        ].join('\n'),
        diffText: null,
        sourceFiles: [],
      });
      await createPlanActionHistory(
        Number(createdPlan.id),
        '스케줄서비스실행',
        `Plan 스케줄 #${scheduleId} 전용 서비스 파일을 직접 실행했습니다.`,
      );
      await db(PLAN_SCHEDULED_TASK_TABLE)
        .where({ id: scheduleId })
        .update({
          last_registered_at: now,
          context_snapshot_generated_at: now,
          context_snapshot_refresh_requested: false,
          managed_service_generated_at: db.fn.now(),
          updated_at: db.fn.now(),
        });

      return {
        createdPlan,
        createdBoardPosts: [],
      };
    }

    const boardPost = await createBoardPost({
      title: buildScheduledBoardPostTitle(effectiveRow),
      content: scheduleNote,
      attachments: [],
      automationType: String(effectiveRow.automation_type_id ?? effectiveRow.automation_type ?? 'none'),
      automationContextIds,
      requestExecutionMode: 'all_at_once',
      requestItems: [],
    });

    await db(PLAN_SCHEDULED_TASK_TABLE)
      .where({ id: scheduleId })
      .update({
        last_registered_at: now,
        context_snapshot_generated_at: now,
        context_snapshot_refresh_requested: false,
        updated_at: db.fn.now(),
      });
    return {
      createdPlan: null,
      createdBoardPosts: [boardPost],
    };
  } finally {
    await releasePlanScheduleRegistrationLock(scheduleId);
  }
}

export async function registerPlanScheduledTaskNow(
  id: number,
  now = new Date(),
  options?: {
    ignoreScheduleDue?: boolean;
    forceManagedServiceGeneration?: boolean;
  },
) {
  await ensurePlanTable();
  await ensurePlanScheduledTaskTable();

  const row = options?.forceManagedServiceGeneration
    ? await db(PLAN_SCHEDULED_TASK_TABLE).where({ id }).first()
    : await db(PLAN_SCHEDULED_TASK_TABLE).where({ id, enabled: true }).first();

  if (
    !row
    || (!options?.forceManagedServiceGeneration && !options?.ignoreScheduleDue && !isScheduleDue(row, now))
  ) {
    return null;
  }

  return registerPlanScheduledTaskRow(row, now);
}
