import { z } from 'zod';
import { db } from '../db/client.js';
import {
  ensurePlanTable,
  normalizePlanAutomationType,
  normalizePlanWorkId,
  PLAN_TABLE,
  planAutomationTypeSchema,
} from './plan-service.js';
import {
  resolveAutomationType,
  resolveStoredAutomationTypeId,
  type AutomationTypeRecord,
} from './automation-type-config-service.js';
import {
  buildAutomationNoteSections,
  parseAutomationContextIds,
  stringifyAutomationContextIds,
} from './automation-context-service.js';
import type { AutomationContextRecord } from './automation-context-config-service.js';

export const BOARD_POSTS_TABLE = 'board_posts';
export const BOARD_POST_REQUESTS_TABLE = 'board_post_requests';

const boardRequestExecutionModes = ['all_at_once', 'after_previous_finished', 'after_previous_success'] as const;
const boardRequestWorkflowStates = ['pending', 'waiting', 'registered', 'completed', 'failed', 'blocked'] as const;
const planFailureWorkerStatuses = new Set(['브랜치실패', '자동작업실패', 'release반영실패', 'main반영실패', '작업취소']);
const planRunningStatuses = new Set(['등록', '작업중', '작업완료', '릴리즈완료']);
const planRunningWorkerStatuses = new Set([
  '대기',
  '브랜치생성중',
  '브랜치준비',
  '자동작업중',
  'release반영대기',
  'release반영중',
  'release반영완료',
  'main반영대기',
  'main반영중',
  'main반영완료',
]);

export const boardPostRequestSchema = z.object({
  title: z.string().trim().min(1).max(200),
  content: z.string().min(1).max(200000),
});

export const boardPostRequestExecutionModeSchema = z.enum(boardRequestExecutionModes);

export const boardPostPayloadSchema = z.object({
  title: z.string().trim().min(1).max(200),
  content: z.string().max(200000).default(''),
  attachments: z.array(
    z.object({
      id: z.string().trim().min(1).max(120),
      name: z.string().trim().min(1).max(255),
      path: z.string().trim().min(1).max(2000),
      publicUrl: z.string().trim().min(1).max(2000),
      size: z.coerce.number().int().min(0).max(10 * 1024 * 1024),
      mimeType: z.string().trim().min(1).max(200),
    }),
  ).max(20).default([]),
  automationType: z.preprocess((value) => value, planAutomationTypeSchema.default('none')),
  automationContextIds: z.array(z.string().trim().min(1).max(160)).max(20).optional().default([]),
  requestExecutionMode: boardPostRequestExecutionModeSchema.default('all_at_once'),
  requestItems: z.array(boardPostRequestSchema).max(20).optional().default([]),
});

export type BoardPostRequestExecutionMode = z.infer<typeof boardPostRequestExecutionModeSchema>;
export type BoardPostRequestPayload = z.infer<typeof boardPostRequestSchema>;
export type BoardPostPayload = z.infer<typeof boardPostPayloadSchema>;
export type BoardPostPayloadInput = z.input<typeof boardPostPayloadSchema>;

export type BoardPostRequestWorkflowState = (typeof boardRequestWorkflowStates)[number];

export type BoardPostRequestItem = {
  id: number;
  boardPostId: number;
  sequence: number;
  title: string;
  content: string;
  planItemId: number | null;
  automationReceivedAt: string | null;
  workflowState: BoardPostRequestWorkflowState;
  status: 'pending' | 'waiting' | 'queued' | 'in_progress' | 'completed' | 'failed' | 'blocked';
  statusLabel: string;
  planStatus: string | null;
  workerStatus: string | null;
  lastError: string | null;
  createdAt: string;
  updatedAt: string;
};

export type BoardPostItem = {
  id: number;
  title: string;
  content: string;
  preview: string;
  attachments: z.infer<typeof boardPostPayloadSchema>['attachments'];
  automationType: z.infer<typeof boardPostPayloadSchema>['automationType'];
  automationPlanItemId: number | null;
  automationReceivedAt: string | null;
  automationContextIds: string[];
  requestExecutionMode: BoardPostRequestExecutionMode;
  requestItems: BoardPostRequestItem[];
  requestSummary: {
    total: number;
    completed: number;
    failed: number;
    running: number;
    queued: number;
    waiting: number;
    blocked: number;
  };
  createdAt: string;
  updatedAt: string;
};

export class BoardPostAutomationLockedError extends Error {
  constructor(action: 'update' | 'delete') {
    super(
      action === 'delete'
        ? '자동화 접수된 작업메모는 삭제할 수 없습니다.'
        : '자동화 접수된 작업메모는 수정할 수 없습니다.',
    );
    this.name = 'BoardPostAutomationLockedError';
  }
}

const MAX_SEQUENCED_PLAN_WORK_ID = 999;
const PLAN_WORK_ID_MAX_LENGTH = 120;

function createPreview(content: string) {
  const normalized = content
    .replace(/```[\s\S]*?```/g, ' ')
    .replace(/!\[[^\]]*\]\([^)]+\)/g, ' ')
    .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
    .replace(/[#>*_`~-]/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();

  return normalized.length > 140 ? `${normalized.slice(0, 137).trimEnd()}...` : normalized;
}

function normalizeBoardPostRequestItems(requestItems: BoardPostRequestPayload[], fallbackTitle: string, fallbackContent: string) {
  if (requestItems.length > 0) {
    return requestItems.map((item) => ({
      title: item.title.trim(),
      content: item.content.trim(),
    }));
  }

  return [
    {
      title: fallbackTitle.trim() || '요청 1',
      content: fallbackContent.trim(),
    },
  ];
}

function resolveBoardRequestWorkflowState(value: unknown): BoardPostRequestWorkflowState {
  return boardRequestWorkflowStates.includes(value as BoardPostRequestWorkflowState)
    ? (value as BoardPostRequestWorkflowState)
    : 'pending';
}

function resolveBoardRequestExecutionMode(value: unknown): BoardPostRequestExecutionMode {
  return boardRequestExecutionModes.includes(value as BoardPostRequestExecutionMode)
    ? (value as BoardPostRequestExecutionMode)
    : 'all_at_once';
}

function resolveBoardRequestStatus(args: {
  workflowState: BoardPostRequestWorkflowState;
  planStatus: string | null;
  workerStatus: string | null;
  lastError: string | null;
}) {
  const { workflowState, planStatus, workerStatus, lastError } = args;

  if (workflowState === 'blocked') {
    return { status: 'blocked' as const, statusLabel: '차단' };
  }

  if (planStatus === '완료' || workflowState === 'completed') {
    return { status: 'completed' as const, statusLabel: '완료' };
  }

  if (lastError || (workerStatus && planFailureWorkerStatuses.has(workerStatus)) || workflowState === 'failed') {
    return { status: 'failed' as const, statusLabel: '실패' };
  }

  if (
    (planStatus && planRunningStatuses.has(planStatus))
    || (workerStatus && planRunningWorkerStatuses.has(workerStatus))
  ) {
    return {
      status: planStatus === '등록' && workerStatus === '대기' ? 'queued' as const : 'in_progress' as const,
      statusLabel: planStatus === '등록' && workerStatus === '대기' ? '대기열' : '진행중',
    };
  }

  if (workflowState === 'waiting') {
    return { status: 'waiting' as const, statusLabel: '선행 대기' };
  }

  if (workflowState === 'registered' || args.planStatus || args.workerStatus) {
    return { status: 'queued' as const, statusLabel: '대기열' };
  }

  return { status: 'pending' as const, statusLabel: '미접수' };
}

function buildBoardAttachmentSection(attachments: z.infer<typeof boardPostPayloadSchema>['attachments']) {
  const lines = attachments
    .map((attachment) => {
      const label = attachment.name.trim() || attachment.path.trim().split('/').pop() || '첨부 파일';
      const path = attachment.path.trim();

      if (!path) {
        return null;
      }

      return `- ${label}: ${path}`;
    })
    .filter((line): line is string => Boolean(line));

  if (lines.length === 0) {
    return [];
  }

  return ['## 첨부 파일', ...lines];
}

export async function buildBoardPostPlanNote(
  title: string,
  content: string,
  attachments: z.infer<typeof boardPostPayloadSchema>['attachments'] = [],
  automationType?: Pick<AutomationTypeRecord, 'name'> | null,
  automationContextIds?: string[],
  availableContexts?: AutomationContextRecord[],
  requestItem?: Pick<BoardPostRequestPayload, 'title' | 'content'> | null,
  sequenceInfo?: { index: number; total: number } | null,
) {
  const note = await buildAutomationNoteSections({
    title: title.trim(),
    sourceLabel: 'board_posts 자동화 접수',
    requestContent: requestItem ? requestItem.content.trim() : content.trim(),
    attachments: attachments.length ? buildBoardAttachmentSection(attachments).slice(1) : [],
    automationType,
    availableContexts,
    selectedContextIds: automationContextIds,
  });

  if (!requestItem) {
    return note;
  }

  const commonContent = content.trim();
  const requestTitle = requestItem.title.trim();
  const requestContent = requestItem.content.trim();
  const lines: string[] = [
    '# 자동화 작업메모',
    '',
    `- 게시판 제목: ${title.trim()}`,
    '- 메모 출처: board_posts 자동화 접수',
    `- 하위 요청: ${requestTitle}`,
  ];

  if (sequenceInfo) {
    lines.push(`- 요청 순서: ${sequenceInfo.index}/${sequenceInfo.total}`);
  }

  if (automationType?.name) {
    lines.push(`- 선택 자동화 유형: ${automationType.name}`);
  }

  lines.push('- 자동화 처리 원칙: 아래에서 선택된 자동화 Context만 우선 참조합니다.', '', '## 자동화 Context');

  const contextSection = note.split('## 자동화 Context\n')[1] ?? '선택된 자동화 Context 없음\n\n## 요청 본문\n';
  const [contextBody] = contextSection.split('\n\n## 요청 본문\n');
  lines.push(contextBody.trim() || '선택된 자동화 Context 없음');

  if (commonContent) {
    lines.push('', '## 공통 메모', commonContent);
  }

  lines.push('', '## 요청 본문', requestContent);

  const attachmentSection = buildBoardAttachmentSection(attachments);
  if (attachmentSection.length > 0) {
    lines.push('', ...attachmentSection);
  }

  return lines.join('\n');
}

function buildSequencedPlanWorkId(baseWorkId: string, sequence: number, suffixLabel?: string | null) {
  const normalizedBaseWorkId = normalizePlanWorkId(baseWorkId);
  const normalizedSuffixLabel = String(suffixLabel ?? '').trim().replace(/^-+|-+$/g, '');
  const suffix = normalizedSuffixLabel ? `-${normalizedSuffixLabel}-${sequence}` : `-${sequence}`;
  const maxBaseLength = Math.max(1, PLAN_WORK_ID_MAX_LENGTH - suffix.length);
  const trimmedBaseWorkId = normalizedBaseWorkId.slice(0, maxBaseLength).trimEnd() || '작업ID';
  return `${trimmedBaseWorkId}${suffix}`;
}

export function resolveSequencedPlanWorkId(
  baseWorkId: string,
  existingWorkIds: Iterable<string>,
  suffixLabel?: string | null,
) {
  const existingWorkIdSet = new Set(
    Array.from(existingWorkIds, (value) => String(value ?? '').trim()).filter(Boolean),
  );

  for (let sequence = 1; sequence <= MAX_SEQUENCED_PLAN_WORK_ID; sequence += 1) {
    const candidate = buildSequencedPlanWorkId(baseWorkId, sequence, suffixLabel);

    if (!existingWorkIdSet.has(candidate)) {
      return candidate;
    }
  }

  throw new Error(`자동화 접수 ID suffix를 더 이상 생성할 수 없습니다. (${MAX_SEQUENCED_PLAN_WORK_ID}개 초과)`);
}

function resolveInsertedId(result: unknown): number | null {
  if (typeof result === 'number' && Number.isInteger(result) && result > 0) {
    return result;
  }

  if (Array.isArray(result)) {
    const first = result[0];

    if (typeof first === 'number' && Number.isInteger(first) && first > 0) {
      return first;
    }

    if (first && typeof first === 'object' && 'id' in first) {
      const id = Number((first as { id?: unknown }).id);
      return Number.isInteger(id) && id > 0 ? id : null;
    }
  }

  if (result && typeof result === 'object' && 'id' in result) {
    const id = Number((result as { id?: unknown }).id);
    return Number.isInteger(id) && id > 0 ? id : null;
  }

  return null;
}

function supportsReturning() {
  const clientName = String(db.client.config.client ?? '').toLowerCase();
  return ['pg', 'postgres', 'postgresql', 'sqlite3', 'better-sqlite3', 'oracledb', 'mssql'].includes(clientName);
}

function isDuplicateSchemaError(error: unknown, codes: string[], patterns: RegExp[]) {
  const candidate = error as { code?: unknown; message?: unknown };
  const code = typeof candidate?.code === 'string' ? candidate.code : '';
  const message = typeof candidate?.message === 'string' ? candidate.message : '';

  return codes.includes(code) || patterns.some((pattern) => pattern.test(message));
}

function isDuplicateTableError(error: unknown) {
  return isDuplicateSchemaError(error, ['42P07'], [/already exists/i]);
}

function isDuplicateColumnError(error: unknown) {
  return isDuplicateSchemaError(error, ['42701'], [/already exists/i, /duplicate column/i]);
}

async function ensureBoardPostRequestsTable() {
  const hasTable = await db.schema.hasTable(BOARD_POST_REQUESTS_TABLE);

  if (!hasTable) {
    try {
      await db.schema.createTable(BOARD_POST_REQUESTS_TABLE, (table) => {
        table.increments('id').primary();
        table.integer('board_post_id').notNullable().index();
        table.integer('sequence_no').notNullable().defaultTo(1);
        table.string('title', 200).notNullable();
        table.text('content').notNullable();
        table.integer('plan_item_id').nullable().index();
        table.timestamp('automation_received_at', { useTz: true }).nullable();
        table.string('workflow_state', 40).notNullable().defaultTo('pending');
        table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
        table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
      });
    } catch (error) {
      if (!isDuplicateTableError(error)) {
        throw error;
      }
    }
  }

  const requiredColumns: Array<[string, (table: any) => void]> = [
    ['board_post_id', (table) => table.integer('board_post_id').notNullable().index()],
    ['sequence_no', (table) => table.integer('sequence_no').notNullable().defaultTo(1)],
    ['title', (table) => table.string('title', 200).notNullable().defaultTo('요청 1')],
    ['content', (table) => table.text('content').notNullable().defaultTo('')],
    ['plan_item_id', (table) => table.integer('plan_item_id').nullable().index()],
    ['automation_received_at', (table) => table.timestamp('automation_received_at', { useTz: true }).nullable()],
    ['workflow_state', (table) => table.string('workflow_state', 40).notNullable().defaultTo('pending')],
    ['created_at', (table) => table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
    ['updated_at', (table) => table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
  ];

  for (const [columnName, createColumn] of requiredColumns) {
    const hasColumn = await db.schema.hasColumn(BOARD_POST_REQUESTS_TABLE, columnName);

    if (!hasColumn) {
      try {
        await db.schema.alterTable(BOARD_POST_REQUESTS_TABLE, (table) => {
          createColumn(table);
        });
      } catch (error) {
        if (!isDuplicateColumnError(error)) {
          throw error;
        }
      }
    }
  }
}

async function syncLegacyBoardPostRequests() {
  const rows = await db(BOARD_POSTS_TABLE)
    .leftJoin(BOARD_POST_REQUESTS_TABLE, `${BOARD_POSTS_TABLE}.id`, `${BOARD_POST_REQUESTS_TABLE}.board_post_id`)
    .whereNull(`${BOARD_POST_REQUESTS_TABLE}.id`)
    .select(
      `${BOARD_POSTS_TABLE}.id`,
      `${BOARD_POSTS_TABLE}.title`,
      `${BOARD_POSTS_TABLE}.content`,
      `${BOARD_POSTS_TABLE}.automation_plan_item_id`,
      `${BOARD_POSTS_TABLE}.automation_received_at`,
    );

  if (!rows.length) {
    return;
  }

  await db(BOARD_POST_REQUESTS_TABLE).insert(
    rows.map((row) => ({
      board_post_id: Number(row.id),
      sequence_no: 1,
      title: String(row.title ?? '').trim() || '요청 1',
      content: String(row.content ?? ''),
      plan_item_id:
        row.automation_plan_item_id === null || row.automation_plan_item_id === undefined
          ? null
          : Number(row.automation_plan_item_id),
      automation_received_at: row.automation_received_at ?? null,
      workflow_state: row.automation_plan_item_id || row.automation_received_at ? 'registered' : 'pending',
      created_at: db.fn.now(),
      updated_at: db.fn.now(),
    })),
  );
}

export async function ensureBoardPostsTable() {
  const hasTable = await db.schema.hasTable(BOARD_POSTS_TABLE);

  if (!hasTable) {
    try {
      await db.schema.createTable(BOARD_POSTS_TABLE, (table) => {
        table.increments('id').primary();
        table.string('title', 200).notNullable();
        table.text('content').notNullable();
        table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
        table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now());
      });
    } catch (error) {
      if (!isDuplicateTableError(error)) {
        throw error;
      }
    }
  }

  const requiredColumns: Array<[string, (table: any) => void]> = [
    ['title', (table) => table.string('title', 200).notNullable().defaultTo('제목 없음')],
    ['content', (table) => table.text('content').notNullable().defaultTo('')],
    ['automation_type', (table) => table.string('automation_type', 40).notNullable().defaultTo('none')],
    ['automation_type_id', (table) => table.string('automation_type_id', 120).nullable()],
    ['attachments_json', (table) => table.text('attachments_json').notNullable().defaultTo('[]')],
    ['automation_context_ids_json', (table) => table.text('automation_context_ids_json').notNullable().defaultTo('[]')],
    ['request_execution_mode', (table) => table.string('request_execution_mode', 40).notNullable().defaultTo('all_at_once')],
    ['automation_plan_item_id', (table) => table.integer('automation_plan_item_id').nullable()],
    ['automation_received_at', (table) => table.timestamp('automation_received_at', { useTz: true }).nullable()],
    ['created_at', (table) => table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
    ['updated_at', (table) => table.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(db.fn.now())],
  ];

  for (const [columnName, createColumn] of requiredColumns) {
    const hasColumn = await db.schema.hasColumn(BOARD_POSTS_TABLE, columnName);

    if (!hasColumn) {
      try {
        await db.schema.alterTable(BOARD_POSTS_TABLE, (table) => {
          createColumn(table);
        });
      } catch (error) {
        if (!isDuplicateColumnError(error)) {
          throw error;
        }
      }
    }
  }

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

  await ensureBoardPostRequestsTable();
  await syncLegacyBoardPostRequests();
}

type PlanRow = {
  id: number;
  status: string | null;
  worker_status: string | null;
  last_error: string | null;
};

function mapBoardPostRequestRow(row: Record<string, unknown>, planRow?: PlanRow | null): BoardPostRequestItem {
  const workflowState = resolveBoardRequestWorkflowState(row.workflow_state);
  const planStatus = planRow?.status ?? null;
  const workerStatus = planRow?.worker_status ?? null;
  const lastError = planRow?.last_error ?? null;
  const resolvedStatus = resolveBoardRequestStatus({
    workflowState,
    planStatus,
    workerStatus,
    lastError,
  });

  return {
    id: Number(row.id ?? 0),
    boardPostId: Number(row.board_post_id ?? 0),
    sequence: Number(row.sequence_no ?? 0),
    title: String(row.title ?? ''),
    content: String(row.content ?? ''),
    planItemId: row.plan_item_id === null || row.plan_item_id === undefined ? null : Number(row.plan_item_id),
    automationReceivedAt:
      row.automation_received_at === null || row.automation_received_at === undefined
        ? null
        : String(row.automation_received_at),
    workflowState,
    status: resolvedStatus.status,
    statusLabel: resolvedStatus.statusLabel,
    planStatus,
    workerStatus,
    lastError,
    createdAt: String(row.created_at ?? ''),
    updatedAt: String(row.updated_at ?? ''),
  };
}

function buildBoardPostSummary(requestItems: BoardPostRequestItem[]) {
  return requestItems.reduce(
    (summary, requestItem) => {
      summary.total += 1;

      if (requestItem.status === 'completed') {
        summary.completed += 1;
      } else if (requestItem.status === 'failed') {
        summary.failed += 1;
      } else if (requestItem.status === 'in_progress') {
        summary.running += 1;
      } else if (requestItem.status === 'queued') {
        summary.queued += 1;
      } else if (requestItem.status === 'waiting' || requestItem.status === 'pending') {
        summary.waiting += 1;
      } else if (requestItem.status === 'blocked') {
        summary.blocked += 1;
      }

      return summary;
    },
    {
      total: 0,
      completed: 0,
      failed: 0,
      running: 0,
      queued: 0,
      waiting: 0,
      blocked: 0,
    },
  );
}

function mapBoardPostRow(
  row: Record<string, unknown>,
  requestRows: Record<string, unknown>[],
  planRowMap: Map<number, PlanRow>,
): BoardPostItem {
  const content = String(row.content ?? '');
  const rawAttachments = row.attachments_json ?? row.attachments ?? '[]';
  let attachments: BoardPostItem['attachments'] = [];

  try {
    const parsed = typeof rawAttachments === 'string' ? JSON.parse(rawAttachments) : rawAttachments;
    attachments = boardPostPayloadSchema.shape.attachments.parse(parsed);
  } catch {
    attachments = [];
  }

  const requestItems = requestRows
    .sort((left, right) => Number(left.sequence_no ?? 0) - Number(right.sequence_no ?? 0))
    .map((requestRow) => {
      const planItemId =
        requestRow.plan_item_id === null || requestRow.plan_item_id === undefined
          ? null
          : Number(requestRow.plan_item_id);
      return mapBoardPostRequestRow(requestRow, planItemId ? planRowMap.get(planItemId) : null);
    });

  const firstRegisteredRequest = requestItems.find((item) => item.planItemId || item.automationReceivedAt);
  const firstReceivedAt = requestItems
    .map((item) => item.automationReceivedAt)
    .filter((value): value is string => Boolean(value))
    .sort()[0] ?? null;

  return {
    id: Number(row.id ?? 0),
    title: String(row.title ?? ''),
    content,
    preview: createPreview(content || requestItems.map((item) => item.content).join('\n\n')),
    attachments,
    automationType: resolveStoredAutomationTypeId(row),
    automationPlanItemId: firstRegisteredRequest?.planItemId ?? null,
    automationReceivedAt: firstReceivedAt,
    automationContextIds: parseAutomationContextIds(row.automation_context_ids_json),
    requestExecutionMode: resolveBoardRequestExecutionMode(row.request_execution_mode),
    requestItems,
    requestSummary: buildBoardPostSummary(requestItems),
    createdAt: String(row.created_at ?? ''),
    updatedAt: String(row.updated_at ?? ''),
  };
}

async function loadBoardPostsWithRequests(postIds?: number[]) {
  await ensureBoardPostsTable();

  const postQuery = db(BOARD_POSTS_TABLE).select('*').orderBy('updated_at', 'desc').orderBy('id', 'desc');

  if (postIds?.length) {
    postQuery.whereIn('id', postIds);
  }

  const rows = await postQuery;

  if (!rows.length) {
    return [];
  }

  const boardPostIds = rows.map((row) => Number(row.id));
  const requestRows = await db(BOARD_POST_REQUESTS_TABLE)
    .select('*')
    .whereIn('board_post_id', boardPostIds)
    .orderBy('sequence_no', 'asc')
    .orderBy('id', 'asc');
  const planItemIds = requestRows
    .map((row) => Number(row.plan_item_id))
    .filter((value) => Number.isFinite(value) && value > 0);
  const planRows = planItemIds.length > 0
    ? await db(PLAN_TABLE).select('id', 'status', 'worker_status', 'last_error').whereIn('id', planItemIds)
    : [];
  const requestRowsByPostId = new Map<number, Record<string, unknown>[]>();
  const planRowMap = new Map<number, PlanRow>();

  for (const requestRow of requestRows) {
    const boardPostId = Number(requestRow.board_post_id ?? 0);
    const currentRows = requestRowsByPostId.get(boardPostId) ?? [];
    currentRows.push(requestRow);
    requestRowsByPostId.set(boardPostId, currentRows);
  }

  for (const planRow of planRows) {
    planRowMap.set(Number(planRow.id), {
      id: Number(planRow.id),
      status: planRow.status === null || planRow.status === undefined ? null : String(planRow.status),
      worker_status: planRow.worker_status === null || planRow.worker_status === undefined ? null : String(planRow.worker_status),
      last_error: planRow.last_error === null || planRow.last_error === undefined ? null : String(planRow.last_error),
    });
  }

  return rows.map((row) => mapBoardPostRow(row, requestRowsByPostId.get(Number(row.id)) ?? [], planRowMap));
}

async function getExistingPlanWorkIds(trx: typeof db) {
  return (await trx(PLAN_TABLE).select('work_id')).map((row) => String(row.work_id ?? ''));
}

async function createPlanFromBoardRequest(
  trx: typeof db,
  args: {
    boardPostRow: Record<string, unknown>;
    requestRow: Record<string, unknown>;
    sequenceInfo: { index: number; total: number };
    planWorkIdBase?: string | null;
    planWorkIdSuffixLabel?: string | null;
    suppressWebPush?: boolean;
  },
) {
  const boardPost = mapBoardPostRow(args.boardPostRow, [args.requestRow], new Map());
  const automationType = await resolveAutomationType(args.boardPostRow.automation_type_id ?? args.boardPostRow.automation_type);
  const existingWorkIds = await getExistingPlanWorkIds(trx);
  const suffixLabel = args.planWorkIdSuffixLabel?.trim() || `req-${Number(args.requestRow.sequence_no ?? 1)}`;
  const workId = args.planWorkIdBase?.trim()
    ? resolveSequencedPlanWorkId(args.planWorkIdBase, existingWorkIds, suffixLabel)
    : `board-post-${Number(args.boardPostRow.id)}-req-${Number(args.requestRow.sequence_no ?? 1)}`;
  const insertQuery = trx(PLAN_TABLE).insert({
    work_id: workId,
    note: await buildBoardPostPlanNote(
      boardPost.title,
      boardPost.content,
      boardPost.attachments,
      automationType,
      boardPost.automationContextIds,
      undefined,
      {
        title: String(args.requestRow.title ?? ''),
        content: String(args.requestRow.content ?? ''),
      },
      args.sequenceInfo,
    ),
    automation_type: normalizePlanAutomationType(args.boardPostRow.automation_type),
    automation_type_id: args.boardPostRow.automation_type_id ?? args.boardPostRow.automation_type,
    automation_context_ids_json: stringifyAutomationContextIds(boardPost.automationContextIds),
    status: '등록',
    release_target: 'release',
    jangsing_processing_required: true,
    auto_deploy_to_main: false,
    suppress_web_push: args.suppressWebPush ?? false,
    worker_status: '대기',
    last_error: null,
    updated_at: trx.fn.now(),
  });
  const insertResult = supportsReturning() ? await insertQuery.returning('id') : await insertQuery;
  const planItemId = resolveInsertedId(insertResult);

  if (!planItemId) {
    throw new Error('자동화 접수 후 Plan ID를 확인하지 못했습니다.');
  }

  await trx(BOARD_POST_REQUESTS_TABLE)
    .where({ id: Number(args.requestRow.id) })
    .update({
      plan_item_id: planItemId,
      automation_received_at: trx.fn.now(),
      workflow_state: 'registered',
      updated_at: trx.fn.now(),
    });

  return planItemId;
}

async function syncBoardPostLegacyLinkColumns(trx: typeof db, boardPostId: number) {
  const requestRows = await trx(BOARD_POST_REQUESTS_TABLE)
    .select('plan_item_id', 'automation_received_at')
    .where({ board_post_id: boardPostId })
    .orderBy('sequence_no', 'asc')
    .orderBy('id', 'asc');
  const firstRegisteredRequest = requestRows.find((row) => row.plan_item_id || row.automation_received_at);
  const firstReceivedAt = requestRows
    .map((row) => row.automation_received_at)
    .filter((value): value is string => Boolean(value))
    .sort()[0] ?? null;

  await trx(BOARD_POSTS_TABLE)
    .where({ id: boardPostId })
    .update({
      automation_plan_item_id:
        firstRegisteredRequest?.plan_item_id === null || firstRegisteredRequest?.plan_item_id === undefined
          ? null
          : Number(firstRegisteredRequest.plan_item_id),
      automation_received_at: firstReceivedAt,
      updated_at: trx.fn.now(),
    });
}

function isBoardPostAutomationLocked(requestRows: Record<string, unknown>[], row: Record<string, unknown>) {
  if (row.automation_received_at || row.automation_plan_item_id) {
    return true;
  }

  return requestRows.some((requestRow) => requestRow.automation_received_at || requestRow.plan_item_id);
}

export async function listBoardPosts() {
  return loadBoardPostsWithRequests();
}

export async function getBoardPost(id: number) {
  const items = await loadBoardPostsWithRequests([id]);
  return items[0] ?? null;
}

export async function createBoardPost(payload: BoardPostPayloadInput) {
  await ensureBoardPostsTable();
  const parsedPayload = boardPostPayloadSchema.parse(payload);
  const normalizedRequestItems = normalizeBoardPostRequestItems(
    parsedPayload.requestItems,
    parsedPayload.title,
    parsedPayload.content,
  );
  const automationType = await resolveAutomationType(parsedPayload.automationType);
  const insertQuery = db(BOARD_POSTS_TABLE).insert({
    title: parsedPayload.title,
    content: parsedPayload.content,
    attachments_json: JSON.stringify(parsedPayload.attachments),
    automation_type: automationType.behaviorType,
    automation_type_id: automationType.id,
    automation_context_ids_json: stringifyAutomationContextIds(parsedPayload.automationContextIds),
    request_execution_mode: parsedPayload.requestExecutionMode,
    created_at: db.fn.now(),
    updated_at: db.fn.now(),
  });
  const insertResult = supportsReturning() ? await insertQuery.returning('id') : await insertQuery;
  const insertedId = resolveInsertedId(insertResult);

  if (!insertedId) {
    throw new Error('게시글 저장 후 ID를 확인하지 못했습니다.');
  }

  await db(BOARD_POST_REQUESTS_TABLE).insert(
    normalizedRequestItems.map((requestItem, index) => ({
      board_post_id: insertedId,
      sequence_no: index + 1,
      title: requestItem.title,
      content: requestItem.content,
      workflow_state: 'pending',
      created_at: db.fn.now(),
      updated_at: db.fn.now(),
    })),
  );

  const item = await getBoardPost(insertedId);

  if (!item) {
    throw new Error('저장된 게시글을 다시 불러오지 못했습니다.');
  }

  return item;
}

export async function receiveBoardPostAutomation(
  id: number,
  options?: {
    planWorkIdBase?: string | null;
    planWorkIdSuffixLabel?: string | null;
    suppressWebPush?: boolean;
  },
) {
  await ensureBoardPostsTable();
  await ensurePlanTable();

  const transactionResult = await db.transaction(async (trx) => {
    const currentRow = await trx(BOARD_POSTS_TABLE).where({ id }).forUpdate().first();

    if (!currentRow) {
      return null;
    }

    const requestRows = await trx(BOARD_POST_REQUESTS_TABLE)
      .where({ board_post_id: id })
      .orderBy('sequence_no', 'asc')
      .orderBy('id', 'asc')
      .forUpdate();

    const requestExecutionMode = resolveBoardRequestExecutionMode(currentRow.request_execution_mode);
    const registeredPlanItemIds = requestRows
      .map((requestRow) => Number(requestRow.plan_item_id))
      .filter((value) => Number.isFinite(value) && value > 0);
    const pendingRequestRows = requestRows.filter((requestRow) => !requestRow.plan_item_id && !requestRow.automation_received_at);
    let targetRequestRows: Record<string, unknown>[] = [];

    if (requestExecutionMode === 'all_at_once') {
      targetRequestRows = pendingRequestRows.filter((requestRow) => resolveBoardRequestWorkflowState(requestRow.workflow_state) !== 'blocked');
    } else if (registeredPlanItemIds.length === 0) {
      const firstPendingRequest = pendingRequestRows[0];
      if (firstPendingRequest) {
        targetRequestRows = [firstPendingRequest];
      }
      const waitingIds = pendingRequestRows.slice(1).map((requestRow) => Number(requestRow.id)).filter(Number.isFinite);
      if (waitingIds.length > 0) {
        await trx(BOARD_POST_REQUESTS_TABLE)
          .whereIn('id', waitingIds)
          .update({
            workflow_state: 'waiting',
            updated_at: trx.fn.now(),
          });
      }
    }

    if (targetRequestRows.length === 0) {
      return {
        planItemIds: registeredPlanItemIds,
        alreadyReceived: true,
      };
    }

    const createdPlanItemIds: number[] = [];

    for (const requestRow of targetRequestRows) {
      createdPlanItemIds.push(
        await createPlanFromBoardRequest(trx, {
          boardPostRow: currentRow,
          requestRow,
          sequenceInfo: {
            index: Number(requestRow.sequence_no ?? 1),
            total: requestRows.length,
          },
          planWorkIdBase: options?.planWorkIdBase,
          planWorkIdSuffixLabel: options?.planWorkIdSuffixLabel,
          suppressWebPush: options?.suppressWebPush,
        }),
      );
    }

    await syncBoardPostLegacyLinkColumns(trx, id);

    return {
      planItemIds: createdPlanItemIds,
      alreadyReceived: false,
    };
  });

  if (!transactionResult) {
    return null;
  }

  return {
    item: await getBoardPost(id),
    ...transactionResult,
  };
}

export async function progressBoardPostAutomationByPlanResult(
  planItemId: number,
  outcome: 'completed' | 'failed',
) {
  await ensureBoardPostsTable();
  await ensurePlanTable();

  const updatedBoardPostId = await db.transaction(async (trx) => {
    const currentRequestRow = await trx(BOARD_POST_REQUESTS_TABLE).where({ plan_item_id: planItemId }).forUpdate().first();

    if (!currentRequestRow) {
      return null;
    }

    const boardPostId = Number(currentRequestRow.board_post_id);
    const boardPostRow = await trx(BOARD_POSTS_TABLE).where({ id: boardPostId }).forUpdate().first();

    if (!boardPostRow) {
      return null;
    }

    const requestExecutionMode = resolveBoardRequestExecutionMode(boardPostRow.request_execution_mode);
    const requestRows = await trx(BOARD_POST_REQUESTS_TABLE)
      .where({ board_post_id: boardPostId })
      .orderBy('sequence_no', 'asc')
      .orderBy('id', 'asc')
      .forUpdate();

    await trx(BOARD_POST_REQUESTS_TABLE)
      .where({ id: Number(currentRequestRow.id) })
      .update({
        workflow_state: outcome === 'completed' ? 'completed' : 'failed',
        updated_at: trx.fn.now(),
      });

    if (requestExecutionMode === 'all_at_once') {
      await syncBoardPostLegacyLinkColumns(trx, boardPostId);
      return boardPostId;
    }

    const nextWaitingRequest = requestRows.find((requestRow) => {
      if (Number(requestRow.id) === Number(currentRequestRow.id)) {
        return false;
      }

      const state = resolveBoardRequestWorkflowState(requestRow.workflow_state);
      return !requestRow.plan_item_id && !requestRow.automation_received_at && (state === 'waiting' || state === 'pending');
    });

    if (!nextWaitingRequest) {
      await syncBoardPostLegacyLinkColumns(trx, boardPostId);
      return boardPostId;
    }

    if (requestExecutionMode === 'after_previous_success' && outcome === 'failed') {
      const blockedRequestIds = requestRows
        .filter((requestRow) => Number(requestRow.sequence_no ?? 0) > Number(currentRequestRow.sequence_no ?? 0))
        .map((requestRow) => Number(requestRow.id))
        .filter(Number.isFinite);

      if (blockedRequestIds.length > 0) {
        await trx(BOARD_POST_REQUESTS_TABLE)
          .whereIn('id', blockedRequestIds)
          .update({
            workflow_state: 'blocked',
            updated_at: trx.fn.now(),
          });
      }
    } else {
      await createPlanFromBoardRequest(trx, {
        boardPostRow,
        requestRow: nextWaitingRequest,
        sequenceInfo: {
          index: Number(nextWaitingRequest.sequence_no ?? 1),
          total: requestRows.length,
        },
      });
    }

    await syncBoardPostLegacyLinkColumns(trx, boardPostId);
    return boardPostId;
  });

  if (!updatedBoardPostId) {
    return null;
  }

  return getBoardPost(updatedBoardPostId);
}

export async function updateBoardPost(id: number, payload: BoardPostPayloadInput) {
  await ensureBoardPostsTable();
  const parsedPayload = boardPostPayloadSchema.parse(payload);
  const normalizedRequestItems = normalizeBoardPostRequestItems(
    parsedPayload.requestItems,
    parsedPayload.title,
    parsedPayload.content,
  );
  const automationType = await resolveAutomationType(parsedPayload.automationType);

  const updatedBoardPostId = await db.transaction(async (trx) => {
    const currentRow = await trx(BOARD_POSTS_TABLE).where({ id }).forUpdate().first();

    if (!currentRow) {
      return null;
    }

    const currentRequestRows = await trx(BOARD_POST_REQUESTS_TABLE)
      .where({ board_post_id: id })
      .orderBy('sequence_no', 'asc')
      .orderBy('id', 'asc')
      .forUpdate();

    if (isBoardPostAutomationLocked(currentRequestRows, currentRow)) {
      throw new BoardPostAutomationLockedError('update');
    }

    await trx(BOARD_POSTS_TABLE)
      .where({ id })
      .update({
        title: parsedPayload.title,
        content: parsedPayload.content,
        attachments_json: JSON.stringify(parsedPayload.attachments),
        automation_type: automationType.behaviorType,
        automation_type_id: automationType.id,
        automation_context_ids_json: stringifyAutomationContextIds(parsedPayload.automationContextIds),
        request_execution_mode: parsedPayload.requestExecutionMode,
        updated_at: trx.fn.now(),
      });

    await trx(BOARD_POST_REQUESTS_TABLE).where({ board_post_id: id }).delete();
    await trx(BOARD_POST_REQUESTS_TABLE).insert(
      normalizedRequestItems.map((requestItem, index) => ({
        board_post_id: id,
        sequence_no: index + 1,
        title: requestItem.title,
        content: requestItem.content,
        workflow_state: 'pending',
        created_at: trx.fn.now(),
        updated_at: trx.fn.now(),
      })),
    );

    return id;
  });

  if (!updatedBoardPostId) {
    return null;
  }

  return getBoardPost(updatedBoardPostId);
}

export async function deleteBoardPost(id: number) {
  await ensureBoardPostsTable();
  return db.transaction(async (trx) => {
    const currentRow = await trx(BOARD_POSTS_TABLE).where({ id }).forUpdate().first();

    if (!currentRow) {
      return false;
    }

    const currentRequestRows = await trx(BOARD_POST_REQUESTS_TABLE)
      .where({ board_post_id: id })
      .forUpdate();

    if (isBoardPostAutomationLocked(currentRequestRows, currentRow)) {
      throw new BoardPostAutomationLockedError('delete');
    }

    await trx(BOARD_POST_REQUESTS_TABLE).where({ board_post_id: id }).delete();
    const deletedCount = await trx(BOARD_POSTS_TABLE).where({ id }).del();
    return deletedCount > 0;
  });
}
