// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';

import 'package:dds/dds_launcher.dart';
import 'package:dwds/src/config/tool_configuration.dart';
import 'package:dwds/src/events.dart';
import 'package:dwds/src/services/proxy_service.dart';
import 'package:dwds/src/utilities/server.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf_web_socket/shelf_web_socket.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:vm_service/vm_service.dart';
import 'package:vm_service_interface/vm_service_interface.dart';

bool _acceptNewConnections = true;

final _clientConnections = <int, StreamChannel>{};
int _clientId = 0;

Logger _logger = Logger('DebugService');

/// Common interface for debug services (Chrome or WebSocket based).
abstract class DebugService<T extends ProxyService> {
  DebugService({
    required this.serverHostname,
    required this.ddsConfig,
    required this.urlEncoder,
    required this.useSse,
  });

  /// The URI pointing to the VM service implementation hosted by the [DebugService].
  String get uri => _uri.toString();

  Uri get _uri => _cachedUri ??= () {
    final dds = _dds;
    if (ddsConfig.enable && dds != null) {
      return useSse ? dds.sseUri : dds.wsUri;
    }
    return useSse
        ? Uri(
            scheme: 'sse',
            host: _server.address.host,
            port: _server.port,
            path: '$authToken/\$debugHandler',
          )
        : Uri(
            scheme: 'ws',
            host: _server.address.host,
            port: _server.port,
            path: authToken,
          );
  }();

  Uri? _cachedUri;
  String? _ddsUri;

  late final T proxyService;

  final UrlEncoder? urlEncoder;

  late final String authToken = _makeAuthToken();
  final bool useSse;

  Future<String> get encodedUri async {
    return _encodedUri ??= await urlEncoder?.call(uri) ?? uri;
  }

  String? _encodedUri;

  DartDevelopmentServiceConfiguration ddsConfig;
  DartDevelopmentServiceLauncher? _dds;

  final String serverHostname;
  late final HttpServer _server;

  String get hostname => _uri.host;
  int get port => _uri.port;

  final serviceExtensionRegistry = ServiceExtensionRegistry();

  /// Null until [close] is called.
  ///
  /// All subsequent calls to [close] will return this future.
  Future<void>? _closed;

  @protected
  @mustCallSuper
  @mustBeOverridden
  // False positive
  // ignore: avoid-redundant-async
  Future<void> initialize({required T proxyService}) async {
    this.proxyService = proxyService;
  }

  @protected
  Future<void> serve({required shelf.Handler handler}) async {
    _server = await startHttpServer(serverHostname, port: 44456);
    serveHttpRequests(_server, handler, (e, s) {
      _logger.warning('Error serving requests', e);
      emitEvent(DwdsEvent.httpRequestException('$runtimeType', '$e:$s'));
    });
  }

  /// Closes the debug service and associated resources.
  Future<void> close() => _closed ??= Future.wait([
    _server.close(),
    if (_dds != null) _dds!.shutdown(),
  ]);

  Future<DartDevelopmentServiceLauncher> startDartDevelopmentService() async {
    // Note: DDS can handle both web socket and SSE connections with no
    // additional configuration.
    final hostname = _server.address.host;
    _dds = await DartDevelopmentServiceLauncher.start(
      remoteVmServiceUri: Uri(
        scheme: 'http',
        host: hostname,
        port: _server.port,
        path: authToken,
      ),
      serviceUri: Uri(
        scheme: 'http',
        host: hostname,
        port: ddsConfig.port ?? 0,
      ),
      devToolsServerAddress: ddsConfig.devToolsServerAddress,
      serveDevTools: ddsConfig.serveDevTools,
    );
    return _dds!;
  }

  void yieldControlToDDS(String uri) {
    // We track the URI of the connected DDS instance seperately instead of
    // relying on _dds being non-null as there's no guarantee that DWDS is the
    // tool starting DDS.
    if (_ddsUri != null) {
      // This exception is identical to the one thrown from
      // sdk/lib/vmservice/vmservice.dart
      throw RPCError(
        '_yieldControlToDDS',
        RPCErrorKind.kFeatureDisabled.code,
        'A DDS instance is already connected at $_ddsUri.',
        {'ddsUri': _ddsUri.toString()},
      );
    }
    _acceptNewConnections = false;
    _ddsUri = uri;
  }

  @protected
  shelf.Handler initializeWebSocketHandler({
    required ProxyService proxyService,
    void Function(Map<String, Object>)? onRequest,
    void Function(Map<String, Object?>)? onResponse,
  }) {
    return _wrapHandler(
      webSocketHandler((webSocket, subprotocol) {
        handleConnection(
          webSocket,
          proxyService,
          serviceExtensionRegistry,
          onRequest: onRequest,
          onResponse: onResponse,
        );
      }),
      authToken: authToken,
    );
  }

  shelf.Handler _wrapHandler(shelf.Handler innerHandler, {String? authToken}) {
    return (shelf.Request request) {
      if (!_acceptNewConnections) {
        return shelf.Response.forbidden(
          'Cannot connect directly to the VM service as a Dart Development '
          'Service (DDS) instance has taken control and can be found at $_ddsUri.'
          '$_ddsUri.',
        );
      }
      if (authToken != null && request.url.pathSegments.first != authToken) {
        return shelf.Response.forbidden('Incorrect auth token');
      }
      return innerHandler(request);
    };
  }

  @protected
  @mustCallSuper
  void handleConnection(
    StreamChannel channel,
    ProxyService proxyService,
    ServiceExtensionRegistry serviceExtensionRegistry, {
    void Function(Map<String, Object>)? onRequest,
    void Function(Map<String, Object?>)? onResponse,
  }) {
    final clientId = _clientId++;
    final responseController = StreamController<Map<String, Object?>>();
    responseController.stream
        .asyncMap<String>((response) async {
          // This error indicates a successful invocation to _yieldControlToDDS.
          // We don't have a good way to access the list of connected clients
          // while also being able to determine which client invoked the RPC
          // without some form of client ID.
          //
          // We can probably do better than this, but it will likely involve some
          // refactoring.
          if (response case {
            'error': {
              'code': DisconnectNonDartDevelopmentServiceClients.kErrorCode,
            },
          }) {
            final nonDdsClients = _clientConnections.entries
                .where((MapEntry<int, StreamChannel> e) => e.key != clientId)
                .map((e) => e.value);
            await Future.wait([
              for (final client in nonDdsClients) client.sink.close(),
            ]);
            // Remove the artificial error and return Success.
            response.remove('error');
            response['result'] = Success().toJson();
          }
          if (onResponse != null) onResponse(response);
          return jsonEncode(response);
        })
        .listen(channel.sink.add, onError: channel.sink.addError);
    final inputStream = channel.stream.map((value) {
      if (value is List<int>) {
        value = utf8.decode(value);
      } else if (value is! String) {
        throw StateError(
          'Got value with unexpected type ${value.runtimeType} from web '
          'socket, expected a List<int> or String.',
        );
      }
      final request = Map<String, Object>.from(jsonDecode(value));
      if (onRequest != null) onRequest(request);
      return request;
    });
    VmServerConnection(
      inputStream,
      responseController.sink,
      serviceExtensionRegistry,
      proxyService,
    ).done.whenComplete(() {
      _clientConnections.remove(clientId);
      if (!_acceptNewConnections && _clientConnections.isEmpty) {
        // DDS has disconnected so we can allow for clients to connect directly
        // to DWDS.
        _ddsUri = null;
        _acceptNewConnections = true;
      }
    });
    _clientConnections[clientId] = channel;
  }

  // Creates a random auth token for more secure connections.
  String _makeAuthToken() {
    final tokenBytes = 8;
    final bytes = Uint8List(tokenBytes);
    final random = Random.secure();
    for (var i = 0; i < tokenBytes; i++) {
      bytes[i] = random.nextInt(256);
    }
    return base64Url.encode(bytes);
  }
}
