// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only

/// Coordinates Backlog board controller state for the FocusFlow Flutter UI.
library;

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:scheduler_core/scheduler_core.dart';

import '../models/backlog/backlog_board_screen_data.dart';
import 'selected_date_controller.dart';

/// Reads Backlog board state from the scheduler application layer.
typedef BacklogBoardReader =
    Future<ApplicationResult<BacklogBoardQueryResult>> Function(
      BacklogBoardReadRequest request,
    );

/// Reads one Backlog task detail model from the scheduler application layer.
typedef BacklogTaskDetailReader =
    Future<ApplicationResult<BacklogTaskDetailReadModel>> Function(
      String taskId,
      CivilDate localDate,
    );

/// Formats a selected date for Backlog loading and empty states.
typedef BacklogDateLabelFormatter = String Function(CivilDate date);

/// Immutable controller request for a Backlog board read.
class BacklogBoardReadRequest {
  /// Creates a Backlog board read request from UI filter state.
  BacklogBoardReadRequest({
    required this.localDate,
    this.searchText,
    List<BacklogFilter> filters = const <BacklogFilter>[],
    this.sortKey = BacklogSortKey.priority,
    this.groupMode = BacklogBoardGroupMode.none,
  }) : filters = List<BacklogFilter>.unmodifiable(filters);

  /// Owner-local date used for slot preview context.
  final CivilDate localDate;

  /// Optional search text entered by the user.
  final String? searchText;

  /// User-selected backlog filters.
  final List<BacklogFilter> filters;

  /// User-selected task ordering.
  final BacklogSortKey sortKey;

  /// User-selected grouping mode.
  final BacklogBoardGroupMode groupMode;
}

/// Base state for the Backlog board screen.
sealed class BacklogBoardScreenState {
  /// Creates a Backlog board screen state.
  const BacklogBoardScreenState();
}

/// Indicates that the Backlog board is loading.
class BacklogBoardLoading extends BacklogBoardScreenState {
  /// Creates a loading Backlog state.
  const BacklogBoardLoading(this.dateLabel);

  /// Display-ready date label for the read in progress.
  final String dateLabel;
}

/// Indicates that the Backlog board has renderable data.
class BacklogBoardReady extends BacklogBoardScreenState {
  /// Creates a ready Backlog state.
  const BacklogBoardReady(this.data);

  /// Presentation data for the Backlog board.
  final BacklogBoardScreenData data;
}

/// Indicates that the Backlog board loaded with no tasks.
class BacklogBoardEmpty extends BacklogBoardScreenState {
  /// Creates an empty Backlog state.
  const BacklogBoardEmpty(this.data);

  /// Presentation data for the empty Backlog board.
  final BacklogBoardScreenData data;
}

/// Indicates that the Backlog board failed to load.
class BacklogBoardFailure extends BacklogBoardScreenState {
  /// Creates a failed Backlog state.
  const BacklogBoardFailure({required this.message, required this.dateLabel});

  /// User-facing failure message or code.
  final String message;

  /// Display-ready date label for the failed read.
  final String dateLabel;
}

/// Loads Backlog board data and tracks UI-local selection/filter state.
class BacklogBoardController extends ChangeNotifier {
  /// Creates a Backlog board controller from read callbacks.
  BacklogBoardController({
    required this.readBoard,
    required this.readTaskDetail,
    required this.selectedDates,
    required this.dateLabelFor,
  }) : _state = BacklogBoardLoading(dateLabelFor(selectedDates.date)) {
    selectedDates.addListener(_handleSelectedDateChanged);
  }

  /// Callback used to read board state.
  final BacklogBoardReader readBoard;

  /// Callback used to read selected task detail state.
  final BacklogTaskDetailReader readTaskDetail;

  /// Selected planning date used for board and suggestion reads.
  final SelectedDateController selectedDates;

  /// Formats dates before a backend read has returned board data.
  final BacklogDateLabelFormatter dateLabelFor;

  /// Private state stored as `_state` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  BacklogBoardScreenState _state;

  /// Private state stored as `_selectedTaskId` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  String? _selectedTaskId;

  /// Private state stored as `_selectedTaskDetail` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  BacklogTaskDetailReadModel? _selectedTaskDetail;

  /// Private state stored as `_searchText` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  String? _searchText;

  /// Private state stored as `_filters` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  List<BacklogFilter> _filters = const <BacklogFilter>[];

  /// Private state stored as `_sortKey` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  BacklogSortKey _sortKey = BacklogSortKey.priority;

  /// Private state stored as `_groupMode` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  BacklogBoardGroupMode _groupMode = BacklogBoardGroupMode.none;

  /// Private state stored as `_loadSequence` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  var _loadSequence = 0;

  /// Private state stored as `_detailSequence` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  var _detailSequence = 0;

  /// Private state stored as `_isDisposed` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  var _isDisposed = false;

  /// Current Backlog board screen state.
  BacklogBoardScreenState get state => _state;

  /// Currently selected Backlog task id, if any.
  String? get selectedTaskId => _selectedTaskId;

  /// Current search text applied to board reads.
  String? get searchText => _searchText;

  /// Current filters applied to board reads.
  List<BacklogFilter> get filters => _filters;

  /// Current sort key applied to board reads.
  BacklogSortKey get sortKey => _sortKey;

  /// Current group mode applied to board reads.
  BacklogBoardGroupMode get groupMode => _groupMode;

  /// Loads the current Backlog board state.
  Future<void> load() async {
    final date = selectedDates.date;
    final sequence = _nextLoadSequence();
    logger.debug(
      () =>
          'Backlog board load started. date=${date.toIsoString()} sequence=$sequence',
    );
    _state = BacklogBoardLoading(dateLabelFor(date));
    notifyListeners();
    try {
      final result = await readBoard(
        BacklogBoardReadRequest(
          localDate: date,
          searchText: _searchText,
          filters: _filters,
          sortKey: _sortKey,
          groupMode: _groupMode,
        ),
      );
      if (_isStale(sequence)) {
        logger.finer(
          () =>
              'Ignoring stale Backlog board load result. '
              'date=${date.toIsoString()} sequence=$sequence currentSequence=$_loadSequence',
        );
        return;
      }
      final failure = result.failure;
      if (failure != null) {
        logger.warn(
          () =>
              'Backlog board load returned failure. '
              'date=${date.toIsoString()} code=${failure.code.name} '
              'detailCode=${failure.detailCode} entityId=${failure.entityId}',
        );
        _state = BacklogBoardFailure(
          message: failure.detailCode ?? failure.code.name,
          dateLabel: dateLabelFor(date),
        );
        notifyListeners();
        return;
      }

      final board = result.requireValue;
      _syncSelectedTask(board);
      final data = BacklogBoardScreenData(
        board: board,
        selectedTaskDetail: _selectedTaskDetail,
      );
      _state = board.summary.totalTaskCount == 0
          ? BacklogBoardEmpty(data)
          : BacklogBoardReady(data);
      logger.debug(
        () =>
            'Backlog board load finished. date=${date.toIsoString()} '
            'sequence=$sequence taskCount=${board.summary.totalTaskCount}',
      );
      notifyListeners();
    } on Object catch (error, stackTrace) {
      if (_isStale(sequence)) {
        logger.finer(
          () =>
              'Ignoring stale Backlog board load exception. '
              'date=${date.toIsoString()} sequence=$sequence currentSequence=$_loadSequence',
        );
        return;
      }
      logger.error(
        () =>
            'Backlog board load threw unexpectedly. date=${date.toIsoString()} sequence=$sequence',
        error: error,
        stackTrace: stackTrace,
      );
      _state = BacklogBoardFailure(
        message: error.toString(),
        dateLabel: dateLabelFor(date),
      );
      notifyListeners();
    }
  }

  /// Selects [taskId] and requests drawer detail data.
  Future<void> selectTask(String taskId) async {
    if (_selectedTaskId == taskId && _selectedTaskDetail != null) {
      return;
    }
    _selectedTaskId = taskId;
    _selectedTaskDetail = null;
    notifyListeners();
    final sequence = _nextDetailSequence();
    final date = selectedDates.date;
    logger.finer(
      () =>
          'Backlog detail load started. taskId=$taskId date=${date.toIsoString()} sequence=$sequence',
    );
    final result = await readTaskDetail(taskId, date);
    if (_isDetailStale(sequence, taskId)) {
      logger.finer(
        () =>
            'Ignoring stale Backlog detail load result. '
            'taskId=$taskId sequence=$sequence currentSequence=$_detailSequence',
      );
      return;
    }
    final failure = result.failure;
    if (failure != null) {
      logger.warn(
        () =>
            'Backlog detail load returned failure. taskId=$taskId '
            'code=${failure.code.name} detailCode=${failure.detailCode}',
      );
      _selectedTaskId = null;
      _selectedTaskDetail = null;
      notifyListeners();
      return;
    }
    _selectedTaskDetail = result.requireValue;
    _refreshDataState();
    notifyListeners();
  }

  /// Clears UI-local task selection.
  void clearSelection() {
    if (_selectedTaskId == null && _selectedTaskDetail == null) {
      return;
    }
    _selectedTaskId = null;
    _selectedTaskDetail = null;
    _detailSequence += 1;
    _refreshDataState();
    notifyListeners();
  }

  /// Updates search text and reloads the board.
  Future<void> updateSearchText(String? value) async {
    final trimmed = value?.trim();
    final next = trimmed == null || trimmed.isEmpty ? null : trimmed;
    if (next == _searchText) {
      return;
    }
    _searchText = next;
    await load();
  }

  /// Updates selected filters and reloads the board.
  Future<void> updateFilters(List<BacklogFilter> value) async {
    _filters = List<BacklogFilter>.unmodifiable(value);
    await load();
  }

  /// Updates sort key and reloads the board.
  Future<void> updateSortKey(BacklogSortKey value) async {
    if (value == _sortKey) {
      return;
    }
    _sortKey = value;
    await load();
  }

  /// Updates group mode and reloads the board.
  Future<void> updateGroupMode(BacklogBoardGroupMode value) async {
    if (value == _groupMode) {
      return;
    }
    _groupMode = value;
    await load();
  }

  /// Retries the latest board read.
  Future<void> retry() => load();

  /// Releases selected-date listeners owned by this controller.
  @override
  void dispose() {
    _isDisposed = true;
    selectedDates.removeListener(_handleSelectedDateChanged);
    super.dispose();
  }

  /// Runs the `_handleSelectedDateChanged` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  void _handleSelectedDateChanged() {
    logger.debug(
      () =>
          'Backlog board selected date changed. date=${selectedDates.date.toIsoString()}',
    );
    clearSelection();
    unawaited(load());
  }

  /// Runs the `_syncSelectedTask` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  void _syncSelectedTask(BacklogBoardQueryResult board) {
    final selectedId = _selectedTaskId;
    if (selectedId == null) {
      return;
    }
    final stillVisible = board.columns
        .expand((column) => column.items)
        .any((item) => item.taskId == selectedId);
    if (stillVisible) {
      return;
    }
    _selectedTaskId = null;
    _selectedTaskDetail = null;
  }

  /// Runs the `_refreshDataState` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  void _refreshDataState() {
    switch (_state) {
      case BacklogBoardReady(:final data):
        _state = BacklogBoardReady(
          BacklogBoardScreenData(
            board: data.board,
            selectedTaskDetail: _selectedTaskDetail,
          ),
        );
      case BacklogBoardEmpty(:final data):
        _state = BacklogBoardEmpty(
          BacklogBoardScreenData(
            board: data.board,
            selectedTaskDetail: _selectedTaskDetail,
          ),
        );
      case BacklogBoardLoading() || BacklogBoardFailure():
        break;
    }
  }

  /// Runs the `_nextLoadSequence` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  int _nextLoadSequence() {
    _loadSequence += 1;
    return _loadSequence;
  }

  /// Runs the `_nextDetailSequence` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  int _nextDetailSequence() {
    _detailSequence += 1;
    return _detailSequence;
  }

  /// Runs the `_isStale` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  bool _isStale(int sequence) {
    return _isDisposed || sequence != _loadSequence;
  }

  /// Runs the `_isDetailStale` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  bool _isDetailStale(int sequence, String taskId) {
    return _isDisposed ||
        sequence != _detailSequence ||
        taskId != _selectedTaskId;
  }
}
