///|
/// One ordinary package import from the consuming `moon.pkg`. `alias` is the
/// name used after `@` in MoonBit source, while `import_path` is the canonical
/// package path materialized under `.mooncakes`.
priv struct PackageImport {
  import_alias : String
  import_path : String
}

///|
fn PackageImport::PackageImport(
  manifest_path : String,
  import_path : String,
  explicit_alias : String?,
) -> PackageImport raise SubclassCommandError {
  guard import_path != "" else {
    raise SubclassCommandError::Failure(
      "\{manifest_path}: package import path must not be empty",
    )
  }
  let import_alias = if explicit_alias is Some(explicit_import_alias) {
    explicit_import_alias
  } else {
    import_path
    .rev_split_once("/")
    .map_or(import_path, pair => pair.1.to_owned())
  }
  guard import_alias != "" else {
    raise SubclassCommandError::Failure(
      "\{manifest_path}: package import '\{import_path}' has no usable alias",
    )
  }
  { import_alias, import_path }
}

///|
fn PackageImport::from_ast(
  manifest_path : String,
  ast : @moon_config.Ast,
) -> PackageImport raise SubclassCommandError {
  match ast {
    @moon_config.Str(import_path, ..) =>
      PackageImport(manifest_path, import_path, None)
    @moon_config.Obj(fields, ..) => {
      let mut import_path : String? = None
      let mut explicit_alias : String? = None
      for field in fields {
        let (key, value) = field
        match (key, value) {
          ("path", @moon_config.Str(path, ..)) => import_path = Some(path)
          ("alias", @moon_config.Str(import_alias, ..)) =>
            explicit_alias = Some(import_alias)
          _ => ()
        }
      }
      guard import_path is Some(import_path) else {
        raise SubclassCommandError::Failure(
          "\{manifest_path}: package import has no string 'path'",
        )
      }
      PackageImport(manifest_path, import_path, explicit_alias)
    }
    _ =>
      raise SubclassCommandError::Failure(
        "\{manifest_path}: ordinary package imports must be strings or path/alias objects",
      )
  }
}

///|
/// In a bindings monorepo, sibling binding modules are conventionally named
/// after the final component of their canonical import path.
fn PackageImport::local_config_path(
  self : PackageImport,
  directory : @path.Path,
) -> @path.Path {
  let binding_directory_name = self.import_path
    .rev_split_once("/")
    .map_or(self.import_path, pair => pair.1.to_owned())
  directory
  .join(@path.Path(binding_directory_name))
  .join(@path.Path("gir.json"))
}

///|
/// Moon materializes a dependency at `.mooncakes/`.
/// Custom source aliases never participate in this filesystem path.
fn PackageImport::materialized_config_path(
  self : PackageImport,
  mooncakes : @path.Path,
) -> @path.Path {
  mooncakes.join(@path.Path(self.import_path)).join(@path.Path("gir.json"))
}

///|
/// The subset of a consuming package manifest needed to resolve the package
/// alias named by `#gobject.subclass(parent=...)`.
priv struct PackageManifest {
  path : String
  imports : Array[PackageImport]
}

///|
fn PackageManifest::parse(
  path : String,
  source : String,
) -> PackageManifest raise SubclassCommandError {
  let (ast, reports) = @moon_config.parse_moon_pkg(source, name=path)
  let diagnostics : Array[String] = []
  for report in reports {
    // moon_config 0.3.9 predates Moon's current executable-package spelling.
    // The parsed AST is still usable, so ignore only that known compatibility
    // diagnostic and continue to reject syntax or unrelated manifest errors.
    if report.msg != "Invalid moon.pkg config: unexpected key `pkgtype`." {
      diagnostics.push(report.to_string())
    }
  }
  if !diagnostics.is_empty() {
    raise SubclassCommandError::Failure(
      "\{path}: MoonBit manifest parser rejected the input\n\{diagnostics.join("\n")}",
    )
  }
  guard ast is @moon_config.Obj(fields, ..) else {
    raise SubclassCommandError::Failure(
      "\{path}: MoonBit manifest parser did not return an object",
    )
  }
  let imports : Array[PackageImport] = []
  for field in fields {
    let (key, value) = field
    if key == "import" {
      guard value is @moon_config.Arr(entries, ..) else {
        raise SubclassCommandError::Failure(
          "\{path}: ordinary package imports are not an array",
        )
      }
      for entry in entries {
        imports.push(PackageImport::from_ast(path, entry))
      }
    }
  }
  { path, imports }
}

///|
/// Resolve the source-level alias to the canonical package import path. The
/// generator must not confuse a custom alias with a `.mooncakes` directory.
fn PackageManifest::resolve_import(
  self : PackageManifest,
  import_alias : String,
) -> PackageImport raise SubclassCommandError {
  let mut resolved : PackageImport? = None
  for package_import in self.imports {
    if package_import.import_alias == import_alias {
      if resolved is Some(existing) {
        raise SubclassCommandError::Failure(
          "\{self.path}: @\{import_alias} refers to both '\{existing.import_path}' and '\{package_import.import_path}'",
        )
      }
      resolved = Some(package_import)
    }
  }
  if resolved is Some(package_import) {
    return package_import
  }
  raise SubclassCommandError::Failure(
    "\{self.path}: @\{import_alias} is not an ordinary package import",
  )
}

///|
/// A MoonBit source file belongs to the package whose `moon.pkg` is in the
/// same directory. All inputs in one invocation must share that package.
async fn SubclassCommandOptions::load_package_manifest(
  self : SubclassCommandOptions,
) -> PackageManifest raise SubclassCommandError {
  guard self.inputs.get(0) is Some(first) else {
    raise SubclassCommandError::Failure("at least one input file is required")
  }
  let package_directory = @path.Path(first).resolve().dirname()
  for input in self.inputs[1:] {
    if @path.Path(input).resolve().dirname() != package_directory {
      raise SubclassCommandError::Failure(
        "input files belong to different MoonBit packages; pass one package per invocation",
      )
    }
  }
  let manifest_path = package_directory.join(@path.Path("moon.pkg"))
  if !self.path_exists(manifest_path) {
    raise SubclassCommandError::Failure(
      "cannot resolve the parent import because \{manifest_path.0} does not exist; pass --config",
    )
  }
  let source = self.read_text(manifest_path.0)
  PackageManifest::parse(manifest_path.0, source)
}