///|
/// Legacy local discovery or explicitly selected resources in a local container.
pub(all) enum BuildInput {
  Npm(@resources.NpmArchive, @resources.Selection)
  Maven(@resources.MavenArchive, @resources.Selection?)
  GitHub(@resources.GitHubArchive, @resources.Selection, @resources.HttpUrl)
  Remote(@resources.PublicArchive, @resources.Selection)
  Local(@resources.RepositoryLocation)
  Selected(@resources.RepositoryLocation, @resources.Selection)
} derive(Eq, Debug)

///|
pub fn BuildInput::from_path(path : String) -> BuildInput raise BuildError {
  guard @resources.RepositoryLocation::parse(path) is Some(location) else {
    raise InvalidBuildConfig("Expected a nonempty local input location")
  }
  Local(location)
}

///|
pub fn BuildInput::location(self : BuildInput) -> @resources.RepositoryLocation {
  match self {
    Npm(archive, _) =>
      @resources.RepositoryLocation::parse(archive.registry.to_string()).unwrap()
    Maven(archive, _) =>
      @resources.RepositoryLocation::parse(archive.repository.to_string()).unwrap()
    GitHub(archive, _, _) =>
      @resources.RepositoryLocation::parse(archive.repository.to_string()).unwrap()
    Remote(archive, _) =>
      @resources.RepositoryLocation::parse(archive.url.to_string()).unwrap()
    Local(location) | Selected(location, _) => location
  }
}

///|
pub fn BuildInput::with_location(
  self : BuildInput,
  location : @resources.RepositoryLocation,
) -> BuildInput raise BuildError {
  match self {
    Npm(archive, selection) => {
      guard @resources.RepositoryUrl::parse(location.to_string())
        is Some(registry) else {
        raise InvalidBuildConfig("Expected an npm registry base URL")
      }
      Npm({ ..archive, registry, }, selection)
    }
    Maven(archive, selection) => {
      guard @resources.RepositoryUrl::parse(location.to_string())
        is Some(repository) else {
        raise InvalidBuildConfig("Expected a Maven repository base URL")
      }
      Maven({ ..archive, repository, }, selection)
    }
    GitHub(archive, selection, api) => {
      guard @resources.GitHubRepository::parse(location.to_string())
        is Some(repository) else {
        raise InvalidBuildConfig("Expected GitHub repository owner/name")
      }
      GitHub({ ..archive, repository, }, selection, api)
    }
    Remote(archive, selection) => {
      guard @resources.HttpUrl::parse(location.to_string()) is Some(url) else {
        raise InvalidBuildConfig(
          "Expected an absolute credential-free HTTP(S) URL",
        )
      }
      Remote({ ..archive, url, }, selection)
    }
    Local(_) => Local(location)
    Selected(_, selection) => Selected(location, selection)
  }
}

///|
pub fn BuildInput::to_json(self : BuildInput) -> Json {
  match self {
    Npm(archive, selection) => {
      let json = Selected(self.location(), selection).to_json()
      guard json is Object(fields) else { abort("selection serialization") }
      ignore(fields.remove("path"))
      fields["npm"] = {
        "registry": archive.registry.to_string(),
        "package": archive.reference.name.to_string(),
        "version": archive.reference.version.to_string(),
      }
      if archive.expected_digest is Some(digest) {
        fields["digest"] = Json::string(digest.to_string())
      }
      Json::object(fields)
    }
    Maven(archive, selection) => {
      let fields = match selection {
        Some(selection) => {
          let json = Selected(self.location(), selection).to_json()
          guard json is Object(fields) else { abort("selection serialization") }
          ignore(fields.remove("path"))
          fields
        }
        None => Map([])
      }
      let coordinates = archive.coordinates
      let maven : Map[String, Json] = {
        "repository": Json::string(archive.repository.to_string()),
        "group": Json::string(coordinates.group.to_string()),
        "artifact": Json::string(coordinates.artifact.to_string()),
        "version": Json::string(coordinates.version.to_string()),
        "checksum": Json::string(archive.checksum.algorithm().to_string()),
      }
      if coordinates.classifier is Some(classifier) {
        maven["classifier"] = Json::string(classifier.to_string())
      }
      fields["maven"] = Json::object(maven)
      if archive.expected_digest is Some(digest) {
        fields["digest"] = Json::string(digest.to_string())
      }
      Json::object(fields)
    }
    GitHub(archive, selection, api) => {
      let json = Remote(
        {
          url: api,
          format: archive.format,
          expected_digest: archive.expected_digest,
        },
        selection,
      ).to_json()
      guard json is Object(fields) else { abort("selection serialization") }
      ignore(fields.remove("url"))
      fields["github"] = {
        "repository": archive.repository.to_string(),
        "tag": archive.reference.tag.to_string(),
        "asset": archive.reference.asset.to_string(),
        "api": api.to_string(),
      }
      Json::object(fields)
    }
    Remote(archive, selection) => {
      let json = Selected(self.location(), selection).to_json()
      guard json is Object(fields) else { abort("selection serialization") }
      ignore(fields.remove("path"))
      fields["url"] = Json::string(archive.url.to_string())
      fields["format"] = Json::string(
        match archive.format {
          @resources.Zip => "zip"
          @resources.Tar => "tar"
          @resources.Tgz => "tgz"
        },
      )
      if archive.expected_digest is Some(digest) {
        fields["digest"] = Json::string(digest.to_string())
      }
      Json::object(fields)
    }
    Local(location) => Json::string(location.to_string())
    Selected(location, selection) => {
      let paths = []
      let globs = []
      for selector in selection.selectors() {
        match selector {
          @resources.Exact(path) => paths.push(Json::string(path.to_string()))
          @resources.Named(path, name) =>
            paths.push(
              Json::object({
                "path": Json::string(path.to_string()),
                "name": Json::string(name.to_string()),
              }),
            )
          @resources.Matching(pattern) =>
            globs.push(Json::string(pattern.to_string()))
        }
      }
      Json::object({
        "path": Json::string(location.to_string()),
        "root": Json::string(selection.root.to_string()),
        "paths": Json::array(paths),
        "globs": Json::array(globs),
      })
    }
  }
}

///|
fn parse_entry_path(text : String) -> @resources.EntryPath raise BuildError {
  @resources.EntryPath::parse(text).unwrap_or_else(() => {
    raise InvalidBuildConfig("Unsafe resource path `" + text + "`")
  })
}

///|
fn config_selection(value : Json) -> @resources.Selection raise BuildError {
  guard config_optional_string(value, "root") is Some(root) &&
    @resources.ResourceRoot::parse(root) is Some(root) else {
    raise InvalidBuildConfig(
      "Expected a safe resource `root`, using `.` for the container root",
    )
  }
  let selectors = []
  match config_field(value, "paths") {
    None => ()
    Some(Array(paths)) =>
      for path in paths {
        match path {
          String(path) =>
            selectors.push(@resources.Exact(parse_entry_path(path)))
          Object(fields) => {
            if fields.length() != 2 {
              raise InvalidBuildConfig(
                "A named resource path requires only `path` and `name`",
              )
            }
            guard config_optional_string(path, "path") is Some(entry) &&
              config_optional_string(path, "name") is Some(name) &&
              @resources.ResourceName::parse(name) is Some(name) else {
              raise InvalidBuildConfig(
                "Expected a safe named resource `path` and `name`",
              )
            }
            selectors.push(@resources.Named(parse_entry_path(entry), name))
          }
          _ =>
            raise InvalidBuildConfig(
              "Expected resource paths to be strings or named path objects",
            )
        }
      }
    _ => raise InvalidBuildConfig("Expected resource `paths` to be an array")
  }
  for glob in config_strings(value, "globs") {
    guard @resources.GlobPattern::parse(glob) is Some(pattern) else {
      raise InvalidBuildConfig("Invalid resource glob `" + glob + "`")
    }
    selectors.push(@resources.Matching(pattern))
  }
  guard @resources.Selection::new(root, selectors) is Some(selection) else {
    raise InvalidBuildConfig(
      "A resource selection requires between 1 and 256 paths or globs",
    )
  }
  selection
}

///|
fn config_digest(value : Json) -> @resources.ContentDigest? raise BuildError {
  match config_optional_string(value, "digest") {
    None => None
    Some(text) => {
      guard @resources.ContentDigest::parse(text) is Some(digest) else {
        raise InvalidBuildConfig(
          "Expected digest sha256: followed by 64 hexadecimal digits",
        )
      }
      Some(digest)
    }
  }
}

///|
fn config_repository_input(
  value : Json,
  provider : String,
) -> BuildInput raise BuildError {
  guard value is Object(fields) &&
    config_field(value, provider) is Some(Object(settings)) else {
    raise InvalidBuildConfig("Expected a " + provider + " input object")
  }
  for key, _ in fields {
    if ![provider, "digest", "root", "paths", "globs"].contains(key) {
      raise InvalidBuildConfig(
        "Unknown " + provider + " input field `" + key + "`",
      )
    }
  }
  let nested = Json::object(settings)
  let allowed = if provider == "npm" {
    ["registry", "package", "version"]
  } else {
    ["repository", "group", "artifact", "version", "classifier", "checksum"]
  }
  for key, _ in settings {
    if !allowed.contains(key) {
      raise InvalidBuildConfig(
        "Unknown " + provider + " coordinate field `" + key + "`",
      )
    }
  }
  let expected_digest = config_digest(value)
  if provider == "npm" {
    guard @resources.RepositoryUrl::parse(
        config_optional_string(nested, "registry").unwrap_or(
          "https://registry.npmjs.org",
        ),
      )
      is Some(registry) &&
      config_optional_string(nested, "package") is Some(name) &&
      @resources.NpmPackageName::parse(name) is Some(name) &&
      config_optional_string(nested, "version") is Some(version) &&
      @resources.NpmVersion::parse(version) is Some(version) else {
      raise InvalidBuildConfig(
        "npm requires a public registry base, package name and exact semantic version",
      )
    }
    Npm(
      { registry, reference: { name, version, }, expected_digest, },
      config_selection(value),
    )
  } else {
    guard @resources.RepositoryUrl::parse(
        config_optional_string(nested, "repository").unwrap_or(
          "https://repo.maven.apache.org/maven2",
        ),
      )
      is Some(repository) &&
      config_optional_string(nested, "group") is Some(group) &&
      @resources.MavenGroupId::parse(group) is Some(group) &&
      config_optional_string(nested, "artifact") is Some(artifact) &&
      @resources.MavenArtifactId::parse(artifact) is Some(artifact) &&
      config_optional_string(nested, "version") is Some(version) &&
      @resources.MavenVersion::parse(version) is Some(version) else {
      raise InvalidBuildConfig(
        "Maven requires a public repository base and exact group/artifact/version JAR coordinates",
      )
    }
    let classifier = match config_optional_string(nested, "classifier") {
      None => None
      Some(text) => {
        guard @resources.MavenClassifier::parse(text) is Some(value) else {
          raise InvalidBuildConfig("Invalid Maven classifier")
        }
        Some(value)
      }
    }
    let checksum = match
      config_optional_string(nested, "checksum").unwrap_or("sha1") {
      "sha1" => @resources.Sha1Sidecar
      "sha256" => @resources.Sha256Sidecar
      _ => raise InvalidBuildConfig("Expected Maven checksum sha1 or sha256")
    }
    let selection = if ["root", "paths", "globs"].any(key => {
        fields.contains(key)
      }) {
      Some(config_selection(value))
    } else {
      None
    }
    Maven(
      {
        repository,
        coordinates: { group, artifact, version, classifier, },
        checksum,
        expected_digest,
      },
      selection,
    )
  }
}

///|
fn config_input(value : Json) -> BuildInput raise BuildError {
  if value is String(path) {
    return BuildInput::from_path(path)
  }
  guard value is Object(fields) else {
    raise InvalidBuildConfig(
      "Expected a local input string or resource selection object",
    )
  }
  if fields.contains("npm") {
    return config_repository_input(value, "npm")
  }
  if fields.contains("maven") {
    return config_repository_input(value, "maven")
  }
  let github = fields.contains("github")
  let remote = fields.contains("url") || github
  let allowed = if github {
    ["github", "format", "digest", "root", "paths", "globs"]
  } else if remote {
    ["url", "format", "digest", "root", "paths", "globs"]
  } else {
    ["path", "root", "paths", "globs"]
  }
  for key, _ in fields {
    if !allowed.contains(key) {
      raise InvalidBuildConfig("Unknown resource selection field `" + key + "`")
    }
  }
  let location = if remote {
    None
  } else {
    guard config_optional_string(value, "path") is Some(path) &&
      @resources.RepositoryLocation::parse(path) is Some(location) else {
      raise InvalidBuildConfig("Expected a resource selection `path`")
    }
    Some(location)
  }
  let selection = config_selection(value)
  if remote {
    let format = match
      config_optional_string(value, "format").unwrap_or("zip") {
      "zip" | "jar" => @resources.Zip
      "tar" => @resources.Tar
      "tgz" | "tar.gz" => @resources.Tgz
      _ =>
        raise InvalidBuildConfig(
          "Expected URL archive format zip, jar, tar, tgz or tar.gz",
        )
    }
    let expected_digest = config_digest(value)
    if github {
      guard config_field(value, "github") is Some(Object(github_fields)) else {
        raise InvalidBuildConfig("Expected a github object")
      }
      for key, _ in github_fields {
        if !["repository", "tag", "asset", "api"].contains(key) {
          raise InvalidBuildConfig("Unknown GitHub field `" + key + "`")
        }
      }
      let github_value = Json::object(github_fields)
      guard config_optional_string(github_value, "repository")
        is Some(repository) &&
        @resources.GitHubRepository::parse(repository) is Some(repository) &&
        config_optional_string(github_value, "tag") is Some(tag) &&
        @resources.GitHubTag::parse(tag) is Some(tag) &&
        config_optional_string(github_value, "asset") is Some(asset) &&
        @resources.GitHubAssetName::parse(asset) is Some(asset) else {
        raise InvalidBuildConfig(
          "GitHub requires repository owner/name, an explicit tag and an exact asset name",
        )
      }
      guard @resources.HttpUrl::parse(
          config_optional_string(github_value, "api").unwrap_or(
            "https://api.github.com",
          ),
        )
        is Some(api) &&
        !api.to_string().contains("?") else {
        raise InvalidBuildConfig(
          "Expected a credential-free GitHub API base URL without query or fragment",
        )
      }
      GitHub(
        { repository, reference: { tag, asset, }, format, expected_digest, },
        selection,
        api,
      )
    } else {
      guard config_optional_string(value, "url") is Some(text) &&
        @resources.HttpUrl::parse(text) is Some(url) else {
        raise InvalidBuildConfig(
          "Expected an absolute credential-free HTTP(S) URL without a fragment",
        )
      }
      Remote({ url, format, expected_digest, }, selection)
    }
  } else {
    Selected(location.unwrap(), selection)
  }
}

///|
fn config_inputs(
  value : Json,
  key : String,
) -> Array[BuildInput] raise BuildError {
  match config_field(value, key) {
    None => []
    Some(Array(values)) => values.map(config_input)
    _ => raise InvalidBuildConfig("Expected `" + key + "` to be an array")
  }
}

///|
/// Smithy accepts only selected model resources; generic selectors stay format-independent.
pub fn selected_models(
  tree : @resources.EntryTree,
  selection : @resources.Selection,
) -> Array[@resources.Resource] raise BuildError {
  let loaded = @resources.select_resources(tree, selection) catch {
    @resources.ResourceError(code, at, message) =>
      raise InvalidBuildConfig(
        "Could not load resource `" +
        at.to_string() +
        "` [" +
        code.to_string() +
        "]: " +
        message,
      )
  }
  for resource in loaded {
    let path = resource.path.to_string()
    let name = resource.name.to_string()
    if (!path.has_suffix(".smithy") && !path.has_suffix(".json")) ||
      path.has_suffix(".smithy") != name.has_suffix(".smithy") ||
      path.has_suffix(".json") != name.has_suffix(".json") {
      raise InvalidBuildConfig(
        "Selected Smithy resource `" +
        resource.provenance.to_string() +
        "` must retain its .smithy or .json format in logical name `" +
        name +
        "`",
      )
    }
  }
  loaded
}