import 'dart:async';

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:flutter/services.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';

import '../../../focus/dpad_navigator.dart';
import '../../../focus/input_mode_tracker.dart';
import '../../../focus/key_event_utils.dart';
import '../../../i18n/strings.g.dart';
import '../../../mixins/mounted_set_state_mixin.dart';
import '../../../models/livetv_channel.dart';
import '../../../models/livetv_program.dart';
import '../../../models/media_grab_operation.dart';
import '../../../providers/multi_server_provider.dart';
import '../../../media/media_server_client.dart';
import '../../../utils/app_logger.dart';
import '../../../utils/formatters.dart';
import '../../../utils/media_image_helper.dart';
import '../../../utils/live_tv_player_navigation.dart';
import '../../../widgets/app_icon.dart';
import '../../../widgets/overlay_sheet.dart';
import '../../../widgets/optimized_media_image.dart';
import '../program_details_sheet.dart';

class GuideTab extends StatefulWidget {
  final List<LiveTvChannel> channels;
  final bool Function(LiveTvChannel channel)? isFavoriteChannel;
  final void Function(LiveTvChannel)? onToggleFavorite;
  final VoidCallback? onNavigateUp;
  final VoidCallback? onBack;

  const GuideTab({
    super.key,
    required this.channels,
    this.isFavoriteChannel,
    this.onToggleFavorite,
    this.onNavigateUp,
    this.onBack,
  });

  @override
  State<GuideTab> createState() => GuideTabState();
}

enum _GuideZone { timeNav, grid }

class GuideTabState extends State<GuideTab> with MountedSetStateMixin {
  static const _slotWidth = 180.0;
  static const _channelColumnWidth = 100.0;
  static const _rowHeight = 64.0;
  static const _timeHeaderHeight = 40.0;
  static const _minutesPerSlot = 30;

  List<LiveTvProgram> _programs = [];
  Set<String> _scheduledRecordingKeys = const {};
  bool _isLoading = true;

  late DateTime _gridStart;
  late DateTime _gridEnd;

  final ScrollController _headerHorizontalController = ScrollController();
  final ScrollController _gridHorizontalController = ScrollController();
  final ScrollController _channelVerticalController = ScrollController();
  final ScrollController _gridVerticalController = ScrollController();
  bool _syncingScroll = false;

  Timer? _timeIndicatorTimer;
  final _dayPickerKey = GlobalKey();

  // Focus state
  final FocusNode _guideFocusNode = FocusNode(debugLabel: 'guide_tab');
  _GuideZone _focusZone = _GuideZone.timeNav;
  int _timeNavIndex = 1; // 0=left arrow, 1=day picker, 2=right arrow
  int _gridChannelIndex = 0;
  int _gridColumn = 0; // 0=channel, 1=program
  bool _hasFocus = false;
  final ValueNotifier<bool> _hasFocusNotifier = ValueNotifier(false);
  LiveTvProgram? _focusedProgram;
  bool _pendingFocus = false;

  /// Focus into the guide content (called from tab bar navigation or initial load).
  void focusContent() {
    if (!InputModeTracker.isKeyboardMode(context)) return;
    // If still loading programs, defer until the Focus widget is in the tree.
    if (_isLoading) {
      _pendingFocus = true;
      return;
    }
    _pendingFocus = false;
    _guideFocusNode.requestFocus();
    setState(() {
      if (widget.channels.isNotEmpty) {
        _focusZone = _GuideZone.grid;
        _gridColumn = 0;
        _gridChannelIndex = 0;
        _focusedProgram = null;
      } else {
        _focusZone = _GuideZone.timeNav;
        _timeNavIndex = 1;
      }
    });
  }

  @override
  void initState() {
    super.initState();
    _initTimeRange();
    _loadPrograms();

    _gridHorizontalController.addListener(_syncGridToHeader);
    _headerHorizontalController.addListener(_syncHeaderToGrid);

    _timeIndicatorTimer = Timer.periodic(const Duration(minutes: 1), (_) {
      // ignore: no-empty-block - setState triggers rebuild to update time indicator
      setStateIfMounted(() {});
    });
  }

  void pauseRefresh() => _timeIndicatorTimer?.cancel();

  void resumeRefresh() {
    _timeIndicatorTimer?.cancel();
    _timeIndicatorTimer = Timer.periodic(const Duration(minutes: 1), (_) {
      // ignore: no-empty-block - setState triggers rebuild to update time indicator
      setStateIfMounted(() {});
    });
    unawaited(_refreshScheduledRecordingKeys());
  }

  @override
  void didUpdateWidget(GuideTab oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.channels.isNotEmpty && _gridChannelIndex >= widget.channels.length) {
      _gridChannelIndex = widget.channels.length - 1;
    }
  }

  @override
  void dispose() {
    _guideFocusNode.dispose();
    _gridVerticalController.dispose();
    _gridHorizontalController.removeListener(_syncGridToHeader);
    _headerHorizontalController.removeListener(_syncHeaderToGrid);
    _headerHorizontalController.dispose();
    _gridHorizontalController.dispose();
    _channelVerticalController.dispose();
    _timeIndicatorTimer?.cancel();
    _hasFocusNotifier.dispose();
    super.dispose();
  }

  void _handleGuideFocusChange(bool hasFocus) {
    if (_hasFocus == hasFocus) return;
    _hasFocus = hasFocus;
    _hasFocusNotifier.value = hasFocus;
  }

  void _syncGridToHeader() {
    if (_syncingScroll) return;
    _syncingScroll = true;
    if (_headerHorizontalController.hasClients) {
      _headerHorizontalController.jumpTo(_gridHorizontalController.offset);
    }
    _syncingScroll = false;
  }

  void _syncHeaderToGrid() {
    if (_syncingScroll) return;
    _syncingScroll = true;
    if (_gridHorizontalController.hasClients) {
      _gridHorizontalController.jumpTo(_headerHorizontalController.offset);
    }
    _syncingScroll = false;
  }

  void _initTimeRange() {
    final now = DateTime.now();
    _gridStart = DateTime(now.year, now.month, now.day, now.hour);
    if (now.minute >= 30) {
      _gridStart = _gridStart.add(const Duration(minutes: 30));
    }
    _gridStart = _gridStart.subtract(const Duration(hours: 1));
    _gridEnd = _gridStart.add(const Duration(hours: 6));
  }

  void _shiftTimeRange(int hours) {
    setState(() {
      _gridStart = _gridStart.add(Duration(hours: hours));
      _gridEnd = _gridStart.add(const Duration(hours: 6));
    });
    _loadPrograms();
  }

  void _jumpToNow() {
    _initTimeRange();
    _loadPrograms();
  }

  Future<void> _loadPrograms() async {
    if (!mounted) return;
    setState(() => _isLoading = true);

    try {
      final multiServer = context.read<MultiServerProvider>();
      final liveTvServers = multiServer.liveTvServers;
      final allPrograms = <LiveTvProgram>[];
      final scheduledRecordingKeys = <String>{};
      final queriedServers = <String>{};

      for (final serverInfo in liveTvServers) {
        if (!queriedServers.add(serverInfo.serverId)) continue;
        try {
          final genericClient = multiServer.getClientForServer(serverInfo.serverId);
          if (genericClient == null) continue;

          final startEpoch = _gridStart.millisecondsSinceEpoch ~/ 1000;
          final endEpoch = _gridEnd.millisecondsSinceEpoch ~/ 1000;

          final fromDt = DateTime.fromMillisecondsSinceEpoch(startEpoch * 1000, isUtc: true);
          final toDt = DateTime.fromMillisecondsSinceEpoch(endEpoch * 1000, isUtc: true);
          final programs = await genericClient.liveTv.fetchSchedule(from: fromDt, to: toDt);
          allPrograms.addAll(programs);
          await _addScheduledRecordingKeysForServer(
            client: genericClient,
            serverId: serverInfo.serverId,
            keys: scheduledRecordingKeys,
          );
        } catch (e) {
          appLogger.e('Failed to load programs from server ${serverInfo.serverId}', error: e);
        }
      }

      if (!mounted) return;

      final shouldFocus = _pendingFocus;

      setState(() {
        _programs = allPrograms;
        _scheduledRecordingKeys = scheduledRecordingKeys;
        _isLoading = false;
      });

      _scrollToNow();

      if (shouldFocus) {
        WidgetsBinding.instance.addPostFrameCallback((_) {
          if (mounted) focusContent();
        });
      }
    } catch (e) {
      appLogger.e('Failed to load guide programs', error: e);
      if (mounted) {
        setState(() => _isLoading = false);
      }
    }
  }

  Future<void> _refreshScheduledRecordingKeys() async {
    if (!mounted) return;
    final multiServer = context.read<MultiServerProvider>();
    final scheduledRecordingKeys = <String>{};
    final queriedServers = <String>{};

    for (final serverInfo in multiServer.liveTvServers) {
      if (!queriedServers.add(serverInfo.serverId)) continue;
      final client = multiServer.getClientForServer(serverInfo.serverId);
      if (client == null) continue;
      await _addScheduledRecordingKeysForServer(
        client: client,
        serverId: serverInfo.serverId,
        keys: scheduledRecordingKeys,
      );
    }

    if (!mounted) return;
    setState(() => _scheduledRecordingKeys = scheduledRecordingKeys);
  }

  Future<void> _addScheduledRecordingKeysForServer({
    required MediaServerClient client,
    required String serverId,
    required Set<String> keys,
  }) async {
    if (!client.capabilities.liveTvDvr) return;
    try {
      final grabs = await client.liveTv.fetchScheduledRecordings();
      for (final grab in grabs) {
        if (!_isActiveScheduledGrab(grab)) continue;
        final program = grab.program;
        if (program == null) continue;
        keys.addAll(_recordingKeysForProgram(program, fallbackServerId: serverId));
      }
    } catch (e) {
      appLogger.d('Failed to load scheduled recordings for $serverId', error: e);
    }
  }

  bool _isActiveScheduledGrab(MediaGrabOperation grab) {
    final status = grab.status?.toLowerCase();
    return status == null || status.isEmpty || status == 'scheduled' || status == 'grabbing';
  }

  bool _isRecordingScheduled(LiveTvProgram program) {
    final keys = _recordingKeysForProgram(program);
    return keys.any(_scheduledRecordingKeys.contains);
  }

  void _handleRecordingStateChanged(LiveTvProgram program, bool isScheduled) {
    final keys = _recordingKeysForProgram(program);
    if (keys.isNotEmpty) {
      setState(() {
        final next = {..._scheduledRecordingKeys};
        if (isScheduled) {
          next.addAll(keys);
        } else {
          next.removeAll(keys);
        }
        _scheduledRecordingKeys = next;
      });
    }
    unawaited(_refreshScheduledRecordingKeys());
  }

  Set<String> _recordingKeysForProgram(LiveTvProgram program, {String? fallbackServerId}) {
    final serverId = _nonEmpty(program.serverId) ?? _nonEmpty(fallbackServerId);
    if (serverId == null) return const <String>{};

    final keys = <String>{};
    void addMediaId(String? value) {
      final normalized = _nonEmpty(value);
      if (normalized != null) keys.add(_recordingKey(serverId, 'media', normalized));
    }

    addMediaId(program.ratingKey);
    addMediaId(program.guid);
    addMediaId(program.key);

    final channelIdentifier = _nonEmpty(program.channelIdentifier);
    final beginsAt = program.beginsAt;
    if (channelIdentifier != null && beginsAt != null) {
      keys.add(_recordingKey(serverId, 'slot', '$channelIdentifier|$beginsAt|${program.endsAt ?? ''}'));
    }

    return keys;
  }

  String _recordingKey(String serverId, String type, String value) => '$serverId\u0000$type\u0000$value';

  String? _nonEmpty(String? value) {
    final trimmed = value?.trim();
    return trimmed == null || trimmed.isEmpty ? null : trimmed;
  }

  void _scrollToNow() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      final now = DateTime.now();
      final minutesSinceStart = now.difference(_gridStart).inMinutes;
      final offset = (minutesSinceStart / _minutesPerSlot) * _slotWidth;
      if (_gridHorizontalController.hasClients) {
        _gridHorizontalController.jumpTo(
          (offset - MediaQuery.sizeOf(context).width / 3).clamp(0, _gridHorizontalController.position.maxScrollExtent),
        );
      }
    });
  }

  List<LiveTvProgram> _getProgramsForChannel(LiveTvChannel channel) {
    return _programs.where((p) => p.channelIdentifier == channel.key).toList()
      ..sort((a, b) => (a.beginsAt ?? 0).compareTo(b.beginsAt ?? 0));
  }

  double _totalGridWidth() {
    final totalMinutes = _gridEnd.difference(_gridStart).inMinutes;
    return (totalMinutes / _minutesPerSlot) * _slotWidth;
  }

  Future<void> _tuneChannel(LiveTvChannel channel) async {
    final multiServer = context.read<MultiServerProvider>();
    await tuneAndNavigateToLiveTv(context, multiServer: multiServer, channel: channel, channels: widget.channels);
  }

  KeyEventResult _handleKeyEvent(FocusNode _, KeyEvent event) {
    final key = event.logicalKey;

    // Back key
    if (key.isBackKey) {
      if (BackKeyUpSuppressor.consumeIfSuppressed(event)) {
        return KeyEventResult.handled;
      }
      if (_focusZone == _GuideZone.grid) {
        if (event is KeyUpEvent) {
          setState(() {
            _focusZone = _GuideZone.timeNav;
            _timeNavIndex = 1;
          });
        }
        return KeyEventResult.handled;
      }
      return handleBackKeyAction(event, () => widget.onBack?.call());
    }

    if (!event.isActionable) return KeyEventResult.ignored;

    return _focusZone == _GuideZone.timeNav ? _handleTimeNavKey(key) : _handleGridKey(key);
  }

  KeyEventResult _handleTimeNavKey(LogicalKeyboardKey key) {
    if (key.isLeftKey) {
      if (_timeNavIndex > 0) {
        setState(() => _timeNavIndex--);
      } else {
        widget.onBack?.call();
      }
      return KeyEventResult.handled;
    }
    if (key.isRightKey) {
      if (_timeNavIndex < 2) setState(() => _timeNavIndex++);
      return KeyEventResult.handled;
    }
    if (key.isDownKey) {
      if (widget.channels.isNotEmpty) {
        setState(() {
          _focusZone = _GuideZone.grid;
          _gridColumn = 0;
          _focusedProgram = null;
        });
        _scrollToChannel(_gridChannelIndex);
      }
      return KeyEventResult.handled;
    }
    if (key.isUpKey) {
      widget.onNavigateUp?.call();
      return KeyEventResult.handled;
    }
    if (key.isSelectKey) {
      switch (_timeNavIndex) {
        case 0:
          _shiftTimeRange(-2);
        case 1:
          _showDayPicker();
        case 2:
          _shiftTimeRange(2);
      }
      return KeyEventResult.handled;
    }
    return KeyEventResult.ignored;
  }

  KeyEventResult _handleGridKey(LogicalKeyboardKey key) {
    if (key.isUpKey) {
      if (_gridChannelIndex > 0) {
        setState(() {
          _gridChannelIndex--;
          if (_gridColumn == 1) _focusedProgram = _findCurrentProgram(_gridChannelIndex);
        });
        _scrollToChannel(_gridChannelIndex);
      } else {
        setState(() {
          _focusZone = _GuideZone.timeNav;
          _timeNavIndex = 1;
        });
      }
      return KeyEventResult.handled;
    }
    if (key.isDownKey) {
      if (_gridChannelIndex < widget.channels.length - 1) {
        setState(() {
          _gridChannelIndex++;
          if (_gridColumn == 1) _focusedProgram = _findCurrentProgram(_gridChannelIndex);
        });
        _scrollToChannel(_gridChannelIndex);
      }
      return KeyEventResult.handled;
    }
    if (key.isRightKey) {
      if (_gridColumn == 0) {
        final program = _findCurrentProgram(_gridChannelIndex);
        if (program != null) {
          setState(() {
            _gridColumn = 1;
            _focusedProgram = program;
          });
          _scrollToProgramTime(program);
        }
      } else {
        // Already in program column — move to next program
        _navigateToAdjacentProgram(_gridChannelIndex, forward: true);
      }
      return KeyEventResult.handled;
    }
    if (key.isLeftKey) {
      if (_gridColumn == 1) {
        // Try moving to previous program; if at first program, go back to channel column
        if (!_navigateToAdjacentProgram(_gridChannelIndex, forward: false)) {
          setState(() {
            _gridColumn = 0;
            _focusedProgram = null;
          });
        }
      } else {
        widget.onBack?.call();
      }
      return KeyEventResult.handled;
    }
    if (key.isSelectKey) {
      if (_gridChannelIndex >= 0 && _gridChannelIndex < widget.channels.length) {
        final channel = widget.channels[_gridChannelIndex];
        if (_gridColumn == 0) {
          _tuneChannel(channel);
        } else if (_focusedProgram != null) {
          _showProgramDetails(channel, _focusedProgram!);
        }
      }
      return KeyEventResult.handled;
    }
    // 'F' key toggles favorite on focused channel
    if (key == LogicalKeyboardKey.keyF && _gridColumn == 0) {
      if (_gridChannelIndex >= 0 && _gridChannelIndex < widget.channels.length) {
        widget.onToggleFavorite?.call(widget.channels[_gridChannelIndex]);
      }
      return KeyEventResult.handled;
    }
    return KeyEventResult.ignored;
  }

  LiveTvProgram? _findCurrentProgram(int channelIndex) {
    if (channelIndex < 0 || channelIndex >= widget.channels.length) return null;
    final channel = widget.channels[channelIndex];
    final programs = _getProgramsForChannel(channel);
    final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;

    // Currently airing
    for (final p in programs) {
      if ((p.beginsAt ?? 0) <= now && (p.endsAt ?? 0) > now) return p;
    }
    // First future program
    for (final p in programs) {
      if ((p.endsAt ?? 0) > now) return p;
    }
    return programs.firstOrNull;
  }

  /// Navigate to the next or previous program on the same channel.
  /// Returns true if navigation succeeded, false if at the boundary.
  bool _navigateToAdjacentProgram(int channelIndex, {required bool forward}) {
    if (channelIndex < 0 || channelIndex >= widget.channels.length) return false;
    final channel = widget.channels[channelIndex];
    final programs = _getProgramsForChannel(channel);
    if (programs.isEmpty || _focusedProgram == null) return false;

    final currentIndex = programs.indexWhere((p) => identical(p, _focusedProgram));
    if (currentIndex < 0) return false;

    final nextIndex = forward ? currentIndex + 1 : currentIndex - 1;
    if (nextIndex < 0 || nextIndex >= programs.length) return false;

    setState(() {
      _focusedProgram = programs[nextIndex];
    });
    _scrollToProgramTime(_focusedProgram);
    return true;
  }

  void _scrollToChannel(int index) {
    if (!_gridVerticalController.hasClients) return;
    final targetTop = index * _rowHeight;
    final targetBottom = targetTop + _rowHeight;
    final viewportTop = _gridVerticalController.offset;
    final viewportBottom = viewportTop + _gridVerticalController.position.viewportDimension;

    double? newOffset;
    if (targetTop < viewportTop) {
      newOffset = targetTop;
    } else if (targetBottom > viewportBottom) {
      newOffset = targetBottom - _gridVerticalController.position.viewportDimension;
    }

    if (newOffset != null) {
      final clamped = newOffset.clamp(0.0, _gridVerticalController.position.maxScrollExtent);
      _gridVerticalController.animateTo(clamped, duration: const Duration(milliseconds: 150), curve: Curves.easeOut);
      if (_channelVerticalController.hasClients) {
        _channelVerticalController.animateTo(
          clamped.clamp(0.0, _channelVerticalController.position.maxScrollExtent),
          duration: const Duration(milliseconds: 150),
          curve: Curves.easeOut,
        );
      }
    }
  }

  void _scrollToProgramTime(LiveTvProgram? program) {
    if (program == null || !_gridHorizontalController.hasClients) return;

    final gridStartEpoch = _gridStart.millisecondsSinceEpoch ~/ 1000;
    final gridEndEpoch = _gridEnd.millisecondsSinceEpoch ~/ 1000;
    final progStart = (program.beginsAt ?? gridStartEpoch).clamp(gridStartEpoch, gridEndEpoch);
    final startOffset = progStart - gridStartEpoch;
    final left = (startOffset / (_minutesPerSlot * 60)) * _slotWidth;

    final viewportWidth = _gridHorizontalController.position.viewportDimension;
    final currentOffset = _gridHorizontalController.offset;

    if (left < currentOffset || left > currentOffset + viewportWidth - 100) {
      final maxScroll = _gridHorizontalController.position.maxScrollExtent;
      _gridHorizontalController.jumpTo((left - 50).clamp(0.0, maxScroll));
    }
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    if (_isLoading) {
      return const Center(child: CircularProgressIndicator());
    }

    return OverlaySheetHost(
      child: Focus(
        focusNode: _guideFocusNode,
        onFocusChange: _handleGuideFocusChange,
        onKeyEvent: _handleKeyEvent,
        child: _buildGuideGrid(theme),
      ),
    );
  }

  Widget _buildGuideGrid(ThemeData theme) {
    return ValueListenableBuilder<bool>(
      valueListenable: _hasFocusNotifier,
      builder: (context, hasFocus, child) {
        return Column(
          children: [
            _buildTimeNavigation(theme),
            Expanded(
              child: ListenableBuilder(
                listenable: _gridHorizontalController,
                builder: (context, child) {
                  return Stack(children: [child!, _buildNowIndicatorOverlay(theme)]);
                },
                child: Column(
                  children: [
                    Row(
                      children: [
                        const SizedBox(width: _channelColumnWidth, height: _timeHeaderHeight),
                        Expanded(
                          child: SingleChildScrollView(
                            controller: _headerHorizontalController,
                            scrollDirection: Axis.horizontal,
                            physics: const ClampingScrollPhysics(),
                            child: SizedBox(
                              width: _totalGridWidth(),
                              height: _timeHeaderHeight,
                              child: _buildTimeHeader(theme),
                            ),
                          ),
                        ),
                      ],
                    ),
                    Expanded(
                      child: Row(
                        children: [
                          SizedBox(
                            width: _channelColumnWidth,
                            child: ListView.builder(
                              controller: _channelVerticalController,
                              physics: const NeverScrollableScrollPhysics(),
                              itemCount: widget.channels.length,
                              itemExtent: _rowHeight,
                              itemBuilder: (context, index) =>
                                  _buildChannelCell(widget.channels[index], theme, index: index),
                            ),
                          ),
                          Expanded(
                            child: NotificationListener<ScrollNotification>(
                              onNotification: (notification) {
                                if (notification is ScrollUpdateNotification &&
                                    notification.metrics.axis == Axis.vertical) {
                                  if (_channelVerticalController.hasClients) {
                                    _channelVerticalController.jumpTo(notification.metrics.pixels);
                                  }
                                }
                                return false;
                              },
                              child: SingleChildScrollView(
                                controller: _gridHorizontalController,
                                scrollDirection: Axis.horizontal,
                                physics: const ClampingScrollPhysics(),
                                child: SizedBox(
                                  width: _totalGridWidth(),
                                  child: ListView.builder(
                                    controller: _gridVerticalController,
                                    itemCount: widget.channels.length,
                                    itemExtent: _rowHeight,
                                    itemBuilder: (context, index) {
                                      final channel = widget.channels[index];
                                      final programs = _getProgramsForChannel(channel);
                                      return _buildProgramRow(channel, programs, theme, channelIndex: index);
                                    },
                                  ),
                                ),
                              ),
                            ),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        );
      },
    );
  }

  Widget _buildNowIndicatorOverlay(ThemeData _) {
    final now = DateTime.now();
    if (now.isBefore(_gridStart) || now.isAfter(_gridEnd)) {
      return const SizedBox.shrink();
    }
    final minutesSinceStart = now.difference(_gridStart).inMinutes.toDouble();
    final nowOffset = (minutesSinceStart / _minutesPerSlot) * _slotWidth;
    final scrollOffset = _gridHorizontalController.hasClients ? _gridHorizontalController.offset : 0.0;
    final left = _channelColumnWidth + nowOffset - scrollOffset;

    // Hide when scrolled behind the channel column
    if (left < _channelColumnWidth) return const SizedBox.shrink();

    final gridHeight = _timeHeaderHeight + widget.channels.length * _rowHeight;

    return Positioned(
      left: left,
      top: 0,
      height: gridHeight,
      child: IgnorePointer(child: Container(width: 2, color: Colors.red)),
    );
  }

  String _dayLabel(DateTime day) {
    final now = DateTime.now();
    final today = DateTime(now.year, now.month, now.day);
    final target = DateTime(day.year, day.month, day.day);

    if (target == today) return t.liveTv.today;

    return DateFormat('EEEE', LocaleSettings.currentLocale.languageCode).format(target);
  }

  List<(String, int)> get _timeSlots => [
    (t.liveTv.midnight, 0),
    (t.liveTv.overnight, 2),
    (t.liveTv.morning, 6),
    (t.liveTv.daytime, 12),
    (t.liveTv.evening, 18),
    (t.liveTv.lateNight, 22),
  ];

  RelativeRect _menuPosition() {
    final renderBox = _dayPickerKey.currentContext?.findRenderObject() as RenderBox?;
    final overlay = Overlay.of(context).context.findRenderObject() as RenderBox?;
    if (renderBox == null || overlay == null) return RelativeRect.fill;

    final buttonPos = renderBox.localToGlobal(Offset.zero);
    final buttonSize = renderBox.size;
    return RelativeRect.fromRect(
      Rect.fromLTWH(buttonPos.dx, buttonPos.dy + buttonSize.height, buttonSize.width, 0),
      Offset.zero & overlay.size,
    );
  }

  void _showDayPicker() {
    final now = DateTime.now();
    final today = DateTime(now.year, now.month, now.day);
    final gridDay = DateTime(_gridStart.year, _gridStart.month, _gridStart.day);
    final theme = Theme.of(context);

    final days = <DateTime>[];
    for (var i = 0; i < 8; i++) {
      days.add(today.add(Duration(days: i)));
    }

    showMenu<Object>(
      context: context,
      position: _menuPosition(),
      items: [
        PopupMenuItem<String>(
          value: 'now',
          child: Text(t.liveTv.now, style: theme.textTheme.bodyMedium),
        ),
        ...days.map((day) {
          final isSelected = day == gridDay;
          final label = _dayLabel(day);
          return PopupMenuItem<DateTime>(
            value: day,
            child: Row(
              children: [
                Expanded(
                  child: Text(
                    label,
                    style: theme.textTheme.bodyMedium?.copyWith(color: isSelected ? theme.colorScheme.primary : null),
                  ),
                ),
                if (isSelected) AppIcon(Symbols.check_rounded, size: 18, color: theme.colorScheme.primary),
              ],
            ),
          );
        }),
      ],
    ).then((value) {
      if (!mounted) return;
      if (value == null) {
        _guideFocusNode.requestFocus();
        return;
      }
      if (value is String && value == 'now') {
        _jumpToNow();
        _guideFocusNode.requestFocus();
      } else if (value is DateTime) {
        _showTimeSlotPicker(value);
      }
    });
  }

  void _showTimeSlotPicker(DateTime day) {
    final theme = Theme.of(context);
    final label = _dayLabel(day).toUpperCase();

    showMenu<int>(
      context: context,
      position: _menuPosition(),
      items: [
        PopupMenuItem<int>(
          value: -1,
          child: Row(
            children: [
              AppIcon(Symbols.chevron_left_rounded, size: 20, color: theme.colorScheme.onSurface),
              const SizedBox(width: 8),
              Text(label, style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold)),
            ],
          ),
        ),
        const PopupMenuDivider(),
        ..._timeSlots.map((slot) {
          return PopupMenuItem<int>(
            value: slot.$2,
            child: Text(slot.$1, style: theme.textTheme.bodyMedium),
          );
        }),
      ],
    ).then((value) {
      if (value == null) {
        _guideFocusNode.requestFocus();
        return;
      }
      if (value == -1) {
        _showDayPicker();
        return;
      }
      setState(() {
        _gridStart = DateTime(day.year, day.month, day.day, value);
        _gridEnd = _gridStart.add(const Duration(hours: 6));
      });
      _loadPrograms();
      _guideFocusNode.requestFocus();
    });
  }

  Widget _timeNavFocusWrap({required Widget child, required int index, required ThemeData theme}) {
    final isFocused = _hasFocus && _focusZone == _GuideZone.timeNav && _timeNavIndex == index;
    if (!isFocused) return child;
    return Container(
      decoration: BoxDecoration(
        color: theme.colorScheme.primary.withValues(alpha: 0.15),
        borderRadius: const BorderRadius.all(Radius.circular(8)),
      ),
      child: child,
    );
  }

  Widget _buildTimeNavigation(ThemeData theme) {
    final timeLabel = formatClockTime(_gridStart, is24Hour: MediaQuery.alwaysUse24HourFormatOf(context));
    final dayLabel = _dayLabel(_gridStart);

    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
      decoration: BoxDecoration(
        border: Border(bottom: BorderSide(color: theme.dividerColor.withValues(alpha: 0.3))),
      ),
      child: Row(
        children: [
          _timeNavFocusWrap(
            index: 0,
            theme: theme,
            child: IconButton(
              icon: const AppIcon(Symbols.chevron_left_rounded),
              onPressed: () => _shiftTimeRange(-2),
              iconSize: 20,
              visualDensity: VisualDensity.compact,
            ),
          ),
          Expanded(
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                _timeNavFocusWrap(
                  index: 1,
                  theme: theme,
                  child: GestureDetector(
                    key: _dayPickerKey,
                    onTap: _showDayPicker,
                    child: Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                      child: Row(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          Text(dayLabel, style: theme.textTheme.labelLarge),
                          const SizedBox(width: 2),
                          AppIcon(Symbols.arrow_drop_down_rounded, size: 18, color: theme.colorScheme.onSurface),
                        ],
                      ),
                    ),
                  ),
                ),
                const SizedBox(width: 8),
                Text(timeLabel, style: theme.textTheme.labelLarge),
              ],
            ),
          ),
          _timeNavFocusWrap(
            index: 2,
            theme: theme,
            child: IconButton(
              icon: const AppIcon(Symbols.chevron_right_rounded),
              onPressed: () => _shiftTimeRange(2),
              iconSize: 20,
              visualDensity: VisualDensity.compact,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildTimeHeader(ThemeData theme) {
    final is24Hour = MediaQuery.alwaysUse24HourFormatOf(context);
    final slots = <Widget>[];
    var current = _gridStart;

    while (current.isBefore(_gridEnd)) {
      final timeStr = formatClockTime(current, is24Hour: is24Hour);
      slots.add(
        SizedBox(
          width: _slotWidth,
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 8),
            child: Align(
              alignment: Alignment.centerLeft,
              child: Text(
                timeStr,
                style: theme.textTheme.labelSmall?.copyWith(color: theme.colorScheme.onSurfaceVariant),
              ),
            ),
          ),
        ),
      );
      current = current.add(const Duration(minutes: _minutesPerSlot));
    }

    return Row(children: slots);
  }

  Widget _buildChannelCell(LiveTvChannel channel, ThemeData theme, {required int index}) {
    final multiServer = context.read<MultiServerProvider>();
    final client = multiServer.getClientForServer(channel.serverId ?? '');

    final isFocused = _hasFocus && _focusZone == _GuideZone.grid && _gridColumn == 0 && _gridChannelIndex == index;

    return _ChannelCell(
      rowHeight: _rowHeight,
      channelColumnWidth: _channelColumnWidth,
      channelThumb: channel.thumb,
      client: client,
      channel: channel,
      theme: theme,
      onTap: () => _tuneChannel(channel),
      onLongPress: widget.onToggleFavorite != null ? () => widget.onToggleFavorite!(channel) : null,
      isFocused: isFocused,
      isFavorite: widget.isFavoriteChannel?.call(channel) ?? false,
      fallbackBuilder: () => _buildChannelNameFallback(channel, theme),
    );
  }

  Widget _buildChannelNameFallback(LiveTvChannel channel, ThemeData theme) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        if (channel.number != null)
          Text(
            channel.number!,
            style: theme.textTheme.labelSmall?.copyWith(color: theme.colorScheme.onSurfaceVariant),
            maxLines: 1,
          ),
        Text(
          channel.displayName,
          style: theme.textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w500),
          maxLines: 1,
          overflow: TextOverflow.ellipsis,
          textAlign: TextAlign.center,
        ),
      ],
    );
  }

  Widget _buildProgramRow(
    LiveTvChannel channel,
    List<LiveTvProgram> programs,
    ThemeData theme, {
    required int channelIndex,
  }) {
    if (programs.isEmpty) {
      return Container(
        height: _rowHeight,
        decoration: BoxDecoration(
          border: Border(bottom: BorderSide(color: theme.dividerColor.withValues(alpha: 0.3))),
        ),
        child: Center(
          child: Text(
            t.liveTv.noPrograms,
            style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant),
          ),
        ),
      );
    }

    final blocks = <Widget>[];
    final gridStartEpoch = _gridStart.millisecondsSinceEpoch ~/ 1000;
    final gridEndEpoch = _gridEnd.millisecondsSinceEpoch ~/ 1000;

    // Determine which program is focused in this row
    final focusProg =
        (_hasFocus && _focusZone == _GuideZone.grid && _gridColumn == 1 && _gridChannelIndex == channelIndex)
        ? _focusedProgram
        : null;

    for (final program in programs) {
      final progStart = (program.beginsAt ?? gridStartEpoch).clamp(gridStartEpoch, gridEndEpoch);
      final progEnd = (program.endsAt ?? gridEndEpoch).clamp(gridStartEpoch, gridEndEpoch);

      if (progEnd <= progStart) continue;

      final startOffset = progStart - gridStartEpoch;
      final duration = progEnd - progStart;
      final left = (startOffset / (_minutesPerSlot * 60)) * _slotWidth;
      final width = (duration / (_minutesPerSlot * 60)) * _slotWidth;
      final clampedWidth = width.clamp(2.0, double.infinity);

      blocks.add(
        Positioned(
          left: left,
          width: clampedWidth,
          top: 0,
          bottom: 0,
          child: _buildProgramBlock(
            channel,
            program,
            theme,
            isFirst: progStart == gridStartEpoch,
            isLast: program == programs.last && progEnd != gridEndEpoch,
            isFocused: identical(program, focusProg),
            tileLeft: left,
            tileWidth: clampedWidth,
          ),
        ),
      );
    }

    return Container(
      height: _rowHeight,
      decoration: BoxDecoration(
        border: Border(bottom: BorderSide(color: theme.dividerColor.withValues(alpha: 0.3))),
      ),
      child: Stack(children: blocks),
    );
  }

  Widget _buildProgramBlock(
    LiveTvChannel channel,
    LiveTvProgram program,
    ThemeData theme, {
    bool isFirst = false,
    bool isLast = false,
    bool isFocused = false,
    double tileLeft = 0,
    double tileWidth = 0,
  }) {
    final isCurrentlyAiring = program.isCurrentlyAiring;
    final isPast = program.endsAt != null && program.endsAt! < DateTime.now().millisecondsSinceEpoch ~/ 1000;
    final isRecordingScheduled = _isRecordingScheduled(program);

    Color materialColor;
    if (isFocused) {
      materialColor = theme.colorScheme.primary.withValues(alpha: 0.15);
    } else if (isCurrentlyAiring) {
      materialColor = theme.colorScheme.onSurface.withValues(alpha: 0.12);
    } else {
      materialColor = theme.colorScheme.onSurface.withValues(alpha: 0.05);
    }

    Color titleColor;
    if (isFocused) {
      titleColor = theme.colorScheme.primary;
    } else if (isCurrentlyAiring) {
      titleColor = theme.colorScheme.onSurface;
    } else {
      titleColor = theme.colorScheme.onSurface;
    }

    Color subtitleColor;
    if (isFocused) {
      subtitleColor = theme.colorScheme.primary.withValues(alpha: 0.7);
    } else if (isCurrentlyAiring) {
      subtitleColor = theme.colorScheme.onSurfaceVariant;
    } else {
      subtitleColor = theme.colorScheme.onSurfaceVariant;
    }

    return Opacity(
      opacity: isPast ? 0.5 : 1.0,
      child: Material(
        color: isFocused ? materialColor : Colors.transparent,
        shape: RoundedRectangleBorder(
          side: isFocused ? BorderSide(color: theme.colorScheme.primary, width: 2) : BorderSide.none,
        ),
        child: InkWell(
          canRequestFocus: false,
          onTap: () => _showProgramDetails(channel, program),
          child: Container(
            decoration: BoxDecoration(
              border: Border(
                left: isFirst ? BorderSide.none : BorderSide(color: theme.dividerColor.withValues(alpha: 0.3)),
                right: isLast ? BorderSide(color: theme.dividerColor.withValues(alpha: 0.3)) : BorderSide.none,
              ),
            ),
            child: ListenableBuilder(
              listenable: _gridHorizontalController,
              builder: (context, _) {
                const basePadding = 6.0;
                final scrollOffset = _gridHorizontalController.hasClients ? _gridHorizontalController.offset : 0.0;
                final maxInset = (tileWidth - 2 * basePadding - 20).clamp(0.0, double.infinity);
                final leftInset = (scrollOffset - tileLeft).clamp(0.0, maxInset);
                return Container(
                  color: isFocused ? null : materialColor,
                  padding: EdgeInsets.fromLTRB(basePadding + leftInset, 4, basePadding, 4),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Row(
                        children: [
                          if (isRecordingScheduled) ...[
                            _RecordingDot(color: Colors.red, tooltip: t.liveTv.recordingScheduled),
                            const SizedBox(width: 5),
                          ],
                          Expanded(
                            child: Text(
                              program.grandparentTitle ?? program.title,
                              style: theme.textTheme.bodyMedium?.copyWith(
                                fontWeight: FontWeight.w600,
                                color: titleColor,
                              ),
                              maxLines: 1,
                              overflow: TextOverflow.ellipsis,
                            ),
                          ),
                        ],
                      ),
                      if (program.grandparentTitle != null)
                        Text(
                          '${program.parentIndex != null && program.index != null ? 'S${program.parentIndex}E${program.index} · ' : ''}${program.title}',
                          style: theme.textTheme.labelSmall?.copyWith(color: subtitleColor),
                          maxLines: 1,
                          overflow: TextOverflow.ellipsis,
                        ),
                      if (program.startTime != null)
                        Text(
                          '${formatClockTime(program.startTime!, is24Hour: MediaQuery.alwaysUse24HourFormatOf(context))} · ${formatDurationTextual(program.durationMinutes * 60000)}',
                          style: theme.textTheme.labelSmall?.copyWith(color: subtitleColor),
                          maxLines: 1,
                          overflow: TextOverflow.ellipsis,
                        ),
                    ],
                  ),
                );
              },
            ),
          ),
        ),
      ),
    );
  }

  void _showProgramDetails(LiveTvChannel channel, LiveTvProgram program) {
    final multiServer = context.read<MultiServerProvider>();
    final client = multiServer.getClientForServer(channel.serverId ?? '');
    String? posterUrl;
    if (program.thumb != null && client != null) {
      posterUrl = MediaImageHelper.getOptimizedImageUrl(
        client: client,
        thumbPath: program.thumb,
        maxWidth: 80,
        maxHeight: 120,
        devicePixelRatio: MediaImageHelper.effectiveDevicePixelRatio(context),
        imageType: ImageType.poster,
      );
    }

    showProgramDetailsSheet(
      context,
      program: program,
      channel: channel,
      posterUrl: posterUrl,
      onTuneChannel: () => _tuneChannel(channel),
      client: client,
      onRecordingStateChanged: (isScheduled) => _handleRecordingStateChanged(program, isScheduled),
    );
  }
}

class _RecordingDot extends StatelessWidget {
  final Color color;
  final String tooltip;

  const _RecordingDot({required this.color, required this.tooltip});

  @override
  Widget build(BuildContext context) {
    return Tooltip(
      message: tooltip,
      child: Semantics(
        label: tooltip,
        child: Container(
          width: 8,
          height: 8,
          decoration: BoxDecoration(color: color, shape: BoxShape.circle),
        ),
      ),
    );
  }
}

class _ChannelCell extends StatefulWidget {
  final double rowHeight;
  final double channelColumnWidth;
  final String? channelThumb;
  final MediaServerClient? client;
  final LiveTvChannel channel;
  final ThemeData theme;
  final VoidCallback onTap;
  final VoidCallback? onLongPress;
  final bool isFocused;
  final bool isFavorite;
  final Widget Function() fallbackBuilder;

  const _ChannelCell({
    required this.rowHeight,
    required this.channelColumnWidth,
    required this.channelThumb,
    required this.client,
    required this.channel,
    required this.theme,
    required this.onTap,
    this.onLongPress,
    required this.isFocused,
    this.isFavorite = false,
    required this.fallbackBuilder,
  });

  @override
  State<_ChannelCell> createState() => _ChannelCellState();
}

class _ChannelCellState extends State<_ChannelCell> {
  bool _hovered = false;

  @override
  Widget build(BuildContext context) {
    final theme = widget.theme;
    final showAction = _hovered || widget.isFocused;

    return MouseRegion(
      onEnter: (_) => setState(() => _hovered = true),
      onExit: (_) => setState(() => _hovered = false),
      child: GestureDetector(
        onSecondaryTap: widget.onLongPress,
        child: Material(
          color: widget.isFocused ? theme.colorScheme.primary.withValues(alpha: 0.15) : Colors.transparent,
          child: InkWell(
            canRequestFocus: false,
            onTap: widget.onTap,
            onLongPress: widget.onLongPress,
            child: Container(
              height: widget.rowHeight,
              padding: const EdgeInsets.symmetric(horizontal: 8),
              decoration: BoxDecoration(
                border: Border(
                  bottom: BorderSide(color: theme.dividerColor.withValues(alpha: 0.3)),
                  right: BorderSide(color: theme.dividerColor.withValues(alpha: 0.3)),
                ),
              ),
              child: Stack(
                alignment: Alignment.center,
                children: [
                  AnimatedOpacity(
                    opacity: showAction ? 0.3 : 1.0,
                    duration: const Duration(milliseconds: 150),
                    child: widget.channelThumb != null && widget.client != null
                        ? OptimizedMediaImage.thumb(
                            client: widget.client!,
                            imagePath: widget.channelThumb,
                            width: widget.channelColumnWidth - 16,
                            height: widget.rowHeight - 16,
                            fit: BoxFit.contain,
                          )
                        : widget.fallbackBuilder(),
                  ),
                  if (showAction) AppIcon(Symbols.play_arrow_rounded, size: 32, color: theme.colorScheme.onSurface),
                  if (widget.isFavorite)
                    Positioned(
                      top: 2,
                      right: 0,
                      child: AppIcon(Symbols.star_rounded, size: 14, color: theme.colorScheme.primary),
                    ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}
