use crate::cmd::config::{resolve_config, ConfigArgsRef};
use crate::cmd::CliContext;
use clap::Parser;
use eternaltwin_core::types::WeakError;
use std::env;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use tokio::process::Command;

/// Arguments to the `dump` task.
#[derive(Debug, Parser)]
pub struct Args {
  /// Output directory
  ///
  /// The parent directory must exist. The output directory itself must be
  /// either missing or empty.
  dir: PathBuf,
  #[clap(flatten)]
  config: crate::cmd::config::Args,
}

impl ConfigArgsRef for Args {
  fn config_args(&self) -> &crate::cmd::config::Args {
    &self.config
  }
}

pub async fn run(cx: CliContext<'_, Args>) -> Result<(), WeakError> {
  let config = resolve_config(&cx, false).await?;
  let pg_dump_exe = "pg_dump";
  let working_dir = env::current_dir().map_err(WeakError::wrap)?;
  let out_dir = &cx.args.dir;
  eprintln!("Working directory: {}", working_dir.display());
  eprintln!("Output directory: {}", out_dir.display());
  eprintln!("`pg_dump` executable: {}", pg_dump_exe);
  eprintln!("--");
  eprintln!("Checking output directory...");
  let out_dir_state = check_out_dir(out_dir).map_err(WeakError::wrap)?;
  eprintln!("State: {:?}", out_dir_state);
  match out_dir_state {
    OutDirState::NotADirectory => return Err(WeakError::new("Output exists and is not a directory")),
    OutDirState::NonEmptyDir => return Err(WeakError::new("Output directory is non-empty")),
    OutDirState::DoesNotExist => {
      fs::create_dir(out_dir).map_err(WeakError::wrap)?;
    }
    OutDirState::EmptyDir => {
      // OK: Already an empty dir
    }
  }
  let out_dir = out_dir.canonicalize().map_err(WeakError::wrap)?;
  eprintln!("Resolved output directory: {}", out_dir.display());

  let mut cmd = Command::new(pg_dump_exe);
  cmd.current_dir(out_dir);
  cmd.env("PGPASSWORD", config.postgres.admin_password.value);
  cmd.arg("--clean");
  cmd.arg("--if-exists");
  cmd.arg("--format").arg("plain");
  cmd.arg("--no-owner");
  cmd.arg("--username").arg(config.postgres.admin_user.value);
  cmd.arg("--no-password");
  cmd.arg("--file").arg("etwin.sql");
  cmd.arg("--host").arg(config.postgres.host.value);
  cmd.arg("--port").arg(config.postgres.port.value.to_string());
  cmd.arg("--dbname").arg(config.postgres.name.value);

  let status = cmd.status().await.map_err(WeakError::wrap)?;

  if status.success() {
    eprintln!("--");
    eprintln!("OK");
    Ok(())
  } else {
    Err(WeakError::new("Error on `pg_dump`"))
  }
}

#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
enum OutDirState {
  DoesNotExist,
  NotADirectory,
  EmptyDir,
  NonEmptyDir,
}

fn check_out_dir(dir: &Path) -> Result<OutDirState, WeakError> {
  let mut read_dir: fs::ReadDir = match fs::read_dir(dir) {
    Ok(read_dir) => read_dir,
    Err(e) => {
      return match e.kind() {
        io::ErrorKind::NotFound => Ok(OutDirState::DoesNotExist),
        io::ErrorKind::Other if !dir.is_dir() => Ok(OutDirState::NotADirectory),
        _ => Err(WeakError::wrap(e)),
      };
    }
  };
  let is_empty = read_dir.next().is_none();
  if is_empty {
    Ok(OutDirState::EmptyDir)
  } else {
    Ok(OutDirState::NonEmptyDir)
  }
}
