// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Connect to a remote host using the happy eyeball algorithm.
/// Preference on IPv4 v.s. IPv6 can be configured via `protocol`:
///
/// - if `protocol` is `NoPreference` (the default behavior),
///   the first successful connection will be returned, regardless of the protocol.
/// - if `protocol` is `FavorV4`,
///   an IPv6 connection will only be returned if no IPv4 address is available
/// - if `protocol` is `OnlyV4`,
///   `connect_to_host` will fail if no IPv4 address is available
/// - if `protocol` is `FavorV6`,
///   an IPv6 connection will only be returned if no IPv4 address is available
/// - if `protocol` is `OnlyV6`,
///   `connect_to_host` will fail if no IPv4 address is available
pub async fn Tcp::connect_to_host(
  host : StringView,
  port~ : Int,
  protocol? : IpProtocolPreference = NoPreference,
) -> Tcp {
  let context = "@socket.Tcp::connect_to_host()"
  let ai = match @event_loop.getaddrinfo(host, context~) {
    Ok(ai) => ai
    Err(msg) => raise ResolveHostnameError(msg)
  }
  defer ai.free()
  let mut result = None
  let mut conn_err = None
  async fn connect_with_protocol(protocol : IpProtocolPreference) {
    @async.with_task_group() <| group => {
      for ai = ai; !ai.is_null(); ai = ai.next() {
        let addr = ai.to_addr(port)
        match (protocol, addr.is_ipv6()) {
          (OnlyV4 | FavorV4, true) => continue
          (OnlyV6 | FavorV6, false) => continue
          _ => ()
        }
        group.spawn_bg(allow_failure=true) <| () => {
          let conn = Tcp::connect(addr) catch {
            err => {
              if conn_err is None {
                conn_err = Some(err)
              }
              raise err
            }
          }
          // Multiple `connect` attempt may happen to complete simultaneously,
          // Keep only one result and properly close the others in this case.
          if result is None {
            result = Some(conn)
          } else {
            conn.close()
          }
          group.return_immediately(())
        }
        @async.sleep(250)
      }
    }
  }

  try {
    match protocol {
      NoPreference | OnlyV4 | OnlyV6 => connect_with_protocol(protocol)
      FavorV4 => {
        connect_with_protocol(OnlyV4)
        if result is None {
          connect_with_protocol(OnlyV6)
        }
      }
      FavorV6 => {
        connect_with_protocol(OnlyV6)
        if result is None {
          connect_with_protocol(OnlyV4)
        }
      }
    }
  } catch {
    err => {
      // It is possible that the whole `connect_to_host` call is cancelled,
      // but a connection attempt succeeded just before the cancellation.
      // We need to properly close the result in this case.
      if result is Some(conn) {
        conn.close()
      }
      raise err
    }
  }
  match result {
    Some(conn) => conn
    None =>
      match (conn_err, protocol) {
        (Some(err), _) => raise err
        (None, OnlyV4) =>
          raise ResolveHostnameError("No available IPv4 address")
        (None, OnlyV6) =>
          raise ResolveHostnameError("No available IPv6 address")
        (None, _) => panic()
      }
  }
}