///|
pub(all) struct ProjectInfo {
  name : String
  relative_path : String
  frontend : String?
  target : String?
  error : String?
}

///|
pub fn ProjectInfo::to_json(self : ProjectInfo) -> Json {
  fn optional(value : String?) -> Json {
    match value {
      Some(value) => Json::string(value)
      None => Json::null()
    }
  }
  Json::object(
    Map::from_array([
      ("name", Json::string(self.name)),
      ("relativePath", Json::string(self.relative_path)),
      ("frontend", optional(self.frontend)),
      ("target", optional(self.target)),
      ("error", optional(self.error)),
    ]),
  )
}

///|
fn validate_project_configuration(
  workspace : @workspace.Workspace,
  project : @workspace.Project,
) -> @version.FormatVersion raise {
  if project.error is Some(message) {
    raise @configuration.Invalid(message)
  }
  let effective = project.document
  effective.validate()
  if effective.boolean("ir.strict_mode", false) &&
    !workspace.warnings.is_empty() {
    raise @configuration.Invalid(
      "Strict configuration rejects warnings: " + workspace.warnings.join("; "),
    )
  }
  if effective.get("ir.mode") is Some(_) &&
    effective.provenance.get("ir.layout") == Some("defaults") {
    if effective.text("ir.mode", "classic") != "classic" {
      raise @configuration.Invalid(
        "Only ir.mode = classic is supported; prefer ir.layout = single-file",
      )
    }
  }
  if effective.text("ir.layout", "single-file") != "single-file" ||
    effective.text("ir.format", "json") != "json" {
    raise @configuration.Invalid(
      "Only single-file JSON IR output is currently supported",
    )
  }
  let version = match effective.json("ir.format_version") {
    Some(Number(n, ..)) if n >= 1.0 && n <= 4.0 && n.to_int().to_double() == n =>
      @version.FormatVersion::baseline(n.to_int().reinterpret_as_uint())
    None => @version.FormatVersion::baseline(4U)
    _ =>
      raise @configuration.Invalid(
        "Supported IR format versions are integers 1 through 4",
      )
  }
  version
}

///|
fn Engine::resolve_project_config(
  self : Engine,
  host : Host,
  workspace : @workspace.Workspace,
  project : @workspace.Project,
  script : String,
  overlay : @configuration.Document,
  cancelled : () -> Bool,
  bind_runtime : (@scheme.Runtime) -> Unit raise,
) -> (Config, @scheme.Runtime, @version.FormatVersion) raise {
  if cancelled() {
    raise @configuration.Invalid("Cancelled")
  }
  let version = validate_project_configuration(workspace, {
    ..project,
    document: @configuration.merge(project.document, overlay),
  })
  let runtime = config_runtime(self.limits)
  runtime.set_cancelled(cancelled)
  bind_runtime(runtime)
  if cancelled() {
    raise @configuration.Invalid("Cancelled")
  }
  let settings = Config::{
    frontend: "scheme",
    frontend_provider: None,
    frontend_profile: None,
    backend: "ir-json",
    output: "",
    package_name: project.name,
    source: if project.anchor == "" {
      ""
    } else {
      "src"
    },
    inference: "off",
    transforms: [],
  }
  settings.overlay(project.document)
  if project.anchor == "" &&
    project.document.provenance.get("project.source_directory") ==
    Some("defaults") {
    settings.source = ""
  }
  let root_script = join(workspace.root, "morphir.config.scm")
  let project_script = join(project.root, "morphir.config.scm")
  for
    path in (if root_script == project_script {
      [root_script]
    } else {
      [root_script, project_script]
    }) {
    if (host.exists)(path) {
      settings.apply(runtime, (host.read)(path), origin=path)
    }
  }
  if script != "" {
    settings.apply(runtime, script)
  }
  if cancelled() {
    raise @configuration.Invalid("Cancelled")
  }
  settings.overlay(overlay)
  (settings, runtime, version)
}

///|
/// Resolve language choices through the host without compiling or reading sources.
/// Unknown frontend/backend names remain visible for external tooling.
pub fn Engine::list_projects(
  self : Engine,
  host : Host,
  workspace : @workspace.Workspace,
  script~ : String = "",
  cancelled~ : () -> Bool = fn() { false },
  bind_runtime~ : (@scheme.Runtime) -> Unit raise = fn(_) { () },
) -> Array[ProjectInfo] raise {
  workspace.projects.map(fn(project) raise {
    if cancelled() {
      raise @configuration.Invalid("Cancelled")
    }
    let path = if project.root == "" { "." } else { project.root }
    try {
      let (settings, _, _) = self.resolve_project_config(
        host,
        workspace,
        project,
        script,
        @configuration.empty(),
        cancelled,
        bind_runtime,
      )
      {
        name: project.name,
        relative_path: path,
        frontend: Some(settings.frontend),
        target: Some(settings.backend),
        error: None,
      }
    } catch {
      @scheme.Limit("cancelled") => raise @configuration.Invalid("Cancelled")
      @configuration.Invalid("Cancelled") =>
        raise @configuration.Invalid("Cancelled")
      error => {
        if cancelled() {
          raise @configuration.Invalid("Cancelled")
        }
        {
          name: project.name,
          relative_path: path,
          frontend: None,
          target: None,
          error: Some(
            match error {
              @configuration.Invalid(message)
              | @scheme.Invalid(message)
              | @scheme.Unsupported(message)
              | @scheme.Unbound(message)
              | @scheme.Limit(message) => message
              @scheme.Syntax(message, position) =>
                message + " at " + position.to_string()
              @scheme.Stage(stage, message) => stage + ": " + message
              _ => error.to_string()
            },
          ),
        }
      }
    }
  })
}