// Copyright 2018-2026 the Deno authors. MIT license.
use std::borrow::Cow;
use std::net::Ipv4Addr;
use std::net::SocketAddr;
use std::net::SocketAddrV4;
use std::rc::Rc;

use deno_core::OpState;
use deno_core::ResourceId;
use deno_error::JsErrorBox;
use deno_net::raw::NetworkStream;
use deno_net::raw::NetworkStreamAddress;
use deno_net::raw::NetworkStreamListener;
use deno_net::raw::NetworkStreamType;
use deno_net::raw::take_network_stream_listener_resource;
use deno_net::raw::take_network_stream_resource;
use hyper::HeaderMap;
use hyper::Uri;
use hyper::header::HOST;

// TODO(mmastrac): I don't like that we have to clone this, but it's one-time setup
#[derive(Clone)]
pub struct HttpListenProperties {
  pub scheme: &'static str,
  pub fallback_host: String,
  pub local_port: Option<u32>,
  pub stream_type: NetworkStreamType,
}

#[derive(Clone)]
pub struct HttpConnectionProperties {
  pub peer_address: Rc<str>,
  pub peer_port: Option<u32>,
  pub local_port: Option<u32>,
  pub stream_type: NetworkStreamType,
  pub scheme: &'static str,
  pub fallback_host: Rc<str>,
}

pub struct HttpRequestProperties<'a> {
  pub authority: Option<Cow<'a, str>>,
}

/// Pluggable trait to determine listen, connection and request properties
/// for embedders that wish to provide alternative routes for incoming HTTP.
#[async_trait::async_trait(?Send)]
pub trait HttpPropertyExtractor {
  type Listener: 'static;
  type Connection;

  /// Given a listener [`ResourceId`], returns the [`HttpPropertyExtractor::Listener`].
  fn get_listener_for_rid(
    state: &mut OpState,
    listener_rid: ResourceId,
  ) -> Result<Self::Listener, JsErrorBox>;

  /// Given a connection [`ResourceId`], returns the [`HttpPropertyExtractor::Connection`].
  fn get_connection_for_rid(
    state: &mut OpState,
    connection_rid: ResourceId,
  ) -> Result<Self::Connection, JsErrorBox>;

  /// Determines the listener properties.
  fn listen_properties_from_listener(
    listener: &Self::Listener,
  ) -> Result<HttpListenProperties, std::io::Error>;

  /// Determines the listener properties given a [`HttpPropertyExtractor::Connection`].
  fn listen_properties_from_connection(
    connection: &Self::Connection,
  ) -> Result<HttpListenProperties, std::io::Error>;

  /// Accept a new [`HttpPropertyExtractor::Connection`] from the given listener [`HttpPropertyExtractor::Listener`].
  async fn accept_connection_from_listener(
    listener: &Self::Listener,
  ) -> Result<Self::Connection, JsErrorBox>;

  /// Determines the connection properties.
  fn connection_properties(
    listen_properties: &HttpListenProperties,
    connection: &Self::Connection,
  ) -> HttpConnectionProperties;

  /// Turn a given [`HttpPropertyExtractor::Connection`] into a [`NetworkStream`].
  fn to_network_stream_from_connection(
    connection: Self::Connection,
  ) -> NetworkStream;

  /// Determines the request properties.
  fn request_properties<'a>(
    connection_properties: &'a HttpConnectionProperties,
    uri: &'a Uri,
    headers: &'a HeaderMap,
  ) -> HttpRequestProperties<'a>;
}

pub struct DefaultHttpPropertyExtractor {}

#[async_trait::async_trait(?Send)]
impl HttpPropertyExtractor for DefaultHttpPropertyExtractor {
  type Listener = NetworkStreamListener;

  type Connection = NetworkStream;

  fn get_listener_for_rid(
    state: &mut OpState,
    listener_rid: ResourceId,
  ) -> Result<NetworkStreamListener, JsErrorBox> {
    take_network_stream_listener_resource(
      &mut state.resource_table,
      listener_rid,
    )
  }

  fn get_connection_for_rid(
    state: &mut OpState,
    stream_rid: ResourceId,
  ) -> Result<NetworkStream, JsErrorBox> {
    take_network_stream_resource(&mut state.resource_table, stream_rid)
      .map_err(JsErrorBox::from_err)
  }

  async fn accept_connection_from_listener(
    listener: &NetworkStreamListener,
  ) -> Result<NetworkStream, JsErrorBox> {
    listener
      .accept()
      .await
      .map_err(JsErrorBox::from_err)
      .map(|(stm, _)| stm)
  }

  fn listen_properties_from_listener(
    listener: &NetworkStreamListener,
  ) -> Result<HttpListenProperties, std::io::Error> {
    let stream_type = listener.stream();
    let local_address = listener.listen_address()?;
    listener_properties(stream_type, local_address)
  }

  fn listen_properties_from_connection(
    connection: &Self::Connection,
  ) -> Result<HttpListenProperties, std::io::Error> {
    let stream_type = connection.stream();
    let local_address = connection.local_address()?;
    listener_properties(stream_type, local_address)
  }

  fn to_network_stream_from_connection(
    connection: Self::Connection,
  ) -> NetworkStream {
    connection
  }

  fn connection_properties(
    listen_properties: &HttpListenProperties,
    connection: &NetworkStream,
  ) -> HttpConnectionProperties {
    // We always want some sort of peer address. If we can't get one, just make up one.
    let peer_address = connection.peer_address().unwrap_or_else(|_| {
      NetworkStreamAddress::Ip(SocketAddr::V4(SocketAddrV4::new(
        Ipv4Addr::new(0, 0, 0, 0),
        0,
      )))
    });
    let peer_port: Option<u32> = match peer_address {
      NetworkStreamAddress::Ip(ip) => Some(ip.port() as _),
      #[cfg(unix)]
      NetworkStreamAddress::Unix(_) => None,
      #[cfg(any(
        target_os = "android",
        target_os = "linux",
        target_os = "macos"
      ))]
      NetworkStreamAddress::Vsock(vsock) => Some(vsock.port()),
      NetworkStreamAddress::Tunnel(ref addr) => Some(addr.port() as _),
      #[cfg(windows)]
      NetworkStreamAddress::WindowsPipe(_) => None,
    };
    let peer_address = match peer_address {
      NetworkStreamAddress::Ip(addr) => Rc::from(addr.ip().to_string()),
      #[cfg(unix)]
      NetworkStreamAddress::Unix(_) => Rc::from("unix"),
      #[cfg(any(
        target_os = "android",
        target_os = "linux",
        target_os = "macos"
      ))]
      NetworkStreamAddress::Vsock(addr) => {
        Rc::from(format!("vsock:{}", addr.cid()))
      }
      NetworkStreamAddress::Tunnel(ref addr) => Rc::from(addr.hostname()),
      #[cfg(windows)]
      NetworkStreamAddress::WindowsPipe(_) => Rc::from("pipe"),
    };
    let local_port = listen_properties.local_port;
    let stream_type = listen_properties.stream_type;
    let scheme = listen_properties.scheme;
    let fallback_host = Rc::from(listen_properties.fallback_host.as_str());

    HttpConnectionProperties {
      peer_address,
      peer_port,
      local_port,
      stream_type,
      scheme,
      fallback_host,
    }
  }

  fn request_properties<'a>(
    connection_properties: &'a HttpConnectionProperties,
    uri: &'a Uri,
    headers: &'a HeaderMap,
  ) -> HttpRequestProperties<'a> {
    let authority = req_host(
      uri,
      headers,
      connection_properties.stream_type,
      connection_properties.local_port.unwrap_or_default(),
    );

    HttpRequestProperties { authority }
  }
}

fn listener_properties(
  stream_type: NetworkStreamType,
  local_address: NetworkStreamAddress,
) -> Result<HttpListenProperties, std::io::Error> {
  let scheme = req_scheme_from_stream_type(stream_type);
  let fallback_host = req_host_from_addr(stream_type, &local_address);
  let local_port: Option<u32> = match local_address {
    NetworkStreamAddress::Ip(ip) => Some(ip.port() as _),
    #[cfg(unix)]
    NetworkStreamAddress::Unix(_) => None,
    #[cfg(any(
      target_os = "android",
      target_os = "linux",
      target_os = "macos"
    ))]
    NetworkStreamAddress::Vsock(vsock) => Some(vsock.port()),
    NetworkStreamAddress::Tunnel(addr) => Some(addr.port() as _),
    #[cfg(windows)]
    NetworkStreamAddress::WindowsPipe(_) => None,
  };
  Ok(HttpListenProperties {
    scheme,
    fallback_host,
    local_port,
    stream_type,
  })
}

/// Compute the fallback address from the [`NetworkStreamListenAddress`]. If the request has no authority/host in
/// its URI, and there is no [`HeaderName::HOST`] header, we fall back to this.
fn req_host_from_addr(
  stream_type: NetworkStreamType,
  addr: &NetworkStreamAddress,
) -> String {
  match addr {
    NetworkStreamAddress::Ip(addr) => {
      if (stream_type == NetworkStreamType::Tls && addr.port() == 443)
        || (stream_type == NetworkStreamType::Tcp && addr.port() == 80)
      {
        if addr.ip().is_loopback() || addr.ip().is_unspecified() {
          return "localhost".to_owned();
        }
        addr.ip().to_string()
      } else {
        if addr.ip().is_loopback() || addr.ip().is_unspecified() {
          return format!("localhost:{}", addr.port());
        }
        addr.to_string()
      }
    }
    // There is no standard way for unix domain socket URLs
    // nginx and nodejs request use http://unix:[socket_path]:/ but it is not a valid URL
    // httpie uses http+unix://[percent_encoding_of_path]/ which we follow
    #[cfg(unix)]
    NetworkStreamAddress::Unix(unix) => percent_encoding::percent_encode(
      unix
        .as_pathname()
        .and_then(|x| x.to_str())
        .unwrap_or_default()
        .as_bytes(),
      percent_encoding::NON_ALPHANUMERIC,
    )
    .to_string(),
    #[cfg(any(
      target_os = "android",
      target_os = "linux",
      target_os = "macos"
    ))]
    NetworkStreamAddress::Vsock(vsock) => {
      format!("{}:{}", vsock.cid(), vsock.port())
    }
    NetworkStreamAddress::Tunnel(addr) => {
      if addr.port() == 443 {
        addr.hostname()
      } else {
        format!("{}:{}", addr.hostname(), addr.port())
      }
    }
    #[cfg(windows)]
    NetworkStreamAddress::WindowsPipe(_) => "localhost".to_owned(),
  }
}

fn req_scheme_from_stream_type(stream_type: NetworkStreamType) -> &'static str {
  match stream_type {
    NetworkStreamType::Tcp => "http://",
    NetworkStreamType::Tls | NetworkStreamType::Tunnel => "https://",
    #[cfg(unix)]
    NetworkStreamType::Unix => "http+unix://",
    #[cfg(any(
      target_os = "android",
      target_os = "linux",
      target_os = "macos"
    ))]
    NetworkStreamType::Vsock => "http+vsock://",
    #[cfg(windows)]
    NetworkStreamType::WindowsPipe => "http://",
  }
}

fn req_host<'a>(
  uri: &'a Uri,
  headers: &'a HeaderMap,
  addr_type: NetworkStreamType,
  port: u32,
) -> Option<Cow<'a, str>> {
  // It is rare that an authority will be passed, but if it does, it takes priority
  if let Some(auth) = uri.authority() {
    match addr_type {
      NetworkStreamType::Tcp => {
        if port == 80 {
          return Some(Cow::Borrowed(auth.host()));
        }
      }
      NetworkStreamType::Tls | NetworkStreamType::Tunnel => {
        if port == 443 {
          return Some(Cow::Borrowed(auth.host()));
        }
      }
      #[cfg(unix)]
      NetworkStreamType::Unix => {}
      #[cfg(any(
        target_os = "android",
        target_os = "linux",
        target_os = "macos"
      ))]
      NetworkStreamType::Vsock => {}
      #[cfg(windows)]
      NetworkStreamType::WindowsPipe => {}
    }
    return Some(Cow::Borrowed(auth.as_str()));
  }

  // TODO(mmastrac): Most requests will use this path and we probably will want to optimize it in the future
  if let Some(host) = headers.get(HOST) {
    // An empty `Host` header value is treated as if no Host header was sent,
    // so the listener's fallback authority is used and `request.url` stays a
    // valid URL. Hyper's HTTP/1 parser already trims OWS from header values,
    // so whitespace-only `Host:` values reach us as empty bytes.
    // See https://github.com/denoland/deno/issues/29872.
    if host.is_empty() {
      return None;
    }
    return Some(match host.to_str() {
      Ok(host) => Cow::Borrowed(host),
      Err(_) => Cow::Owned(
        host
          .as_bytes()
          .iter()
          .cloned()
          .map(char::from)
          .collect::<String>(),
      ),
    });
  }

  None
}
