import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;

import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:universal_gamepad/universal_gamepad.dart';
import 'package:window_manager/window_manager.dart';

import '../utils/app_logger.dart';
import '../utils/key_event_simulator.dart' as key_sim;
import '../utils/platform_detector.dart';

/// Suppresses synthetic gamepad key events when the OS has just delivered an
/// equivalent native key event, which happens with Steam Input on Windows.
class GamepadDuplicateInputGuard {
  static const defaultSuppressionWindow = Duration(milliseconds: 120);

  static final Map<LogicalKeyboardKey, Set<LogicalKeyboardKey>> _nativeAliasesBySyntheticKey = {
    LogicalKeyboardKey.arrowUp: {LogicalKeyboardKey.arrowUp},
    LogicalKeyboardKey.arrowDown: {LogicalKeyboardKey.arrowDown},
    LogicalKeyboardKey.arrowLeft: {LogicalKeyboardKey.arrowLeft},
    LogicalKeyboardKey.arrowRight: {LogicalKeyboardKey.arrowRight},
    LogicalKeyboardKey.enter: {
      LogicalKeyboardKey.enter,
      LogicalKeyboardKey.numpadEnter,
      LogicalKeyboardKey.select,
      LogicalKeyboardKey.gameButtonA,
    },
    LogicalKeyboardKey.escape: {
      LogicalKeyboardKey.escape,
      LogicalKeyboardKey.goBack,
      LogicalKeyboardKey.browserBack,
      LogicalKeyboardKey.gameButtonB,
    },
    LogicalKeyboardKey.gameButtonX: {LogicalKeyboardKey.gameButtonX, LogicalKeyboardKey.contextMenu},
  };

  static final Set<LogicalKeyboardKey> _trackedNativeKeys = _nativeAliasesBySyntheticKey.values
      .expand((keys) => keys)
      .toSet();

  final DateTime Function() _now;
  final bool Function()? _enabled;
  final Duration suppressionWindow;
  final Map<LogicalKeyboardKey, DateTime> _lastNativeEvents = {};
  final Set<LogicalKeyboardKey> _nativeKeysPressed = {};

  GamepadDuplicateInputGuard({
    DateTime Function()? now,
    bool Function()? enabled,
    this.suppressionWindow = defaultSuppressionWindow,
  }) : _now = now ?? DateTime.now,
       _enabled = enabled;

  bool get _isEnabled => _enabled?.call() ?? true;

  bool handleNativeKeyEvent(KeyEvent event) {
    if (!_isEnabled || !_trackedNativeKeys.contains(event.logicalKey)) return false;

    final now = _now();
    _lastNativeEvents[event.logicalKey] = now;
    if (event is KeyUpEvent) {
      _nativeKeysPressed.remove(event.logicalKey);
    } else {
      _nativeKeysPressed.add(event.logicalKey);
    }
    _prune(now);
    return false;
  }

  bool shouldSuppressSyntheticKey(LogicalKeyboardKey logicalKey) {
    if (!_isEnabled) return false;

    final now = _now();
    _prune(now);
    for (final key in _nativeAliasesBySyntheticKey[logicalKey] ?? {logicalKey}) {
      if (_nativeKeysPressed.contains(key)) return true;

      final lastNativeEvent = _lastNativeEvents[key];
      if (lastNativeEvent != null && now.difference(lastNativeEvent) <= suppressionWindow) {
        return true;
      }
    }
    return false;
  }

  void clear() {
    _lastNativeEvents.clear();
    _nativeKeysPressed.clear();
  }

  void _prune(DateTime now) {
    _lastNativeEvents.removeWhere((_, timestamp) => now.difference(timestamp) > suppressionWindow);
  }
}

/// Service that bridges gamepad input to Flutter's focus navigation system.
///
/// Listens to gamepad events from the `universal_gamepad` package and translates
/// them into focus navigation actions and key events that integrate with the
/// existing keyboard navigation system.
class GamepadService with WindowListener {
  static final Map<GamepadButton, LogicalKeyboardKey> _syntheticKeyByButton = {
    GamepadButton.dpadUp: LogicalKeyboardKey.arrowUp,
    GamepadButton.dpadDown: LogicalKeyboardKey.arrowDown,
    GamepadButton.dpadLeft: LogicalKeyboardKey.arrowLeft,
    GamepadButton.dpadRight: LogicalKeyboardKey.arrowRight,
    GamepadButton.a: LogicalKeyboardKey.enter,
    GamepadButton.b: LogicalKeyboardKey.escape,
    GamepadButton.x: LogicalKeyboardKey.gameButtonX,
  };

  static GamepadService? _instance;
  StreamSubscription<GamepadEvent>? _subscription;
  final GamepadDuplicateInputGuard _duplicateInputGuard;

  /// Callback to switch InputModeTracker to keyboard mode.
  /// Set by InputModeTracker when it initializes.
  static VoidCallback? onGamepadInput;

  /// Callback for L1 bumper press (previous tab).
  /// Screens with tabs can listen to this.
  static VoidCallback? onL1Pressed;

  /// Callback for R1 bumper press (next tab).
  /// Screens with tabs can listen to this.
  static VoidCallback? onR1Pressed;

  // Deadzone for analog sticks (0.0 to 1.0)
  static const double _stickDeadzone = 0.5;

  // Auto-repeat timing for held directional inputs (D-pad / stick)
  static const Duration _repeatInitialDelay = Duration(milliseconds: 400);
  static const Duration _repeatInterval = Duration(milliseconds: 80);

  Timer? _repeatTimer;

  // Track stick state to detect deadzone crossings
  bool _leftStickUp = false;
  bool _leftStickDown = false;
  bool _leftStickLeft = false;
  bool _leftStickRight = false;

  // Track button states to prevent repeated events from button holds
  final Set<GamepadButton> _pressedButtons = {};
  final Set<GamepadButton> _suppressedButtons = {};

  // Whether the app window is currently focused — ignore gamepad input when false
  bool _windowFocused = true;
  bool _nativeKeyHandlerRegistered = false;

  GamepadService._({GamepadDuplicateInputGuard? duplicateInputGuard})
    : _duplicateInputGuard = duplicateInputGuard ?? GamepadDuplicateInputGuard(enabled: () => Platform.isWindows);

  static GamepadService get instance {
    _instance ??= GamepadService._();
    return _instance!;
  }

  /// Start listening to gamepad events.
  /// Only active on desktop platforms (macOS, Windows, Linux).
  static bool get _isDesktop => PlatformDetector.isDesktopOS();

  void start() async {
    appLogger.i('GamepadService: Starting on ${Platform.operatingSystem}');

    try {
      final gamepads = await Gamepad.instance.listGamepads();
      appLogger.i('GamepadService: Found ${gamepads.length} gamepad(s)');
      for (final gamepad in gamepads) {
        appLogger.i('  - ${gamepad.name} (id: ${gamepad.id})');
      }
    } catch (e) {
      appLogger.e('GamepadService: Error listing gamepads', error: e);
    }

    // Track window focus so we ignore gamepad input when another app is active
    // (window_manager is desktop-only)
    if (_isDesktop) {
      windowManager.addListener(this);
      _windowFocused = await windowManager.isFocused();
    }
    _registerNativeKeyHandler();

    unawaited(_subscription?.cancel());
    _subscription = Gamepad.instance.events.listen(
      _handleGamepadEvent,
      onError: (e) => appLogger.e('GamepadService: Stream error', error: e),
    );
    appLogger.i('GamepadService: Listening for gamepad events');
  }

  void stop() {
    _stopDirectionRepeat();
    _unregisterNativeKeyHandler();
    _subscription?.cancel();
    _subscription = null;
    _duplicateInputGuard.clear();
    _suppressedButtons.clear();
    if (_isDesktop) {
      windowManager.removeListener(this);
    }
    Gamepad.instance.dispose();
  }

  @override
  void onWindowFocus() {
    _windowFocused = true;
    _duplicateInputGuard.clear();
    Gamepad.instance.resume();
  }

  @override
  void onWindowBlur() {
    _windowFocused = false;
    _stopDirectionRepeat();

    // Send key-up for any face buttons that are mid-hold so widgets
    // waiting for the release (e.g. long-press timers) don't get stuck.
    if (_pressedButtons.contains(GamepadButton.a)) {
      _simulateKeyUp(LogicalKeyboardKey.enter);
    }
    if (_pressedButtons.contains(GamepadButton.x)) {
      _simulateKeyUp(LogicalKeyboardKey.gameButtonX);
    }
    _pressedButtons.clear();
    _suppressedButtons.clear();
    _duplicateInputGuard.clear();

    // Reset analog stick state so re-focus doesn't inherit stale direction
    _leftStickUp = false;
    _leftStickDown = false;
    _leftStickLeft = false;
    _leftStickRight = false;

    // Release native device handles so other apps can use the gamepad.
    Gamepad.instance.pause();
  }

  void _registerNativeKeyHandler() {
    if (_nativeKeyHandlerRegistered || !Platform.isWindows) return;
    HardwareKeyboard.instance.addHandler(_handleNativeKeyEvent);
    _nativeKeyHandlerRegistered = true;
  }

  void _unregisterNativeKeyHandler() {
    if (!_nativeKeyHandlerRegistered) return;
    HardwareKeyboard.instance.removeHandler(_handleNativeKeyEvent);
    _nativeKeyHandlerRegistered = false;
  }

  bool _handleNativeKeyEvent(KeyEvent event) {
    return _duplicateInputGuard.handleNativeKeyEvent(event);
  }

  void _handleGamepadEvent(GamepadEvent event) {
    switch (event) {
      case final GamepadConnectionEvent e:
        appLogger.i('GamepadService: Gamepad ${e.connected ? "connected" : "disconnected"}: ${e.info.name}');
      case final GamepadButtonEvent e:
        _handleButton(e);
      case final GamepadAxisEvent e:
        _handleAxis(e);
    }
  }

  void _handleButton(GamepadButtonEvent event) {
    if (!_windowFocused) return;

    // Switch to keyboard mode on any button press
    if (event.pressed) {
      onGamepadInput?.call();
      _setTraditionalFocusHighlight();
    }
    // Ensure a frame is scheduled so addPostFrameCallback-based key
    // simulation fires promptly. Without this, key-up events can be
    // delayed indefinitely when the app is idle, causing the long-press
    // timer to fire before the release is delivered.
    key_sim.scheduleFrameIfIdle();

    final wasPressed = _pressedButtons.contains(event.button);

    if (event.pressed && !wasPressed) {
      _pressedButtons.add(event.button);
      if (_shouldSuppressButton(event.button)) {
        _suppressedButtons.add(event.button);
        return;
      }

      // D-pad — navigate with auto-repeat while held
      switch (event.button) {
        case GamepadButton.dpadUp:
          _startDirectionRepeat(TraversalDirection.up);
          return;
        case GamepadButton.dpadDown:
          _startDirectionRepeat(TraversalDirection.down);
          return;
        case GamepadButton.dpadLeft:
          _startDirectionRepeat(TraversalDirection.left);
          return;
        case GamepadButton.dpadRight:
          _startDirectionRepeat(TraversalDirection.right);
          return;
        // Face buttons — send KeyDown on press, KeyUp on release
        // so widget-level long-press timers work naturally
        case GamepadButton.a:
          _simulateKeyDown(LogicalKeyboardKey.enter);
        case GamepadButton.x:
          _simulateKeyDown(LogicalKeyboardKey.gameButtonX);
        // Immediate actions on press
        case GamepadButton.b:
          _simulateKeyPress(LogicalKeyboardKey.escape);
        case GamepadButton.leftShoulder:
          onL1Pressed?.call();
        case GamepadButton.rightShoulder:
          onR1Pressed?.call();
        default:
          break;
      }
    } else if (!event.pressed && wasPressed) {
      _pressedButtons.remove(event.button);
      if (_suppressedButtons.remove(event.button)) return;

      // D-pad release — stop repeat
      switch (event.button) {
        case GamepadButton.dpadUp:
        case GamepadButton.dpadDown:
        case GamepadButton.dpadLeft:
        case GamepadButton.dpadRight:
          _stopDirectionRepeat();
        // Face button release — send KeyUp
        case GamepadButton.a:
          _simulateKeyUp(LogicalKeyboardKey.enter);
        case GamepadButton.x:
          _simulateKeyUp(LogicalKeyboardKey.gameButtonX);
        default:
          break;
      }
    }
  }

  bool _shouldSuppressButton(GamepadButton button) {
    final syntheticKey = _syntheticKeyByButton[button];
    return syntheticKey != null && _duplicateInputGuard.shouldSuppressSyntheticKey(syntheticKey);
  }

  void _handleAxis(GamepadAxisEvent event) {
    if (!_windowFocused) return;

    // Switch to keyboard mode on significant axis input
    if (event.value.abs() > 0.3) {
      onGamepadInput?.call();
      _setTraditionalFocusHighlight();
      SchedulerBinding.instance.ensureVisualUpdate();
    }

    switch (event.axis) {
      case GamepadAxis.leftStickY:
        _handleLeftStickY(event.value);
      case GamepadAxis.leftStickX:
        _handleLeftStickX(event.value);
      default:
        break;
    }
  }

  void _moveFocus(TraversalDirection direction) {
    // Convert direction to arrow key and simulate a key press
    // This allows widgets like HubSection that intercept key events to handle navigation
    final logicalKey = _directionToKey(direction);
    _simulateKeyPress(logicalKey);
  }

  /// Fire [direction] immediately, then auto-repeat after an initial delay.
  void _startDirectionRepeat(TraversalDirection direction) {
    _stopDirectionRepeat();
    _moveFocus(direction);
    _repeatTimer = Timer(_repeatInitialDelay, () {
      _repeatTimer = Timer.periodic(_repeatInterval, (_) {
        _moveFocus(direction);
      });
    });
  }

  void _stopDirectionRepeat() {
    _repeatTimer?.cancel();
    _repeatTimer = null;
  }

  LogicalKeyboardKey _directionToKey(TraversalDirection direction) {
    switch (direction) {
      case TraversalDirection.up:
        return LogicalKeyboardKey.arrowUp;
      case TraversalDirection.down:
        return LogicalKeyboardKey.arrowDown;
      case TraversalDirection.left:
        return LogicalKeyboardKey.arrowLeft;
      case TraversalDirection.right:
        return LogicalKeyboardKey.arrowRight;
    }
  }

  /// Simulate a full key press (down + up) in a single frame.
  void _simulateKeyPress(LogicalKeyboardKey logicalKey) {
    SchedulerBinding.instance.addPostFrameCallback((_) {
      _dispatchKeyDown(logicalKey);
      _dispatchKeyUp(logicalKey);
    });
  }

  /// Simulate only key down — pair with [_simulateKeyUp] on release
  /// so widget-level long-press timers see real hold duration.
  void _simulateKeyDown(LogicalKeyboardKey logicalKey) {
    SchedulerBinding.instance.addPostFrameCallback((_) {
      _dispatchKeyDown(logicalKey);
    });
  }

  /// Simulate only key up — the release half of [_simulateKeyDown].
  void _simulateKeyUp(LogicalKeyboardKey logicalKey) {
    SchedulerBinding.instance.addPostFrameCallback((_) {
      _dispatchKeyUp(logicalKey);
    });
  }

  void _dispatchKeyDown(LogicalKeyboardKey logicalKey) {
    _dispatchKeyEvent(
      KeyDownEvent(
        physicalKey: _getPhysicalKey(logicalKey),
        logicalKey: logicalKey,
        timeStamp: Duration(milliseconds: DateTime.now().millisecondsSinceEpoch),
        deviceType: ui.KeyEventDeviceType.gamepad,
      ),
    );
  }

  void _dispatchKeyUp(LogicalKeyboardKey logicalKey) {
    _dispatchKeyEvent(
      KeyUpEvent(
        physicalKey: _getPhysicalKey(logicalKey),
        logicalKey: logicalKey,
        timeStamp: Duration(milliseconds: DateTime.now().millisecondsSinceEpoch),
        deviceType: ui.KeyEventDeviceType.gamepad,
      ),
    );
  }

  void _dispatchKeyEvent(KeyEvent event) {
    FocusNode? node = FocusManager.instance.primaryFocus;
    while (node != null) {
      if (node.onKeyEvent != null) {
        if (node.onKeyEvent!(node, event) == KeyEventResult.handled) break;
      }
      node = node.parent;
    }
  }

  PhysicalKeyboardKey _getPhysicalKey(LogicalKeyboardKey logicalKey) {
    if (logicalKey == LogicalKeyboardKey.gameButtonA) {
      return PhysicalKeyboardKey.gameButtonA;
    } else if (logicalKey == LogicalKeyboardKey.gameButtonB) {
      return PhysicalKeyboardKey.gameButtonB;
    } else if (logicalKey == LogicalKeyboardKey.gameButtonX) {
      return PhysicalKeyboardKey.gameButtonX;
    } else if (logicalKey == LogicalKeyboardKey.arrowUp) {
      return PhysicalKeyboardKey.arrowUp;
    } else if (logicalKey == LogicalKeyboardKey.arrowDown) {
      return PhysicalKeyboardKey.arrowDown;
    } else if (logicalKey == LogicalKeyboardKey.arrowLeft) {
      return PhysicalKeyboardKey.arrowLeft;
    } else if (logicalKey == LogicalKeyboardKey.arrowRight) {
      return PhysicalKeyboardKey.arrowRight;
    } else if (logicalKey == LogicalKeyboardKey.escape) {
      return PhysicalKeyboardKey.escape;
    }
    return PhysicalKeyboardKey.enter;
  }

  // W3C: leftStickY -1.0 = up, 1.0 = down
  void _handleLeftStickY(double value) {
    if (value > _stickDeadzone && !_leftStickDown) {
      _leftStickDown = true;
      _leftStickUp = false;
      _startDirectionRepeat(TraversalDirection.down);
    } else if (value < -_stickDeadzone && !_leftStickUp) {
      _leftStickUp = true;
      _leftStickDown = false;
      _startDirectionRepeat(TraversalDirection.up);
    } else if (value.abs() <= _stickDeadzone) {
      if (_leftStickUp || _leftStickDown) _stopDirectionRepeat();
      _leftStickUp = false;
      _leftStickDown = false;
    }
  }

  void _handleLeftStickX(double value) {
    if (value < -_stickDeadzone && !_leftStickLeft) {
      _leftStickLeft = true;
      _leftStickRight = false;
      _startDirectionRepeat(TraversalDirection.left);
    } else if (value > _stickDeadzone && !_leftStickRight) {
      _leftStickRight = true;
      _leftStickLeft = false;
      _startDirectionRepeat(TraversalDirection.right);
    } else if (value.abs() <= _stickDeadzone) {
      if (_leftStickLeft || _leftStickRight) _stopDirectionRepeat();
      _leftStickLeft = false;
      _leftStickRight = false;
    }
  }

  // Ensure Material uses traditional (keyboard) focus highlights when navigating
  // via gamepad. Synthetic key events we dispatch below don't go through the
  // platform key pipeline, so Flutter won't automatically flip highlight mode.
  void _setTraditionalFocusHighlight() {
    if (FocusManager.instance.highlightStrategy != FocusHighlightStrategy.alwaysTraditional) {
      FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
    }
  }
}
