import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../game/engine/game_engine.dart';
import '../../game/models/grid.dart';
import '../../l10n/gen/app_localizations.dart';
import '../../services/audio_service.dart';
import '../../state/game_session_notifier.dart';
import '../../state/providers.dart';
import '../theme/palette.dart';
import '../widgets/board_geometry.dart';
import '../widgets/board_painter.dart';
import '../widgets/board_widget.dart';
import '../widgets/effects_overlay.dart';
import '../widgets/hud_widget.dart';
import '../widgets/piece_painter.dart';
import '../widgets/season_background.dart';
import '../widgets/tray_widget.dart';
import '../widgets/tutorial_overlay.dart';

/// Renders whatever session [gameSessionProvider] holds; callers start the
/// stage (via SeasonFlowNotifier) before navigating here.
class GameScreen extends ConsumerStatefulWidget {
  const GameScreen({super.key});

  @override
  ConsumerState<GameScreen> createState() => _GameScreenState();
}

class _GameScreenState extends ConsumerState<GameScreen>
    with TickerProviderStateMixin {
  final _boardKey = GlobalKey();
  final _stackKey = GlobalKey();
  final _effectsKey = GlobalKey<EffectsOverlayState>();
  late final AnimationController _shake = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 350),
  );

  bool _tutorialStartChecked = false;
  bool _endlessNewBest = false;

  int? _dragIndex;
  Offset? _dragGlobal;

  /// How far the dragged piece floats above the finger so it stays visible.
  static const double _lift = 70;

  RenderBox? get _boardBox =>
      _boardKey.currentContext?.findRenderObject() as RenderBox?;

  /// Global top-left of the dragged piece, rendered at board cell size.
  Offset? _draggedPieceTopLeft(GameViewState view) {
    final i = _dragIndex;
    final g = _dragGlobal;
    final box = _boardBox;
    if (i == null || g == null || box == null || i >= view.tray.length) {
      return null;
    }
    final geo = BoardGeometry(boardSize: box.size.width);
    final piece = view.tray[i];
    final (w, h) = pieceCellBounds(piece);
    final pw = w * geo.cellSize;
    final ph = h * geo.cellSize;
    return g + Offset(-pw / 2, -_lift - ph);
  }

  GhostSpec? _ghost(GameViewState view) {
    final i = _dragIndex;
    final box = _boardBox;
    final topLeftGlobal = _draggedPieceTopLeft(view);
    if (i == null || box == null || topLeftGlobal == null) return null;

    final geo = BoardGeometry(boardSize: box.size.width);
    final piece = view.tray[i];
    final (w, h) = pieceCellBounds(piece);
    final centerLocal = box.globalToLocal(
      topLeftGlobal + Offset(w * geo.cellSize / 2, h * geo.cellSize / 2),
    );
    // Only preview while the piece is actually over the board.
    if (centerLocal.dx < -geo.cellSize ||
        centerLocal.dy < -geo.cellSize ||
        centerLocal.dx > box.size.width + geo.cellSize ||
        centerLocal.dy > box.size.height + geo.cellSize) {
      return null;
    }

    final topLeftLocal = box.globalToLocal(topLeftGlobal);
    final (ax, ay) = geo.snapAnchor(piece, topLeftLocal);
    final legal =
        ref.read(gameSessionProvider.notifier).canPlaceAt(i, ax, ay);
    return GhostSpec(piece: piece, anchorX: ax, anchorY: ay, legal: legal);
  }

  void _onDragEnd(GameViewState view) {
    final i = _dragIndex;
    final ghost = _ghost(view);
    setState(() {
      _dragIndex = null;
      _dragGlobal = null;
    });
    if (i != null && ghost != null && ghost.legal) {
      ref
          .read(gameSessionProvider.notifier)
          .tryPlace(i, ghost.anchorX, ghost.anchorY);
    }
  }

  @override
  void dispose() {
    _shake.dispose();
    super.dispose();
  }

  void _onSessionChange(GameViewState? prev, GameViewState? next) {
    if (next == null) return;
    final audio = ref.read(audioServiceProvider);
    if (prev?.fxTick != next.fxTick && next.lastPlacement != null) {
      final placement = next.lastPlacement!;
      final hapticsOn = ref.read(soundEnabledProvider);
      if (placement.linesCleared > 0) {
        audio.play(placement.comboStreak >= 2 ? Sfx.combo : Sfx.clear);
        if (hapticsOn) HapticFeedback.mediumImpact();
        if (placement.comboStreak >= 4) {
          if (hapticsOn) HapticFeedback.heavyImpact();
          _shake.forward(from: 0);
        }
      } else {
        audio.play(Sfx.place);
        if (hapticsOn) HapticFeedback.lightImpact();
      }
      ref.read(tutorialProvider.notifier).onPlaced();
      if (placement.linesCleared > 0) {
        ref.read(tutorialProvider.notifier).onLineCleared();
      }
      final boardBox = _boardBox;
      final stackBox =
          _stackKey.currentContext?.findRenderObject() as RenderBox?;
      if (boardBox != null && stackBox != null) {
        final topLeft =
            stackBox.globalToLocal(boardBox.localToGlobal(Offset.zero));
        _effectsKey.currentState?.onPlacement(
          placement,
          boardRect: topLeft & boardBox.size,
        );
      }
    }
    if (prev?.phase != next.phase) {
      // A finished stage ends the tutorial; otherwise the overlay would sit
      // on top of the result card and leak into the next stage.
      if (next.phase != GamePhase.playing) {
        ref.read(tutorialProvider.notifier).skip();
      }
      if (next.phase == GamePhase.won || next.phase == GamePhase.lost) {
        ref.read(adServiceProvider).onStageCompleted();
      }
      if (next.phase == GamePhase.won) {
        audio.play(Sfx.win);
        // recordResult keeps the best run, so re-entry is harmless.
        if (!next.endless) {
          ref
              .read(seasonFlowProvider.notifier)
              .recordWin(stars: next.starsEarned, score: next.score);
          final flow = ref.read(seasonFlowProvider);
          if (flow != null) {
            ref.read(analyticsProvider).stageEnd(
                  seasonId: flow.pack.seasonId,
                  stageId: flow.stage.id,
                  won: true,
                  stars: next.starsEarned,
                  score: next.score,
                  movesUsed: next.moveLimit - next.movesLeft,
                );
          }
          // recordWin (above) has already updated progress synchronously, so
          // the policy's cleared-stage count includes this win. Fire-and-forget
          // — a review prompt must never block the result card.
          ref
              .read(reviewServiceProvider)
              .maybeRequestAfterWin(stars: next.starsEarned);
        }
        final stackBox =
            _stackKey.currentContext?.findRenderObject() as RenderBox?;
        if (stackBox != null) {
          _effectsKey.currentState?.onWin(stackBox.size);
        }
      }
      if (next.phase == GamePhase.lost) audio.play(Sfx.lose);
      if (next.phase == GamePhase.lost && next.endless) {
        ref.read(endlessBestProvider.notifier).record(next.score).then((isNew) {
          if (mounted) setState(() => _endlessNewBest = isNew);
          ref.read(analyticsProvider).endlessEnd(score: next.score, isNewBest: isNew);
        });
      }
      if (next.phase == GamePhase.lost && !next.endless) {
        final flow = ref.read(seasonFlowProvider);
        if (flow != null) {
          ref.read(analyticsProvider).stageEnd(
                seasonId: flow.pack.seasonId,
                stageId: flow.stage.id,
                won: false,
                stars: 0,
                score: next.score,
                movesUsed: next.moveLimit - next.movesLeft,
              );
        }
      }
      if (next.phase == GamePhase.won || next.phase == GamePhase.lost) {
        ref.read(streakProvider.notifier).onStagePlayed(DateTime.now());
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    ref.listen<GameViewState?>(gameSessionProvider, _onSessionChange);
    ref.listen(streakProvider, (prev, next) {
      final milestone = next.hitMilestone;
      if (milestone != null && prev?.hitMilestone != milestone) {
        final l10n = AppLocalizations.of(context)!;
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(l10n.streakMilestone(milestone))),
        );
      }
    });
    final view = ref.watch(gameSessionProvider);
    if (view == null) {
      return const Scaffold(body: Center(child: CircularProgressIndicator()));
    }

    final tutorialStep = ref.watch(tutorialProvider);
    if (!_tutorialStartChecked) {
      _tutorialStartChecked = true;
      final flow = ref.read(seasonFlowProvider);
      if (flow != null &&
          flow.index == 0 &&
          !ref.read(saveRepositoryProvider).tutorialDone) {
        WidgetsBinding.instance.addPostFrameCallback(
            (_) => ref.read(tutorialProvider.notifier).start());
      }
    }

    final ghost = _ghost(view);
    final draggedTopLeft = _draggedPieceTopLeft(view);
    final boardBox = _boardBox;

    final theme = ref.watch(activeThemeProvider);
    return Scaffold(
      backgroundColor: Colors.transparent,
      body: Stack(
        fit: StackFit.expand,
        children: [
          SeasonBackground(theme: theme),
          SafeArea(
            child: Stack(
              key: _stackKey,
              children: [
                Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    children: [
                      // Close/back sits on its own top row so it never overlaps
                      // the HUD's moves chip (which lives at the top-left).
                      if (Navigator.of(context).canPop())
                        Align(
                          alignment: Alignment.centerLeft,
                          child: IconButton(
                            padding: EdgeInsets.zero,
                            visualDensity: VisualDensity.compact,
                            icon: const Icon(Icons.close, color: Colors.white54),
                            onPressed: () => Navigator.of(context).pop(),
                          ),
                        ),
                      HudWidget(view: view),
                      Expanded(
                        child: Center(
                          child: AnimatedBuilder(
                            animation: _shake,
                            builder: (context, child) {
                              final t = _shake.value;
                              final dx =
                                  math.sin(t * math.pi * 10) * 6 * (1 - t);
                              return Transform.translate(
                                  offset: Offset(dx, 0), child: child);
                            },
                            child: BoardWidget(
                              key: _boardKey,
                              view: view,
                              ghost: ghost,
                            ),
                          ),
                        ),
                      ),
                      TrayWidget(
                        tray: view.tray,
                        draggingIndex: _dragIndex,
                        onDragStart: (index, global) => setState(() {
                          _dragIndex = index;
                          _dragGlobal = global;
                        }),
                        onDragUpdate: (global) =>
                            setState(() => _dragGlobal = global),
                        onDragEnd: () => _onDragEnd(view),
                      ),
                    ],
                  ),
                ),
                if (_dragIndex != null &&
                    draggedTopLeft != null &&
                    boardBox != null &&
                    _dragIndex! < view.tray.length)
                  _draggedPieceOverlay(view, draggedTopLeft, boardBox),
                Positioned.fill(child: EffectsOverlay(key: _effectsKey)),
                if (view.phase != GamePhase.playing) _resultOverlay(view),
                if (tutorialStep != null)
                  Positioned.fill(
                    child: TutorialOverlay(
                      step: tutorialStep,
                      handFrom: _tutorialHandFrom(),
                      handTo: _tutorialHandTo(view),
                      onSkip: () => ref.read(tutorialProvider.notifier).skip(),
                      onDismissHud: () =>
                          ref.read(tutorialProvider.notifier).dismissHud(),
                    ),
                  ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Offset _tutorialHandFrom() {
    final stackBox =
        _stackKey.currentContext?.findRenderObject() as RenderBox?;
    if (stackBox == null) return Offset.zero;
    // Tray sits at the bottom; aim at the left slot.
    final size = stackBox.size;
    return Offset(size.width * 0.18, size.height - 80);
  }

  Offset _tutorialHandTo(GameViewState view) {
    final boardBox = _boardBox;
    final stackBox =
        _stackKey.currentContext?.findRenderObject() as RenderBox?;
    if (boardBox == null || stackBox == null || view.tray.isEmpty) {
      return Offset.zero;
    }
    final geo = BoardGeometry(boardSize: boardBox.size.width);
    final notifier = ref.read(gameSessionProvider.notifier);
    for (var y = 0; y < GridState.size; y++) {
      for (var x = 0; x < GridState.size; x++) {
        if (notifier.canPlaceAt(0, x, y)) {
          final local =
              Offset((x + 0.5) * geo.cellSize, (y + 0.5) * geo.cellSize);
          return stackBox.globalToLocal(boardBox.localToGlobal(local));
        }
      }
    }
    return Offset.zero;
  }

  Widget _draggedPieceOverlay(
      GameViewState view, Offset topLeftGlobal, RenderBox boardBox) {
    final stackBox =
        _stackKey.currentContext!.findRenderObject()! as RenderBox;
    final local = stackBox.globalToLocal(topLeftGlobal);
    final cellSize = boardBox.size.width / 8;
    return Positioned(
      left: local.dx,
      top: local.dy,
      child: IgnorePointer(
        child: PieceWidget(piece: view.tray[_dragIndex!], cellSize: cellSize),
      ),
    );
  }

  Widget _resultOverlay(GameViewState view) {
    final l10n = AppLocalizations.of(context)!;
    final notifier = ref.read(gameSessionProvider.notifier);
    final theme = Theme.of(context);

    final flow = ref.read(seasonFlowProvider);
    final (title, actions) = switch ((view.phase, view.stuckReason)) {
      (GamePhase.won, _) => (
          l10n.stageClear,
          [
            if (flow != null && flow.hasNext)
              FilledButton(
                onPressed: () {
                  ref.read(seasonFlowProvider.notifier).nextStage();
                  if (!view.endless) {
                    ref.read(adServiceProvider).maybeShowInterstitial();
                  }
                },
                child: Text(l10n.nextStage),
              ),
            TextButton(
              onPressed: notifier.restart,
              child: Text(l10n.playAgain),
            ),
          ],
        ),
      (GamePhase.stuck, StuckReason.outOfMoves) => (
          l10n.outOfMoves,
          [
            if (!view.rescueUsed)
              FilledButton(
                onPressed: () async {
                  final earned =
                      await ref.read(adServiceProvider).showRewarded();
                  if (!earned) return;
                  ref.read(analyticsProvider).rescueUsed(type: 'extra_moves');
                  notifier.addExtraMoves();
                },
                child: Text(l10n.plusFiveMoves),
              ),
            if (view.rescueUsed)
              FilledButton(
                onPressed: notifier.declineAndLose,
                child: Text(l10n.giveUp),
              )
            else
              TextButton(
                onPressed: notifier.declineAndLose,
                child: Text(l10n.giveUp),
              ),
          ],
        ),
      (GamePhase.stuck, _) => (
          l10n.boardFull,
          [
            if (!view.rescueUsed)
              FilledButton(
                onPressed: () async {
                  final earned =
                      await ref.read(adServiceProvider).showRewarded();
                  if (!earned) return;
                  ref.read(analyticsProvider).rescueUsed(type: 'continue');
                  notifier.useContinue();
                },
                child: Text(l10n.watchAdContinue),
              ),
            if (view.rescueUsed)
              FilledButton(
                onPressed: notifier.declineAndLose,
                child: Text(l10n.giveUp),
              )
            else
              TextButton(
                onPressed: notifier.declineAndLose,
                child: Text(l10n.giveUp),
              ),
          ],
        ),
      (GamePhase.lost, _) when view.endless => (
          l10n.gameOver,
          [
            FilledButton(
              onPressed: () {
                setState(() => _endlessNewBest = false);
                notifier.restart();
              },
              child: Text(l10n.playAgain),
            ),
          ],
        ),
      (_, _) => (
          l10n.stageFailed,
          [
            FilledButton(
              onPressed: () {
                ref.read(adServiceProvider).maybeShowInterstitial();
                notifier.restart();
              },
              child: Text(l10n.playAgain),
            ),
          ],
        ),
    };

    return Positioned.fill(
      child: ColoredBox(
        color: Colors.black54,
        child: Center(
          child: Card(
            color: GamePalette.boardBackground,
            child: Padding(
              padding: const EdgeInsets.all(24),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Text(title, style: theme.textTheme.headlineSmall),
                  if (view.phase == GamePhase.won) ...[
                    const SizedBox(height: 12),
                    Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        for (var i = 0; i < 3; i++)
                          TweenAnimationBuilder<double>(
                            key: ValueKey(i),
                            tween: Tween(begin: 0, end: 1),
                            duration: Duration(milliseconds: 400 + i * 250),
                            curve: Interval(i * 0.22, 1, curve: Curves.elasticOut),
                            builder: (context, v, child) =>
                                Transform.scale(scale: v, child: child),
                            child: Icon(
                              Icons.star,
                              size: 44,
                              color: i < view.starsEarned ? Colors.amber : Colors.white24,
                            ),
                          ),
                      ],
                    ),
                  ],
                  if (view.phase == GamePhase.lost && view.endless) ...[
                    const SizedBox(height: 10),
                    Text('${view.score}',
                        style: theme.textTheme.displaySmall
                            ?.copyWith(fontWeight: FontWeight.w900)),
                    const SizedBox(height: 4),
                    Text(
                      _endlessNewBest
                          ? l10n.newBest
                          : l10n.bestScore(ref.read(endlessBestProvider)),
                      style: TextStyle(
                        color:
                            _endlessNewBest ? Colors.amber : Colors.white60,
                        fontWeight: FontWeight.w800,
                      ),
                    ),
                  ],
                  if (view.phase == GamePhase.lost && !view.endless && view.objectiveProgress > 0) ...[
                    const SizedBox(height: 16),
                    SizedBox(
                      width: 88,
                      height: 88,
                      child: Stack(
                        fit: StackFit.expand,
                        children: [
                          CircularProgressIndicator(
                            value: view.objectiveProgress,
                            strokeWidth: 7,
                            backgroundColor: Colors.white12,
                            color: Colors.amber,
                          ),
                          Center(
                            child: Text(
                              l10n.almostThere((view.objectiveProgress * 100).round()),
                              textAlign: TextAlign.center,
                              maxLines: 2,
                              overflow: TextOverflow.ellipsis,
                              style: theme.textTheme.labelSmall,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ],
                  const SizedBox(height: 20),
                  ...actions,
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}
