use std::collections::HashMap;

use serde::{Deserialize, Serialize};
use specta::Type;

use crate::{
  gateway::Gateway,
  os_profile::{ClientOs, OsProfile},
};

use super::vpn_state::ConnectInfo;

#[derive(Debug, Deserialize, Serialize)]
pub struct LaunchGuiRequest {
  user: String,
  envs: HashMap<String, String>,
}

impl LaunchGuiRequest {
  pub fn new(user: String, envs: HashMap<String, String>) -> Self {
    Self { user, envs }
  }

  pub fn user(&self) -> &str {
    &self.user
  }

  pub fn envs(&self) -> &HashMap<String, String> {
    &self.envs
  }
}

#[derive(Debug, Deserialize, Serialize, Type, Clone)]
pub struct ConnectArgs {
  cookie: String,
  vpnc_script: Option<String>,

  user_agent: Option<String>,
  os: Option<ClientOs>,
  os_version: Option<String>,
  client_version: Option<String>,
  host_id: Option<String>,

  certificate: Option<String>,
  sslkey: Option<String>,
  key_password: Option<String>,

  hip: bool,
  csd_uid: u32,
  csd_wrapper: Option<String>,

  reconnect_timeout: u32,
  mtu: u32,
  disable_ipv6: bool,
  no_dtls: bool,
  local_hostname: Option<String>,
  force_dpd: u32,
  no_xmlpost: bool,
  #[serde(rename = "allowExtendSession")]
  allow_extend_session: bool,
}

impl ConnectArgs {
  pub fn new(cookie: String) -> Self {
    Self {
      cookie,
      vpnc_script: None,
      user_agent: None,
      os: None,
      os_version: None,
      client_version: None,
      host_id: None,
      certificate: None,
      sslkey: None,
      key_password: None,
      hip: false,
      csd_uid: 0,
      csd_wrapper: None,
      reconnect_timeout: 300,
      mtu: 0,
      disable_ipv6: false,
      no_dtls: false,
      local_hostname: None,
      force_dpd: 0,
      no_xmlpost: false,
      allow_extend_session: false,
    }
  }

  pub fn cookie(&self) -> &str {
    &self.cookie
  }

  pub fn vpnc_script(&self) -> Option<String> {
    self.vpnc_script.clone()
  }

  pub fn user_agent(&self) -> Option<String> {
    self.user_agent.clone()
  }

  pub fn openconnect_os(&self) -> Option<String> {
    self.os.as_ref().map(|os| os.to_openconnect_os().to_string())
  }

  pub fn os(&self) -> Option<ClientOs> {
    self.os.clone()
  }

  pub fn os_version(&self) -> Option<String> {
    self.os_version.clone()
  }

  pub fn client_version(&self) -> Option<String> {
    self.client_version.clone()
  }

  pub fn host_id(&self) -> Option<String> {
    self.host_id.clone()
  }

  pub fn certificate(&self) -> Option<String> {
    self.certificate.clone()
  }

  pub fn sslkey(&self) -> Option<String> {
    self.sslkey.clone()
  }

  pub fn key_password(&self) -> Option<String> {
    self.key_password.clone()
  }

  pub fn hip(&self) -> bool {
    self.hip
  }

  pub fn csd_uid(&self) -> u32 {
    self.csd_uid
  }

  pub fn csd_wrapper(&self) -> Option<String> {
    self.csd_wrapper.clone()
  }

  pub fn reconnect_timeout(&self) -> u32 {
    self.reconnect_timeout
  }

  pub fn mtu(&self) -> u32 {
    self.mtu
  }

  pub fn disable_ipv6(&self) -> bool {
    self.disable_ipv6
  }

  pub fn no_dtls(&self) -> bool {
    self.no_dtls
  }

  pub fn local_hostname(&self) -> Option<String> {
    self.local_hostname.clone()
  }

  pub fn force_dpd(&self) -> u32 {
    self.force_dpd
  }

  pub fn no_xmlpost(&self) -> bool {
    self.no_xmlpost
  }

  pub fn allow_extend_session(&self) -> bool {
    self.allow_extend_session
  }
}

#[derive(Debug, Deserialize, Serialize, Type, Clone)]
pub struct ConnectRequest {
  info: ConnectInfo,
  args: ConnectArgs,
}

impl ConnectRequest {
  pub fn new(info: ConnectInfo, cookie: String) -> Self {
    Self {
      info,
      args: ConnectArgs::new(cookie),
    }
  }

  pub fn with_vpnc_script<T: Into<Option<String>>>(mut self, vpnc_script: T) -> Self {
    self.args.vpnc_script = vpnc_script.into();
    self
  }

  pub fn with_hip(mut self, hip: bool) -> Self {
    self.args.hip = hip;
    self
  }

  pub fn with_csd_uid(mut self, csd_uid: u32) -> Self {
    self.args.csd_uid = csd_uid;
    self
  }

  pub fn with_csd_wrapper<T: Into<Option<String>>>(mut self, csd_wrapper: T) -> Self {
    self.args.csd_wrapper = csd_wrapper.into();
    self
  }

  pub fn with_os_profile(mut self, profile: &OsProfile) -> Self {
    self.args.os = Some(profile.client_os());
    self.args.os_version = Some(profile.os_version().to_string());
    self.args.client_version = Some(profile.client_version().to_string());
    self.args.host_id = Some(profile.host_identity().host_id().to_string());
    self.args.user_agent = Some(profile.user_agent().to_string());
    self
  }

  pub fn with_certificate<T: Into<Option<String>>>(mut self, certificate: T) -> Self {
    self.args.certificate = certificate.into();
    self
  }

  pub fn with_sslkey<T: Into<Option<String>>>(mut self, sslkey: T) -> Self {
    self.args.sslkey = sslkey.into();
    self
  }

  pub fn with_key_password<T: Into<Option<String>>>(mut self, key_password: T) -> Self {
    self.args.key_password = key_password.into();
    self
  }

  pub fn with_reconnect_timeout(mut self, reconnect_timeout: u32) -> Self {
    self.args.reconnect_timeout = reconnect_timeout;
    self
  }

  pub fn with_mtu(mut self, mtu: u32) -> Self {
    self.args.mtu = mtu;
    self
  }

  pub fn with_disable_ipv6(mut self, disable_ipv6: bool) -> Self {
    self.args.disable_ipv6 = disable_ipv6;
    self
  }

  pub fn with_no_dtls(mut self, no_dtls: bool) -> Self {
    self.args.no_dtls = no_dtls;
    self
  }

  pub fn with_local_hostname<T: Into<Option<String>>>(mut self, local_hostname: T) -> Self {
    self.args.local_hostname = local_hostname.into();
    self
  }

  pub fn with_force_dpd(mut self, force_dpd: u32) -> Self {
    self.args.force_dpd = force_dpd;
    self
  }

  pub fn with_no_xmlpost(mut self, no_xmlpost: bool) -> Self {
    self.args.no_xmlpost = no_xmlpost;
    self
  }

  pub fn with_allow_extend_session(mut self, allow_extend_session: bool) -> Self {
    self.args.allow_extend_session = allow_extend_session;
    self
  }

  pub fn gateway(&self) -> &Gateway {
    self.info.gateway()
  }

  pub fn info(&self) -> &ConnectInfo {
    &self.info
  }

  pub fn args(&self) -> &ConnectArgs {
    &self.args
  }
}

#[derive(Debug, Deserialize, Serialize, Type)]
pub struct DisconnectRequest;

#[derive(Debug, Deserialize, Serialize)]
pub struct UpdateLogLevelRequest(pub String);

/// Requests that can be sent to the service
#[derive(Debug, Deserialize, Serialize)]
pub enum WsRequest {
  Connect(Box<ConnectRequest>),
  Disconnect(DisconnectRequest),
  UpdateLogLevel(UpdateLogLevelRequest),
}

#[derive(Debug, Deserialize, Serialize)]
pub struct UpdateGuiRequest {
  pub path: String,
  pub checksum: String,
}

#[cfg(test)]
mod tests {
  use serde_json::json;

  use super::*;
  use crate::os_profile::OsProfileBuilder;

  fn test_connect_info() -> ConnectInfo {
    let gateway = Gateway::new("Gateway".to_string(), "vpn.example.com".to_string());
    ConnectInfo::new("portal.example.com".to_string(), gateway.clone(), vec![gateway])
  }

  #[test]
  fn connect_request_serializes_allow_extend_session() {
    let gateway = Gateway::new("Gateway".to_string(), "vpn.example.com".to_string());
    let info = ConnectInfo::new("portal.example.com".to_string(), gateway.clone(), vec![gateway]);
    let req = ConnectRequest::new(info, "authcookie=AUTH".to_string()).with_allow_extend_session(true);
    let value = serde_json::to_value(req).unwrap();

    assert_eq!(value["args"]["allowExtendSession"], json!(true));
  }

  #[test]
  fn with_os_profile_sets_user_agent_from_profile() {
    let profile = OsProfileBuilder::new(ClientOs::Linux).client_version("6.0.0").build();

    let req = ConnectRequest::new(test_connect_info(), "cookie".to_string()).with_os_profile(&profile);

    assert_eq!(req.args().user_agent(), Some(profile.user_agent().to_string()));
  }

  #[test]
  fn with_os_profile_sets_host_id_from_profile_runtime_identity() {
    let profile = OsProfileBuilder::new(ClientOs::Linux).build();

    let req = ConnectRequest::new(test_connect_info(), "cookie".to_string()).with_os_profile(&profile);

    assert_eq!(
      req.args().host_id(),
      Some(profile.host_identity().host_id().to_string())
    );
  }
}
