'use client';

import { useMemo, useRef, useState, useEffect } from 'react';
import Link from 'next/link';
import {
  addHours,
  addDays,
  addMonths,
  addWeeks,
  differenceInCalendarDays,
  differenceInCalendarMonths,
  differenceInCalendarWeeks,
  differenceInHours,
  startOfDay,
  startOfMonth,
  startOfQuarter,
  startOfWeek,
  startOfYear,
} from 'date-fns';
import { Button } from '@/components/ui/button';
import { updateDevItem, type DevItem } from '@/services/api/devtracker';
import { colors } from '@/styles/theme';
import '../styles/gantt.css';
import { toast } from '@/lib/toast';
import { ApiClientError } from '@/lib/api-client';

type ViewMode = 'day' | 'week' | 'month' | 'quarter' | 'year';

type DevtrackerGanttProps = {
  items: DevItem[];
  locale: string;
  t: Record<string, any>;
};

type Column = {
  key: string;
  start: Date;
  label: string;
  upperLabel: string;
  showUpper: boolean;
};

type TaskLayout = {
  id: string;
  left: number;
  width: number;
  isMilestone: boolean;
};

const ROW_HEIGHT = 60;
const BAR_HEIGHT = 28;
const MILESTONE_SIZE = 12;

const VIEW_SETTINGS: Record<ViewMode, { columnWidth: number }> = {
  day: { columnWidth: 56 },
  week: { columnWidth: 140 },
  month: { columnWidth: 160 },
  quarter: { columnWidth: 140 },
  year: { columnWidth: 110 },
};

const formatDate = (date: Date, locale: string, options: Intl.DateTimeFormatOptions) => {
  return new Intl.DateTimeFormat(locale, options).format(date);
};

const formatMonth = (date: Date, locale: string) => {
  return formatDate(date, locale, { month: 'short' });
};

const formatYear = (date: Date, locale: string) => {
  return formatDate(date, locale, { year: 'numeric' });
};

const formatWeekRange = (date: Date, locale: string) => {
  const start = startOfWeek(date, { weekStartsOn: 1 });
  const end = addDays(start, 6);
  const startLabel = formatDate(start, locale, { month: 'numeric', day: 'numeric' });
  const endLabel = formatDate(end, locale, { month: 'numeric', day: 'numeric' });
  return `${startLabel}~${endLabel}`;
};

const formatQuarter = (date: Date, locale: string) => {
  const quarter = Math.floor(date.getMonth() / 3) + 1;
  return `${formatYear(date, locale)} Q${quarter}`;
};

const getViewUnit = (view: ViewMode) => {
  if (view === 'day') return 'day';
  if (view === 'week') return 'week';
  return 'month';
};

const getUnitStart = (date: Date, view: ViewMode) => {
  if (view === 'day') return startOfDay(date);
  if (view === 'week') return startOfWeek(date, { weekStartsOn: 1 });
  return startOfMonth(date);
};

const addUnit = (date: Date, amount: number, view: ViewMode) => {
  if (view === 'day') return addDays(date, amount);
  if (view === 'week') return addWeeks(date, amount);
  return addMonths(date, amount);
};

const getUnitHours = (date: Date, view: ViewMode) => {
  const unitStart = getUnitStart(date, view);
  const unitEnd = addUnit(unitStart, 1, view);
  return Math.max(1, differenceInHours(unitEnd, unitStart));
};

const getColumnOffsetFromDate = (date: Date, base: Date, view: ViewMode) => {
  const unitStart = getUnitStart(date, view);
  const baseStart = getUnitStart(base, view);
  const offset = getOffset(unitStart, baseStart, view);
  const unitHours = getUnitHours(unitStart, view);
  const fraction = (date.getTime() - unitStart.getTime()) / (unitHours * 60 * 60 * 1000);
  return offset + fraction;
};

const getDateFromColumnOffset = (offset: number, base: Date, view: ViewMode) => {
  const baseStart = getUnitStart(base, view);
  const whole = Math.floor(offset);
  const fraction = offset - whole;
  const unitStart = addUnit(baseStart, whole, view);
  const unitHours = getUnitHours(unitStart, view);
  const ms = unitStart.getTime() + fraction * unitHours * 60 * 60 * 1000;
  return new Date(ms);
};

const getColumns = (start: Date, end: Date, view: ViewMode, locale: string) => {
  const columns: Column[] = [];
  let upperKey = '';
  if (view === 'day') {
    let cursor = startOfDay(start);
    const last = startOfDay(end);
    while (cursor <= last) {
      const key = `${cursor.getFullYear()}-${cursor.getMonth()}`;
      const showUpper = key !== upperKey;
      upperKey = key;
      columns.push({
        key: cursor.toISOString(),
        start: cursor,
        label: formatDate(cursor, locale, { day: 'numeric' }),
        upperLabel: showUpper ? formatMonth(cursor, locale) : '',
        showUpper,
      });
      cursor = addDays(cursor, 1);
    }
    return columns;
  }

  if (view === 'week') {
    let cursor = startOfWeek(start, { weekStartsOn: 1 });
    const last = startOfWeek(end, { weekStartsOn: 1 });
    while (cursor <= last) {
      const key = `${cursor.getFullYear()}-${cursor.getMonth()}`;
      const showUpper = key !== upperKey;
      upperKey = key;
      columns.push({
        key: cursor.toISOString(),
        start: cursor,
        label: formatWeekRange(cursor, locale),
        upperLabel: showUpper ? formatMonth(cursor, locale) : '',
        showUpper,
      });
      cursor = addWeeks(cursor, 1);
    }
    return columns;
  }

  if (view === 'month') {
    let cursor = startOfMonth(start);
    const last = startOfMonth(end);
    while (cursor <= last) {
      const key = `${cursor.getFullYear()}`;
      const showUpper = key !== upperKey;
      upperKey = key;
      columns.push({
        key: cursor.toISOString(),
        start: cursor,
        label: formatMonth(cursor, locale),
        upperLabel: showUpper ? formatYear(cursor, locale) : '',
        showUpper,
      });
      cursor = addMonths(cursor, 1);
    }
    return columns;
  }

  if (view === 'quarter') {
    let cursor = startOfQuarter(start);
    const last = startOfQuarter(end);
    while (cursor <= last) {
      const key = `${cursor.getFullYear()}-Q${Math.floor(cursor.getMonth() / 3) + 1}`;
      const showUpper = key !== upperKey;
      upperKey = key;
      columns.push({
        key: cursor.toISOString(),
        start: cursor,
        label: formatMonth(cursor, locale),
        upperLabel: showUpper ? formatQuarter(cursor, locale) : '',
        showUpper,
      });
      cursor = addMonths(cursor, 1);
    }
    return columns;
  }

  let cursor = startOfYear(start);
  const last = startOfYear(end);
  while (cursor <= last) {
    const key = `${cursor.getFullYear()}`;
    const showUpper = key !== upperKey;
    upperKey = key;
    columns.push({
      key: cursor.toISOString(),
      start: cursor,
      label: formatMonth(cursor, locale),
      upperLabel: showUpper ? formatYear(cursor, locale) : '',
      showUpper,
    });
    cursor = addMonths(cursor, 1);
  }
  return columns;
};

const getOffset = (start: Date, base: Date, view: ViewMode) => {
  const unit = getViewUnit(view);
  if (unit === 'day') return differenceInCalendarDays(start, base);
  if (unit === 'week') return differenceInCalendarWeeks(start, base, { weekStartsOn: 1 });
  return differenceInCalendarMonths(start, base);
};

type ResolvedItem = {
  item: DevItem;
  devStartAt: Date;
  devEndAt: Date;
  testStartAt: Date | null;
  testEndAt: Date | null;
  hasTestSegment: boolean;
  isMilestone: boolean;
};

export function DevtrackerGantt({ items, locale, t }: DevtrackerGanttProps) {
  const [viewMode, setViewMode] = useState<ViewMode>('week');
  const listRef = useRef<HTMLDivElement | null>(null);
  const timelineRef = useRef<HTMLDivElement | null>(null);
  const timelineHeaderRef = useRef<HTMLDivElement | null>(null);
  const isSyncing = useRef(false);
  const [overrides, setOverrides] = useState<
    Record<string, { startAt?: string; devEtaAt?: string; testEtaAt?: string; etaAt?: string }>
  >({});
  const [dragState, setDragState] = useState<{
    id: string;
    segment: 'dev' | 'test';
    type: 'move' | 'resize-start' | 'resize-end';
    startX: number;
    startOffset: number;
    endOffset: number;
    startAt: Date;
    endAt: Date;
    durationMs: number;
    devEtaAt?: Date | null;
    testEtaAt?: Date | null;
  } | null>(null);
  const [dragPreview, setDragPreview] = useState<{
    id: string;
    segment: 'dev' | 'test';
    type: 'move' | 'resize-start' | 'resize-end';
    startAt: Date;
    endAt: Date;
    deltaMs: number;
  } | null>(null);
  const dragPreviewRef = useRef<{
    id: string;
    segment: 'dev' | 'test';
    type: 'move' | 'resize-start' | 'resize-end';
    startAt: Date;
    endAt: Date;
    deltaMs: number;
  } | null>(null);

  const resolvedItems = useMemo<ResolvedItem[]>(() => {
    return items
      .map((item) => {
        if (item.status === 'DONE' || item.status === 'ARCHIVED') {
          return null;
        }
        const override = overrides[item.id];
        const preview = dragPreview?.id === item.id ? dragPreview : null;
        const baseStartAt = override?.startAt
          ? new Date(override.startAt)
          : item.startAt
          ? new Date(item.startAt)
          : null;
        const baseDevEtaAt = override?.devEtaAt
          ? new Date(override.devEtaAt)
          : item.devEtaAt
          ? new Date(item.devEtaAt)
          : null;
        const baseTestEtaAt = override?.testEtaAt
          ? new Date(override.testEtaAt)
          : item.testEtaAt
          ? new Date(item.testEtaAt)
          : null;

        if (!baseStartAt) return null;

        let devStartAt = baseStartAt;
        let devEndAt = baseDevEtaAt;
        let testEndAt = baseTestEtaAt;

        if (preview?.segment === 'dev') {
          if (preview.type === 'move') {
            devStartAt = new Date(devStartAt.getTime() + preview.deltaMs);
            devEndAt = new Date(devEndAt.getTime() + preview.deltaMs);
            if (baseTestEtaAt) {
              testEndAt = new Date(baseTestEtaAt.getTime() + preview.deltaMs);
            }
          } else if (preview.type === 'resize-start') {
            devStartAt = preview.startAt;
          } else {
            devEndAt = preview.endAt;
            if (baseDevEtaAt && baseTestEtaAt) {
              const delta = preview.endAt.getTime() - baseDevEtaAt.getTime();
              testEndAt = new Date(baseTestEtaAt.getTime() + delta);
            }
          }
        }

        if (preview?.segment === 'test') {
          testEndAt = preview.endAt;
        }

        if (!devEndAt) return null;

        const isMilestone = false;
        const testStartAt = devEndAt;
        const hasTestSegment = Boolean(testStartAt && testEndAt && testEndAt > testStartAt);
        return {
          item,
          isMilestone,
          devStartAt,
          devEndAt,
          testStartAt,
          testEndAt,
          hasTestSegment,
        };
      })
      .filter(
        (value): value is ResolvedItem => Boolean(value),
      );
  }, [items, overrides, dragPreview]);

  const range = useMemo(() => {
    if (resolvedItems.length === 0) return null;
    const starts: number[] = [];
    const ends: number[] = [];
    resolvedItems.forEach((entry) => {
      if (entry.devStartAt && entry.devEndAt) {
        starts.push(entry.devStartAt.getTime());
        ends.push(entry.devEndAt.getTime());
      }
      if (entry.hasTestSegment && entry.testStartAt && entry.testEndAt) {
        starts.push(entry.testStartAt.getTime());
        ends.push(entry.testEndAt.getTime());
      }
    });
    if (starts.length === 0 || ends.length === 0) return null;
    const minStart = new Date(Math.min(...starts));
    const maxEnd = new Date(Math.max(...ends));
    return {
      start: getUnitStart(minStart, viewMode),
      end: getUnitStart(maxEnd, viewMode),
    };
  }, [resolvedItems, viewMode]);

  const columns = useMemo(() => {
    if (!range) return [];
    return getColumns(range.start, range.end, viewMode, locale);
  }, [range, viewMode, locale]);

  const columnWidth = VIEW_SETTINGS[viewMode].columnWidth;

  const layout = useMemo(() => {
    if (!range) return { rows: [], tasks: new Map<string, TaskLayout>(), links: [] as Array<[string, string]> };

    const tasks = new Map<string, TaskLayout>();
    const rows = resolvedItems;
    rows.forEach((entry) => {
      const totalEndAt = entry.testEndAt ?? entry.devEndAt;
      const startOffset = getColumnOffsetFromDate(entry.devStartAt, range.start, viewMode);
      const endOffset = getColumnOffsetFromDate(totalEndAt, range.start, viewMode);
      const left = startOffset * columnWidth;
      const width = Math.max(10, (endOffset - startOffset) * columnWidth);
      tasks.set(entry.item.id, { id: entry.item.id, left, width, isMilestone: entry.isMilestone });
    });

    const links: Array<[string, string]> = [];
    rows.forEach((item) => {
      if (item.item.parentId && tasks.has(item.item.parentId)) {
        links.push([item.item.parentId, item.item.id]);
      }
      const deps = (item.item as { dependencies?: string[] }).dependencies;
      if (deps && deps.length > 0) {
        deps.forEach((dep) => {
          if (tasks.has(dep)) links.push([dep, item.item.id]);
        });
      }
    });

    return { rows, tasks, links };
  }, [range, resolvedItems, viewMode, columnWidth]);

  useEffect(() => {
    if (!dragState || !range) return;
    const onMove = (event: MouseEvent) => {
      const deltaX = event.clientX - dragState.startX;
      const deltaColumns = deltaX / columnWidth;
      let nextStartOffset = dragState.startOffset;
      let nextEndOffset = dragState.endOffset;
      const minSpanColumns = 1 / getUnitHours(getUnitStart(dragState.startAt, viewMode), viewMode);

      if (dragState.type === 'move') {
        nextStartOffset += deltaColumns;
        nextEndOffset += deltaColumns;
      } else if (dragState.type === 'resize-start') {
        nextStartOffset = Math.min(dragState.startOffset + deltaColumns, dragState.endOffset - minSpanColumns);
      } else {
        nextEndOffset = Math.max(dragState.endOffset + deltaColumns, dragState.startOffset + minSpanColumns);
      }

      let nextStart = getDateFromColumnOffset(nextStartOffset, range.start, viewMode);
      let nextEnd = getDateFromColumnOffset(nextEndOffset, range.start, viewMode);
      if (dragState.type === 'move') {
        nextStart = getDateFromColumnOffset(nextStartOffset, range.start, viewMode);
        nextEnd = new Date(nextStart.getTime() + dragState.durationMs);
      } else if (dragState.type === 'resize-start') {
        nextStart = getDateFromColumnOffset(nextStartOffset, range.start, viewMode);
        nextEnd = dragState.endAt;
      } else {
        nextStart = dragState.startAt;
        nextEnd = getDateFromColumnOffset(nextEndOffset, range.start, viewMode);
      }
      const safeEnd = nextEnd <= nextStart ? addHours(nextStart, 1) : nextEnd;
      const preview = {
        id: dragState.id,
        segment: dragState.segment,
        type: dragState.type,
        startAt: nextStart,
        endAt: safeEnd,
        deltaMs: nextStart.getTime() - dragState.startAt.getTime(),
      };
      dragPreviewRef.current = preview;
      setDragPreview(preview);
    };

    const onUp = async () => {
      const preview = dragPreviewRef.current;
      if (preview && preview.id === dragState.id) {
        const payload: { startAt?: string; devEtaAt?: string; testEtaAt?: string } = {};
        if (dragState.segment === 'dev') {
          if (dragState.type === 'move') {
            const delta = preview.deltaMs;
            payload.startAt = preview.startAt.toISOString();
            if (dragState.devEtaAt) {
              payload.devEtaAt = new Date(dragState.devEtaAt.getTime() + delta).toISOString();
            }
            if (dragState.testEtaAt) {
              payload.testEtaAt = new Date(dragState.testEtaAt.getTime() + delta).toISOString();
            }
          } else {
            if (dragState.type === 'resize-start') {
              payload.startAt = preview.startAt.toISOString();
            }
            if (dragState.type === 'resize-end') {
              payload.devEtaAt = preview.endAt.toISOString();
              if (dragState.testEtaAt) {
                const delta = preview.endAt.getTime() - (dragState.devEtaAt?.getTime() ?? preview.endAt.getTime());
                if (delta !== 0) {
                  payload.testEtaAt = new Date(dragState.testEtaAt.getTime() + delta).toISOString();
                }
              }
            }
          }
        } else {
          if (dragState.type === 'move' || dragState.type === 'resize-end') {
            payload.testEtaAt = preview.endAt.toISOString();
          }
        }

        try {
          await updateDevItem(dragState.id, payload);
          setOverrides((prev) => ({
            ...prev,
            [dragState.id]: {
              startAt: payload.startAt ?? prev[dragState.id]?.startAt,
              devEtaAt: payload.devEtaAt ?? prev[dragState.id]?.devEtaAt,
              testEtaAt: payload.testEtaAt ?? prev[dragState.id]?.testEtaAt,
            },
          }));
        } catch (error) {
          if (error instanceof ApiClientError) {
            toast.error(error.message);
          } else {
            toast.error(t.devtracker?.toast?.updateFailed);
          }
        }
      }

      document.body.style.userSelect = '';
      setDragState(null);
      setDragPreview(null);
      dragPreviewRef.current = null;
    };

    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);

    return () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
  }, [dragState, range, viewMode, t.devtracker?.toast?.updateFailed, columnWidth]);

  useEffect(() => {
    const listEl = listRef.current;
    const timelineEl = timelineRef.current;
    if (!listEl || !timelineEl) return;

    const syncScroll = (source: HTMLDivElement, target: HTMLDivElement) => {
      if (isSyncing.current) return;
      isSyncing.current = true;
      target.scrollTop = source.scrollTop;
      requestAnimationFrame(() => {
        isSyncing.current = false;
      });
    };

    const onListScroll = () => syncScroll(listEl, timelineEl);
    const onTimelineScroll = () => {
      syncScroll(timelineEl, listEl);
      if (timelineHeaderRef.current) {
        timelineHeaderRef.current.scrollLeft = timelineEl.scrollLeft;
      }
    };

    listEl.addEventListener('scroll', onListScroll);
    timelineEl.addEventListener('scroll', onTimelineScroll);

    return () => {
      listEl.removeEventListener('scroll', onListScroll);
      timelineEl.removeEventListener('scroll', onTimelineScroll);
    };
  }, []);

  if (!range || columns.length === 0) {
    return <div className="text-center text-gray-400">{t.devtracker?.empty?.gantt}</div>;
  }

  const timelineContentWidth = columns.length * columnWidth;

  const bodyHeight = layout.rows.length * ROW_HEIGHT;

  const scrollToToday = () => {
    if (!timelineRef.current || !range) return;
    const today = startOfDay(new Date());
    const offset = getOffset(today, range.start, viewMode);
    const target = Math.max(0, offset * columnWidth - timelineRef.current.clientWidth / 3);
    timelineRef.current.scrollLeft = target;
  };

  const scrollByOffset = (direction: 'prev' | 'next') => {
    if (!timelineRef.current) return;
    const delta = timelineRef.current.clientWidth * 0.6;
    timelineRef.current.scrollLeft += direction === 'next' ? delta : -delta;
  };

  const beginDrag = (
    event: React.MouseEvent<HTMLElement>,
    itemId: string,
    segment: 'dev' | 'test',
    type: 'move' | 'resize-start' | 'resize-end',
  ) => {
    event.preventDefault();
    event.stopPropagation();
    if (!range) return;
    const resolved = resolvedItems.find((entry) => entry.item.id === itemId);
    if (!resolved) return;
    const segmentStart = segment === 'dev' ? resolved.devStartAt : resolved.testStartAt;
    const segmentEnd = segment === 'dev' ? resolved.devEndAt : resolved.testEndAt;
    if (!segmentStart || !segmentEnd) return;
    const totalEndAt = resolved.testEndAt ?? resolved.devEndAt;
    const dragStartAt = type === 'move' ? resolved.devStartAt : segmentStart;
    const dragEndAt = type === 'move' ? totalEndAt : segmentEnd;
    const startOffset = getColumnOffsetFromDate(segmentStart, range.start, viewMode);
    const endOffset = getColumnOffsetFromDate(segmentEnd, range.start, viewMode);
    document.body.style.userSelect = 'none';
    setDragState({
      id: itemId,
      segment,
      type,
      startX: event.clientX,
      startOffset,
      endOffset,
      startAt: dragStartAt,
      endAt: dragEndAt,
      durationMs: dragEndAt.getTime() - dragStartAt.getTime(),
      devEtaAt: resolved.devEndAt,
      testEtaAt: resolved.testEndAt,
    });
    const preview = {
      id: itemId,
      segment,
      type,
      startAt: dragStartAt,
      endAt: dragEndAt,
      deltaMs: 0,
    };
    dragPreviewRef.current = preview;
    setDragPreview(preview);
  };

  const getDurationDays = (start: Date, end: Date) => {
    return Math.max(1, differenceInCalendarDays(end, start) + 1);
  };

  const getSegmentLayout = (start: Date, end: Date) => {
    if (!range) return null;
    const startOffset = getColumnOffsetFromDate(start, range.start, viewMode);
    const endOffset = getColumnOffsetFromDate(end, range.start, viewMode);
    const left = startOffset * columnWidth;
    const width = Math.max(12, (endOffset - startOffset) * columnWidth);
    return { left, width };
  };

  return (
    <div className="devtracker-gantt">
      <div className="devtracker-gantt__toolbar">
        <div className="devtracker-gantt__toolbar-title">{t.devtracker?.gantt?.toolbar?.view}</div>
        <div className="devtracker-gantt__toolbar-actions">
          <Button size="sm" variant="outline" onClick={() => scrollByOffset('prev')}>
            {t.devtracker?.gantt?.toolbar?.prev}
          </Button>
          <Button size="sm" variant="outline" onClick={scrollToToday}>
            {t.devtracker?.gantt?.toolbar?.today}
          </Button>
          <Button size="sm" variant="outline" onClick={() => scrollByOffset('next')}>
            {t.devtracker?.gantt?.toolbar?.next}
          </Button>
        </div>
        <div className="devtracker-gantt__view-switch">
          {(['day', 'week', 'month', 'quarter', 'year'] as const).map((mode, index) => (
            <Button
              key={`${mode}-${index}`}
              size="sm"
              variant={viewMode === mode ? 'default' : 'outline'}
              onClick={() => setViewMode(mode)}
            >
              {t.devtracker?.gantt?.views?.[mode]}
            </Button>
          ))}
        </div>
      </div>

      <div className="devtracker-gantt__panel">
        <div className="devtracker-gantt__sidebar">
          <div className="devtracker-gantt__header" style={{ height: 56 }}>
            {t.devtracker?.fields?.title}
          </div>
          <div className="devtracker-gantt__list" ref={listRef}>
            {layout.rows.map((row, index) => {
              return (
                <div key={`${row.item.id}-${index}`} className="devtracker-gantt__row" style={{ height: ROW_HEIGHT }}>
                  <Link
                    href={`/devtracker/items/${row.item.id}`}
                    className="devtracker-gantt__code"
                  >
                    {row.item.title}
                  </Link>
                </div>
              );
            })}
          </div>
        </div>

        <div className="devtracker-gantt__timeline">
          <div
            className="devtracker-gantt__header devtracker-gantt__header--timeline"
            style={{ height: 56 }}
            ref={timelineHeaderRef}
          >
            <div
              className="devtracker-gantt__header-grid"
              style={{
                width: timelineContentWidth,
                gridTemplateColumns: `repeat(${columns.length}, minmax(0, 1fr))`,
              }}
            >
              {columns.map((column, index) => (
                <div key={`${column.key}-upper-${index}`} className="devtracker-gantt__header-cell devtracker-gantt__header-cell--upper">
                  {column.upperLabel}
                </div>
              ))}
            </div>
            <div
              className="devtracker-gantt__header-grid"
              style={{
                width: timelineContentWidth,
                gridTemplateColumns: `repeat(${columns.length}, minmax(0, 1fr))`,
              }}
            >
              {columns.map((column, index) => (
                <div key={`${column.key}-${index}`} className="devtracker-gantt__header-cell">
                  {column.label}
                </div>
              ))}
            </div>
          </div>
          <div
            className="devtracker-gantt__timeline-body"
            ref={timelineRef}
            style={{ minHeight: bodyHeight }}
          >
            <div
              className="devtracker-gantt__grid"
              style={{
                width: timelineContentWidth,
                gridTemplateColumns: `repeat(${columns.length}, minmax(0, 1fr))`,
                height: bodyHeight,
              }}
            >
              {columns.map((column, index) => (
                <div key={`${column.key}-grid-${index}`} className="devtracker-gantt__grid-cell" />
              ))}
            </div>
            <div className="devtracker-gantt__bars" style={{ width: timelineContentWidth, height: bodyHeight }}>
              {layout.rows.map((row, rowIndex) => {
                const layoutItem = layout.tasks.get(row.item.id);
                if (!layoutItem) return null;
                if (layoutItem.isMilestone) {
                  return (
                    <div
                      key={`${row.item.id}-${rowIndex}`}
                      className="devtracker-gantt__milestone"
                      style={{
                        left: layoutItem.left + 6,
                        top: rowIndex * ROW_HEIGHT + (ROW_HEIGHT - MILESTONE_SIZE) / 2,
                        width: MILESTONE_SIZE,
                        height: MILESTONE_SIZE,
                      }}
                    />
                  );
                }

                const totalEndAt = row.testEndAt ?? row.devEndAt;
                const totalLayout = getSegmentLayout(row.devStartAt, totalEndAt);
                if (!totalLayout) return null;
                const totalDuration = getDurationDays(row.devStartAt, totalEndAt);
                const devDuration = getDurationDays(row.devStartAt, row.devEndAt);
                const devPercent = Math.min(100, Math.max(0, (devDuration / totalDuration) * 100));
                const top = rowIndex * ROW_HEIGHT + (ROW_HEIGHT - BAR_HEIGHT) / 2;
                return (
                  <div
                    key={`${row.item.id}-${rowIndex}`}
                    className={`devtracker-gantt__bar devtracker-gantt__bar--stack ${
                      dragState?.id === row.item.id ? 'devtracker-gantt__bar--dragging' : ''
                    }`}
                    style={{
                      left: totalLayout.left,
                      top,
                      width: totalLayout.width,
                      height: BAR_HEIGHT,
                    }}
                    onMouseDown={(event) => beginDrag(event, row.item.id, 'dev', 'move')}
                    title={row.item.title}
                  >
                    <span
                      className="devtracker-gantt__handle devtracker-gantt__handle--start"
                      onMouseDown={(event) => beginDrag(event, row.item.id, 'dev', 'resize-start')}
                    />
                    {row.hasTestSegment ? (
                      <span
                        className="devtracker-gantt__handle devtracker-gantt__handle--split"
                        style={{ left: `${devPercent}%` }}
                        onMouseDown={(event) => beginDrag(event, row.item.id, 'dev', 'resize-end')}
                      />
                    ) : null}
                    <span
                      className="devtracker-gantt__handle devtracker-gantt__handle--end"
                      onMouseDown={(event) =>
                        beginDrag(event, row.item.id, row.hasTestSegment ? 'test' : 'dev', 'resize-end')
                      }
                    />
                    <div className="devtracker-gantt__bar-track">
                      <div
                        className="devtracker-gantt__bar-segment dev"
                        style={{ width: `${devPercent}%` }}
                      />
                      <div
                        className="devtracker-gantt__bar-segment test"
                        style={{ width: `${100 - devPercent}%` }}
                      />
                    </div>
                    {totalLayout.width > 120 ? (
                      <span className="devtracker-gantt__bar-label">
                        {row.item.title} · {totalDuration}
                        {t.devtracker?.gantt?.days}
                      </span>
                    ) : null}
                  </div>
                );
              })}
              <svg
                className="devtracker-gantt__links"
                width={timelineContentWidth}
                height={bodyHeight}
              >
                <defs>
                  <marker
                    id="gantt-arrow"
                    markerWidth="6"
                    markerHeight="6"
                    refX="6"
                    refY="3"
                    orient="auto"
                  >
                    <path d="M0,0 L6,3 L0,6 Z" fill={colors.border} />
                  </marker>
                </defs>
                {layout.links.map(([from, to], index) => {
                  const fromLayout = layout.tasks.get(from);
                  const toLayout = layout.tasks.get(to);
                  const fromIndex = layout.rows.findIndex((row) => row.item.id === from);
                  const toIndex = layout.rows.findIndex((row) => row.item.id === to);
                  if (!fromLayout || !toLayout || fromIndex < 0 || toIndex < 0) return null;
                  const startX = fromLayout.left + fromLayout.width;
                  const startY = fromIndex * ROW_HEIGHT + ROW_HEIGHT / 2;
                  const endX = toLayout.left;
                  const endY = toIndex * ROW_HEIGHT + ROW_HEIGHT / 2;
                  const midX = Math.min(startX + 24, endX - 24);
                  const path = `M${startX} ${startY} L${midX} ${startY} L${midX} ${endY} L${endX} ${endY}`;
                  return (
                    <path
                      key={`${from}-${to}-${index}`}
                      d={path}
                      fill="none"
                      stroke={colors.border}
                      strokeWidth="1"
                      markerEnd="url(#gantt-arrow)"
                    />
                  );
                })}
              </svg>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}
