import '../../features/workspaces/domain/workspace_task.dart';
import 'proto_socket_envelope.dart';
import 'proto_socket_lifecycle.dart';

/// Typed client exception mapped from a Core `task` channel error envelope.
///
/// Preserves the `retryable` hint so callers can distinguish transient failures
/// (`internal.error`) from terminal ones (`task.not_found`, `task.conflict`).
class ProtoSocketTaskException implements Exception {
  final String code;
  final String message;
  final bool retryable;

  const ProtoSocketTaskException({
    required this.code,
    required this.message,
    required this.retryable,
  });

  factory ProtoSocketTaskException.fromError(ProtoSocketEnvelopeError error) {
    return ProtoSocketTaskException(
      code: error.code,
      message: error.message,
      retryable: error.retryable,
    );
  }

  @override
  String toString() =>
      'ProtoSocketTaskException(code: $code, message: $message, '
      'retryable: $retryable)';
}

/// Flutter client for the Core `task` channel proto-socket actions.
///
/// Sends request envelopes through a [ProtoSocketTransport] and maps responses
/// onto [WorkspaceTask] models. Action names match the Core dispatcher exactly:
/// `task.list`, `task.get`, `task.enqueue`, `task.create`.
class ProtoSocketTaskService {
  final ProtoSocketTransport _transport;
  int _messageCounter = 0;

  ProtoSocketTaskService(this._transport);

  Future<List<WorkspaceTask>> listTasks({int limit = 20}) async {
    final response = await _transport.sendRequest(
      _request('task.list', {'limit': limit}),
    );
    _throwIfError(response);
    final rawTasks = response.payload['tasks'];
    if (rawTasks is! List) return const [];
    return rawTasks
        .whereType<Map>()
        .map((item) => WorkspaceTask.fromMap(Map<String, Object?>.from(item)))
        .toList(growable: false);
  }

  Future<WorkspaceTask> getTask(String id) async {
    final response = await _transport.sendRequest(
      _request('task.get', {'id': id}),
    );
    _throwIfError(response);
    return _parseTask(response, 'task.get');
  }

  Future<WorkspaceTask> enqueueTask(String id) async {
    final response = await _transport.sendRequest(
      _request('task.enqueue', {'id': id}),
    );
    _throwIfError(response);
    return _parseTask(response, 'task.enqueue');
  }

  Future<WorkspaceTask> createTask({
    required String title,
    required String source,
    Map<String, Object?> payload = const {},
    Map<String, Object?>? metadata,
    Map<String, Object?>? external,
  }) async {
    final request = <String, Object?>{
      'title': title,
      'source': source,
      'payload': payload,
      'metadata': ?metadata,
      'external': ?external,
    };
    final response = await _transport.sendRequest(
      _request('task.create', request),
    );
    _throwIfError(response);
    return _parseTask(response, 'task.create');
  }

  ProtoSocketEnvelope _request(String action, Map<String, Object?> payload) {
    return ProtoSocketEnvelope(
      id: _newMessageId(),
      type: 'request',
      channel: 'task',
      action: action,
      payload: payload,
    );
  }

  /// Maps a success response carrying a single `task` map onto a model.
  ///
  /// A malformed success payload (missing or wrong-typed `task`) is treated as a
  /// service-level failure so callers never silently receive an empty task.
  WorkspaceTask _parseTask(ProtoSocketEnvelope response, String action) {
    final rawTask = response.payload['task'];
    if (rawTask is! Map) {
      throw ProtoSocketTaskException(
        code: 'client.malformed_response',
        message: '$action response is missing a task payload',
        retryable: false,
      );
    }
    return WorkspaceTask.fromMap(Map<String, Object?>.from(rawTask));
  }

  void _throwIfError(ProtoSocketEnvelope response) {
    final error = response.error;
    if (error != null) {
      throw ProtoSocketTaskException.fromError(error);
    }
  }

  String _newMessageId() =>
      'msg-${DateTime.now().microsecondsSinceEpoch}-${_messageCounter++}';
}
