mod app;
mod archive;
pub mod auth;
mod bff;
mod clock;
mod config;
mod dev;
mod extract;
mod forum;
mod job;
mod oauth_clients;
#[cfg(test)]
pub(crate) mod test;
mod users;
use eternaltwin_config::{ClockConfig, Config, MailerType, StoreConfig};

pub use crate::dev::DevApi;
pub use crate::extract::InternalAuthKey;
use axum::body::Body;
use axum::extract::Extension;
use axum::http::Request;
use axum::middleware::Next;
use axum::response::Response;
use axum::Router;
use eternaltwin_auth_store::mem::MemAuthStore;
use eternaltwin_auth_store::pg::PgAuthStore;
use eternaltwin_config::postgres::PostgresConfig;
use eternaltwin_core::api::OwnRef;
use eternaltwin_core::auth::AuthStore;
use eternaltwin_core::clock::{Clock, DynScheduler, ErasedScheduler, Scheduler, SystemClock, VirtualClock};
use eternaltwin_core::core::{Instant, Secret};
use eternaltwin_core::dinoparc::DinoparcStore;
use eternaltwin_core::email::{EmailFormatter, Mailer};
use eternaltwin_core::forum::ForumStore;
use eternaltwin_core::hammerfest::HammerfestStore;
use eternaltwin_core::job::{DynJobRuntime, JobRuntime, JobStore, OpaqueTask, OpaqueValue};
use eternaltwin_core::link::LinkStore;
use eternaltwin_core::oauth::OauthProviderStore;
use eternaltwin_core::password::PasswordService;
use eternaltwin_core::twinoid::TwinoidStore;
use eternaltwin_core::types::{AnyError, WeakError};
use eternaltwin_core::user::UserStore;
use eternaltwin_core::uuid::{Uuid4Generator, UuidGenerator};
use eternaltwin_dinoparc_store::mem::MemDinoparcStore;
use eternaltwin_dinoparc_store::pg::PgDinoparcStore;
use eternaltwin_email_formatter::html::HtmlEmailFormatter;
use eternaltwin_email_formatter::json::JsonEmailFormatter;
use eternaltwin_forum_store::mem::MemForumStore;
use eternaltwin_forum_store::pg::PgForumStore;
use eternaltwin_hammerfest_store::mem::MemHammerfestStore;
use eternaltwin_hammerfest_store::pg::PgHammerfestStore;
use eternaltwin_job_store::mem::MemJobStore;
use eternaltwin_job_store::pg::PgJobStore;
use eternaltwin_link_store::mem::MemLinkStore;
use eternaltwin_link_store::pg::PgLinkStore;
use eternaltwin_mailer::mem::MemMailer;
use eternaltwin_mailer::smtp::{InvalidHeaderNameError, SmtpMailer};
use eternaltwin_oauth_client::RfcOauthClient;
use eternaltwin_oauth_provider_store::mem::MemOauthProviderStore;
use eternaltwin_oauth_provider_store::pg::PgOauthProviderStore;
use eternaltwin_password::scrypt::{OsRng, ScryptPasswordService};
use eternaltwin_services::auth::{AuthService, DynAuthService};
use eternaltwin_services::dinoparc::{DinoparcService, DynDinoparcService};
use eternaltwin_services::forum::{DynForumService, ForumService};
use eternaltwin_services::hammerfest::{DynHammerfestService, HammerfestService};
use eternaltwin_services::job::{DynJobService, JobService};
use eternaltwin_services::oauth::{DynOauthService, OauthService};
use eternaltwin_services::twinoid::{DynTwinoidService, TwinoidService};
use eternaltwin_services::user::{DynUserService, UserService};
use eternaltwin_twinoid_store::mem::MemTwinoidStore;
use eternaltwin_twinoid_store::pg::PgTwinoidStore;
use eternaltwin_user_store::mem::MemUserStore;
use eternaltwin_user_store::pg::PgUserStore;
pub use serde::Serialize;
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
use sqlx::PgPool;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;

#[derive(Debug)]
struct ServerError(AnyError);

#[derive(Clone)]
pub struct EternaltwinSystem {
  pub dev: DevApi,
  pub auth: Arc<DynAuthService>,
  pub clock: Arc<dyn Scheduler<Timer = Pin<Box<dyn Future<Output = ()> + Send>>>>,
  pub dinoparc: Arc<DynDinoparcService>,
  pub forum: Arc<DynForumService>,
  pub hammerfest: Arc<DynHammerfestService>,
  pub job: Arc<DynJobService>,
  pub job_runtime: Arc<DynJobRuntime<'static>>,
  pub oauth: Arc<DynOauthService>,
  pub twinoid: Arc<DynTwinoidService>,
  pub user: Arc<DynUserService>,
}

/// On-demand and cached pg pool factory
struct LazyPgPool<'a, ConfigMeta> {
  config: &'a PostgresConfig<ConfigMeta>,
  pool: Option<PgPool>,
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, thiserror::Error)]
pub enum GetPgPoolError {
  #[error("missing database configuration")]
  MissingConfig,
  #[error("failed to connect to the database")]
  Connect(#[from] WeakError),
}

impl<'a, ConfigMeta> LazyPgPool<'a, ConfigMeta> {
  pub fn new(config: &'a PostgresConfig<ConfigMeta>) -> Self {
    Self { config, pool: None }
  }

  pub async fn get(&mut self) -> Result<PgPool, GetPgPoolError> {
    let pool = match &self.pool {
      Some(pool) => pool,
      None => {
        let pool: PgPool = PgPoolOptions::new()
          .max_connections(self.config.max_connections.value)
          .connect_with(
            PgConnectOptions::new()
              .host(&self.config.host.value)
              .port(self.config.port.value)
              .database(&self.config.name.value)
              .username(&self.config.user.value)
              .password(&self.config.password.value),
          )
          .await
          .map_err(WeakError::wrap)?;

        self.pool.insert(pool)
      }
    };
    Ok(pool.clone())
  }
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, thiserror::Error)]
#[allow(clippy::enum_variant_names)]
pub enum CreateSystemError {
  #[error("failed to acquire database pool")]
  PgPool(#[from] GetPgPoolError),
  #[error("missing baclend.secret")]
  MissingBackendSecret,
  #[error("failed to build smtp client")]
  SmtpBuilder(#[from] InvalidHeaderNameError),
  #[error("failed to create PgDinoparcStore")]
  PgDinoparcStore(#[source] WeakError),
  #[error("failed to create PgHammerfestStore")]
  PgHammerfestStore(#[source] WeakError),
  #[error("failed to create PgTwinoidStore")]
  PgTwinoidStore(#[source] WeakError),
}

impl EternaltwinSystem {
  pub async fn create_dyn<ConfigMeta>(config: &Config<ConfigMeta>) -> Result<Self, CreateSystemError> {
    let dev = DevApi::new();
    let (clock, scheduler): (Arc<dyn Clock>, Arc<DynScheduler>) = match config.backend.clock.value {
      ClockConfig::System => {
        let clock = Arc::new(ErasedScheduler::new(SystemClock));
        let scheduler = Arc::clone(&clock);
        (clock, scheduler)
      }
      ClockConfig::Virtual => {
        let clock = Arc::new(ErasedScheduler::new(VirtualClock::new(Instant::ymd_hms(
          2020, 1, 1, 0, 0, 0,
        ))));
        let scheduler = Arc::clone(&clock);
        (clock, scheduler)
      }
    };
    let mut db = LazyPgPool::new(&config.postgres);
    let secret_str = match config.backend.secret.value.as_ref() {
      Some(secret) => Secret::new(secret.to_string()),
      None => return Err(CreateSystemError::MissingBackendSecret),
    };
    let secret_bytes = secret_str.as_str().as_bytes().to_vec();

    let rng = OsRng;

    // TODO: Seeded RNG (use clock + seed)
    let uuid_generator: Arc<dyn UuidGenerator> = Arc::new(Uuid4Generator);

    let email_formatter: Arc<dyn EmailFormatter> = match config.backend.mailer.value {
      MailerType::Mock => Arc::new(JsonEmailFormatter),
      MailerType::Network => Arc::new(HtmlEmailFormatter),
    };
    let mailer: Arc<dyn Mailer> = match config.backend.mailer.value {
      MailerType::Mock => Arc::new(MemMailer::new()),
      MailerType::Network => Arc::new({
        let mut mailer = SmtpMailer::builder(
          config.mailer.host.value.clone(),
          config.mailer.username.value.clone(),
          config.mailer.password.value.clone(),
          config.mailer.sender.value.clone(),
        );
        for header in config.mailer.headers.value.iter() {
          mailer.ascii_header(&header.name, header.value.clone())?;
        }
        Arc::new(mailer.build())
      }),
    };
    let password: Arc<dyn PasswordService> = Arc::new(ScryptPasswordService::new(
      rng,
      config.scrypt.max_time.value,
      config.scrypt.max_mem_frac.value,
    ));

    let auth_store: Arc<dyn AuthStore> = match config.backend.auth_store.value {
      StoreConfig::Postgres => Arc::new(PgAuthStore::new(
        Arc::clone(&clock),
        OwnRef(db.get().await?),
        Arc::clone(&uuid_generator),
        secret_str.clone(),
      )),
      StoreConfig::Memory => Arc::new(MemAuthStore::new(Arc::clone(&clock), Arc::clone(&uuid_generator))),
    };
    let dinoparc_store: Arc<dyn DinoparcStore> = match config.backend.dinoparc_store.value {
      StoreConfig::Postgres => Arc::new(
        PgDinoparcStore::new(Arc::clone(&clock), OwnRef(db.get().await?), Arc::clone(&uuid_generator))
          .await
          .map_err(WeakError::wrap_any)
          .map_err(CreateSystemError::PgDinoparcStore)?,
      ),
      StoreConfig::Memory => Arc::new(MemDinoparcStore::new(Arc::clone(&clock))),
    };
    let forum_store: Arc<dyn ForumStore> = match config.backend.forum_store.value {
      StoreConfig::Postgres => Arc::new(PgForumStore::new(
        Arc::clone(&clock),
        OwnRef(db.get().await?),
        Arc::clone(&uuid_generator),
      )),
      StoreConfig::Memory => Arc::new(MemForumStore::new(Arc::clone(&clock), Arc::clone(&uuid_generator))),
    };
    let hammerfest_store: Arc<dyn HammerfestStore> = match config.backend.hammerfest_store.value {
      StoreConfig::Postgres => Arc::new(
        PgHammerfestStore::new(
          Arc::clone(&clock),
          OwnRef(db.get().await?),
          secret_str.clone(),
          Arc::clone(&uuid_generator),
        )
        .await
        .map_err(WeakError::wrap_any)
        .map_err(CreateSystemError::PgHammerfestStore)?,
      ),
      StoreConfig::Memory => Arc::new(MemHammerfestStore::new(Arc::clone(&clock))),
    };
    let job_store: Arc<dyn JobStore<OpaqueTask, OpaqueValue>> = match config.backend.job_store.value {
      StoreConfig::Postgres => Arc::new(PgJobStore::new(
        OwnRef(db.get().await?),
        secret_str.clone(),
        Arc::clone(&uuid_generator),
      )),
      StoreConfig::Memory => Arc::new(MemJobStore::new(Arc::clone(&uuid_generator))),
    };
    let link_store: Arc<dyn LinkStore> = match config.backend.link_store.value {
      StoreConfig::Postgres => Arc::new(PgLinkStore::new(Arc::clone(&clock), OwnRef(db.get().await?))),
      StoreConfig::Memory => Arc::new(MemLinkStore::new(Arc::clone(&clock))),
    };
    let oauth_provider_store: Arc<dyn OauthProviderStore> = match config.backend.oauth_provider_store.value {
      StoreConfig::Postgres => Arc::new(PgOauthProviderStore::new(
        Arc::clone(&clock),
        OwnRef(db.get().await?),
        Arc::clone(&password),
        Arc::clone(&uuid_generator),
        secret_str.clone(),
      )),
      StoreConfig::Memory => Arc::new(MemOauthProviderStore::new(
        Arc::clone(&clock),
        Arc::clone(&password),
        Arc::clone(&uuid_generator),
      )),
    };
    let user_store: Arc<dyn UserStore> = match config.backend.user_store.value {
      StoreConfig::Postgres => Arc::new(PgUserStore::new(
        Arc::clone(&clock),
        OwnRef(db.get().await?),
        secret_str.clone(),
        Arc::clone(&uuid_generator),
      )),
      StoreConfig::Memory => Arc::new(MemUserStore::new(Arc::clone(&clock), Arc::clone(&uuid_generator))),
    };
    let twinoid_store: Arc<dyn TwinoidStore> = match config.backend.twinoid_store.value {
      StoreConfig::Postgres => Arc::new(
        PgTwinoidStore::new(
          Arc::clone(&clock),
          OwnRef(db.get().await?),
          secret_str.clone(),
          Arc::clone(&uuid_generator),
        )
        .await
        .map_err(CreateSystemError::PgTwinoidStore)?,
      ),
      StoreConfig::Memory => Arc::new(MemTwinoidStore::new(Arc::clone(&clock))),
    };

    let auth = Arc::new(AuthService::new(
      Arc::clone(&auth_store),
      Arc::clone(&clock),
      Arc::clone(&dinoparc_store),
      Arc::clone(&email_formatter),
      Arc::clone(&hammerfest_store),
      Arc::clone(&link_store),
      Arc::clone(&mailer),
      Arc::clone(&oauth_provider_store),
      Arc::clone(&password),
      Arc::clone(&user_store),
      Arc::clone(&twinoid_store),
      Arc::clone(&uuid_generator),
      secret_bytes.clone(),
      secret_bytes,
    ));

    let dinoparc = Arc::new(DinoparcService::new(
      Arc::clone(&dinoparc_store),
      Arc::clone(&link_store),
      Arc::clone(&user_store),
    ));

    let forum = Arc::new(ForumService::new(
      Arc::clone(&clock),
      Arc::clone(&forum_store),
      Arc::clone(&user_store),
    ));

    let hammerfest = Arc::new(HammerfestService::new(
      Arc::clone(&hammerfest_store),
      Arc::clone(&link_store),
      Arc::clone(&user_store),
    ));

    let oauth = Arc::new(OauthService::new(
      Arc::clone(&oauth_provider_store),
      Arc::clone(&user_store),
    ));

    let twinoid = Arc::new(TwinoidService::new(
      Arc::clone(&clock),
      Arc::clone(&link_store),
      Arc::clone(&twinoid_store),
      Arc::clone(&user_store),
    ));

    let user = Arc::new(UserService::new(
      Arc::clone(&auth_store),
      Arc::clone(&clock),
      Arc::clone(&dinoparc_store),
      Arc::clone(&hammerfest_store),
      Arc::clone(&link_store),
      password,
      Arc::clone(&twinoid_store),
      Arc::clone(&user_store),
    ));

    let job_runtime = JobRuntime::new(
      Arc::clone(&scheduler),
      Arc::clone(&job_store),
      Arc::clone(&hammerfest_store),
      Arc::clone(&twinoid_store),
    );
    // job_runtime.register::<ArchiveTwinoidUsers>();
    // job_runtime.register::<ScrapeAllHammerfestProfiles>();
    // job_runtime.register::<ScrapeHammerfestThread>();
    // job_runtime.register::<ScrapeHammerfestThreadList>();
    // job_runtime.register::<ScrapeHammerfestTheme>();
    let job_runtime = Arc::new(job_runtime);

    let job = Arc::new(JobService::new(
      Arc::clone(&scheduler),
      Arc::clone(&job_runtime),
      Arc::clone(&job_store),
      Arc::clone(&user_store),
    ));

    Ok(EternaltwinSystem {
      dev,
      auth,
      clock: scheduler,
      dinoparc,
      forum,
      hammerfest,
      job,
      job_runtime,
      oauth,
      twinoid,
      user,
    })
  }
}

#[derive(Clone)]
pub struct ExtendedEternaltwinSystem {
  pub base: EternaltwinSystem,
  pub twinoid_oauth_client: Arc<dyn RfcOauthClient>,
}

pub fn router() -> Router<(), Body> {
  Router::new()
    .nest("/app", app::router())
    .nest("/archive", archive::router())
    .nest("/auth", auth::router())
    .nest("/clock", clock::router())
    .nest("/config", config::router())
    .nest("/forum", forum::router())
    .nest("/job", job::router())
    .nest("/oauth_clients", oauth_clients::router())
    .nest("/users", users::router())
}

pub fn app(system: EternaltwinSystem) -> Router<(), Body> {
  Router::new()
    .nest("/api/v1", router())
    .nest("/oauth", bff::oauth::router())
    .nest("/actions", bff::actions::router())
    .layer(Extension(system.dev.clone()))
    .layer(Extension(system))
    .layer(axum::middleware::from_fn(on_request))
}

async fn on_request<B>(request: Request<B>, next: Next<B>) -> Response {
  let method = request.method();
  let path = request.uri().path();
  eprintln!("--> {} {}", method, path);
  let method = method.clone();
  let path = path.to_string();
  let start_time = std::time::Instant::now();
  let response = next.run(request).await;
  let duration = start_time.elapsed();
  eprintln!(
    "<-- {} {} {}ms {}",
    method,
    path,
    duration.as_millis(),
    response.status()
  );
  response
}
