import { router } from '@inertiajs/react';
import { CalendarClock, ChevronRight, Plus, Search, X } from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { toast } from 'sonner';

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
    Popover,
    PopoverContent,
    PopoverTrigger,
} from '@/components/ui/popover';
import { Skeleton } from '@/components/ui/skeleton';
import {
    DAY_MS,
    MILESTONE_SIZE,
    barFor,
    dependencyPath,
    durationInDays,
    firstSaturdayOffset,
    ganttRange,
    isOverdue,
    orderRows,
    subtaskProgress,
} from '@/lib/gantt-geometry';
import type { GanttBar } from '@/lib/gantt-geometry';
import { TASK_DETAIL_STATUS_MAP } from '@/lib/status-config';
import { cn } from '@/lib/utils';
import type {
    GanttDependency,
    GanttTask,
    ProjectGanttData,
} from '@/types/project';

interface ProjectGanttProps {
    /** `undefined` selama partial reload prop `gantt` belum selesai. */
    data?: ProjectGanttData;
    /** Owner/PIC project — hanya mereka yang boleh menyunting ketergantungan. */
    canManage: boolean;
    /** Membuka sheet detail task milik tab Tasks. */
    onOpenTask: (taskId: number) => void;
}

const ROW_HEIGHT = 44;
const BAR_HEIGHT = 20;

/** Batang ringkasan (task yang punya subtask) dibuat lebih tipis dari batang biasa. */
const SUMMARY_HEIGHT = 14;

/**
 * Bentuk kurung khas batang ringkasan Gantt: pipih di tengah dengan kaki turun
 * di kedua ujung, supaya sekilas terlihat beda dari task yang dikerjakan sendiri.
 */
const SUMMARY_CLIP =
    'polygon(0 0, 100% 0, 100% 100%, calc(100% - 8px) 45%, 8px 45%, 0 100%)';

/** Di bawah lebar ini bentuk kurungnya tidak terbaca, jadi dipakai batang biasa. */
const MIN_SUMMARY_WIDTH = 24;

/**
 * Lebar panel kiri disimpan sebagai custom property, bukan angka di JS: kolom
 * tanggal hanya muncul di layar lebar, dan variabelnya membuat pergantian itu
 * murni urusan CSS.
 */
const LABEL_COLUMN = 'w-[var(--gantt-label)] shrink-0';
const LABEL_WIDTH_VARS = '[--gantt-label:288px] lg:[--gantt-label:456px]';

/** Kolom tanggal & durasi di panel kiri — disembunyikan di layar sempit. */
const DATE_COLUMN = 'hidden w-20 shrink-0 pr-2 text-right lg:block';
const DURATION_COLUMN = 'hidden w-14 shrink-0 pr-3 text-right lg:block';

/** Lebar satu hari per skala tampilan. */
const PX_PER_DAY = { day: 32, week: 10 } as const;

type Scale = keyof typeof PX_PER_DAY;

const firstErrorMessage = (errors: Record<string, string>) =>
    Object.values(errors)[0] || 'Gagal memperbarui ketergantungan task';

/** Tanggal ringkas untuk label header dan tooltip. */
const shortDate = (value: string | null) =>
    value
        ? new Date(value).toLocaleDateString('id-ID', {
              day: 'numeric',
              month: 'short',
              year: 'numeric',
          })
        : '-';

/** Versi pendek tanpa tahun, untuk kolom sempit di panel kiri. */
const columnDate = (value: string | null) =>
    value
        ? new Date(value).toLocaleDateString('id-ID', {
              day: 'numeric',
              month: 'short',
          })
        : '—';

/** Tanggal ke-`index` dari awal rentang, aman terhadap pergantian DST. */
function dateAt(startMs: number, index: number): Date {
    const date = new Date(startMs);
    date.setDate(date.getDate() + index);

    return date;
}

export function ProjectGantt({
    data,
    canManage,
    onOpenTask,
}: ProjectGanttProps) {
    if (!data) {
        return (
            <div className="space-y-3">
                <Skeleton className="h-10 w-full" />
                <Skeleton className="h-64 w-full" />
            </div>
        );
    }

    return (
        <GanttChart data={data} canManage={canManage} onOpenTask={onOpenTask} />
    );
}

function GanttChart({
    data,
    canManage,
    onOpenTask,
}: {
    data: ProjectGanttData;
    canManage: boolean;
    onOpenTask: (taskId: number) => void;
}) {
    const { tasks, dependencies } = data;

    const range = useMemo(() => ganttRange(tasks), [tasks]);
    const rows = useMemo(() => orderRows(tasks), [tasks]);

    // Selalu dibuka harian, berapa pun panjang project-nya: skala mingguan
    // menyembunyikan hari dan itu justru yang paling sering dicari. Rentang
    // panjang tetap terbaca karena bagan digulir ke hari ini saat dibuka.
    const [scale, setScale] = useState<Scale>('day');

    const pxPerDay = PX_PER_DAY[scale];

    // Posisi tiap batang, dikunci ke id task supaya panah ketergantungan bisa
    // mencarinya tanpa menelusuri ulang daftar baris.
    const bars = useMemo(() => {
        const placed = new Map<number, { bar: GanttBar; rowIndex: number }>();

        if (!range) {
            return placed;
        }

        rows.forEach((row, rowIndex) => {
            const bar = barFor(row.task, range, pxPerDay);

            if (bar) {
                placed.set(row.task.id, { bar, rowIndex });
            }
        });

        return placed;
    }, [rows, range, pxPerDay]);

    const months = useMemo(() => {
        if (!range) {
            return [];
        }

        const groups: { label: string; days: number }[] = [];

        for (let index = 0; index < range.days; index++) {
            const date = dateAt(range.startMs, index);
            const label = date.toLocaleDateString('id-ID', {
                month: 'long',
                year: 'numeric',
            });
            const current = groups.at(-1);

            if (current?.label === label) {
                current.days += 1;

                continue;
            }

            groups.push({ label, days: 1 });
        }

        return groups;
    }, [range]);

    // Kolom header: per hari di skala harian, per minggu di skala mingguan.
    const columns = useMemo(() => {
        if (!range) {
            return [];
        }

        const step = scale === 'day' ? 1 : 7;
        const cells: {
            index: number;
            label: string;
            /** Nama hari, hanya di skala harian. */
            weekday: string | null;
            span: number;
        }[] = [];

        for (let index = 0; index < range.days; index += step) {
            const date = dateAt(range.startMs, index);

            cells.push({
                index,
                label:
                    scale === 'day'
                        ? String(date.getDate())
                        : date.toLocaleDateString('id-ID', {
                              day: 'numeric',
                              month: 'short',
                          }),
                // 'short' ("Sen"), bukan 'narrow': bentuk narrow memulangkan satu
                // huruf, dan Senin/Selasa/Sabtu sama-sama jadi "S".
                weekday:
                    scale === 'day'
                        ? date.toLocaleDateString('id-ID', { weekday: 'short' })
                        : null,
                span: Math.min(step, range.days - index),
            });
        }

        return cells;
    }, [range, scale]);

    // Kemajuan hanya ada untuk task yang punya subtask; sisanya sengaja kosong.
    const progressByTask = useMemo(() => subtaskProgress(tasks), [tasks]);

    const scrollRef = useRef<HTMLDivElement>(null);

    // Bagan dibuka pada hari ini, bukan di ujung kiri rentang: project yang sudah
    // berjalan setahun akan membuka layar berisi bulan-bulan yang sudah lewat.
    useEffect(() => {
        const container = scrollRef.current;

        if (!container || !range) {
            return;
        }

        const offset = Math.round(
            (new Date().setHours(0, 0, 0, 0) - range.startMs) / DAY_MS,
        );

        if (offset < 0 || offset >= range.days) {
            return;
        }

        container.scrollLeft = Math.max(
            0,
            (offset + 0.5) * pxPerDay - container.clientWidth / 3,
        );
    }, [range, pxPerDay]);

    const submit = (
        method: 'post' | 'delete',
        url: string,
        payload: Record<string, number> = {},
    ) => {
        // `only: ['gantt']` wajib, bukan optimasi: prop-nya Inertia::optional,
        // jadi respons penuh justru TIDAK memuatnya dan bagan akan kosong lagi.
        const options = {
            preserveScroll: true,
            preserveState: true,
            only: ['gantt'],
            onError: (errors: Record<string, string>) =>
                toast.error(firstErrorMessage(errors)),
        };

        if (method === 'delete') {
            router.delete(url, options);

            return;
        }

        router.post(url, payload, options);
    };

    const addDependency = (successorId: number, predecessorId: number) =>
        submit('post', `/tasks/${successorId}/dependencies`, {
            predecessor_id: predecessorId,
        });

    const removeDependency = (successorId: number, predecessorId: number) =>
        submit('delete', `/tasks/${successorId}/dependencies/${predecessorId}`);

    if (rows.length === 0) {
        return <GanttEmptyState message="Project ini belum punya task." />;
    }

    if (!range) {
        return (
            <GanttEmptyState message="Belum ada task yang punya tanggal. Isi tenggat task untuk melihatnya di sini." />
        );
    }

    const timelineWidth = range.days * pxPerDay;
    const todayOffset = Math.round(
        (new Date().setHours(0, 0, 0, 0) - range.startMs) / DAY_MS,
    );
    const showToday = todayOffset >= 0 && todayOffset < range.days;

    return (
        <div className="space-y-4">
            <div className="flex flex-wrap items-center justify-between gap-3">
                <StatusLegend />

                <div className="flex items-center gap-1 rounded-lg border p-1">
                    {(['day', 'week'] as const).map((option) => (
                        <Button
                            key={option}
                            type="button"
                            size="sm"
                            variant={scale === option ? 'secondary' : 'ghost'}
                            className="h-7 px-3 text-xs"
                            onClick={() => setScale(option)}
                        >
                            {option === 'day' ? 'Harian' : 'Mingguan'}
                        </Button>
                    ))}
                </div>
            </div>

            {/* Tingginya dibatasi supaya bagan menggulir sendiri, bukan ikut
                halaman: tanpa scroll container di sini `sticky top-0` pada
                header tidak punya apa pun untuk ditempeli, dan baris tanggal
                hilang begitu daftar task lebih panjang dari satu layar. */}
            <div
                ref={scrollRef}
                className={cn(
                    'max-h-[88vh] overflow-auto rounded-xl border',
                    LABEL_WIDTH_VARS,
                )}
            >
                <div className="min-w-max">
                    {/* Kedua bagian header memakai `bg-muted` yang pekat, bukan
                        `bg-muted/40`. Keduanya melayang di atas konten lain —
                        baris ini menempel saat digulir vertikal, panel kirinya
                        saat digulir horizontal — dan latar tembus pandang
                        membuat kolom tanggal serta batang terbaca menembusnya.
                        Latar baris induk hanya jaring pengaman untuk sisa ruang
                        di kanan ketika rentangnya lebih sempit dari layar. */}
                    <div className="sticky top-0 z-40 flex border-b bg-background">
                        <div
                            className={cn(
                                'sticky left-0 z-30 flex items-end border-r bg-muted py-2 text-xs font-semibold text-muted-foreground',
                                LABEL_COLUMN,
                            )}
                        >
                            <span className="min-w-0 flex-1 px-3">Task</span>
                            <span className={DATE_COLUMN}>Mulai</span>
                            <span className={DATE_COLUMN}>Tenggat</span>
                            <span className={DURATION_COLUMN}>Durasi</span>
                        </div>

                        <div
                            className="relative bg-muted"
                            style={{ width: timelineWidth }}
                        >
                            {/* Penanda hari ini duduk di header, bukan di ujung
                                atas garisnya: header ikut menempel saat digulir,
                                jadi garis merahnya tetap punya keterangan di
                                baris ke-80 sekalipun. */}
                            {showToday && (
                                <span
                                    className="pointer-events-none absolute bottom-0 z-10 -translate-x-1/2 rounded-t bg-rose-500 px-1 text-[9px] leading-4 font-medium whitespace-nowrap text-white"
                                    style={{
                                        left: (todayOffset + 0.5) * pxPerDay,
                                    }}
                                >
                                    Hari ini
                                </span>
                            )}

                            <div className="flex">
                                {months.map((month) => (
                                    <div
                                        key={month.label}
                                        className="shrink-0 border-r px-2 py-1 text-xs font-semibold whitespace-nowrap"
                                        style={{
                                            width: month.days * pxPerDay,
                                        }}
                                    >
                                        {month.label}
                                    </div>
                                ))}
                            </div>

                            <div className="flex">
                                {columns.map((column) => (
                                    <div
                                        key={column.index}
                                        className="shrink-0 border-r py-1 text-center text-[10px] text-muted-foreground"
                                        style={{
                                            width: column.span * pxPerDay,
                                        }}
                                    >
                                        {column.weekday && (
                                            <span className="block text-[9px] leading-3 opacity-70">
                                                {column.weekday}
                                            </span>
                                        )}
                                        {column.label}
                                    </div>
                                ))}
                            </div>
                        </div>
                    </div>

                    <div className="flex">
                        <div
                            className={cn(
                                // z-30: di atas panah ketergantungan (z-20),
                                // yang tanpa ini menang lewat urutan DOM dan
                                // tergambar melintasi panel saat digulir ke
                                // kanan. Tetap di bawah header (z-40).
                                'sticky left-0 z-30 border-r bg-background',
                                LABEL_COLUMN,
                            )}
                        >
                            {rows.map((row) => (
                                <TaskLabel
                                    key={row.task.id}
                                    task={row.task}
                                    depth={row.depth}
                                    canManage={canManage}
                                    tasks={tasks}
                                    dependencies={dependencies}
                                    onAdd={addDependency}
                                    onRemove={removeDependency}
                                    onOpenTask={onOpenTask}
                                />
                            ))}
                        </div>

                        <div
                            className="relative"
                            style={{
                                width: timelineWidth,
                                height: rows.length * ROW_HEIGHT,
                            }}
                        >
                            {/* Akhir pekan diarsir hanya di skala harian —
                                di skala mingguan satu kolom sudah memuat
                                keduanya, jadi arsirannya kehilangan arti.
                                Polanya berulang tiap 7 hari dengan Sabtu-Minggu
                                di depan, lalu digeser ke Sabtu pertama. */}
                            {scale === 'day' && (
                                <div
                                    aria-hidden
                                    className="pointer-events-none absolute inset-0 text-muted-foreground/10"
                                    style={{
                                        backgroundImage: `repeating-linear-gradient(to right, currentColor 0 ${2 * pxPerDay}px, transparent ${2 * pxPerDay}px ${7 * pxPerDay}px)`,
                                        backgroundPosition: `${firstSaturdayOffset(range.startMs) * pxPerDay}px 0`,
                                    }}
                                />
                            )}

                            {/* Garis kolom digambar sebagai satu gradien, bukan
                                satu div per hari: rentang dua tahun di skala
                                harian berarti ratusan elemen yang tidak pernah
                                disentuh siapa pun. */}
                            <div
                                aria-hidden
                                className="pointer-events-none absolute inset-0 text-border"
                                style={{
                                    backgroundImage:
                                        'repeating-linear-gradient(to right, currentColor 0 1px, transparent 1px 100%)',
                                    backgroundSize: `${pxPerDay * (scale === 'day' ? 1 : 7)}px 100%`,
                                }}
                            />

                            {showToday && (
                                <div
                                    aria-hidden
                                    className="pointer-events-none absolute top-0 bottom-0 z-10 w-px bg-rose-500/70"
                                    style={{
                                        left: (todayOffset + 0.5) * pxPerDay,
                                    }}
                                />
                            )}

                            {rows.map((row, rowIndex) => (
                                <GanttRowBar
                                    key={row.task.id}
                                    task={row.task}
                                    bar={bars.get(row.task.id)?.bar}
                                    rowIndex={rowIndex}
                                    progress={progressByTask.get(row.task.id)}
                                    onOpenTask={onOpenTask}
                                />
                            ))}

                            <DependencyArrows
                                dependencies={dependencies}
                                bars={bars}
                            />
                        </div>
                    </div>
                </div>
            </div>
        </div>
    );
}

function GanttEmptyState({ message }: { message: string }) {
    return (
        <div className="flex flex-col items-center gap-2 rounded-xl border border-dashed py-16 text-center">
            <CalendarClock className="text-muted-foreground" size={28} />
            <p className="text-sm text-muted-foreground">{message}</p>
        </div>
    );
}

function StatusLegend() {
    return (
        <div className="flex flex-wrap items-center gap-x-4 gap-y-2">
            {Object.entries(TASK_DETAIL_STATUS_MAP).map(([status, config]) => (
                <span
                    key={status}
                    className="flex items-center gap-1.5 text-xs text-muted-foreground"
                >
                    <span
                        className={cn('h-2.5 w-2.5 rounded-sm', config.dot)}
                    />
                    {config.label}
                </span>
            ))}

            <span className="flex items-center gap-1.5 text-xs text-muted-foreground">
                <span className="h-2.5 w-2.5 rounded-sm bg-muted ring-2 ring-rose-500" />
                Terlambat
            </span>
        </div>
    );
}

function GanttRowBar({
    task,
    bar,
    rowIndex,
    progress,
    onOpenTask,
}: {
    task: GanttTask;
    bar?: GanttBar;
    rowIndex: number;
    /** Rasio subtask yang selesai; `undefined` bila task ini tidak punya subtask. */
    progress?: number;
    onOpenTask: (taskId: number) => void;
}) {
    const status = TASK_DETAIL_STATUS_MAP[task.status];
    // Keterlambatan ditumpangkan sebagai garis tepi, bukan warna batang: warna
    // batang sudah dipakai status, dan menimpanya berarti kehilangan informasi
    // apakah task terlambat itu belum mulai atau sedang berjalan.
    const overdue = isOverdue(task);
    const duration = durationInDays(task);
    const tooltip = [
        `${task.code} ${task.name}`,
        `Mulai: ${shortDate(task.start_date)}`,
        `Tenggat: ${shortDate(task.due_date)}${overdue ? ' (terlambat)' : ''}`,
        duration !== null ? `Durasi: ${duration} hari` : null,
        progress !== undefined
            ? `Subtask selesai: ${Math.round(progress * 100)}%`
            : null,
        task.assignees.length > 0
            ? `Pelaksana: ${task.assignees.map((person) => person.name).join(', ')}`
            : 'Belum ada pelaksana',
    ]
        .filter(Boolean)
        .join('\n');

    // Task yang punya subtask digambar sebagai batang ringkasan: yang dilaporkan
    // di baris itu adalah rangkuman pekerjaan di bawahnya, bukan pekerjaan yang
    // dikerjakan sendiri.
    const isSummary = progress !== undefined;
    const barWidth = bar ? Math.max(bar.width, 4) : 0;
    const useBracket = isSummary && barWidth >= MIN_SUMMARY_WIDTH;
    const barHeight = isSummary ? SUMMARY_HEIGHT : BAR_HEIGHT;

    // Batangnya tombol, bukan div ber-`title`: isinya sekarang bisa dibuka, dan
    // sebagai tombol ia sekaligus bisa dicapai keyboard — `title` sendiri tidak
    // pernah sampai ke pengguna yang tidak memakai tetikus.
    const openProps = {
        type: 'button' as const,
        title: tooltip,
        'aria-label': `Buka detail ${task.code} ${task.name}`,
        onClick: () => onOpenTask(task.id),
    };

    return (
        <div
            className="absolute right-0 left-0 border-b"
            style={{ top: rowIndex * ROW_HEIGHT, height: ROW_HEIGHT }}
        >
            {!bar ? null : bar.isMilestone ? (
                // Task bertenggat tapi tanpa tanggal mulai: satu titik di
                // tenggatnya, bukan batang berdurasi karangan.
                <button
                    {...openProps}
                    className={cn(
                        'absolute rotate-45 cursor-pointer rounded-[2px] ring-2',
                        status?.dot ?? 'bg-slate-500',
                        overdue ? 'ring-rose-500' : 'ring-background',
                    )}
                    style={{
                        left: bar.left,
                        width: MILESTONE_SIZE,
                        height: MILESTONE_SIZE,
                        top: (ROW_HEIGHT - MILESTONE_SIZE) / 2,
                        transform: 'translateX(-50%) rotate(45deg)',
                    }}
                />
            ) : (
                <button
                    {...openProps}
                    className={cn(
                        'absolute flex cursor-pointer items-center overflow-hidden text-[10px] font-medium text-white',
                        isSummary ? 'rounded-sm' : 'rounded-md px-2',
                        status?.dot ?? 'bg-slate-500',
                        overdue && 'ring-2 ring-rose-500',
                    )}
                    style={{
                        left: bar.left,
                        width: barWidth,
                        height: barHeight,
                        top: (ROW_HEIGHT - barHeight) / 2,
                        clipPath: useBracket ? SUMMARY_CLIP : undefined,
                    }}
                >
                    {progress !== undefined && progress > 0 && (
                        // Bagian gelap = porsi subtask yang sudah selesai.
                        <div
                            aria-hidden
                            className="absolute inset-y-0 left-0 bg-black/35"
                            style={{ width: `${progress * 100}%` }}
                        />
                    )}

                    {!isSummary && (
                        <span className="relative truncate">{task.code}</span>
                    )}
                </button>
            )}
        </div>
    );
}

function DependencyArrows({
    dependencies,
    bars,
}: {
    dependencies: GanttDependency[];
    bars: Map<number, { bar: GanttBar; rowIndex: number }>;
}) {
    const rowCenter = (rowIndex: number) =>
        rowIndex * ROW_HEIGHT + ROW_HEIGHT / 2;

    return (
        <svg
            aria-hidden
            className="pointer-events-none absolute inset-0 z-20 h-full w-full overflow-visible text-muted-foreground"
        >
            <defs>
                <marker
                    id="gantt-arrowhead"
                    markerWidth="6"
                    markerHeight="6"
                    refX="5"
                    refY="3"
                    orient="auto"
                >
                    <path d="M0 0 L6 3 L0 6 z" fill="currentColor" />
                </marker>
            </defs>

            {dependencies.map((dependency) => {
                const from = bars.get(dependency.predecessor_id);
                const to = bars.get(dependency.successor_id);

                // Salah satu ujungnya tidak punya tanggal, jadi tidak punya
                // titik di sumbu waktu — relasinya tetap ada, hanya tak tergambar.
                if (!from || !to) {
                    return null;
                }

                return (
                    <path
                        key={`${dependency.predecessor_id}-${dependency.successor_id}`}
                        d={dependencyPath(
                            {
                                x: from.bar.exitX,
                                y: rowCenter(from.rowIndex),
                            },
                            { x: to.bar.entryX, y: rowCenter(to.rowIndex) },
                            ROW_HEIGHT,
                        )}
                        fill="none"
                        stroke="currentColor"
                        strokeWidth="1.5"
                        markerEnd="url(#gantt-arrowhead)"
                    />
                );
            })}
        </svg>
    );
}

function TaskLabel({
    task,
    depth,
    canManage,
    tasks,
    dependencies,
    onAdd,
    onRemove,
    onOpenTask,
}: {
    task: GanttTask;
    depth: number;
    canManage: boolean;
    tasks: GanttTask[];
    dependencies: GanttDependency[];
    onAdd: (successorId: number, predecessorId: number) => void;
    onRemove: (successorId: number, predecessorId: number) => void;
    onOpenTask: (taskId: number) => void;
}) {
    const predecessors = dependencies.filter(
        (dependency) => dependency.successor_id === task.id,
    );
    const duration = durationInDays(task);
    const overdue = isOverdue(task);

    return (
        <div
            className="group flex items-center border-b"
            style={{ height: ROW_HEIGHT }}
        >
            <div
                className="flex min-w-0 flex-1 items-center gap-1.5 pr-2"
                style={{ paddingLeft: 12 + depth * 16 }}
            >
                {depth > 0 && (
                    <ChevronRight
                        className="shrink-0 text-muted-foreground"
                        size={12}
                    />
                )}

                <button
                    type="button"
                    className="cursor-pointer truncate text-left text-sm hover:underline"
                    title={task.name}
                    onClick={() => onOpenTask(task.id)}
                >
                    {task.name}
                </button>

                {predecessors.length > 0 && (
                    <span
                        className="shrink-0 rounded bg-muted px-1 text-[10px] text-muted-foreground"
                        title={`${predecessors.length} task harus selesai lebih dulu`}
                    >
                        {predecessors.length}
                    </span>
                )}

                {/* Tombolnya hanya tumbuh di task induk. Penjaganya `parent_id`,
                    bukan `depth`: subtask yang induknya tersaring `visibleTo()`
                    naik jadi baris tingkat atas, dan ia tetap subtask di mata
                    server. */}
                {canManage && task.parent_id === null && (
                    <DependencyEditor
                        task={task}
                        tasks={tasks}
                        predecessorIds={predecessors.map(
                            (dependency) => dependency.predecessor_id,
                        )}
                        onAdd={onAdd}
                        onRemove={onRemove}
                    />
                )}
            </div>

            <span className={cn(DATE_COLUMN, 'text-xs text-muted-foreground')}>
                {columnDate(task.start_date)}
            </span>
            <span
                className={cn(
                    DATE_COLUMN,
                    'text-xs',
                    overdue
                        ? 'font-semibold text-rose-600 dark:text-rose-400'
                        : 'text-muted-foreground',
                )}
            >
                {columnDate(task.due_date)}
            </span>
            <span
                className={cn(DURATION_COLUMN, 'text-xs text-muted-foreground')}
            >
                {duration === null ? '—' : `${duration}h`}
            </span>
        </div>
    );
}

function DependencyEditor({
    task,
    tasks,
    predecessorIds,
    onAdd,
    onRemove,
}: {
    task: GanttTask;
    tasks: GanttTask[];
    predecessorIds: number[];
    onAdd: (successorId: number, predecessorId: number) => void;
    onRemove: (successorId: number, predecessorId: number) => void;
}) {
    const [keyword, setKeyword] = useState('');

    const byId = useMemo(
        () => new Map(tasks.map((candidate) => [candidate.id, candidate])),
        [tasks],
    );

    // Penyaringan di sini hanya soal kenyamanan; lingkaran ketergantungan tetap
    // ditolak server, yang punya gambaran utuh grafnya.
    const candidates = tasks.filter(
        (candidate) =>
            candidate.id !== task.id &&
            // Subtask tidak ikut: ia hanya memecah pekerjaan induknya, jadi
            // tidak pernah menjadi prasyarat task lain. Server menolaknya juga.
            candidate.parent_id === null &&
            !predecessorIds.includes(candidate.id) &&
            `${candidate.name} ${candidate.code}`
                .toLowerCase()
                .includes(keyword.trim().toLowerCase()),
    );

    return (
        <Popover>
            <PopoverTrigger asChild>
                <Button
                    type="button"
                    size="icon"
                    variant="ghost"
                    className="ml-auto h-6 w-6 shrink-0 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 data-[state=open]:opacity-100"
                    title="Atur ketergantungan"
                >
                    <Plus size={14} />
                </Button>
            </PopoverTrigger>

            <PopoverContent align="start" className="w-80 p-0">
                {/* Judulnya menyebut task yang SEDANG diatur, lengkap dengan
                    namanya. Sebelumnya hanya kodenya yang muncul, jadi tidak ada
                    cara memastikan popover ini milik baris yang benar. */}
                <div className="border-b px-3 py-2">
                    <p className="text-xs font-semibold">Ketergantungan task</p>
                    <p
                        className="truncate text-[11px] text-muted-foreground"
                        title={`${task.code} ${task.name}`}
                    >
                        {task.code} · {task.name}
                    </p>
                </div>

                {/* Dua bagian di bawah sengaja dipisah dan diberi label sendiri.
                    Versi sebelumnya memakai satu judul "Task berikut menahan
                    mulainya X" untuk keduanya, sehingga saat belum ada prasyarat
                    sama sekali judul itu langsung disusul daftar kandidat —
                    seolah semua task di project sedang menahan task ini. */}
                <div className="border-b">
                    <p className="px-3 pt-2 text-[11px] font-medium text-muted-foreground">
                        Harus selesai lebih dulu
                    </p>

                    {predecessorIds.length === 0 ? (
                        <p className="px-3 pt-1 pb-2 text-xs text-muted-foreground">
                            Belum ada. Task ini bisa mulai kapan saja.
                        </p>
                    ) : (
                        <ul className="max-h-32 overflow-y-auto py-1">
                            {predecessorIds.map((predecessorId) => {
                                const predecessor = byId.get(predecessorId);

                                return (
                                    <li
                                        key={predecessorId}
                                        className="flex items-center gap-2 px-3 py-1"
                                    >
                                        <span className="truncate text-xs">
                                            <span className="text-muted-foreground">
                                                {predecessor?.code ??
                                                    `#${predecessorId}`}
                                            </span>{' '}
                                            {predecessor?.name ?? ''}
                                        </span>
                                        <Button
                                            type="button"
                                            size="icon"
                                            variant="ghost"
                                            className="ml-auto h-6 w-6 shrink-0"
                                            title="Lepas ketergantungan ini"
                                            onClick={() =>
                                                onRemove(task.id, predecessorId)
                                            }
                                        >
                                            <X size={12} />
                                        </Button>
                                    </li>
                                );
                            })}
                        </ul>
                    )}
                </div>

                <div className="p-2">
                    <p className="px-1 pb-1.5 text-[11px] font-medium text-muted-foreground">
                        Tambah prasyarat
                    </p>

                    <div className="relative">
                        <Search
                            className="absolute top-1/2 left-2 -translate-y-1/2 text-muted-foreground"
                            size={14}
                        />
                        <Input
                            value={keyword}
                            onChange={(event) => setKeyword(event.target.value)}
                            placeholder="Cari task..."
                            className="h-8 pl-7 text-xs"
                        />
                    </div>

                    <ul className="mt-2 max-h-48 overflow-y-auto">
                        {candidates.length === 0 && (
                            <li className="px-2 py-3 text-center text-xs text-muted-foreground">
                                {keyword.trim()
                                    ? 'Tidak ada task yang cocok.'
                                    : 'Tidak ada task lain yang bisa dijadikan prasyarat.'}
                            </li>
                        )}

                        {candidates.map((candidate) => (
                            <li key={candidate.id}>
                                <button
                                    type="button"
                                    className="w-full truncate rounded px-2 py-1.5 text-left text-xs hover:bg-muted"
                                    onClick={() => onAdd(task.id, candidate.id)}
                                >
                                    <span className="text-muted-foreground">
                                        {candidate.code}
                                    </span>{' '}
                                    {candidate.name}
                                </button>
                            </li>
                        ))}
                    </ul>
                </div>
            </PopoverContent>
        </Popover>
    );
}
