#![allow(unused_variables)]
#![allow(async_fn_in_trait)]
#![warn(unused_extern_crates)]

pub mod action;
pub mod app;
pub mod cli;
pub mod completion;
pub mod components;
pub mod config;
pub mod database;
pub mod external_editor;
pub mod focus;
pub mod keyring;
pub mod popups;
pub mod tui;
pub mod ui;
pub mod utils;
pub mod vim;

use std::{
  env, fs,
  io::{self, Write},
  path::Path,
};

use clap::Parser;
use cli::{Cli, CliCommand, Driver, extract_driver_from_url, prompt_for_database_selection};
use color_eyre::eyre::Result;
use config::{
  Config, ConnectionString, default_config_contents, existing_config_path, preferred_config_path,
};
use dotenvy::dotenv;
use keyring::get_password;
use tracing::Instrument;

use crate::{
  app::App,
  utils::{initialize_logging, initialize_panic_handler, session_id},
};

async fn run_app(mut args: Cli, config: Config, driver: Driver) -> Result<()> {
  let mouse_mode = args.mouse_mode.take();
  let mut app = App::new(mouse_mode, config)?;
  app.run(driver, args).await?;
  Ok(())
}

fn supports_ssl_required(driver: Driver) -> bool {
  match driver {
    Driver::Postgres | Driver::MySql => true,
    #[cfg(feature = "oracle")]
    Driver::Oracle => true,
    _ => false,
  }
}

fn resolve_driver(args: &mut Cli, config: &Config) -> Result<Driver> {
  let url = args.connection_url.clone().or_else(|| {
    env::var("DATABASE_URL").map_or(None, |url| {
      if url.is_empty() {
        None
      } else {
        println!("Using DATABASE_URL from environment variable");
        Some(url)
      }
    })
  });
  let has_cli_input = args.driver.is_some()
    || args.user.is_some()
    || args.password.is_some()
    || args.host.is_some()
    || args.port.is_some()
    || args.database.is_some();

  args.connection_name = None;

  let (driver, url) = match (url, has_cli_input) {
    (Some(u), _) => {
      if let Some(driver) = args.driver.take() { Ok(driver) } else { extract_driver_from_url(&u) }
        .map(|d| (d, Some(u)))
    },
    (None, true) => {
      if let Some(driver) = args.driver.take() {
        Ok((driver, None))
      } else {
        Ok((prompt_for_driver()?, None))
      }
    },
    (None, false) => Ok(match prompt_for_database_selection(config)? {
      Some((conn, name)) => {
        args.connection_name = Some(name.clone());
        if !args.enable_cleartext_plugin {
          args.enable_cleartext_plugin = conn.enable_cleartext_plugin.unwrap_or(false);
        }
        let url = match conn.connection {
          ConnectionString::Raw { connection_string } => Ok(connection_string),
          ConnectionString::Structured { details } => {
            let password = get_password(&name, &details.username, args.reenter_password)?;
            details.connection_string(conn.driver, password)
          },
        }?;

        (conn.driver, Some(url))
      },
      None => (prompt_for_driver()?, None),
    }),
  }?;

  args.connection_url = url;

  Ok(driver)
}

fn ensure_config_file(path: &Path) -> Result<()> {
  if path.exists() {
    return Ok(());
  }

  if let Some(parent) = path.parent() {
    fs::create_dir_all(parent)?;
  }
  fs::write(path, default_config_contents())?;
  Ok(())
}

fn edit_config_file() -> Result<()> {
  let config_path = existing_config_path().unwrap_or_else(preferred_config_path);
  ensure_config_file(&config_path)?;
  external_editor::open_editor(&config_path)
}

async fn tokio_main() -> Result<()> {
  let mut args = Cli::parse();
  dotenv().ok();
  if args.command == Some(CliCommand::Edit) {
    return edit_config_file();
  }

  initialize_logging()?;

  initialize_panic_handler()?;

  let session_marker = format!("[{}]", session_id());
  let session_span = tracing::info_span!(
    "session",
    id = %session_marker,
    db = tracing::field::Empty,
    conn = tracing::field::Empty,
  );
  async move {
    let config = Config::new()?;
    let driver = resolve_driver(&mut args, &config)?;
    let driver_name = format!("{driver:?}");
    tracing::Span::current().record("db", driver_name.as_str());
    if args.enable_cleartext_plugin && driver != Driver::MySql {
      eprintln!(
        "warning: --enable-cleartext-plugin is only supported for mysql connections and will be ignored"
      );
    }
    if args.ssl_required && !supports_ssl_required(driver) {
      eprintln!(
        "warning: --ssl-required is only supported for postgres, mysql, and oracle connections and will be ignored"
      );
    }

    run_app(args, config, driver).await
  }
  .instrument(session_span)
  .await
}

#[tokio::main]
async fn main() -> Result<()> {
  match tokio_main().await {
    Err(e) => {
      eprintln!("{} error: Something went wrong", env!("CARGO_PKG_NAME"));
      Err(e)
    },
    _ => Ok(()),
  }
}

pub fn prompt_for_driver() -> Result<Driver> {
  let mut driver = String::new();
  #[allow(unused_mut)]
  let mut drivers = vec!["postgres", "mysql", "sqlite"];
  #[cfg(feature = "oracle")]
  drivers.push("oracle");
  #[cfg(feature = "duckdb")]
  drivers.push("duckdb");
  print!("Database driver ({}): ", drivers.join(", "));
  io::stdout().flush()?;
  io::stdin().read_line(&mut driver)?;
  driver.trim().to_lowercase().parse()
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn ssl_required_support_matches_tls_capable_drivers() {
    assert!(supports_ssl_required(Driver::Postgres));
    assert!(supports_ssl_required(Driver::MySql));
    #[cfg(feature = "oracle")]
    assert!(supports_ssl_required(Driver::Oracle));
    assert!(!supports_ssl_required(Driver::Sqlite));
    #[cfg(feature = "duckdb")]
    assert!(!supports_ssl_required(Driver::DuckDb));
  }
}
