///|
pub(all) struct EvidenceSourceSpec {
  artifact_ref : String
  producer_product_id : String
  contract_id : String
  claim_class : String
  result_id : String
} derive(Debug, Eq, ToJson)

///|
pub(all) struct EvidenceBundleSpec {
  contract_id : String
  bundle_id : String
  sources : Array[EvidenceSourceSpec]
} derive(Debug, Eq, ToJson)

///|
pub(all) struct EvidenceBundleEntry {
  artifact_ref : String
  object_ref : String
  producer_product_id : String
  contract_id : String
  claim_class : String
  result_id : String
  sha256 : String
  size_bytes : Int
} derive(Debug, Eq, ToJson)

///|
pub(all) struct EvidenceBundleManifest {
  contract_id : String
  bundle_id : String
  content_digest : String
  entries : Array[EvidenceBundleEntry]
  recorded_at : String
} derive(Debug, Eq, ToJson)

///|
fn decode_evidence_source(value : Json) -> EvidenceSourceSpec raise {
  let fields = wire_object(value, "MoonFlow evidence source")
  {
    artifact_ref: wire_string(fields, "artifact_ref"),
    producer_product_id: wire_string(fields, "producer_product_id"),
    contract_id: wire_string(fields, "contract_id"),
    claim_class: wire_string(fields, "claim_class"),
    result_id: wire_string(fields, "result_id"),
  }
}

///|
pub fn decode_evidence_bundle_spec(value : Json) -> EvidenceBundleSpec raise {
  let fields = wire_object(value, "MoonFlow evidence bundle spec")
  guard fields.get("sources") is Some(Array(values)) else {
    fail("MoonFlow evidence bundle spec requires sources")
  }
  let spec = EvidenceBundleSpec::{
    contract_id: wire_string(fields, "contract_id"),
    bundle_id: wire_string(fields, "bundle_id"),
    sources: values.map(value => decode_evidence_source(value)),
  }
  if spec.contract_id != "moonflow.evidence-bundle-request.v1" {
    fail("unsupported evidence bundle request contract")
  }
  if spec.sources.is_empty() {
    fail("evidence bundle requires at least one source")
  }
  for source in spec.sources {
    if !artifact_ref_is_workspace_relative(source.artifact_ref) {
      fail("evidence source must be workspace-relative: \{source.artifact_ref}")
    }
    if claim_class_rank(source.claim_class) is None {
      fail("unsupported evidence claim: \{source.claim_class}")
    }
  }
  spec
}

///|
pub fn EvidenceBundleManifest::quality_issues(
  self : EvidenceBundleManifest,
) -> Array[String] {
  let issues : Array[String] = []
  if self.contract_id != "moonflow.evidence-bundle.v1" {
    issues.push("unsupported evidence bundle contract")
  }
  if self.bundle_id.trim().is_empty() {
    issues.push("bundle_id is required")
  }
  if !self.content_digest.has_prefix("sha256:") ||
    self.content_digest.length() != 71 {
    issues.push("content_digest must be a full sha256 digest")
  }
  if self.entries.is_empty() {
    issues.push("evidence bundle entries are required")
  }
  if self.recorded_at.trim().is_empty() {
    issues.push("recorded_at is required")
  }
  let seen : Array[String] = []
  for entry in self.entries {
    if !artifact_ref_is_workspace_relative(entry.artifact_ref) ||
      !artifact_ref_is_workspace_relative(entry.object_ref) {
      issues.push("bundle entry paths must be workspace-relative")
    }
    if seen.contains(entry.artifact_ref) {
      issues.push("duplicate evidence source: \{entry.artifact_ref}")
    } else {
      seen.push(entry.artifact_ref)
    }
    if !entry.sha256.has_prefix("sha256:") || entry.sha256.length() != 71 {
      issues.push("entry sha256 must be a full digest: \{entry.artifact_ref}")
    }
    if entry.size_bytes < 0 {
      issues.push("entry size cannot be negative")
    }
    if claim_class_rank(entry.claim_class) is None {
      issues.push("unsupported entry claim: \{entry.claim_class}")
    }
  }
  issues
}

///|
pub fn evidence_bundle_manifest(
  spec : EvidenceBundleSpec,
  entries : Array[EvidenceBundleEntry],
  content_digest : String,
  recorded_at : String,
) -> EvidenceBundleManifest raise {
  if entries.length() != spec.sources.length() {
    fail("every evidence source must produce one content-addressed entry")
  }
  let manifest = EvidenceBundleManifest::{
    contract_id: "moonflow.evidence-bundle.v1",
    bundle_id: spec.bundle_id,
    content_digest,
    entries,
    recorded_at,
  }
  let issues = manifest.quality_issues()
  if !issues.is_empty() {
    fail("invalid evidence bundle: \{issues.join("; ")}")
  }
  manifest
}