///|
pub(all) struct ProvenanceOptions {
  builder_id : String
  build_type : String
  source_uri : String
  revision : String
  subject_digest : String
  invocation_id : String
  started_on : String
  finished_on : String
  reproducible : Bool
} derive(Eq, Debug)

///|
pub fn default_provenance_options() -> ProvenanceOptions {
  {
    builder_id: "https://github.com/liyun6666/moonseal",
    build_type: "https://moonbitlang.com/build/v1",
    source_uri: "",
    revision: "",
    subject_digest: "",
    invocation_id: "",
    started_on: "",
    finished_on: "",
    reproducible: true,
  }
}

///|
fn provenance_subject(project : Project, digest : String) -> Json {
  let digest_value : Json = if digest.is_empty() {
    {}
  } else {
    { "sha256": digest }
  }
  {
    "name": if project.version.is_empty() {
      "pkg:mooncakes/\{project.name}"
    } else {
      "pkg:mooncakes/\{project.name}@\{project.version}"
    },
    "digest": digest_value,
  }
}

///|
fn provenance_external_parameters(project : Project) -> Json {
  {
    "module": project.name,
    "version": project.version,
    "license": normalize_license(project.license),
    "target": "wasm",
  }
}

///|
fn provenance_internal_parameters() -> Json {
  {
    "commands": [
      "moon update", "moon check --target wasm", "moon test --target wasm", "moon build --target wasm --release",
    ],
  }
}

///|
fn provenance_source(options : ProvenanceOptions) -> Json {
  {
    "uri": options.source_uri,
    "digest": if options.revision.is_empty() {
      {}
    } else {
      { "gitCommit": options.revision }
    },
  }
}

///|
fn provenance_dependency_material(dependency : Dependency) -> Json {
  { "uri": dependency_purl(dependency), "digest": {} }
}

///|
fn provenance_dependencies(project : Project) -> Array[Json] {
  project.dependencies.map(provenance_dependency_material)
}

///|
fn provenance_metadata(options : ProvenanceOptions) -> Json {
  {
    "invocationId": options.invocation_id,
    "startedOn": options.started_on,
    "finishedOn": options.finished_on,
  }
}

///|
/// Create an in-toto Statement carrying a SLSA Provenance v1 predicate. The
/// caller supplies revision and digest values because the portable core does
/// not read Git state or hash build artifacts.
pub fn provenance(project : Project, options : ProvenanceOptions) -> Json {
  let subjects : Array[Json] = [
    provenance_subject(project, options.subject_digest),
  ]
  let dependencies = provenance_dependencies(project)
  {
    "_type": "https://in-toto.io/Statement/v1",
    "subject": subjects,
    "predicateType": "https://slsa.dev/provenance/v1",
    "predicate": {
      "buildDefinition": {
        "buildType": options.build_type,
        "externalParameters": provenance_external_parameters(project),
        "internalParameters": provenance_internal_parameters(),
        "resolvedDependencies": [provenance_source(options), ..dependencies],
      },
      "runDetails": {
        "builder": { "id": options.builder_id },
        "metadata": provenance_metadata(options),
        "byproducts": [],
      },
      "moonseal": {
        "reproducible": options.reproducible,
        "repository": project.repository,
        "dependencyCount": project.dependencies.length(),
      },
    },
  }
}

///|
pub fn render_provenance(
  project : Project,
  options : ProvenanceOptions,
  indent? : Int = 2,
) -> String {
  provenance(project, options).stringify(indent~)
}

///|
/// Validate data required for a useful, publishable provenance statement.
pub fn provenance_warnings(
  project : Project,
  options : ProvenanceOptions,
) -> Array[String] {
  let warnings = []
  if project.name.is_empty() {
    warnings.push("Project name is required.")
  }
  if project.version.is_empty() {
    warnings.push("Project version is required.")
  }
  if options.builder_id.is_empty() {
    warnings.push("Builder identity is required.")
  }
  if options.build_type.is_empty() {
    warnings.push("Build type is required.")
  }
  if options.source_uri.is_empty() {
    warnings.push("Source repository URI is required.")
  }
  if options.revision.is_empty() {
    warnings.push("Source revision is required.")
  }
  if options.subject_digest.is_empty() {
    warnings.push("Artifact SHA-256 digest is recommended.")
  } else if !provenance_is_sha256(options.subject_digest) {
    warnings.push("Artifact digest must be 64 hexadecimal characters.")
  }
  if options.invocation_id.is_empty() {
    warnings.push("Build invocation ID is recommended.")
  }
  for dependency in project.dependencies {
    if dependency.version.is_empty() {
      warnings.push(
        "Dependency \{dependency.name} is unpinned in provenance materials.",
      )
    }
  }
  warnings
}

///|
fn provenance_is_hex(char : Char) -> Bool {
  (char >= '0' && char <= '9') ||
  (char >= 'a' && char <= 'f') ||
  (char >= 'A' && char <= 'F')
}

///|
fn provenance_is_sha256(value : StringView) -> Bool {
  guard value.length() == 64 else { return false }
  for char in value {
    guard provenance_is_hex(char) else { return false }
  }
  true
}

///|
pub fn render_provenance_warnings(
  project : Project,
  options : ProvenanceOptions,
) -> String {
  let warnings = provenance_warnings(project, options)
  let out = StringBuilder::new()
  if warnings.is_empty() {
    out.write_string("PROVENANCE READY  Required provenance data is present.\n")
  } else {
    out.write_string("PROVENANCE INCOMPLETE  \{warnings.length()} issue(s)\n\n")
    for warning in warnings {
      out.write_string("WARN  \{warning}\n")
    }
  }
  out.to_string()
}