/// The processing status of a [SyncOperation].
enum SyncOperationStatus {
  /// Waiting to be sent to the remote.
  pending,

  /// Currently being transmitted.
  inProgress,

  /// Successfully applied on the remote.
  completed,

  /// Failed after all retry attempts; requires operator attention.
  failed,
}

/// A single unit of work that must be applied to a remote system.
///
/// Operations are created locally (e.g. while offline) and flushed to the
/// remote when connectivity is restored.
class SyncOperation {
  /// Unique identifier for this operation.
  final String id;

  /// Logical type of the operation (e.g. `'createOrder'`, `'adjustInventory'`).
  final String type;

  /// Serialisable payload describing the operation.
  final Map<String, dynamic> payload;

  /// When this operation was enqueued.
  final DateTime enqueuedAt;

  /// Current processing status.
  final SyncOperationStatus status;

  /// Number of times this operation has been attempted.
  final int attemptCount;

  /// Human-readable error from the last failed attempt, if any.
  final String? lastError;

  const SyncOperation({
    required this.id,
    required this.type,
    required this.payload,
    required this.enqueuedAt,
    this.status = SyncOperationStatus.pending,
    this.attemptCount = 0,
    this.lastError,
  });

  /// Returns a copy of this operation with the given fields replaced.
  SyncOperation copyWith({SyncOperationStatus? status, int? attemptCount, String? lastError}) {
    return SyncOperation(
      id: id,
      type: type,
      payload: payload,
      enqueuedAt: enqueuedAt,
      status: status ?? this.status,
      attemptCount: attemptCount ?? this.attemptCount,
      lastError: lastError ?? this.lastError,
    );
  }

  @override
  String toString() =>
      'SyncOperation(id: $id, type: $type, status: $status, attempts: $attemptCount)';
}

/// Result returned after attempting to flush a [SyncOperation].
class SyncFlushResult {
  /// The operation that was attempted.
  final SyncOperation operation;

  /// Whether the flush succeeded.
  final bool success;

  /// Error message if [success] is `false`.
  final String? errorMessage;

  const SyncFlushResult({required this.operation, required this.success, this.errorMessage});

  const SyncFlushResult.success({required this.operation}) : success = true, errorMessage = null;

  const SyncFlushResult.failure({required this.operation, required this.errorMessage})
    : success = false;

  @override
  String toString() =>
      'SyncFlushResult(id: ${operation.id}, success: $success, error: $errorMessage)';
}

/// Abstraction for queuing offline operations and flushing them when online.
///
/// Implementations persist the queue across app restarts so that operations
/// captured while offline survive process termination.
abstract class SyncQueue {
  /// Adds [operation] to the tail of the queue.
  Future<void> enqueue(SyncOperation operation);

  /// Returns all operations currently in the queue, ordered by [SyncOperation.enqueuedAt].
  Future<List<SyncOperation>> pendingOperations();

  /// Attempts to apply [operation] to the remote via [flush].
  ///
  /// On success the operation is removed from the queue.
  /// On failure the operation's [SyncOperation.attemptCount] and
  /// [SyncOperation.lastError] are updated and it remains in the queue.
  Future<SyncFlushResult> process(
    SyncOperation operation,
    Future<void> Function(SyncOperation) flush,
  );

  /// Removes [operation] from the queue regardless of its status.
  Future<void> remove(String operationId);

  /// Removes all completed and failed operations, retaining only pending ones.
  Future<void> pruneCompleted();

  /// Returns the number of operations currently in the queue.
  Future<int> get length;
}

/// An in-memory [SyncQueue] suitable for testing and development.
///
/// Operations are not persisted across process restarts.
class InMemorySyncQueue implements SyncQueue {
  final List<SyncOperation> _queue = [];

  @override
  Future<void> enqueue(SyncOperation operation) async {
    _queue.add(operation);
  }

  @override
  Future<List<SyncOperation>> pendingOperations() async {
    return _queue.where((op) => op.status == SyncOperationStatus.pending).toList()
      ..sort((a, b) => a.enqueuedAt.compareTo(b.enqueuedAt));
  }

  @override
  Future<SyncFlushResult> process(
    SyncOperation operation,
    Future<void> Function(SyncOperation) flush,
  ) async {
    final index = _queue.indexWhere((op) => op.id == operation.id);
    if (index == -1) {
      return SyncFlushResult.failure(
        operation: operation,
        errorMessage: 'Operation not found in queue',
      );
    }

    // Mark as in-progress.
    _queue[index] = _queue[index].copyWith(status: SyncOperationStatus.inProgress);

    try {
      await flush(operation);
      _queue[index] = _queue[index].copyWith(
        status: SyncOperationStatus.completed,
        attemptCount: operation.attemptCount + 1,
      );
      return SyncFlushResult.success(operation: _queue[index]);
    } catch (e) {
      _queue[index] = _queue[index].copyWith(
        status: SyncOperationStatus.failed,
        attemptCount: operation.attemptCount + 1,
        lastError: e.toString(),
      );
      return SyncFlushResult.failure(operation: _queue[index], errorMessage: e.toString());
    }
  }

  @override
  Future<void> remove(String operationId) async {
    _queue.removeWhere((op) => op.id == operationId);
  }

  @override
  Future<void> pruneCompleted() async {
    _queue.removeWhere(
      (op) => op.status == SyncOperationStatus.completed || op.status == SyncOperationStatus.failed,
    );
  }

  @override
  Future<int> get length async => _queue.length;
}
