use anyhow::Context;
use diesel::{connection::SimpleConnection, Connection, PgConnection};
use diesel_migrations::{EmbeddedMigrations, MigrationHarness};
use lemmy_utils::error::LemmyError;
use tracing::info;

const MIGRATIONS: EmbeddedMigrations = embed_migrations!();

/// This SQL code sets up the `r` schema, which contains things that can be safely dropped and
/// replaced instead of being changed using migrations. It may not create or modify things outside
/// of the `r` schema (indicated by `r.` before the name), unless a comment says otherwise.
///
/// Currently, this code is only run after the server starts and there's at least 1 pending
/// migration to run. This means every time you change something here, you must also create a
/// migration (a blank up.sql file works fine). This behavior will be removed when we implement a
/// better way to avoid useless schema updates and locks.
///
/// If you add something that depends on something (such as a table) created in a new migration,
/// then down.sql must use `CASCADE` when dropping it. This doesn't need to be fixed in old
/// migrations because the "replaceable-schema" migration runs `DROP SCHEMA IF EXISTS r CASCADE` in
/// down.sql.
const REPLACEABLE_SCHEMA: &[&str] = &[
  "DROP SCHEMA IF EXISTS r CASCADE;",
  "CREATE SCHEMA r;",
  include_str!("../replaceable_schema/utils.sql"),
  include_str!("../replaceable_schema/triggers.sql"),
];

pub fn run(db_url: &str) -> Result<(), LemmyError> {
  // Migrations don't support async connection
  let mut conn = PgConnection::establish(db_url).with_context(|| "Error connecting to database")?;

  // Run all pending migrations except for the newest one, then run the newest one in the same
  // transaction as `REPLACEABLE_SCHEMA`. This code will be becone less hacky when the conditional
  // setup of things in `REPLACEABLE_SCHEMA` is done without using the number of pending
  // migrations.
  info!("Running Database migrations (This may take a long time)...");
  let migrations = conn
    .pending_migrations(MIGRATIONS)
    .map_err(|e| anyhow::anyhow!("Couldn't determine pending migrations: {e}"))?;
  for migration in migrations.iter().rev().skip(1).rev() {
    conn
      .run_migration(migration)
      .map_err(|e| anyhow::anyhow!("Couldn't run DB Migrations: {e}"))?;
  }
  conn.transaction::<_, LemmyError, _>(|conn| {
    if let Some(migration) = migrations.last() {
      // Migration is run with a savepoint since there's already a transaction
      conn
        .run_migration(migration)
        .map_err(|e| anyhow::anyhow!("Couldn't run DB Migrations: {e}"))?;
    } else if !cfg!(debug_assertions) {
      // In production, skip running `REPLACEABLE_SCHEMA` to avoid locking things in the schema. In
      // CI, always run it because `diesel migration` commands would otherwise prevent it.
      return Ok(());
    }
    conn
      .batch_execute(&REPLACEABLE_SCHEMA.join("\n"))
      .context("Couldn't run SQL files in crates/db_schema/replaceable_schema")?;

    Ok(())
  })?;
  info!("Database migrations complete.");

  Ok(())
}
