import { quarterEndDate, quarterStartDate } from './boardCalendar';

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[];
  todayLeftPct?: number | null;
}

const DAY_MS = 86_400_000;
const RANGE_PAD_DAYS = 2;
/** 하루·단일일정 막대가 타임라인에서 묻히지 않도록 */
const MIN_BAR_WIDTH_PCT = 2;
/** 간트 진행률(18px) 뱃지가 잘리지 않도록 */
const MIN_GANTT_BAR_WIDTH_PCT = 11;
/** 오늘 눈금과 겹치는 기존 날짜 라벨 제거 (주간 눈금 등) */
const TODAY_TICK_MIN_GAP_PCT = 3.2;
/** 이 이상이면 일/주 단위 대신 월별 눈금 */
const MONTH_TICK_MIN_DAYS = 28;

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 quarterRange(quarterKey: string): { start: Date; end: Date } {
  return {
    start: startOfDay(quarterStartDate(quarterKey)),
    end: startOfDay(quarterEndDate(quarterKey)),
  };
}

const TIMELINE_MONTH_COUNT = 3;

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

function monthEnd(d: Date): Date {
  return startOfDay(new Date(d.getFullYear(), d.getMonth() + 1, 0));
}

function addMonths(monthAnchor: Date, delta: number): Date {
  return new Date(monthAnchor.getFullYear(), monthAnchor.getMonth() + delta, 1);
}

function monthSpanCount(firstMonth: Date, lastMonth: Date): number {
  return (lastMonth.getFullYear() - firstMonth.getFullYear()) * 12 + (lastMonth.getMonth() - firstMonth.getMonth()) + 1;
}

/** 월 초 기준 정확히 3개월 */
function threeMonthWindowFrom(windowStart: Date): { start: Date; end: Date } {
  const start = monthStart(windowStart);
  const end = monthEnd(addMonths(start, TIMELINE_MONTH_COUNT - 1));
  return { start, end };
}

/**
 * 상세 타임라인 — 항상 3개월 분할
 * - 1달 업무: 앞·뒤 1달씩 (±1)
 * - 2달 업무: 앞 1달 + 업무 2달
 * - 3달 업무: 업무 3달 그대로 (여유 달 없음)
 * - 4달 이상: 오늘(또는 기준일)이 포함된 3달, 업무 범위 안에서 이동
 */
function resolveThreeMonthWindow(
  coreStart: Date,
  coreEnd: Date,
  refDate: Date = new Date(),
): { start: Date; end: Date } {
  const firstMonth = monthStart(coreStart);
  const lastMonth = monthStart(coreEnd);
  const spanMonths = monthSpanCount(firstMonth, lastMonth);

  if (spanMonths <= 1) {
    return threeMonthWindowFrom(addMonths(firstMonth, -1));
  }

  if (spanMonths === 2) {
    return threeMonthWindowFrom(addMonths(firstMonth, -1));
  }

  if (spanMonths === 3) {
    return threeMonthWindowFrom(firstMonth);
  }

  const ref = startOfDay(refDate);
  const refClamped =
    ref.getTime() < coreStart.getTime() ? coreStart : ref.getTime() > coreEnd.getTime() ? coreEnd : ref;

  let windowStart = monthStart(refClamped);
  const earliest = firstMonth;
  const latestStart = addMonths(lastMonth, -(TIMELINE_MONTH_COUNT - 1));

  if (windowStart < earliest) windowStart = earliest;
  if (windowStart > latestStart) windowStart = latestStart;

  return threeMonthWindowFrom(windowStart);
}

interface TimelineFocusRange {
  start: Date;
  end: Date;
}

function getSelectedMilestoneSpan(
  milestones: TimelineMilestoneInput[],
  selectedId: string | null | undefined,
): { start: Date; end: Date } | null {
  if (!selectedId) return null;
  const m = milestones.find((x) => x.id === selectedId);
  if (!m) return null;

  const spans = milestonePeriodSpans(m);
  if (spans.length === 0) {
    if (m.startDate || m.dueDate) {
      const start = parseDay(m.startDate ?? m.dueDate!);
      const end = parseDay(m.dueDate ?? m.startDate!);
      return { start, end: end.getTime() < start.getTime() ? start : end };
    }
    return null;
  }

  const startMs = Math.min(...spans.map((s) => s.startMs));
  const endMs = Math.max(...spans.map((s) => s.endMs));
  return { start: new Date(startMs), end: new Date(endMs) };
}

/**
 * 선택 업무 기준 타임라인 — 항상 3개월
 */
function resolveTimelineFocusRange(
  milestones: TimelineMilestoneInput[],
  selectedId: string | null | undefined,
  fallback: TimelineRangeFallback,
): TimelineFocusRange {
  const span = getSelectedMilestoneSpan(milestones, selectedId);

  if (span) {
    return resolveThreeMonthWindow(span.start, span.end);
  }

  if (fallback.quarter) {
    return quarterRange(fallback.quarter);
  }

  return resolveThreeMonthWindow(new Date(), new Date());
}

function computeRange(
  milestones: TimelineMilestoneInput[],
  fallback: TimelineRangeFallback,
  options?: { selectedId?: string | null; focusQuarter?: boolean },
): { start: Date; end: Date } | null {
  if (options?.focusQuarter !== false) {
    return resolveTimelineFocusRange(milestones, options?.selectedId, fallback);
  }

  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 range = quarterRange(fallback.quarter);
    return { start: range.start, end: range.end };
  }

  return null;
}

function buildMonthTicks(rangeStart: Date, rangeEnd: Date): TimelineTick[] {
  const rangeMs = Math.max(rangeEnd.getTime() - rangeStart.getTime(), DAY_MS);
  const now = startOfDay(new Date());
  const todayInRange = now >= rangeStart && now <= rangeEnd;
  const nowMs = now.getTime();
  const ticks: TimelineTick[] = [];

  for (let i = 0; i < TIMELINE_MONTH_COUNT; i++) {
    const d = new Date(rangeStart.getFullYear(), rangeStart.getMonth() + i, 1);
    ticks.push({
      label: `${d.getMonth() + 1}월`,
      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;
    ticks.push({
      label: fmtTimelineDayLabel(now),
      leftPct: todayLeftPct,
      isToday: true,
    });
    ticks.sort((a, b) => a.leftPct - b.leftPct);
  }

  return ticks;
}

function buildTicks(rangeStart: Date, rangeEnd: Date, focusQuarter?: boolean): TimelineTick[] {
  if (focusQuarter !== false) {
    return buildMonthTicks(rangeStart, rangeEnd);
  }

  const rangeMs = Math.max(rangeEnd.getTime() - rangeStart.getTime(), DAY_MS);
  const totalDays = Math.ceil(rangeMs / DAY_MS) + 1;
  const stepDays = totalDays > 14 ? 7 : totalDays > 7 ? 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 pickTimelineSortMs(m: TimelineMilestoneInput): number {
  const spans = milestonePeriodSpans(m);
  if (spans.length > 0) return Math.min(...spans.map((s) => s.startMs));
  if (m.startDate) return parseDay(m.startDate).getTime();
  if (m.dueDate) return parseDay(m.dueDate).getTime();
  if (m.createdAt) return new Date(m.createdAt).getTime();
  return Number.MAX_SAFE_INTEGER;
}

export function sortMilestonesForTimeline(milestones: TimelineMilestoneInput[]): TimelineMilestoneInput[] {
  return [...milestones].sort((a, b) => {
    const diff = pickTimelineSortMs(a) - pickTimelineSortMs(b);
    if (diff !== 0) return diff;
    return a.order - b.order;
  });
}

/** 분기 전체 간트 — 프로젝트명 · 업무명 라벨 */
export function flattenTasksToTimelineMilestones(
  tasks: Array<{ id: string; title: string; milestones?: TimelineMilestoneInput[] }>,
): TimelineMilestoneInput[] {
  const items: TimelineMilestoneInput[] = [];
  for (const t of tasks) {
    for (const m of t.milestones ?? []) {
      items.push({
        ...m,
        title: `${t.title} · ${m.title}`,
      });
    }
  }
  return sortMilestonesForTimeline(items);
}

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

  const rangeStartMs = rangeStart.getTime();
  const rangeEndMs = rangeEnd.getTime();
  const rangeMs = Math.max(rangeEndMs - 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 clipStartMs = Math.max(span.startMs, rangeStartMs);
      const clipEndMs = Math.min(span.endMs, rangeEndMs);
      if (clipEndMs < clipStartMs) return;

      const spanMs = clipEndMs - clipStartMs;
      const isPoint = spanMs === 0;

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

      let leftPct = ((clipStartMs - 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?: {
    hideUndatedBars?: boolean;
    selectedId?: string | null;
    focusQuarter?: boolean;
    viewport?: { start: Date; end: Date };
    ganttBars?: boolean;
  },
): MilestoneTimelineModel | null {
  if (milestones.length === 0) return null;

  const focusQuarter = options?.focusQuarter !== false && !options?.viewport;
  const range =
    options?.viewport ??
    computeRange(milestones, fallback, {
      selectedId: options?.selectedId,
      focusQuarter,
    });
  if (!range) return null;

  const ordered = sortMilestonesForTimeline(milestones);

  const minBarWidthPct = options?.ganttBars ? MIN_GANTT_BAR_WIDTH_PCT : MIN_BAR_WIDTH_PCT;
  const rows = buildRows(ordered, range.start, range.end, options?.hideUndatedBars, minBarWidthPct);
  if (rows.length === 0 && !focusQuarter && !options?.viewport) return null;

  const useMonthTicks =
    focusQuarter || preferMonthTicks(range.start, range.end);
  const ticks = useMonthTicks
    ? buildMonthTicksInRange(range.start, range.end)
    : buildTicks(range.start, range.end, false);
  const todayTick = ticks.find((tick) => tick.isToday);

  return {
    rangeStartLabel: fmtTimelineDayLabel(range.start),
    rangeEndLabel: fmtTimelineDayLabel(range.end),
    ticks,
    rows,
    todayLeftPct: todayTick?.leftPct ?? null,
  };
}

export interface PortfolioTaskGroup {
  id: string;
  title: string;
  milestones: TimelineMilestoneInput[];
}

export function computeFullTimelineRange(
  milestones: TimelineMilestoneInput[],
  fallback: TimelineRangeFallback = {},
): { start: Date; end: Date } | null {
  return computeRange(milestones, fallback, { focusQuarter: false });
}

export type TimelineViewportMode = 'focus' | 'project' | 'portfolio';

/** 보기 모드별 차트 초기 viewport (pan/zoom 데이터 범위는 computeFullTimelineRange) */
export function resolveTimelineInitialViewport(
  viewMode: TimelineViewportMode,
  milestones: TimelineMilestoneInput[],
  fallback: TimelineRangeFallback = {},
  selectedId?: string | null,
): { start: Date; end: Date } | null {
  const dataRange = computeFullTimelineRange(milestones, fallback);
  if (!dataRange) return null;

  if (viewMode === 'project') {
    return dataRange;
  }

  if (viewMode === 'focus') {
    const focusRange = computeRange(milestones, fallback, {
      selectedId,
      focusQuarter: true,
    });
    return focusRange ?? resolvePortfolioInitialViewport(dataRange);
  }

  return resolvePortfolioInitialViewport(dataRange);
}

export function resolvePortfolioInitialViewport(
  dataRange: { start: Date; end: Date },
  refDate: Date = new Date(),
): { start: Date; end: Date } {
  return resolveThreeMonthWindow(dataRange.start, dataRange.end, refDate);
}

export function clampPortfolioViewport(
  viewport: { start: Date; end: Date },
  dataRange: { start: Date; end: Date },
): { start: Date; end: Date } {
  const dataStartMs = dataRange.start.getTime();
  const dataEndMs = dataRange.end.getTime();
  const dataSpanMs = Math.max(dataEndMs - dataStartMs, DAY_MS);

  let windowMs = Math.max(viewport.end.getTime() - viewport.start.getTime(), 7 * DAY_MS);
  windowMs = Math.min(windowMs, dataSpanMs);

  let startMs = viewport.start.getTime();
  let endMs = startMs + windowMs;

  if (endMs > dataEndMs) {
    endMs = dataEndMs;
    startMs = endMs - windowMs;
  }
  if (startMs < dataStartMs) {
    startMs = dataStartMs;
    endMs = startMs + windowMs;
  }

  endMs = Math.min(endMs, dataEndMs);
  startMs = Math.max(startMs, dataStartMs);

  return { start: new Date(startMs), end: new Date(endMs) };
}

export function panPortfolioViewport(
  viewport: { start: Date; end: Date },
  dataRange: { start: Date; end: Date },
  deltaMs: number,
): { start: Date; end: Date } {
  return clampPortfolioViewport(
    {
      start: new Date(viewport.start.getTime() + deltaMs),
      end: new Date(viewport.end.getTime() + deltaMs),
    },
    dataRange,
  );
}

export function zoomPortfolioViewport(
  viewport: { start: Date; end: Date },
  dataRange: { start: Date; end: Date },
  factor: number,
  anchorPct: number,
): { start: Date; end: Date } {
  const startMs = viewport.start.getTime();
  const endMs = viewport.end.getTime();
  const windowMs = Math.max(endMs - startMs, DAY_MS);
  const dataSpanMs = Math.max(dataRange.end.getTime() - dataRange.start.getTime(), DAY_MS);

  const minWindow = 7 * DAY_MS;
  const newWindow = Math.min(dataSpanMs, Math.max(minWindow, windowMs * factor));
  const anchorMs = startMs + windowMs * Math.min(1, Math.max(0, anchorPct));
  const newStartMs = anchorMs - newWindow * anchorPct;

  return clampPortfolioViewport(
    { start: new Date(newStartMs), end: new Date(newStartMs + newWindow) },
    dataRange,
  );
}

function preferMonthTicks(rangeStart: Date, rangeEnd: Date): boolean {
  const days = (rangeEnd.getTime() - rangeStart.getTime()) / DAY_MS;
  if (days >= MONTH_TICK_MIN_DAYS) return true;
  if (monthStart(rangeStart).getTime() !== monthStart(rangeEnd).getTime()) return true;
  return days >= 14;
}

function shouldUseMonthTicks(rangeStart: Date, rangeEnd: Date): boolean {
  return preferMonthTicks(rangeStart, rangeEnd);
}

function buildMonthTicksInRange(rangeStart: Date, rangeEnd: Date): TimelineTick[] {
  const rangeMs = Math.max(rangeEnd.getTime() - rangeStart.getTime(), DAY_MS);
  const now = startOfDay(new Date());
  const todayInRange = now >= rangeStart && now <= rangeEnd;
  const nowMs = now.getTime();
  const ticks: TimelineTick[] = [];

  for (let cursor = monthStart(rangeStart); cursor <= rangeEnd; cursor = addMonths(cursor, 1)) {
    ticks.push({
      label: `${cursor.getMonth() + 1}월`,
      leftPct: ((cursor.getTime() - rangeStart.getTime()) / rangeMs) * 100,
      isToday: todayInRange && cursor.getTime() === nowMs,
    });
  }

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

  return ticks;
}

export function milestoneHasTimelineDates(m: TimelineMilestoneInput): boolean {
  return milestoneHasDates(m);
}
