import { quarterDateBounds } from './hubSchedule';

export interface TimelineMilestoneInput {
  id: string;
  title: string;
  startDate?: string | null;
  dueDate?: string | null;
  periodEntries?: Array<{
    id: string;
    startDate?: string | null;
    dueDate?: string | null;
    note?: string | null;
  }> | null;
  progress?: number;
  completedAt?: string | null;
  order: number;
  createdAt?: string;
}

export interface TimelineRangeFallback {
  startDate?: string | null;
  dueDate?: string | null;
  quarter?: string | null;
}

export interface TimelineTick {
  label: string;
  leftPct: number;
  isToday?: boolean;
}

export interface TimelineRow {
  id: string;
  milestoneId: string;
  title: string;
  leftPct: number;
  widthPct: number;
  progress: number;
}

export interface MilestoneTimelineModel {
  rangeStartLabel: string;
  rangeEndLabel: string;
  ticks: TimelineTick[];
  rows: TimelineRow[];
}

const DAY_MS = 86_400_000;
const RANGE_PAD_DAYS = 2;
/** 하루·단일일정 막대가 타임라인에서 묻히지 않도록 */
const MIN_BAR_WIDTH_PCT = 2;
/** 오늘 눈금과 겹치는 기존 날짜 라벨 제거 (주간 눈금 등) */
const TODAY_TICK_MIN_GAP_PCT = 3.2;

export function taskTimelineFallback(task: {
  startDate?: string | null;
  dueDate?: string | null;
  quarter?: string | null;
}): TimelineRangeFallback {
  return {
    startDate: task.startDate,
    dueDate: task.dueDate,
    quarter: task.quarter,
  };
}

function startOfDay(d: Date): Date {
  return new Date(d.getFullYear(), d.getMonth(), d.getDate());
}

function parseDay(iso: string): Date {
  return startOfDay(new Date(iso));
}

export function fmtTimelineDayLabel(d: Date): string {
  return `${d.getMonth() + 1}.${String(d.getDate()).padStart(2, '0')}`;
}

function milestoneProgress(m: TimelineMilestoneInput): number {
  if (m.completedAt) return 100;
  return Math.min(100, Math.max(0, m.progress ?? 0));
}

function collectMilestoneTimes(milestones: TimelineMilestoneInput[]): number[] {
  const times: number[] = [];
  for (const m of milestones) {
    const periods = m.periodEntries?.length
      ? m.periodEntries
      : m.startDate || m.dueDate
        ? [{ startDate: m.startDate, dueDate: m.dueDate }]
        : [];
    for (const p of periods) {
      if (p.startDate) times.push(parseDay(p.startDate).getTime());
      if (p.dueDate) times.push(parseDay(p.dueDate).getTime());
    }
  }
  return times;
}

function computeRange(
  milestones: TimelineMilestoneInput[],
  fallback: TimelineRangeFallback,
): { start: Date; end: Date } | null {
  const times = collectMilestoneTimes(milestones);

  if (times.length >= 1) {
    const min = Math.min(...times);
    const max = Math.max(...times);
    const pad = RANGE_PAD_DAYS * DAY_MS;
    return {
      start: new Date(min - pad),
      end: new Date(max + pad),
    };
  }

  if (fallback.startDate && fallback.dueDate) {
    return {
      start: parseDay(fallback.startDate),
      end: parseDay(fallback.dueDate),
    };
  }

  if (fallback.quarter) {
    const { min, max } = quarterDateBounds(fallback.quarter);
    return { start: parseDay(min), end: parseDay(max) };
  }

  return null;
}

function buildTicks(rangeStart: Date, rangeEnd: Date): TimelineTick[] {
  const rangeMs = Math.max(rangeEnd.getTime() - rangeStart.getTime(), DAY_MS);
  const totalDays = Math.ceil(rangeMs / DAY_MS) + 1;
  const stepDays = totalDays > 45 ? 7 : totalDays > 28 ? 2 : 1;
  const now = startOfDay(new Date());
  const todayInRange = now >= rangeStart && now <= rangeEnd;
  const nowMs = now.getTime();

  const ticks: TimelineTick[] = [];

  for (let t = rangeStart.getTime(); t <= rangeEnd.getTime(); t += stepDays * DAY_MS) {
    const d = new Date(t);
    ticks.push({
      label: fmtTimelineDayLabel(d),
      leftPct: ((d.getTime() - rangeStart.getTime()) / rangeMs) * 100,
      isToday: todayInRange && d.getTime() === nowMs,
    });
  }

  if (todayInRange && !ticks.some((tick) => tick.isToday)) {
    const todayLeftPct = ((nowMs - rangeStart.getTime()) / rangeMs) * 100;
    const todayTick: TimelineTick = {
      label: fmtTimelineDayLabel(now),
      leftPct: todayLeftPct,
      isToday: true,
    };
    const spaced = ticks.filter(
      (tick) => Math.abs(tick.leftPct - todayLeftPct) >= TODAY_TICK_MIN_GAP_PCT,
    );
    spaced.push(todayTick);
    spaced.sort((a, b) => a.leftPct - b.leftPct);
    return spaced;
  }

  return ticks;
}

function milestoneHasDates(m: TimelineMilestoneInput): boolean {
  if (m.periodEntries?.some((p) => p.startDate || p.dueDate)) return true;
  return !!(m.startDate || m.dueDate);
}

function milestonePeriodSpans(m: TimelineMilestoneInput): Array<{ startMs: number; endMs: number }> {
  const raw = m.periodEntries?.length
    ? m.periodEntries
    : m.startDate || m.dueDate
      ? [{ startDate: m.startDate, dueDate: m.dueDate }]
      : [];

  const spans: Array<{ startMs: number; endMs: number }> = [];
  for (const p of raw) {
    if (!p.startDate && !p.dueDate) continue;
    if (p.startDate && p.dueDate) {
      let startMs = parseDay(p.startDate).getTime();
      let endMs = parseDay(p.dueDate).getTime();
      if (endMs < startMs) [startMs, endMs] = [endMs, startMs];
      spans.push({ startMs, endMs });
      continue;
    }
    const ms = parseDay((p.dueDate ?? p.startDate)!).getTime();
    spans.push({ startMs: ms, endMs: ms });
  }
  return spans;
}

function buildRows(
  ordered: TimelineMilestoneInput[],
  rangeStart: Date,
  rangeEnd: Date,
  hideUndatedBars?: boolean,
): TimelineRow[] {
  const items =
    hideUndatedBars === false
      ? ordered
      : ordered.filter((m) => milestoneHasDates(m));

  const rangeStartMs = rangeStart.getTime();
  const rangeMs = Math.max(rangeEnd.getTime() - rangeStartMs, DAY_MS);

  const rows: TimelineRow[] = [];

  for (const m of items) {
    const spans = milestonePeriodSpans(m);
    if (spans.length === 0) continue;

    spans.forEach((span, index) => {
      const { startMs, endMs } = span;
      const spanMs = endMs - startMs;
      const isPoint = spanMs === 0;

      let widthPct = isPoint ? MIN_BAR_WIDTH_PCT : (spanMs / rangeMs) * 100;
      widthPct = Math.max(MIN_BAR_WIDTH_PCT, widthPct);

      let leftPct = ((startMs - rangeStartMs) / rangeMs) * 100;
      if (isPoint) leftPct -= widthPct / 2;
      leftPct = Math.max(0, Math.min(100 - widthPct, leftPct));

      rows.push({
        id: spans.length > 1 ? `${m.id}:${index}` : m.id,
        milestoneId: m.id,
        title: m.title,
        leftPct,
        widthPct,
        progress: milestoneProgress(m),
      });
    });
  }

  return rows;
}

export function buildMilestoneTimeline(
  milestones: TimelineMilestoneInput[],
  fallback: TimelineRangeFallback = {},
  options?: { preserveOrder?: boolean; hideUndatedBars?: boolean },
): MilestoneTimelineModel | null {
  if (milestones.length === 0) return null;

  const range = computeRange(milestones, fallback);
  if (!range) return null;

  const ordered = options?.preserveOrder
    ? [...milestones]
    : [...milestones].sort((a, b) => a.order - b.order);

  const rows = buildRows(ordered, range.start, range.end, options?.hideUndatedBars);
  if (rows.length === 0) return null;

  const ticks = buildTicks(range.start, range.end);

  return {
    rangeStartLabel: fmtTimelineDayLabel(range.start),
    rangeEndLabel: fmtTimelineDayLabel(range.end),
    ticks,
    rows,
  };
}
