///|
/// A migration, as the three files it becomes.
///
/// Produced without touching the filesystem so that the interesting part —
/// what SQL a schema edit turns into — is testable, and so a failure part-way
/// through cannot leave a `.sql` on disk with no snapshot beside it.
pub struct Migration {
  idx : Int
  /// File stem, e.g. `0001_add_column_accounts_plan`.
  tag : String
  /// Contents of `/.sql`.
  sql : String
  /// Contents of `/meta/_snapshot.json`.
  snapshot : String
} derive(Debug, Eq)

///|
/// What `generate` would do.
pub struct Plan {
  changes : Array[@ddl.Change]
  /// `None` when the schema already matches the last snapshot.
  migration : Migration?
  /// The journal as it will be after the migration is written.
  journal : Journal
} derive(Debug, Eq)

///|
/// Work out the next migration from the two snapshots around it.
///
/// `name` overrides the generated slug; without one the migration is named
/// after its first change, so the migrations directory reads as a changelog.
pub fn plan(
  journal~ : Journal,
  previous~ : @ddl.Snapshot,
  next~ : @ddl.Snapshot,
  now~ : Int64,
  name? : String,
) -> Plan raise Error {
  let changes = @ddl.diff(previous, next)
  if changes.is_empty() {
    return { changes, migration: None, journal }
  }
  let idx = journal.next_index()
  let tag = "\{pad_index(idx)}_\{name.unwrap_or(@ddl.auto_name(changes))}"
  let sql = @ddl.migration_sql(@ddl.statements(previous, next))
  let entries = journal.entries.copy()
  entries.push({ idx, tag, when_: now, breakpoints: true })
  {
    changes,
    migration: Some({ idx, tag, sql, snapshot: next.to_json_string() }),
    journal: { ..journal, entries, },
  }
}

///|
/// Parse every schema source and lower them all into one snapshot.
///
/// Sources are `(name, contents)` pairs; the name only ever shows up in a
/// parse error, so the caller decides whether that is a path or something
/// else.
pub fn snapshot_of_sources(
  sources : Array[(String, String)],
  dialect~ : @aya.Dialect,
) -> @ddl.Snapshot raise Error {
  let specs = []
  for src in sources {
    let (name, text) = src
    for spec in @gen.entities_of(text, name~) {
      specs.push(spec)
    }
  }
  @ddl.snapshot_of(specs, dialect~)
}

///|
/// A four-digit migration index: `12` -> `0012`.
///
/// Fixed width so the directory sorts the way the migrations run, which is the
/// order anyone reading it will assume anyway.
pub fn pad_index(idx : Int) -> String {
  let s = idx.to_string()
  if s.length() >= 4 {
    s
  } else {
    "0".repeat(4 - s.length()) + s
  }
}

///|
pub fn Config::meta_dir(self : Config) -> String {
  self.out + "/meta"
}

///|
pub fn Config::journal_path(self : Config) -> String {
  self.meta_dir() + "/_journal.json"
}

///|
pub fn Config::migration_path(self : Config, tag : String) -> String {
  self.out + "/" + tag + ".sql"
}

///|
pub fn Config::snapshot_path(self : Config, idx : Int) -> String {
  self.meta_dir() + "/" + pad_index(idx) + "_snapshot.json"
}

///|
/// A short report of what a plan would change, one line per change.
pub fn describe(changes : Array[@ddl.Change]) -> String {
  if changes.is_empty() {
    "no schema changes"
  } else {
    changes.map(c => "  " + c.summary()).join("\n")
  }
}