import 'package:protobuf/well_known_types/google/protobuf/struct.pb.dart';

const protoSocketProtocolVersion = 'nomadcode.proto-socket.v1';

class ProtoSocketEnvelope {
  final String protocolVersion;
  final String id;
  final String correlationId;
  final String type;
  final String channel;
  final String action;
  final Map<String, Object?> actor;
  final Map<String, Object?> auth;
  final Map<String, Object?> payload;
  final ProtoSocketEnvelopeError? error;
  final Map<String, Object?> meta;

  const ProtoSocketEnvelope({
    required this.id,
    required this.type,
    required this.channel,
    required this.action,
    this.protocolVersion = protoSocketProtocolVersion,
    this.correlationId = '',
    this.actor = const {},
    this.auth = const {},
    this.payload = const {},
    this.error,
    this.meta = const {},
  });

  factory ProtoSocketEnvelope.fromStruct(Struct value) =>
      ProtoSocketEnvelope.fromMap(structToMap(value));

  factory ProtoSocketEnvelope.fromMap(Map<String, Object?> value) {
    return ProtoSocketEnvelope(
      protocolVersion:
          _stringValue(value['protocol_version']) ?? protoSocketProtocolVersion,
      id: _stringValue(value['id']) ?? '',
      correlationId: _stringValue(value['correlation_id']) ?? '',
      type: _stringValue(value['type']) ?? '',
      channel: _stringValue(value['channel']) ?? '',
      action: _stringValue(value['action']) ?? '',
      actor: _mapValue(value['actor']),
      auth: _mapValue(value['auth']),
      payload: _mapValue(value['payload']),
      error: ProtoSocketEnvelopeError.fromObject(value['error']),
      meta: _mapValue(value['meta']),
    );
  }

  Map<String, Object?> toMap() {
    final value = <String, Object?>{
      'protocol_version': protocolVersion,
      'id': id,
      'correlation_id': correlationId,
      'type': type,
      'channel': channel,
      'action': action,
      'payload': payload,
    };
    if (actor.isNotEmpty) value['actor'] = actor;
    if (auth.isNotEmpty) value['auth'] = auth;
    if (error != null) value['error'] = error!.toMap();
    if (meta.isNotEmpty) value['meta'] = meta;
    return value;
  }

  Struct toStruct() => mapToStruct(toMap());
}

class ProtoSocketEnvelopeError {
  final String code;
  final String message;
  final bool retryable;
  final Map<String, Object?> details;

  const ProtoSocketEnvelopeError({
    required this.code,
    required this.message,
    required this.retryable,
    this.details = const {},
  });

  factory ProtoSocketEnvelopeError.fromMap(Map<String, Object?> value) {
    return ProtoSocketEnvelopeError(
      code: _stringValue(value['code']) ?? '',
      message: _stringValue(value['message']) ?? '',
      retryable: _boolValue(value['retryable']) ?? false,
      details: _mapValue(value['details']),
    );
  }

  static ProtoSocketEnvelopeError? fromObject(Object? value) {
    if (value == null) return null;
    if (value is Map<String, Object?>) {
      return ProtoSocketEnvelopeError.fromMap(value);
    }
    if (value is Map) {
      return ProtoSocketEnvelopeError.fromMap(
        value.map((key, entry) => MapEntry(key.toString(), entry)),
      );
    }
    return null;
  }

  Map<String, Object?> toMap() {
    final value = <String, Object?>{
      'code': code,
      'message': message,
      'retryable': retryable,
    };
    if (details.isNotEmpty) value['details'] = details;
    return value;
  }
}

Struct mapToStruct(Map<String, Object?> value) => Struct(
  fields: value.entries.map(
    (entry) => MapEntry(entry.key, valueFromObject(entry.value)),
  ),
);

Map<String, Object?> structToMap(Struct value) =>
    value.fields.map((key, entry) => MapEntry(key, objectFromValue(entry)));

Value valueFromObject(Object? value) {
  if (value == null) return Value(nullValue: NullValue.NULL_VALUE);
  if (value is bool) return Value(boolValue: value);
  if (value is num) return Value(numberValue: value.toDouble());
  if (value is String) return Value(stringValue: value);
  if (value is Map<String, Object?>) {
    return Value(structValue: mapToStruct(value));
  }
  if (value is Map) {
    return Value(
      structValue: mapToStruct(
        value.map((key, entry) => MapEntry(key.toString(), entry)),
      ),
    );
  }
  if (value is Iterable) {
    return Value(
      listValue: ListValue(
        values: value.map((entry) => valueFromObject(entry)),
      ),
    );
  }
  throw ArgumentError.value(value, 'value', 'Unsupported Struct value type');
}

Object? objectFromValue(Value value) {
  switch (value.whichKind()) {
    case Value_Kind.nullValue:
      return null;
    case Value_Kind.numberValue:
      return value.numberValue;
    case Value_Kind.stringValue:
      return value.stringValue;
    case Value_Kind.boolValue:
      return value.boolValue;
    case Value_Kind.structValue:
      return structToMap(value.structValue);
    case Value_Kind.listValue:
      return value.listValue.values.map(objectFromValue).toList();
    case Value_Kind.notSet:
      return null;
  }
}

String? _stringValue(Object? value) => value is String ? value : null;

bool? _boolValue(Object? value) => value is bool ? value : null;

Map<String, Object?> _mapValue(Object? value) {
  if (value is Map<String, Object?>) return value;
  if (value is Map) {
    return value.map((key, entry) => MapEntry(key.toString(), entry));
  }
  return const {};
}
