'use client';

import React, { useEffect, useState, useCallback, useMemo, useRef, Suspense } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { formatDistanceToNow } from 'date-fns';
import { utcToLocal, formatDateTime, getUserTimeZone, generateUUID } from '@alga-psa/core';
import { getTicketingDisplaySettings } from '../../actions/ticketDisplaySettings';
import { ConfirmationDialog } from "@alga-psa/ui/components/ConfirmationDialog";
import { DeleteEntityDialog } from "@alga-psa/ui/components/DeleteEntityDialog";
import { preCheckDeletion } from "@alga-psa/auth/lib/preCheckDeletion";
import type { DeletionValidationResult } from "@alga-psa/types";
import {
    ITicket,
    IComment,
    ITimeSheet,
    ITimePeriod,
    ITimePeriodView,
    ITimeEntry,
    IClient,
    IClientLocation,
    IContact,
    IUser,
    IUserWithRoles,
    ITeam,
    ITicketResource,
    ITicketCategory
} from "@alga-psa/types";
import { ITag } from "@alga-psa/types";
import { TagManager } from "@alga-psa/tags/components";
import { findTagsByEntityId } from "@alga-psa/tags/actions";
import { useTags } from '@alga-psa/tags/context';
import TicketInfo from "./TicketInfo";
import TicketProperties from "./TicketProperties";
import TicketDocumentsSection from "./TicketDocumentsSection";
import TicketEmailNotifications from "./TicketEmailNotifications";
import TicketConversation from "./TicketConversation";
import { TicketActivityTimeline } from "./TicketActivityTimeline";
import { useSession } from 'next-auth/react';
import { toast } from 'react-hot-toast';
import { handleError, isActionPermissionError, isActionMessageError, getErrorMessage } from '@alga-psa/ui/lib/errorHandling';
import { useDrawer } from "@alga-psa/ui";
import { useCatalogShortcut } from "@alga-psa/ui/keyboard-shortcuts";
import { useSchedulingCallbacks } from '@alga-psa/ui/context';
import { findUserById, getCurrentUser, getCurrentUserPermissions } from "@alga-psa/user-composition/actions";
import { findBoardById } from "@alga-psa/tickets/actions";
import { findCommentsByTicketId, deleteComment, createComment, updateComment, findCommentById } from "@alga-psa/tickets/actions";
import { useDocumentsCrossFeature } from '@alga-psa/core/context/DocumentsCrossFeatureContext';
import { getAllActiveContacts, getClientLocations, getContactByContactNameId, getContactsByClient, getClientById, getAllClients } from "../../actions/clientLookupActions";
import { updateTicketWithCache } from "../../actions/optimizedTicketActions";
import { getTicketById, updateTicket, deleteTicket } from "../../actions/ticketActions";
import {
    checkTicketClosure,
    getTicketAutoCloseState,
    type ITicketAutoCloseState,
} from "../../actions/close-rules/closeRuleActions";
import { getTicketChecklistItems, type ITicketChecklistItem } from "../../actions/checklists/ticketChecklistActions";
import type { CloseRuleFailure } from "../../lib/validateTicketClosure";
import TicketChecklistSection, { summarizeChecklist } from "./TicketChecklistSection";
import { Dialog, DialogContent, DialogFooter } from "@alga-psa/ui/components/Dialog";
import { TextArea } from "@alga-psa/ui/components/TextArea";
import { getTicketStatuses } from "@alga-psa/reference-data/actions";
import { getAllPriorities } from "@alga-psa/reference-data/actions";
import { addTicketResource, getTicketResources, removeTicketResource, assignTeamToTicket, removeTeamFromTicket } from "@alga-psa/tickets/actions";
import { getTeamById, getTeams } from '@alga-psa/teams/actions';
import AgentScheduleDrawer from "./AgentScheduleDrawer";
import { Button } from "@alga-psa/ui/components/Button";
import Drawer from '@alga-psa/ui/components/Drawer';
import { Input } from "@alga-psa/ui/components/Input";
import { PresenceBar } from '@alga-psa/ui/presence/PresenceBar';
import { ExternalLink, Mail, History, Trash2 } from 'lucide-react';
import { WorkItemType } from "@alga-psa/types";
import { ReflectionContainer } from "@alga-psa/ui/ui-reflection/ReflectionContainer";
import { PartialBlock, StyledText } from '@blocknote/core';
import { useTicketTimeTracking } from "@alga-psa/ui/hooks";
import { IntervalTrackingService } from "@alga-psa/ui/services";
import { convertBlockNoteToMarkdown } from "@alga-psa/formatting/blocknoteUtils";
import BackNav from '@alga-psa/ui/components/BackNav';
import { ResponseStateBadge } from '@alga-psa/ui/components';
import TicketNavigation from './TicketNavigation';
import TicketOriginBadge from '../TicketOriginBadge';
import { useTranslation } from '@alga-psa/ui/lib/i18n/client';
import { useTicketLiveContext } from './TicketLiveProvider';
import { buildTicketTimeEntryContext, createTicketTimeEntryOnComplete } from '../../lib/timeEntryContext';
import { getTicketOrigin } from '../../lib/ticketOrigin';
import {
    setTicketWatchListOnAttributes,
    type TicketWatchListEntry,
} from '@shared/lib/tickets/watchList';
import {
    addChildrenToBundleAction,
    findTicketByNumberAction,
    promoteBundleMasterAction,
    removeChildFromBundleAction,
    unbundleMasterTicketAction,
    updateBundleSettingsAction,
    searchEligibleChildTicketsAction,
    type EligibleChildTicket
} from '../../actions/ticketBundleActions';
import { deleteDraftClipboardImages } from '../../actions/comment-actions/clipboardImageDraftActions';
import {
    resolveCommentReferencedImageDocuments,
    type CommentImageDocumentReference,
} from '../../lib/commentImageDocuments';
import { isBoardLiveTicketTimerEnabled } from '../../lib/boardLiveTicketTimer';
import { hasAdminSettingsViewAccess } from './commentMetadataDebug';
import { normalizeTicketLiveField, type TicketLiveConflictState } from './ticketLiveFields';

interface PendingCommentDelete {
    commentId: string;
    imageDocuments: CommentImageDocumentReference[];
}

const LIVE_UPDATE_REFETCH_DEBOUNCE_MS = 200;
const LIVE_UPDATE_HIGHLIGHT_MS = 600;

interface TicketDetailsProps {
    id?: string; // Made optional to maintain backward compatibility
    initialTicket: ITicket & { tenant: string | undefined };
    initialBundle?: any;
    aggregatedChildClientComments?: any[];
    onClose?: () => void; // Callback when user wants to close the ticket screen
    isInDrawer?: boolean;

    // Pre-fetched data props
    initialComments?: IComment[];
    initialDocuments?: any[];
    initialClient?: IClient | null;
    initialContacts?: IContact[];
    initialContactInfo?: IContact | null;
    initialCreatedByUser?: IUser | null;
    initialBoard?: any;
    initialAdditionalAgents?: ITicketResource[];
    initialAvailableAgents?: IUserWithRoles[];
    initialUserMap?: Record<string, { user_id: string; first_name: string; last_name: string; email?: string, user_type: string, avatarUrl: string | null }>;
    initialContactMap?: Record<string, { contact_id: string; full_name: string; email?: string; avatarUrl: string | null }>;
    statusOptions?: { value: string; label: string; is_closed?: boolean; className?: string; board_id?: string | null }[];
    agentOptions?: { value: string; label: string }[];
    boardOptions?: { value: string; label: string }[];
    priorityOptions?: { value: string; label: string }[];
    initialCategories?: ITicketCategory[];
    initialClients?: IClient[];
    initialLocations?: IClientLocation[];
    initialAgentSchedules?: { userId: string; minutes: number }[];

    // Current user (for drawer usage)
    currentUser?: IUser | null;

    // Optimized handlers
    onTicketUpdate?: (field: string, value: any) => Promise<void>;
    onBatchTicketUpdate?: (changes: Record<string, unknown>) => Promise<boolean>;
    onAddComment?: (content: string, isInternal: boolean, isResolution: boolean, closesTicket?: boolean) => Promise<void>;
    onUpdateDescription?: (content: string) => Promise<boolean>;
    isSubmitting?: boolean;
    /**
     * Optional injected UI for survey summary (e.g. @alga-psa/surveys TicketSurveySummaryCard).
     * This keeps @alga-psa/tickets from importing other vertical slices directly.
     */
    surveySummaryCard?: React.ReactNode;

    /**
     * Optional injected UI for cross-slice composition (e.g. assets associations).
     * This keeps @alga-psa/tickets from importing other vertical slices directly.
     */
    associatedAssets?: React.ReactNode;

    /**
     * Optional injected UI for contact quick view (e.g. @alga-psa/clients ContactDetailsView).
     * If omitted, TicketDetails falls back to a minimal drawer with a link to open the contact page.
     */
    renderContactDetails?: (args: {
        id: string;
        contact: IContact;
        clients: IClient[];
        userId?: string;
    }) => React.ReactNode;

    /**
     * Optional injected UI for creating project tasks from tickets.
     */
    renderCreateProjectTask?: (args: { ticket: ITicket; additionalAgents?: { user_id: string; name: string }[] }) => React.ReactNode;

    /**
     * Optional injected UI for client quick view (e.g. @alga-psa/clients ClientDetails).
     * If omitted, TicketDetails falls back to a minimal drawer with a link to open the client page.
     */
    renderClientDetails?: (args: {
        id: string;
        client: IClient;
    }) => React.ReactNode;

    /**
     * Optional injected UI for interval management (e.g. @alga-psa/scheduling IntervalManagement).
     * Shows auto-tracked time intervals below the ticket timer.
     */
    renderIntervalManagement?: (args: { ticketId: string; userId: string }) => React.ReactNode;
    hideSlaStatus?: boolean;
    hideTimeEntry?: boolean;
    hideMaterials?: boolean;
    uploadTicketAttachmentAction?: (
        formData: FormData,
        params: { userId: string; ticketId: string }
    ) => Promise<any>;
    deleteDraftTicketAttachmentImagesAction?: (input: {
        ticketId: string;
        documentIds: string[];
    }) => Promise<{ deletedDocumentIds: string[]; failures: Array<{ documentId: string; reason: string }> }>;
    resolveTicketAttachmentViewUrl?: (document: { document_id?: string; file_id?: string }) => string;
    disableAttachmentFolderSelection?: boolean;
    disableAttachmentSharing?: boolean;
    disableAttachmentLinking?: boolean;
    disableAgentSchedule?: boolean;
}

const TicketDetails: React.FC<TicketDetailsProps> = ({
    id = 'ticket-details',
    initialTicket,
    initialBundle = null,
    aggregatedChildClientComments = [],
    onClose,
    isInDrawer = false,
    // Pre-fetched data with defaults
    initialComments = [],
    initialDocuments = [],
    initialClient = null,
    initialContacts = [],
    initialContactInfo = null,
    initialCreatedByUser = null,
    initialBoard = null,
    initialAdditionalAgents = [],
    initialAvailableAgents = [],
    initialUserMap = {},
    initialContactMap = {},
    statusOptions = [],
    agentOptions = [],
    boardOptions = [],
    priorityOptions = [],
    initialCategories = [],
    initialClients = [],
    initialLocations = [],
    initialAgentSchedules = [],
    // Current user (for drawer usage)
    currentUser,
    // Optimized handlers
    onTicketUpdate,
    onBatchTicketUpdate,
    onAddComment,
    onUpdateDescription,
    isSubmitting = false,
    surveySummaryCard,
    associatedAssets = null,
    renderContactDetails,
    renderCreateProjectTask,
    renderClientDetails,
    renderIntervalManagement,
    hideSlaStatus = false,
    hideTimeEntry = false,
    hideMaterials = false,
    uploadTicketAttachmentAction,
    deleteDraftTicketAttachmentImagesAction,
    resolveTicketAttachmentViewUrl,
    disableAttachmentFolderSelection = false,
    disableAttachmentSharing = false,
    disableAttachmentLinking = false,
    disableAgentSchedule = false,
}) => {
    const { t } = useTranslation('features/tickets');
    const ticketLive = useTicketLiveContext();
    const { data: session } = useSession();
    const [hasHydrated, setHasHydrated] = useState(false);
    const [canViewCommentMetadataDebug, setCanViewCommentMetadataDebug] = useState(false);
    const { getDocumentByTicketId, deleteDocument } = useDocumentsCrossFeature();
    const router = useRouter();
    const searchParams = useSearchParams();

    useEffect(() => {
        setHasHydrated(true);
    }, []);

    // Show title in sticky header only when the card title scrolls out of view
    useEffect(() => {
        const el = cardTitleRef.current;
        if (!el) return;
        const observer = new IntersectionObserver(
            ([entry]) => setCardTitleVisible(entry.isIntersecting),
            { threshold: 0 }
        );
        observer.observe(el);
        return () => observer.disconnect();
    }, []);

    useEffect(() => {
        let cancelled = false;
        void getCurrentUserPermissions().then((perms) => {
            if (!cancelled) {
                setCanViewCommentMetadataDebug(hasAdminSettingsViewAccess(perms));
            }
        });
        return () => {
            cancelled = true;
        };
    }, []);

    // Use passed currentUser if available (for drawer), otherwise fallback to session
    const userId = currentUser?.user_id || session?.user?.id;
    const tenant = initialTicket.tenant;

    const [ticket, setTicket] = useState(initialTicket);
    const [bundle, setBundle] = useState<any>(initialBundle);
    const [cardTitleVisible, setCardTitleVisible] = useState(true);
    const cardTitleRef = useRef<HTMLHeadingElement>(null);
    const [isEmailNotificationLogsDrawerOpen, setIsEmailNotificationLogsDrawerOpen] = useState(false);
    const [isActivityLogDrawerOpen, setIsActivityLogDrawerOpen] = useState(false);
    const [activityLogRefreshKey, setActivityLogRefreshKey] = useState(0);
    // Single-ticket delete (mirrors the client/contact detail-page delete pattern)
    const [isDeleteTicketDialogOpen, setIsDeleteTicketDialogOpen] = useState(false);

    // Close rules: blocked-close dialog + checklist + auto-close banner state
    const [closeBlockedDialog, setCloseBlockedDialog] = useState<{
        isOpen: boolean;
        statusId: string | null;
        failures: CloseRuleFailure[];
        canOverride: boolean;
    }>({ isOpen: false, statusId: null, failures: [], canOverride: false });
    const [closeOverrideReason, setCloseOverrideReason] = useState('');
    const [isSubmittingCloseOverride, setIsSubmittingCloseOverride] = useState(false);
    const [checklistItems, setChecklistItems] = useState<ITicketChecklistItem[] | undefined>(undefined);
    const [autoCloseState, setAutoCloseState] = useState<ITicketAutoCloseState | null>(null);

    useEffect(() => {
        let cancelled = false;
        if (!ticket.ticket_id) return;
        getTicketChecklistItems(ticket.ticket_id)
            .then((items) => { if (!cancelled) setChecklistItems(items); })
            .catch((err) => console.error('Failed to load ticket checklist:', err));
        getTicketAutoCloseState(ticket.ticket_id)
            .then((state) => { if (!cancelled) setAutoCloseState(state); })
            .catch((err) => console.error('Failed to load auto-close state:', err));
        return () => { cancelled = true; };
        // Re-check the pending auto-close whenever the status changes — a
        // status move usually cancels or reschedules it.
    }, [ticket.ticket_id, ticket.status_id]);

    const checklistSummary = useMemo(
        () => summarizeChecklist(checklistItems ?? []),
        [checklistItems]
    );

    const submitCloseOverride = async () => {
        if (!closeBlockedDialog.statusId || !ticket.ticket_id) return;
        setIsSubmittingCloseOverride(true);
        try {
            await updateTicketWithCache(ticket.ticket_id, { status_id: closeBlockedDialog.statusId }, {
                overrideCloseRules: true,
                overrideCloseRulesReason: closeOverrideReason.trim() || null,
            });
            setTicket((prev: any) => ({ ...prev, status_id: closeBlockedDialog.statusId, response_state: null }));
            setCloseBlockedDialog({ isOpen: false, statusId: null, failures: [], canOverride: false });
            setCloseOverrideReason('');
            toast.success('Ticket closed');
        } catch (error) {
            handleError(error, 'Failed to close ticket');
        } finally {
            setIsSubmittingCloseOverride(false);
        }
    };
    const [ticketDeleteValidation, setTicketDeleteValidation] = useState<DeletionValidationResult | null>(null);
    const [isTicketDeleteValidating, setIsTicketDeleteValidating] = useState(false);
    const [isTicketDeleteProcessing, setIsTicketDeleteProcessing] = useState(false);
    const [conversations, setConversations] = useState<IComment[]>(initialComments);
    const [documents, setDocuments] = useState<any[]>(initialDocuments);
    const [client, setClient] = useState<IClient | null>(initialClient);
    const [contactInfo, setContactInfo] = useState<IContact | null>(initialContactInfo);
    const [createdByUser, setCreatedByUser] = useState<IUser | null>(initialCreatedByUser);

    const closedStatusOptions = useMemo(() => {
        const boardId = ticket.board_id;
        if (!boardId) {
            return [];
        }
        return (statusOptions || [])
            .filter(
                (opt) =>
                    !!opt.is_closed &&
                    opt.board_id === boardId
            )
            .map(({ value, label }) => ({ value, label }));
    }, [statusOptions, ticket.board_id]);
    const [board, setBoard] = useState<any>(initialBoard);
    const [savedBoardId, setSavedBoardId] = useState<string | null>(initialBoard?.board_id ?? initialTicket.board_id ?? null);
    const isLiveTicketTimerEnabled = useMemo(() => isBoardLiveTicketTimerEnabled(board), [board]);
    const [clients, setClients] = useState<IClient[]>(initialClients);
    const [contacts, setContacts] = useState<IContact[]>(initialContacts);
    const [locations, setLocations] = useState<IClientLocation[]>(initialLocations);
    const [dateTimeFormat, setDateTimeFormat] = useState<string>('MMM d, yyyy h:mm a');
    const [responseStateTrackingEnabled, setResponseStateTrackingEnabled] = useState<boolean>(true);
    const [createdRelativeTime, setCreatedRelativeTime] = useState<string>('');
    const [updatedRelativeTime, setUpdatedRelativeTime] = useState<string>('');
    const [addChildTicketNumber, setAddChildTicketNumber] = useState<string>('');
    const [selectedChildTicket, setSelectedChildTicket] = useState<{
        ticket_id: string;
        client_id: string | null;
        ticket_number: string;
    } | null>(null);
    const [searchResults, setSearchResults] = useState<EligibleChildTicket[]>([]);
    const [isSearching, setIsSearching] = useState(false);
    const [showSearchResults, setShowSearchResults] = useState(false);
    const searchInputRef = useRef<HTMLInputElement>(null);
    const searchContainerRef = useRef<HTMLDivElement>(null);
    const [isUpdatingBundleSettings, setIsUpdatingBundleSettings] = useState(false);
    const [isAddChildMultiClientConfirmOpen, setIsAddChildMultiClientConfirmOpen] = useState(false);
    const [pendingChildToAdd, setPendingChildToAdd] = useState<{ ticket_id: string; ticket_number?: string | null; client_id?: string | null } | null>(null);
    const [isWatchListSaving, setIsWatchListSaving] = useState(false);
    const [allContactsForWatchList, setAllContactsForWatchList] = useState<IContact[]>([]);
    const [allContactsForWatchListLoading, setAllContactsForWatchListLoading] = useState(false);
    const ticketOrigin = useMemo(() => getTicketOrigin(ticket as any), [ticket]);
    const ticketOriginLabels = useMemo(() => ({
        internal: t('origin.internal', 'Created Internally'),
        clientPortal: t('origin.clientPortal', 'Created via Client Portal'),
        inboundEmail: t('origin.inboundEmail', 'Created via Inbound Email'),
        api: t('origin.api', 'Created via API'),
        other: t('origin.other', 'Created via Other'),
    }), [t]);
    const [ticketInfoDirtyFields, setTicketInfoDirtyFields] = useState<string[]>([]);
    const [ticketPropertiesDirtyFields, setTicketPropertiesDirtyFields] = useState<string[]>([]);
    const [liveHighlightedFields, setLiveHighlightedFields] = useState<string[]>([]);
    const [liveFieldConflicts, setLiveFieldConflicts] = useState<Partial<Record<string, TicketLiveConflictState>>>({});
    const [reactionRefreshVersion, setReactionRefreshVersion] = useState(0);
    const [livePendingFieldVersion, setLivePendingFieldVersion] = useState(0);
    const ticketInfoDirtyFieldsRef = useRef<string[]>([]);
    const ticketPropertiesDirtyFieldsRef = useRef<string[]>([]);
    const pendingLiveNetworkFieldsRef = useRef<Set<string>>(new Set());
    const pendingRemoteUpdateRef = useRef<{
        updatedFields: string[];
        updatedBy: { userId: string; displayName: string };
        updatedAt: string;
    } | null>(null);
    const remoteUpdateTimerRef = useRef<NodeJS.Timeout | null>(null);
    const liveHighlightTimersRef = useRef<Record<string, NodeJS.Timeout>>({});

    const runWithPendingLiveFields = useCallback(async <T,>(fields: string[], fn: () => Promise<T>): Promise<T> => {
        const normalizedFields = Array.from(new Set(fields.map((field) => normalizeTicketLiveField(field))));

        for (const field of normalizedFields) {
            pendingLiveNetworkFieldsRef.current.add(field);
        }
        setLivePendingFieldVersion((current) => current + 1);

        try {
            return await fn();
        } finally {
            for (const field of normalizedFields) {
                pendingLiveNetworkFieldsRef.current.delete(field);
            }
            setLivePendingFieldVersion((current) => current + 1);
        }
    }, []);

    const liveDirtyFieldSet = useMemo(() => {
        const fields = new Set<string>();

        for (const field of ticketInfoDirtyFields) {
            fields.add(normalizeTicketLiveField(field));
        }
        for (const field of ticketPropertiesDirtyFields) {
            fields.add(normalizeTicketLiveField(field));
        }
        for (const field of pendingLiveNetworkFieldsRef.current) {
            fields.add(normalizeTicketLiveField(field));
        }

        return fields;
    }, [livePendingFieldVersion, ticketInfoDirtyFields, ticketPropertiesDirtyFields]);

    const liveEditingUsers = useMemo(() => {
        const editingUsersByField: Partial<Record<string, string[]>> = {};
        const seenByField = new Map<string, Set<string>>();

        for (const presenceUser of ticketLive.presence) {
            if (!presenceUser.editingField) {
                continue;
            }

            const field = normalizeTicketLiveField(presenceUser.editingField);
            const seenUsers = seenByField.get(field) ?? new Set<string>();
            if (seenUsers.has(presenceUser.userId)) {
                continue;
            }

            seenUsers.add(presenceUser.userId);
            seenByField.set(field, seenUsers);
            editingUsersByField[field] = [...(editingUsersByField[field] ?? []), presenceUser.displayName];
        }

        return editingUsersByField;
    }, [ticketLive.presence]);

    useEffect(() => {
        ticketInfoDirtyFieldsRef.current = ticketInfoDirtyFields;
    }, [ticketInfoDirtyFields]);

    useEffect(() => {
        ticketPropertiesDirtyFieldsRef.current = ticketPropertiesDirtyFields;
    }, [ticketPropertiesDirtyFields]);

    const clearRemoteUpdateTimer = useCallback(() => {
        if (remoteUpdateTimerRef.current) {
            clearTimeout(remoteUpdateTimerRef.current);
            remoteUpdateTimerRef.current = null;
        }
    }, []);

    const highlightLiveFields = useCallback((fields: string[]) => {
        const normalizedFields = Array.from(new Set(fields.map((field) => normalizeTicketLiveField(field))));

        if (normalizedFields.length === 0) {
            return;
        }

        setLiveHighlightedFields((current) => Array.from(new Set([...current, ...normalizedFields])));

        for (const field of normalizedFields) {
            if (liveHighlightTimersRef.current[field]) {
                clearTimeout(liveHighlightTimersRef.current[field]);
            }

            liveHighlightTimersRef.current[field] = setTimeout(() => {
                setLiveHighlightedFields((current) => current.filter((currentField) => currentField !== field));
                delete liveHighlightTimersRef.current[field];
            }, LIVE_UPDATE_HIGHLIGHT_MS);
        }
    }, []);

    const getLiveFieldLabel = useCallback((field: string) => {
        switch (normalizeTicketLiveField(field)) {
            case 'title':
                return t('fields.title', 'title').toLowerCase();
            case 'status_id':
                return t('fields.status', 'status').toLowerCase();
            case 'priority_id':
                return t('fields.priority', 'priority').toLowerCase();
            case 'assigned_to':
                return t('fields.assignedTo', 'assigned to').toLowerCase();
            case 'board_id':
                return t('info.board', 'board').toLowerCase();
            case 'category_id':
                return t('fields.category', 'category').toLowerCase();
            case 'itil_impact':
                return t('itil.impact', 'impact').toLowerCase();
            case 'itil_urgency':
                return t('itil.urgency', 'urgency').toLowerCase();
            case 'client_id':
                return t('fields.client', 'client').toLowerCase();
            case 'contact_name_id':
                return t('properties.contact', 'contact').toLowerCase();
            case 'location_id':
                return t('properties.location', 'location').toLowerCase();
            case 'response_state':
                return t('fields.responseState', 'response state').toLowerCase();
            case 'due_date':
                return t('fields.dueDate', 'due date').toLowerCase();
            default:
                return normalizeTicketLiveField(field).replace(/_/g, ' ');
        }
    }, [t]);

    const isRemoteUpdateAccessError = useCallback((error: unknown) => {
        if (isActionPermissionError(error)) {
            return true;
        }

        if (error instanceof Error) {
            return /403|forbidden|permission denied/i.test(error.message);
        }

        if (typeof error === 'string') {
            return /403|forbidden|permission denied/i.test(error);
        }

        return false;
    }, []);

    const clearLiveFieldConflict = useCallback((field: string) => {
        setLiveFieldConflicts((current) => {
            if (!current[field]) {
                return current;
            }

            const next = { ...current };
            delete next[field];
            return next;
        });
    }, []);

    const handleKeepLiveConflict = useCallback((field: string) => {
        clearLiveFieldConflict(field);
    }, [clearLiveFieldConflict]);

    const handleTakeLiveConflict = useCallback((field: string) => {
        clearLiveFieldConflict(field);
        highlightLiveFields([field]);
    }, [clearLiveFieldConflict, highlightLiveFields]);

    const refreshTicketSnapshot = useCallback(async (updatedFields: string[] = []) => {
        if (!ticket.ticket_id) {
            return { refreshed: false as const };
        }

        try {
            const latestTicket = await getTicketById(ticket.ticket_id);
            const normalizedUpdatedFields = new Set(updatedFields.map((field) => normalizeTicketLiveField(field)));

            setTicket(latestTicket);
            setItilImpact(latestTicket.itil_impact || undefined);
            setItilUrgency(latestTicket.itil_urgency || undefined);
            setSavedBoardId(latestTicket.board_id ?? null);

            const shouldRefreshClientContext =
                normalizedUpdatedFields.has('client_id') || latestTicket.client_id !== ticket.client_id;
            const shouldRefreshContactContext =
                normalizedUpdatedFields.has('contact_name_id') || latestTicket.contact_name_id !== ticket.contact_name_id;
            const shouldRefreshLocationContext =
                normalizedUpdatedFields.has('location_id') || latestTicket.location_id !== ticket.location_id;

            if (shouldRefreshClientContext) {
                if (!latestTicket.client_id) {
                    setClient(null);
                    setContacts([]);
                    setLocations([]);
                    setContactInfo(null);
                } else {
                    const [latestClient, latestContacts, latestLocations] = await Promise.all([
                        getClientById(latestTicket.client_id),
                        getContactsByClient(latestTicket.client_id),
                        getClientLocations(latestTicket.client_id),
                    ]);

                    setClient(latestClient);
                    setContacts(latestContacts || []);
                    setLocations(latestLocations || []);
                    setContactInfo(
                        latestTicket.contact_name_id
                            ? await getContactByContactNameId(latestTicket.contact_name_id)
                            : null
                    );
                }
            } else {
                if (shouldRefreshContactContext) {
                    setContactInfo(
                        latestTicket.contact_name_id
                            ? await getContactByContactNameId(latestTicket.contact_name_id)
                            : null
                    );
                }

                if (shouldRefreshLocationContext && latestTicket.client_id) {
                    setLocations(await getClientLocations(latestTicket.client_id));
                }
            }

            if (normalizedUpdatedFields.has('comments')) {
                setConversations(await findCommentsByTicketId(ticket.ticket_id));
            }

            if (normalizedUpdatedFields.has('comment_reactions')) {
                setReactionRefreshVersion((value) => value + 1);
            }

            return { refreshed: true as const, latestTicket };
        } catch (error) {
            if (isRemoteUpdateAccessError(error)) {
                router.push('/msp/tickets');
                return { refreshed: false as const, redirected: true as const };
            }

            console.warn('Failed to refresh live ticket snapshot', error);
            return { refreshed: false as const, error };
        }
    }, [
        getClientLocations,
        getContactByContactNameId,
        getClientById,
        getContactsByClient,
        isRemoteUpdateAccessError,
        router,
        ticket.client_id,
        ticket.contact_name_id,
        ticket.location_id,
        ticket.ticket_id,
    ]);

    const flushPendingRemoteUpdate = useCallback(async () => {
        const pendingUpdate = pendingRemoteUpdateRef.current;
        pendingRemoteUpdateRef.current = null;
        remoteUpdateTimerRef.current = null;

        if (!pendingUpdate || pendingUpdate.updatedFields.length === 0) {
            return;
        }

        const dirtyFields = new Set<string>();

        for (const field of ticketInfoDirtyFieldsRef.current) {
            dirtyFields.add(normalizeTicketLiveField(field));
        }
        for (const field of ticketPropertiesDirtyFieldsRef.current) {
            dirtyFields.add(normalizeTicketLiveField(field));
        }
        for (const field of pendingLiveNetworkFieldsRef.current) {
            dirtyFields.add(normalizeTicketLiveField(field));
        }

        const updatedFields = Array.from(new Set(pendingUpdate.updatedFields.map((field) => normalizeTicketLiveField(field))));
        const overlappingFields = updatedFields.filter((field) => dirtyFields.has(field));
        const nonOverlappingFields = updatedFields.filter((field) => !dirtyFields.has(field));

        const refreshResult = await refreshTicketSnapshot(pendingUpdate.updatedFields);
        if (!refreshResult.refreshed) {
            return;
        }

        if (nonOverlappingFields.length > 0) {
            highlightLiveFields(nonOverlappingFields);
        }

        if (overlappingFields.length > 0) {
            setLiveFieldConflicts((current) => {
                const next = { ...current };

                for (const field of overlappingFields) {
                    next[field] = {
                        updatedFields: pendingUpdate.updatedFields.filter(
                            (updatedField) => normalizeTicketLiveField(updatedField) === field
                        ),
                        updatedBy: pendingUpdate.updatedBy,
                        updatedAt: pendingUpdate.updatedAt,
                    };
                }

                return next;
            });
            return;
        }

        if (dirtyFields.size > 0 && nonOverlappingFields.length > 0) {
            toast.success(
                t('liveUpdates.remoteFieldUpdated', '{{name}} updated {{field}}')
                    .replace('{{name}}', pendingUpdate.updatedBy.displayName)
                    .replace('{{field}}', getLiveFieldLabel(nonOverlappingFields[0]))
            );
        }
    }, [getLiveFieldLabel, highlightLiveFields, refreshTicketSnapshot, t]);

    const queueRemoteUpdate = useCallback((update: { updatedFields: string[]; updatedBy: { userId: string; displayName: string }; updatedAt: string }) => {
        const currentPendingUpdate = pendingRemoteUpdateRef.current;

        pendingRemoteUpdateRef.current = currentPendingUpdate
            ? {
                updatedFields: Array.from(new Set([...currentPendingUpdate.updatedFields, ...update.updatedFields])),
                updatedBy: update.updatedBy,
                updatedAt: update.updatedAt,
            }
            : update;

        clearRemoteUpdateTimer();
        remoteUpdateTimerRef.current = setTimeout(() => {
            void flushPendingRemoteUpdate();
        }, LIVE_UPDATE_REFETCH_DEBOUNCE_MS);
    }, [clearRemoteUpdateTimer, flushPendingRemoteUpdate]);

    useEffect(() => {
        setBundle(initialBundle);
    }, [initialBundle]);

    useEffect(() => {
        setBoard(initialBoard);
        setSavedBoardId(initialBoard?.board_id ?? initialTicket.board_id ?? null);
    }, [initialBoard, initialTicket.board_id]);

    useEffect(() => {
        if (!ticketLive.enabled || ticketLive.reconnectVersion === 0) {
            return;
        }

        void refreshTicketSnapshot();
    }, [refreshTicketSnapshot, ticketLive.enabled, ticketLive.reconnectVersion]);

    useEffect(() => {
        const remoteUpdate = ticketLive.lastRemoteUpdate;
        if (!ticketLive.enabled || !remoteUpdate || remoteUpdate.updatedBy.userId === userId) {
            return;
        }

        queueRemoteUpdate(remoteUpdate);
    }, [queueRemoteUpdate, ticketLive.enabled, ticketLive.lastRemoteUpdate, userId]);

    useEffect(() => {
        return () => {
            clearRemoteUpdateTimer();

            for (const timer of Object.values(liveHighlightTimersRef.current)) {
                clearTimeout(timer);
            }

            liveHighlightTimersRef.current = {};
        };
    }, [clearRemoteUpdateTimer]);

    useEffect(() => {
        setLiveFieldConflicts((current) => {
            const nextEntries = Object.entries(current).filter(([field]) => liveDirtyFieldSet.has(field));

            if (nextEntries.length === Object.keys(current).length) {
                return current;
            }

            return Object.fromEntries(nextEntries);
        });
    }, [liveDirtyFieldSet]);

    useEffect(() => {
        let cancelled = false;

        const loadBoard = async () => {
            if (!savedBoardId) {
                if (!cancelled) {
                    setBoard(null);
                }
                return;
            }

            try {
                const fetchedBoard = await findBoardById(savedBoardId);
                if (!cancelled) {
                    setBoard(fetchedBoard ?? null);
                }
            } catch (error) {
                console.error('Failed to refresh board metadata:', error);
            }
        };

        loadBoard();

        return () => {
            cancelled = true;
        };
    }, [savedBoardId]);

    // Use pre-fetched options directly
    const [userMap, setUserMap] = useState<Record<string, { user_id: string; first_name: string; last_name: string; email?: string, user_type: string, avatarUrl: string | null }>>(initialUserMap);
    const [contactMap] = useState<Record<string, { contact_id: string; full_name: string; email?: string; avatarUrl: string | null }>>(initialContactMap);

    const [availableAgents, setAvailableAgents] = useState<IUserWithRoles[]>(initialAvailableAgents);
    const [additionalAgents, setAdditionalAgents] = useState<ITicketResource[]>(initialAdditionalAgents);

    const additionalAgentsForInfo = useMemo(() => {
        return additionalAgents.map(a => {
            const userId = a.additional_user_id || a.assigned_to;
            const agent = availableAgents.find(u => u.user_id === userId);
            return {
                user_id: userId,
                name: agent ? `${agent.first_name} ${agent.last_name || ''}`.trim() : '',
            };
        });
    }, [additionalAgents, availableAgents]);

    const [newCommentContent, setNewCommentContent] = useState<PartialBlock[]>([{
        type: "paragraph",
        props: {
            textAlignment: "left",
            backgroundColor: "default",
            textColor: "default"
        },
        content: [{
            type: "text",
            text: "",
            styles: {}
        }]
    }]);
    const [activeTab, setActiveTab] = useState('all-comments');
    const [isEditing, setIsEditing] = useState(false);
    const [currentComment, setCurrentComment] = useState<IComment | null>(null);

    const [elapsedTime, setElapsedTime] = useState(0);
    const [isRunning, setIsRunning] = useState(false);
    const [timeDescription, setTimeDescription] = useState('');
    const [timeEntriesRefreshKey, setTimeEntriesRefreshKey] = useState(0);
    const [tags, setTags] = useState<ITag[]>([]);
    const { tags: allTags } = useTags();
    const [currentTimeSheet, setCurrentTimeSheet] = useState<ITimeSheet | null>(null);
    const [currentTimePeriod, setCurrentTimePeriod] = useState<ITimePeriodView | null>(null);

    const [team, setTeam] = useState<ITeam | null>(null);
    const [teams, setTeams] = useState<ITeam[]>([]);
    const [isChangeContactDialogOpen, setIsChangeContactDialogOpen] = useState(false);
    const [isChangeClientDialogOpen, setIsChangeClientDialogOpen] = useState(false);
    const [clientFilterState, setClientFilterState] = useState<'all' | 'active' | 'inactive'>('all');
    const [clientTypeFilter, setClientTypeFilter] = useState<'all' | 'company' | 'individual'>('all');
    const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
    const [commentToDelete, setCommentToDelete] = useState<PendingCommentDelete | null>(null);
    const [isDeletingComment, setIsDeletingComment] = useState(false);
    const [isTimeEntryPeriodDialogOpen, setIsTimeEntryPeriodDialogOpen] = useState(false);
    const [pendingDeleteTimeEntry, setPendingDeleteTimeEntry] = useState<{ entry_id: string; user_name: string | null } | null>(null);
    const [isDeletingTimeEntry, setIsDeletingTimeEntry] = useState(false);

    // Debounced search for child tickets
    const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
    const childTicketSearchSeqRef = useRef(0);

    const cancelChildTicketSearch = useCallback(() => {
        childTicketSearchSeqRef.current += 1;
        if (searchTimeoutRef.current) {
            clearTimeout(searchTimeoutRef.current);
            searchTimeoutRef.current = null;
        }
        setIsSearching(false);
    }, []);

    const resetChildTicketPickerState = useCallback(() => {
        cancelChildTicketSearch();
        setSearchResults([]);
        setShowSearchResults(false);
        setSelectedChildTicket(null);
    }, [cancelChildTicketSearch]);

    // ITIL-specific state for editing
    const [itilImpact, setItilImpact] = useState<number | undefined>(ticket.itil_impact || undefined);
    const [itilUrgency, setItilUrgency] = useState<number | undefined>(ticket.itil_urgency || undefined);
    // NOTE: ITIL categories are now managed through the unified category system

    const { openDrawer, closeDrawer, replaceDrawer } = useDrawer();
    const { launchTimeEntry, deleteTimeEntry } = useSchedulingCallbacks();

    const resetTicketDeleteState = useCallback(() => {
        if (isTicketDeleteProcessing) return;
        setIsDeleteTicketDialogOpen(false);
        setTicketDeleteValidation(null);
        setIsTicketDeleteValidating(false);
    }, [isTicketDeleteProcessing]);

    const runTicketDeleteValidation = useCallback(async () => {
        if (!ticket.ticket_id) return;
        setIsTicketDeleteValidating(true);
        try {
            const result = await preCheckDeletion('ticket', ticket.ticket_id);
            setTicketDeleteValidation(result);
        } catch (error: any) {
            console.error('Failed to validate ticket deletion:', error);
            setTicketDeleteValidation({
                canDelete: false,
                code: 'VALIDATION_FAILED',
                message: t('delete.validationError', {
                    defaultValue: 'Failed to validate deletion. Please try again.',
                }),
                dependencies: [],
                alternatives: [],
            });
        } finally {
            setIsTicketDeleteValidating(false);
        }
    }, [ticket.ticket_id, t]);

    const handleDeleteTicket = useCallback(() => {
        setIsDeleteTicketDialogOpen(true);
        void runTicketDeleteValidation();
    }, [runTicketDeleteValidation]);

    const confirmTicketDelete = useCallback(async () => {
        if (!ticket.ticket_id) return;
        setIsTicketDeleteProcessing(true);
        try {
            const result = await deleteTicket(ticket.ticket_id);

            if (!result.success) {
                // Surface blocking dependencies / permission issues in the dialog.
                setTicketDeleteValidation(result);
                return;
            }

            setIsDeleteTicketDialogOpen(false);
            setTicketDeleteValidation(null);
            toast.success(t('delete.success', {
                defaultValue: 'Ticket #{{number}} deleted successfully.',
                number: ticket.ticket_number,
            }));

            if (isInDrawer) {
                closeDrawer();
                onClose?.();
            } else {
                // Preserve the list filters the user came in with (same mechanism
                // as the Back button / BackNav) so the tickets list reopens filtered.
                const returnFilters = searchParams?.get('returnFilters') ?? null;
                const filtersQuery = returnFilters ? decodeURIComponent(returnFilters) : '';
                router.push(filtersQuery ? `/msp/tickets?${filtersQuery}` : '/msp/tickets');
            }
        } catch (error: any) {
            console.error('Failed to delete ticket:', error);
            toast.error(error?.message || t('delete.error', {
                defaultValue: 'Failed to delete ticket. Please try again.',
            }));
        } finally {
            setIsTicketDeleteProcessing(false);
        }
    }, [ticket.ticket_id, ticket.ticket_number, isInDrawer, closeDrawer, onClose, router, searchParams, t]);

    // Create a single instance of the service
    const intervalService = useMemo(() => new IntervalTrackingService(), []);

    useEffect(() => {
        const loadTeams = async () => {
            try {
                const fetchedTeams = await getTeams();
                setTeams(fetchedTeams);
            } catch (error) {
                console.error('Failed to load teams:', error);
            }
        };
        loadTeams();
    }, []);

    useEffect(() => {
        if (!ticket.assigned_team_id) {
            setTeam(null);
            return;
        }

        const cached = teams.find(t => t.team_id === ticket.assigned_team_id);
        if (cached) {
            setTeam(cached);
            return;
        }

        const loadTeam = async () => {
            try {
                const fetchedTeam = await getTeamById(ticket.assigned_team_id!);
                setTeam(fetchedTeam);
            } catch (error) {
                console.error('Failed to load assigned team:', error);
                setTeam(null);
            }
        };
        loadTeam();
    }, [ticket.assigned_team_id, teams]);

    // Timer logic
    const tick = useCallback(() => {
        setElapsedTime(prevTime => {
            return prevTime + 1;
        });
    }, [isRunning]);

    useEffect(() => {
        let intervalId: NodeJS.Timeout | undefined;
        if (isRunning) {
            intervalId = setInterval(tick, 1000);
        }
        return () => {
            if (intervalId) {
                clearInterval(intervalId);
            }
        };
    }, [isRunning, tick]);

    // Load ticketing display settings
    useEffect(() => {
        const loadDisplaySettings = async () => {
            try {
                const settings = await getTicketingDisplaySettings();
                if (settings?.dateTimeFormat) {
                    setDateTimeFormat(settings.dateTimeFormat);
                }
                setResponseStateTrackingEnabled(settings?.responseStateTrackingEnabled ?? true);
            } catch (error) {
                console.error('Failed to load ticketing display settings:', error);
            }
        };
        loadDisplaySettings();
    }, []);

    // Calculate relative time strings only on client side to avoid hydration mismatch
    useEffect(() => {
        const tz = getUserTimeZone();
        
        if (ticket.entered_at) {
            const localDate = utcToLocal(ticket.entered_at, tz);
            const formattedDate = formatDateTime(localDate, tz, dateTimeFormat);
            const distance = formatDistanceToNow(new Date(ticket.entered_at));
            setCreatedRelativeTime(`${formattedDate} (${distance} ago)`);
        }
        
        if (ticket.updated_at) {
            const localDate = utcToLocal(ticket.updated_at, tz);
            const formattedDate = formatDateTime(localDate, tz, dateTimeFormat);
            const distance = formatDistanceToNow(new Date(ticket.updated_at));
            setUpdatedRelativeTime(`${formattedDate} (${distance} ago)`);
        }
    }, [ticket.entered_at, ticket.updated_at, dateTimeFormat]);

    // Fetch tags when component mounts
    useEffect(() => {
        const fetchTags = async () => {
            if (!ticket.ticket_id) return;
            
            try {
                const ticketTags = await findTagsByEntityId(ticket.ticket_id, 'ticket');
                setTags(ticketTags);
            } catch (error) {
                console.error('Error fetching tags:', error);
            }
        };
        fetchTags();
    }, [ticket.ticket_id]);

    // Add automatic interval tracking using the custom hook
    // Unique holder ID per tab for lock ownership
    const [holderId] = useState<string>(() => {
        if (typeof window !== 'undefined') {
            const existing = sessionStorage.getItem('tabHolderId');
            if (existing) return existing;
            const id = generateUUID();
            sessionStorage.setItem('tabHolderId', id);
            return id;
        }
        return Math.random().toString(36).slice(2);
    });

    const {
        isTracking,
        currentIntervalId,
        isLockedByOther,
        startTracking,
        stopTracking,
        refreshLockState,
    } = useTicketTimeTracking(
        initialTicket.ticket_id || '',
        initialTicket.ticket_number || '',
        initialTicket.title || '',
        userId || '',
        { autoStart: false, holderId }
    );

    // Stabilize startTracking for effects to avoid repeated auto-attempts due to function identity changes
    const startTrackingRef = React.useRef(startTracking);
    useEffect(() => { startTrackingRef.current = startTracking; }, [startTracking]);

    // Reflect tracking state into local stopwatch state
    useEffect(() => {
        console.log('[TicketDetails] isTracking changed ->', isTracking);
        setIsRunning(!!isTracking);
    }, [isTracking]);

    // Proactive auto-start on mount when userId/ticketId ready (no dialog on lock)
    const autoStartedRef = React.useRef(false);
    useEffect(() => {
        const auto = async () => {
            if (autoStartedRef.current) return;
            if (!initialTicket.ticket_id || !userId) return;
            if (isTracking) return;
            if (!isLiveTicketTimerEnabled) return;
            console.log('[TicketDetails] auto-start attempt');
            try {
                const started = await startTrackingRef.current(false);
                console.log('[TicketDetails] auto-start result ->', started);
                if (started) {
                    setElapsedTime(0);
                    autoStartedRef.current = true;
                }
            } catch (e) {
                // Ignore auto-start failures; time tracking is best-effort here.
            }
        };
        auto();
        // only attempt once when ids are ready and not already tracking
    }, [initialTicket.ticket_id, userId, isTracking, isLiveTicketTimerEnabled]);

    // New screens start from zero; no seeding from existing intervals

    useEffect(() => {
        const disableLiveTimerInView = async () => {
            if (isLiveTicketTimerEnabled) {
                return;
            }

            try {
                await stopTracking();
            } catch {
                // Ignore stop errors while enforcing board policy.
            }

            setIsRunning(false);
            setElapsedTime(0);
        };

        disableLiveTimerInView();
    }, [isLiveTicketTimerEnabled, stopTracking]);

    // Poll lock state periodically to update UI lock indicator
    useEffect(() => {
        let id: any;
        const poll = async () => {
            try { await refreshLockState(); } catch {}
        };
        id = setInterval(poll, 5000);
        poll();
        return () => clearInterval(id);
    }, [refreshLockState]);
    
    // Function to close the current interval before navigation
    // Enhanced function to close the interval - will find and close any open interval for this ticket
    const closeCurrentInterval = useCallback(async () => {
        try {
            // If we have a currentIntervalId, use it
            if (currentIntervalId) {
                console.debug('Closing known interval before navigation:', currentIntervalId);
                await intervalService.endInterval(currentIntervalId);
                return;
            }
            
            // If currentIntervalId is null, try to find any open interval for this ticket
            console.debug('No currentIntervalId available, checking for open intervals');
            if (userId && initialTicket.ticket_id) {
                const openInterval = await intervalService.getOpenInterval(initialTicket.ticket_id, userId);
                if (openInterval) {
                    console.debug('Found open interval to close:', openInterval.id);
                    await intervalService.endInterval(openInterval.id);
                } else {
                    console.debug('No open intervals found for this ticket');
                }
            }
        } catch (error: any) {
            console.error('Error closing interval:', error);
        }
    }, [currentIntervalId, intervalService, userId, initialTicket.ticket_id]);
    
    // Fixed navigation function - wait for interval to close before navigating
    const handleBackToTickets = useCallback(async () => {
        try {
            // Stop tracking and release lock before leaving
            await stopTracking();
            // Wait for the interval to close
            await closeCurrentInterval();
            
            // Navigate after interval is closed
            if (onClose) {
                onClose();
            } else {
                // Use proper routing to tickets dashboard instead of router.back()
                router.push('/msp/tickets');
            }
        } catch (error) {
            console.error('Error closing interval before navigation:', error);
            // Navigate anyway to prevent user from being stuck
            if (onClose) {
                onClose();
            } else {
                // Use proper routing to tickets dashboard instead of router.back()
                router.push('/msp/tickets');
            }
        }
    }, [closeCurrentInterval, onClose, router, stopTracking]);

    // Handle timer control actions with locking
    const [isReplaceDialogOpen, setIsReplaceDialogOpen] = useState(false);

    const doStart = useCallback(async (force = false) => {
        if (!initialTicket.ticket_id || !userId) return;
        try {
            const started = await startTracking(force);
            if (started) {
                setElapsedTime(0);
                setIsRunning(true);
            } else if (!force) {
                // Locked elsewhere
                setIsReplaceDialogOpen(true);
            }
        } catch (e) {
            console.error('Failed to start tracking:', e);
        }
    }, [initialTicket.ticket_id, userId, startTracking]);

    const handleStartClick = useCallback(() => {
        doStart(false);
    }, [doStart]);

    const handleConfirmReplace = useCallback(async () => {
        setIsReplaceDialogOpen(false);
        await doStart(true);
    }, [doStart]);

    const handlePauseClick = useCallback(async () => {
        try {
            await stopTracking();
        } catch {}
        setIsRunning(false);
    }, [stopTracking]);

    const handleStopClick = useCallback(async () => {
        try {
            await stopTracking();
        } catch {}
        setIsRunning(false);
        setElapsedTime(0);
    }, [stopTracking]);

    // Ensure we stop tracking only when component unmounts (not on re-renders)
    const stopTrackingRef = React.useRef(stopTracking);
    useEffect(() => {
        stopTrackingRef.current = stopTracking;
    }, [stopTracking]);
    useEffect(() => {
        return () => {
            stopTrackingRef.current?.().catch(() => {});
        };
    }, []);

    const handleClientClick = async () => {
        if (!client?.client_id) return;

        openDrawer(<div className="p-4 text-sm text-gray-600">Loading…</div>, undefined, undefined, '900px');
        try {
            const fullClient = await getClientById(client.client_id);
            if (!fullClient) {
                replaceDrawer(<div className="p-4 text-sm text-gray-600">Client not found.</div>);
                return;
            }

            replaceDrawer(
                renderClientDetails
                    ? renderClientDetails({
                        id: `${id}-client-details`,
                        client: fullClient
                    })
                    : (
                        <div className="p-4 space-y-3">
                            <div className="text-lg font-semibold">{fullClient.client_name}</div>
                            <Button
                                id="ticket-details-open-client"
                                type="button"
                                variant="outline"
                                onClick={() => window.open(`/msp/clients/${fullClient.client_id}`, '_blank', 'noopener,noreferrer')}
                            >
                                Open Client <ExternalLink className="ml-2 h-4 w-4" />
                            </Button>
                        </div>
                    ),
                undefined,
                '900px'
            );
        } catch (e) {
            const message = e instanceof Error ? e.message : 'Failed to load client.';
            replaceDrawer(<div className="p-4 text-sm text-red-600">{message}</div>);
        }
    };

    const handleContactClick = async () => {
        const contactNameId = ticket.contact_name_id || contactInfo?.contact_name_id;
        if (!contactNameId) {
            openDrawer(<div className="text-sm text-gray-600">No contact selected.</div>);
            return;
        }

        openDrawer(<div className="p-4 text-sm text-gray-600">Loading…</div>, undefined, undefined, '900px');
        try {
            const contact = await getContactByContactNameId(contactNameId);
            if (!contact) {
                replaceDrawer(<div className="p-4 text-sm text-gray-600">Contact not found.</div>);
                return;
            }

            const minimalClients =
                clients.length > 0 ? clients : client ? [client] : [];

            replaceDrawer(
                renderContactDetails
                    ? renderContactDetails({
                        id: `${id}-contact-details`,
                        contact,
                        clients: minimalClients,
                        userId
                    })
                    : (
                        <div className="p-4 space-y-3">
                            <div className="text-lg font-semibold">
                                {contact.full_name || 'Contact'}
                            </div>
                            <Button
                                id="ticket-details-open-contact"
                                type="button"
                                variant="outline"
                                onClick={() => window.open(`/msp/contacts/${contact.contact_name_id}`, '_blank', 'noopener,noreferrer')}
                            >
                                Open Contact <ExternalLink className="ml-2 h-4 w-4" />
                            </Button>
                        </div>
                    ),
                undefined,
                '900px'
            );
        } catch (e) {
            const message = e instanceof Error ? e.message : 'Failed to load contact.';
            replaceDrawer(<div className="p-4 text-sm text-red-600">{message}</div>);
        }
    };

  const handleAgentClick = (userId: string) => {
    if (disableAgentSchedule) {
      return;
    }

    openDrawer(
      <AgentScheduleDrawer
        agentId={userId}
      />
    );
  };

    const handleAddAgent = async (userId: string) => {
        try {
            const result = await addTicketResource(ticket.ticket_id!, userId, 'support');

            if (result) {
                setAdditionalAgents(prev => [...prev, result]);
                toast.success(t('messages.agentAdded'));
            } else {
                setTicket(prevTicket => ({
                    ...prevTicket,
                    assigned_to: userId
                }));
                toast.success(t('messages.agentAssigned'));
            }
        } catch (error) {
            handleError(error, t('messages.addAgentFailed'));
        }
    };  
    
    const handleRemoveAgent = async (assignmentId: string) => {
        try {
            await removeTicketResource(assignmentId);
            setAdditionalAgents(prev => prev.filter(agent => agent.assignment_id !== assignmentId));
            toast.success(t('messages.agentRemoved'));
        } catch (error) {
            handleError(error, t('messages.removeAgentFailed'));
        }
    };

    const handleSelectChange = async (field: keyof ITicket, newValue: string | null) => {
        const normalizedValue =
            field === 'assigned_to'
                ? (newValue && newValue !== 'unassigned' ? newValue : null)
                : newValue;

        // Pre-close check: when this status change would close the ticket,
        // surface unmet close rules in a dialog instead of submitting a write
        // that the server would reject. The write paths re-enforce, so this is
        // UX only — never trust it for correctness.
        if (field === 'status_id' && normalizedValue && ticket.ticket_id) {
            try {
                const check = await checkTicketClosure(ticket.ticket_id, normalizedValue);
                if (check.wouldClose && !check.allowed) {
                    setCloseOverrideReason('');
                    setCloseBlockedDialog({
                        isOpen: true,
                        statusId: normalizedValue,
                        failures: check.failures,
                        canOverride: check.canOverride,
                    });
                    return;
                }
            } catch (checkError) {
                // Fall through to the normal update; the server still enforces.
                console.error('Close rules pre-check failed:', checkError);
            }
        }

        // Store the previous value before updating
        const previousValue = ticket[field];

        // Optimistically update the UI
        setTicket(prevTicket => ({ ...prevTicket, [field]: normalizedValue }));

        try {
            await runWithPendingLiveFields([field], async () => {
                // Use the optimized handler if provided
                if (onTicketUpdate) {
                    await onTicketUpdate(field, normalizedValue);
                    if (field === 'board_id') {
                        setSavedBoardId(normalizedValue);
                    }

                    // If we're changing the assigned_to field, we need to handle additional resources
                    // This will be handled by the container component and passed back in props
                } else {
                    // Fallback to the original implementation if no optimized handler is provided
                    const result = await updateTicket(ticket.ticket_id || '', { [field]: normalizedValue });

                    if (result === 'success') {
                        console.log(`${field} changed to: ${normalizedValue}`);
                        if (field === 'board_id') {
                            setSavedBoardId(normalizedValue);
                        }

                        // If we're changing the assigned_to field, refresh the additional resources
                        if (field === 'assigned_to') {
                            try {
                                // Refresh the additional resources
                                const resources = await getTicketResources(ticket.ticket_id!);
                                setAdditionalAgents(resources);
                                console.log('Additional resources refreshed after assignment change');
                            } catch (resourceError) {
                                console.error('Error refreshing additional resources:', resourceError);
                            }
                        }
                    } else {
                        console.error(`Failed to update ticket ${field}`);
                        // Revert to previous value on failure
                        setTicket(prevTicket => ({ ...prevTicket, [field]: previousValue }));
                    }
                }
            });
        } catch (error) {
            console.error(`Error updating ticket ${field}:`, error);
            // Revert to previous value on error
            setTicket(prevTicket => ({ ...prevTicket, [field]: previousValue }));
        }
    };

    const handleAssignTeam = useCallback(async (teamId: string) => {
        // Optimistically update UI before server call
        const previousTicket = ticket;
        const previousTeam = team;
        const previousAgents = additionalAgents;

        const teamDetails = teams.find(t => t.team_id === teamId) || null;
        const assignedTo = ticket.assigned_to || teamDetails?.manager_id || ticket.assigned_to;

        setTicket(prevTicket => ({
            ...prevTicket,
            assigned_team_id: teamId,
            assigned_to: assignedTo
        }));
        if (teamDetails) {
            setTeam(teamDetails);
        }

        try {
            await assignTeamToTicket(ticket.ticket_id || '', teamId);

            // If we didn't have team details from local state, fetch them
            if (!teamDetails) {
                const fetchedTeam = await getTeamById(teamId);
                setTeam(fetchedTeam || null);
            }

            if (ticket.ticket_id) {
                const resources = await getTicketResources(ticket.ticket_id);
                setAdditionalAgents(resources);
            }

            toast.success(t('messages.teamAssignSuccess'));
        } catch (error) {
            console.error('Error assigning team:', error);
            // Revert on failure
            setTicket(previousTicket);
            setTeam(previousTeam);
            setAdditionalAgents(previousAgents);
            toast.error(t('messages.teamAssignFailed'));
        }
    }, [ticket, team, additionalAgents, teams]);

    const handleRemoveTeamAssignment = useCallback(async (
        mode: 'remove_all' | 'keep_all' | 'selective',
        keepUserIds?: string[]
    ) => {
        try {
            await removeTeamFromTicket(ticket.ticket_id || '', { mode, keepUserIds });

            setTicket(prevTicket => ({
                ...prevTicket,
                assigned_team_id: null
            }));
            setTeam(null);

            if (ticket.ticket_id) {
                const resources = await getTicketResources(ticket.ticket_id);
                setAdditionalAgents(resources);
            }

            toast.success(t('messages.teamRemoveSuccess'));
        } catch (error) {
            console.error('Error removing team assignment:', error);
            toast.error(t('messages.teamRemoveFailed'));
        }
    }, [ticket.ticket_id]);

    const [editorKey, setEditorKey] = useState(0);
    const refreshTicketDocuments = useCallback(async () => {
        if (!ticket.ticket_id) return;

        try {
            const docs = await getDocumentByTicketId(ticket.ticket_id);
            if (isActionPermissionError(docs)) {
                console.warn('Permission denied while refreshing ticket documents', {
                    ticketId: ticket.ticket_id,
                    reason: docs.permissionError,
                });
                return;
            }
            setDocuments(docs || []);
        } catch (error) {
            console.error('Failed to refresh ticket documents:', error);
        }
    }, [ticket.ticket_id]);

    const resetCommentDeleteState = useCallback(() => {
        setIsDeleteDialogOpen(false);
        setCommentToDelete(null);
        setIsDeletingComment(false);
    }, []);

    const handleAddNewComment = async (
        isInternal: boolean,
        isResolution: boolean,
        closeStatusId: string | null = null
    ): Promise<boolean> => {
        // Check if content is empty
        const contentStr = JSON.stringify(newCommentContent);
        const hasContent = contentStr !== JSON.stringify([{
            type: "paragraph",
            props: {
                textAlignment: "left",
                backgroundColor: "default",
                textColor: "default"
            },
            content: [{
                type: "text",
                text: "",
                styles: {}
            }]
        }]);

        if (!hasContent) {
            console.log("Cannot add empty comment");
            return false;
        }
    
        try {
            if (!userId) {
                console.error("No valid user ID found");
                return false;
            }
            
            // Use the centralized utility to convert BlockNote content to markdown
            const markdownContent = await convertBlockNoteToMarkdown(newCommentContent);
            console.log("Converted markdown content:", markdownContent);
    
            // The resolution toggle is paired with an immediate close when a
            // close-status is selected and the ticket isn't already in that
            // status. Surface that intent to the server so the email
            // subscriber can suppress the duplicate comment email — the
            // close email will carry the resolution body.
            const willCloseTicket = Boolean(
                isResolution && closeStatusId && ticket.status_id !== closeStatusId
            );

            // Use the optimized handler if provided
            if (onAddComment) {
                await onAddComment(
                    JSON.stringify(newCommentContent),
                    isInternal,
                    isResolution,
                    willCloseTicket
                );

                // Optimistically update the response state in UI to match server behavior:
                // - Internal note: no change
                // - Client-visible comment from internal user (MSP portal): awaiting client
                if (!isInternal && responseStateTrackingEnabled) {
                    setTicket((prev: any) => ({
                        ...prev,
                        response_state: 'awaiting_client'
                    }));
                }

                // Refresh comments to ensure immediate UI update
                if (ticket.ticket_id) {
                    try {
                        const updatedComments = await findCommentsByTicketId(ticket.ticket_id);
                        setConversations(updatedComments);
                    } catch (e) {
                        console.error('Failed to refresh comments after add:', e);
                    }
                }

                // If this was a resolution note and a closed status was selected, close the ticket.
                if (isResolution && closeStatusId && ticket.status_id !== closeStatusId) {
                    // Backend clears response_state when closing; keep UI consistent.
                    setTicket((prev: any) => ({ ...prev, response_state: null }));
                    await handleSelectChange('status_id', closeStatusId);
                }
                
                // Reset the comment input
                setNewCommentContent([{
                    type: "paragraph",
                    props: {
                        textAlignment: "left",
                        backgroundColor: "default",
                        textColor: "default"
                    },
                    content: [{
                        type: "text",
                        text: "",
                        styles: {}
                    }]
                }]);
                
                return true;
            } else {
                // Use the regular createComment action for MSP portal
                if (ticket.ticket_id && userId) {
                    // Call the regular comment creation action
                    const newComment = await createComment({
                        ticket_id: ticket.ticket_id,
                        note: JSON.stringify(newCommentContent),
                        is_internal: isInternal,
                        is_resolution: isResolution,
                        user_id: userId,
                        author_type: 'internal', // Will be overridden based on user type in the action
                        // See email-subscriber suppression note above.
                        ...(willCloseTicket ? { metadata: { closes_ticket: true } } : {})
                    });
                    
                    if (newComment) {
                        // Refresh comments after adding
                        const updatedComments = await findCommentsByTicketId(ticket.ticket_id);
                        setConversations(updatedComments);

                        if (isResolution && closeStatusId && ticket.status_id !== closeStatusId) {
                            setTicket((prev: any) => ({ ...prev, response_state: null }));
                            await handleSelectChange('status_id', closeStatusId);
                        }
                        
                        // Reset the comment input
                        setNewCommentContent([{
                            type: "paragraph",
                            props: {
                                textAlignment: "left",
                                backgroundColor: "default",
                                textColor: "default"
                            },
                            content: [{
                                type: "text",
                                text: "",
                                styles: {}
                            }]
                        }]);
                        console.log("New note added successfully");
                        return true;
                    } else {
                        console.error('Failed to add comment');
                        return false;
                    }
                } else {
                    console.error('Ticket ID is missing');
                    return false;
                }
            }
        } catch (error) {
            console.error("Error adding new note:", error);
            return false;
        }
    };

    const handleAddReplyComment = async (
        content: PartialBlock[],
        parentCommentId: string,
        isInternal: boolean
    ): Promise<boolean> => {
        const contentStr = JSON.stringify(content);
        const hasContent = contentStr !== JSON.stringify([{
            type: "paragraph",
            props: {
                textAlignment: "left",
                backgroundColor: "default",
                textColor: "default"
            },
            content: [{
                type: "text",
                text: "",
                styles: {}
            }]
        }]);

        if (!hasContent || !ticket.ticket_id || !userId) {
            return false;
        }

        try {
            await createComment({
                ticket_id: ticket.ticket_id,
                note: contentStr,
                is_internal: isInternal,
                is_resolution: false,
                user_id: userId,
                author_type: 'internal',
                parent_comment_id: parentCommentId
            });

            const updatedComments = await findCommentsByTicketId(ticket.ticket_id);
            setConversations(updatedComments);

            if (!isInternal && responseStateTrackingEnabled) {
                setTicket((prev: any) => ({
                    ...prev,
                    response_state: 'awaiting_client'
                }));
            }

            return true;
        } catch (error) {
            handleError(error, t('messages.addCommentFailed', 'Failed to add comment'));
            return false;
        }
    };
    
    const handleEdit = (conversation: IComment) => {
        // Only allow users to edit their own comments
        if (userId === conversation.user_id) {
            setIsEditing(true);
            setCurrentComment(conversation);
        } else {
            toast.error(t('messages.editOwnCommentOnly'));
        }
    };

    const handleSave = async (updates: Partial<IComment>) => {
        if (!currentComment) return;

        try {
            // Extract plain text from the content for markdown
            const extractPlainText = (noteStr: string): string => {
                try {
                    const blocks = JSON.parse(noteStr);
                    return blocks.map((block: any) => {
                        if (!block.content) return '';
                        
                        if (Array.isArray(block.content)) {
                            return block.content
                                .filter((item: any) => item && item.type === 'text')
                                .map((item: any) => item.text || '')
                                .join('');
                        }
                        
                        if (typeof block.content === 'string') {
                            return block.content;
                        }
                        
                        return '';
                    }).filter((text: string) => text.trim() !== '').join('\n\n');
                } catch (e) {
                    console.error("Error parsing note JSON:", e);
                    return noteStr || "";
                }
            };
            
            // Extract markdown content directly if note is being updated
            if (updates.note) {
                const markdownContent = extractPlainText(updates.note);
                console.log("Extracted markdown content for update:", markdownContent);
                updates.markdown_content = markdownContent;
            }

            await updateComment(currentComment.comment_id!, updates);

            const updatedCommentData = await findCommentById(currentComment.comment_id!);
            if (updatedCommentData) {
                setConversations(prevConversations =>
                    prevConversations.map((conv):IComment =>
                        conv.comment_id === updatedCommentData.comment_id ? updatedCommentData : conv
                    )
                );
            }

            setIsEditing(false);
            setCurrentComment(null);
        } catch (error) {
            handleError(error, t('messages.saveCommentFailed'));
        }
    };
const handleClose = () => {
    setIsEditing(false);
    setCurrentComment(null);
};



    // This function is no longer used directly - we use handleDeleteRequest instead
    // Keeping it for backward compatibility with other components that might use it
    const handleDelete = async (comment: IComment) => {
        if (!comment.comment_id) return;
        
        try {
            await deleteComment(comment.comment_id);
            setConversations(prevConversations =>
                prevConversations.filter(conv => conv.comment_id !== comment.comment_id)
            );
        } catch (error) {
            console.error("Error deleting comment:", error);
        }
    };

    const handleContentChange = useCallback((_blocks: PartialBlock[]) => {
        // Edit-mode comment content is owned by CommentItem local state and
        // persisted through handleSave. Avoid mutating currentComment on each
        // keystroke to prevent unnecessary TicketDetails rerenders.
    }, []);

    const handleUpdateDescription = async (content: string) => {
        try {
            // Use the optimized handler if provided
            if (onUpdateDescription) {
                const success = await onUpdateDescription(content);
                
                if (success) {
                    // Update the local ticket state
                    const currentAttributes = ticket.attributes || {};
                    const updatedAttributes = {
                        ...currentAttributes,
                        description: content
                    };
                    
                    setTicket(prev => ({
                        ...prev,
                        attributes: updatedAttributes,
                        updated_at: new Date().toISOString()
                    }));
                }
                
                return success;
            } else {
                // Fallback to the original implementation
                if (!ticket.ticket_id) {
                    console.error('Ticket ID is missing');
                    return false;
                }

                // Update the ticket's attributes.description field
                const currentAttributes = ticket.attributes || {};
                const updatedAttributes = {
                    ...currentAttributes,
                    description: content
                };

                // Update the ticket
                await updateTicket(ticket.ticket_id, {
                    attributes: updatedAttributes,
                    updated_at: new Date().toISOString()
                });

                // Update the local ticket state
                setTicket(prev => ({
                    ...prev,
                    attributes: updatedAttributes,
                    updated_at: new Date().toISOString()
                }));


                toast.success(t('messages.descriptionUpdated'));
                return true;
            }
        } catch (error) {
            handleError(error, t('messages.updateDescriptionFailed'));
            return false;
        }
    };

    const handleAddTimeEntry = async () => {
        try {
            if (!ticket.ticket_id) {
                toast.error(t('messages.ticketIdMissing'));
                return;
            }

            const baseOnComplete = createTicketTimeEntryOnComplete({
                stopTracking,
                setElapsedTime,
                setIsRunning,
            });
            await launchTimeEntry({
                openDrawer,
                closeDrawer,
                context: buildTicketTimeEntryContext({
                    ticket,
                    clientName: client?.client_name ?? null,
                    elapsedTime,
                    timeDescription,
                    masterTicketNumber: bundle?.masterTicket?.ticket_number ?? null,
                }),
                onComplete: () => {
                    baseOnComplete();
                    setTimeEntriesRefreshKey((value) => value + 1);
                },
            });
        } catch (error) {
            handleError(error, t('messages.prepareTimeEntryFailed'));
        }
    };

    const handleEditTimeEntry = async (entry: { entry_id: string }) => {
        try {
            if (!ticket.ticket_id) {
                toast.error(t('messages.ticketIdMissing'));
                return;
            }

            await launchTimeEntry({
                openDrawer,
                closeDrawer,
                context: buildTicketTimeEntryContext({
                    ticket,
                    clientName: client?.client_name ?? null,
                    elapsedTime: 0,
                    timeDescription: '',
                    masterTicketNumber: bundle?.masterTicket?.ticket_number ?? null,
                }),
                existingEntryId: entry.entry_id,
                onComplete: () => {
                    setTimeEntriesRefreshKey((value) => value + 1);
                },
            });
        } catch (error) {
            handleError(error, t('messages.prepareTimeEntryFailed'));
        }
    };

    const handleRequestDeleteTimeEntry = (entry: { entry_id: string; user_name: string | null }) => {
        setPendingDeleteTimeEntry(entry);
    };

    useCatalogShortcut('record.addTime', () => { void handleAddTimeEntry(); });

    const handleConfirmDeleteTimeEntry = async () => {
        if (!pendingDeleteTimeEntry) return;
        setIsDeletingTimeEntry(true);
        try {
            await deleteTimeEntry(pendingDeleteTimeEntry.entry_id);
            toast.success(t('messages.timeEntryDeleted', { defaultValue: 'Time entry deleted' }));
            setTimeEntriesRefreshKey((value) => value + 1);
        } catch (error) {
            handleError(error, t('messages.deleteTimeEntryFailed', { defaultValue: 'Failed to delete time entry' }));
        } finally {
            setIsDeletingTimeEntry(false);
            setPendingDeleteTimeEntry(null);
        }
    };

    const handleUpdateWatchList = async (watchList: TicketWatchListEntry[]): Promise<boolean> => {
        if (!ticket.ticket_id || isWatchListSaving) {
            return false;
        }

        setIsWatchListSaving(true);
        try {
            const updatedAttributes = setTicketWatchListOnAttributes(ticket.attributes, watchList);
            await updateTicketWithCache(ticket.ticket_id, {
                attributes: updatedAttributes ?? null,
            });

            setTicket((prevTicket) => ({
                ...prevTicket,
                attributes: updatedAttributes ?? null,
                updated_at: new Date().toISOString(),
            }));
            return true;
        } catch (error) {
            console.error('Error updating watch list:', error);
            toast.error(t('messages.watchListUpdateFailed'));
            return false;
        } finally {
            setIsWatchListSaving(false);
        }
    };

    const handleLoadAllContactsForWatchList = useCallback(async () => {
        if (allContactsForWatchListLoading || allContactsForWatchList.length > 0) {
            return;
        }

        setAllContactsForWatchListLoading(true);
        try {
            const allContacts = await getAllActiveContacts('asc');
            setAllContactsForWatchList(allContacts || []);
        } catch (error) {
            console.error('Error loading all contacts for watch list:', error);
            toast.error(t('messages.loadAllContactsFailed'));
        } finally {
            setAllContactsForWatchListLoading(false);
        }
    }, [allContactsForWatchList.length, allContactsForWatchListLoading]);

    const handleChangeContact = () => {
        setIsChangeContactDialogOpen(true);
    };

    const handleChangeClient = () => {
        setIsChangeClientDialogOpen(true);
    };

    const handleTagsChange = (updatedTags: ITag[]) => {
        setTags(updatedTags);
    };

    const handleContactChange = async (newContactId: string | null) => {
        try {
            await runWithPendingLiveFields(['contact_name_id'], () => updateTicket(ticket.ticket_id!, { contact_name_id: newContactId }));
            
            if (newContactId) {
                const contactData = await getContactByContactNameId(newContactId);
                setContactInfo(contactData);
            } else {
                setContactInfo(null);
            }

            setIsChangeContactDialogOpen(false);
            toast.success(t('messages.contactUpdated'));
        } catch (error) {
            handleError(error, t('messages.updateContactFailed'));
        }
    };

    const handleItilFieldChange = async (field: string, value: any) => {
        try {
            // First update local state immediately for UI responsiveness
            switch (field) {
                case 'itil_impact':
                    setItilImpact(value);
                    break;
                case 'itil_urgency':
                    setItilUrgency(value);
                    break;
                // NOTE: itil_category and itil_subcategory are now handled by unified CategoryPicker
            }

            // Create update object with the specific ITIL field
            const updateData: any = {};
            updateData[field] = value;

            // If we're updating impact or urgency, calculate the new ITIL priority
            if (field === 'itil_impact' || field === 'itil_urgency') {
                const currentImpact = field === 'itil_impact' ? value : itilImpact;
                const currentUrgency = field === 'itil_urgency' ? value : itilUrgency;

                // NOTE: Priority mapping is now handled in the backend
                // The backend will calculate and map ITIL priority to the correct priority_id
            }

            // NOTE: Category management is now unified through the CategoryPicker

            await runWithPendingLiveFields([field], () => updateTicketWithCache(ticket.ticket_id!, updateData));

            // Update local ticket state to reflect the change
            setTicket(prevTicket => ({
                ...prevTicket,
                ...updateData
            }));

            if (field === 'itil_impact') {
                toast.success(t('messages.itilImpactUpdated'));
            } else if (field === 'itil_urgency') {
                toast.success(t('messages.itilUrgencyUpdated'));
            }
        } catch (error) {
            if (field === 'itil_urgency') {
                handleError(error, t('messages.itilUrgencyUpdateFailed'));
            } else {
                handleError(error, t('messages.itilImpactUpdateFailed'));
            }
        }
    };

    // Handler for batch save changes from TicketInfo
    const handleBatchSaveChanges = useCallback(async (changes: Record<string, unknown>): Promise<boolean> => {
        // If we have a batch handler from container, use it
        if (onBatchTicketUpdate) {
            const success = await runWithPendingLiveFields(Object.keys(changes), () => onBatchTicketUpdate(changes));
            if (success) {
                // Update local ticket state with the saved changes
                setTicket(prevTicket => ({
                    ...prevTicket,
                    ...changes,
                    updated_at: new Date().toISOString()
                }));
            }
            return success;
        }

        // Fallback: save each change individually
        try {
            for (const [field, value] of Object.entries(changes)) {
                if (field === 'itil_impact' || field === 'itil_urgency') {
                    await handleItilFieldChange(field, value);
                } else {
                    await handleSelectChange(field as keyof ITicket, value as string | null);
                }
            }
            return true;
        } catch (error) {
            console.error('Error in batch save:', error);
            return false;
        }
    }, [handleItilFieldChange, onBatchTicketUpdate, runWithPendingLiveFields]);

    const handleClientChange = async (newClientId: string) => {
        try {
            await runWithPendingLiveFields(['client_id', 'contact_name_id', 'location_id'], () => updateTicket(ticket.ticket_id!, {
                client_id: newClientId,
                contact_name_id: null, // Reset contact when client changes
                location_id: null // Reset location when client changes
            }));
            
            const [clientData, contactsData, locationData] = await Promise.all([
                getClientById(newClientId),
                getContactsByClient(newClientId),
                getClientLocations(newClientId),
            ]);
            
            setClient(clientData);
            setContacts(contactsData || []);
            setLocations(locationData || []);
            setContactInfo(null); // Reset contact info

            setIsChangeClientDialogOpen(false);
            toast.success(t('messages.clientUpdated'));
        } catch (error) {
            handleError(error, t('messages.updateClientFailed'));
        }
    };
    
    const handleLocationChange = async (newLocationId: string | null) => {
        try {
            await runWithPendingLiveFields(['location_id'], () => updateTicket(ticket.ticket_id!, {
                location_id: newLocationId
            }));
            
            // Update the ticket state with the new location
            setTicket(prevTicket => ({
                ...prevTicket,
                location_id: newLocationId,
                location: newLocationId ? locations.find(l => l.location_id === newLocationId) : undefined
            }));

            toast.success(t('messages.locationUpdated'));
        } catch (error) {
            handleError(error, t('messages.updateLocationFailed'));
        }
    };

    const handleDeleteRequest = (conversation: IComment) => {
        // Only allow users to delete their own comments
        if (userId === conversation.user_id) {
            setCommentToDelete({
                commentId: conversation.comment_id!,
                imageDocuments: resolveCommentReferencedImageDocuments(conversation.note, documents),
            });
            setIsDeleteDialogOpen(true);
        } else {
            toast.error(t('messages.deleteOwnCommentError'));
        }
    };

    const handleDeleteConfirm = async (deleteImages: boolean) => {
        if (!commentToDelete) return;

        setIsDeletingComment(true);
        try {
            await deleteComment(commentToDelete.commentId);
            setConversations(prevConversations =>
                prevConversations.filter(conv => conv.comment_id !== commentToDelete.commentId)
            );

            if (deleteImages && commentToDelete.imageDocuments.length > 0 && ticket.ticket_id) {
                try {
                    const result = await deleteDraftClipboardImages({
                        ticketId: ticket.ticket_id,
                        documentIds: commentToDelete.imageDocuments.map((document) => document.documentId),
                        deleteDocumentFn: deleteDocument,
                    });

                    const deletedCount = result.deletedDocumentIds.length;
                    const failedCount = result.failures.length;

                    if (deletedCount > 0) {
                        await refreshTicketDocuments();
                    }

                    if (failedCount > 0) {
                        toast.error(t('messages.pastedImageDeleteFailed', { count: failedCount }));
                    } else if (deletedCount > 0) {
                        toast.success(t('messages.commentWithImagesDeleted', { count: deletedCount }));
                    } else {
                        toast.success(t('messages.commentDeleteSuccess'));
                    }
                } catch (imageDeleteError) {
                    console.error('Failed to delete pasted images during comment deletion:', imageDeleteError);
                    toast.error(t('messages.pastedImagesDeleteFailed'));
                }
            } else {
                toast.success(t('messages.commentDeleteSuccess'));
            }
        } catch (error) {
            handleError(error, t('messages.deleteCommentFailed'));
        } finally {
            resetCommentDeleteState();
        }
    };

    const deleteDialogImageCount = commentToDelete?.imageDocuments.length ?? 0;
    const deleteDialogHasImages = deleteDialogImageCount > 0;
    const deleteDialogMessage = deleteDialogHasImages
        ? t('messages.deleteCommentWithImages', {
            defaultValue: 'This comment includes {{count}} pasted images that were uploaded as ticket documents. Delete the comment only, or also delete the pasted images permanently?',
            count: deleteDialogImageCount,
          })
        : t('messages.deleteCommentConfirm', 'Are you sure you want to delete this comment? This action cannot be undone.');

    // Function to open ticket in a new window
    const openTicketInNewWindow = useCallback(() => {
        if (ticket.ticket_id) {
            window.open(`/msp/tickets/${ticket.ticket_id}`, '_blank');
        }
    }, [ticket.ticket_id]);

    const handleRemoveChildFromBundle = useCallback(async (childTicketId: string) => {
        try {
            await removeChildFromBundleAction({ childTicketId });
            toast.success(t('messages.removedFromBundle'));
            router.refresh();
        } catch (error) {
            handleError(error, t('messages.removeFromBundleFailed'));
        }
    }, [router]);

    const handleUnbundleMaster = useCallback(async () => {
        if (!ticket.ticket_id) return;
        try {
            await unbundleMasterTicketAction({ masterTicketId: ticket.ticket_id });
            toast.success(t('messages.bundleRemoved'));
            router.refresh();
        } catch (error) {
            handleError(error, t('messages.unbundleFailed'));
        }
    }, [ticket.ticket_id, router]);

    const performAddChildToBundle = useCallback(async (childTicketId: string) => {
        if (!ticket.ticket_id) return;
        const result = await addChildrenToBundleAction({ masterTicketId: ticket.ticket_id, childTicketIds: [childTicketId] });
        if (isActionMessageError(result)) {
            toast.error(getErrorMessage(result));
            return;
        }
        toast.success(t('messages.addedToBundle'));
        setAddChildTicketNumber('');
        resetChildTicketPickerState();
        router.refresh();
    }, [ticket.ticket_id, router, resetChildTicketPickerState]);

    const handleAddChildToBundle = useCallback(async () => {
        if (!ticket.ticket_id) return;
        const normalized = addChildTicketNumber.trim();
        if (!normalized) return;

        // Check if we have a selected ticket from search results
        if (selectedChildTicket && selectedChildTicket.ticket_number === normalized) {
            // Use the selected ticket from search
            if (ticket.client_id && selectedChildTicket.client_id && selectedChildTicket.client_id !== ticket.client_id) {
                setPendingChildToAdd({
                    ticket_id: selectedChildTicket.ticket_id,
                    ticket_number: normalized,
                    client_id: selectedChildTicket.client_id
                });
                setIsAddChildMultiClientConfirmOpen(true);
                return;
            }
            await performAddChildToBundle(selectedChildTicket.ticket_id);
            return;
        }

        // Fallback: search by exact ticket number
        try {
            const found = await findTicketByNumberAction({ ticketNumber: normalized });
            if (!found) {
                toast.error(t('messages.ticketNotFound'));
                return;
            }
            if (found.ticket_id === ticket.ticket_id) {
                toast.error(t('messages.cannotAddMasterAsChild'));
                return;
            }
            if (found.master_ticket_id) {
                toast.error(t('messages.ticketAlreadyBundled'));
                return;
            }

            if (ticket.client_id && found.client_id && found.client_id !== ticket.client_id) {
                setPendingChildToAdd(found);
                setIsAddChildMultiClientConfirmOpen(true);
                return;
            }

            await performAddChildToBundle(found.ticket_id);
        } catch (error) {
            handleError(error, t('messages.addToBundleFailed'));
        }
    }, [ticket.ticket_id, ticket.client_id, addChildTicketNumber, selectedChildTicket, performAddChildToBundle]);

    const bundleHasMultipleClients = useMemo(() => {
        if (!bundle?.isBundleMaster || !Array.isArray(bundle.children)) return false;
        const ids = new Set<string>();
        if (ticket.client_id) ids.add(ticket.client_id);
        for (const child of bundle.children) {
            if (child?.client_id) ids.add(child.client_id);
        }
        return ids.size > 1;
    }, [bundle?.isBundleMaster, bundle?.children, ticket.client_id]);

    // Debounced search for eligible child tickets
    const performSearch = useCallback(async (query: string) => {
        const normalizedQuery = query.trim();
        if (!ticket.board_id || !normalizedQuery) {
            setSearchResults([]);
            return;
        }

        const searchSeq = ++childTicketSearchSeqRef.current;
        setIsSearching(true);
        try {
            const results = await searchEligibleChildTicketsAction({
                boardId: ticket.board_id,
                searchQuery: normalizedQuery,
                excludeTicketId: ticket.ticket_id,
                limit: 10
            });
            if (searchSeq !== childTicketSearchSeqRef.current) return;
            setSearchResults(results);
        } catch (error) {
            console.error('Failed to search tickets:', error);
            if (searchSeq !== childTicketSearchSeqRef.current) return;
            setSearchResults([]);
        } finally {
            if (searchSeq === childTicketSearchSeqRef.current) {
                setIsSearching(false);
            }
        }
    }, [ticket.board_id, ticket.ticket_id]);

    const debouncedSearch = useCallback((query: string) => {
        if (searchTimeoutRef.current) {
            clearTimeout(searchTimeoutRef.current);
            searchTimeoutRef.current = null;
        }

        if (!query.trim()) {
            cancelChildTicketSearch();
            setSearchResults([]);
            setShowSearchResults(false);
            return;
        }

        searchTimeoutRef.current = setTimeout(() => {
            performSearch(query);
        }, 300); // 300ms debounce
    }, [performSearch, cancelChildTicketSearch]);

    const handleSearchInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
        const value = e.target.value;
        setAddChildTicketNumber(value);
        // Clear selected ticket if input doesn't match the selected ticket number
        if (selectedChildTicket && value !== selectedChildTicket.ticket_number) {
            setSelectedChildTicket(null);
        }
        setShowSearchResults(true);
        debouncedSearch(value);
    }, [debouncedSearch, selectedChildTicket]);

    const handleSelectSearchResult = useCallback((selectedTicket: EligibleChildTicket) => {
        cancelChildTicketSearch();
        setAddChildTicketNumber(selectedTicket.ticket_number);
        setSelectedChildTicket({
            ticket_id: selectedTicket.ticket_id,
            client_id: selectedTicket.client_id,
            ticket_number: selectedTicket.ticket_number
        });
        setShowSearchResults(false);
        setSearchResults([]);
    }, [cancelChildTicketSearch]);

    // Handle click outside to close search results
    useEffect(() => {
        const handleClickOutside = (event: MouseEvent) => {
            if (searchContainerRef.current && !searchContainerRef.current.contains(event.target as Node)) {
                setShowSearchResults(false);
            }
        };

        document.addEventListener('mousedown', handleClickOutside);
        return () => {
            document.removeEventListener('mousedown', handleClickOutside);
        };
    }, []);

    // Cleanup timeout on unmount
    useEffect(() => {
        return () => {
            if (searchTimeoutRef.current) {
                clearTimeout(searchTimeoutRef.current);
            }
        };
    }, []);

    const handlePromoteChildToMaster = useCallback(async (childTicketId: string) => {
        if (!ticket.ticket_id) return;
        try {
            await promoteBundleMasterAction({ oldMasterTicketId: ticket.ticket_id, newMasterTicketId: childTicketId });
            toast.success(t('messages.promotedMaster'));
            router.push(`/msp/tickets/${childTicketId}`);
            router.refresh();
        } catch (error) {
            handleError(error, t('messages.promoteMasterFailed'));
        }
    }, [ticket.ticket_id, router]);

    const handleToggleBundleMode = useCallback(async () => {
        if (!ticket.ticket_id || !bundle?.isBundleMaster) return;
        try {
            setIsUpdatingBundleSettings(true);
            const nextMode = bundle.mode === 'link_only' ? 'sync_updates' : 'link_only';
            await updateBundleSettingsAction({ masterTicketId: ticket.ticket_id, mode: nextMode });
            toast.success(t('messages.bundleSettingsUpdated'));
            router.refresh();
        } catch (error) {
            handleError(error, t('messages.updateBundleSettingsFailed'));
        } finally {
            setIsUpdatingBundleSettings(false);
        }
    }, [ticket.ticket_id, bundle?.isBundleMaster, bundle?.mode, router]);

    if (!tenant) {
        return (
            <div id="ticket-error-message" className="p-4">
                Error: tenant is not defined
            </div>
        );
    }

    const livePresenceUsers = ticketLive.presence.map((user) => ({
        id: user.userId,
        name: user.displayName,
        avatarUrl: user.avatarUrl ?? null,
        color: user.color,
    }));

    const connectionStatusLabel = ticketLive.connectionStatus === 'reconnecting'
        ? t('liveUpdates.connection.reconnecting', 'Live updates offline — reconnecting…')
        : ticketLive.connectionStatus === 'unavailable'
            ? t('liveUpdates.connection.unavailable', 'Live updates unavailable')
            : null;

    return (
        <ReflectionContainer id={id} label={`Ticket Details - ${ticket.ticket_number}`}>
            <div className="bg-gray-100 dark:bg-gray-900">
                <div className="sticky top-0 z-10 bg-gray-100 dark:bg-gray-900 py-2 flex gap-3">
                    {!isInDrawer && (
                        <div className="flex-shrink-0 self-start">
                            <BackNav href="/msp/tickets"><span className="text-right">← Back to<br />Tickets </span></BackNav>
                        </div>
                    )}
                    <div className="min-w-0 flex-1 space-y-1">
                        <div className="flex items-center justify-between">
                            <div className="flex items-center gap-3">
                                {!isInDrawer && ticket.ticket_id && (
                                    <TicketNavigation currentTicketId={ticket.ticket_id} />
                                )}
                                <h6 className="text-sm font-medium whitespace-nowrap">#{ticket.ticket_number}</h6>
                                {responseStateTrackingEnabled && ticket.response_state ? (
                                    <ResponseStateBadge
                                        responseState={ticket.response_state}
                                        size="sm"
                                        showTooltip={false}
                                        className="flex-shrink-0"
                                    />
                                ) : null}
                                <TicketOriginBadge
                                    origin={ticketOrigin}
                                    labels={ticketOriginLabels}
                                    size="sm"
                                    className="flex-shrink-0"
                                />
                            </div>

                            <div className="flex items-center gap-2">
                                {/* Add popout button only when in drawer */}
                                {isInDrawer && (
                                    <Button
                                        id="ticket-popout-button"
                                        variant="outline"
                                        size="sm"
                                        onClick={openTicketInNewWindow}
                                        className="flex items-center gap-2"
                                        aria-label="Open in new tab"
                                    >
                                        <ExternalLink className="h-4 w-4" />
                                        <span>{t('fields.openInNewTab', 'Open in new tab')}</span>
                                    </Button>
                                )}
                                <Button
                                    id={`${id}-delete-ticket-button`}
                                    variant="destructive"
                                    size="sm"
                                    onClick={handleDeleteTicket}
                                    className="flex items-center gap-2"
                                >
                                    <Trash2 className="h-4 w-4" />
                                    <span>{t('actions.delete', { defaultValue: 'Delete' })}</span>
                                </Button>
                            </div>
                        </div>
                        <h1
                            className={`text-lg font-bold truncate transition-all duration-200 ${cardTitleVisible ? 'opacity-0 max-h-0 overflow-hidden' : 'opacity-100 max-h-8'}`}
                        >
                            {ticket.title}
                        </h1>
                        {ticketLive.enabled ? (
                            <div className="flex flex-wrap items-center gap-3 text-sm">
                                {ticketLive.connectionStatus === 'connected' && livePresenceUsers.length > 0 ? (
                                    <PresenceBar users={livePresenceUsers} />
                                ) : null}
                                {connectionStatusLabel ? (
                                    <span className="text-xs font-medium text-amber-700" data-testid="ticket-live-connection-status">
                                        {connectionStatusLabel}
                                    </span>
                                ) : null}
                            </div>
                        ) : null}
                    </div>
                </div>

                <div className="flex items-center space-x-5 mb-5 text-sm text-gray-600">
                    {ticket.entered_at && (
                        <p>
                            {t('fields.created', 'Created')} {createdRelativeTime || (() => {
                                const tz = hasHydrated ? getUserTimeZone() : 'UTC';
                                const localDate = utcToLocal(ticket.entered_at, tz);
                                return formatDateTime(localDate, tz, dateTimeFormat);
                            })()}
                        </p>
                    )}
                    {ticket.updated_at && (
                        <p>
                            {t('fields.updated', 'Updated')} {updatedRelativeTime || (() => {
                                const tz = hasHydrated ? getUserTimeZone() : 'UTC';
                                const localDate = utcToLocal(ticket.updated_at, tz);
                                return formatDateTime(localDate, tz, dateTimeFormat);
                            })()}
                        </p>
                    )}
                </div>
                {/* Delete Ticket Dialog (with dependency validation) */}
                <DeleteEntityDialog
                    id={`${id}-delete-ticket-dialog`}
                    isOpen={isDeleteTicketDialogOpen}
                    onClose={resetTicketDeleteState}
                    onConfirmDelete={confirmTicketDelete}
                    entityName={`#${ticket.ticket_number}`}
                    validationResult={ticketDeleteValidation}
                    isValidating={isTicketDeleteValidating}
                    isDeleting={isTicketDeleteProcessing}
                />

                {/* Confirmation Dialog for Comment Deletion */}
                <ConfirmationDialog
                    id={`${id}-delete-comment-dialog`}
                    isOpen={isDeleteDialogOpen}
                    onClose={resetCommentDeleteState}
                    onConfirm={() => handleDeleteConfirm(true)}
                    onCancel={deleteDialogHasImages ? () => handleDeleteConfirm(false) : undefined}
                    title="Delete Comment"
                    message={deleteDialogMessage}
                    confirmLabel={deleteDialogHasImages ? 'Delete Comment + Images' : 'Delete'}
                    thirdButtonLabel={deleteDialogHasImages ? 'Delete Comment Only' : undefined}
                    cancelLabel="Cancel"
                    isConfirming={isDeletingComment}
                />
                
                {/* Blocked-close dialog: unmet close rules with quick actions and
                    a permissioned "Close anyway" override. */}
                <Dialog
                    id={`${id}-close-blocked-dialog`}
                    isOpen={closeBlockedDialog.isOpen}
                    onClose={() => {
                        setCloseBlockedDialog({ isOpen: false, statusId: null, failures: [], canOverride: false });
                        setCloseOverrideReason('');
                    }}
                    title="This ticket can't be closed yet"
                >
                    <DialogContent>
                        <p className="text-sm text-gray-600 mb-3">
                            The board's close rules require the following before this ticket can be closed:
                        </p>
                        <ul className="space-y-2 mb-4">
                            {closeBlockedDialog.failures.map((failure) => (
                                <li
                                    key={failure.rule}
                                    className="flex items-center justify-between gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900"
                                >
                                    <span>{failure.message}</span>
                                    {failure.rule === 'checklist_incomplete' && (
                                        <Button
                                            id={`${id}-close-blocked-view-checklist`}
                                            type="button"
                                            variant="outline"
                                            size="sm"
                                            onClick={() => {
                                                setCloseBlockedDialog({ isOpen: false, statusId: null, failures: [], canOverride: false });
                                                document
                                                    .getElementById(`${id}-checklist-section`)
                                                    ?.scrollIntoView({ behavior: 'smooth', block: 'start' });
                                            }}
                                        >
                                            View checklist
                                        </Button>
                                    )}
                                    {failure.rule === 'resolution_comment' && (
                                        <Button
                                            id={`${id}-close-blocked-add-comment`}
                                            type="button"
                                            variant="outline"
                                            size="sm"
                                            onClick={() => {
                                                setCloseBlockedDialog({ isOpen: false, statusId: null, failures: [], canOverride: false });
                                                document
                                                    .getElementById(`${id}-conversation`)
                                                    ?.scrollIntoView({ behavior: 'smooth', block: 'start' });
                                            }}
                                        >
                                            Add comment
                                        </Button>
                                    )}
                                </li>
                            ))}
                        </ul>
                        {closeBlockedDialog.canOverride && (
                            <div className="mb-2">
                                <TextArea
                                    id={`${id}-close-override-reason`}
                                    value={closeOverrideReason}
                                    onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setCloseOverrideReason(e.target.value)}
                                    placeholder="Reason for closing anyway (optional, recorded in the audit log)"
                                    rows={2}
                                />
                            </div>
                        )}
                        <DialogFooter>
                            <Button
                                id={`${id}-close-blocked-cancel`}
                                type="button"
                                variant="outline"
                                onClick={() => {
                                    setCloseBlockedDialog({ isOpen: false, statusId: null, failures: [], canOverride: false });
                                    setCloseOverrideReason('');
                                }}
                            >
                                Cancel
                            </Button>
                            {closeBlockedDialog.canOverride && (
                                <Button
                                    id={`${id}-close-blocked-close-anyway`}
                                    type="button"
                                    variant="destructive"
                                    onClick={submitCloseOverride}
                                    disabled={isSubmittingCloseOverride}
                                >
                                    {isSubmittingCloseOverride ? 'Closing…' : 'Close anyway'}
                                </Button>
                            )}
                        </DialogFooter>
                    </DialogContent>
                </Dialog>

                {/* Timer Replace Confirmation */}
                <ConfirmationDialog
                    id={`${id}-replace-timer-dialog`}
                    isOpen={isReplaceDialogOpen}
                    onClose={() => setIsReplaceDialogOpen(false)}
                    onConfirm={handleConfirmReplace}
                    title="Timer Active Elsewhere"
                    message="This ticket's timer is active in another window. Do you want to take over and replace it here?"
                    confirmLabel="Replace Here"
                    cancelLabel="Cancel"
                />

                <ConfirmationDialog
                    id={`${id}-bundle-add-child-multi-client-confirm`}
                    isOpen={isAddChildMultiClientConfirmOpen}
                    onClose={() => {
                        setIsAddChildMultiClientConfirmOpen(false);
                        setPendingChildToAdd(null);
                    }}
                    onConfirm={async () => {
                        if (!pendingChildToAdd?.ticket_id) {
                            setIsAddChildMultiClientConfirmOpen(false);
                            return;
                        }
                        try {
                            await performAddChildToBundle(pendingChildToAdd.ticket_id);
                        } catch (error) {
                            handleError(error, t('messages.addToBundleFailed'));
                        } finally {
                            setIsAddChildMultiClientConfirmOpen(false);
                            setPendingChildToAdd(null);
                        }
                    }}
                    title="Bundle spans multiple clients"
                    message={`This will add ${pendingChildToAdd?.ticket_number || 'this ticket'} from a different client into the bundle. Confirm you want to proceed.`}
                    confirmLabel="Proceed"
                    cancelLabel="Cancel"
                />

                <ConfirmationDialog
                    id={`${id}-time-period-dialog`}
                    isOpen={isTimeEntryPeriodDialogOpen}
                    onClose={() => setIsTimeEntryPeriodDialogOpen(false)}
                    onConfirm={() => {
                        setIsTimeEntryPeriodDialogOpen(false);
                        router.push('/msp/settings?tab=time-entry&subtab=time-periods');
                    }}
                    title="No Active Time Period"
                    message="No active time period found. Time periods need to be set up in the billing dashboard before adding time entries."
                    confirmLabel="Go to Time Periods Setup"
                    cancelLabel="Cancel"
                />

                <ConfirmationDialog
                    id={`${id}-delete-time-entry-dialog`}
                    isOpen={pendingDeleteTimeEntry !== null}
                    onClose={() => {
                        if (!isDeletingTimeEntry) setPendingDeleteTimeEntry(null);
                    }}
                    onConfirm={handleConfirmDeleteTimeEntry}
                    title={t('timeEntries.deleteConfirmTitle', { defaultValue: 'Delete time entry?' })}
                    message={t('timeEntries.deleteConfirmMessage', {
                        defaultValue: 'This will permanently delete the time entry. This action cannot be undone.',
                    })}
                    confirmLabel={t('timeEntries.deleteConfirmAction', { defaultValue: 'Delete' })}
                    cancelLabel={t('common.cancel', { defaultValue: 'Cancel' })}
                    isConfirming={isDeletingTimeEntry}
                />

                <div className="flex gap-6 min-w-0">
                    <div className="flex-grow col-span-2 min-w-0" id="ticket-main-content">
                        <Suspense fallback={<div id="ticket-info-skeleton" className="animate-pulse bg-gray-200 dark:bg-gray-800 h-64 rounded-lg mb-6"></div>}>
                            <div className="mb-6">
                                {bundle?.isBundleChild && bundle?.masterTicket ? (
                                    <div className="mb-3 rounded-lg border border-amber-200 dark:border-amber-700 bg-amber-50 dark:bg-amber-900/30 px-4 py-2 text-sm text-amber-900 dark:text-amber-200" id="ticket-bundle-child-banner">
                                        This ticket is bundled under{' '}
                                        <a className="font-medium underline" href={`/msp/tickets/${bundle.masterTicket.ticket_id}`}>
                                            {bundle.masterTicket.ticket_number}
                                        </a>
                                        . Workflow fields are locked; work from the master ticket.
                                    </div>
                                ) : null}

                                {bundle?.isBundleMaster ? (
                                    <div className="mb-3 rounded-lg border border-indigo-200 dark:border-indigo-700 bg-indigo-50 dark:bg-indigo-900/30 px-4 py-2 text-sm text-indigo-900 dark:text-indigo-200" id="ticket-bundle-master-banner">
                                        This ticket is the master of a bundle ({Array.isArray(bundle.children) ? bundle.children.length : 0} children). Mode:{' '}
                                        {(bundle.mode || 'sync_updates').split('_').map((word: string) => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}.
                                        {bundleHasMultipleClients ? (
                                            <span className="ml-2 inline-flex items-center rounded bg-amber-100 dark:bg-amber-900/30 px-2 py-0.5 text-[11px] font-medium text-amber-900 dark:text-amber-200">
                                                Multiple clients
                                            </span>
                                        ) : null}
                                    </div>
                                ) : null}

                                {bundle?.isBundleMaster ? (
                                    <div className="mb-4 rounded-lg border border-gray-200 bg-white p-3" id="ticket-bundle-master-panel">
                                        <div className="flex items-center justify-between mb-2">
                                            <div className="text-sm font-semibold text-gray-900">Bundle</div>
                                            <div className="flex items-center gap-2">
                                                <Button
                                                    id="ticket-bundle-toggle-mode-button"
                                                    variant="outline"
                                                    size="sm"
                                                    onClick={handleToggleBundleMode}
                                                    disabled={isUpdatingBundleSettings}
                                                >
                                                    Mode: {(bundle.mode || 'sync_updates').split('_').map((word: string) => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}
                                                </Button>
                                                <Button
                                                    id="ticket-bundle-unbundle-button"
                                                    variant="outline"
                                                    size="sm"
                                                    onClick={handleUnbundleMaster}
                                                >
                                                    Unbundle
                                                </Button>
                                            </div>
                                        </div>
                                        <div className="text-xs text-gray-500 mb-2">
                                            {t('details.bundle.childrenDescription')}
                                        </div>
                                        <div className="flex items-center gap-2 mb-3" ref={searchContainerRef}>
                                            <div className="relative flex-1">
                                                <Input
                                                    id="ticket-bundle-add-child-input"
                                                    ref={searchInputRef}
                                                    value={addChildTicketNumber}
                                                    onChange={handleSearchInputChange}
                                                    onFocus={() => addChildTicketNumber.trim() && setShowSearchResults(true)}
                                                    placeholder="Search ticket number or title…"
                                                    className="h-8"
                                                    containerClassName="mb-0"
                                                    autoComplete="off"
                                                />
                                                {isSearching && (
                                                    <div className="absolute right-2 top-1/2 -translate-y-1/2">
                                                        <div className="w-4 h-4 border-2 border-gray-300 border-t-transparent rounded-full animate-spin" />
                                                    </div>
                                                )}
                                                {showSearchResults && (
                                                    <div className="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-md shadow-lg max-h-48 overflow-y-auto">
                                                        {searchResults.length > 0 ? (
                                                            <ul className="py-1">
                                                                {searchResults.map((result) => (
                                                                    <li
                                                                        key={result.ticket_id}
                                                                        className="px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
                                                                        onClick={() => handleSelectSearchResult(result)}
                                                                    >
                                                                        <div className="min-w-0">
                                                                            <span className="text-sm text-blue-600">
                                                                                {result.ticket_number}
                                                                            </span>
                                                                            <div className="text-xs text-gray-500 truncate">
                                                                                {(result.client_name ? `${result.client_name} · ` : '')}{result.title}
                                                                            </div>
                                                                        </div>
                                                                    </li>
                                                                ))}
                                                            </ul>
                                                        ) : addChildTicketNumber.trim().length > 0 && !isSearching ? (
                                                            <div className="px-3 py-2 text-sm text-gray-500">
                                                                No tickets found
                                                            </div>
                                                        ) : null}
                                                    </div>
                                                )}
                                            </div>
                                            <Button
                                                id="ticket-bundle-add-child-button"
                                                size="sm"
                                                onClick={handleAddChildToBundle}
                                                disabled={!addChildTicketNumber.trim()}
                                            >
                                                Add
                                            </Button>
                                        </div>
                                        <div className="max-h-56 overflow-y-auto rounded border border-gray-100">
                                            {Array.isArray(bundle.children) && bundle.children.length > 0 ? (
                                                <ul>
                                                    {bundle.children.map((child: any) => (
                                                        <li key={child.ticket_id} className="flex items-center justify-between gap-3 px-3 py-2 border-b border-gray-100 last:border-b-0">
                                                            <div className="min-w-0">
                                                                <a className="text-sm text-blue-600 hover:underline" href={`/msp/tickets/${child.ticket_id}`}>
                                                                    {child.ticket_number}
                                                                </a>
                                                                <div className="text-xs text-gray-500 truncate">
                                                                    {(child.client_name ? `${child.client_name} · ` : '')}{child.title}
                                                                </div>
                                                            </div>
                                                            <div className="flex items-center gap-2">
                                                                <Button
                                                                    id={`ticket-bundle-promote-child-${child.ticket_id}`}
                                                                    variant="outline"
                                                                    size="sm"
                                                                    onClick={() => handlePromoteChildToMaster(child.ticket_id)}
                                                                >
                                                                    Promote
                                                                </Button>
                                                                <Button
                                                                    id={`ticket-bundle-remove-child-${child.ticket_id}`}
                                                                    variant="outline"
                                                                    size="sm"
                                                                    onClick={() => handleRemoveChildFromBundle(child.ticket_id)}
                                                                >
                                                                    Remove
                                                                </Button>
                                                            </div>
                                                        </li>
                                                    ))}
                                                </ul>
                                            ) : (
                                                <div className="px-3 py-2 text-sm text-gray-500">No children in this bundle.</div>
                                            )}
                                        </div>
                                    </div>
                                ) : null}

                                {(autoCloseState || checklistSummary.requiredTotal > 0) && (
                                    <div id={`${id}-close-rules-banner`} className="mb-4 flex flex-wrap items-center gap-2">
                                        {autoCloseState && (
                                            <div
                                                id={`${id}-auto-close-banner`}
                                                className="flex-1 min-w-[260px] rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-900"
                                            >
                                                {`Will close automatically on ${new Date(autoCloseState.scheduled_close_at).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })} unless there's new activity.`}
                                                {autoCloseState.warning_sent_at ? ' The customer has been warned.' : ''}
                                            </div>
                                        )}
                                        {checklistSummary.requiredTotal > 0 && (
                                            <button
                                                type="button"
                                                id={`${id}-checklist-progress-chip`}
                                                className={`inline-flex items-center gap-1 rounded-full border px-3 py-1 text-xs font-medium ${
                                                    checklistSummary.requiredDone === checklistSummary.requiredTotal
                                                        ? 'border-green-300 bg-green-50 text-green-800'
                                                        : 'border-amber-300 bg-amber-50 text-amber-900'
                                                }`}
                                                onClick={() =>
                                                    document
                                                        .getElementById(`${id}-checklist-section`)
                                                        ?.scrollIntoView({ behavior: 'smooth', block: 'start' })
                                                }
                                            >
                                                {`${checklistSummary.requiredDone} of ${checklistSummary.requiredTotal} required checklist items done`}
                                            </button>
                                        )}
                                    </div>
                                )}

                                <TicketInfo
                                    id={`${id}-info`}
                                    titleRef={cardTitleRef}
                                    ticket={ticket}
                                    conversations={conversations}
                                    statusOptions={statusOptions}
                                    agentOptions={agentOptions}
                                    boardOptions={boardOptions}
                                    priorityOptions={priorityOptions}
                                    onSelectChange={handleSelectChange}
                                    onSaveChanges={handleBatchSaveChanges}
                                    onUpdateDescription={handleUpdateDescription}
                                    isSubmitting={isSubmitting}
                                    users={availableAgents}
                                    tags={tags}
                                    allTagTexts={allTags.filter(tag => tag.tagged_type === 'ticket').map(tag => tag.tag_text)}
                                    onTagsChange={handleTagsChange}
                                    isInDrawer={isInDrawer}
                                    onItilFieldChange={handleItilFieldChange}
                                    initialCategories={initialCategories}
                                    itilImpact={itilImpact}
                                    itilUrgency={itilUrgency}
                                    isBundledChild={Boolean(bundle?.isBundleChild)}
                                    responseStateTrackingEnabled={responseStateTrackingEnabled}
                                    renderProjectTaskActions={renderCreateProjectTask}
                                    teams={teams}
                                    onAssignTeam={handleAssignTeam}
                                    onRemoveTeamAssignment={async () => {
                                        await handleRemoveTeamAssignment('remove_all');
                                    }}
                                    onClipboardImageUploaded={refreshTicketDocuments}
                                    uploadTicketAttachmentAction={uploadTicketAttachmentAction}
                                    deleteDraftTicketAttachmentImagesAction={deleteDraftTicketAttachmentImagesAction}
                                    resolveTicketAttachmentViewUrl={resolveTicketAttachmentViewUrl}
                                    onOpenEmailNotificationLogs={() => setIsEmailNotificationLogsDrawerOpen(true)}
                                    onOpenActivityLog={() => {
                                        setActivityLogRefreshKey((value) => value + 1);
                                        setIsActivityLogDrawerOpen(true);
                                    }}
                                    hideSlaStatus={hideSlaStatus}
                                    additionalAgents={additionalAgentsForInfo}
                                    onLiveDirtyFieldsChange={setTicketInfoDirtyFields}
                                    liveHighlightedFields={liveHighlightedFields}
                                    liveFieldConflicts={liveFieldConflicts}
                                    liveFrozenFields={Object.keys(liveFieldConflicts)}
                                    onKeepLiveConflict={handleKeepLiveConflict}
                                    onTakeLiveConflict={handleTakeLiveConflict}
                                    liveEditingUsers={liveEditingUsers}
                                    onLiveEditingFieldChange={ticketLive.setEditingField}
                                />
                            </div>
                        </Suspense>
                        <Suspense fallback={<div id="ticket-conversation-skeleton" className="animate-pulse bg-gray-200 h-96 rounded-lg mb-6"></div>}>
                            <div className="mb-6">
                                <TicketConversation
                                    id={`${id}-conversation`}
                                    ticket={ticket}
                                    conversations={conversations}
                                    documents={documents}
                                    userMap={userMap}
                                    contactMap={contactMap}
                                    currentUser={currentUser ? {
                                        id: currentUser.user_id,
                                        name: `${currentUser.first_name} ${currentUser.last_name}`,
                                        email: currentUser.email,
                                        avatarUrl: null
                                    } : session?.user}
                                    activeTab={activeTab}
                                    closedStatusOptions={closedStatusOptions}
                                    isEditing={isEditing}
                                    currentComment={currentComment}
                                    editorKey={editorKey}
                                    onNewCommentContentChange={setNewCommentContent}
                                    onAddNewComment={handleAddNewComment}
                                    onAddReplyComment={handleAddReplyComment}
                                    onTabChange={setActiveTab}
                                    onEdit={handleEdit}
                                    onSave={handleSave}
                                    onClose={handleClose}
                                    onDelete={handleDeleteRequest}
                                    onContentChange={handleContentChange}
                                    isSubmitting={isSubmitting}
                                    hideInternalTab={false}
                                    externalComments={bundle?.isBundleMaster ? aggregatedChildClientComments : []}
                                    onClipboardImageUploaded={refreshTicketDocuments}
                                    uploadTicketAttachmentAction={uploadTicketAttachmentAction}
                                    deleteDraftTicketAttachmentImagesAction={deleteDraftTicketAttachmentImagesAction}
                                    resolveTicketAttachmentViewUrl={resolveTicketAttachmentViewUrl}
                                    defaultNewestFirst
                                    canViewCommentMetadataDebug={canViewCommentMetadataDebug}
                                    reactionRefreshVersion={reactionRefreshVersion}
                                />
                            </div>
                        </Suspense>
                        
                        <div className="mb-6">
                            <TicketChecklistSection
                                id={`${id}-checklist-section`}
                                ticketId={ticket.ticket_id || ''}
                                initialItems={checklistItems}
                                onItemsChanged={setChecklistItems}
                            />
                        </div>

                        <Suspense fallback={<div id="ticket-documents-skeleton" className="animate-pulse bg-gray-200 h-64 rounded-lg mb-6"></div>}>
                            <TicketDocumentsSection
                                id={`${id}-documents-section`}
                                ticketId={ticket.ticket_id || ''}
                                initialDocuments={documents}
                                forceUploadToRoot={disableAttachmentFolderSelection}
                                allowDocumentSharing={!disableAttachmentSharing}
                                allowLinkExistingDocuments={!disableAttachmentLinking}
                                allowBlockDocuments={!disableAttachmentLinking}
                                onDocumentCreated={async () => {
                                    router.refresh();
                                }}
                            />
                        </Suspense>

                    </div>
                    <div className={isInDrawer ? "w-96" : "w-1/4"} id="ticket-properties-container">
                        <Suspense fallback={<div id="ticket-properties-skeleton" className="animate-pulse bg-gray-200 h-96 rounded-lg mb-6"></div>}>
                                <TicketProperties
                                    id={`${id}-properties`}
                                    ticket={ticket}
                                client={client}
                                contactInfo={contactInfo}
                                createdByUser={createdByUser}
                                board={board}
                                elapsedTime={elapsedTime}
                                isRunning={isRunning}
                                timeDescription={timeDescription}
                                isTimerLocked={isLockedByOther}
                                onStart={handleStartClick}
                                onPause={handlePauseClick}
                                onStop={handleStopClick}
                                onTimeDescriptionChange={setTimeDescription}
                                onAddTimeEntry={handleAddTimeEntry}
                                onClientClick={handleClientClick}
                                onContactClick={handleContactClick}
                                team={team}
                                teams={teams}
                                additionalAgents={additionalAgents}
                                availableAgents={availableAgents}
                                onAgentClick={handleAgentClick}
                                disableAgentSchedule={disableAgentSchedule}
                                onAddAgent={handleAddAgent}
                                onRemoveAgent={handleRemoveAgent}
                                currentTimeSheet={currentTimeSheet}
                                currentTimePeriod={currentTimePeriod}
                                userId={userId || ''}
                                tenant={tenant}
                                contacts={contacts}
                                clients={clients}
                                locations={locations}
                                clientFilterState={clientFilterState}
                                clientTypeFilter={clientTypeFilter}
                                onChangeContact={handleContactChange}
                                onChangeClient={handleClientChange}
                                onChangeLocation={handleLocationChange}
                                onClientFilterStateChange={setClientFilterState}
                                onClientTypeFilterChange={setClientTypeFilter}
                                tags={tags}
                                allTagTexts={allTags.filter(tag => tag.tagged_type === 'ticket').map(tag => tag.tag_text)}
                                onTagsChange={handleTagsChange}
                                onItilFieldChange={handleItilFieldChange}
                                onUpdateWatchList={handleUpdateWatchList}
                                watchListSaving={isWatchListSaving}
                                allContactsForWatchList={allContactsForWatchList}
                                allContactsForWatchListLoading={allContactsForWatchListLoading}
                                onLoadAllContactsForWatchList={handleLoadAllContactsForWatchList}
                                surveySummaryCard={surveySummaryCard}
                                    hideTimeEntry={hideTimeEntry}
                                    hideMaterials={hideMaterials}
                                    renderIntervalManagement={renderIntervalManagement}
                                    onRemoveTeamAssignment={handleRemoveTeamAssignment}
                                    onAssignTeam={handleAssignTeam}
                                    isLiveTicketTimerEnabled={isLiveTicketTimerEnabled}
                                    timeEntriesRefreshKey={timeEntriesRefreshKey}
                                    onEditTimeEntry={handleEditTimeEntry}
                                    onDeleteTimeEntry={handleRequestDeleteTimeEntry}
                                    onLiveDirtyFieldsChange={setTicketPropertiesDirtyFields}
                                    liveHighlightedFields={liveHighlightedFields}
                                    liveFieldConflicts={liveFieldConflicts}
                                    liveFrozenFields={Object.keys(liveFieldConflicts)}
                                    onKeepLiveConflict={handleKeepLiveConflict}
                                    onTakeLiveConflict={handleTakeLiveConflict}
                                    liveEditingUsers={liveEditingUsers}
                                    onLiveEditingFieldChange={ticketLive.setEditingField}
                                />
                        </Suspense>
                        
                        {associatedAssets ? <div className="mt-6" id="associated-assets-container">{associatedAssets}</div> : null}
                    </div>
                </div>
            </div>
            <Drawer
                id={`${id}-email-notification-logs-drawer`}
                isOpen={isEmailNotificationLogsDrawerOpen}
                onClose={() => setIsEmailNotificationLogsDrawerOpen(false)}
                width="48rem"
            >
                <div className="space-y-4 pr-8">
                    <div className="flex items-center gap-2">
                        <Mail className="h-5 w-5 text-[rgb(var(--color-text-700))]" />
                        <h2 className="text-lg font-semibold text-[rgb(var(--color-text-900))]">
                            Email Notification Logs
                        </h2>
                    </div>
                    <TicketEmailNotifications
                        id={`${id}-email-notifications`}
                        ticketId={ticket.ticket_id || ''}
                        variant="flat"
                    />
                </div>
            </Drawer>
            <Drawer
                id="ticket-activity-log-drawer"
                isOpen={isActivityLogDrawerOpen}
                onClose={() => setIsActivityLogDrawerOpen(false)}
                width="48rem"
            >
                <div className="space-y-4 pr-8">
                    <div className="flex items-center gap-2">
                        <History className="h-5 w-5 text-[rgb(var(--color-text-700))]" />
                        <h2 className="text-lg font-semibold text-[rgb(var(--color-text-900))]">
                            Ticket Activity
                        </h2>
                    </div>
                    {ticket.ticket_id ? (
                        <TicketActivityTimeline
                            ticketId={ticket.ticket_id}
                            refreshKey={conversations.length + activityLogRefreshKey}
                        />
                    ) : null}
                </div>
            </Drawer>
        </ReflectionContainer>
    );
};

export default TicketDetails;
