///|
pub enum DependencyHealthSeverity {
  DependencyInfo
  DependencyWarning
  DependencyError
} derive(Eq, Debug)

///|
pub(all) struct DependencyIssue {
  severity : DependencyHealthSeverity
  code : String
  dependency : String
  message : String
} derive(Eq, Debug)

///|
pub(all) struct NamespaceCount {
  scope : String
  count : Int
} derive(Eq, Debug)

///|
pub(all) struct DependencyHealth {
  total : Int
  pinned : Int
  unpinned : Int
  unique : Int
  namespaces : Array[NamespaceCount]
  issues : Array[DependencyIssue]
} derive(Eq, Debug)

///|
pub fn DependencyHealth::error_count(self : DependencyHealth) -> Int {
  self.issues.fold(init=0, fn(count, issue) {
    if issue.severity == DependencyError {
      count + 1
    } else {
      count
    }
  })
}

///|
pub fn DependencyHealth::warning_count(self : DependencyHealth) -> Int {
  self.issues.fold(init=0, fn(count, issue) {
    if issue.severity == DependencyWarning {
      count + 1
    } else {
      count
    }
  })
}

///|
pub fn DependencyHealth::is_healthy(self : DependencyHealth) -> Bool {
  self.error_count() == 0
}

///|
fn dependency_namespace(name : StringView) -> String {
  match name.split_once("/") {
    Some((owner, _)) => owner.to_owned()
    None => ""
  }
}

///|
fn dependency_package_name(name : StringView) -> String {
  match name.split_once("/") {
    Some((_, package_name)) => package_name.to_owned()
    None => name.to_owned()
  }
}

///|
fn dependency_valid_name(name : StringView) -> Bool {
  guard name.split_once("/") is Some((owner, package_name)) else {
    return false
  }
  !owner.is_empty() &&
  !package_name.is_empty() &&
  !owner.contains(" ") &&
  !package_name.contains(" ") &&
  !package_name.contains("/")
}

///|
fn dependency_version_core(version : StringView) -> StringView {
  match version.split_once("-") {
    Some((core, _)) => core
    None =>
      match version.split_once("+") {
        Some((core, _)) => core
        None => version
      }
  }
}

///|
fn dependency_is_digits(value : StringView) -> Bool {
  guard !value.is_empty() else { return false }
  for char in value {
    guard char >= '0' && char <= '9' else { return false }
  }
  true
}

///|
fn dependency_valid_version(version : StringView) -> Bool {
  guard !version.is_empty() else { return false }
  let core = dependency_version_core(version)
  let parts = core.split(".").collect()
  guard parts.length() == 3 else { return false }
  parts.all(dependency_is_digits)
}

///|
fn dependency_is_prerelease(version : StringView) -> Bool {
  version.contains("-") &&
  !version.has_suffix("-") &&
  dependency_valid_version(version)
}

///|
fn dependency_add_issue(
  issues : Array[DependencyIssue],
  severity : DependencyHealthSeverity,
  code : String,
  dependency : String,
  message : String,
) -> Unit {
  issues.push({ severity, code, dependency, message })
}

///|
fn dependency_seen_name_version(
  dependencies : Array[Dependency],
  until : Int,
  name : String,
  version : String,
) -> Bool {
  let mut index = 0
  while index < until {
    let dependency = dependencies[index]
    if dependency.name == name && dependency.version == version {
      return true
    }
    index += 1
  }
  false
}

///|
fn dependency_has_other_version(
  dependencies : Array[Dependency],
  name : String,
  version : String,
) -> Bool {
  dependencies.any(fn(dependency) {
    dependency.name == name && dependency.version != version
  })
}

///|
fn dependency_unique_count(dependencies : Array[Dependency]) -> Int {
  let mut count = 0
  let mut index = 0
  while index < dependencies.length() {
    let dependency = dependencies[index]
    if !dependency_seen_name_version(
        dependencies,
        index,
        dependency.name,
        dependency.version,
      ) {
      count += 1
    }
    index += 1
  }
  count
}

///|
fn dependency_namespace_counts(
  dependencies : Array[Dependency],
) -> Array[NamespaceCount] {
  let counts : Array[NamespaceCount] = []
  for dependency in dependencies {
    let scope = dependency_namespace(dependency.name)
    let mut found = false
    let mut index = 0
    while index < counts.length() {
      if counts[index].scope == scope {
        let previous = counts[index]
        counts[index] = { scope: previous.scope, count: previous.count + 1 }
        found = true
        break
      }
      index += 1
    }
    if !found {
      counts.push({ scope, count: 1 })
    }
  }
  counts
}

///|
/// Analyze direct Mooncakes dependencies for reproducibility and metadata
/// quality. The analysis is deterministic and does not require network access.
pub fn analyze_dependencies(project : Project) -> DependencyHealth {
  let issues = []
  let dependencies = project.dependencies
  let mut pinned = 0
  let mut index = 0
  while index < dependencies.length() {
    let dependency = dependencies[index]
    if dependency.version.is_empty() {
      dependency_add_issue(
        issues,
        DependencyWarning,
        "MD001",
        dependency.name,
        "Dependency does not pin a version.",
      )
    } else {
      pinned += 1
      if !dependency_valid_version(dependency.version) {
        dependency_add_issue(
          issues,
          DependencyWarning,
          "MD002",
          dependency.name,
          "Version is not canonical semantic versioning: \{dependency.version}.",
        )
      } else if dependency_is_prerelease(dependency.version) {
        dependency_add_issue(
          issues,
          DependencyInfo,
          "MD003",
          dependency.name,
          "Dependency uses prerelease version \{dependency.version}.",
        )
      }
    }
    if !dependency_valid_name(dependency.name) {
      dependency_add_issue(
        issues,
        DependencyError,
        "MD004",
        dependency.name,
        "Mooncakes dependency must use namespace/package coordinates.",
      )
    }
    if dependency_seen_name_version(
        dependencies,
        index,
        dependency.name,
        dependency.version,
      ) {
      dependency_add_issue(
        issues,
        DependencyWarning,
        "MD005",
        dependency.name,
        "Duplicate dependency declaration for \{dependency.name}@\{dependency.version}.",
      )
    }
    if dependency_has_other_version(
        dependencies,
        dependency.name,
        dependency.version,
      ) &&
      !issues.any(fn(issue) {
        issue.code == "MD006" && issue.dependency == dependency.name
      }) {
      dependency_add_issue(
        issues,
        DependencyError,
        "MD006",
        dependency.name,
        "Dependency is declared with conflicting versions.",
      )
    }
    index += 1
  }
  {
    total: dependencies.length(),
    pinned,
    unpinned: dependencies.length() - pinned,
    unique: dependency_unique_count(dependencies),
    namespaces: dependency_namespace_counts(dependencies),
    issues,
  }
}

///|
fn dependency_severity_label(severity : DependencyHealthSeverity) -> String {
  match severity {
    DependencyInfo => "INFO"
    DependencyWarning => "WARN"
    DependencyError => "ERROR"
  }
}

///|
pub fn dependency_purl(dependency : Dependency) -> String {
  if dependency.version.is_empty() {
    "pkg:mooncakes/\{dependency.name}"
  } else {
    "pkg:mooncakes/\{dependency.name}@\{dependency.version}"
  }
}

///|
pub fn render_dependency_text(health : DependencyHealth) -> String {
  let out = StringBuilder::new()
  out.write_string("MoonSeal dependency health\n")
  out.write_string(
    "Dependencies: \{health.total} total | \{health.unique} unique | \{health.pinned} pinned | \{health.unpinned} unpinned\n",
  )
  if health.namespaces.length() > 0 {
    out.write_string("Namespaces:")
    for item in health.namespaces {
      let label = if item.scope.is_empty() { "(invalid)" } else { item.scope }
      out.write_string(" \{label}=\{item.count}")
    }
    out.write_string("\n")
  }
  if health.issues.is_empty() {
    out.write_string("\nHEALTHY  No dependency metadata issues found.\n")
  } else {
    out.write_string("\n")
    for issue in health.issues {
      out.write_string(
        "\{dependency_severity_label(issue.severity)} \{issue.code} [\{issue.dependency}]: \{issue.message}\n",
      )
    }
  }
  out.to_string()
}

///|
pub fn dependency_health_json(health : DependencyHealth) -> Json {
  let namespaces : Array[Json] = health.namespaces.map(fn(item) {
    { "namespace": item.scope, "count": item.count }
  })
  let issues : Array[Json] = health.issues.map(fn(issue) {
    {
      "severity": dependency_severity_label(issue.severity),
      "code": issue.code,
      "dependency": issue.dependency,
      "message": issue.message,
    }
  })
  {
    "healthy": health.is_healthy(),
    "summary": {
      "total": health.total,
      "unique": health.unique,
      "pinned": health.pinned,
      "unpinned": health.unpinned,
      "errors": health.error_count(),
      "warnings": health.warning_count(),
    },
    "namespaces": namespaces,
    "issues": issues,
  }
}

///|
pub fn render_dependency_json(
  health : DependencyHealth,
  indent? : Int = 2,
) -> String {
  dependency_health_json(health).stringify(indent~)
}

///|
pub fn dependency_inventory(project : Project) -> Json {
  let dependencies : Array[Json] = project.dependencies.map(fn(dependency) {
    {
      "name": dependency.name,
      "namespace": dependency_namespace(dependency.name),
      "package": dependency_package_name(dependency.name),
      "version": dependency.version,
      "pinned": !dependency.version.is_empty(),
      "purl": dependency_purl(dependency),
    }
  })
  {
    "project": project.name,
    "version": project.version,
    "dependencies": dependencies,
  }
}