import 'dart:async';

import 'proto_socket_endpoint_config.dart';
import 'proto_socket_envelope.dart';

enum ProtoSocketConnectionState { disconnected, connecting, connected, failed }

class ProtoSocketDiagnostics {
  final ProtoSocketConnectionState state;
  final String? connectionId;
  final String? protocolVersion;
  final String? channel;
  final String? action;
  final String? errorCode;
  final String? timestamp;

  const ProtoSocketDiagnostics({
    required this.state,
    this.connectionId,
    this.protocolVersion,
    this.channel,
    this.action,
    this.errorCode,
    this.timestamp,
  });

  ProtoSocketDiagnostics copyWithState(ProtoSocketConnectionState next) {
    return ProtoSocketDiagnostics(
      state: next,
      connectionId: connectionId,
      protocolVersion: protocolVersion,
      channel: channel,
      action: action,
      errorCode: errorCode,
      timestamp: timestamp,
    );
  }

  ProtoSocketDiagnostics fromEnvelope(ProtoSocketEnvelope envelope) {
    return ProtoSocketDiagnostics(
      state: state,
      connectionId:
          _stringValue(envelope.meta['connection_id']) ?? connectionId,
      protocolVersion:
          _nonEmptyString(envelope.protocolVersion) ??
          _stringValue(envelope.meta['protocol_version']) ??
          protocolVersion,
      channel:
          _nonEmptyString(envelope.channel) ??
          _stringValue(envelope.meta['channel']) ??
          channel,
      action:
          _nonEmptyString(envelope.action) ??
          _stringValue(envelope.meta['action']) ??
          action,
      errorCode:
          _nonEmptyString(envelope.error?.code) ??
          _nonEmptyString(_stringValue(envelope.meta['error_code'])),
      timestamp: _stringValue(envelope.meta['timestamp']) ?? timestamp,
    );
  }
}

abstract interface class ProtoSocketTransport {
  Future<ProtoSocketEnvelope> sendRequest(
    ProtoSocketEnvelope envelope, {
    Duration timeout = const Duration(seconds: 30),
  });
  Stream<ProtoSocketEnvelope> get events;
  Future<void> close();
}

/// Pluggable connector. Production wires a real proto-socket client; tests
/// inject a fake to drive lifecycle transitions deterministically without
/// opening network sockets.
abstract interface class ProtoSocketConnector {
  Future<ProtoSocketTransport> connect(ProtoSocketEndpointConfig config);
  Future<void> disconnect();
}

/// App-facing lifecycle facade for proto-socket integration.
///
/// Owns the connection state machine and exposes a stream for observers.
/// Does NOT auto-connect on construction — bootstrap creates the facade with
/// a config and an explicit `connect()` call (or skips it entirely when the
/// integration is disabled).
class ProtoSocketLifecycle {
  final ProtoSocketConnector _connector;
  final StreamController<ProtoSocketConnectionState> _stateController =
      StreamController<ProtoSocketConnectionState>.broadcast();
  final StreamController<ProtoSocketDiagnostics> _diagnosticsController =
      StreamController<ProtoSocketDiagnostics>.broadcast();

  ProtoSocketConnectionState _state = ProtoSocketConnectionState.disconnected;
  ProtoSocketDiagnostics _diagnostics = const ProtoSocketDiagnostics(
    state: ProtoSocketConnectionState.disconnected,
  );
  Object? _lastError;
  ProtoSocketTransport? _transport;
  StreamSubscription<ProtoSocketEnvelope>? _eventDiagnosticsSub;

  ProtoSocketLifecycle({required ProtoSocketConnector connector})
    : _connector = connector;

  ProtoSocketConnectionState get state => _state;
  Stream<ProtoSocketConnectionState> get stateStream => _stateController.stream;
  ProtoSocketDiagnostics get diagnostics => _diagnostics;
  Stream<ProtoSocketDiagnostics> get diagnosticsStream =>
      _diagnosticsController.stream;
  Object? get lastError => _lastError;
  ProtoSocketTransport? get transport => _transport;

  Future<void> connect(ProtoSocketEndpointConfig config) async {
    if (_state == ProtoSocketConnectionState.connecting ||
        _state == ProtoSocketConnectionState.connected) {
      return;
    }
    // Starting a (re)connect clears any stale diagnostics from a prior
    // connection so the debug surface never shows a previous connection id,
    // channel, action, error code, or timestamp while reconnecting.
    _emit(ProtoSocketConnectionState.connecting, resetConnection: true);
    try {
      final transport = await _connector.connect(config);
      await _eventDiagnosticsSub?.cancel();
      _eventDiagnosticsSub = transport.events.listen(_recordDiagnostics);
      _transport = _DiagnosticsProtoSocketTransport(
        delegate: transport,
        onEnvelope: _recordDiagnostics,
      );
      _lastError = null;
      _emit(ProtoSocketConnectionState.connected);
    } catch (e) {
      _transport = null;
      await _eventDiagnosticsSub?.cancel();
      _eventDiagnosticsSub = null;
      _lastError = e;
      _emit(ProtoSocketConnectionState.failed, resetConnection: true);
      rethrow;
    }
  }

  Future<void> disconnect() async {
    if (_state == ProtoSocketConnectionState.disconnected) return;
    await _connector.disconnect();
    await _eventDiagnosticsSub?.cancel();
    _eventDiagnosticsSub = null;
    _transport = null;
    _emit(ProtoSocketConnectionState.disconnected, resetConnection: true);
  }

  Future<void> dispose() async {
    await _eventDiagnosticsSub?.cancel();
    _eventDiagnosticsSub = null;
    await _diagnosticsController.close();
    await _stateController.close();
  }

  void _emit(ProtoSocketConnectionState next, {bool resetConnection = false}) {
    _state = next;
    // `resetConnection` drops connection-specific fields (keeping the constant
    // protocol version) so disconnect/failed/reconnect transitions do not leave
    // stale diagnostics behind. Normal transitions preserve recorded values.
    _emitDiagnostics(
      resetConnection
          ? ProtoSocketDiagnostics(
              state: next,
              protocolVersion: _diagnostics.protocolVersion,
            )
          : _diagnostics.copyWithState(next),
    );
    if (!_stateController.isClosed) {
      _stateController.add(next);
    }
  }

  void _recordDiagnostics(ProtoSocketEnvelope envelope) {
    _emitDiagnostics(_diagnostics.fromEnvelope(envelope));
  }

  void _emitDiagnostics(ProtoSocketDiagnostics next) {
    _diagnostics = next;
    if (!_diagnosticsController.isClosed) {
      _diagnosticsController.add(next);
    }
  }
}

class _DiagnosticsProtoSocketTransport implements ProtoSocketTransport {
  final ProtoSocketTransport _delegate;
  final void Function(ProtoSocketEnvelope envelope) _onEnvelope;

  _DiagnosticsProtoSocketTransport({
    required ProtoSocketTransport delegate,
    required void Function(ProtoSocketEnvelope envelope) onEnvelope,
  }) : _delegate = delegate,
       _onEnvelope = onEnvelope;

  @override
  Stream<ProtoSocketEnvelope> get events => _delegate.events;

  @override
  Future<ProtoSocketEnvelope> sendRequest(
    ProtoSocketEnvelope envelope, {
    Duration timeout = const Duration(seconds: 30),
  }) async {
    final response = await _delegate.sendRequest(envelope, timeout: timeout);
    _onEnvelope(response);
    return response;
  }

  @override
  Future<void> close() => _delegate.close();
}

/// Test/fallback connector for disabled or fake scenarios.
class NoopProtoSocketConnector implements ProtoSocketConnector {
  @override
  Future<ProtoSocketTransport> connect(
    ProtoSocketEndpointConfig config,
  ) async => _NoopProtoSocketTransport();

  @override
  Future<void> disconnect() async {}
}

class _NoopProtoSocketTransport implements ProtoSocketTransport {
  final StreamController<ProtoSocketEnvelope> _events =
      StreamController<ProtoSocketEnvelope>.broadcast();

  @override
  Stream<ProtoSocketEnvelope> get events => _events.stream;

  @override
  Future<ProtoSocketEnvelope> sendRequest(
    ProtoSocketEnvelope envelope, {
    Duration timeout = const Duration(seconds: 30),
  }) {
    return Future.error(StateError('proto-socket transport is not connected'));
  }

  @override
  Future<void> close() => _events.close();
}

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

String? _nonEmptyString(String? value) {
  if (value == null || value.isEmpty) return null;
  return value;
}
