// Copyright (c) 2014, 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:io';

import 'package:async/async.dart';

import 'src/multi_headers.dart';
import 'src/utils.dart';

/// The error code for an error caused by a port already being in use.
final _addressInUseErrno = _computeAddressInUseErrno();
int _computeAddressInUseErrno() {
  if (Platform.isWindows) return 10048;
  if (Platform.isMacOS) return 48;
  assert(Platform.isLinux);
  return 98;
}

/// An implementation of `dart:io`'s [HttpServer] that wraps multiple servers
/// and forwards methods to all of them.
///
/// This is useful for serving the same application on multiple network
/// interfaces while still having a unified way of controlling the servers. In
/// particular, it supports serving on both the IPv4 and IPv6 loopback addresses
/// using [HttpMultiServer.loopback].
class HttpMultiServer extends StreamView<HttpRequest> implements HttpServer {
  /// The wrapped servers.
  final Set<HttpServer> _servers;

  /// Returns the default value of the `Server` header for all responses
  /// generated by each server.
  ///
  /// If the wrapped servers have different default values, it's not defined
  /// which value is returned.
  @override
  String? get serverHeader => _servers.first.serverHeader;

  @override
  set serverHeader(String? value) {
    for (var server in _servers) {
      server.serverHeader = value;
    }
  }

  /// Returns the default set of headers added to all response objects.
  ///
  /// If the wrapped servers have different default headers, it's not defined
  /// which header is returned for accessor methods.
  @override
  final HttpHeaders defaultResponseHeaders;

  @override
  Duration? get idleTimeout => _servers.first.idleTimeout;
  @override
  set idleTimeout(Duration? value) {
    for (var server in _servers) {
      server.idleTimeout = value;
    }
  }

  @override
  bool get autoCompress => _servers.first.autoCompress;
  @override
  set autoCompress(bool value) {
    for (var server in _servers) {
      server.autoCompress = value;
    }
  }

  /// Returns the port that one of the wrapped servers is listening on.
  ///
  /// If the wrapped servers are listening on different ports, it's not defined
  /// which port is returned.
  @override
  int get port => _servers.first.port;

  /// Returns the address that one of the wrapped servers is listening on.
  ///
  /// If the wrapped servers are listening on different addresses, it's not
  /// defined which address is returned.
  @override
  InternetAddress get address => _servers.first.address;

  @override
  set sessionTimeout(int value) {
    for (var server in _servers) {
      server.sessionTimeout = value;
    }
  }

  /// Creates an [HttpMultiServer] wrapping [servers].
  ///
  /// All [servers] should have the same configuration and none should be
  /// listened to when this is called.
  HttpMultiServer(Iterable<HttpServer> servers)
      : _servers = servers.toSet(),
        defaultResponseHeaders = MultiHeaders(
            servers.map((server) => server.defaultResponseHeaders)),
        super(StreamGroup.merge(servers));

  /// Creates an [HttpServer] listening on all available loopback addresses for
  /// this computer.
  ///
  /// See [HttpServer.bind].
  static Future<HttpServer> loopback(int port,
          {int backlog = 0, bool v6Only = false, bool shared = false}) =>
      _loopback(
          port,
          (address, port) => HttpServer.bind(address, port,
              backlog: backlog, v6Only: v6Only, shared: shared));

  /// Like [loopback], but supports HTTPS requests.
  ///
  /// See [HttpServer.bindSecure].
  static Future<HttpServer> loopbackSecure(int port, SecurityContext context,
          {int backlog = 0,
          bool v6Only = false,
          bool requestClientCertificate = false,
          bool shared = false}) =>
      _loopback(
          port,
          (address, port) => HttpServer.bindSecure(address, port, context,
              backlog: backlog,
              v6Only: v6Only,
              shared: shared,
              requestClientCertificate: requestClientCertificate));

  /// Bind an [HttpServer] with handling for special addresses 'localhost' and
  /// 'any'.
  ///
  /// For address 'localhost' behaves like [loopback].
  ///
  /// For 'any' listens on [InternetAddress.anyIPv6] if the system supports IPv6
  /// otherwise [InternetAddress.anyIPv4]. Note [InternetAddress.anyIPv6]
  /// listens on all hostnames for both IPv4 and IPv6.
  ///
  /// For any other address forwards directly to `HttpServer.bind` where
  /// the IPvX support may vary.
  ///
  /// See [HttpServer.bind].
  static Future<HttpServer> bind(dynamic address, int port,
      {int backlog = 0, bool v6Only = false, bool shared = false}) async {
    if (address == 'localhost') {
      return HttpMultiServer.loopback(port,
          backlog: backlog, v6Only: v6Only, shared: shared);
    }
    if (address == 'any') {
      return HttpServer.bind(
          await supportsIPv6
              ? InternetAddress.anyIPv6
              : InternetAddress.anyIPv4,
          port,
          backlog: backlog,
          v6Only: v6Only,
          shared: shared);
    }
    return HttpServer.bind(address, port,
        backlog: backlog, v6Only: v6Only, shared: shared);
  }

  /// Bind a secure [HttpServer] with handling for special addresses 'localhost'
  /// and 'any'.
  ///
  /// For address 'localhost' behaves like [loopback].
  ///
  /// For 'any' listens on [InternetAddress.anyIPv6] if the system supports IPv6
  /// otherwise [InternetAddress.anyIPv4]. Note [InternetAddress.anyIPv6]
  /// listens on all hostnames for both IPv4 and IPv6.
  ///
  /// See [HttpServer.bindSecure].
  static Future<HttpServer> bindSecure(
      dynamic address, int port, SecurityContext context,
      {int backlog = 0, bool v6Only = false, bool shared = false}) async {
    if (address == 'localhost') {
      return await HttpMultiServer.loopbackSecure(port, context,
          backlog: backlog, v6Only: v6Only, shared: shared);
    }
    if (address == 'any') {
      return await HttpServer.bindSecure(
          await supportsIPv6
              ? InternetAddress.anyIPv6
              : InternetAddress.anyIPv4,
          port,
          context,
          backlog: backlog,
          v6Only: v6Only,
          shared: shared);
    }
    return await HttpServer.bindSecure(address, port, context,
        backlog: backlog, v6Only: v6Only, shared: shared);
  }

  /// A helper method for initializing loopback servers.
  ///
  /// [bind] should forward to either [HttpServer.bind] or
  /// [HttpServer.bindSecure].
  static Future<HttpServer> _loopback(
      int port, Future<HttpServer> Function(InternetAddress, int port) bind,
      [int remainingRetries = 5]) async {
    if (!await supportsIPv4) {
      return await bind(InternetAddress.loopbackIPv6, port);
    }

    final v4Server = await bind(InternetAddress.loopbackIPv4, port);
    if (!await supportsIPv6) return v4Server;

    try {
      // Reuse the IPv4 server's port so that if [port] is 0, both servers use
      // the same ephemeral port.
      final v6Server = await bind(InternetAddress.loopbackIPv6, v4Server.port);
      return HttpMultiServer([v4Server, v6Server]);
    } on SocketException catch (error) {
      // If there is already a server listening we'll lose the reference on a
      // rethrow.
      await v4Server.close();

      if (error.osError?.errorCode != _addressInUseErrno) rethrow;
      if (port != 0) rethrow;
      if (remainingRetries == 0) rethrow;

      // A port being available on IPv4 doesn't necessarily mean that the same
      // port is available on IPv6. If it's not (which is rare in practice),
      // we try again until we find one that's available on both.
      return await _loopback(port, bind, remainingRetries - 1);
    }
  }

  @override
  Future close({bool force = false}) =>
      Future.wait(_servers.map((server) => server.close(force: force)));

  /// Returns an HttpConnectionsInfo object summarizing the total number of
  /// current connections handled by all the servers.
  @override
  HttpConnectionsInfo connectionsInfo() {
    final info = HttpConnectionsInfo();
    for (var server in _servers) {
      final subInfo = server.connectionsInfo();
      info
        ..total += subInfo.total
        ..active += subInfo.active
        ..idle += subInfo.idle
        ..closing += subInfo.closing;
    }
    return info;
  }
}
