/**
 * 메모 마크다운 렌더링 공통 유틸.
 * 메모 페이지와 대시보드에서 동일 로직 사용 (drift 방지).
 */

import { marked } from 'marked';
import DOMPurify from 'dompurify';

marked.use({ gfm: true, breaks: true });

// ─── 타입 ───

export type DueStatus = 'overdue' | 'soon' | 'normal';

export interface ParsedDueDate {
  date: Date;
  original: string; // @2026-04-15
  isoDate: string;  // 2026-04-15
}

// ─── 내부 패턴 ───

const TASK_LINE_RE = /^(\s*- \[)([ x])(\].*)$/;
const DUE_RE = /@(\d{4}-\d{2}-\d{2})/g;
// checkbox input: 속성 순서 독립 (marked 출력 변동에 강건)
const CHECKBOX_INPUT_RE = /<input\b(?=[^>]*type="checkbox")(?=[^>]*disabled)[^>]*>/gi;

// ─── 공개 함수 ───

export interface RenderOptions {
  compact?: boolean;
  interactive?: boolean;
  /** 서버에서 내려온 memo_task_state — {"<task_index>": {"checked_at": "<ISO>"}} */
  taskStates?: Record<string, { checked_at?: string }>;
  /** 현재 시각 tick. 지정 시 checked_at 경과 항목에 task-hidden 클래스 부여. */
  now?: Date;
  /** 체크 후 자동 숨김까지 대기 시간 (ms). 기본 10초. */
  hideAfterMs?: number;
}

export const DEFAULT_HIDE_AFTER_MS = 10_000;

/**
 * 메모 마크다운 → HTML 변환.
 * - GFM task list checkbox를 인터랙티브하게 교체
 * - checkbox line의 @YYYY-MM-DD를 due badge로 변환
 * - #태그를 클릭 가능 링크로 변환
 */
export function renderMemoHtml(markdown: string, options: RenderOptions = {}): string {
  const {
    interactive = true,
    taskStates = {},
    now,
    hideAfterMs = DEFAULT_HIDE_AFTER_MS,
  } = options;

  // 1. #태그 → 링크 (marked 전에 처리, 마크다운 파싱 방해 않는 위치)
  let text = markdown.replace(
    /(?:^|(?<=\s))#([가-힣a-zA-Z0-9_]{2,})/g,
    '<a href="/memos?tag=$1" class="text-accent hover:underline">#$1</a>'
  );

  // 2. marked → HTML
  let html = marked(text) as string;

  // 3. checkbox input 교체 (인터랙티브 + data-task-index + data-checked-at)
  let taskIdx = 0;
  html = html.replace(CHECKBOX_INPUT_RE, (match) => {
    const idx = taskIdx++;
    const isChecked = /checked/.test(match);
    const checkedAt = taskStates[String(idx)]?.checked_at ?? '';
    const dataCheckedAt = checkedAt ? ` data-checked-at="${checkedAt}"` : '';
    if (interactive) {
      return `<input type="checkbox" data-task-index="${idx}"${dataCheckedAt} ${isChecked ? 'checked' : ''} class="memo-checkbox" />`;
    }
    return `<input type="checkbox"${dataCheckedAt} ${isChecked ? 'checked' : ''} disabled class="memo-checkbox" />`;
  });

  // 4. checkbox line의 due date badge 변환 + 자동 숨김 클래스
  const nowMs = now ? now.getTime() : Date.now();
  html = html.replace(/<li>(<input[^>]*class="memo-checkbox"[^>]*>)(.*?)<\/li>/gi, (fullMatch, inputTag, rest) => {
    const isChecked = /checked/.test(inputTag);
    // checked_at 추출 → 10초 경과 판정
    const checkedAtMatch = /data-checked-at="([^"]+)"/.exec(inputTag);
    let hidden = false;
    if (isChecked && checkedAtMatch) {
      const checkedAtMs = Date.parse(checkedAtMatch[1]);
      if (!Number.isNaN(checkedAtMs) && nowMs - checkedAtMs >= hideAfterMs) {
        hidden = true;
      }
    }

    // 마지막 @YYYY-MM-DD만 badge로 변환
    let lastDue: ParsedDueDate | null = null;
    let lastMatch: { index: number; length: number } | null = null;
    let m: RegExpExecArray | null;
    DUE_RE.lastIndex = 0;
    while ((m = DUE_RE.exec(rest)) !== null) {
      const parsed = parseDueDateStr(m[1]);
      if (parsed) {
        lastDue = parsed;
        lastMatch = { index: m.index, length: m[0].length };
      }
    }

    const classes = [
      isChecked ? 'memo-task-done' : '',
      hidden ? 'memo-task-hidden' : '',
    ].filter(Boolean).join(' ');
    const classAttr = classes ? ` class="${classes}"` : '';

    if (lastDue && lastMatch) {
      const status = isChecked ? 'normal' : getDueStatus(lastDue.date);
      const badgeClass = getDueBadgeClass(status, isChecked);
      const label = formatDueLabel(lastDue.date, status);
      const badge = `<span class="${badgeClass}">${label}</span>`;
      const newRest = rest.slice(0, lastMatch.index) + badge + rest.slice(lastMatch.index + lastMatch.length);
      return `<li${classAttr}>${inputTag}${newRest}</li>`;
    }

    return `<li${classAttr}>${inputTag}${rest}</li>`;
  });

  // 5. DOMPurify sanitize
  return DOMPurify.sanitize(html, {
    USE_PROFILES: { html: true },
    FORBID_TAGS: ['style', 'script'],
    FORBID_ATTR: ['onerror', 'onclick'],
    ALLOW_UNKNOWN_PROTOCOLS: false,
    ADD_TAGS: ['input'],
    ADD_ATTR: ['type', 'checked', 'data-task-index', 'data-checked-at', 'class'],
  });
}

/**
 * 체크 후 hideAfterMs 이내에 다시 가려져야 할 항목의 남은 시간(ms) 중 최소값을 반환.
 * 다음 tick 타이머를 정확히 예측하는 데 사용. 가려질 항목이 없으면 null.
 */
export function nextHideInMs(
  taskStates: Record<string, { checked_at?: string }> | undefined | null,
  now: Date,
  hideAfterMs = DEFAULT_HIDE_AFTER_MS,
): number | null {
  if (!taskStates) return null;
  const nowMs = now.getTime();
  let soonest: number | null = null;
  for (const key of Object.keys(taskStates)) {
    const at = taskStates[key]?.checked_at;
    if (!at) continue;
    const atMs = Date.parse(at);
    if (Number.isNaN(atMs)) continue;
    const remain = atMs + hideAfterMs - nowMs;
    if (remain > 0 && (soonest === null || remain < soonest)) {
      soonest = remain;
    }
  }
  return soonest;
}

/**
 * 메모에 표시할 "숨긴 체크 항목 개수". 토글 버튼 라벨용.
 */
export function countHiddenTasks(
  taskStates: Record<string, { checked_at?: string }> | undefined | null,
  now: Date,
  hideAfterMs = DEFAULT_HIDE_AFTER_MS,
): number {
  if (!taskStates) return 0;
  const nowMs = now.getTime();
  let n = 0;
  for (const key of Object.keys(taskStates)) {
    const at = taskStates[key]?.checked_at;
    if (!at) continue;
    const atMs = Date.parse(at);
    if (Number.isNaN(atMs)) continue;
    if (nowMs - atMs >= hideAfterMs) n++;
  }
  return n;
}

/**
 * n번째 task item의 checkbox 토글. 일반 줄은 카운트하지 않음.
 */
export function toggleTaskLine(content: string, taskIndex: number): string {
  const lines = content.split('\n');
  let ti = 0;
  for (let i = 0; i < lines.length; i++) {
    const m = lines[i].match(TASK_LINE_RE);
    if (m) {
      if (ti === taskIndex) {
        lines[i] = m[1] + (m[2] === ' ' ? 'x' : ' ') + m[3];
        break;
      }
      ti++;
    }
  }
  return lines.join('\n');
}

/**
 * checkbox 줄인지 판별.
 */
export function isCheckboxLine(line: string): boolean {
  return TASK_LINE_RE.test(line);
}

/**
 * 날짜 문자열 파싱 (로컬 기준). 잘못된 날짜는 null.
 */
export function parseDueDateStr(isoDate: string): ParsedDueDate | null {
  const parts = isoDate.split('-').map(Number);
  if (parts.length !== 3) return null;
  const [y, m, d] = parts;
  const date = new Date(y, m - 1, d);
  // 유효성 검증 (Date가 입력과 일치하는지)
  if (date.getFullYear() !== y || date.getMonth() !== m - 1 || date.getDate() !== d) {
    return null;
  }
  return { date, original: `@${isoDate}`, isoDate };
}

/**
 * 마감 상태 판별.
 */
export function getDueStatus(dueDate: Date): DueStatus {
  const now = new Date();
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const due = new Date(dueDate.getFullYear(), dueDate.getMonth(), dueDate.getDate());
  const diffDays = Math.floor((due.getTime() - today.getTime()) / 86400000);

  if (diffDays < 0) return 'overdue';
  if (diffDays <= 3) return 'soon';
  return 'normal';
}

// ─── 내부 헬퍼 ───

function getDueBadgeClass(status: DueStatus, isChecked: boolean): string {
  if (isChecked) return 'due-badge due-done';
  switch (status) {
    case 'overdue': return 'due-badge due-overdue';
    case 'soon': return 'due-badge due-soon';
    default: return 'due-badge due-normal';
  }
}

function formatDueLabel(date: Date, status: DueStatus): string {
  const m = date.getMonth() + 1;
  const d = date.getDate();
  const now = new Date();
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const due = new Date(date.getFullYear(), date.getMonth(), date.getDate());
  const diffDays = Math.floor((due.getTime() - today.getTime()) / 86400000);

  if (diffDays === 0) return '오늘';
  if (diffDays === 1) return '내일';
  if (diffDays === -1) return '어제';
  if (diffDays < -1) return `${-diffDays}일 초과`;
  if (diffDays <= 3) return `${diffDays}일 후`;
  return `${m}/${d}`;
}

/**
 * 오늘 날짜를 YYYY-MM-DD 형식으로 반환.
 */
export function todayIso(): string {
  const d = new Date();
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${y}-${m}-${day}`;
}
