import { usePage } from '@inertiajs/react';
import { useEcho } from '@laravel/echo-react';
import {
    Ban,
    Check,
    CheckCheck,
    ClockAlert,
    FileText,
    MessageSquare,
    Paperclip,
    Pencil,
    Send,
    Trash2,
    X,
} from 'lucide-react';
import {
    Fragment,
    useEffect,
    useLayoutEffect,
    useMemo,
    useRef,
    useState,
} from 'react';

import { ConfirmDeleteDialog } from '@/components/ConfirmDeleteDialog';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { Textarea } from '@/components/ui/textarea';
import {
    Tooltip,
    TooltipContent,
    TooltipProvider,
    TooltipTrigger,
} from '@/components/ui/tooltip';
import { useMentionAutocomplete } from '@/hooks/use-mention-autocomplete';
import type {
    CommentAttachment,
    CommentDeliveryStatus,
} from '@/hooks/use-task-comments';
import { useTaskComments } from '@/hooks/use-task-comments';
import { useTypingIndicator } from '@/hooks/use-typing-indicator';
import type { MentionCandidate } from '@/lib/mention-utils';
import { highlightCandidates, splitMentions } from '@/lib/mention-utils';
import {
    avatarUrl,
    cn,
    formatDateTime,
    formatRelativeTime,
    getAvatarColor,
    getInitials,
    isTypingTarget,
} from '@/lib/utils';
import type { Auth } from '@/types/auth';
import type { Task } from '@/types/task';

// Jarak (px) dari bawah kontainer yang masih dianggap "user sedang di bawah".
const NEAR_BOTTOM_THRESHOLD_PX = 120;

// Jarak (px) dari puncak yang memicu pemuatan pesan yang lebih lama.
const LOAD_OLDER_THRESHOLD_PX = 80;

/**
 * Umur maksimal sebuah pesan untuk masih boleh disunting — cerminan
 * `TaskCommentController::EDIT_WINDOW_MINUTES`. Server tetap yang menegakkannya;
 * angka di sini hanya menentukan kapan tombolnya berhenti ditawarkan.
 */
const EDIT_WINDOW_MS = 20 * 60_000;

/**
 * Selang pemeriksaan ulang jendela di atas. Tanpa denyut ini tombol sunting
 * baru padam saat ada sebab lain yang memicu render — bisa berjam-jam kemudian.
 */
const EDIT_WINDOW_TICK_MS = 60_000;

/**
 * Tombol yang menggeser kursor tanpa mengubah isi komposer. Hanya untuk tombol
 * inilah keadaan sebutan perlu dibaca ulang saat `keyup`; perubahan teks sudah
 * ditangani `onChange`.
 */
const CURSOR_MOVE_KEYS = new Set([
    'ArrowLeft',
    'ArrowRight',
    'ArrowUp',
    'ArrowDown',
    'Home',
    'End',
]);

interface TaskDiscussionSectionProps {
    task: Task;
    open: boolean;
    canComment?: boolean;
}

/**
 * Menampilkan satu lampiran di dalam balon diskusi: thumbnail untuk gambar,
 * kartu klik-untuk-buka untuk dokumen. Keduanya membuka file di tab baru.
 */
function AttachmentPreview({ attachment }: { attachment: CommentAttachment }) {
    const url = attachment.file_url ?? undefined;

    // Belum punya tautan: lampiran yang masih ikut terbang bersama pesannya.
    // Ditampilkan sebagai kartu mati, bukan tautan yang tidak menuju ke mana pun.
    if (!url) {
        return (
            <div className="flex items-center gap-2 rounded-lg border border-border/50 bg-background/60 px-3 py-2 text-xs opacity-60">
                <FileText className="h-4 w-4 shrink-0 text-muted-foreground" />
                <span className="truncate font-medium text-foreground/80">
                    {attachment.file_name}
                </span>
            </div>
        );
    }

    if (attachment.is_image) {
        return (
            <a
                href={url}
                target="_blank"
                rel="noopener noreferrer"
                className="block overflow-hidden rounded-lg border border-border/50 transition-opacity hover:opacity-90"
            >
                <img
                    src={url}
                    alt={attachment.file_name}
                    loading="lazy"
                    className="max-h-60 w-auto max-w-full object-cover"
                />
            </a>
        );
    }

    return (
        <a
            href={url}
            target="_blank"
            rel="noopener noreferrer"
            className="flex items-center gap-2 rounded-lg border border-border/50 bg-background/60 px-3 py-2 text-xs transition-colors hover:bg-background"
        >
            <FileText className="h-4 w-4 shrink-0 text-muted-foreground" />
            <span className="truncate font-medium text-foreground/80">
                {attachment.file_name}
            </span>
        </a>
    );
}

/**
 * Penanda pengiriman pada pesan sendiri, mengikuti kebiasaan aplikasi chat:
 * satu centang selama pesannya masih di jalan, dua centang begitu server
 * menyimpannya, dan jam bertanda seru bila gagal — yang gagal bisa diklik untuk
 * dikirim ulang.
 */
function MessageDeliveryStatus({
    status,
    onRetry,
}: {
    status?: CommentDeliveryStatus;
    onRetry: () => void;
}) {
    if (status === 'sending') {
        return (
            <Check
                className="h-3.5 w-3.5 shrink-0 text-muted-foreground"
                role="img"
                aria-label="Sedang dikirim"
            />
        );
    }

    if (status === 'failed') {
        return (
            <button
                type="button"
                onClick={onRetry}
                className="shrink-0 cursor-pointer text-destructive transition-opacity hover:opacity-70"
                aria-label="Gagal terkirim, kirim ulang"
                title="Gagal terkirim — klik untuk kirim ulang"
            >
                <ClockAlert className="h-3.5 w-3.5" />
            </button>
        );
    }

    return (
        <CheckCheck
            className="h-3.5 w-3.5 shrink-0 text-primary"
            role="img"
            aria-label="Terkirim"
        />
    );
}

/** Label tanggal relatif untuk pemisah grup komentar (Hari Ini / Kemarin / tanggal). */
const getDateLabel = (dateStr: string) => {
    const date = new Date(dateStr);
    const today = new Date();
    const yesterday = new Date(today);
    yesterday.setDate(yesterday.getDate() - 1);

    if (date.toDateString() === today.toDateString()) {
        return 'Today';
    }

    if (date.toDateString() === yesterday.toDateString()) {
        return 'Yesterday';
    }

    return date.toLocaleDateString('id-ID', {
        day: 'numeric',
        month: 'long',
        year: 'numeric',
    });
};

export function TaskDiscussionSection({
    task,
    open,
    canComment = false,
}: TaskDiscussionSectionProps) {
    const { auth } = usePage<Auth>().props;
    const scrollContainerRef = useRef<HTMLDivElement>(null);
    const textareaRef = useRef<HTMLTextAreaElement>(null);
    const fileInputRef = useRef<HTMLInputElement>(null);
    // Ditandai saat user mengirim pesannya sendiri: daftar wajib turun ke bawah.
    const scrollToBottomRef = useRef(false);
    // Jarak dari dasar sesaat sebelum potongan lama disisipkan, untuk menahan
    // posisi baca setelah tingginya bertambah di atas.
    const restoreScrollFromBottomRef = useRef<number | null>(null);
    // Percakapan ini sudah pernah ditempatkan ke pesan terbaru sejak dibuka.
    const hasAnchoredRef = useRef(false);
    // Permintaan potongan lama sedang berjalan (dibaca serentak oleh handler
    // gulir, sebelum state sempat ikut berubah).
    const pendingOlderRef = useRef(false);

    const mention = useMentionAutocomplete(task?.code, open);

    // Id komentar yang sedang dikonfirmasi hapus (null = dialog tertutup).
    const [pendingDeleteId, setPendingDeleteId] = useState<number | null>(null);
    const [isDeleting, setIsDeleting] = useState(false);
    // Id komentar yang sedang disunting (null = tidak ada). Teksnya sendiri
    // tinggal di komposer, bukan di state terpisah — kotak yang dipakai memang
    // kotak yang sama dengan saat menulis pesan baru.
    const [editingId, setEditingId] = useState<number | null>(null);
    const [isSavingEdit, setIsSavingEdit] = useState(false);
    // Denyut penanda waktu, dipakai hanya untuk memadamkan tombol sunting
    // begitu jendela suntingnya lewat.
    const [now, setNow] = useState(() => Date.now());

    const {
        comments,
        isLoading: isLoadingComments,
        hasMore,
        isLoadingOlder,
        loadOlderComments,
        commentForm,
        pendingFiles,
        addPendingFiles,
        removePendingFile,
        applyRealtimeEvent,
        sendComment,
        retryComment,
        editComment,
        deleteComment,
    } = useTaskComments({ task, open });

    // useEcho sengaja dibiarkan di komponen agar subscribe/unsubscribe channel
    // tetap terikat pada `open`/`task.code`; logikanya didelegasikan ke hook.
    const { channel } = useEcho(
        `task.${task.code}`,
        ['.TaskCommentCreated', '.TaskCommentUpdated', '.TaskCommentDeleted'],
        applyRealtimeEvent,
        [open, task.code],
    );

    const { typingUser, emitTyping } = useTypingIndicator({
        channel,
        currentUserName: auth?.user?.name,
    });

    // Precompute label tanggal + penanda header sekali per perubahan `comments`,
    // menghindari pemanggilan getDateLabel 2x per komentar di setiap render.
    const decoratedComments = useMemo(() => {
        return comments.map((comment, index) => {
            const dateLabel = getDateLabel(comment.created_at);
            const prevDateLabel =
                index > 0 ? getDateLabel(comments[index - 1].created_at) : null;

            return {
                comment,
                dateLabel,
                showDateHeader: dateLabel !== prevDateLabel,
            };
        });
    }, [comments]);

    // Percakapan yang baru dibuka belum pernah ditempatkan ke pesan terbaru.
    useEffect(() => {
        hasAnchoredRef.current = false;
    }, [open, task.code]);

    /**
     * Menempatkan posisi baca setiap kali daftar pesan berubah.
     *
     * `useLayoutEffect`, bukan `useEffect`: seluruh penempatan di sini memakai
     * `scrollHeight` yang baru, jadi ia harus berjalan setelah tata letak
     * diperbarui tetapi sebelum layar digambar — kalau tidak, isinya sempat
     * terlihat di posisi lama lebih dulu.
     */
    useLayoutEffect(() => {
        const container = scrollContainerRef.current;

        if (!container) {
            return;
        }

        // Potongan lama baru saja disisipkan di atas: kembalikan posisi baca,
        // kalau tidak daftarnya melompat karena isinya bertambah di atas titik
        // pandang.
        const anchor = restoreScrollFromBottomRef.current;

        if (anchor !== null) {
            container.scrollTop = container.scrollHeight - anchor;
            restoreScrollFromBottomRef.current = null;

            return;
        }

        // Pemuatan pertama percakapan ini: mendarat di pesan terbaru, bukan di
        // pesan paling lama. Tanpa ini panel terbuka di ujung yang salah —
        // pemeriksaan "dekat bawah" di bawah tidak pernah benar saat isinya
        // sekaligus melampaui tinggi kotak.
        if (!hasAnchoredRef.current && comments.length > 0) {
            hasAnchoredRef.current = true;
            container.scrollTop = container.scrollHeight;

            return;
        }

        // Setelah mengirim pesan sendiri, selalu turun ke bawah — di situlah
        // pesannya mendarat, berapa pun jauhnya user sedang membaca ke atas.
        // Penempatannya langsung, bukan animasi: animasi menuju `scrollHeight`
        // yang terbaca saat itu berhenti beberapa piksel di atas dasar ketika
        // tinggi isinya masih bertambah setelahnya.
        if (scrollToBottomRef.current) {
            scrollToBottomRef.current = false;
            container.scrollTop = container.scrollHeight;

            return;
        }

        // Selebihnya hanya auto-scroll bila user memang sudah berada di dekat
        // bawah, supaya tidak mengganggu saat ia sedang membaca riwayat di atas.
        const distanceFromBottom =
            container.scrollHeight -
            container.scrollTop -
            container.clientHeight;

        if (distanceFromBottom <= NEAR_BOTTOM_THRESHOLD_PX) {
            container.scrollTo({
                top: container.scrollHeight,
                behavior: 'smooth',
            });
        }
    }, [comments]);

    /**
     * Mengetik di mana pun selagi detail task terbuka langsung mendarat di kotak
     * pesan, seperti aplikasi chat — tak perlu mengklik kotaknya lebih dulu.
     *
     * Huruf pemicunya ikut terketik dengan sendirinya: fokus sudah berpindah
     * sebelum browser menuliskan karakternya, jadi tidak ada yang perlu
     * disisipkan manual.
     */
    useEffect(() => {
        if (!open || !canComment) {
            return;
        }

        const focusComposer = (event: KeyboardEvent) => {
            // Hanya karakter tunggal tanpa modifier. Escape, panah, dan
            // pintasan seperti Ctrl+F tetap milik browser maupun sheet-nya —
            // begitu pula ketikan yang sudah punya tujuan sendiri (kolom lain,
            // kotak sunting pesan).
            if (
                event.key.length !== 1 ||
                event.metaKey ||
                event.ctrlKey ||
                event.altKey ||
                isTypingTarget(event.target)
            ) {
                return;
            }

            // Ketikan hanya diambil alih selagi fokus masih di dalam panel ini,
            // atau belum bertuan sama sekali. Select, dropdown, dan dialog
            // konfirmasi dipasang Radix di luar sheet lewat portal dan punya
            // typeahead sendiri — huruf yang diketik di sana sudah punya tujuan.
            const panel = textareaRef.current?.closest('[role="dialog"]');

            if (
                event.target instanceof Node &&
                event.target !== document.body &&
                !panel?.contains(event.target)
            ) {
                return;
            }

            textareaRef.current?.focus();
        };

        document.addEventListener('keydown', focusComposer);

        return () => document.removeEventListener('keydown', focusComposer);
    }, [canComment, open]);

    // Jendela suntingnya pendek, jadi tombolnya harus padam sendiri selagi
    // panel dibiarkan terbuka.
    useEffect(() => {
        if (!open || !canComment) {
            return;
        }

        const tick = setInterval(() => setNow(Date.now()), EDIT_WINDOW_TICK_MS);

        return () => clearInterval(tick);
    }, [canComment, open]);

    /**
     * Menggulir mendekati puncak berarti meminta potongan yang lebih lama.
     * Jarak dari dasar dicatat lebih dulu supaya posisi baca bisa dikembalikan
     * setelah baris baru tersisip di atas.
     */
    const handleScroll = () => {
        const container = scrollContainerRef.current;

        // Penanda dibaca dari ref, bukan dari state: dua event gulir yang tiba
        // sebelum React sempat render ulang sama-sama melihat `isLoadingOlder`
        // masih `false`, lalu meminta potongan yang sama dua kali — dan yang
        // kedua menimpa jangkar posisi baca milik yang pertama.
        if (
            !container ||
            !hasMore ||
            isLoadingOlder ||
            pendingOlderRef.current ||
            container.scrollTop > LOAD_OLDER_THRESHOLD_PX
        ) {
            return;
        }

        pendingOlderRef.current = true;
        restoreScrollFromBottomRef.current =
            container.scrollHeight - container.scrollTop;

        void loadOlderComments().finally(() => {
            pendingOlderRef.current = false;
        });
    };

    // Auto-resize textarea mengikuti isi (dibatasi maksimal via max-height CSS).
    const autoResizeTextarea = () => {
        const el = textareaRef.current;

        if (!el) {
            return;
        }

        el.style.height = 'auto';
        el.style.height = `${el.scrollHeight}px`;
    };

    // Kembalikan tinggi textarea setelah isinya dikosongkan.
    const resetTextareaHeight = () => {
        requestAnimationFrame(() => {
            if (textareaRef.current) {
                textareaRef.current.style.height = 'auto';
            }
        });
    };

    // Pesan yang sedang disunting, bila ada. Diambil dari daftar supaya
    // lampiran dan teks aslinya tetap terbaca saat menyimpan.
    const editingComment =
        editingId === null
            ? null
            : (comments.find((c) => c.id === editingId) ?? null);

    const draftText = commentForm.data.comment.trim();

    // Menyunting: pesan tetap harus punya isi — teks, atau lampiran yang sudah
    // menempel padanya. Menulis baru: teks, atau lampiran yang ikut dikirim.
    //
    // Pesan yang masih terbang sengaja tidak ikut menahan yang berikutnya:
    // komposer sudah kosong sejak balonnya muncul, jadi menolak Enter di sini
    // hanya menelan pesan kedua tanpa memberi tahu apa pun. Request-nya sendiri
    // sudah diantre berurutan di useTaskComments.
    const canSend = editingComment
        ? (Boolean(draftText) ||
              (editingComment.attachments?.length ?? 0) > 0) &&
          !isSavingEdit
        : Boolean(draftText) || pendingFiles.length > 0;

    /**
     * Simpan suntingan yang sedang terbuka di komposer. Teks yang tidak berubah
     * cukup menutup mode sunting — kalau tidak, menekan Enter tanpa mengubah
     * apa pun menandai pesannya "diedit" tanpa alasan.
     */
    const saveEdit = async (comment: (typeof comments)[number]) => {
        if (draftText === (comment.comment ?? '')) {
            cancelEditing();

            return;
        }

        setIsSavingEdit(true);

        const saved = await editComment(comment.id, draftText);

        setIsSavingEdit(false);

        if (saved) {
            cancelEditing();
        }
    };

    const handleSend = () => {
        if (!canSend) {
            return;
        }

        if (editingComment) {
            void saveEdit(editingComment);

            return;
        }

        scrollToBottomRef.current = true;
        sendComment();
        resetTextareaHeight();
    };

    // Sisipkan nama pilihan menggantikan sebutan yang sedang diketik, lalu
    // kembalikan kursor ke belakang nama itu.
    const chooseMention = (candidate: MentionCandidate) => {
        const el = textareaRef.current;

        if (!el) {
            return;
        }

        const { text, cursor } = mention.applySuggestion(
            commentForm.data.comment,
            el.selectionStart,
            candidate,
        );

        commentForm.setData('comment', text);

        requestAnimationFrame(() => {
            el.focus();
            el.setSelectionRange(cursor, cursor);
            autoResizeTextarea();
        });
    };

    const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
        // Saat daftar sebutan terbuka, tombol navigasi miliknya lebih dulu —
        // termasuk Enter, yang di sini memilih nama alih-alih mengirim pesan.
        if (mention.isOpen) {
            if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
                e.preventDefault();
                mention.moveActive(e.key === 'ArrowDown' ? 1 : -1);

                return;
            }

            if (e.key === 'Enter' || e.key === 'Tab') {
                e.preventDefault();
                chooseMention(mention.suggestions[mention.activeIndex]);

                return;
            }

            if (e.key === 'Escape') {
                e.preventDefault();
                mention.close();

                return;
            }
        }

        // Escape membatalkan penyuntingan, mengembalikan kotak ke keadaan siap
        // menulis pesan baru.
        if (e.key === 'Escape' && editingId !== null) {
            e.preventDefault();
            cancelEditing();

            return;
        }

        // Enter = kirim, Shift+Enter = baris baru (pola umum aplikasi chat).
        if (e.key === 'Enter' && !e.shiftKey) {
            e.preventDefault();
            handleSend();
        }
    };

    const handleFilesSelected = (e: React.ChangeEvent<HTMLInputElement>) => {
        const files = Array.from(e.target.files ?? []);

        if (files.length > 0) {
            void addPendingFiles(files);
        }

        // Reset agar memilih file yang sama berturut-turut tetap memicu onChange.
        e.target.value = '';
    };

    /**
     * Pindahkan isi pesan ke komposer. Kotaknya satu-satunya di panel ini, jadi
     * menyunting terasa sama dengan menulis — termasuk daftar sebutan `@Nama`
     * yang ikut hidup tanpa perlu disediakan dua kali.
     */
    const startEditing = (comment: (typeof comments)[number]) => {
        setEditingId(comment.id);
        commentForm.setData('comment', comment.comment ?? '');

        requestAnimationFrame(() => {
            const el = textareaRef.current;

            if (!el) {
                return;
            }

            el.focus();
            // Kursor di ujung teks, bukan di awalnya: yang menyunting hampir
            // selalu hendak menambah atau membetulkan bagian belakang.
            el.setSelectionRange(el.value.length, el.value.length);
            autoResizeTextarea();
        });
    };

    const cancelEditing = () => {
        setEditingId(null);
        commentForm.reset();
        resetTextareaHeight();
    };

    const confirmDelete = async () => {
        if (pendingDeleteId == null) {
            return;
        }

        setIsDeleting(true);

        try {
            await deleteComment(pendingDeleteId);
        } finally {
            setIsDeleting(false);
            setPendingDeleteId(null);
        }
    };

    return (
        <section className="space-y-4">
            <div className="flex items-center justify-between">
                <div className="flex items-center gap-2">
                    <MessageSquare className="h-4 w-4 text-muted-foreground" />
                    <h4 className="text-sm font-semibold tracking-tight">
                        Diskusi
                    </h4>
                </div>
                {/* Yang terhitung hanya pesan yang sudah termuat — panel
                    membuka sepotong percakapan terakhir saja. Selama masih ada
                    yang lebih lama di server, angkanya diberi "+" ketimbang
                    mengaku sebagai jumlah seluruh percakapan. */}
                <span className="rounded-full bg-muted px-2 py-0.5 text-[10px] font-bold text-muted-foreground">
                    {comments.length}
                    {hasMore && '+'}
                </span>
            </div>

            <div
                ref={scrollContainerRef}
                onScroll={handleScroll}
                aria-live="polite"
                aria-busy={isLoadingComments}
                // Tinggi dikunci, bukan sekadar dibatasi: panel yang ikut tumbuh
                // mengikuti jumlah pesan membuat komposer berpindah tempat tiap
                // kali pesan masuk atau potongan lama dimuat.
                className="relative flex h-[60vh] flex-col overflow-y-auto px-1 pr-2"
            >
                {/* Percakapan menempel ke dasar seperti aplikasi chat. `mt-auto`
                    mendorong isinya turun selama masih pendek, lalu menciut
                    sendiri begitu isinya melampaui tinggi kotak — berbeda dari
                    `justify-end`, yang pada kontainer bergulir dapat membuat
                    pesan paling atas tidak terjangkau. */}
                <div className="mt-auto space-y-4">
                    {/* Yang tampil hanya percakapan terakhir; menggulir ke puncak
                    memuat potongan sebelumnya. */}
                    {isLoadingOlder && (
                        <p className="py-2 text-center text-[10px] text-muted-foreground italic">
                            Memuat pesan sebelumnya...
                        </p>
                    )}
                    {!isLoadingOlder && !hasMore && comments.length > 0 && (
                        <p className="py-2 text-center text-[10px] text-muted-foreground/70 italic">
                            Awal percakapan
                        </p>
                    )}

                    {isLoadingComments && comments.length === 0 ? (
                        <div className="space-y-4">
                            {[0, 1, 2].map((i) => (
                                <div
                                    key={i}
                                    className={cn(
                                        'flex gap-3',
                                        i % 2 === 0
                                            ? 'flex-row'
                                            : 'flex-row-reverse',
                                    )}
                                >
                                    <Skeleton className="mt-auto mb-1 h-8 w-8 shrink-0 rounded-full" />
                                    <div className="flex max-w-[85%] flex-1 flex-col gap-2 rounded-2xl bg-muted/40 p-3">
                                        <Skeleton className="h-3 w-24" />
                                        <Skeleton className="h-3 w-40" />
                                        <Skeleton className="h-3 w-32" />
                                    </div>
                                </div>
                            ))}
                        </div>
                    ) : comments.length === 0 ? (
                        <p className="rounded-lg border border-dashed py-6 text-center text-xs text-muted-foreground italic">
                            No discussion yet. Start one below.
                        </p>
                    ) : (
                        <TooltipProvider delayDuration={300}>
                            {decoratedComments.map(
                                ({ comment, dateLabel, showDateHeader }) => {
                                    const isMe = Boolean(
                                        auth?.user?.id &&
                                        String(comment.user?.id) ===
                                            String(auth.user.id),
                                    );
                                    const authorName =
                                        comment.user?.name || 'Unknown User';
                                    const avatarInitials =
                                        getInitials(authorName);
                                    const avatarColor = getAvatarColor(
                                        comment.user?.id ?? authorName,
                                    );
                                    // Sunting hanya ditawarkan selama pesannya
                                    // masih muda dan sudah punya id di server.
                                    const canEditNow =
                                        !comment.status &&
                                        now -
                                            new Date(
                                                comment.created_at,
                                            ).getTime() <
                                            EDIT_WINDOW_MS;
                                    // Selama satu pesan sedang disunting,
                                    // tombol pesan lain ikut diam — kalau tidak,
                                    // satu klik membuang suntingan yang sedang
                                    // berjalan tanpa peringatan.
                                    // Diturunkan dari `editingComment`, bukan
                                    // `editingId` mentah: kalau baris yang
                                    // sedang disunting hilang dari daftar
                                    // (refetch yang menggeser jendela),
                                    // `editingId` tetap terisi dan tombol di
                                    // seluruh pesan ikut mati tanpa jalan
                                    // keluar selain menutup panel.
                                    const areActionsHidden =
                                        Boolean(comment.status) ||
                                        editingComment !== null;

                                    return (
                                        <Fragment
                                            key={
                                                comment.clientKey ?? comment.id
                                            }
                                        >
                                            {showDateHeader && (
                                                <div className="my-3 flex justify-center">
                                                    <span className="rounded-lg bg-muted px-3 py-1 text-[10px] font-medium text-muted-foreground shadow-sm">
                                                        {dateLabel}
                                                    </span>
                                                </div>
                                            )}
                                            <div
                                                className={cn(
                                                    'relative flex w-full animate-in gap-3 duration-300 fade-in slide-in-from-bottom-2',
                                                    isMe
                                                        ? 'flex-row-reverse'
                                                        : 'flex-row',
                                                )}
                                            >
                                                <Avatar className="mt-auto mb-1 h-8 w-8 shrink-0 ring-4 ring-background">
                                                    <AvatarImage
                                                        src={avatarUrl(
                                                            comment.user
                                                                ?.avatar,
                                                        )}
                                                        alt=""
                                                    />
                                                    <AvatarFallback
                                                        className={cn(
                                                            'text-[10px] font-semibold',
                                                            avatarColor,
                                                        )}
                                                    >
                                                        {avatarInitials}
                                                    </AvatarFallback>
                                                </Avatar>
                                                <div
                                                    className={cn(
                                                        'group relative flex max-w-[85%] flex-col space-y-1 rounded-2xl p-3 transition-colors',
                                                        isMe
                                                            ? 'bg-primary/10 hover:bg-primary/15'
                                                            : 'bg-muted/40 hover:bg-muted/60',
                                                        // Pesan yang isinya sedang berada di komposer,
                                                        // supaya jelas yang mana yang sedang disunting.
                                                        editingId ===
                                                            comment.id &&
                                                            'ring-2 ring-primary/40',
                                                    )}
                                                >
                                                    <div className="flex w-full items-baseline justify-between gap-4">
                                                        <span className="text-xs font-semibold text-foreground">
                                                            {isMe
                                                                ? 'You'
                                                                : authorName}
                                                        </span>
                                                        <div className="flex items-center gap-2">
                                                            {comment.is_edited && (
                                                                <span className="text-[10px] text-muted-foreground italic">
                                                                    diedit
                                                                </span>
                                                            )}
                                                            <Tooltip>
                                                                <TooltipTrigger
                                                                    asChild
                                                                >
                                                                    <span className="cursor-default text-[10px] text-muted-foreground">
                                                                        {formatRelativeTime(
                                                                            comment.created_at,
                                                                        )}
                                                                    </span>
                                                                </TooltipTrigger>
                                                                <TooltipContent>
                                                                    {formatDateTime(
                                                                        comment.created_at,
                                                                    )}
                                                                    {comment.is_edited && (
                                                                        <>
                                                                            {
                                                                                ' · '
                                                                            }
                                                                            Diedit{' '}
                                                                            {formatDateTime(
                                                                                comment.updated_at ??
                                                                                    comment.created_at,
                                                                            )}
                                                                        </>
                                                                    )}
                                                                </TooltipContent>
                                                            </Tooltip>
                                                            {isMe &&
                                                                !comment.is_deleted && (
                                                                    <MessageDeliveryStatus
                                                                        status={
                                                                            comment.status
                                                                        }
                                                                        onRetry={() =>
                                                                            void retryComment(
                                                                                comment.id,
                                                                            )
                                                                        }
                                                                    />
                                                                )}
                                                            {/* Sunting & hapus menuntut dua hal, sama seperti
                                                            di server: pesan ini miliknya, dan ia memang
                                                            masih boleh menulis di task ini (viewer dan
                                                            project beku tidak).

                                                            Yang belum boleh ditekan disembunyikan dengan
                                                            `invisible`, bukan dilepas dari tata letak —
                                                            baik pesan yang belum sampai maupun yang
                                                            jendela suntingnya sudah lewat. Kalau tidak,
                                                            balonnya melebar dan menyempit sendiri. */}
                                                            {isMe &&
                                                                canComment &&
                                                                !comment.is_deleted && (
                                                                    <div
                                                                        className={cn(
                                                                            'flex items-center gap-2',
                                                                            areActionsHidden &&
                                                                                'invisible',
                                                                        )}
                                                                    >
                                                                        <button
                                                                            type="button"
                                                                            onClick={() =>
                                                                                startEditing(
                                                                                    comment,
                                                                                )
                                                                            }
                                                                            className={cn(
                                                                                'cursor-pointer opacity-0 transition-opacity group-hover:opacity-100 hover:text-primary focus-visible:opacity-100 focus-visible:outline-none',
                                                                                !canEditNow &&
                                                                                    'invisible',
                                                                            )}
                                                                            aria-label="Edit discussion message"
                                                                            title="Edit discussion message"
                                                                        >
                                                                            <Pencil className="h-3 w-3" />
                                                                        </button>
                                                                        <button
                                                                            type="button"
                                                                            onClick={() =>
                                                                                setPendingDeleteId(
                                                                                    comment.id,
                                                                                )
                                                                            }
                                                                            className="cursor-pointer opacity-0 transition-opacity group-hover:opacity-100 hover:text-destructive focus-visible:opacity-100 focus-visible:outline-none"
                                                                            aria-label="Delete discussion message"
                                                                            title="Delete discussion message"
                                                                        >
                                                                            <Trash2 className="h-3 w-3" />
                                                                        </button>
                                                                    </div>
                                                                )}
                                                        </div>
                                                    </div>
                                                    {comment.is_deleted && (
                                                        <p className="flex items-center gap-1.5 text-[15px] text-muted-foreground italic">
                                                            <Ban className="h-3.5 w-3.5 shrink-0" />
                                                            {isMe
                                                                ? 'Anda menghapus pesan ini'
                                                                : 'Pesan ini telah dihapus'}
                                                        </p>
                                                    )}
                                                    {comment.comment && (
                                                        <p className="text-[15px] leading-relaxed wrap-break-word whitespace-pre-wrap text-foreground/80">
                                                            {splitMentions(
                                                                comment.comment,
                                                                highlightCandidates(
                                                                    comment.mentioned_names,
                                                                    mention.candidates,
                                                                ),
                                                            ).map(
                                                                (
                                                                    segment,
                                                                    segmentIndex,
                                                                ) =>
                                                                    segment.isMention ? (
                                                                        <span
                                                                            key={
                                                                                segmentIndex
                                                                            }
                                                                            className="rounded bg-primary/10 px-1 font-medium text-primary"
                                                                        >
                                                                            {
                                                                                segment.text
                                                                            }
                                                                        </span>
                                                                    ) : (
                                                                        <Fragment
                                                                            key={
                                                                                segmentIndex
                                                                            }
                                                                        >
                                                                            {
                                                                                segment.text
                                                                            }
                                                                        </Fragment>
                                                                    ),
                                                            )}
                                                        </p>
                                                    )}
                                                    {comment.attachments &&
                                                        comment.attachments
                                                            .length > 0 && (
                                                            <div className="flex flex-col gap-2 pt-1">
                                                                {comment.attachments.map(
                                                                    (
                                                                        attachment,
                                                                    ) => (
                                                                        <AttachmentPreview
                                                                            key={
                                                                                attachment.id
                                                                            }
                                                                            attachment={
                                                                                attachment
                                                                            }
                                                                        />
                                                                    ),
                                                                )}
                                                            </div>
                                                        )}
                                                </div>
                                            </div>
                                        </Fragment>
                                    );
                                },
                            )}
                        </TooltipProvider>
                    )}
                </div>
            </div>

            {/* Composer menempel di dasar area scroll agar tetap terlihat penuh
                berapa pun tinggi isinya (mis. saat chip lampiran menambah baris). */}
            <div className="sticky bottom-0 z-10 -mx-1 space-y-2 bg-card px-1 pt-2 pb-4">
                {typingUser && (
                    <div className="flex animate-in items-center gap-2 px-1 text-xs text-muted-foreground italic fade-in">
                        <span className="flex h-1.5 items-center gap-1">
                            <span
                                className="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground/50"
                                style={{ animationDelay: '0ms' }}
                            />
                            <span
                                className="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground/50"
                                style={{ animationDelay: '150ms' }}
                            />
                            <span
                                className="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground/50"
                                style={{ animationDelay: '300ms' }}
                            />
                        </span>
                        {typingUser} sedang mengetik...
                    </div>
                )}

                {canComment && editingComment && (
                    <div className="flex items-center gap-2 rounded-lg border border-primary/30 bg-primary/5 px-3 py-2">
                        <Pencil className="h-3.5 w-3.5 shrink-0 text-primary" />
                        <div className="min-w-0 flex-1">
                            <p className="text-xs font-medium text-primary">
                                Mengedit pesan
                            </p>
                            <p className="truncate text-[11px] text-muted-foreground">
                                {editingComment.comment}
                            </p>
                        </div>
                        <button
                            type="button"
                            onClick={cancelEditing}
                            className="shrink-0 cursor-pointer rounded-full p-1 text-muted-foreground transition-colors hover:bg-background hover:text-destructive"
                            aria-label="Batal mengedit"
                            title="Batal mengedit (Esc)"
                        >
                            <X className="h-3.5 w-3.5" />
                        </button>
                    </div>
                )}

                {canComment && (
                    <div className="group relative">
                        <div className="flex gap-3">
                            <div className="relative flex-1 rounded-xl border border-input bg-background transition-all focus-within:border-primary/30 focus-within:ring-4 focus-within:ring-primary/5">
                                {mention.isOpen && (
                                    <ul
                                        role="listbox"
                                        aria-label="Sebut seseorang"
                                        className="absolute bottom-full left-0 z-50 mb-2 max-h-56 w-64 overflow-y-auto rounded-xl border bg-popover p-1 shadow-lg"
                                    >
                                        {mention.suggestions.map(
                                            (candidate, index) => (
                                                <li key={candidate.id}>
                                                    <button
                                                        type="button"
                                                        role="option"
                                                        aria-selected={
                                                            index ===
                                                            mention.activeIndex
                                                        }
                                                        // Mencegah textarea kehilangan fokus sebelum
                                                        // klik sempat terbaca.
                                                        onMouseDown={(e) =>
                                                            e.preventDefault()
                                                        }
                                                        onClick={() =>
                                                            chooseMention(
                                                                candidate,
                                                            )
                                                        }
                                                        className={cn(
                                                            'flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition-colors',
                                                            index ===
                                                                mention.activeIndex
                                                                ? 'bg-accent text-accent-foreground'
                                                                : 'hover:bg-accent/50',
                                                        )}
                                                    >
                                                        <Avatar className="size-6 shrink-0">
                                                            <AvatarImage
                                                                src={avatarUrl(
                                                                    candidate.avatar,
                                                                )}
                                                                alt=""
                                                            />
                                                            <AvatarFallback className="text-[10px]">
                                                                {getInitials(
                                                                    candidate.name,
                                                                )}
                                                            </AvatarFallback>
                                                        </Avatar>
                                                        <span className="truncate">
                                                            {candidate.name}
                                                        </span>
                                                    </button>
                                                </li>
                                            ),
                                        )}
                                    </ul>
                                )}

                                <Textarea
                                    ref={textareaRef}
                                    placeholder="Write feedback or a progress update... (@ untuk menyebut seseorang)"
                                    value={commentForm.data.comment}
                                    onChange={(e) => {
                                        commentForm.setData(
                                            'comment',
                                            e.target.value,
                                        );
                                        mention.syncFromCursor(
                                            e.target.value,
                                            e.target.selectionStart,
                                        );
                                        emitTyping();
                                        autoResizeTextarea();
                                    }}
                                    onBlur={mention.close}
                                    onKeyDown={handleKeyDown}
                                    // Kursor juga berpindah tanpa mengubah teks
                                    // — panah, klik, Home/End. Tanpa disegarkan
                                    // di sini, dropdown sebutan bertahan pada
                                    // keadaan terakhir saat mengetik, dan Enter
                                    // memilih nama alih-alih mengirim pesan.
                                    //
                                    // Dibatasi ke tombol yang memang menggeser
                                    // kursor: menyegarkan pada Enter justru
                                    // membuka lagi dropdown yang baru saja
                                    // ditutup `applySuggestion`, karena kursor
                                    // berhenti tepat sesudah "@Nama " dan
                                    // potongan itu terbaca sebagai sebutan baru.
                                    onKeyUp={(e) => {
                                        if (!CURSOR_MOVE_KEYS.has(e.key)) {
                                            return;
                                        }

                                        mention.syncFromCursor(
                                            e.currentTarget.value,
                                            e.currentTarget.selectionStart,
                                        );
                                    }}
                                    onClick={(e) =>
                                        mention.syncFromCursor(
                                            e.currentTarget.value,
                                            e.currentTarget.selectionStart,
                                        )
                                    }
                                    className="max-h-40 min-h-20 resize-none overflow-y-auto border-0 bg-transparent shadow-none ring-0 placeholder:text-muted-foreground/60 focus-visible:ring-0 focus-visible:ring-offset-0"
                                    // Sengaja tidak ikut nonaktif saat pesan
                                    // sedang terbang: isinya sudah pindah ke
                                    // balon yang punya penanda kirim sendiri,
                                    // jadi mengabu-abukan kotaknya hanya
                                    // membuat komposer berkedip tiap kirim.
                                />

                                {/* Selama menyunting, lampiran yang sudah
                                    diantre ikut menyingkir — sama seperti
                                    tombolnya, karena suntingan tidak membawa
                                    lampiran. Berkasnya tidak dibuang: chip-nya
                                    kembali begitu keluar dari mode sunting. */}
                                {!editingComment && pendingFiles.length > 0 && (
                                    <div className="flex flex-wrap gap-2 px-2 pb-2">
                                        {pendingFiles.map((file, index) => (
                                            <span
                                                key={`${file.name}-${index}`}
                                                className="flex max-w-full items-center gap-1.5 rounded-lg bg-muted px-2 py-1 text-[11px] text-muted-foreground"
                                            >
                                                <Paperclip className="h-3 w-3 shrink-0" />
                                                <span className="max-w-40 truncate font-medium text-foreground/80">
                                                    {file.name}
                                                </span>
                                                <button
                                                    type="button"
                                                    onClick={() =>
                                                        removePendingFile(index)
                                                    }
                                                    className="shrink-0 cursor-pointer rounded-full p-0.5 transition-colors hover:bg-background hover:text-destructive"
                                                    aria-label={`Remove attachment ${file.name}`}
                                                >
                                                    <X className="h-3 w-3" />
                                                </button>
                                            </span>
                                        ))}
                                    </div>
                                )}

                                <input
                                    ref={fileInputRef}
                                    type="file"
                                    multiple
                                    className="hidden"
                                    onChange={handleFilesSelected}
                                />
                                <div className="flex items-center justify-between gap-2 px-2 pb-2">
                                    <div className="flex items-center gap-2">
                                        {/* Menyunting tidak menyentuh lampiran —
                                            server hanya menerima teksnya — jadi
                                            tombolnya menyingkir agar tidak
                                            menjanjikan yang tidak terjadi. */}
                                        {!editingComment && (
                                            <TooltipProvider
                                                delayDuration={300}
                                            >
                                                <Tooltip>
                                                    <TooltipTrigger asChild>
                                                        <Button
                                                            type="button"
                                                            size="icon"
                                                            variant="ghost"
                                                            className="h-8 w-8 shrink-0 rounded-lg text-muted-foreground hover:text-foreground"
                                                            onClick={() =>
                                                                fileInputRef.current?.click()
                                                            }
                                                            aria-label="Lampirkan file"
                                                        >
                                                            <Paperclip className="h-4 w-4" />
                                                        </Button>
                                                    </TooltipTrigger>
                                                    <TooltipContent side="top">
                                                        Lampirkan file
                                                    </TooltipContent>
                                                </Tooltip>
                                            </TooltipProvider>
                                        )}
                                        <span className="hidden text-[10px] text-muted-foreground/60 sm:inline">
                                            {editingComment
                                                ? 'Enter untuk simpan · Esc untuk batal'
                                                : 'Enter untuk kirim · Shift+Enter baris baru'}
                                        </span>
                                    </div>
                                    <Button
                                        size="sm"
                                        className="h-8 shrink-0 gap-1.5 rounded-lg shadow-sm"
                                        disabled={!canSend}
                                        onClick={handleSend}
                                    >
                                        {editingComment ? (
                                            <Pencil className="h-3.5 w-3.5" />
                                        ) : (
                                            <Send className="h-3.5 w-3.5" />
                                        )}
                                        {/* Di luar mode sunting labelnya tetap:
                                            pesannya sudah terlihat terkirim di
                                            balonnya sendiri, dan teks yang
                                            berganti bolak-balik tiap kirim hanya
                                            menambah kedipan. */}
                                        <span>
                                            {editingComment ? 'Simpan' : 'Send'}
                                        </span>
                                    </Button>
                                </div>
                            </div>
                        </div>
                    </div>
                )}
            </div>

            <ConfirmDeleteDialog
                open={pendingDeleteId != null}
                onOpenChange={(next) => {
                    if (!next) {
                        setPendingDeleteId(null);
                    }
                }}
                onConfirm={confirmDelete}
                processing={isDeleting}
                title="Delete Message?"
                description="Deleted discussion messages cannot be restored."
                confirmLabel="Delete"
                cancelLabel="Cancel"
                processingLabel="Deleting..."
                variant="center"
            />
        </section>
    );
}
