// lib/src/dispatcher.dart

import 'dart:async';

import 'package:angel3_container/angel3_container.dart';
import 'package:angel3_reactivex/angel3_reactivex.dart';
import 'package:angel3_event_bus/event_bus.dart';
import 'package:angel3_mq/mq.dart';

import 'command.dart';
import 'handler.dart';
import 'batch.dart';
import 'chain.dart';

/// A class that handles dispatching and processing of commands.
///
/// This dispatcher supports both synchronous and asynchronous command execution,
/// as well as queueing commands for later processing.
class Dispatcher implements QueueingDispatcher {
  final Container container;
  final EventBus _eventBus;
  final Subject<Command> _commandSubject;
  final MQClient _queue;
  final Map<Type, Type> _handlers = {};

  /// Creates a new [Dispatcher] instance.
  ///
  /// [container] is used for dependency injection and to retrieve necessary services.
  Dispatcher(this.container)
      : _eventBus = container.make<EventBus>(),
        _commandSubject = BehaviorSubject<Command>(),
        _queue = container.make<MQClient>() {
    _setupCommandProcessing();
  }

  /// Sets up the command processing pipeline.
  ///
  /// This method initializes the stream that processes commands and emits events.
  void _setupCommandProcessing() {
    _commandSubject
        .flatMap((command) => Stream.fromFuture(_processCommand(command))
            .map((result) => CommandEvent(command, result: result))
            .onErrorReturnWith(
                (error, stackTrace) => CommandEvent(command, error: error)))
        .listen((event) {
      _eventBus.fire(event);
    });
  }

  /// Dispatches a command for execution.
  ///
  /// If the command implements [ShouldQueue], it will be dispatched to a queue.
  /// Otherwise, it will be executed immediately.
  ///
  /// [command] is the command to be dispatched.
  @override
  Future<dynamic> dispatch(Command command) {
    if (command is ShouldQueue) {
      return dispatchToQueue(command);
    } else {
      return dispatchNow(command);
    }
  }

  /// Dispatches a command for immediate execution.
  ///
  /// [command] is the command to be executed.
  /// [handler] is an optional specific handler for the command.
  @override
  Future<dynamic> dispatchNow(Command command, [Handler? handler]) {
    final completer = Completer<dynamic>();
    _commandSubject.add(command);

    _eventBus
        .on<CommandEvent>()
        .where((event) => event.command == command)
        .take(1)
        .listen((event) {
      if (event.error != null) {
        completer.completeError(event.error);
      } else {
        completer.complete(event.result);
      }
    });

    return completer.future;
  }

  /// Processes a command by finding and executing its appropriate handler.
  ///
  /// [command] is the command to be processed.
  Future<dynamic> _processCommand(Command command) async {
    final handlerType = _handlers[command.runtimeType];
    if (handlerType != null) {
      final handler = container.make(handlerType) as Handler;
      return await handler.handle(command);
    } else {
      throw Exception('No handler found for command: ${command.runtimeType}');
    }
  }

  /// Dispatches a command to a queue for later processing.
  ///
  /// [command] is the command to be queued.
  @override
  Future<dynamic> dispatchToQueue(Command command) async {
    final message = Message(
      payload: command,
      headers: {
        'commandType': command.runtimeType.toString(),
      },
    );
    _queue.sendMessage(
      message: message,
      // You might want to specify an exchange name and routing key if needed
      // exchangeName: 'your_exchange_name',
      // routingKey: 'your_routing_key',
    );
    return message.id;
  }

  /// Dispatches a command synchronously.
  ///
  /// This is an alias for [dispatchNow].
  ///
  /// [command] is the command to be executed.
  /// [handler] is an optional specific handler for the command.
  @override
  Future<dynamic> dispatchSync(Command command, [Handler? handler]) {
    return dispatchNow(command, handler);
  }

  /// Finds a batch by its ID.
  ///
  /// [batchId] is the ID of the batch to find.
  @override
  Future<Batch?> findBatch(String batchId) async {
    // Implement batch finding logic
    throw UnimplementedError();
  }

  /// Creates a new pending batch of commands.
  ///
  /// [commands] is the list of commands to be included in the batch.
  @override
  PendingBatch batch(List<Command> commands) {
    return PendingBatch(this, commands);
  }

  /// Creates a new pending chain of commands.
  ///
  /// [commands] is the list of commands to be included in the chain.
  @override
  PendingChain chain(List<Command> commands) {
    return PendingChain(this, commands);
  }

  /// Applies a list of pipes to the command processing pipeline.
  ///
  /// [pipes] is the list of pipes to be applied.
  @override
  Dispatcher pipeThrough(List<Pipe> pipes) {
    _commandSubject.transform(
      StreamTransformer.fromHandlers(
        handleData: (data, sink) {
          var result = data;
          for (var pipe in pipes) {
            result = pipe(result);
          }
          sink.add(result);
        },
      ),
    );
    return this;
  }

  /// Maps command types to their respective handler types.
  ///
  /// [handlers] is a map where keys are command types and values are handler types.
  @override
  Dispatcher map(Map<Type, Type> handlers) {
    _handlers.addAll(handlers);
    return this;
  }

  /// Dispatches a command to be executed after the current request-response cycle.
  ///
  /// [command] is the command to be dispatched after the response.
  @override
  void dispatchAfterResponse(Command command) {
    final message = Message(
      payload: command,
      headers: {
        'commandType': command.runtimeType.toString(),
        'dispatchAfterResponse': 'true',
      },
    );

    _queue.sendMessage(
      message: message,
      // You might want to specify an exchange name if needed
      // exchangeName: 'your_exchange_name',
      // If you want to use a specific queue for after-response commands:
      routingKey: 'after_response_queue',
    );
  }
}

abstract class QueueingDispatcher {
  Future<dynamic> dispatch(Command command);
  Future<dynamic> dispatchSync(Command command, [Handler? handler]);
  Future<dynamic> dispatchNow(Command command, [Handler? handler]);
  Future<dynamic> dispatchToQueue(Command command);
  Future<Batch?> findBatch(String batchId);
  PendingBatch batch(List<Command> commands);
  PendingChain chain(List<Command> commands);
  Dispatcher pipeThrough(List<Pipe> pipes);
  Dispatcher map(Map<Type, Type> handlers);
  void dispatchAfterResponse(Command command);
}

typedef Pipe = Command Function(Command);

class CommandCompletedEvent extends AppEvent {
  final dynamic result;

  CommandCompletedEvent(this.result);

  @override
  List<Object?> get props => [result];
}

class CommandErrorEvent extends AppEvent {
  final dynamic error;

  CommandErrorEvent(this.error);

  @override
  List<Object?> get props => [error];
}

class CommandEvent extends AppEvent {
  final Command command;
  final dynamic result;
  final dynamic error;

  CommandEvent(this.command, {this.result, this.error});

  @override
  List<Object?> get props => [command, result, error];
}