///|
/// A dependency record used to audit an SBOM-like manifest.
pub(all) struct Dependency {
  name : String
  version : String
  license : String
  source : String
  digest : String
}

///|
/// License and provenance policy for release dependency audits.
pub(all) struct LicensePolicy {
  allowed_licenses : Array[String]
  require_source : Bool
  require_digest : Bool
  allow_unknown_version : Bool
}

///|
/// A dependency audit result with actionable findings.
pub(all) struct DependencyAudit {
  dependencies_checked : Int
  findings : Array[AuditFinding]
}

///|
/// Construct a strict policy suitable for a published package.
pub fn LicensePolicy::strict(allowed_licenses : Array[String]) -> LicensePolicy {
  {
    allowed_licenses,
    require_source: true,
    require_digest: true,
    allow_unknown_version: false,
  }
}

///|
/// Construct a permissive policy for local development while retaining warnings.
pub fn LicensePolicy::development() -> LicensePolicy {
  {
    allowed_licenses: [],
    require_source: false,
    require_digest: false,
    allow_unknown_version: true,
  }
}

///|
/// Audit dependency metadata independently from file integrity.
pub fn audit_dependencies(
  dependencies : Array[Dependency],
  policy : LicensePolicy,
) -> DependencyAudit {
  let findings : Array[AuditFinding] = []
  let seen_names : Array[String] = []
  for dependency in dependencies {
    audit_dependency_name(dependency, seen_names, findings)
    audit_dependency_version(dependency, policy, findings)
    audit_dependency_license(dependency, policy, findings)
    audit_dependency_provenance(dependency, policy, findings)
    seen_names.push(dependency.name)
  }
  { dependencies_checked: dependencies.length(), findings }
}

///|
/// Return whether the dependency audit has a blocking finding.
pub fn DependencyAudit::has_errors(self : DependencyAudit) -> Bool {
  for finding in self.findings {
    if finding.severity == AuditSeverity::Error {
      return true
    }
  }
  false
}

///|
/// Count dependency findings by severity.
pub fn DependencyAudit::count(
  self : DependencyAudit,
  severity : AuditSeverity,
) -> Int {
  let mut count = 0
  for finding in self.findings {
    if finding.severity == severity {
      count = count + 1
    }
  }
  count
}

///|
fn audit_dependency_name(
  dependency : Dependency,
  seen : Array[String],
  findings : Array[AuditFinding],
) -> Unit {
  if dependency.name.length() == 0 {
    findings.push({
      code: "DEPENDENCY_NAME_EMPTY",
      severity: AuditSeverity::Error,
      path: "",
      message: "dependency name is empty",
      remediation: "use the canonical package or module identifier",
    })
  }
  for old_name in seen {
    if old_name == dependency.name {
      findings.push({
        code: "DEPENDENCY_DUPLICATE",
        severity: AuditSeverity::Warning,
        path: dependency.name,
        message: "dependency appears more than once",
        remediation: "merge records and keep one reviewed version",
      })
      return
    }
  }
}

///|
fn audit_dependency_version(
  dependency : Dependency,
  policy : LicensePolicy,
  findings : Array[AuditFinding],
) -> Unit {
  if dependency.version.length() == 0 {
    let severity = if policy.allow_unknown_version {
      AuditSeverity::Warning
    } else {
      AuditSeverity::Error
    }
    findings.push({
      code: "DEPENDENCY_VERSION_UNKNOWN",
      severity,
      path: dependency.name,
      message: "dependency version is missing",
      remediation: "pin an immutable release or commit identifier",
    })
  }
}

///|
fn audit_dependency_license(
  dependency : Dependency,
  policy : LicensePolicy,
  findings : Array[AuditFinding],
) -> Unit {
  if dependency.license.length() == 0 {
    findings.push({
      code: "LICENSE_UNKNOWN",
      severity: AuditSeverity::Error,
      path: dependency.name,
      message: "dependency license is not declared",
      remediation: "verify the upstream license and record it in the SBOM",
    })
    return
  }
  if policy.allowed_licenses.length() > 0 &&
    !contains_string(policy.allowed_licenses, dependency.license) {
    findings.push({
      code: "LICENSE_NOT_ALLOWED",
      severity: AuditSeverity::Error,
      path: dependency.name,
      message: "dependency license is outside the configured allow-list",
      remediation: "obtain legal review or replace the dependency",
    })
  }
}

///|
fn audit_dependency_provenance(
  dependency : Dependency,
  policy : LicensePolicy,
  findings : Array[AuditFinding],
) -> Unit {
  if policy.require_source && dependency.source.length() == 0 {
    findings.push({
      code: "SOURCE_MISSING",
      severity: AuditSeverity::Error,
      path: dependency.name,
      message: "dependency source location is missing",
      remediation: "record the authoritative upstream repository or registry URL",
    })
  }
  if policy.require_digest && !is_lower_hex(dependency.digest) {
    findings.push({
      code: "DEPENDENCY_DIGEST_MISSING",
      severity: AuditSeverity::Error,
      path: dependency.name,
      message: "dependency digest is missing or not lowercase hexadecimal",
      remediation: "record a content digest for reproducible dependency resolution",
    })
  }
}

///|
fn contains_string(values : Array[String], target : String) -> Bool {
  for value in values {
    if value == target {
      return true
    }
  }
  false
}