///|
pub struct AssetPolicy {
  allowed_extensions : Array[String]
  min_width_px : Int
  max_path_length : Int
  reject_parent_segments : Bool
  require_primary_asset : Bool
} derive(Debug, Eq)

///|
pub fn AssetPolicy::default() -> AssetPolicy {
  {
    allowed_extensions: ["svg", "png", "jpg", "jpeg", "webp"],
    min_width_px: 16,
    max_path_length: 240,
    reject_parent_segments: true,
    require_primary_asset: true,
  }
}

///|
pub fn AssetPolicy::strict() -> AssetPolicy {
  {
    allowed_extensions: ["svg", "png", "jpg", "jpeg", "webp"],
    min_width_px: 32,
    max_path_length: 160,
    reject_parent_segments: true,
    require_primary_asset: true,
  }
}

///|
pub fn AssetPolicy::with_min_width(
  self : AssetPolicy,
  width : Int,
) -> AssetPolicy {
  { ..self, min_width_px: if width < 0 { 0 } else { width } }
}

///|
pub fn Space::pixels(value : Int) -> Space {
  Pixels(value)
}

///|
pub fn Space::multiplier(value : Double) -> Space {
  Multiplier(value)
}

///|
pub fn BackgroundRule::light_or_dark() -> BackgroundRule {
  LightOrDark
}

///|
pub fn Logo::new(
  name : String,
  path : String,
  min_width_px : Int,
  clear_space : Space,
  background : BackgroundRule,
) -> Logo {
  { name, path, min_width_px, clear_space, background }
}

///|
pub fn Logo::extension(self : Logo) -> String {
  asset_extension(self.path)
}

///|
pub fn Logo::is_vector(self : Logo) -> Bool {
  self.extension() == "svg"
}

///|
pub fn Logo::is_raster(self : Logo) -> Bool {
  self.extension() == "png" ||
  self.extension() == "jpg" ||
  self.extension() == "jpeg" ||
  self.extension() == "webp"
}

///|
pub fn Logo::audit(self : Logo, policy : AssetPolicy) -> AuditReport {
  let findings : Array[Finding] = []
  if self.name.trim().to_owned() == "" {
    findings.push(Finding::{
      severity: Error,
      path: "name",
      message: "asset name is required",
    })
  }
  if self.path.trim().to_owned() == "" {
    findings.push(Finding::{
      severity: Error,
      path: "path",
      message: "asset path is required",
    })
  }
  if self.path.length() > policy.max_path_length {
    findings.push(Finding::{
      severity: Error,
      path: "path",
      message: "asset path is too long",
    })
  }
  if policy.reject_parent_segments &&
    (self.path.contains("../") || self.path.contains("..\\")) {
    findings.push(Finding::{
      severity: Error,
      path: "path",
      message: "asset path cannot escape the project root",
    })
  }
  if !asset_has_supported_extension(self.path, policy.allowed_extensions) {
    findings.push(Finding::{
      severity: Error,
      path: "path",
      message: "asset extension is not supported",
    })
  }
  if self.min_width_px < policy.min_width_px {
    findings.push(Finding::{
      severity: Error,
      path: "min_width_px",
      message: "asset is below the minimum display width",
    })
  }
  match self.clear_space {
    Pixels(value) if value <= 0 =>
      findings.push(Finding::{
        severity: Error,
        path: "clear_space",
        message: "clear space must be positive",
      })
    Multiplier(value) if value <= 0.0 =>
      findings.push(Finding::{
        severity: Error,
        path: "clear_space",
        message: "clear-space multiplier must be positive",
      })
    _ => ()
  }
  AuditReport::from_findings(findings)
}

///|
pub fn AssetSet::audit(self : AssetSet, policy : AssetPolicy) -> AuditReport {
  let findings : Array[Finding] = []
  if policy.require_primary_asset && self.primary_logo.path == "" {
    findings.push(Finding::{
      severity: Error,
      path: "primary_logo",
      message: "primary logo is required",
    })
  }
  let primary = self.primary_logo.audit(policy)
  for finding in primary.findings {
    findings.push(Finding::{ ..finding, path: "primary_logo." + finding.path })
  }
  for i, logo in self.alternate_logos {
    let report = logo.audit(policy)
    for finding in report.findings {
      findings.push(Finding::{
        ..finding,
        path: "alternate_logos[" + i.to_string() + "]." + finding.path,
      })
    }
  }
  AuditReport::from_findings(findings)
}

///|
pub fn BrandBook::asset_health(self : BrandBook) -> Int {
  let report = self.assets.audit(AssetPolicy::default())
  let total = 1 + self.assets.alternate_logos.length()
  if report.has_errors() {
    0
  } else {
    total * 100 / total
  }
}

///|
pub fn AssetSet::all_paths(self : AssetSet) -> Array[String] {
  let paths : Array[String] = [self.primary_logo.path]
  for logo in self.alternate_logos {
    paths.push(logo.path)
  }
  paths
}

///|
pub fn normalize_asset_path(path : String) -> String {
  let value = path.replace(old="\\", new="/")
  let parts = value.split("/")
  let result : Array[String] = []
  for part in parts {
    let trimmed = part.trim().to_owned()
    if trimmed != "" && trimmed != "." {
      result.push(trimmed)
    }
  }
  result.join("/")
}

///|
pub fn is_safe_asset_path(path : String) -> Bool {
  let normalized = normalize_asset_path(path)
  !normalized.has_prefix("../") &&
  normalized != ".." &&
  !normalized.contains("/../")
}

///|
pub fn asset_extension(path : String) -> String {
  let mut extension = StringBuilder::new()
  for ch in path {
    if ch == '.' {
      extension = StringBuilder::new()
    } else {
      extension.write_char(ch)
    }
  }
  ascii_lower(extension.to_string())
}

///|
pub fn asset_has_supported_extension(
  path : String,
  extensions : Array[String],
) -> Bool {
  let extension = asset_extension(path)
  extensions.any(fn(item) { ascii_lower(item) == extension })
}