import '../../core/rng.dart';
import '../models/cell.dart';
import '../models/grid.dart';
import '../models/objective.dart';
import '../models/piece.dart';
import '../models/stage.dart';
import 'game_event.dart';
import 'line_clear.dart';
import 'piece_generator.dart';
import 'placement.dart';
import 'scoring.dart';

enum GamePhase { playing, won, stuck, lost }

enum StuckReason { outOfMoves, boardDead }

class PlacementResult {
  const PlacementResult({
    required this.events,
    required this.pointsGained,
    required this.linesCleared,
    required this.gemsCleared,
    required this.clearedRows,
    required this.clearedCols,
    required this.comboStreak,
  });

  final List<GameEvent> events;
  final int pointsGained;
  final int linesCleared;
  final int gemsCleared;

  /// Which lines vanished, for the UI clear animation.
  final List<int> clearedRows;
  final List<int> clearedCols;

  /// Streak level after this placement, for combo effects.
  final int comboStreak;
}

/// The single stateful pure-Dart session object for one stage attempt.
/// UI layers wrap this behind a notifier; it never imports Flutter.
class GameEngine {
  GameEngine(this._stage, {int attempt = 0, PieceGenerator? generator})
      : _generator =
            generator ?? PieceGenerator(SeededRng(_stage.seed ^ attempt)),
        _grid = _stage.initialGrid(),
        _objectives = List.of(_stage.objectives),
        _moveLimit = _stage.moveLimit {
    _tray = _generator.nextTray(_grid);
    _checkStuck();
  }

  static const int extraMovesGrant = 5;
  static const int continueRowsCleared = 2;

  final StageConfig _stage;
  final PieceGenerator _generator;

  GridState _grid;
  late List<Piece> _tray;
  List<Objective> _objectives;
  int _moveLimit;
  int _movesUsed = 0;
  int _score = 0;
  ComboState _combo = ComboState.initial;
  GamePhase _phase = GamePhase.playing;
  StuckReason? _stuckReason;
  bool _rescueUsed = false;

  GridState get grid => _grid;
  List<Piece> get tray => List.unmodifiable(_tray);
  int get score => _score;
  ComboState get combo => _combo;
  int get movesUsed => _movesUsed;
  // movesLeft: endless is effectively infinite.
  int get movesLeft =>
      _stage.endless ? 1 << 30 : _moveLimit - _movesUsed;

  // UI branch selector for endless mode.
  bool get endless => _stage.endless;
  List<Objective> get objectives => List.unmodifiable(_objectives);
  GamePhase get phase => _phase;
  StuckReason? get stuckReason => _stuckReason;
  bool get rescueUsed => _rescueUsed;

  int get starsEarned => _phase == GamePhase.won
      ? _stage.stars.starsFor(movesLeft: movesLeft)
      : 0;

  /// Overall objective completion in [0, 1] for the near-miss screen.
  double get objectiveProgress {
    if (_objectives.isEmpty) return 0;
    var total = 0.0;
    for (final obj in _objectives) {
      final frac = obj.current / obj.target;
      total += frac > 1 ? 1 : frac;
    }
    return total / _objectives.length;
  }

  /// Read-only legality probe (drag ghost preview, AI players, tests).
  bool tryPlaceWouldSucceed(int trayIndex, int x, int y) =>
      _phase == GamePhase.playing && canPlace(_grid, _tray[trayIndex], x, y);

  PlacementResult? tryPlace(int trayIndex, int x, int y) {
    if (_phase != GamePhase.playing) return null;
    final piece = _tray[trayIndex];
    if (!canPlace(_grid, piece, x, y)) return null;

    _tray.removeAt(trayIndex);
    _movesUsed++;

    final placed = place(_grid, piece, x, y);
    final clear = detectAndClear(placed);
    _grid = clear.grid;

    final delta = scorePlacement(
      cellsPlaced: piece.size,
      linesCleared: clear.linesCleared,
      combo: _combo,
    );
    _score += delta.points;
    _combo = delta.combo;

    final events = <GameEvent>[
      PiecePlaced(piece: piece, x: x, y: y),
      if (clear.linesCleared > 0)
        LinesCleared(lines: clear.linesCleared, gems: clear.gemsCleared),
      ScoreChanged(total: _score),
    ];
    _objectives = [
      for (final obj in _objectives)
        events.fold(obj, (o, event) => o.onEvent(event)),
    ];

    if (!_stage.endless && _objectives.every((o) => o.isComplete)) {
      _phase = GamePhase.won;
    } else {
      if (_tray.isEmpty) _tray = _generator.nextTray(_grid);
      _checkStuck();
    }

    return PlacementResult(
      events: events,
      pointsGained: delta.points,
      linesCleared: clear.linesCleared,
      gemsCleared: clear.gemsCleared,
      clearedRows: clear.clearedRows,
      clearedCols: clear.clearedCols,
      comboStreak: _combo.streak,
    );
  }

  void _checkStuck() {
    if (!_stage.endless && movesLeft <= 0) {
      _phase = GamePhase.stuck;
      _stuckReason = StuckReason.outOfMoves;
    } else if (!anyPlacementExists(_grid, _tray)) {
      _phase = GamePhase.stuck;
      _stuckReason = StuckReason.boardDead;
    } else {
      _phase = GamePhase.playing;
      _stuckReason = null;
    }
  }

  /// Rewarded-ad continue for a dead board: clears the two most-filled rows
  /// and redeals. One rescue (continue or extra moves) per attempt.
  void useContinue() {
    if (_phase != GamePhase.stuck ||
        _stuckReason != StuckReason.boardDead ||
        _rescueUsed) {
      throw StateError('continue not available');
    }
    _rescueUsed = true;

    final rowCounts = <(int, int)>[];
    for (var y = 0; y < GridState.size; y++) {
      var count = 0;
      for (var x = 0; x < GridState.size; x++) {
        if (_grid.isOccupied(x, y)) count++;
      }
      rowCounts.add((y, count));
    }
    rowCounts.sort((a, b) {
      final c = b.$2.compareTo(a.$2);
      return c != 0 ? c : a.$1.compareTo(b.$1);
    });
    for (final (y, _) in rowCounts.take(continueRowsCleared)) {
      for (var x = 0; x < GridState.size; x++) {
        _grid = _grid.withCell(x, y, const Cell(CellType.empty));
      }
    }

    _tray = _generator.nextTray(_grid);
    _checkStuck();
  }

  /// Rewarded-ad rescue for move exhaustion: +5 moves. One rescue per attempt.
  void addExtraMoves() {
    if (_phase != GamePhase.stuck ||
        _stuckReason != StuckReason.outOfMoves ||
        _rescueUsed) {
      throw StateError('extra moves not available');
    }
    _rescueUsed = true;
    _moveLimit += extraMovesGrant;
    _checkStuck();
  }

  /// Player declines rescue (or forfeits): the attempt is lost.
  void declineAndLose() {
    if (_phase == GamePhase.won || _phase == GamePhase.lost) {
      throw StateError('attempt already finished');
    }
    _phase = GamePhase.lost;
  }

  /// Booster: empties one filled cell. No move/score/combo/objective effect.
  /// Allowed only mid-attempt (playing or stuck). Returns false on an empty
  /// cell or finished attempt so the caller keeps the booster.
  bool useHammer(int x, int y) {
    if (_phase == GamePhase.won || _phase == GamePhase.lost) return false;
    if (!_grid.isOccupied(x, y)) return false;
    _grid = _grid.withCell(x, y, const Cell(CellType.empty));
    _checkStuck();
    return true;
  }

  /// Booster: re-deals the tray. No move/score effect. Re-checks stuck so a
  /// dead board with a hopeless tray can become playable again.
  bool useShuffle() {
    if (_phase == GamePhase.won || _phase == GamePhase.lost) return false;
    _tray = _generator.nextTray(_grid);
    _checkStuck();
    return true;
  }

  /// Booster: empties one row or one column (exactly one of [row]/[col]).
  /// No move/score/objective effect. Re-checks stuck.
  bool useLineBomb({int? row, int? col}) {
    if (_phase == GamePhase.won || _phase == GamePhase.lost) return false;
    if ((row == null) == (col == null)) return false; // need exactly one
    for (var i = 0; i < GridState.size; i++) {
      final x = col ?? i;
      final y = row ?? i;
      _grid = _grid.withCell(x, y, const Cell(CellType.empty));
    }
    _checkStuck();
    return true;
  }
}
