import 'dart:async';

import 'package:flutter/foundation.dart';

import 'engine.dart';
import 'levels.dart';
import 'types.dart';

class MismatchState {
  final int token;
  final List<String> ids;

  const MismatchState({required this.token, required this.ids});

  MismatchState copyWith({int? token, List<String>? ids}) {
    return MismatchState(token: token ?? this.token, ids: ids ?? this.ids);
  }
}

class GameController extends ChangeNotifier {
  final LevelConfig level;
  final List<String> allFaceIds;
  late final int flipBackDelayMs;
  late final int? maxMistakes;

  GameStatus status = GameStatus.playing;
  List<CardModel> cards = [];
  List<String> flippedIds = [];
  int moves = 0;
  int mistakes = 0;
  MismatchState mismatch = const MismatchState(token: 0, ids: []);
  int score = 0;
  int streak = 0;

  bool _locked = false;
  Timer? _timer;

  GameController({required this.level, required this.allFaceIds}) {
    flipBackDelayMs = level.flipBackDelayMs ?? 600;
    maxMistakes = level.maxMistakes;
    cards = _build();
  }

  List<CardModel> _build() {
    assert(
      level.totalCards % 2 == 0,
      'level.totalCards must be even, current=${level.totalCards}, level=${level.id}',
    );

    return createDeck(
      rows: level.rows,
      totalCards: level.totalCards,
      poolSize: level.poolSize,
      allFaceIds: allFaceIds,
    );
  }

  int _pow2(int n) => 1 << n;

  void _clearTimer() {
    _timer?.cancel();
    _timer = null;
  }

  void reset() {
    _clearTimer();
    _locked = false;
    status = GameStatus.playing;
    cards = _build();
    flippedIds = [];
    moves = 0;
    mistakes = 0;
    mismatch = const MismatchState(token: 0, ids: []);
    score = 0;
    streak = 0;
    notifyListeners();
  }

  void reduceMistakes(int n) {
    if (n <= 0) return;
    mistakes = (mistakes - n).clamp(0, 1 << 30);
    if (status == GameStatus.lost) {
      status = GameStatus.playing;
    }
    mismatch = mismatch.copyWith(ids: []);
    notifyListeners();
  }

  void reduceMoves(int n) {
    if (n <= 0) return;
    moves = (moves - n).clamp(0, 1 << 30);
    notifyListeners();
  }

  void flip(String cardId) {
    if (status != GameStatus.playing || _locked) return;

    final index = cards.indexWhere((e) => e.id == cardId);
    if (index < 0) return;

    final target = cards[index];
    if (target.isMatched || target.isFlipped) return;

    cards = cards
        .map((e) => e.id == cardId ? e.copyWith(isFlipped: true) : e)
        .toList();
    flippedIds = [...flippedIds, cardId];

    if (flippedIds.length < 2) {
      notifyListeners();
      return;
    }

    _locked = true;

    final aId = flippedIds[0];
    final bId = flippedIds[1];
    final a = cards.firstWhere((e) => e.id == aId);
    final b = cards.firstWhere((e) => e.id == bId);

    moves += 1;

    if (a.faceId == b.faceId) {
      cards = cards.map((e) {
        if (e.id == aId || e.id == bId) {
          return e.copyWith(isMatched: true);
        }
        return e;
      }).toList();

      streak += 1;
      score += _pow2(streak - 1);
      flippedIds = [];
      mismatch = mismatch.copyWith(ids: []);

      if (cards.every((e) => e.isMatched)) {
        status = GameStatus.won;
      }

      _locked = false;
      notifyListeners();
      return;
    }

    mistakes += 1;
    score = score > 0 ? score - 1 : 0;
    streak = 0;
    mismatch = MismatchState(token: mismatch.token + 1, ids: [aId, bId]);

    final lostNow = maxMistakes != null && mistakes >= maxMistakes!;
    if (lostNow) {
      status = GameStatus.lost;
    }

    _clearTimer();
    _timer = Timer(Duration(milliseconds: flipBackDelayMs), () {
      cards = cards.map((e) {
        if (e.id == aId || e.id == bId) {
          return e.copyWith(isFlipped: false);
        }
        return e;
      }).toList();

      flippedIds = [];
      mismatch = mismatch.copyWith(ids: []);

      final lost = maxMistakes != null && mistakes >= maxMistakes!;
      if (lost) {
        status = GameStatus.lost;
      }

      _locked = false;
      notifyListeners();
    });

    notifyListeners();
  }

  int reviveWithReward({
    int baseSeconds = 30,
    int penaltyPerMove = 10,
    int? rollbackMistakes,
    int? rollbackMoves,
  }) {
    final backMistakes = rollbackMistakes ?? mistakes;
    final rewardSec = baseSeconds < 0 ? 0 : baseSeconds;

    _locked = false;
    _clearTimer();
    mistakes = (mistakes - backMistakes).clamp(0, 1 << 30);

    if (rollbackMoves != null) {
      moves = (moves - rollbackMoves).clamp(0, 1 << 30);
    }

    flippedIds = [];
    mismatch = mismatch.copyWith(ids: []);
    status = GameStatus.playing;
    notifyListeners();

    return rewardSec;
  }

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