import React, {
  useEffect,
  useMemo,
  useState,
  useCallback,
  useRef,
} from 'react';
import {
  DndContext,
  closestCenter,
  PointerSensor,
  useSensor,
  useSensors,
} from '@dnd-kit/core';
import {
  SortableContext,
  verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import useChannelsStore from '../../store/channels';
import API from '../../api';
import ChannelForm from '../forms/Channel';
import ChannelBatchForm from '../forms/ChannelBatch';
import RecordingForm from '../forms/Recording';
import { useDebounce, copyToClipboard } from '../../utils';
import useVideoStore from '../../store/useVideoStore';
import useSettingsStore from '../../store/settings';
import {
  Tv2,
  ScreenShare,
  Scroll,
  SquareMinus,
  CirclePlay,
  SquarePen,
  Copy,
  ScanEye,
  EllipsisVertical,
  ArrowUpNarrowWide,
  ArrowUpDown,
  ArrowDownWideNarrow,
  Search,
  EyeOff,
  Pencil,
} from 'lucide-react';
import { listOverriddenFields } from '../../utils/forms/ChannelUtils.js';
import { buildLiveStreamUrl } from '../../utils/components/FloatingVideoUtils.js';
import {
  Box,
  TextInput,
  Popover,
  ActionIcon,
  Button,
  Paper,
  Flex,
  Text,
  Group,
  useMantineTheme,
  Center,
  Switch,
  Menu,
  MultiSelect,
  Pagination,
  NativeSelect,
  UnstyledButton,
  Stack,
  Select,
  NumberInput,
  Tooltip,
  Skeleton,
} from '@mantine/core';
import './table.css';
import useChannelsTableStore from '../../store/channelsTable';
import ChannelTableStreams from './ChannelTableStreams';
import LazyLogo from '../LazyLogo';
import useLocalStorage from '../../hooks/useLocalStorage';
import useEPGsStore from '../../store/epgs';
import { useChannelLogoSelection } from '../../hooks/useSmartLogos';
import { CustomTable, useTable } from './CustomTable';
import ChannelsTableOnboarding from './ChannelsTable/ChannelsTableOnboarding';
import ChannelTableHeader from './ChannelsTable/ChannelTableHeader';
import useOutputProfilesStore from '../../store/outputProfiles';
import {
  EditableTextCell,
  EditableNumberCell,
  EditableGroupCell,
  EditableEPGCell,
  EditableLogoCell,
} from './ChannelsTable/EditableCell';
import { DraggableRow } from './ChannelsTable/DraggableRow';
import useWarningsStore from '../../store/warnings';
import ConfirmationDialog from '../ConfirmationDialog';
import useAuthStore from '../../store/auth';
import { USER_LEVELS } from '../../constants';

const m3uUrlBase = `${window.location.protocol}//${window.location.host}/output/m3u`;
const epgUrlBase = `${window.location.protocol}//${window.location.host}/output/epg`;
const hdhrUrlBase = `${window.location.protocol}//${window.location.host}/hdhr`;

const ChannelEnabledSwitch = React.memo(
  ({ rowId, selectedProfileId, selectedTableIds }) => {
    // Directly extract the channels set once to avoid re-renders on every change.
    const isEnabled = useChannelsStore(
      useCallback(
        (state) =>
          selectedProfileId === '0' ||
          state.profiles[selectedProfileId]?.channels.has(rowId),
        [rowId, selectedProfileId]
      )
    );

    const handleToggle = () => {
      if (selectedTableIds.length > 1) {
        API.updateProfileChannels(
          selectedTableIds,
          selectedProfileId,
          !isEnabled
        );
      } else {
        API.updateProfileChannel(rowId, selectedProfileId, !isEnabled);
      }
    };

    return (
      <Center style={{ width: '100%' }}>
        <Switch
          size="xs"
          checked={isEnabled}
          onChange={handleToggle}
          disabled={selectedProfileId === '0'}
        />
      </Center>
    );
  }
);

const ChannelRowActions = React.memo(
  ({
    theme,
    row,
    table,
    editChannel,
    deleteChannel,
    handleWatchStream,
    createRecording,
    getChannelURL,
  }) => {
    // Extract the channel ID once to ensure consistency
    const channelId = row.original.id;
    const channelUuid = row.original.uuid;

    const authUser = useAuthStore((s) => s.user);

    const onEdit = useCallback(() => {
      // Use the ID directly to avoid issues with filtered tables
      console.log(`Editing channel ID: ${channelId}`);
      editChannel(row.original);
    }, [channelId, row.original]);

    const onDelete = useCallback(() => {
      console.log(`Deleting channel ID: ${channelId}`);
      deleteChannel(channelId);
    }, [channelId]);

    const onPreview = useCallback(() => {
      // Use direct channel UUID for preview to avoid issues
      console.log(`Previewing channel UUID: ${channelUuid}`);
      handleWatchStream(row.original);
    }, [channelUuid]);

    const onRecord = useCallback(() => {
      console.log(`Recording channel ID: ${channelId}`);
      createRecording(row.original);
    }, [channelId]);

    const tableSize = table?.tableSize ?? 'default';
    const iconSize =
      tableSize == 'default' ? 'sm' : tableSize == 'compact' ? 'xs' : 'md';

    return (
      <Box style={{ width: '100%', justifyContent: 'left' }}>
        <Center>
          <ActionIcon
            size={iconSize}
            variant="transparent"
            color={theme.tailwind.yellow[3]}
            onClick={onEdit}
            disabled={authUser.user_level != USER_LEVELS.ADMIN}
          >
            <SquarePen size="18" />
          </ActionIcon>

          <ActionIcon
            size={iconSize}
            variant="transparent"
            color={theme.tailwind.red[6]}
            onClick={onDelete}
            disabled={authUser.user_level != USER_LEVELS.ADMIN}
          >
            <SquareMinus size="18" />
          </ActionIcon>

          <ActionIcon
            size={iconSize}
            variant="transparent"
            color={theme.tailwind.green[5]}
            onClick={onPreview}
          >
            <CirclePlay size="18" />
          </ActionIcon>

          <Menu>
            <Menu.Target>
              <ActionIcon variant="transparent" size={iconSize}>
                <EllipsisVertical size="18" />
              </ActionIcon>
            </Menu.Target>

            <Menu.Dropdown>
              <Menu.Item leftSection={<Copy size="14" />}>
                <UnstyledButton
                  size="xs"
                  onClick={() => copyToClipboard(getChannelURL(row.original))}
                >
                  <Text size="xs">Copy URL</Text>
                </UnstyledButton>
              </Menu.Item>
              <Menu.Item
                onClick={onRecord}
                disabled={authUser.user_level != USER_LEVELS.ADMIN}
                leftSection={
                  <div
                    style={{
                      borderRadius: '50%',
                      width: '10px',
                      height: '10px',
                      display: 'flex',
                      backgroundColor: 'red',
                    }}
                  ></div>
                }
              >
                <Text size="xs">Record</Text>
              </Menu.Item>
            </Menu.Dropdown>
          </Menu>
        </Center>
      </Box>
    );
  },
  // Custom comparator: skip re-render when the channel's data object hasn't
  // changed. row.original is stable when the underlying channel hasn't been
  // updated; it becomes a new reference when the store replaces that channel.
  (prevProps, nextProps) => prevProps.row.original === nextProps.row.original
);

const ChannelsTable = ({ onReady }) => {
  // EPG data lookup
  const tvgsById = useEPGsStore((s) => s.tvgsById);
  const epgs = useEPGsStore((s) => s.epgs);
  const tvgsLoaded = useEPGsStore((s) => s.tvgsLoaded);
  const hasUnassignedEPGChannels = useChannelsTableStore(
    (s) => s.hasUnassignedEPGChannels
  );

  // Get channel logos for logo selection
  const { ensureLogosLoaded } = useChannelLogoSelection();

  const theme = useMantineTheme();
  const channelGroups = useChannelsStore((s) => s.channelGroups);
  const hasSignaledReady = useRef(false);

  /**
   * STORES
   */

  // store/channelsTable
  const data = useChannelsTableStore((s) => s.channels);
  const pageCount = useChannelsTableStore((s) => s.pageCount);

  const rowClassMap = useMemo(() => {
    const map = {};
    for (const channel of data) {
      const hasStreams = channel.streams?.length > 0;
      if (!hasStreams) {
        map[channel.id] = 'no-streams-row';
      } else if (channel.streams.some((s) => s.is_stale)) {
        map[channel.id] = 'has-stale-streams-row';
      }
    }
    return map;
  }, [data]);
  const setSelectedChannelIds = useChannelsTableStore(
    (s) => s.setSelectedChannelIds
  );
  const selectedChannelIds = useChannelsTableStore((s) => s.selectedChannelIds);
  const setExpandedChannelId = useChannelsTableStore(
    (s) => s.setExpandedChannelId
  );
  const pagination = useChannelsTableStore((s) => s.pagination);
  const setPagination = useChannelsTableStore((s) => s.setPagination);
  const sorting = useChannelsTableStore((s) => s.sorting);
  const setSorting = useChannelsTableStore((s) => s.setSorting);
  const totalCount = useChannelsTableStore((s) => s.totalCount);
  const allRowIds = useChannelsTableStore((s) => s.allQueryIds);
  const setAllRowIds = useChannelsTableStore((s) => s.setAllQueryIds);

  // store/channels
  const hasChannels = useChannelsStore((s) => s.channelIds.length > 0);
  const profiles = useChannelsStore((s) => s.profiles);
  const selectedProfileId = useChannelsStore((s) => s.selectedProfileId);
  const [, setTablePrefs] = useLocalStorage('channel-table-prefs', {
    pageSize: 50,
  });

  // store/settings
  const env_mode = useSettingsStore((s) => s.environment.env_mode);
  const outputProfiles = useOutputProfilesStore((s) => s.profiles);
  const showVideo = useVideoStore((s) => s.showVideo);

  // store/warnings
  const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed);
  const suppressWarning = useWarningsStore((s) => s.suppressWarning);

  /**
   * useState
   */
  const [channel, setChannel] = useState(null);
  const [channelModalOpen, setChannelModalOpen] = useState(false);
  const [channelBatchModalOpen, setChannelBatchModalOpen] = useState(false);
  const [recordingModalOpen, setRecordingModalOpen] = useState(false);
  const [showDisabled, setShowDisabled] = useState(true);
  const [showOnlyStreamlessChannels, setShowOnlyStreamlessChannels] =
    useState(false);
  const [showOnlyStaleChannels, setShowOnlyStaleChannels] = useState(false);
  const [showOnlyOverriddenChannels, setShowOnlyOverriddenChannels] =
    useState(false);
  const [visibilityFilter, setVisibilityFilter] = useState('active');

  const [paginationString, setPaginationString] = useState('');
  const [filters, setFilters] = useState({
    name: '',
    channel_group: '',
    epg: '',
  });
  const [, setIsLoading] = useState(true);

  const [hdhrUrl, setHDHRUrl] = useState(hdhrUrlBase);
  const [epgUrl, setEPGUrl] = useState(epgUrlBase);
  const [m3uUrl, setM3UUrl] = useState(m3uUrlBase);

  const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
  const [deleteTarget, setDeleteTarget] = useState(null);
  const [isBulkDelete, setIsBulkDelete] = useState(false);
  const [channelToDelete, setChannelToDelete] = useState(null);
  const [deleting, setDeleting] = useState(false);

  const hasFetchedData = useRef(false);
  const fetchVersionRef = useRef(0); // Track fetch version to prevent stale updates
  const lastFetchParamsRef = useRef(null); // Track last fetch params to prevent duplicate requests
  const fetchInProgressRef = useRef(false); // Track if a fetch is currently in progress

  // Drag-and-drop sensors
  const sensors = useSensors(
    useSensor(PointerSensor, {
      activationConstraint: {
        distance: 8, // Require 8px movement before dragging starts
      },
    })
  );

  // Column sizing state for resizable columns
  // Store in localStorage but with empty object as default
  const [columnSizing, setColumnSizing] = useLocalStorage(
    'channels-table-column-sizing',
    {}
  );

  // M3U and EPG URL configuration state
  const [m3uParams, setM3uParams] = useState({
    cachedlogos: true,
    direct: false,
    tvg_id_source: 'channel_number',
    output_format: '',
    output_profile: '',
  });
  const [epgParams, setEpgParams] = useState({
    cachedlogos: true,
    tvg_id_source: 'channel_number',
    days: 0,
    prev_days: 0,
  });

  /**
   * Derived variables
   */
  const groupOptions = Object.values(channelGroups)
    .filter((group) => group.hasChannels)
    .map((group) => group.name)
    .sort((a, b) => a.localeCompare(b));

  const epgOptions = Object.values(epgs)
    .filter((epg) => epg.is_active && epg.has_channels)
    .map((epg) => epg.name)
    .sort((a, b) => a.localeCompare(b));
  // Only show 'No EPG' if there are channels without an EPG assigned
  const epgSelectOptions = [
    ...(hasUnassignedEPGChannels ? [{ value: 'null', label: 'No EPG' }] : []),
    ...epgOptions.map((opt) => ({ value: opt, label: opt })),
  ];
  const debouncedFilters = useDebounce(filters, 500, () => {
    setPagination({
      ...pagination,
      pageIndex: 0,
    });
  });

  const channelsTableLength =
    Object.keys(data).length > 0 || hasFetchedData.current
      ? Object.keys(data).length
      : undefined;

  /**
   * Functions
   */
  const fetchData = useCallback(async () => {
    // Build params first to check for duplicates
    const params = new URLSearchParams();
    params.append('page', pagination.pageIndex + 1);
    params.append('page_size', pagination.pageSize);
    params.append('include_streams', 'true');
    if (selectedProfileId !== '0') {
      params.append('channel_profile_id', selectedProfileId);
    }
    if (showDisabled === true) {
      params.append('show_disabled', true);
    }
    if (showOnlyStreamlessChannels === true) {
      params.append('only_streamless', true);
    }
    if (showOnlyStaleChannels === true) {
      params.append('only_stale', true);
    }
    if (showOnlyOverriddenChannels === true) {
      params.append('only_has_overrides', true);
    }
    // The backend defaults to "active"; send other choices explicitly so
    // hidden rows surface when the user opts into "Hidden Only" or "Show All".
    if (visibilityFilter && visibilityFilter !== 'active') {
      params.append('visibility_filter', visibilityFilter);
    }

    // Apply sorting
    if (sorting.length > 0) {
      let sortField = sorting[0].id;
      // Map frontend column ids to backend ordering field names
      const fieldMapping = {
        channel_group: 'channel_group__name',
        epg: 'epg_data__name',
      };
      if (fieldMapping[sortField]) {
        sortField = fieldMapping[sortField];
      }
      const sortDirection = sorting[0].desc ? '-' : '';
      params.append('ordering', `${sortDirection}${sortField}`);
    }

    // Apply debounced filters
    Object.entries(debouncedFilters).forEach(([key, value]) => {
      if (value) {
        if (Array.isArray(value)) {
          // Convert null values to "null" string for URL parameter
          const processedValue = value
            .map((v) => (v === null ? 'null' : v))
            .join(',');
          params.append(key, processedValue);
        } else {
          params.append(key, value);
        }
      }
    });

    const paramsString = params.toString();

    // Skip if same fetch is already in progress (prevents StrictMode double-fetch)
    if (
      fetchInProgressRef.current &&
      lastFetchParamsRef.current === paramsString
    ) {
      return;
    }

    // Increment fetch version to track this specific fetch request
    const currentFetchVersion = ++fetchVersionRef.current;
    lastFetchParamsRef.current = paramsString;
    fetchInProgressRef.current = true;

    setIsLoading(true);

    try {
      const [, ids] = await Promise.all([
        API.queryChannels(params),
        API.getAllChannelIds(params),
      ]);

      fetchInProgressRef.current = false;

      // Skip state updates if a newer fetch has been initiated
      if (currentFetchVersion !== fetchVersionRef.current) {
        return;
      }

      setIsLoading(false);
      hasFetchedData.current = true;

      setTablePrefs((prev) => ({
        ...prev,
        pageSize: pagination.pageSize,
      }));
      setAllRowIds(ids);

      // Signal ready after first successful data fetch AND EPG data is loaded
      // This prevents the EPG column from showing "Not Assigned" while EPG data is still loading
      if (!hasSignaledReady.current && onReady && tvgsLoaded) {
        hasSignaledReady.current = true;
        onReady();
      }
    } catch (error) {
      fetchInProgressRef.current = false;

      // Skip state updates if a newer fetch has been initiated
      if (currentFetchVersion !== fetchVersionRef.current) {
        return;
      }
      setIsLoading(false);
      // API layer handles "Invalid page" errors by resetting and retrying
      // Just re-throw to show notification for actual errors
      throw error;
    }
  }, [
    pagination,
    sorting,
    debouncedFilters,
    showDisabled,
    selectedProfileId,
    showOnlyStreamlessChannels,
    showOnlyStaleChannels,
    showOnlyOverriddenChannels,
    visibilityFilter,
  ]);

  const stopPropagation = useCallback((e) => {
    e.stopPropagation();
  }, []);

  const handleFilterChange = (e) => {
    const { name, value } = e.target;
    // Then update filters
    setFilters((prev) => ({
      ...prev,
      [name]: value,
    }));
  };

  const handleGroupChange = (value) => {
    // Then update filters
    setFilters((prev) => ({
      ...prev,
      channel_group: value ? value : '',
    }));
  };

  const handleEPGChange = (value) => {
    // Map 'null' (string) back to 'null' for backend, but keep UI label correct
    const processedValue = value
      ? value.map((v) => (v === 'null' ? 'null' : v))
      : '';
    setFilters((prev) => ({
      ...prev,
      epg: processedValue,
    }));
  };

  const editChannel = useCallback(async (ch = null, opts = {}) => {
    if (opts.forceAdd) {
      setChannel(null);
      setChannelModalOpen(true);
      return;
    }
    const currentSelection =
      useChannelsTableStore.getState().selectedChannelIds;
    console.log('editChannel called with:', { ch, currentSelection });

    if (currentSelection.length > 1) {
      setChannelBatchModalOpen(true);
    } else {
      let channelToEdit = ch;
      if (!channelToEdit && currentSelection.length === 1) {
        const selectedId = currentSelection[0];
        channelToEdit = useChannelsTableStore
          .getState()
          .channels.find((d) => d.id === selectedId);
      }
      setChannel(channelToEdit);
      setChannelModalOpen(true);
    }
  }, []);

  const deleteChannel = async (id) => {
    console.log(`Deleting channel with ID: ${id}`);

    const rows = table.getRowModel().rows;
    const knownChannel = rows.find((row) => row.original.id === id)?.original;

    table.setSelectedTableIds([]);

    if (selectedChannelIds.length > 0) {
      // Use bulk delete for multiple selections
      setIsBulkDelete(true);
      setChannelToDelete(null);

      if (isWarningSuppressed('delete-channels')) {
        // Skip warning if suppressed
        return executeDeleteChannels();
      }

      setConfirmDeleteOpen(true);
      return;
    }

    // Single channel delete
    setIsBulkDelete(false);
    setDeleteTarget(id);
    setChannelToDelete(knownChannel); // Store the channel object for displaying details

    if (isWarningSuppressed('delete-channel')) {
      // Skip warning if suppressed
      return executeDeleteChannel(id);
    }

    setConfirmDeleteOpen(true);
  };

  const executeDeleteChannel = async (id) => {
    setDeleting(true);
    try {
      await API.deleteChannel(id);
      API.requeryChannels();
    } finally {
      setDeleting(false);
      setConfirmDeleteOpen(false);
    }
  };

  const deleteChannels = async () => {
    if (isWarningSuppressed('delete-channels')) {
      // Skip warning if suppressed
      return executeDeleteChannels();
    }

    setIsBulkDelete(true);
    setConfirmDeleteOpen(true);
  };

  const executeDeleteChannels = async () => {
    setIsLoading(true);
    setDeleting(true);
    try {
      await API.deleteChannels(table.selectedTableIds);
      await API.requeryChannels();
      setSelectedChannelIds([]);
      table.setSelectedTableIds([]);
    } finally {
      setDeleting(false);
      setIsLoading(false);
      setConfirmDeleteOpen(false);
    }
  };

  const createRecording = useCallback((channel) => {
    console.log(`Recording channel ID: ${channel.id}`);
    setChannel(channel);
    setRecordingModalOpen(true);
  }, []);

  const getChannelURL = useCallback(
    (channel) => {
      // Make sure we're using the channel UUID consistently
      if (!channel || !channel.uuid) {
        console.error('Invalid channel object or missing UUID:', channel);
        return '';
      }

      const uri = buildLiveStreamUrl(`/proxy/ts/stream/${channel.uuid}`);
      let channelUrl = `${window.location.protocol}//${window.location.host}${uri}`;
      if (env_mode == 'dev') {
        channelUrl = `${window.location.protocol}//${window.location.hostname}:5656${uri}`;
      }

      return channelUrl;
    },
    [env_mode]
  );

  const handleWatchStream = useCallback(
    (channel) => {
      const url = getChannelURL(channel);
      showVideo(url, 'live', { name: channel.name, channelId: channel.id });
    },
    [getChannelURL, showVideo]
  );

  const onRowSelectionChange = (newSelection) => {
    setSelectedChannelIds(newSelection);
  };

  const onRowExpansionChange = useCallback(
    (expandedIds) => {
      setExpandedChannelId(expandedIds.length > 0 ? expandedIds[0] : null);
    },
    [setExpandedChannelId]
  );

  const onPageSizeChange = (e) => {
    setPagination({
      ...pagination,
      pageSize: e.target.value,
    });
  };

  const onPageIndexChange = (pageIndex) => {
    if (!pageIndex || pageIndex > pageCount) {
      return;
    }

    setPagination({
      ...pagination,
      pageIndex: pageIndex - 1,
    });
  };

  const closeChannelBatchForm = () => {
    setChannelBatchModalOpen(false);
  };

  const closeChannelForm = () => {
    setChannel(null);
    setChannelModalOpen(false);
  };

  const closeRecordingForm = () => {
    // setChannel(null);
    setRecordingModalOpen(false);
  };

  // Build URLs with parameters
  const buildM3UUrl = () => {
    const params = new URLSearchParams();
    if (!m3uParams.cachedlogos) params.append('cachedlogos', 'false');
    if (m3uParams.direct) params.append('direct', 'true');
    if (m3uParams.tvg_id_source !== 'channel_number')
      params.append('tvg_id_source', m3uParams.tvg_id_source);
    if (m3uParams.output_format)
      params.append('output_format', m3uParams.output_format);
    if (m3uParams.output_profile)
      params.append('output_profile', m3uParams.output_profile);

    const baseUrl = m3uUrl;
    return params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
  };

  const buildEPGUrl = () => {
    const params = new URLSearchParams();
    if (!epgParams.cachedlogos) params.append('cachedlogos', 'false');
    if (epgParams.tvg_id_source !== 'channel_number')
      params.append('tvg_id_source', epgParams.tvg_id_source);
    if (epgParams.days > 0) params.append('days', epgParams.days.toString());
    if (epgParams.prev_days > 0)
      params.append('prev_days', epgParams.prev_days.toString());

    const baseUrl = epgUrl;
    return params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
  };
  // Example copy URLs
  const copyM3UUrl = async () => {
    await copyToClipboard(buildM3UUrl(), {
      successTitle: 'M3U URL Copied!',
      successMessage: 'The M3U URL has been copied to your clipboard.',
    });
  };

  const copyEPGUrl = async () => {
    await copyToClipboard(buildEPGUrl(), {
      successTitle: 'EPG URL Copied!',
      successMessage: 'The EPG URL has been copied to your clipboard.',
    });
  };

  const copyHDHRUrl = async () => {
    await copyToClipboard(hdhrUrl, {
      successTitle: 'HDHR URL Copied!',
      successMessage: 'The HDHR URL has been copied to your clipboard.',
    });
  };

  const onSortingChange = (column) => {
    const sortField = sorting[0]?.id;
    const sortDirection = sorting[0]?.desc;

    if (sortField == column) {
      if (sortDirection == false) {
        setSorting([
          {
            id: column,
            desc: true,
          },
        ]);
      } else {
        setSorting([]);
      }
    } else {
      setSorting([
        {
          id: column,
          desc: false,
        },
      ]);
    }
  };

  const handleDragEnd = async (event) => {
    const { active, over } = event;

    if (!over || active.id === over.id) {
      return;
    }

    const activeIndex = rows.findIndex((row) => row.id === active.id);
    const overIndex = rows.findIndex((row) => row.id === over.id);

    if (activeIndex === -1 || overIndex === -1) {
      return;
    }

    const activeChannel = rows[activeIndex].original;
    const overChannel = rows[overIndex].original;

    try {
      // Optimistically update the local state
      const reorderedData = [...data];
      const [movedItem] = reorderedData.splice(activeIndex, 1);
      reorderedData.splice(overIndex, 0, movedItem);
      useChannelsTableStore.setState({ channels: reorderedData });

      // Call backend to reorder
      await API.reorderChannel(
        activeChannel.id,
        overIndex > activeIndex
          ? overChannel.id
          : rows[overIndex - 1]?.original.id || null
      );

      // Refetch to get updated channel numbers
      await API.requeryChannels();
    } catch (error) {
      // Revert on error
      console.error('Failed to reorder channel:', error);
      await API.requeryChannels();
    }
  };

  /**
   * useEffect
   */
  useEffect(() => {
    fetchData();
  }, [fetchData]);

  useEffect(() => {
    const profileString =
      selectedProfileId != '0' ? `/${profiles[selectedProfileId].name}` : '';
    setHDHRUrl(`${hdhrUrlBase}${profileString}`);
    setEPGUrl(`${epgUrlBase}${profileString}`);
    setM3UUrl(`${m3uUrlBase}${profileString}`);
  }, [selectedProfileId, profiles]);

  useEffect(() => {
    const startItem = pagination.pageIndex * pagination.pageSize + 1; // +1 to start from 1, not 0
    const endItem = Math.min(
      (pagination.pageIndex + 1) * pagination.pageSize,
      totalCount
    );
    setPaginationString(`${startItem} to ${endItem} of ${totalCount}`);
  }, [pagination.pageIndex, pagination.pageSize, totalCount]);

  // Signal ready when EPG data finishes loading (if channels were already fetched)
  useEffect(() => {
    if (
      hasFetchedData.current &&
      !hasSignaledReady.current &&
      onReady &&
      tvgsLoaded
    ) {
      hasSignaledReady.current = true;
      onReady();
    }
  }, [tvgsLoaded, onReady]);

  const columns = useMemo(
    () => [
      {
        id: 'expand',
        size: 20,
        enableResizing: false,
      },
      {
        id: 'select',
        size: 30,
        enableResizing: false,
      },
      {
        id: 'enabled',
        size: 45,
        enableResizing: false,
        cell: ({ row, table }) => {
          return (
            <ChannelEnabledSwitch
              rowId={row.original.id}
              selectedProfileId={selectedProfileId}
              selectedTableIds={table.getState().selectedTableIds}
            />
          );
        },
      },
      {
        id: 'channel_number',
        // Prefer the backend-resolved effective_channel_number so overrides
        // show through to the table. Inline save still writes to the
        // override row via buildInlinePatch in EditableCell.
        accessorFn: (row) => row.effective_channel_number ?? row.channel_number,
        size: columnSizing.channel_number || 40,
        minSize: 30,
        maxSize: 100,
        cell: (props) => <EditableNumberCell {...props} />,
      },
      {
        id: 'name',
        accessorFn: (row) => row.effective_name ?? row.name,
        size: columnSizing.name || 200,
        minSize: 100,
        grow: true,
        cell: (props) => {
          const row = props.row?.original || {};
          const overriddenLabels = listOverriddenFields(row);
          return (
            <Flex align="center" gap={6} style={{ minWidth: 0 }}>
              <Box style={{ minWidth: 0, flex: 1 }}>
                <EditableTextCell {...props} />
              </Box>
              {overriddenLabels.length > 0 && (
                <Tooltip
                  label={`Overrides active: ${overriddenLabels.join(', ')}`}
                >
                  <Box
                    component="span"
                    role="img"
                    aria-label={`Overrides active: ${overriddenLabels.join(', ')}`}
                    style={{ display: 'inline-flex' }}
                  >
                    <Pencil size={14} color="#eab308" aria-hidden="true" />
                  </Box>
                </Tooltip>
              )}
              {row.hidden_from_output && (
                <Tooltip label="Hidden from HDHR, M3U, EPG, and XC output.">
                  <Box
                    component="span"
                    role="img"
                    aria-label="Hidden from HDHR, M3U, EPG, and XC output"
                    style={{ display: 'inline-flex' }}
                  >
                    <EyeOff size={14} color="#9ca3af" aria-hidden="true" />
                  </Box>
                </Tooltip>
              )}
            </Flex>
          );
        },
      },
      {
        id: 'epg',
        header: 'EPG',
        // Effective EPG id so overridden EPG assignments show in the table.
        accessorFn: (row) => row.effective_epg_data_id ?? row.epg_data_id,
        cell: (props) => (
          <EditableEPGCell
            {...props}
            tvgsById={tvgsById}
            epgs={epgs}
            tvgsLoaded={tvgsLoaded}
          />
        ),
        size: columnSizing.epg || 200,
        minSize: 120,
      },
      {
        id: 'channel_group',
        accessorFn: (row) => {
          const effectiveGroupId =
            row.effective_channel_group_id ?? row.channel_group_id;
          return channelGroups[effectiveGroupId]
            ? channelGroups[effectiveGroupId].name
            : '';
        },
        cell: (props) => (
          <EditableGroupCell {...props} channelGroups={channelGroups} />
        ),
        size: columnSizing.channel_group || 200,
        minSize: 120,
      },
      {
        id: 'logo',
        accessorFn: (row) => row.effective_logo_id ?? row.logo_id,
        size: 75,
        minSize: 50,
        maxSize: 120,
        enableResizing: false,
        header: '',
        cell: (props) => (
          <EditableLogoCell
            {...props}
            LazyLogo={LazyLogo}
            ensureLogosLoaded={ensureLogosLoaded}
          />
        ),
      },
      {
        id: 'actions',
        size: 100,
        enableResizing: false,
        header: '',
        cell: ({ row, table }) => (
          <ChannelRowActions
            theme={theme}
            row={row}
            table={table}
            editChannel={editChannel}
            deleteChannel={deleteChannel}
            handleWatchStream={handleWatchStream}
            createRecording={createRecording}
            getChannelURL={getChannelURL}
          />
        ),
      },
    ],
    // Note: columnSizing is intentionally excluded from dependencies to prevent
    // columns from being recreated during drag operations (which causes infinite loops).
    // The column.size values are only used for INITIAL sizing - TanStack Table manages
    // the actual sizes through its own state after initialization.
    // Note: logos is intentionally excluded - LazyLogo components handle their own logo data
    // from the store, so we don't need to recreate columns when logos load.
    // Note: tvgsLoaded is intentionally excluded - EditableEPGCell handles loading state internally
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [selectedProfileId, channelGroups, theme, tvgsById, epgs, editChannel]
  );

  const renderHeaderCell = (header) => {
    let sortingIcon = ArrowUpDown;
    if (sorting[0]?.id == header.id) {
      if (sorting[0].desc === false) {
        sortingIcon = ArrowUpNarrowWide;
      } else {
        sortingIcon = ArrowDownWideNarrow;
      }
    }

    switch (header.id) {
      case 'epg':
        return (
          <MultiSelect
            placeholder="EPG"
            variant="unstyled"
            data={epgSelectOptions}
            className="table-input-header"
            size="xs"
            searchable
            clearable
            onClick={stopPropagation}
            onChange={handleEPGChange}
            value={
              Array.isArray(filters.epg)
                ? filters.epg
                : filters.epg
                  ? filters.epg.split(',').filter(Boolean)
                  : []
            }
            style={{ width: '100%' }}
            rightSectionPointerEvents="auto"
            rightSection={React.createElement(sortingIcon, {
              onClick: (e) => {
                e.stopPropagation();
                onSortingChange('epg');
              },
              size: 14,
              style: { cursor: 'pointer' },
            })}
          />
        );
      case 'enabled':
        return (
          <Center style={{ width: '100%' }}>
            <ScanEye size="16" />
          </Center>
        );

      case 'channel_number':
        return (
          <Flex gap={2} align="center">
            #
            <Center
              onClick={(e) => {
                e.stopPropagation();
                onSortingChange('channel_number');
              }}
              style={{ cursor: 'pointer' }}
            >
              {React.createElement(sortingIcon, {
                size: 14,
              })}
            </Center>
          </Flex>
        );

      case 'name':
        return (
          <TextInput
            name="name"
            placeholder="Name"
            value={filters.name || ''}
            onClick={(e) => e.stopPropagation()}
            onChange={handleFilterChange}
            size="xs"
            variant="unstyled"
            className="table-input-header"
            leftSection={<Search size={14} opacity={0.5} />}
            style={{ width: '100%' }}
            rightSectionPointerEvents="auto"
            rightSection={React.createElement(sortingIcon, {
              onClick: (e) => {
                e.stopPropagation();
                onSortingChange('name');
              },
              size: 14,
              style: { cursor: 'pointer' },
            })}
          />
        );

      case 'channel_group':
        return (
          <MultiSelect
            placeholder="Group"
            className="table-input-header"
            variant="unstyled"
            data={groupOptions}
            size="xs"
            searchable
            clearable
            onClick={stopPropagation}
            onChange={handleGroupChange}
            value={
              Array.isArray(filters.channel_group)
                ? filters.channel_group
                : filters.channel_group
                  ? filters.channel_group.split(',').filter(Boolean)
                  : []
            }
            style={{ width: '100%' }}
            rightSectionPointerEvents="auto"
            rightSection={React.createElement(sortingIcon, {
              onClick: (e) => {
                e.stopPropagation();
                onSortingChange('channel_group');
              },
              size: 14,
              style: { cursor: 'pointer' },
            })}
          />
        );
    }
  };

  const table = useTable({
    data,
    columns,
    allRowIds,
    pageCount,
    filters,
    pagination,
    sorting,
    columnSizing,
    setColumnSizing,
    manualPagination: true,
    manualSorting: true,
    manualFiltering: true,
    enableRowSelection: true,
    enableDragDrop: true,
    onRowSelectionChange: onRowSelectionChange,
    onRowExpansionChange: onRowExpansionChange,
    state: {
      pagination,
      sorting,
    },
    columnResizeMode: 'onChange',
    getExpandedRowHeight: (row) => {
      return 20 + 28 * row.original.streams.length;
    },
    expandedRowRenderer: ({ row }) => {
      return (
        <Box
          key={row.id}
          className="tr"
          style={{ display: 'flex', width: '100%' }}
        >
          <ChannelTableStreams channel={row.original} />
        </Box>
      );
    },
    headerCellRenderFns: {
      name: renderHeaderCell,
      channel_number: renderHeaderCell,
      channel_group: renderHeaderCell,
      enabled: renderHeaderCell,
      epg: renderHeaderCell,
    },
    getRowStyles: (row) => {
      const cls = rowClassMap[row.original.id];
      return cls ? { className: cls } : {};
    },
  });

  const rows = table.getRowModel().rows;

  return (
    <>
      <Box>
        {/* Header Row: outside the Paper */}
        <Flex style={{ alignItems: 'center', paddingBottom: 10 }} gap={15}>
          <Text
            w={88}
            h={24}
            style={{
              fontFamily: 'Inter, sans-serif',
              fontWeight: 500,
              fontSize: '20px',
              lineHeight: 1,
              letterSpacing: '-0.3px',
              color: 'gray.6', // Adjust this to match MUI's theme.palette.text.secondary
              marginBottom: 0,
            }}
          >
            Channels
          </Text>
          <Flex
            style={{
              display: 'flex',
              alignItems: 'center',
              marginLeft: 10,
            }}
          >
            <Text
              w={37}
              h={17}
              style={{
                fontFamily: 'Inter, sans-serif',
                fontWeight: 400,
                fontSize: '14px',
                lineHeight: 1,
                letterSpacing: '-0.3px',
                color: 'gray.6', // Adjust this to match MUI's theme.palette.text.secondary
              }}
            >
              Links:
            </Text>
            <Group gap={5} style={{ paddingLeft: 10 }}>
              <Popover
                withArrow
                shadow="md"
                zIndex={1000}
                position="bottom-start"
                withinPortal
              >
                <Popover.Target>
                  <Button
                    leftSection={<Tv2 size={18} />}
                    size="compact-sm"
                    p={5}
                    color="green"
                    variant="subtle"
                    style={{
                      borderColor: theme.palette.custom.greenMain,
                      color: theme.palette.custom.greenMain,
                    }}
                  >
                    HDHR
                  </Button>
                </Popover.Target>
                <Popover.Dropdown>
                  <Stack
                    gap="sm"
                    style={{
                      minWidth: 250,
                      maxWidth: 'min(400px, 80vw)',
                      width: 'max-content',
                    }}
                  >
                    <Text size="sm" c="dimmed">
                      Use this URL in HDHomeRun-compatible apps and IPTV
                      clients.
                    </Text>
                    <TextInput
                      value={hdhrUrl}
                      size="sm"
                      readOnly
                      style={{ width: '100%' }}
                      rightSection={
                        <ActionIcon
                          onClick={copyHDHRUrl}
                          size="sm"
                          variant="transparent"
                          color="gray.5"
                        >
                          <Copy size="16" />
                        </ActionIcon>
                      }
                    />
                  </Stack>
                </Popover.Dropdown>
              </Popover>
              <Popover
                withArrow
                shadow="md"
                zIndex={1000}
                position="bottom-start"
                withinPortal
              >
                <Popover.Target>
                  <Button
                    leftSection={<ScreenShare size={18} />}
                    size="compact-sm"
                    p={5}
                    variant="subtle"
                    style={{
                      borderColor: theme.palette.custom.indigoMain,
                      color: theme.palette.custom.indigoMain,
                    }}
                  >
                    M3U
                  </Button>
                </Popover.Target>
                <Popover.Dropdown>
                  <Stack
                    gap="sm"
                    style={{
                      minWidth: 300,
                      maxWidth: 'min(500px, 90vw)',
                      width: 'max-content',
                    }}
                    onClick={stopPropagation}
                    onMouseDown={stopPropagation}
                  >
                    <Text size="sm" c="dimmed">
                      Use this URL in your media player or IPTV app to load your
                      channel list.
                    </Text>
                    <TextInput
                      value={buildM3UUrl()}
                      size="sm"
                      readOnly
                      label="Generated URL"
                      rightSection={
                        <ActionIcon
                          onClick={copyM3UUrl}
                          size="sm"
                          variant="transparent"
                          color="gray.5"
                        >
                          <Copy size="16" />
                        </ActionIcon>
                      }
                    />
                    <Switch
                      label="Use cached logos"
                      description="Proxy channel logos through Dispatcharr"
                      size="sm"
                      checked={m3uParams.cachedlogos}
                      onChange={(event) =>
                        setM3uParams((prev) => ({
                          ...prev,
                          cachedlogos: event.target.checked,
                        }))
                      }
                    />
                    <Switch
                      label="Direct stream URLs"
                      description="Bypass the Dispatcharr proxy; client connects directly to the source"
                      size="sm"
                      checked={m3uParams.direct}
                      onChange={(event) =>
                        setM3uParams((prev) => ({
                          ...prev,
                          direct: event.target.checked,
                        }))
                      }
                    />
                    <Select
                      label="TVG-ID Source"
                      description="Value used as the tvg-id attribute in the M3U"
                      size="sm"
                      value={m3uParams.tvg_id_source}
                      onChange={(value) =>
                        setM3uParams((prev) => ({
                          ...prev,
                          tvg_id_source: value,
                        }))
                      }
                      comboboxProps={{ withinPortal: false }}
                      data={[
                        { value: 'channel_number', label: 'Channel Number' },
                        { value: 'tvg_id', label: 'TVG-ID' },
                        { value: 'gracenote', label: 'Gracenote Station ID' },
                      ]}
                    />
                    <Select
                      label="Output Format"
                      description="Container format for streams embedded in this M3U"
                      clearable
                      placeholder="Server default"
                      value={m3uParams.output_format || null}
                      onChange={(value) =>
                        setM3uParams((prev) => ({
                          ...prev,
                          output_format: value || '',
                        }))
                      }
                      comboboxProps={{ withinPortal: false }}
                      data={[
                        { value: 'mpegts', label: 'MPEG-TS' },
                        { value: 'fmp4', label: 'fMP4 (fragmented MP4)' },
                      ]}
                    />
                    <Select
                      label="Output Profile"
                      description="Pre-delivery transcode profile applied to all streams in this M3U"
                      clearable
                      searchable
                      placeholder="No transcoding"
                      value={m3uParams.output_profile || null}
                      onChange={(value) =>
                        setM3uParams((prev) => ({
                          ...prev,
                          output_profile: value || '',
                        }))
                      }
                      comboboxProps={{ withinPortal: false }}
                      data={outputProfiles
                        .filter((p) => p.is_active)
                        .map((p) => ({ value: `${p.id}`, label: p.name }))}
                    />
                  </Stack>
                </Popover.Dropdown>
              </Popover>
              <Popover
                withArrow
                shadow="md"
                zIndex={1000}
                position="bottom-start"
                withinPortal
              >
                <Popover.Target>
                  <Button
                    leftSection={<Scroll size={18} />}
                    size="compact-sm"
                    p={5}
                    variant="subtle"
                    color="gray.5"
                    style={{
                      borderColor: theme.palette.custom.greyBorder,
                      color: theme.palette.custom.greyBorder,
                    }}
                  >
                    EPG
                  </Button>
                </Popover.Target>
                <Popover.Dropdown>
                  <Stack
                    gap="sm"
                    style={{
                      minWidth: 300,
                      maxWidth: 'min(450px, 85vw)',
                      width: 'max-content',
                    }}
                    onClick={stopPropagation}
                    onMouseDown={stopPropagation}
                  >
                    <Text size="sm" c="dimmed">
                      Use this URL in your IPTV app for program guide data.
                      Per-user defaults for days forward/back can be set in
                      account settings, which apply automatically for XC
                      clients.
                    </Text>
                    <TextInput
                      value={buildEPGUrl()}
                      size="sm"
                      readOnly
                      label="Generated URL"
                      rightSection={
                        <ActionIcon
                          onClick={copyEPGUrl}
                          size="sm"
                          variant="transparent"
                          color="gray.5"
                        >
                          <Copy size="16" />
                        </ActionIcon>
                      }
                    />
                    <Switch
                      label="Use cached logos"
                      description="Proxy channel logos through Dispatcharr"
                      size="sm"
                      checked={epgParams.cachedlogos}
                      onChange={(event) =>
                        setEpgParams((prev) => ({
                          ...prev,
                          cachedlogos: event.target.checked,
                        }))
                      }
                    />
                    <Select
                      label="TVG-ID Source"
                      description="Value used to match EPG channels to M3U streams"
                      size="sm"
                      value={epgParams.tvg_id_source}
                      onChange={(value) =>
                        setEpgParams((prev) => ({
                          ...prev,
                          tvg_id_source: value,
                        }))
                      }
                      comboboxProps={{ withinPortal: false }}
                      data={[
                        { value: 'channel_number', label: 'Channel Number' },
                        { value: 'tvg_id', label: 'TVG-ID' },
                        { value: 'gracenote', label: 'Gracenote Station ID' },
                      ]}
                    />
                    <NumberInput
                      label="Days forward (0 = all)"
                      description="Limit EPG to this many future days; 0 returns all available data"
                      size="sm"
                      min={0}
                      max={365}
                      value={epgParams.days}
                      onChange={(value) =>
                        setEpgParams((prev) => ({
                          ...prev,
                          days: value || 0,
                        }))
                      }
                    />
                    <NumberInput
                      label="Days back (0 = none)"
                      description="Include this many past days of EPG data (max 30)"
                      size="sm"
                      min={0}
                      max={30}
                      value={epgParams.prev_days}
                      onChange={(value) =>
                        setEpgParams((prev) => ({
                          ...prev,
                          prev_days: value || 0,
                        }))
                      }
                    />
                  </Stack>
                </Popover.Dropdown>
              </Popover>
            </Group>
          </Flex>
        </Flex>

        {/* Paper container: contains top toolbar and table (or ghost state) */}
        <Paper
          style={{
            display: 'flex',
            flexDirection: 'column',
            height: 'calc(100vh - 60px)',
            backgroundColor: '#27272A',
          }}
        >
          <ChannelTableHeader
            rows={rows}
            editChannel={editChannel}
            deleteChannels={deleteChannels}
            selectedTableIds={table.selectedTableIds}
            table={table}
            showDisabled={showDisabled}
            setShowDisabled={setShowDisabled}
            showOnlyStreamlessChannels={showOnlyStreamlessChannels}
            setShowOnlyStreamlessChannels={setShowOnlyStreamlessChannels}
            showOnlyStaleChannels={showOnlyStaleChannels}
            setShowOnlyStaleChannels={setShowOnlyStaleChannels}
            showOnlyOverriddenChannels={showOnlyOverriddenChannels}
            setShowOnlyOverriddenChannels={setShowOnlyOverriddenChannels}
            visibilityFilter={visibilityFilter}
            setVisibilityFilter={setVisibilityFilter}
          />

          {/* Table or ghost empty state inside Paper */}
          <Box>
            {channelsTableLength === 0 && !hasChannels && (
              <ChannelsTableOnboarding editChannel={editChannel} />
            )}
          </Box>

          {(channelsTableLength > 0 || hasChannels) && (
            <Box
              style={{
                display: 'flex',
                flexDirection: 'column',
                height: 'calc(100vh - 100px)',
              }}
            >
              <Box
                style={{
                  flex: 1,
                  overflowY: 'auto',
                  overflowX: 'auto',
                  border: 'solid 1px rgb(68,68,68)',
                  borderRadius: 'var(--mantine-radius-default)',
                }}
              >
                <DndContext
                  sensors={sensors}
                  collisionDetection={closestCenter}
                  onDragEnd={handleDragEnd}
                >
                  <SortableContext
                    items={rows.map((row) => row.id)}
                    strategy={verticalListSortingStrategy}
                  >
                    <CustomTable table={table} />
                  </SortableContext>
                </DndContext>
              </Box>

              <Box
                style={{
                  position: 'sticky',
                  bottom: 0,
                  zIndex: 3,
                  backgroundColor: '#27272A',
                }}
              >
                <Group
                  gap={5}
                  justify="center"
                  style={{
                    padding: 8,
                    borderTop: '1px solid #666',
                  }}
                >
                  <Text size="xs">Page Size</Text>
                  <NativeSelect
                    size="xxs"
                    value={pagination.pageSize}
                    data={['25', '50', '100', '250']}
                    onChange={onPageSizeChange}
                    style={{ paddingRight: 20 }}
                  />
                  <Pagination
                    total={pageCount}
                    value={pagination.pageIndex + 1}
                    onChange={onPageIndexChange}
                    size="xs"
                    withEdges
                    style={{ paddingRight: 20 }}
                  />
                  <Text size="xs">{paginationString}</Text>
                </Group>
              </Box>
            </Box>
          )}
        </Paper>

        <ChannelForm
          channel={channel}
          isOpen={channelModalOpen}
          onClose={closeChannelForm}
        />

        <ChannelBatchForm
          channelIds={selectedChannelIds}
          isOpen={channelBatchModalOpen}
          onClose={closeChannelBatchForm}
        />

        <RecordingForm
          channel={channel}
          isOpen={recordingModalOpen}
          onClose={closeRecordingForm}
        />
      </Box>

      <ConfirmationDialog
        opened={confirmDeleteOpen}
        onClose={() => setConfirmDeleteOpen(false)}
        onConfirm={() =>
          isBulkDelete
            ? executeDeleteChannels()
            : executeDeleteChannel(deleteTarget)
        }
        loading={deleting}
        title={`Confirm ${isBulkDelete ? 'Bulk ' : ''}Channel Deletion`}
        message={
          isBulkDelete ? (
            `Are you sure you want to delete ${table.selectedTableIds.length} channels? This action cannot be undone.`
          ) : channelToDelete ? (
            <div style={{ whiteSpace: 'pre-line' }}>
              {`Are you sure you want to delete the following channel?

Name: ${channelToDelete.name}
Channel Number: ${channelToDelete.channel_number}

This action cannot be undone.`}
            </div>
          ) : (
            'Are you sure you want to delete this channel? This action cannot be undone.'
          )
        }
        confirmLabel="Delete"
        cancelLabel="Cancel"
        actionKey={isBulkDelete ? 'delete-channels' : 'delete-channel'}
        onSuppressChange={suppressWarning}
        size="md"
      />
    </>
  );
};

export default ChannelsTable;
