///|
#derive(Eq, Debug)
pub(all) enum BuildPolicy {
  SourceOnly
  Required(String)
}

///|
pub extend BuildPolicy with Eq::{equal, not_equal}

///|
pub extend BuildPolicy with @debug.Debug::{to_repr}

///|
pub(all) struct ProjectDependency {
  module_name : String
  version : String
  api_profile : String
  semantic_pin : String
}

///|
pub(all) struct GeneratedFile {
  path : String
  content : @data.Content
  format : String
}

///|
pub(all) struct GeneratedProject {
  id : String
  root : String
  members : Array[String]
  dependencies : Array[ProjectDependency]
  build_contract : String
}

///|
pub(all) struct GenerationBatch {
  warnings : Array[String]
  structured_warnings : Array[@diagnostics.Warning]
  files : Array[GeneratedFile]
  projects : Array[GeneratedProject]
}

///|
/// `targets` lists the build targets of `pipeline.validation = "required"`. An empty list means source-only.
pub(all) struct GenerationBackend {
  generate : (@data.IRUnit, Json, () -> Bool) -> GenerationBatch raise
  write_bytes : Bool
  validate_options : (Json) -> Unit raise
  targets : Array[String]
}

///|
pub(all) struct LibraryProject {
  id : String
  source : String
  root : String
  members : Array[String]
  dependencies : Array[ProjectDependency]
  build_contract : String
  policy : BuildPolicy
}

///|
pub fn Engine::register_generation_backend(
  self : Engine,
  name : String,
  backend : GenerationBackend,
) -> Unit raise {
  if name == "" ||
    name == "checkpoint" ||
    self.backends.contains(name) ||
    self.generators.contains(name) {
    raise @configuration.Invalid("generation.backend_conflict: " + name)
  }
  self.generators[name] = backend
}

///|
/// A generation backend refused the unit. Each problem names the IR node.
pub(all) suberror GenerationFailed {
  GenerationFailed(Array[GenerationProblem])
}

///|
fn elm_problems(problems : Array[@elm.Problem]) -> GenerationFailed {
  GenerationFailed(
    problems.map(p => {
      code: p.code,
      node_path: p.node_path,
      detail: p.detail,
    }),
  )
}

///|
fn moonbit_generation(
  unit : @data.IRUnit,
  options : Json,
  cancelled : () -> Bool,
  metadata_capabilities : @linked_metadata.Capabilities,
) -> GenerationBatch raise {
  let result = @moonbit.generate(
    {
      unit_id: unit.id,
      ir: unit.distribution,
      metadata: unit.metadata.map(m => { node_id: m.node_id, data: m.data, }),
      annotations: unit.annotations,
      provenance: unit.provenance,
    },
    naming=moonbit_naming_style(options),
    metadata_capabilities~,
    cancelled~,
  ) catch {
    @moonbit.Invalid(code, node_path, detail) =>
      raise GenerationFailed([{ code, node_path, detail, }])
    error => raise error
  }
  {
    warnings: [],
    structured_warnings: result.warnings,
    files: result.artifacts.map(a => {
      path: a.path,
      content: match a.content {
        Text(t) => @data.Text(t)
        Binary(b) => @data.Binary(b)
      },
      format: match a.role {
        Source => "moonbit-source"
        ModuleManifest | PackageManifest => "moonbit-manifest"
        Metadata => "ion-binary"
      },
    }),
    projects: [
      {
        id: result.project.module_name,
        root: result.project.root,
        members: result.project.members,
        dependencies: result.project.dependencies.map(d => {
          module_name: d.module_name,
          version: d.version,
          api_profile: d.api_profile,
          semantic_pin: d.semantic_pin,
        }),
        build_contract: "moonbit-library-v1",
      },
    ],
  }
}

///|
fn elm_generation(
  unit : @data.IRUnit,
  options : Json,
  cancelled : () -> Bool,
) -> GenerationBatch raise {
  let output = @elm.generate(
    unit.id,
    unit.distribution,
    @elm.Options::from_json(options),
    cancelled~,
  ) catch {
    Failed(problems) => raise elm_problems(problems)
    Refused(p) => raise elm_problems([p])
  }
  {
    warnings: output.warnings,
    structured_warnings: [],
    files: output.files.map(f => {
      path: f.path,
      content: @data.Text(f.content),
      format: match f.role {
        Source => "elm-source"
        ElmManifest => "elm-manifest"
        MorphirManifest => "morphir-manifest"
      },
    }),
    projects: [
      {
        id: output.package_name,
        root: "",
        members: output.files.map(f => f.path),
        dependencies: output.dependencies.map(d => {
          module_name: d.name,
          version: d.range,
          api_profile: "elm-package",
          semantic_pin: "",
        }),
        build_contract: "elm-package-v1",
      },
    ],
  }
}

///|
/// The elm backend reports option problems as configuration errors, so the engine needs no Elm types.
fn elm_validate_options(options : Json) -> Unit raise {
  @elm.validate_options(options) catch {
    @elm.ElmGenerationError::Refused(p) =>
      raise @configuration.Invalid(
        p.code + " at " + p.node_path + ": " + p.detail,
      )
    @elm.ElmGenerationError::Failed(problems) =>
      raise @configuration.Invalid(
        problems
        .map(p => p.code + " at " + p.node_path + ": " + p.detail)
        .join("\n"),
      )
    other => raise other
  }
}

///|
/// The naming option selects a documented generation policy; other options remain ignored.
fn moonbit_naming_style(options : Json) -> @moonbit.NamingStyle raise {
  guard options is Object(fields) else {
    raise @configuration.Invalid("backends.moonbit must be a table")
  }
  match fields.get("naming") {
    None | Some(String("readable")) => @moonbit.Readable
    Some(String("legacy-hex")) => @moonbit.LegacyHex
    _ =>
      raise @configuration.Invalid(
        "backends.moonbit.naming must be 'readable' or 'legacy-hex'",
      )
  }
}

///|
fn moonbit_validate_options(options : Json) -> Unit raise {
  ignore(moonbit_naming_style(options))
}

///|
fn portable_path(path : String, allow_empty~ : Bool = false) -> Unit raise {
  if (path == "" && !allow_empty) ||
    normalize(path) != path ||
    path.contains(":") ||
    path.contains("\\") ||
    path.contains("\u0000") {
    raise @configuration.Invalid("generation.invalid_path: " + path)
  }
  for part in path.split("/") {
    if part.has_suffix(".") ||
      part.has_suffix(" ") ||
      part.to_owned().to_array().any(c => c.to_int() < 32 || c.to_int() > 126) {
      raise @configuration.Invalid("generation.nonportable_path: " + path)
    }
  }
}

///|
fn validate_batch(
  batch : GenerationBatch,
  base : String,
  source : String,
  policy : BuildPolicy,
  artifacts : Array[ContentArtifact],
  projects : Array[LibraryProject],
) -> Unit raise {
  let seen = Map([])
  for file in batch.files {
    portable_path(file.path)
    let key = file.path.to_lower()
    if seen.contains(key) ||
      seen
      .keys()
      .any(existing => {
        @workspace.within(existing, key) || @workspace.within(key, existing)
      }) {
      raise @configuration.Invalid("generation.path_collision: " + file.path)
    }
    seen[key] = true
  }
  let roots = []
  let members = Map([])
  let ids = Map([])
  for project in batch.projects {
    portable_path(project.root, allow_empty=true)
    if project.id == "" ||
      ids.contains(project.id) ||
      project.build_contract == "" {
      raise @configuration.Invalid("generation.invalid_project: " + project.id)
    }
    ids[project.id] = true
    let key = project.root.to_lower()
    if roots.any(r => @workspace.within(r, key) || @workspace.within(key, r)) {
      raise @configuration.Invalid(
        "generation.project_overlap: " + project.root,
      )
    }
    roots.push(key)
    if project.members.is_empty() {
      raise @configuration.Invalid("generation.empty_project: " + project.id)
    }
    for member_path in project.members {
      portable_path(member_path)
      let path = join(project.root, member_path)
      if !batch.files.any(f => f.path == path) || members.contains(path) {
        raise @configuration.Invalid("generation.invalid_membership: " + path)
      }
      members[path] = true
    }
  }
  if members.length() != batch.files.length() || batch.projects.is_empty() {
    raise @configuration.Invalid("generation.unclaimed_artifact")
  }
  for file in batch.files {
    artifacts.push({
      path: join(base, file.path),
      source,
      content: file.content,
      format: file.format,
    })
  }
  for project in batch.projects {
    projects.push({
      id: project.id,
      source,
      root: join(base, project.root),
      members: project.members.map(m => join(join(base, project.root), m)),
      dependencies: project.dependencies.copy(),
      build_contract: project.build_contract,
      policy,
    })
  }
}