import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { z } from 'zod';
import { hasErrorLogViewAccessToken } from '../services/error-log-service.js';
import { notifyPlanEvent } from '../services/plan-notification-service.js';
import { shouldNotifyPlanRestart } from '../services/plan-notification-policy.js';
import {
  PLAN_ACTION_TABLE,
  PLAN_ISSUE_TABLE,
  PLAN_RELEASE_REVIEW_TABLE,
  PLAN_SOURCE_WORK_TABLE,
  PLAN_TABLE,
  appendLatestIssueAction,
  cancelPlanRelease,
  createPlanItem,
  createPlanActionHistory,
  createPlanSourceWorkHistory,
  createPlanSchema,
  deletePlanItem,
  ensurePlanTable,
  getPlanSourceWorkHistory,
  getBoardPostLinkedToPlanItem,
  getPlanItemById,
  formatPlanNotificationLabel,
  issueActionSchema,
  listPlanActionHistories,
  listPlanIssueHistories,
  listPlanItems,
  listPlanReleaseReviewBoardItems,
  listPlanSourceWorkHistories,
  listPlanQuerySchema,
  mapPlanActionRow,
  mapPlanIssueRow,
  mapPlanSourceWorkRow,
  markPlanAsStarted,
  planStatuses,
  markPlanAsCompleted,
  markPlanAsDevelopmentComplete,
  queuePlanRetryFromFailure,
  queuePlanRetryFromIssueAction,
  requestPlanMainMerge,
  resumePlanDevelopmentFromRelease,
  retryPlanBranch,
  retryPlanWork,
  retryPlanMerge,
  setupSchema,
  updatePlanReleaseReviewSchema,
  upsertPlanReleaseReview,
  updatePlanItem,
  updatePlanItemJangsingProcessingRequired,
  updatePlanJangsingProcessingSchema,
  updatePlanSchema,
} from '../services/plan-service.js';
import { db } from '../db/client.js';
import { getEnv } from '../config/env.js';
import { recreateReleaseBranchFromMain } from '../services/git-service.js';
import { registerErrorLogBoardPosts } from '../services/error-log-plan-registration-service.js';
import {
  PLAN_SCHEDULED_TASK_TABLE,
  createPlanScheduledTask,
  createPlanScheduledTaskSchema,
  deletePlanScheduledTask,
  ensurePlanScheduledTaskTable,
  getPlanScheduledTaskById,
  listPlanScheduledTasks,
  mapPlanScheduledTaskRow,
  registerPlanScheduledTaskNow,
  syncManagedServiceGenerationCompletion,
  updatePlanScheduledTask,
  updatePlanScheduledTaskSchema,
} from '../services/plan-schedule-service.js';
import { getVisitorClientByClientId } from '../services/visitor-history-service.js';
import { progressBoardPostAutomationByPlanResult } from '../services/board-service.js';

const completeActionSchema = z.object({
  note: z.string().trim().min(1).optional(),
});

const actionNoteSchema = z.object({
  actionNote: z.string().trim().min(1),
  actionType: z.string().trim().min(1).optional(),
});

const createSourceWorkSchema = z.object({
  summary: z.string().trim().min(1),
  branchName: z.string().trim().min(1),
  commitHash: z.string().trim().min(1).nullable().optional(),
  previewUrl: z.string().trim().url().nullable().optional(),
  changedFiles: z.array(z.string()).default([]),
  commandLog: z.string().nullable().optional(),
  diffText: z.string().nullable().optional(),
  sourceFiles: z
    .array(
      z.object({
        path: z.string().trim().min(1),
        previousPath: z.string().trim().min(1).nullable().optional(),
        status: z.enum(['added', 'modified', 'deleted', 'renamed', 'binary', 'unknown']),
        language: z.string().trim().min(1),
        content: z.string(),
      }),
    )
    .default([]),
});

export async function registerPlanRoutes(app: FastifyInstance) {
  function getRequestTraceContext(request: FastifyRequest) {
    return {
      ip: request.ip,
      remoteAddress: request.raw.socket.remoteAddress,
      host: request.headers.host,
      origin: request.headers.origin,
      referer: request.headers.referer,
      userAgent: request.headers['user-agent'],
      clientId: request.headers['x-client-id'],
    };
  }

  function isLoopbackAddress(value: string | null | undefined) {
    const normalizedValue = String(value ?? '').trim();
    return normalizedValue === '127.0.0.1' || normalizedValue === '::1' || normalizedValue === '::ffff:127.0.0.1';
  }

  function hasPlanAccessToken(accessToken: string | string[] | undefined) {
    return hasErrorLogViewAccessToken(accessToken);
  }

  function hasPlanAccess(request: { headers: Record<string, unknown>; ip?: string; socket?: { remoteAddress?: string | null }; raw?: { socket?: { remoteAddress?: string | null } } }) {
    if (hasPlanAccessToken(request.headers['x-access-token'] as string | string[] | undefined)) {
      return true;
    }

    return isLoopbackAddress(request.ip) || isLoopbackAddress(request.raw?.socket?.remoteAddress) || isLoopbackAddress(request.socket?.remoteAddress);
  }

  function requirePlanAccessToken(
    request: { headers: Record<string, unknown>; ip?: string; socket?: { remoteAddress?: string | null }; raw?: { socket?: { remoteAddress?: string | null } } },
    reply: Parameters<FastifyInstance['get']>[1] extends (request: any, reply: infer T) => any ? T : any,
  ) {
    if (hasPlanAccess(request)) {
      return true;
    }

    void reply.code(403).send({
      message: '권한 토큰이 등록된 사용자만 수정할 수 있습니다.',
    });
    return false;
  }

  async function handleListPlanScheduledTasks(request: FastifyRequest, reply: FastifyReply) {
    if (!hasPlanAccess(request)) {
      return reply.code(403).send({
        message: '권한 토큰이 등록된 사용자만 스케줄을 사용할 수 있습니다.',
      });
    }

    const rows = await listPlanScheduledTasks();

    return {
      items: rows.map(mapPlanScheduledTaskRow),
    };
  }

  async function handleCreatePlanScheduledTask(request: FastifyRequest, reply: FastifyReply) {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    const payload = createPlanScheduledTaskSchema.parse(request.body ?? {});
    const row = await createPlanScheduledTask(payload);
    const ignoreScheduleDueForImmediateRegistration =
      payload.repeatWindows.length === 0
      && payload.scheduleWeekdays.length === 0
      && payload.scheduleDateRanges.length === 0;
    const immediateRegistration =
      payload.executionMode === 'managed-service' && payload.recreateManagedServiceOnNextSave
        ? await registerPlanScheduledTaskNow(Number(row.id), new Date(), {
            forceManagedServiceGeneration: true,
          })
        : payload.enabled && payload.immediateRunEnabled
        ? await registerPlanScheduledTaskNow(Number(row.id), new Date(), {
            ignoreScheduleDue: ignoreScheduleDueForImmediateRegistration,
          })
        : null;
    const latestRow = await getPlanScheduledTaskById(Number(row.id));

    return {
      ok: true,
      item: mapPlanScheduledTaskRow(latestRow ?? row),
      registeredPlan: immediateRegistration?.createdPlan ? await getPlanItemById(Number(immediateRegistration.createdPlan.id)) : null,
      registeredBoardPosts: immediateRegistration?.createdBoardPosts ?? [],
    };
  }

  async function handleGetPlanScheduledTask(request: FastifyRequest, reply: FastifyReply) {
    if (!hasPlanAccess(request)) {
      return reply.code(403).send({
        message: '권한 토큰이 등록된 사용자만 스케줄을 사용할 수 있습니다.',
      });
    }

    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    const row = await getPlanScheduledTaskById(id);

    if (!row) {
      return reply.code(404).send({
        message: '스케줄을 찾을 수 없습니다.',
      });
    }

    return {
      item: mapPlanScheduledTaskRow(row),
    };
  }

  async function handleUpdatePlanScheduledTask(request: FastifyRequest, reply: FastifyReply) {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    const payload = updatePlanScheduledTaskSchema.parse(request.body ?? {});
    const row = await updatePlanScheduledTask(id, payload);

    if (!row) {
      return reply.code(404).send({
        message: '수정할 스케줄을 찾을 수 없습니다.',
      });
    }

    const shouldTriggerImmediateRegistration = (
      row
      && String(row.execution_mode ?? '') === 'managed-service'
      && payload.recreateManagedServiceOnNextSave === true
    ) || (
      row
      && Boolean(row.enabled ?? true)
      && Boolean(row.immediate_run_enabled ?? true)
      && payload.enabled !== false
    );
    const effectiveRepeatWindows =
      payload.repeatWindows !== undefined
        ? payload.repeatWindows
        : (row ? mapPlanScheduledTaskRow(row).repeatWindows : []);
    const effectiveScheduleDateRanges =
      payload.scheduleDateRanges !== undefined
        ? payload.scheduleDateRanges
        : (row ? mapPlanScheduledTaskRow(row).scheduleDateRanges : []);
    const effectiveScheduleWeekdays =
      payload.scheduleWeekdays !== undefined
        ? payload.scheduleWeekdays
        : (row ? mapPlanScheduledTaskRow(row).scheduleWeekdays : []);
    const immediateRegistration = shouldTriggerImmediateRegistration
        ? await registerPlanScheduledTaskNow(id, new Date(), {
          ignoreScheduleDue:
            payload.recreateManagedServiceOnNextSave === true
              ? false
              : effectiveRepeatWindows.length === 0
                && effectiveScheduleWeekdays.length === 0
                && effectiveScheduleDateRanges.length === 0,
          forceManagedServiceGeneration: payload.recreateManagedServiceOnNextSave === true,
        })
      : null;
    const latestRow = await getPlanScheduledTaskById(Number(id));

    return {
      ok: true,
      item: mapPlanScheduledTaskRow(latestRow ?? row),
      registeredPlan: immediateRegistration?.createdPlan ? await getPlanItemById(Number(immediateRegistration.createdPlan.id)) : null,
      registeredBoardPosts: immediateRegistration?.createdBoardPosts ?? [],
    };
  }

  app.post('/api/plan/registrations/error-logs', async (request, reply) => {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    const payload = z.object({
      rangeStart: z.coerce.date().optional(),
      rangeEnd: z.coerce.date().optional(),
      maxGroups: z.coerce.number().int().min(1).max(24).optional(),
    }).parse(request.body ?? {});

    const result = await registerErrorLogBoardPosts(payload);

    return {
      ok: true,
      rangeStart: result.rangeStart.toISOString(),
      rangeEnd: result.rangeEnd.toISOString(),
      recentLogCount: result.recentLogs.length,
      candidateCount: result.candidates.length,
      rawCandidateCount: result.rawCandidates.length,
      createdBoardPosts: result.createdPosts,
      skippedBoardPosts: result.skippedPosts,
    };
  });

  async function handleDeletePlanScheduledTask(request: FastifyRequest, reply: FastifyReply) {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    const row = await deletePlanScheduledTask(id);

    if (!row) {
      return reply.code(404).send({
        message: '삭제할 스케줄을 찾을 수 없습니다.',
      });
    }

    return {
      ok: true,
      id,
    };
  }

  app.get('/api/plan/statuses', async () => ({
    items: planStatuses,
  }));

  app.post('/api/plan/setup', async (request) => {
    const payload = setupSchema.parse(request.body ?? {});

    if (payload.recreate) {
      await db.schema.dropTableIfExists(PLAN_SCHEDULED_TASK_TABLE);
      await db.schema.dropTableIfExists(PLAN_ACTION_TABLE);
      await db.schema.dropTableIfExists(PLAN_ISSUE_TABLE);
      await db.schema.dropTableIfExists(PLAN_SOURCE_WORK_TABLE);
      await db.schema.dropTableIfExists(PLAN_TABLE);
    }

    await ensurePlanTable();
    await ensurePlanScheduledTaskTable();

    return {
      ok: true,
      table: PLAN_TABLE,
      scheduleTable: PLAN_SCHEDULED_TASK_TABLE,
      releaseReviewTable: PLAN_RELEASE_REVIEW_TABLE,
      statuses: planStatuses,
    };
  });

  app.get('/api/plan/release-reviews', async (request) => {
    const items = await listPlanReleaseReviewBoardItems({
      maskNote: !hasPlanAccess(request),
    });

    return {
      items,
    };
  });

  app.patch('/api/plan/release-reviews/:planItemId', async (request, reply) => {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    const planItemId = z.coerce.number().int().positive().parse((request.params as { planItemId: string }).planItemId);
    const payload = updatePlanReleaseReviewSchema.parse(request.body ?? {});
    const clientId = String(request.headers['x-client-id'] ?? '').trim();
    const visitor = clientId ? await getVisitorClientByClientId(clientId) : null;
    const review = await upsertPlanReleaseReview(planItemId, payload, {
      clientId: clientId || null,
      nickname: visitor?.nickname ?? null,
    });

    if (!review) {
      return reply.code(404).send({
        message: '검수 대상을 찾을 수 없습니다.',
      });
    }

    return {
      ok: true,
      item: review,
    };
  });

  app.get('/api/plan/scheduled-tasks', handleListPlanScheduledTasks);
  app.get('/api/plan/schedule/tasks', handleListPlanScheduledTasks);
  app.get('/api/plan/schedule', handleListPlanScheduledTasks);
  app.get('/api/plan/schedules', handleListPlanScheduledTasks);
  app.get('/api/plans/scheduled-tasks', handleListPlanScheduledTasks);
  app.get('/api/plans/schedule/tasks', handleListPlanScheduledTasks);
  app.get('/api/plans/schedule', handleListPlanScheduledTasks);
  app.get('/api/plans/schedules', handleListPlanScheduledTasks);
  app.get('/api/plan/scheduled-tasks/:id', handleGetPlanScheduledTask);
  app.get('/api/plan/schedule/tasks/:id', handleGetPlanScheduledTask);
  app.get('/api/plan/schedule/:id', handleGetPlanScheduledTask);
  app.get('/api/plan/schedules/:id', handleGetPlanScheduledTask);
  app.get('/api/plans/scheduled-tasks/:id', handleGetPlanScheduledTask);
  app.get('/api/plans/schedule/tasks/:id', handleGetPlanScheduledTask);
  app.get('/api/plans/schedule/:id', handleGetPlanScheduledTask);
  app.get('/api/plans/schedules/:id', handleGetPlanScheduledTask);
  app.post('/api/plan/scheduled-tasks', handleCreatePlanScheduledTask);
  app.post('/api/plan/schedule/tasks', handleCreatePlanScheduledTask);
  app.post('/api/plan/schedule', handleCreatePlanScheduledTask);
  app.post('/api/plan/schedules', handleCreatePlanScheduledTask);
  app.post('/api/plans/scheduled-tasks', handleCreatePlanScheduledTask);
  app.post('/api/plans/schedule/tasks', handleCreatePlanScheduledTask);
  app.post('/api/plans/schedule', handleCreatePlanScheduledTask);
  app.post('/api/plans/schedules', handleCreatePlanScheduledTask);
  app.patch('/api/plan/scheduled-tasks/:id', handleUpdatePlanScheduledTask);
  app.patch('/api/plan/schedule/tasks/:id', handleUpdatePlanScheduledTask);
  app.patch('/api/plan/schedule/:id', handleUpdatePlanScheduledTask);
  app.patch('/api/plan/schedules/:id', handleUpdatePlanScheduledTask);
  app.patch('/api/plans/scheduled-tasks/:id', handleUpdatePlanScheduledTask);
  app.patch('/api/plans/schedule/tasks/:id', handleUpdatePlanScheduledTask);
  app.patch('/api/plans/schedule/:id', handleUpdatePlanScheduledTask);
  app.patch('/api/plans/schedules/:id', handleUpdatePlanScheduledTask);
  app.delete('/api/plan/scheduled-tasks/:id', handleDeletePlanScheduledTask);
  app.delete('/api/plan/schedule/tasks/:id', handleDeletePlanScheduledTask);
  app.delete('/api/plan/schedule/:id', handleDeletePlanScheduledTask);
  app.delete('/api/plan/schedules/:id', handleDeletePlanScheduledTask);
  app.delete('/api/plans/scheduled-tasks/:id', handleDeletePlanScheduledTask);
  app.delete('/api/plans/schedule/tasks/:id', handleDeletePlanScheduledTask);
  app.delete('/api/plans/schedule/:id', handleDeletePlanScheduledTask);
  app.delete('/api/plans/schedules/:id', handleDeletePlanScheduledTask);

  app.get('/api/plan/items', async (request, reply) => {
    const parsedQuery = listPlanQuerySchema.safeParse(request.query ?? {});

    if (!parsedQuery.success) {
      return reply.code(400).send({
        message: '유효하지 않은 status 쿼리입니다.',
      });
    }

    const query = parsedQuery.data;
    const items = await listPlanItems(query.status, {
      maskNote: !hasPlanAccess(request),
    });

    return {
      items,
    };
  });

  app.get('/api/plan/items/:id', async (request, reply) => {
    const hasAccess = hasPlanAccess(request);
    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    const row = await getPlanItemById(id, {
      maskNote: !hasAccess,
    });

    if (!row) {
      return reply.code(404).send({
        message: '작업 항목을 찾을 수 없습니다.',
      });
    }

    return {
      item: row,
    };
  });

  app.post('/api/plan/items', async (request, reply) => {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    try {
      const payload = createPlanSchema.parse(request.body ?? {});
      const createdRow = await createPlanItem(payload);
      const row = await getPlanItemById(Number(createdRow.id));

      return {
        ok: true,
        item: row,
      };
    } catch (error) {
      return reply.code(409).send({
        message: error instanceof Error ? error.message : '작업 항목 등록에 실패했습니다.',
      });
    }
  });

  app.patch('/api/plan/items/:id', async (request, reply) => {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    try {
      const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
      const payload = updatePlanSchema.parse(request.body ?? {});
      const updatedRow = await updatePlanItem(id, payload);

      if (!updatedRow) {
        return reply.code(404).send({
          message: '수정할 작업 항목을 찾을 수 없습니다.',
        });
      }

      const row = await getPlanItemById(id);

      return {
        ok: true,
        item: row,
      };
    } catch (error) {
      return reply.code(409).send({
        message: error instanceof Error ? error.message : '작업 항목 수정에 실패했습니다.',
      });
    }
  });

  app.patch('/api/plan/items/:id/jangsing-processing', async (request, reply) => {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    try {
      const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
      const payload = updatePlanJangsingProcessingSchema.parse(request.body ?? {});
      const updatedRow = await updatePlanItemJangsingProcessingRequired(id, payload.jangsingProcessingRequired);

      if (!updatedRow) {
        return reply.code(404).send({
          message: '수정할 작업 항목을 찾을 수 없습니다.',
        });
      }

      return {
        ok: true,
        item: await getPlanItemById(id),
      };
    } catch (error) {
      return reply.code(409).send({
        message: error instanceof Error ? error.message : '기능동작확인 수정에 실패했습니다.',
      });
    }
  });

  app.delete('/api/plan/items/:id', async (request, reply) => {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    request.log.warn(
      {
        planItemId: id,
        trace: getRequestTraceContext(request),
      },
      'Plan item delete requested',
    );
    const linkedBoardPost = await getBoardPostLinkedToPlanItem(id);

    if (linkedBoardPost) {
      request.log.warn(
        {
          planItemId: id,
          boardPostId: linkedBoardPost.id,
          boardPostTitle: linkedBoardPost.title,
          trace: getRequestTraceContext(request),
        },
        'Plan item delete blocked because it is linked to a board post',
      );

      return reply.code(409).send({
        message: `자동화 접수된 항목은 삭제할 수 없습니다. 연결 게시글 #${linkedBoardPost.id}`,
      });
    }

    const row = await deletePlanItem(id);

    if (!row) {
      return reply.code(404).send({
        message: '삭제할 작업 항목을 찾을 수 없습니다.',
      });
    }

    return {
      ok: true,
      id,
    };
  });

  app.post('/api/plan/items/:id/actions/complete-development', async (request, reply) => {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    const row = await markPlanAsDevelopmentComplete(id);

    if (!row) {
      return reply.code(404).send({
        message: '작업 항목을 찾을 수 없습니다.',
      });
    }

    await syncManagedServiceGenerationCompletion(id);

    const planLabel = formatPlanNotificationLabel(String(row.work_id), id);
    await notifyPlanEvent(
      id,
      `[${planLabel}] release 반영 대기`,
      '수동 작업완료로 release 반영 대기 상태가 되었습니다.',
      'development-completed',
    );
    await progressBoardPostAutomationByPlanResult(id, 'completed');

    return {
      ok: true,
      item: await getPlanItemById(id),
    };
  });

  app.post('/api/plan/items/:id/actions/complete', async (request, reply) => {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    const payload = completeActionSchema.parse(request.body ?? {});
    const row = await markPlanAsCompleted(id, payload.note);

    if (!row) {
      return reply.code(404).send({
        message: '작업 항목을 찾을 수 없습니다.',
      });
    }

    await syncManagedServiceGenerationCompletion(id);

    const planLabel = formatPlanNotificationLabel(String(row.work_id), id);
    await notifyPlanEvent(
      id,
      `[${planLabel}] 완료 처리`,
      payload.note ?? '작업이 완료 처리되었습니다.',
      'plan-completed',
    );
    await progressBoardPostAutomationByPlanResult(id, 'completed');

    return {
      ok: true,
      item: await getPlanItemById(id),
    };
  });

  app.post('/api/plan/items/:id/actions/start-work', async (request, reply) => {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    const row = await markPlanAsStarted(id);

    if (!row) {
      return reply.code(404).send({
        message: '작업 항목을 찾을 수 없습니다.',
      });
    }

    const planLabel = formatPlanNotificationLabel(String(row.work_id), id);
    await notifyPlanEvent(
      id,
      `[${planLabel}] 작업시작`,
      '작업이 시작되었습니다.',
      'work-started',
    );

    return {
      ok: true,
      item: await getPlanItemById(id),
    };
  });

  app.post('/api/plan/items/:id/actions/retry-branch', async (request, reply) => {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    const row = await retryPlanBranch(id);

    if (!row) {
      return reply.code(404).send({
        message: '작업 항목을 찾을 수 없습니다.',
      });
    }

    const planLabel = formatPlanNotificationLabel(String(row.work_id), id);
    await notifyPlanEvent(
      id,
      `[${planLabel}] 작업 재시작`,
      '브랜치 재시도를 요청했습니다.',
      'plan-restarted',
    );

    return {
      ok: true,
      item: await getPlanItemById(id),
    };
  });

  app.post('/api/plan/items/:id/actions/retry-work', async (request, reply) => {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    const row = await retryPlanWork(id);

    if (!row) {
      return reply.code(404).send({
        message: '작업 항목을 찾을 수 없습니다.',
      });
    }

    const planLabel = formatPlanNotificationLabel(String(row.work_id), id);
    await notifyPlanEvent(
      id,
      `[${planLabel}] 작업 재시작`,
      '자동 작업 재처리를 요청했습니다.',
      'plan-restarted',
    );

    return {
      ok: true,
      item: await getPlanItemById(id),
    };
  });

  app.post('/api/plan/items/:id/actions/retry-merge', async (request, reply) => {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    const row = await retryPlanMerge(id);

    if (!row) {
      return reply.code(404).send({
        message: '작업 항목을 찾을 수 없습니다.',
      });
    }

    const planLabel = formatPlanNotificationLabel(String(row.work_id), id);
    await notifyPlanEvent(
      id,
      `[${planLabel}] 작업 재시작`,
      row.worker_status === 'main반영대기' ? 'main 반영 재시도를 요청했습니다.' : 'release 반영 재시도를 요청했습니다.',
      'plan-restarted',
    );

    return {
      ok: true,
      item: await getPlanItemById(id),
    };
  });

  app.post('/api/plan/items/:id/actions/cancel-release', async (request, reply) => {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    const item = await getPlanItemById(id);

    if (!item) {
      return reply.code(404).send({
        message: '작업 항목을 찾을 수 없습니다.',
      });
    }

    try {
      const env = getEnv();
      const isReleaseMergeFailure = item.status === '작업완료' && item.workerStatus === 'release반영실패';
      const sourceWorkCount = Math.max(0, Number(item.usageSnapshot?.sourceWorkCount ?? 0) || 0);
      const requiresRollbackBeforeCancel =
        sourceWorkCount > 0 &&
        (item.status === '릴리즈완료' || item.workerStatus === 'main반영실패');

      if (!isReleaseMergeFailure && requiresRollbackBeforeCancel) {
        await recreateReleaseBranchFromMain(
          {
            repoPath: env.PLAN_GIT_REPO_PATH,
            releaseBranch: env.PLAN_RELEASE_BRANCH,
            mainBranch: env.PLAN_MAIN_BRANCH,
          },
          String(item.releaseTarget ?? env.PLAN_RELEASE_BRANCH),
        );
      }

      const result = await cancelPlanRelease(id);

      return {
        ok: true,
        item: result?.item ?? (await getPlanItemById(id)),
        message: result?.message,
      };
    } catch (error) {
      return reply.code(409).send({
        message: error instanceof Error ? error.message : 'release 작업취소 처리에 실패했습니다.',
      });
    }
  });

  app.post('/api/plan/items/:id/actions/request-main-merge', async (request, reply) => {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    const result = await requestPlanMainMerge(id);

    if (!result) {
      return reply.code(404).send({
        message: '작업 항목을 찾을 수 없습니다.',
      });
    }

    return {
      ok: true,
      item: result.item,
      message: result.message,
    };
  });

  app.get('/api/plan/items/:id/issues', async (request, reply) => {
    if (!hasPlanAccess(request)) {
      return reply.code(403).send({
        message: '상세 조회 권한이 없습니다.',
      });
    }

    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    const item = await getPlanItemById(id);

    if (!item) {
      return reply.code(404).send({
        message: '작업 항목을 찾을 수 없습니다.',
      });
    }

    const rows = await listPlanIssueHistories(id);

    return {
      items: rows.map(mapPlanIssueRow),
    };
  });

  app.post('/api/plan/items/:id/issues/action', async (request, reply) => {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    const payload = issueActionSchema.parse(request.body ?? {});
    const item = await getPlanItemById(id);

    if (!item) {
      return reply.code(404).send({
        message: '작업 항목을 찾을 수 없습니다.',
      });
    }

    try {
      const row = await appendLatestIssueAction(id, payload.actionNote, payload.resolve);
      const retryResult = await queuePlanRetryFromIssueAction(id, payload.actionNote, payload.retry);

      if (payload.resolve) {
        const planLabel = formatPlanNotificationLabel(String(item.workId), id);
        await notifyPlanEvent(
          id,
          `[${planLabel}] 이슈 해결 처리`,
          `${row.issue_tag} 이슈가 해결 처리되었습니다.`,
          'issue-resolved',
        );
      }

      if (shouldNotifyPlanRestart(retryResult)) {
        const planLabel = formatPlanNotificationLabel(String(item.workId), id);
        await notifyPlanEvent(
          id,
          `[${planLabel}] 작업 재시작`,
          retryResult?.message ?? '작업 재시작을 요청했습니다.',
          'plan-restarted',
        );
      }

      return {
        ok: true,
        item: mapPlanIssueRow(row),
        planItem: retryResult?.item ?? (await getPlanItemById(id)),
        message: retryResult?.message,
      };
    } catch (error) {
      return reply.code(409).send({
        message: error instanceof Error ? error.message : '이슈 조치 기록 저장에 실패했습니다.',
      });
    }
  });

  app.get('/api/plan/items/:id/actions', async (request, reply) => {
    if (!hasPlanAccess(request)) {
      return reply.code(403).send({
        message: '상세 조회 권한이 없습니다.',
      });
    }

    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    const item = await getPlanItemById(id);

    if (!item) {
      return reply.code(404).send({
        message: '작업 항목을 찾을 수 없습니다.',
      });
    }

    const rows = await listPlanActionHistories(id);

    return {
      items: rows.map(mapPlanActionRow),
    };
  });

  app.get('/api/plan/items/:id/source-works', async (request, reply) => {
    if (!hasPlanAccess(request)) {
      return reply.code(403).send({
        message: '상세 조회 권한이 없습니다.',
      });
    }

    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    const item = await getPlanItemById(id);

    if (!item) {
      return reply.code(404).send({
        message: '작업 항목을 찾을 수 없습니다.',
      });
    }

    const rows = await listPlanSourceWorkHistories(id);

    return {
      items: rows.map(mapPlanSourceWorkRow),
    };
  });

  app.get('/api/plan/items/:id/source-works/:sourceWorkId', async (request, reply) => {
    if (!hasPlanAccess(request)) {
      return reply.code(403).send({
        message: '상세 조회 권한이 없습니다.',
      });
    }

    const params = z.object({
      id: z.coerce.number().int().positive(),
      sourceWorkId: z.coerce.number().int().positive(),
    }).parse(request.params);
    const row = await getPlanSourceWorkHistory(params.id, params.sourceWorkId);

    if (!row) {
      return reply.code(404).send({
        message: '소스 작업 이력을 찾을 수 없습니다.',
      });
    }

    return {
      item: mapPlanSourceWorkRow(row),
    };
  });

  app.post('/api/plan/items/:id/source-works', async (request, reply) => {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    const payload = createSourceWorkSchema.parse(request.body ?? {});
    const item = await getPlanItemById(id);

    if (!item) {
      return reply.code(404).send({
        message: '작업 항목을 찾을 수 없습니다.',
      });
    }

    const row = await createPlanSourceWorkHistory(id, payload);

    return {
      ok: true,
      item: mapPlanSourceWorkRow(row),
    };
  });

  app.post('/api/plan/items/:id/actions/note', async (request, reply) => {
    if (!requirePlanAccessToken(request, reply)) {
      return;
    }

    const id = z.coerce.number().int().positive().parse((request.params as { id: string }).id);
    const payload = actionNoteSchema.parse(request.body ?? {});
    const item = await getPlanItemById(id);

    if (!item) {
      return reply.code(404).send({
        message: '작업 항목을 찾을 수 없습니다.',
      });
    }

    if (!item.startedAt) {
      return reply.code(409).send({
        message: '작업시작 이후부터 조치 이력을 기록할 수 있습니다.',
      });
    }

    const row = await createPlanActionHistory(id, payload.actionType ?? '추가조치', payload.actionNote);
    const releaseResumeResult = await resumePlanDevelopmentFromRelease(id, payload.actionNote);
    const retryResult = releaseResumeResult?.message
      ? releaseResumeResult
      : await queuePlanRetryFromFailure(id, payload.actionNote);

    if (shouldNotifyPlanRestart(retryResult)) {
      const planLabel = formatPlanNotificationLabel(String(item.workId), id);
      await notifyPlanEvent(
        id,
        `[${planLabel}] 작업 재시작`,
        retryResult?.message ?? '작업 재시작을 요청했습니다.',
        'plan-restarted',
      );
    }

    return {
      ok: true,
      item: mapPlanActionRow(row),
      planItem: retryResult?.item ?? (await getPlanItemById(id)),
      message: retryResult?.message,
    };
  });
}
