import 'dart:async';

import 'proto_socket_envelope.dart';
import 'proto_socket_lifecycle.dart';

class ChangedFile {
  final String path;
  final String changeType;

  const ChangedFile({
    required this.path,
    required this.changeType,
  });

  factory ChangedFile.fromMap(Map<String, Object?> map) {
    final path = map['path'];
    final changeType = map['change_type'];
    if (path is! String || changeType is! String) {
      throw const FormatException('Invalid ChangedFile payload');
    }
    return ChangedFile(path: path, changeType: changeType);
  }
}

class BranchUpdatedEvent {
  final String id;
  final String type;
  final String provider;
  final String deliveryId;
  final String repoId;
  final String branch;
  final String before;
  final String after;
  final List<ChangedFile> changedFiles;
  final String observedAt;
  final String createdAt;

  const BranchUpdatedEvent({
    required this.id,
    required this.type,
    required this.provider,
    required this.deliveryId,
    required this.repoId,
    required this.branch,
    required this.before,
    required this.after,
    required this.changedFiles,
    required this.observedAt,
    required this.createdAt,
  });

  factory BranchUpdatedEvent.fromMap(Map<String, Object?> map) {
    final id = map['id'];
    final type = map['type'];
    final provider = map['provider'];
    final deliveryId = map['delivery_id'];
    final repoId = map['repo_id'];
    final branch = map['branch'];
    final before = map['before'];
    final after = map['after'];
    final rawFiles = map['changed_files'];
    final observedAt = map['observed_at'];
    final createdAt = map['created_at'];

    if (id is! String ||
        type is! String ||
        provider is! String ||
        deliveryId is! String ||
        repoId is! String ||
        branch is! String ||
        before is! String ||
        after is! String ||
        rawFiles is! List ||
        observedAt is! String ||
        createdAt is! String) {
      throw const FormatException('Invalid BranchUpdatedEvent payload');
    }

    final changedFiles = <ChangedFile>[];
    for (final file in rawFiles) {
      if (file is! Map) {
        throw const FormatException('Invalid ChangedFile item');
      }
      changedFiles.add(ChangedFile.fromMap(Map<String, Object?>.from(file)));
    }

    return BranchUpdatedEvent(
      id: id,
      type: type,
      provider: provider,
      deliveryId: deliveryId,
      repoId: repoId,
      branch: branch,
      before: before,
      after: after,
      changedFiles: changedFiles,
      observedAt: observedAt,
      createdAt: createdAt,
    );
  }
}

class ProtoSocketBranchEventException implements Exception {
  final String code;
  final String message;
  final bool retryable;

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

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

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

class ProtoSocketBranchEventService {
  final ProtoSocketTransport _transport;
  final StreamController<BranchUpdatedEvent> _updates =
      StreamController<BranchUpdatedEvent>.broadcast();
  final Set<String> _filters = {};
  StreamSubscription<ProtoSocketEnvelope>? _subscription;
  int _messageCounter = 0;

  ProtoSocketBranchEventService(this._transport) {
    _subscription = _transport.events.listen(
      _handleEnvelope,
      onError: _updates.addError,
    );
  }

  Stream<BranchUpdatedEvent> get branchUpdates => _updates.stream;

  Future<void> subscribe({required String repoId, required String branch}) async {
    final response = await _transport.sendRequest(ProtoSocketEnvelope(
      id: _newMessageId(),
      type: 'request',
      channel: 'event',
      action: 'event.subscribe',
      payload: {
        'events': ['branch.updated'],
        'repo_id': repoId,
        'branch': branch,
      },
    ));

    final error = response.error;
    if (error != null) {
      throw ProtoSocketBranchEventException.fromError(error);
    }

    _filters.add('$repoId:$branch');
  }

  void dispose() {
    _subscription?.cancel();
    _updates.close();
  }

  void _handleEnvelope(ProtoSocketEnvelope envelope) {
    if (envelope.type == 'event' &&
        envelope.channel == 'event' &&
        envelope.action == 'branch.updated') {
      try {
        final event = BranchUpdatedEvent.fromMap(envelope.payload);
        final key = '${event.repoId}:${event.branch}';
        if (_filters.contains(key)) {
          if (!_updates.isClosed) {
            _updates.add(event);
          }
        }
      } catch (err) {
        if (!_updates.isClosed) {
          _updates.addError(err);
        }
      }
    }
  }

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