///|
/// A profile cannot be registered or composed. Conflicts identify both
/// providers. A missing profile names the identity requested by configuration.
pub(all) suberror ProfileError {
  InvalidIdentity(String)
  DuplicateProfile(String)
  MissingProfile(String)
  TargetConflict(name~ : String, existing~ : String, incoming~ : String)
} derive(Eq, Debug)

///|
/// A reusable, named set of extension target registrations.
/// Profile modules construct this value through the same API as applications.
/// Use a module-qualified identity with a version, such as `example/orbit@1`.
/// No profile is installed or activated by constructing it.
pub struct Profile {
  identity : String
  targets : ArrayView[@target.Target]
} derive(Eq, Debug)

///|
/// Validate a profile and copy its target list. Reject an empty identity,
/// duplicate registrations, and attempts to register either standard identity.
/// Standard aliases cannot be constructed as extension identities.
///
/// ```mbt check
/// test {
///   let profile = @dialect.Profile(
///     "example/orbit@1",
///     [@target.Target::extension("orbit")][:],
///   )
///   let dialect = @dialect.Dialect::gleam_1_19().with_profile(profile)
///   assert_eq(dialect.knows_target("orbit"), true)
///   assert_eq(dialect.knows_target("unregistered"), false)
/// }
/// ```
pub fn Profile::Profile(
  identity : String,
  targets : ArrayView[@target.Target],
) -> Profile raise ProfileError {
  if identity.is_empty() {
    raise InvalidIdentity(identity)
  }
  let mut registry = @target.Registry::standard()
  for target in targets {
    let existing = if target is Extension(_) { identity } else { "gleam-1.19" }
    registry = registry.register(target) catch {
      _ =>
        raise TargetConflict(name=target.name(), existing~, incoming=identity)
    }
  }
  let extensions = registry.all().filter(t => t is Extension(_))
  { identity, targets: extensions[:], }
}

///|
/// Compose this dialect with a profile. Preserve the original dialect on
/// success and failure. Compatible profiles commute; a repeated profile or
/// target fails explicitly, with no override and no registration-order winner.
pub fn Dialect::with_profile(
  self : Dialect,
  profile : Profile,
) -> Dialect raise ProfileError {
  if self.profiles.any(p => p.identity == profile.identity) {
    raise DuplicateProfile(profile.identity)
  }
  let mut targets = self.targets
  for target in profile.targets {
    let mut existing = self.name
    for p in self.profiles {
      if p.targets.contains(target) {
        existing = p.identity
      }
    }
    targets = targets.register(target) catch {
      _ =>
        raise TargetConflict(
          name=target.name(),
          existing~,
          incoming=profile.identity,
        )
    }
  }
  let profiles = self.profiles.to_owned()
  profiles.push(profile)
  profiles.sort_by((a, b) => a.identity.compare(b.identity))
  { ..self, targets, profiles: profiles[:], }
}

///|
/// An application-supplied registry of available profiles. It contains values,
/// not module loaders. Standard parsing needs no profile registry.
pub struct Profiles {
  available : ArrayView[Profile]
} derive(Eq, Debug)

///|
/// Copy and validate the available profiles. Reject duplicate identities even
/// when their registrations are identical, so lookup never depends on order.
pub fn Profiles::Profiles(
  available : ArrayView[Profile],
) -> Profiles raise ProfileError {
  let seen : Array[String] = []
  for profile in available {
    if seen.contains(profile.identity) {
      raise DuplicateProfile(profile.identity)
    }
    seen.push(profile.identity)
  }
  { available: available.to_owned()[:], }
}

///|
/// No optional profiles are available. Lookup of any identity fails explicitly.
pub fn Profiles::empty() -> Profiles {
  { available: [][:], }
}

///|
/// Find an available profile by its exact identity. Raise `MissingProfile`
/// instead of loading code, consulting a global registry, or using a fallback.
pub fn Profiles::get(
  self : Profiles,
  identity : String,
) -> Profile raise ProfileError {
  for profile in self.available {
    if profile.identity == identity {
      return profile
    }
  }
  raise MissingProfile(identity)
}

///|
/// Apply named profiles in order to the supplied base dialect. Conflicts and
/// missing identities raise `ProfileError`. The base remains unchanged.
pub fn Profiles::apply(
  self : Profiles,
  base : Dialect,
  identities : ArrayView[String],
) -> Dialect raise ProfileError {
  let mut dialect = base
  for identity in identities {
    dialect = dialect.with_profile(self.get(identity))
  }
  dialect
}