///|
pub(all) suberror KitError {
  /// `aya.json` could not be read.
  BadConfig(reason~ : String)
  /// `meta/_journal.json` could not be read.
  BadJournal(reason~ : String)
  /// The migrations directory and the config disagree about the dialect.
  DialectMismatch(journal~ : String, config~ : String)
} derive(Debug)

///|
/// Where the project keeps its schema and its migrations.
///
/// A file rather than flags, for the same reason drizzle has one: every
/// command needs the same three answers, and a repository should not depend on
/// each developer typing them the same way.
pub(all) struct Config {
  dialect : @aya.Dialect
  /// Files or directories to scan for `#aya.table` declarations.
  schema : Array[String]
  /// Directory holding the migrations and their snapshots.
  out : String
} derive(Debug, Eq)

///|
/// The name looked for when `--config` is not given.
pub const CONFIG_FILE = "aya.json"

///|
/// What a project gets without a config file.
pub fn Config::new() -> Config {
  { dialect: Sqlite, schema: ["src"], out: "migrations" }
}

///|
/// Read a config file.
///
/// Every key is optional: a project that only overrides `schema` should not
/// have to restate the defaults.
pub fn Config::from_json_string(text : String) -> Config raise KitError {
  let json = @json.parse(text) catch {
    e => raise BadConfig(reason="not valid JSON: \{e}")
  }
  guard json is Object(obj) else {
    raise BadConfig(reason="expected a JSON object")
  }
  let base = Config::new()
  let dialect = match obj.get("dialect") {
    None => base.dialect
    Some(String(s)) =>
      @ddl.dialect_of_name(s) catch {
        e => raise BadConfig(reason=e.to_string())
      }
    Some(_) => raise BadConfig(reason="`dialect` must be a string")
  }
  let schema = match obj.get("schema") {
    None => base.schema
    Some(String(s)) => [s]
    Some(Array(items)) => {
      let paths = []
      for item in items {
        guard item is String(s) else {
          raise BadConfig(reason="`schema` entries must be strings")
        }
        paths.push(s)
      }
      paths
    }
    Some(_) => raise BadConfig(reason="`schema` must be a string or an array")
  }
  let out = match obj.get("out") {
    None => base.out
    Some(String(s)) => s
    Some(_) => raise BadConfig(reason="`out` must be a string")
  }
  { dialect, schema, out }
}

///|
/// Render a config file, for `aya-kit init`.
pub fn Config::to_json_string(self : Config) -> String {
  let schema = self.schema.map(s => "\n    \{quote(s)}").join(",")
  let list = if self.schema.is_empty() { "[]" } else { "[\{schema}\n  ]" }
  "{\n" +
  "  \"dialect\": \{quote(@ddl.dialect_name(self.dialect))},\n" +
  "  \"schema\": \{list},\n" +
  "  \"out\": \{quote(self.out)}\n" +
  "}\n"
}

///|
/// Minimal JSON string quoting, for the two files this package writes.
fn quote(s : String) -> String {
  let buf = StringBuilder()
  buf.write_string("\"")
  for c in s {
    match c {
      '"' => buf.write_string("\\\"")
      '\\' => buf.write_string("\\\\")
      _ => buf.write_char(c)
    }
  }
  buf.write_string("\"")
  buf.to_string()
}

///|
pub impl Show for KitError with fn output(self, logger) {
  logger.write_string(
    match self {
      BadConfig(reason~) => "\{CONFIG_FILE}: \{reason}"
      BadJournal(reason~) => "migration journal: \{reason}"
      DialectMismatch(journal~, config~) =>
        "the migrations directory was written for `\{journal}` but the config says `\{config}`"
    },
  )
}