import { createHash } from 'node:crypto';
import { db } from '../db/client.js';
import { listBoardPosts, createBoardPost } from './board-service.js';
import { listErrorLogs } from './error-log-service.js';
import { ensurePlanTable, PLAN_TABLE } from './plan-service.js';

const DEFAULT_ERROR_LOG_PLAN_GROUP_LIMIT = 6;
const ERROR_LOG_BOARD_POST_MARKER_PREFIX = '<!-- error-log-plan-work-id:';

type ErrorLogCandidate = {
  fingerprint: string;
  workId: string;
  source: string;
  sourceLabel: string | null;
  errorType: string;
  errorName: string | null;
  errorMessage: string;
  requestPath: string | null;
  requestPathGroup: string;
  requestPaths: string[];
  statusCode: number | null;
  count: number;
  firstCreatedAt: unknown;
  lastCreatedAt: unknown;
  sampleLogId: number | null;
  sampleLogIds: number[];
  errorNames: string[];
  representativeMessages: string[];
  groupedScopes?: string[];
  groupedCandidateCount?: number;
};

type ErrorLogRegistrationSkip = {
  workId: string;
  reason: string;
  boardPostId?: number;
  planId?: number;
};

type ErrorLogBoardPostRegistration = {
  postId: number;
  title: string;
  workId: string;
  count: number;
};

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

  const date = value instanceof Date ? value : new Date(String(value));
  return Number.isNaN(date.getTime()) ? null : date;
}

function formatIsoTimestamp(value: unknown) {
  const date = normalizeDateBoundary(value);
  return date ? date.toISOString() : String(value ?? '');
}

function normalizeRequestPathGroup(requestPath: unknown) {
  const normalized = String(requestPath ?? '')
    .trim()
    .toLowerCase()
    .replace(/\?.*$/u, '')
    .replace(/\/\d+(?=\/|$)/gu, '/:id')
    .replace(/[0-9a-f]{8,}(?=\/|$)/giu, ':id');

  if (!normalized) {
    return 'unknown';
  }

  const segments = normalized
    .split('/')
    .map((segment) => segment.trim())
    .filter(Boolean)
    .slice(0, 3);

  return segments.length > 0 ? `/${segments.join('/')}` : normalized;
}

function buildErrorLogPlanFingerprint(log: Record<string, unknown>) {
  return createHash('sha1')
    .update(
      [
        String(log.source ?? '').trim().toLowerCase(),
        String(log.errorType ?? '').trim().toLowerCase(),
        log.statusCode == null ? '' : String(log.statusCode),
      ].join('||'),
    )
    .digest('hex')
    .slice(0, 12);
}

function buildErrorLogPlanWorkId(log: Record<string, unknown>) {
  return `error-fix-${buildErrorLogPlanFingerprint(log)}`;
}

function buildErrorLogPlanCandidates(logs: Array<Record<string, unknown>>) {
  const grouped = new Map<string, any>();

  for (const log of logs) {
    const fingerprint = createHash('sha1')
      .update(
        [
          String(log.source ?? '').trim().toLowerCase(),
          String(log.errorType ?? '').trim().toLowerCase(),
          String(log.errorName ?? '').trim().toLowerCase(),
          log.statusCode == null ? '' : String(log.statusCode),
          normalizeRequestPathGroup(log.requestPath),
        ].join('||'),
      )
      .digest('hex')
      .slice(0, 12);

    const createdAtMs = normalizeDateBoundary(log.createdAt)?.getTime() ?? 0;
    const existing = grouped.get(fingerprint);
    const requestPathGroup = normalizeRequestPathGroup(log.requestPath);
    const errorMessage = String(log.errorMessage ?? '').trim();

    if (!existing) {
      grouped.set(fingerprint, {
        sample: log,
        count: 1,
        firstCreatedAt: log.createdAt,
        firstTimeMs: createdAtMs,
        lastCreatedAt: log.createdAt,
        lastTimeMs: createdAtMs,
        requestPathGroup,
        requestPaths: log.requestPath ? new Set([String(log.requestPath).trim()]) : new Set(),
        errorMessages: errorMessage ? new Set([errorMessage]) : new Set(),
        errorNames: log.errorName ? new Set([String(log.errorName).trim()]) : new Set(),
        sampleLogIds: log.id != null ? [Number(log.id)] : [],
      });
      continue;
    }

    existing.count += 1;

    if (log.requestPath) {
      existing.requestPaths.add(String(log.requestPath).trim());
    }

    if (errorMessage) {
      existing.errorMessages.add(errorMessage);
    }

    if (log.errorName) {
      existing.errorNames.add(String(log.errorName).trim());
    }

    if (log.id != null && existing.sampleLogIds.length < 5) {
      existing.sampleLogIds.push(Number(log.id));
    }

    if (createdAtMs <= existing.firstTimeMs) {
      existing.firstCreatedAt = log.createdAt;
      existing.firstTimeMs = createdAtMs;
    }

    if (createdAtMs >= existing.lastTimeMs) {
      existing.sample = log;
      existing.lastCreatedAt = log.createdAt;
      existing.lastTimeMs = createdAtMs;
    }
  }

  return [...grouped.entries()]
    .map(([fingerprint, entry]) => ({
      fingerprint,
      workId: buildErrorLogPlanWorkId(entry.sample),
      source: String(entry.sample.source ?? ''),
      sourceLabel: entry.sample.sourceLabel ? String(entry.sample.sourceLabel) : null,
      errorType: String(entry.sample.errorType ?? ''),
      errorName: entry.sample.errorName ? String(entry.sample.errorName) : null,
      errorMessage: String(entry.sample.errorMessage ?? ''),
      requestPath: entry.sample.requestPath ? String(entry.sample.requestPath) : null,
      requestPathGroup: entry.requestPathGroup,
      requestPaths: [...entry.requestPaths].filter(Boolean).slice(0, 5),
      statusCode: entry.sample.statusCode == null ? null : Number(entry.sample.statusCode),
      count: entry.count,
      firstCreatedAt: entry.firstCreatedAt,
      lastCreatedAt: entry.lastCreatedAt,
      sampleLogId: entry.sample.id == null ? null : Number(entry.sample.id),
      sampleLogIds: entry.sampleLogIds,
      errorNames: [...entry.errorNames].filter(Boolean).slice(0, 5),
      representativeMessages: [...entry.errorMessages].filter(Boolean).slice(0, 5),
    }))
    .sort((left, right) => {
      if (right.count !== left.count) {
        return right.count - left.count;
      }

      const leftLastTime = normalizeDateBoundary(left.lastCreatedAt)?.getTime() ?? 0;
      const rightLastTime = normalizeDateBoundary(right.lastCreatedAt)?.getTime() ?? 0;

      if (rightLastTime !== leftLastTime) {
        return rightLastTime - leftLastTime;
      }

      return Number(right.sampleLogId ?? 0) - Number(left.sampleLogId ?? 0);
    });
}

function mergeErrorLogPlanCandidateBucket(bucket: ErrorLogCandidate[], bucketIndex: number): ErrorLogCandidate {
  const sortedBucket = [...bucket].sort((left, right) => {
    if (right.count !== left.count) {
      return right.count - left.count;
    }

    return String(left.workId ?? '').localeCompare(String(right.workId ?? ''));
  });

  const representative = sortedBucket[0];
  const uniqueFingerprints = [...new Set(sortedBucket.map((candidate) => candidate.fingerprint).filter(Boolean))].sort();
  const mergedFingerprint = createHash('sha1')
    .update(uniqueFingerprints.join('||'))
    .digest('hex')
    .slice(0, 12);
  const firstCreatedAt = sortedBucket
    .map((candidate) => normalizeDateBoundary(candidate.firstCreatedAt)?.getTime() ?? Number.POSITIVE_INFINITY)
    .reduce((min, value) => Math.min(min, value), Number.POSITIVE_INFINITY);
  const lastCreatedAt = sortedBucket
    .map((candidate) => normalizeDateBoundary(candidate.lastCreatedAt)?.getTime() ?? 0)
    .reduce((max, value) => Math.max(max, value), 0);
  const requestPaths = [...new Set(sortedBucket.flatMap((candidate) => candidate.requestPaths ?? []).filter(Boolean))].slice(0, 8);
  const representativeMessages = [...new Set(sortedBucket.flatMap((candidate) => candidate.representativeMessages ?? []).filter(Boolean))].slice(0, 8);
  const errorNames = [...new Set(sortedBucket.flatMap((candidate) => candidate.errorNames ?? []).filter(Boolean))].slice(0, 8);
  const groupedScopes = sortedBucket
    .slice(0, 8)
    .map((candidate) => {
      const parts = [candidate.sourceLabel || candidate.source, candidate.errorType];

      if (candidate.requestPathGroup && candidate.requestPathGroup !== 'unknown') {
        parts.push(candidate.requestPathGroup);
      }

      return parts.filter(Boolean).join(' / ');
    })
    .filter(Boolean);

  return {
    fingerprint: mergedFingerprint,
    workId: `error-fix-bundle-${mergedFingerprint}`,
    source: representative.source,
    sourceLabel: representative.sourceLabel,
    errorType: sortedBucket.length > 1 ? '다중 에러 묶음' : representative.errorType,
    errorName: representative.errorName,
    errorMessage: representative.errorMessage,
    requestPath: representative.requestPath,
    requestPathGroup: representative.requestPathGroup,
    requestPaths,
    statusCode: representative.statusCode,
    count: sortedBucket.reduce((sum, candidate) => sum + Number(candidate.count ?? 0), 0),
    firstCreatedAt: Number.isFinite(firstCreatedAt) ? new Date(firstCreatedAt).toISOString() : representative.firstCreatedAt,
    lastCreatedAt: lastCreatedAt > 0 ? new Date(lastCreatedAt).toISOString() : representative.lastCreatedAt,
    sampleLogId: representative.sampleLogId,
    sampleLogIds: [...new Set(sortedBucket.flatMap((candidate) => candidate.sampleLogIds ?? []).filter((value) => value != null))].slice(0, 8),
    errorNames,
    representativeMessages,
    groupedScopes,
    groupedCandidateCount: sortedBucket.length,
  };
}

function coalesceErrorLogPlanCandidates(candidates: ErrorLogCandidate[], maxGroups = DEFAULT_ERROR_LOG_PLAN_GROUP_LIMIT) {
  const sortedCandidates = [...candidates].sort((left, right) => {
    if (right.count !== left.count) {
      return right.count - left.count;
    }

    return String(left.workId ?? '').localeCompare(String(right.workId ?? ''));
  });

  if (sortedCandidates.length <= maxGroups) {
    return sortedCandidates;
  }

  const bucketSize = Math.ceil(sortedCandidates.length / maxGroups);
  const merged: ErrorLogCandidate[] = [];

  for (let index = 0; index < sortedCandidates.length; index += bucketSize) {
    const bucket = sortedCandidates.slice(index, index + bucketSize);

    if (bucket.length === 0) {
      continue;
    }

    merged.push(mergeErrorLogPlanCandidateBucket(bucket, merged.length + 1));
  }

  return merged.slice(0, maxGroups);
}

function filterLogsWithinRange(logs: Array<Record<string, unknown>>, rangeStart: Date, rangeEnd: Date) {
  const startTime = rangeStart.getTime();
  const endTime = rangeEnd.getTime();

  return logs.filter((log) => {
    const createdAtMs = normalizeDateBoundary(log.createdAt)?.getTime();
    return createdAtMs != null && createdAtMs >= startTime && createdAtMs <= endTime;
  });
}

function formatErrorLogPlanNote(candidate: ErrorLogCandidate, rangeStart: Date, rangeEnd: Date) {
  const lines = [
    `조회 구간: ${formatIsoTimestamp(rangeStart)} ~ ${formatIsoTimestamp(rangeEnd)}`,
    `발생 건수: ${candidate.count}건`,
    `최근 발생: ${formatIsoTimestamp(candidate.lastCreatedAt)}`,
    `최초 발생: ${formatIsoTimestamp(candidate.firstCreatedAt)}`,
    `에러 유형: ${candidate.errorType}`,
  ];

  if (candidate.errorName) {
    lines.push(`에러 이름: ${candidate.errorName}`);
  }

  if (candidate.sourceLabel || candidate.source) {
    lines.push(`발생 위치: ${candidate.sourceLabel || candidate.source}`);
  }

  if (candidate.groupedCandidateCount && candidate.groupedCandidateCount > 1) {
    lines.push(`묶인 에러 그룹: ${candidate.groupedCandidateCount}개`);
  }

  if (candidate.requestPathGroup && candidate.requestPathGroup !== 'unknown') {
    lines.push(`주요 경로 그룹: ${candidate.requestPathGroup}`);
  }

  if (Array.isArray(candidate.requestPaths) && candidate.requestPaths.length > 0) {
    lines.push(`대표 경로: ${candidate.requestPaths.join(', ')}`);
  }

  if (candidate.statusCode != null) {
    lines.push(`상태 코드: ${candidate.statusCode}`);
  }

  if (Array.isArray(candidate.representativeMessages) && candidate.representativeMessages.length > 0) {
    lines.push('대표 메시지:');
    lines.push(...candidate.representativeMessages.map((message, index) => `${index + 1}. ${message}`));
  } else {
    lines.push(`대표 메시지: ${String(candidate.errorMessage ?? '').trim()}`);
  }

  if (Array.isArray(candidate.sampleLogIds) && candidate.sampleLogIds.length > 0) {
    lines.push(`대표 로그 ID: ${candidate.sampleLogIds.join(', ')}`);
  } else {
    lines.push(`대표 로그 ID: ${candidate.sampleLogId}`);
  }

  if (Array.isArray(candidate.groupedScopes) && candidate.groupedScopes.length > 0) {
    lines.push('묶인 에러 범위:');
    lines.push(...candidate.groupedScopes.map((scope, index) => `${index + 1}. ${scope}`));
  }

  lines.push('');
  lines.push('처리 요청:');
  lines.push('1. 재현 경로와 영향 범위를 확인합니다.');
  lines.push('2. 수정이 필요한 경우 별도 Plan으로 소스 작업을 진행합니다.');
  lines.push('3. 테스트와 재발 방지 필요 여부를 검토합니다.');

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

function buildBoardPostMarker(workId: string) {
  return `${ERROR_LOG_BOARD_POST_MARKER_PREFIX}${workId} -->`;
}

function buildErrorLogBoardPostTitle(candidate: ErrorLogCandidate) {
  const scope = candidate.requestPathGroup && candidate.requestPathGroup !== 'unknown'
    ? ` ${candidate.requestPathGroup}`
    : '';
  const title = `에러로그 조치 계획: ${candidate.errorType}${scope}`.replace(/\s+/g, ' ').trim();
  return title.length > 200 ? `${title.slice(0, 197).trimEnd()}...` : title;
}

function buildErrorLogBoardPostContent(candidate: ErrorLogCandidate, rangeStart: Date, rangeEnd: Date) {
  const detailNote = formatErrorLogPlanNote(candidate, rangeStart, rangeEnd);
  return [
    buildBoardPostMarker(candidate.workId),
    `# ${buildErrorLogBoardPostTitle(candidate)}`,
    '',
    detailNote,
  ].join('\n');
}

export async function registerErrorLogBoardPosts(args?: {
  rangeStart?: Date;
  rangeEnd?: Date;
  maxGroups?: number;
}) {
  await ensurePlanTable();

  const rangeEnd = args?.rangeEnd ?? new Date();
  const rangeStart = args?.rangeStart ?? new Date(rangeEnd.getTime() - 24 * 60 * 60 * 1000);
  const maxGroups = args?.maxGroups ?? DEFAULT_ERROR_LOG_PLAN_GROUP_LIMIT;
  const [errorLogs, existingBoardPosts] = await Promise.all([
    listErrorLogs(200),
    listBoardPosts(),
  ]);

  const recentLogs = filterLogsWithinRange(errorLogs as Array<Record<string, unknown>>, rangeStart, rangeEnd);
  const rawCandidates = buildErrorLogPlanCandidates(recentLogs as Array<Record<string, unknown>>);
  const candidates = coalesceErrorLogPlanCandidates(rawCandidates, maxGroups);
  const createdPosts: ErrorLogBoardPostRegistration[] = [];
  const skippedPosts: ErrorLogRegistrationSkip[] = [];

  for (const candidate of candidates) {
    const marker = buildBoardPostMarker(candidate.workId);
    const existingBoardPost = existingBoardPosts.find((post) => String(post.content ?? '').includes(marker));

    if (existingBoardPost) {
      skippedPosts.push({
        workId: candidate.workId,
        boardPostId: existingBoardPost.id,
        reason: `기존 게시글 #${existingBoardPost.id}가 있습니다.`,
      });
      continue;
    }

    const latestOpenPlan = await db(PLAN_TABLE)
      .select(['id', 'status'])
      .where({ work_id: candidate.workId })
      .whereNot({ status: '완료' as never })
      .orderBy('id', 'desc')
      .first();

    if (latestOpenPlan) {
      skippedPosts.push({
        workId: candidate.workId,
        planId: Number(latestOpenPlan.id),
        reason: `기존 미완료 Plan #${latestOpenPlan.id}가 있습니다.`,
      });
      continue;
    }

    const createdPost = await createBoardPost({
      title: buildErrorLogBoardPostTitle(candidate),
      content: buildErrorLogBoardPostContent(candidate, rangeStart, rangeEnd),
      attachments: [],
      automationType: 'none',
      automationContextIds: [],
      requestExecutionMode: 'all_at_once',
      requestItems: [],
    });

    createdPosts.push({
      postId: createdPost.id,
      title: createdPost.title,
      workId: candidate.workId,
      count: candidate.count,
    });
  }

  return {
    rangeStart,
    rangeEnd,
    recentLogs,
    rawCandidates,
    candidates,
    createdPosts,
    skippedPosts,
  };
}
