use std::{cell::RefCell, fs, sync::Arc};

use anyhow::bail;
use clap::Args;
use common::vpn_utils::find_csd_wrapper;
use gpapi::{
  auth::SamlAuthResult,
  clap::{args::Os, ToVerboseArg},
  credential::{Credential, PasswordCredential},
  error::PortalError,
  gateway::{gateway_login, GatewayLogin},
  gp_params::{ClientOs, GpParams},
  portal::{prelogin, retrieve_config, Prelogin},
  process::{
    auth_launcher::SamlAuthLauncher,
    users::{get_non_root_user, get_user_by_name},
  },
  utils::{request::RequestIdentityError, shutdown_signal},
  GP_USER_AGENT,
};
use inquire::{Password, PasswordDisplayMode, Select, Text};
use log::{info, warn};
use openconnect::Vpn;

use crate::{cli::SharedArgs, GP_CLIENT_LOCK_FILE};

#[derive(Args)]
pub(crate) struct ConnectArgs {
  #[arg(help = "The portal server to connect to")]
  server: String,

  #[arg(short, long, help = "The gateway to connect to, it will prompt if not specified")]
  gateway: Option<String>,

  #[arg(short, long, help = "The username to use, it will prompt if not specified")]
  user: Option<String>,

  #[arg(long, help = "Read the password from standard input")]
  passwd_on_stdin: bool,

  #[arg(long, help = "Read the cookie from standard input")]
  cookie_on_stdin: bool,

  #[arg(long, short, help = "The VPNC script to use")]
  script: Option<String>,

  #[arg(long, help = "Connect the server as a gateway, instead of a portal")]
  as_gateway: bool,

  #[arg(
    long,
    help = "Use the default CSD wrapper to generate the HIP report and send it to the server"
  )]
  hip: bool,

  #[arg(
    short,
    long,
    help = "Use SSL client certificate file in pkcs#8 (.pem) or pkcs#12 (.p12, .pfx) format"
  )]
  certificate: Option<String>,

  #[arg(short = 'k', long, help = "Use SSL private key file in pkcs#8 (.pem) format")]
  sslkey: Option<String>,

  #[arg(short = 'p', long, help = "The key passphrase of the private key")]
  key_password: Option<String>,

  #[arg(long, help = "Same as the '--csd-user' option in the openconnect command")]
  csd_user: Option<String>,

  #[arg(long, help = "Same as the '--csd-wrapper' option in the openconnect command")]
  csd_wrapper: Option<String>,

  #[arg(long, default_value = "300", help = "Reconnection retry timeout in seconds")]
  reconnect_timeout: u32,

  #[arg(short, long, help = "Request MTU from server (legacy servers only)")]
  mtu: Option<u32>,

  #[arg(long, help = "Do not ask for IPv6 connectivity")]
  disable_ipv6: bool,

  #[arg(long, default_value = GP_USER_AGENT, help = "The user agent to use")]
  user_agent: String,

  #[arg(long, value_enum, default_value_t = ConnectArgs::default_os())]
  os: Os,

  #[arg(long, help = "If not specified, it will be computed based on the --os option")]
  os_version: Option<String>,

  #[arg(long, help = "Disable DTLS and ESP")]
  no_dtls: bool,

  #[cfg(feature = "webview-auth")]
  #[arg(long, help = "The HiDPI mode, useful for high-resolution screens")]
  hidpi: bool,

  #[cfg(feature = "webview-auth")]
  #[arg(long, help = "Do not reuse the remembered authentication cookie")]
  clean: bool,

  #[cfg(feature = "webview-auth")]
  #[arg(long, help = "Use the default browser to authenticate")]
  default_browser: bool,

  #[arg(
    long,
    help = "Use the specified browser to authenticate, e.g., `default`, `firefox`, `chrome`, `chromium`, or the path to the browser executable"
  )]
  browser: Option<String>,
}

impl ConnectArgs {
  fn default_os() -> Os {
    if cfg!(target_os = "macos") {
      Os::Mac
    } else {
      Os::Linux
    }
  }

  fn os_version(&self) -> String {
    if let Some(os_version) = self.os_version.as_deref() {
      return os_version.to_string();
    }

    match self.os {
      Os::Linux => format!("Linux {}", whoami::distro()),
      Os::Windows => String::from("Microsoft Windows 11 Pro , 64-bit"),
      Os::Mac => String::from("Apple Mac OS X 13.4.0"),
    }
  }
}

pub(crate) struct ConnectHandler<'a> {
  args: &'a ConnectArgs,
  shared_args: &'a SharedArgs<'a>,
  latest_key_password: RefCell<Option<String>>,
}

impl<'a> ConnectHandler<'a> {
  pub(crate) fn new(args: &'a ConnectArgs, shared_args: &'a SharedArgs) -> Self {
    Self {
      args,
      shared_args,
      latest_key_password: Default::default(),
    }
  }

  fn build_gp_params(&self) -> GpParams {
    GpParams::builder()
      .user_agent(&self.args.user_agent)
      .client_os(ClientOs::from(&self.args.os))
      .os_version(self.args.os_version())
      .ignore_tls_errors(self.shared_args.ignore_tls_errors)
      .certificate(self.args.certificate.clone())
      .sslkey(self.args.sslkey.clone())
      .key_password(self.latest_key_password.borrow().clone())
      .build()
  }

  pub(crate) async fn handle(&self) -> anyhow::Result<()> {
    #[cfg(feature = "webview-auth")]
    if self.args.default_browser && self.args.browser.is_some() {
      bail!("Cannot use `--default-browser` and `--browser` options at the same time");
    }

    self.latest_key_password.replace(self.args.key_password.clone());

    loop {
      let Err(err) = self.handle_impl().await else {
        return Ok(());
      };

      let Some(root_cause) = err.root_cause().downcast_ref::<RequestIdentityError>() else {
        return Err(err);
      };

      match root_cause {
        RequestIdentityError::NoKey => {
          eprintln!("ERROR: No private key found in the certificate file");
          eprintln!("ERROR: Please provide the private key file using the `-k` option");
          return Ok(());
        }
        RequestIdentityError::NoPassphrase(cert_type) | RequestIdentityError::DecryptError(cert_type) => {
          // Decrypt the private key error, ask for the key password
          let message = format!("Enter the {} passphrase:", cert_type);
          let password = Password::new(&message)
            .without_confirmation()
            .with_display_mode(PasswordDisplayMode::Masked)
            .prompt()?;

          self.latest_key_password.replace(Some(password));
        }
      }
    }
  }

  pub(crate) async fn handle_impl(&self) -> anyhow::Result<()> {
    let server = self.args.server.as_str();
    let as_gateway = self.args.as_gateway;

    if as_gateway {
      info!("Treating the server as a gateway");
      return self.connect_gateway_with_prelogin(server).await;
    }

    let Err(err) = self.connect_portal_with_prelogin(server).await else {
      return Ok(());
    };

    warn!("Failed to connect portal with prelogin: {}", err);
    if err.root_cause().downcast_ref::<PortalError>().is_some() {
      info!("Trying the gateway authentication workflow...");
      self.connect_gateway_with_prelogin(server).await?;

      eprintln!("\nNOTE: the server may be a gateway, not a portal.");
      eprintln!("NOTE: try to use the `--as-gateway` option if you were authenticated twice.");

      Ok(())
    } else {
      Err(err)
    }
  }

  async fn connect_portal_with_prelogin(&self, portal: &str) -> anyhow::Result<()> {
    let gp_params = self.build_gp_params();

    let prelogin = prelogin(portal, &gp_params).await?;

    let cred = self.obtain_credential(&prelogin, portal).await?;
    let mut portal_config = retrieve_config(portal, &cred, &gp_params).await?;

    let selected_gateway = match &self.args.gateway {
      Some(gateway) => portal_config
        .find_gateway(gateway)
        .ok_or_else(|| anyhow::anyhow!("Cannot find gateway specified: {}", gateway))?,
      None => {
        portal_config.sort_gateways(prelogin.region());
        let gateways = portal_config.gateways();

        if gateways.len() > 1 {
          let gateway = Select::new("Which gateway do you want to connect to?", gateways)
            .with_vim_mode(true)
            .prompt()?;
          info!("Connecting to the selected gateway: {}", gateway);
          gateway
        } else {
          info!("Connecting to the only available gateway: {}", gateways[0]);
          gateways[0]
        }
      }
    };

    let gateway = selected_gateway.server();
    let cred = portal_config.auth_cookie().into();

    let cookie = match self.login_gateway(gateway, &cred, &gp_params).await {
      Ok(cookie) => cookie,
      Err(err) => {
        info!("Gateway login failed: {}", err);
        return self.connect_gateway_with_prelogin(gateway).await;
      }
    };

    self.connect_gateway(gateway, &cookie).await
  }

  async fn connect_gateway_with_prelogin(&self, gateway: &str) -> anyhow::Result<()> {
    info!("Performing the gateway authentication...");

    let mut gp_params = self.build_gp_params();
    gp_params.set_is_gateway(true);

    let prelogin = prelogin(gateway, &gp_params).await?;
    let cred = self.obtain_credential(&prelogin, gateway).await?;

    let cookie = self.login_gateway(gateway, &cred, &gp_params).await?;

    self.connect_gateway(gateway, &cookie).await
  }

  async fn login_gateway(&self, gateway: &str, cred: &Credential, gp_params: &GpParams) -> anyhow::Result<String> {
    let mut gp_params = gp_params.clone();

    loop {
      match gateway_login(gateway, cred, &gp_params).await? {
        GatewayLogin::Cookie(cookie) => return Ok(cookie),
        GatewayLogin::Mfa(message, input_str) => {
          let otp = Text::new(&message).prompt()?;
          gp_params.set_input_str(&input_str);
          gp_params.set_otp(&otp);

          info!("Retrying gateway login with MFA...");
        }
      }
    }
  }

  async fn connect_gateway(&self, gateway: &str, cookie: &str) -> anyhow::Result<()> {
    let mtu = self.args.mtu.unwrap_or(0);
    let csd_uid = get_csd_uid(&self.args.csd_user)?;
    let csd_wrapper = if self.args.csd_wrapper.is_some() {
      self.args.csd_wrapper.clone()
    } else if self.args.hip {
      find_csd_wrapper()
    } else {
      None
    };

    let os = ClientOs::from(&self.args.os).to_openconnect_os().to_string();
    let vpn = Vpn::builder(gateway, cookie)
      .script(self.args.script.clone())
      .user_agent(self.args.user_agent.clone())
      .os(Some(os))
      .certificate(self.args.certificate.clone())
      .sslkey(self.args.sslkey.clone())
      .key_password(self.latest_key_password.borrow().clone())
      .csd_uid(csd_uid)
      .csd_wrapper(csd_wrapper)
      .reconnect_timeout(self.args.reconnect_timeout)
      .mtu(mtu)
      .disable_ipv6(self.args.disable_ipv6)
      .no_dtls(self.args.no_dtls)
      .build()?;

    let vpn = Arc::new(vpn);
    let vpn_clone = vpn.clone();

    // Listen for the interrupt signal in the background
    tokio::spawn(async move {
      shutdown_signal().await;
      info!("Received the interrupt signal, disconnecting...");
      vpn_clone.disconnect();
    });

    vpn.connect(write_pid_file);

    if fs::metadata(GP_CLIENT_LOCK_FILE).is_ok() {
      info!("Removing PID file");
      fs::remove_file(GP_CLIENT_LOCK_FILE)?;
    }

    Ok(())
  }

  async fn obtain_credential(&self, prelogin: &Prelogin, server: &str) -> anyhow::Result<Credential> {
    if self.args.cookie_on_stdin {
      return read_cookie_from_stdin();
    }

    let is_gateway = prelogin.is_gateway();

    match prelogin {
      Prelogin::Saml(prelogin) => {
        let browser = if prelogin.support_default_browser() {
          self.args.browser.as_deref()
        } else if !cfg!(feature = "webview-auth") {
          bail!("The server does not support authentication via the default browser and the gpclient is not built with the `webview-auth` feature");
        } else {
          None
        };

        let os_version = self.args.os_version();
        let verbose = self.shared_args.verbose.to_verbose_arg();
        let auth_launcher = SamlAuthLauncher::new(&self.args.server)
          .gateway(is_gateway)
          .saml_request(prelogin.saml_request())
          .user_agent(&self.args.user_agent)
          .os(self.args.os.as_str())
          .os_version(Some(&os_version))
          .fix_openssl(self.shared_args.fix_openssl)
          .ignore_tls_errors(self.shared_args.ignore_tls_errors)
          .browser(browser)
          .verbose(verbose);

        #[cfg(feature = "webview-auth")]
        let use_default_browser = prelogin.support_default_browser() && self.args.default_browser;
        #[cfg(feature = "webview-auth")]
        let auth_launcher = auth_launcher
          .hidpi(self.args.hidpi)
          .clean(self.args.clean)
          .default_browser(use_default_browser);

        let cred = auth_launcher.launch().await?;
        Ok(cred)
      }

      Prelogin::Standard(prelogin) => {
        let prefix = if is_gateway { "Gateway" } else { "Portal" };
        println!("{} ({}: {})", prelogin.auth_message(), prefix, server);

        let user = self.args.user.as_ref().map_or_else(
          || Text::new(&format!("{}:", prelogin.label_username())).prompt(),
          |user| Ok(user.to_owned()),
        )?;

        let password = if self.args.passwd_on_stdin {
          info!("Reading password from standard input");
          let mut input = String::new();
          std::io::stdin().read_line(&mut input)?;
          input.trim_end().to_owned()
        } else {
          Password::new(&format!("{}:", prelogin.label_password()))
            .without_confirmation()
            .with_display_mode(PasswordDisplayMode::Masked)
            .prompt()?
        };

        let password_cred = PasswordCredential::new(&user, &password);

        Ok(password_cred.into())
      }
    }
  }
}

fn read_cookie_from_stdin() -> anyhow::Result<Credential> {
  info!("Reading cookie from standard input");

  let mut cookie = String::new();
  std::io::stdin().read_line(&mut cookie)?;

  let Ok(auth_result) = serde_json::from_str::<SamlAuthResult>(cookie.trim_end()) else {
    bail!("Failed to parse auth data")
  };

  Credential::try_from(auth_result)
}

fn write_pid_file() {
  let pid = std::process::id();

  fs::write(GP_CLIENT_LOCK_FILE, pid.to_string()).unwrap();
  info!("Wrote PID {} to {}", pid, GP_CLIENT_LOCK_FILE);
}

fn get_csd_uid(csd_user: &Option<String>) -> anyhow::Result<u32> {
  if let Some(csd_user) = csd_user {
    get_user_by_name(csd_user).map(|user| user.uid())
  } else {
    get_non_root_user().map_or_else(|_| Ok(0), |user| Ok(user.uid()))
  }
}
