///|
/// Deterministic resource dependency and packaging analysis.
///
/// glTF documents can reference buffer files, images, data URIs, and GLB
/// embedded chunks. This module turns those references into a manifest that a
/// build system can validate, deduplicate, package, or reject under a path
/// safety policy without reading the host filesystem.

///|
pub(all) enum ResourceKind {
  Buffer
  Image
  EmbeddedBuffer
  EmbeddedImage
  GlbBin
  Unknown
} derive(Debug, ToJson, Eq)

///|
pub(all) struct ResourceRecord {
  id : String
  kind : ResourceKind
  index : Int
  uri : String?
  normalized_uri : String?
  extension : String?
  media_type : String?
  embedded : Bool
  external : Bool
  safe : Bool
  declared_bytes : Int
  mut reference_count : Int
  fingerprint : Int
  references : Array[String]
  issues : Array[String]
} derive(Debug, ToJson, Eq)

///|
pub(all) struct ResourceManifest {
  resources : Array[ResourceRecord]
  external_count : Int
  embedded_count : Int
  unsafe_count : Int
  missing_uri_count : Int
  duplicate_uri_count : Int
  declared_bytes : Int
  referenced_bytes : Int
  fingerprint : Int
  valid : Bool
} derive(Debug, ToJson, Eq)

///|
pub(all) struct ResourcePolicy {
  allow_external : Bool
  allow_absolute_uri : Bool
  allow_parent_segments : Bool
  allow_data_uri : Bool
  max_external_count : Int
  max_total_declared_bytes : Int
  require_file_extension : Bool
  allowed_image_extensions : Array[String]
  allowed_buffer_extensions : Array[String]
} derive(Debug, ToJson, Eq)

///|
pub(all) struct ResourcePolicyResult {
  passed : Bool
  violations : Array[String]
  external_overage : Int
  byte_overage : Int
  unsafe_count : Int
  unsupported_extension_count : Int
  missing_uri_count : Int
} derive(Debug, ToJson, Eq)

///|
pub(all) struct PackageEntry {
  source_id : String
  destination : String
  kind : ResourceKind
  uri : String?
  deduplicated : Bool
  safe : Bool
  issues : Array[String]
} derive(Debug, ToJson, Eq)

///|
pub(all) struct PackagePlan {
  entries : Array[PackageEntry]
  destination_root : String
  total_entries : Int
  deduplicated_entries : Int
  unsafe_entries : Int
  valid : Bool
} derive(Debug, ToJson, Eq)

///|
pub fn default_resource_policy() -> ResourcePolicy {
  {
    allow_external: true,
    allow_absolute_uri: false,
    allow_parent_segments: false,
    allow_data_uri: true,
    max_external_count: 10000,
    max_total_declared_bytes: 2 * 1024 * 1024 * 1024,
    require_file_extension: false,
    allowed_image_extensions: ["png", "jpg", "jpeg", "webp", "ktx2"],
    allowed_buffer_extensions: ["bin", "buffer"],
  }
}

///|
pub fn embedded_resource_policy() -> ResourcePolicy {
  {
    allow_external: false,
    allow_absolute_uri: false,
    allow_parent_segments: false,
    allow_data_uri: true,
    max_external_count: 0,
    max_total_declared_bytes: 2 * 1024 * 1024 * 1024,
    require_file_extension: false,
    allowed_image_extensions: ["png", "jpg", "jpeg", "webp", "ktx2"],
    allowed_buffer_extensions: ["bin", "buffer"],
  }
}

///|
pub fn resource_kind_name(kind : ResourceKind) -> String {
  match kind {
    Buffer => "buffer"
    Image => "image"
    EmbeddedBuffer => "embedded-buffer"
    EmbeddedImage => "embedded-image"
    GlbBin => "glb-bin"
    Unknown => "unknown"
  }
}

///|
pub fn resource_fingerprint(value : String) -> Int {
  let mut hash = 216613626
  let mut index = 0
  for character in value {
    hash = (hash ^ character.to_int()) * 16777619
    index += 1
  }
  hash ^ index
}

///|
pub fn resource_fingerprint_bytes(data : Bytes) -> Int {
  binary_checksum(data).reinterpret_as_int()
}

///|
pub fn resource_id(kind : ResourceKind, index : Int) -> String {
  "\{resource_kind_name(kind)}[\{index}]"
}

///|
pub fn resource_uri_is_embedded(uri : String?) -> Bool {
  match uri {
    Some(value) => value.has_prefix("data:")
    None => false
  }
}

///|
pub fn resource_uri_is_external(uri : String?) -> Bool {
  match uri {
    Some(value) =>
      match classify_uri(value) {
        Relative | Absolute => true
        _ => false
      }
    None => false
  }
}

///|
pub fn resource_uri_is_safe(uri : String?, policy : ResourcePolicy) -> Bool {
  match uri {
    None => true
    Some(value) =>
      match classify_uri(value) {
        Empty | Fragment => true
        Data => policy.allow_data_uri
        Relative =>
          (policy.allow_parent_segments || !value.contains("..")) &&
          uri_is_safe_external(value)
        Absolute => policy.allow_absolute_uri
      }
  }
}

///|
pub fn resource_uri_normalized(uri : String?) -> String? {
  match uri {
    Some(value) =>
      match classify_uri(value) {
        Relative => Some(normalize_relative_uri(value))
        _ => Some(value)
      }
    None => None
  }
}

///|
pub fn resource_uri_extension(uri : String?) -> String? {
  match uri {
    Some(value) => uri_extension(value)
    None => None
  }
}

///|
pub fn resource_uri_media_type(uri : String?) -> String? {
  match uri {
    Some(value) => uri_data_media_type(value)
    None => None
  }
}

///|
pub fn resource_uri_payload_length(uri : String?) -> Int {
  match uri {
    None => 0
    Some(value) =>
      if value.has_prefix("data:") {
        match value.split(",").collect().get(1) {
          Some(payload) => payload.length()
          None => 0
        }
      } else {
        0
      }
  }
}

///|
fn record_for_uri(
  kind : ResourceKind,
  index : Int,
  uri : String?,
  declared_bytes : Int,
  reference : String,
  policy : ResourcePolicy,
) -> ResourceRecord {
  let embedded = resource_uri_is_embedded(uri)
  let external = resource_uri_is_external(uri)
  let safe = resource_uri_is_safe(uri, policy)
  let normalized = resource_uri_normalized(uri)
  let extension = resource_uri_extension(uri)
  let media_type = resource_uri_media_type(uri)
  let issues : Array[String] = []
  match uri {
    None =>
      if kind == Buffer || kind == Image {
        issues.push(
          "resource has no URI and is expected to be resolved from embedded data",
        )
      }
    Some(value) => {
      if !safe {
        issues.push("resource URI violates the configured path safety policy")
      }
      if value == "" {
        issues.push("resource URI is empty")
      }
    }
  }
  {
    id: resource_id(kind, index),
    kind,
    index,
    uri,
    normalized_uri: normalized,
    extension,
    media_type,
    embedded,
    external,
    safe,
    declared_bytes: if declared_bytes < 0 {
      0
    } else {
      declared_bytes
    },
    reference_count: 1,
    fingerprint: match normalized {
      Some(value) => resource_fingerprint(value)
      None => resource_fingerprint(resource_id(kind, index))
    },
    references: [reference],
    issues,
  }
}

///|
pub fn document_buffer_resource_records(
  doc : GltfDocument,
  policy : ResourcePolicy,
) -> Array[ResourceRecord] {
  let result : Array[ResourceRecord] = []
  for index, buffer in doc.buffers {
    let kind = match buffer.uri {
      Some(uri) => if uri.has_prefix("data:") { EmbeddedBuffer } else { Buffer }
      None => EmbeddedBuffer
    }
    result.push(
      record_for_uri(
        kind,
        index,
        buffer.uri,
        buffer.byte_length,
        "buffers[\{index}]",
        policy,
      ),
    )
  }
  result
}

///|
pub fn document_image_resource_records(
  doc : GltfDocument,
  policy : ResourcePolicy,
) -> Array[ResourceRecord] {
  let result : Array[ResourceRecord] = []
  for index, image in doc.images {
    let kind = match image.uri {
      Some(uri) => if uri.has_prefix("data:") { EmbeddedImage } else { Image }
      None => EmbeddedImage
    }
    let bytes = match image.buffer_view {
      Some(view) =>
        match doc.buffer_views.get(view) {
          Some(value) => value.byte_length
          None => 0
        }
      None => resource_uri_payload_length(image.uri)
    }
    result.push(
      record_for_uri(kind, index, image.uri, bytes, "images[\{index}]", policy),
    )
  }
  result
}

///|
pub fn document_resource_records(
  doc : GltfDocument,
  policy : ResourcePolicy,
) -> Array[ResourceRecord] {
  let records = document_buffer_resource_records(doc, policy)
  for record in document_image_resource_records(doc, policy) {
    records.push(record)
  }
  records
}

///|
fn add_texture_references(
  doc : GltfDocument,
  records : Array[ResourceRecord],
) -> Unit {
  for material_index, material in doc.materials {
    for semantic in material_texture_semantics(material) {
      match material_texture_info(material, semantic) {
        Some(info) =>
          match doc.textures.get(info.index) {
            Some(texture) =>
              match texture.source {
                Some(image) =>
                  for record in records {
                    if record.kind == Image || record.kind == EmbeddedImage {
                      if record.index == image {
                        record.reference_count += 1
                        record.references.push(
                          "materials[\{material_index}].\{texture_semantic_name(semantic)}",
                        )
                      }
                    }
                  }
                None => ()
              }
            None => ()
          }
        None => ()
      }
    }
  }
}

///|
fn add_mesh_buffer_references(
  doc : GltfDocument,
  records : Array[ResourceRecord],
) -> Unit {
  for accessor in doc.accessors {
    match accessor.buffer_view {
      Some(view_index) =>
        match doc.buffer_views.get(view_index) {
          Some(view) =>
            for record in records {
              if (record.kind == Buffer || record.kind == EmbeddedBuffer) &&
                record.index == view.buffer {
                record.reference_count += 1
                record.references.push("accessor.bufferView")
              }
            }
          None => ()
        }
      None => ()
    }
  }
}

///|
pub fn resource_manifest(
  doc : GltfDocument,
  policy : ResourcePolicy,
) -> ResourceManifest {
  let records = document_resource_records(doc, policy)
  add_texture_references(doc, records)
  add_mesh_buffer_references(doc, records)
  let mut external = 0
  let mut embedded = 0
  let mut unsafe_count = 0
  let mut missing_uri = 0
  let mut declared = 0
  let mut referenced = 0
  let fingerprints : Map[Int, Bool] = Map([])
  let mut duplicate = 0
  let mut fingerprint = 216613626
  for record in records {
    if record.external {
      external += 1
    }
    if record.embedded {
      embedded += 1
    }
    if !record.safe {
      unsafe_count += 1
    }
    if record.uri is None {
      missing_uri += 1
    }
    declared += record.declared_bytes
    if record.reference_count > 0 {
      referenced += record.declared_bytes
    }
    if fingerprints.contains(record.fingerprint) {
      duplicate += 1
    } else {
      fingerprints[record.fingerprint] = true
    }
    fingerprint = (fingerprint ^ record.fingerprint) * 16777619
  }
  {
    resources: records,
    external_count: external,
    embedded_count: embedded,
    unsafe_count,
    missing_uri_count: missing_uri,
    duplicate_uri_count: duplicate,
    declared_bytes: declared,
    referenced_bytes: referenced,
    fingerprint,
    valid: unsafe_count == 0,
  }
}

///|
pub fn resource_manifest_default(doc : GltfDocument) -> ResourceManifest {
  resource_manifest(doc, default_resource_policy())
}

///|
pub fn resource_manifest_external_uris(
  manifest : ResourceManifest,
) -> Array[String] {
  let result : Array[String] = []
  for resource in manifest.resources {
    if resource.external {
      match resource.normalized_uri {
        Some(uri) => if !result.contains(uri) { result.push(uri) }
        None => ()
      }
    }
  }
  result
}

///|
pub fn resource_manifest_embedded_count(manifest : ResourceManifest) -> Int {
  manifest.resources.fold(init=0, (count, resource) => {
    if resource.embedded {
      count + 1
    } else {
      count
    }
  })
}

///|
pub fn resource_manifest_external_bytes(manifest : ResourceManifest) -> Int {
  manifest.resources.fold(init=0, (sum, resource) => {
    if resource.external {
      sum + resource.declared_bytes
    } else {
      sum
    }
  })
}

///|
pub fn resource_manifest_image_count(manifest : ResourceManifest) -> Int {
  manifest.resources.fold(init=0, (count, resource) => {
    if resource.kind == Image || resource.kind == EmbeddedImage {
      count + 1
    } else {
      count
    }
  })
}

///|
pub fn resource_manifest_buffer_count(manifest : ResourceManifest) -> Int {
  manifest.resources.fold(init=0, (count, resource) => {
    if resource.kind == Buffer || resource.kind == EmbeddedBuffer {
      count + 1
    } else {
      count
    }
  })
}

///|
pub fn resource_manifest_duplicate_groups(
  manifest : ResourceManifest,
) -> Array[Array[String]] {
  let groups : Map[Int, Array[String]] = Map([])
  for resource in manifest.resources {
    match groups.get(resource.fingerprint) {
      Some(values) => values.push(resource.id)
      None => groups[resource.fingerprint] = [resource.id]
    }
  }
  let result : Array[Array[String]] = []
  for _, values in groups {
    if values.length() > 1 {
      result.push(values)
    }
  }
  result
}

///|
pub fn resource_extension_count(
  manifest : ResourceManifest,
  extension : String,
) -> Int {
  manifest.resources.fold(init=0, (count, resource) => {
    match resource.extension {
      Some(value) => if value == extension { count + 1 } else { count }
      None => count
    }
  })
}

///|
fn resource_extension_allowed(
  record : ResourceRecord,
  policy : ResourcePolicy,
) -> Bool {
  match record.extension {
    None => !policy.require_file_extension
    Some(extension) =>
      if record.kind == Image || record.kind == EmbeddedImage {
        policy.allowed_image_extensions.contains(extension)
      } else if record.kind == Buffer || record.kind == EmbeddedBuffer {
        policy.allowed_buffer_extensions.contains(extension)
      } else {
        true
      }
  }
}

///|
pub fn resource_policy_result(
  manifest : ResourceManifest,
  policy : ResourcePolicy,
) -> ResourcePolicyResult {
  let external_overage = if manifest.external_count > policy.max_external_count {
    manifest.external_count - policy.max_external_count
  } else {
    0
  }
  let byte_overage = if manifest.declared_bytes >
    policy.max_total_declared_bytes {
    manifest.declared_bytes - policy.max_total_declared_bytes
  } else {
    0
  }
  let mut unsupported = 0
  let mut missing_uri = 0
  let violations : Array[String] = []
  for resource in manifest.resources {
    if !resource_extension_allowed(resource, policy) {
      unsupported += 1
    }
    if resource.uri is None &&
      (resource.kind == Buffer || resource.kind == Image) {
      missing_uri += 1
    }
  }
  if !policy.allow_external && manifest.external_count > 0 {
    violations.push("external resource references are disabled")
  }
  if external_overage > 0 {
    violations.push("external resource budget exceeded by \{external_overage}")
  }
  if byte_overage > 0 {
    violations.push("declared resource bytes exceed policy by \{byte_overage}")
  }
  if manifest.unsafe_count > 0 {
    violations.push("resource manifest contains unsafe URI paths")
  }
  if unsupported > 0 {
    violations.push("\{unsupported} resources use unsupported extensions")
  }
  if missing_uri > 0 {
    violations.push(
      "\{missing_uri} resources require embedded or external resolution",
    )
  }
  {
    passed: violations.length() == 0,
    violations,
    external_overage,
    byte_overage,
    unsafe_count: manifest.unsafe_count,
    unsupported_extension_count: unsupported,
    missing_uri_count: missing_uri,
  }
}

///|
pub fn resource_package_destination(
  root : String,
  resource : ResourceRecord,
) -> String {
  let filename = match resource.normalized_uri {
    Some(uri) => uri
    None => "\{resource_kind_name(resource.kind)}/\{resource.index}"
  }
  "\{root}/\{filename}"
}

///|
pub fn package_plan(
  manifest : ResourceManifest,
  destination_root : String,
) -> PackagePlan {
  let entries : Array[PackageEntry] = []
  let destinations : Map[String, Int] = Map([])
  let mut deduplicated = 0
  let mut unsafe_entries = 0
  for resource in manifest.resources {
    let destination = resource_package_destination(destination_root, resource)
    let is_duplicate = destinations.contains(destination)
    if is_duplicate {
      deduplicated += 1
    } else {
      destinations[destination] = resource.index
    }
    if !resource.safe {
      unsafe_entries += 1
    }
    entries.push({
      source_id: resource.id,
      destination,
      kind: resource.kind,
      uri: resource.uri,
      deduplicated: is_duplicate,
      safe: resource.safe,
      issues: resource.issues,
    })
  }
  {
    entries,
    destination_root,
    total_entries: manifest.resources.length(),
    deduplicated_entries: deduplicated,
    unsafe_entries,
    valid: unsafe_entries == 0,
  }
}

///|
pub fn package_plan_valid(plan : PackagePlan) -> Bool {
  plan.valid && plan.unsafe_entries == 0
}

///|
pub fn package_plan_unique_destinations(plan : PackagePlan) -> Array[String] {
  let result : Array[String] = []
  for entry in plan.entries {
    if !result.contains(entry.destination) {
      result.push(entry.destination)
    }
  }
  result
}

///|
pub fn resource_manifest_summary_lines(
  manifest : ResourceManifest,
) -> Array[String] {
  [
    "resources=\{manifest.resources.length()}, buffers=\{resource_manifest_buffer_count(manifest)}, images=\{resource_manifest_image_count(manifest)}",
    "external=\{manifest.external_count}, embedded=\{manifest.embedded_count}, unsafe=\{manifest.unsafe_count}",
    "declared_bytes=\{manifest.declared_bytes}, referenced_bytes=\{manifest.referenced_bytes}",
    "duplicates=\{manifest.duplicate_uri_count}, fingerprint=\{manifest.fingerprint}",
  ]
}