use crate::cmd::CliContext;
use clap::ArgGroup;
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command, Error, FromArgMatches};
use eternaltwin_config::load::{ConfigRoot, ConfigRoots, EnvConfig, MetaLoadConfig, Profile, WorkingDir};
use eternaltwin_config::{
  BuiltinEternaltwinConfigProfile, Config, ConfigChain, ConfigFormat, ConfigSource, ConfigValue,
  EternaltwinConfigProfile, LoadConfigSource,
};
use eternaltwin_core::forum::{ForumSectionDisplayName, ForumSectionKey, UpsertSystemSectionOptions};
use eternaltwin_core::oauth::{OauthClientDisplayName, OauthClientKey, UpsertSystemClientOptions};
use eternaltwin_core::password::Password;
use eternaltwin_core::types::WeakError;
use eternaltwin_core::user::Username;
use eternaltwin_core::user::{UpsertSeedUser, UserIdRef, UserRef, UserUsernameRef};
use eternaltwin_system::EternaltwinSystem;
use std::collections::BTreeMap;
use std::str::FromStr;
use url::Url;

/// Arguments to the `config` task.
///
/// The config task loads the configuration, prints it and exits. Its purpose
/// is to help ensuring that the config is properly set-up.
///
/// The config is loaded in a few steps:
/// 1. Parse the command line arguments
/// 2. Resolve the environment. Real environment variables always win, then the
///    `.env` files are used.
/// <https://symfony.com/doc/current/configuration.html#overriding-environment-values-via-env-local>
#[derive(Debug)]
pub struct Args {
  profile: Option<EternaltwinConfigProfile>,
  env_config: Option<bool>,
  config_roots: Vec<ConfigCommand>,
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum ConfigCommand {
  Format(ConfigFormat),
  Url(String),
  Data(String),
  Search(String),
}

impl FromArgMatches for Args {
  fn from_arg_matches(matches: &ArgMatches) -> Result<Self, Error> {
    Self::from_arg_matches_mut(&mut matches.clone())
  }

  fn from_arg_matches_mut(matches: &mut ArgMatches) -> Result<Self, Error> {
    let profile = matches.remove_one::<EternaltwinConfigProfile>("profile");

    let env_config = match (
      matches.get_flag("env_config_true"),
      matches.get_flag("env_config_false"),
    ) {
      (true, false) => Some(true),
      (false, true) => Some(false),
      (false, false) => None,
      (true, true) => unreachable!(),
    };

    let mut config_commands: BTreeMap<usize, ConfigCommand> = BTreeMap::new();

    match (
      matches.indices_of("config_format"),
      matches.get_many::<ConfigFormat>("config_format"),
    ) {
      (None, None) => {}
      (Some(indices), Some(values)) => {
        for (i, v) in Iterator::zip(indices, values) {
          config_commands.insert(i, ConfigCommand::Format(*v));
        }
      }
      _ => unreachable!(),
    }

    match (matches.indices_of("config"), matches.get_many::<String>("config")) {
      (None, None) => {}
      (Some(indices), Some(values)) => {
        for (i, v) in Iterator::zip(indices, values) {
          config_commands.insert(i, ConfigCommand::Url(v.clone()));
        }
      }
      _ => unreachable!(),
    }

    match (
      matches.indices_of("config_data"),
      matches.get_many::<String>("config_data"),
    ) {
      (None, None) => {}
      (Some(indices), Some(values)) => {
        for (i, v) in Iterator::zip(indices, values) {
          config_commands.insert(i, ConfigCommand::Data(v.clone()));
        }
      }
      _ => unreachable!(),
    }

    match (
      matches.indices_of("config_search"),
      matches.get_many::<String>("config_search"),
    ) {
      (None, None) => {}
      (Some(indices), Some(values)) => {
        for (i, v) in Iterator::zip(indices, values) {
          config_commands.insert(i, ConfigCommand::Search(v.clone()));
        }
      }
      _ => unreachable!(),
    }

    Ok(Args {
      profile,
      env_config,
      config_roots: config_commands.into_values().collect(),
    })
  }

  fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), Error> {
    self.update_from_arg_matches_mut(&mut matches.clone())
  }

  fn update_from_arg_matches_mut(&mut self, matches: &mut ArgMatches) -> Result<(), Error> {
    self.profile = matches.remove_one::<EternaltwinConfigProfile>("profile");
    self.env_config = None;
    Ok(())
  }
}

impl clap::Args for Args {
  fn augment_args(cmd: Command) -> Command {
    cmd
      .arg(
        Arg::new("profile")
          .short('p')
          .long("profile")
          .value_name("PROFILE")
          .value_parser(value_parser!(EternaltwinConfigProfile))
          .action(ArgAction::Set)
          .help(r#"Eternaltwin configuration profile, for defaults and customization"#)
          .long_help(r#"Eternaltwin profile. Profiles control which config to load. Builtin profiles are "dev", "production", "sdk", "test". They control default values. Custom profiles use "dev" for default values. [env: ETERNALTWIN_PROFILE]"#),
      )
      .arg(
        Arg::new("env_config_true")
          .long("env-config")
          .overrides_with("env_config_false")
          .overrides_with("env_config_true")
          .action(ArgAction::SetTrue)
          .help("Allow reading configuration from environment variables")
          .long_help(r#"Allow reading configuration from environment variables. Negate it using "--no-env-config" [default: true]."#),
      )
      .arg(
        Arg::new("env_config_false")
          .long("no-env-config")
          .overrides_with("env_config_false")
          .overrides_with("env_config_true")
          .action(ArgAction::SetTrue)
          .hide(true)
      )
      .group(ArgGroup::new("env_config").args(["env_config_true", "env_config_false"]))
      .arg(Arg::new("config").short('c').long("config").value_name("CONFIG_URL").action(ArgAction::Append).help("Configuration file location").long_help(r#"Configuration file location. The format can be a file:// url, a relative path starting with "./" or "../". In the future more options may be provided"#))
      .arg(Arg::new("config_data").long("config-data").value_name("CONFIG_DATA").action(ArgAction::Append).help("Inline configuration").long_help(r"Inline configuration"))
      .arg(Arg::new("config_search").long("config-search").value_name("PATTERN").action(ArgAction::Append).help("Search config file in parent directories").long_help(r#"Search config file in parent directories. This works as if appending more and more `../` in front of the pattern. Stops at the first match or when the root is reached."#))
      .arg(
        Arg::new("config_format")
          .long("config-format")
          .value_name("FORMAT")
          .value_parser(value_parser!(ConfigFormat))
          .action(ArgAction::Append)
          .help(r#"Configuration format for the next config argument"#)
          .long_help(r#"Configuration format for the next config argument. Possibles values are "auto", "json", "toml"."#),
      )
  }

  fn augment_args_for_update(cmd: Command) -> Command {
    Self::augment_args(cmd)
  }
}

/// Like [`Deref`], but the target is [`crate::cmd::config::Args`].
pub trait ConfigArgsRef: Send + Sync {
  fn config_args(&self) -> &Args;
}

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

pub async fn resolve_config<A: ConfigArgsRef, Out>(
  cx: &CliContext<'_, A, Out>,
  verbose: bool,
) -> Result<Config<ConfigSource>, WeakError> {
  if verbose {
    eprintln!("start early initialization");
  }
  let config = resolve_load_config(cx).await?;
  if verbose {
    eprintln!("early initialization complete");
    print_load_config(&config);
    eprintln!("configuration resolution: start");
  }
  let config = ConfigChain::resolve_from_roots(
    config.get(&ConfigRoots).value,
    config.get(&WorkingDir).value,
    config.get(&Profile).value,
  );
  if verbose {
    eprintln!("configuration resolution: done");
    print_config_chain(&config);
  }
  let config: Config<ConfigSource> = config.merge();
  if verbose {
    let mut safe_config = config.clone();
    safe_config.backend.secret.value = None;
    eprintln!("resolved config:");
    println!(
      "{}",
      serde_json::to_string_pretty(&safe_config).expect("serialization succeeds")
    );
  }
  Ok(config)
}

pub async fn resolve_load_config<A: ConfigArgsRef, Out>(
  cx: &CliContext<'_, A, Out>,
) -> Result<MetaLoadConfig, WeakError> {
  let args = cx.args.config_args();
  let config = MetaLoadConfig::new(cx.working_dir.to_path_buf());
  let config = match args.env_config {
    Some(env_config) => config.arg_env_config(env_config),
    None => config,
  };
  let config = match &args.profile {
    Some(profile) => config.arg_profile(profile.clone()),
    None => {
      if config.get(&EnvConfig).value {
        match cx.env.get("ETERNALTWIN_PROFILE") {
          Some(profile) => config.env_profile(EternaltwinConfigProfile::parse(profile)),
          None => config,
        }
      } else {
        config
      }
    }
  };
  let config = config.refresh_env_config_from_profile();
  let profile: BuiltinEternaltwinConfigProfile = config.get(&Profile).value.as_builtin_or_dev();
  let config = if args.config_roots.is_empty() {
    config
  } else {
    let mut config_roots: Vec<ConfigRoot> = Vec::new();
    let mut format = ConfigValue {
      value: ConfigFormat::Auto,
      meta: LoadConfigSource::Default(profile),
    };
    for cmd in &args.config_roots {
      match cmd {
        ConfigCommand::Format(ref new_format) => {
          if matches!(format.meta, LoadConfigSource::Default(_)) {
            format = ConfigValue {
              value: *new_format,
              meta: LoadConfigSource::Args,
            }
          } else {
            panic!("duplicate `--config-format`");
          }
        }
        ConfigCommand::Url(c) => {
          config_roots.push(ConfigRoot::Url {
            url: ConfigValue {
              value: c.clone(),
              meta: LoadConfigSource::Args,
            },
            format,
          });
          format = ConfigValue {
            value: ConfigFormat::Auto,
            meta: LoadConfigSource::Default(profile),
          };
        }
        ConfigCommand::Data(c) => {
          config_roots.push(ConfigRoot::Data {
            data: ConfigValue {
              value: c.clone(),
              meta: LoadConfigSource::Args,
            },
            format,
          });
          format = ConfigValue {
            value: ConfigFormat::Auto,
            meta: LoadConfigSource::Default(profile),
          };
        }
        ConfigCommand::Search(c) => {
          config_roots.push(ConfigRoot::Search {
            patterns: ConfigValue {
              value: vec![c.clone()],
              meta: LoadConfigSource::Args,
            },
            format,
          });
          format = ConfigValue {
            value: ConfigFormat::Auto,
            meta: LoadConfigSource::Default(profile),
          };
        }
      }
    }
    config.set_config_roots(config_roots, LoadConfigSource::Args)
  };
  Ok(config)
}

pub fn print_load_config(config: &MetaLoadConfig) {
  {
    let profile = config.get(&Profile);
    eprintln!(
      "- profile = {:?} [source = {:?}]",
      profile.value.as_str(),
      profile.meta.source
    );
  }
  {
    let env_config = config.get(&EnvConfig);
    eprintln!(
      "- env_config = {:?} [source = {:?}]",
      env_config.value, env_config.meta.source
    );
  }
  {
    let working_dir = config.get(&WorkingDir);
    eprintln!(
      "- working_dir = {:?} [source = {:?}]",
      working_dir.value, working_dir.meta.source
    );
  }
  {
    let config_roots = config.get(&ConfigRoots);
    eprintln!("- config_roots [source = {:?}]", config_roots.meta.source);
    eprintln!("  - Default");
    for root in config_roots.value {
      match root {
        ConfigRoot::Data { data, format } => {
          eprintln!("  - Data");
          eprintln!("    - format = {:?} [source = {:?}]", &format.value, format.meta);
          eprintln!("    - data = (hidden) [source = {:?}]", data.meta);
        }
        ConfigRoot::Url { url, format } => {
          eprintln!("  - Url");
          eprintln!("    - format = {:?} [source = {:?}]", &format.value, format.meta);
          eprintln!("    - url = {:?} [source = {:?}]", &url.value, url.meta);
        }
        ConfigRoot::Search { patterns, format } => {
          eprintln!("  - Search");
          eprintln!("    - format = {:?} [source = {:?}]", &format.value, format.meta);
          let empty_warning = if patterns.value.is_empty() { " (empty)" } else { "" };
          eprintln!("    - patterns{} [source = {:?}]", empty_warning, patterns.meta);
          for pattern in &patterns.value {
            eprintln!("      - {pattern:?}");
          }
        }
      }
    }
  }
}

pub fn print_config_chain(config: &ConfigChain) {
  eprintln!("loaded config (from highest priority to lowest):");
  for item in &config.items {
    match &item.source {
      ConfigSource::File(Some(file)) => eprintln!("- {}", Url::from_file_path(file).unwrap()),
      ConfigSource::File(None) => eprintln!("- (anonymous file)"),
      ConfigSource::Cli => eprintln!("- Args"),
      ConfigSource::Env => eprintln!("- Env"),
      ConfigSource::Default => eprintln!("- Default"),
    }
  }
  eprintln!("- Default");
}

pub async fn populate_system<TyConfigMeta>(
  system: &EternaltwinSystem,
  config: &Config<TyConfigMeta>,
  verbose: bool,
) -> Result<(), WeakError> {
  for (user_key, user) in &config.seed.user.value {
    if verbose {
      eprintln!("- upsert user {user_key:?}");
    }
    let (r, username) = match user.id.value {
      Some(id) => (UserRef::Id(UserIdRef::new(id)), user.username.value.as_ref()),
      None => {
        let new_username = match &user.username.value {
          Some(new_username) => {
            assert_eq!(
              new_username.as_str(),
              user_key,
              "if provided, seed user username must match key"
            );
            Some(new_username)
          }
          None => None,
        };
        let ref_username = Username::from_str(user_key).expect("invalid username");
        (UserRef::Username(UserUsernameRef::new(ref_username)), new_username)
      }
    };

    system
      .user
      .as_ref()
      .upsert_seed_user(UpsertSeedUser {
        r#ref: r,
        username: username.cloned(),
        display_name: user.display_name.value.clone(),
        email: None,
        password: user.password.value.as_deref().map(Password::from),
        is_administrator: user.is_administrator.value,
      })
      .await
      .unwrap_or_else(|e| panic!("failed user upsert {user_key:?}: {e}"));
  }

  for (client_key, client_config) in &config.seed.app.value {
    if verbose {
      eprintln!("- upsert app {client_key:?}");
    }
    system
      .oauth
      .as_ref()
      .upsert_system_client(&UpsertSystemClientOptions {
        key: OauthClientKey::from_str(format!("{client_key}@clients").as_str())
          .unwrap_or_else(|e| panic!("invalid client key {client_key:?}: {e}")),
        display_name: OauthClientDisplayName::from_str(client_config.display_name.value.as_str()).unwrap_or_else(|e| {
          panic!(
            "invalid client display name {:?}: {e}",
            client_config.display_name.value
          )
        }),
        app_uri: client_config.uri.value.clone(),
        callback_uri: client_config.oauth_callback.value.clone(),
        secret: Password::from(client_config.secret.value.as_bytes()),
      })
      .await
      .unwrap_or_else(|e| panic!("failed client upsert {client_key:?}: {e}"));
  }

  for (section_key, section_config) in &config.seed.forum_section.value {
    if verbose {
      eprintln!("- upsert forum section {section_key:?}");
    }
    system
      .forum
      .as_ref()
      .upsert_system_section(&UpsertSystemSectionOptions {
        key: ForumSectionKey::from_str(section_key.as_str())
          .unwrap_or_else(|e| panic!("invalid section key {section_key:?}: {e}")),
        display_name: ForumSectionDisplayName::from_str(section_config.display_name.value.as_str()).unwrap_or_else(
          |e| {
            panic!(
              "invalid section display name {:?}: {e}",
              section_config.display_name.value
            )
          },
        ),
        locale: section_config.locale.value,
      })
      .await
      .unwrap_or_else(|e| panic!("failed section upsert {section_key:?}: {e}"));
  }

  Ok(())
}

pub async fn run(cx: CliContext<'_, Args>) -> Result<(), WeakError> {
  resolve_config(&cx, true).await?;
  Ok(())
}

#[cfg(test)]
mod test {
  use super::*;
  use clap::Args as _;

  #[test]
  fn check_args() {
    Args::augment_args(Command::new("eternaltwin-config")).debug_assert()
  }
}
