///|
pub struct ValidationPolicy {
  min_colors : Int
  min_component_states : Int
  min_logo_width : Int
  require_metadata_license : Bool
  require_component_tokens : Bool
} derive(Debug, Eq)

///|
pub fn ValidationPolicy::default() -> ValidationPolicy {
  {
    min_colors: 3,
    min_component_states: 1,
    min_logo_width: 16,
    require_metadata_license: false,
    require_component_tokens: false,
  }
}

///|
pub fn ValidationPolicy::strict() -> ValidationPolicy {
  {
    min_colors: 3,
    min_component_states: 2,
    min_logo_width: 32,
    require_metadata_license: true,
    require_component_tokens: true,
  }
}

///|
pub fn ValidationPolicy::with_min_colors(
  self : ValidationPolicy,
  count : Int,
) -> ValidationPolicy {
  { ..self, min_colors: if count < 0 { 0 } else { count } }
}

///|
pub fn ValidationPolicy::with_min_logo_width(
  self : ValidationPolicy,
  width : Int,
) -> ValidationPolicy {
  { ..self, min_logo_width: if width < 0 { 0 } else { width } }
}

///|
pub fn validate_identifier(text : String) -> Bool {
  let value = normalize_identifier(text)
  value != "" && value.length() <= 80
}

///|
pub fn validate_hex_token(text : String) -> Bool {
  is_hex_color(text)
}

///|
pub fn validate_angle_degrees(angle : Int) -> Bool {
  angle >= 0 && angle < 360
}

///|
pub fn validate_positive_integer(value : Int) -> Bool {
  value > 0
}

///|
pub fn validate_non_negative_integer(value : Int) -> Bool {
  value >= 0
}

///|
pub fn validate_name(text : String, max_length : Int) -> Bool {
  let value = text.trim().to_owned()
  value != "" &&
  value.length() <= max_length &&
  !value.contains("\n") &&
  !value.contains("\r")
}

///|
pub fn AuditReport::error_count(self : AuditReport) -> Int {
  let mut total = 0
  for finding in self.findings {
    if finding.severity == Severity::Error {
      total += 1
    }
  }
  total
}

///|
pub fn AuditReport::warning_count(self : AuditReport) -> Int {
  let mut total = 0
  for finding in self.findings {
    if finding.severity == Severity::Warning {
      total += 1
    }
  }
  total
}

///|
pub fn AuditReport::info_count(self : AuditReport) -> Int {
  let mut total = 0
  for finding in self.findings {
    if finding.severity == Severity::Info {
      total += 1
    }
  }
  total
}

///|
pub fn AuditReport::contains_path(self : AuditReport, path : String) -> Bool {
  self.findings.any(fn(finding) { finding.path == path })
}

///|
pub fn AuditReport::paths(self : AuditReport) -> Array[String] {
  let result : Array[String] = []
  for finding in self.findings {
    result.push(finding.path)
  }
  result
}

///|
pub fn AuditReport::to_markdown(self : AuditReport) -> String {
  let lines : Array[String] = [
    "# Validation",
    "",
    "Summary: " + self.summary(),
    "",
    "| Severity | Path | Message |",
    "| --- | --- | --- |",
  ]
  for finding in self.findings {
    lines.push(
      "| " +
      finding.severity.label() +
      " | `" +
      finding.path +
      "` | " +
      finding.message +
      " |",
    )
  }
  lines.join("\n")
}

///|
pub fn BrandBook::validate_with_policy(
  self : BrandBook,
  policy : ValidationPolicy,
) -> AuditReport {
  let findings = self.audit().findings
  if self.palette.colors.length() < policy.min_colors {
    findings.push(Finding::{
      severity: Warning,
      path: "palette.colors",
      message: "policy expects at least " +
      policy.min_colors.to_string() +
      " color tokens",
    })
  }
  if self.assets.primary_logo.min_width_px < policy.min_logo_width {
    findings.push(Finding::{
      severity: Error,
      path: "assets.primary_logo.min_width_px",
      message: "policy minimum logo width is " +
      policy.min_logo_width.to_string() +
      "px",
    })
  }
  for i, component in self.components {
    if component.states.length() < policy.min_component_states {
      findings.push(Finding::{
        severity: Warning,
        path: "components[" + i.to_string() + "].states",
        message: "policy expects at least " +
        policy.min_component_states.to_string() +
        " states",
      })
    }
    if policy.require_component_tokens && component.tokens.length() == 0 {
      findings.push(Finding::{
        severity: Error,
        path: "components[" + i.to_string() + "].tokens",
        message: "policy requires token references for every component",
      })
    }
  }
  if policy.require_metadata_license &&
    self.metadata.license.trim().to_owned() == "" {
    findings.push(Finding::{
      severity: Error,
      path: "metadata.license",
      message: "strict policy requires an explicit license",
    })
  }
  AuditReport::from_findings(findings)
}

///|
pub fn BrandBook::is_publishable(self : BrandBook) -> Bool {
  !self.audit().has_errors() &&
  self.name.trim().to_owned() != "" &&
  self.palette.colors.length() >= 3
}

///|
pub fn BrandBook::quality_score(self : BrandBook) -> Int {
  let report = self.audit()
  let mut score = 100 - report.error_count() * 20 - report.warning_count() * 5
  if self.components.length() >= 3 {
    score += 3
  }
  if self.typography.scale.length() >= 3 {
    score += 3
  }
  if self.forbidden.length() >= 2 {
    score += 2
  }
  if score < 0 {
    0
  } else if score > 100 {
    100
  } else {
    score
  }
}

///|
pub fn Palette::validate_colors(self : Palette) -> AuditReport {
  let findings : Array[Finding] = []
  for i, color in self.colors {
    if !validate_identifier(color.name) {
      findings.push(Finding::{
        severity: Error,
        path: "colors[" + i.to_string() + "].name",
        message: "invalid token identifier",
      })
    }
    if !validate_hex_token(color.value) {
      findings.push(Finding::{
        severity: Error,
        path: "colors[" + i.to_string() + "].value",
        message: "invalid six-digit hex token",
      })
    }
  }
  AuditReport::from_findings(findings)
}

///|
pub fn Typography::validate_scale(self : Typography) -> AuditReport {
  let findings : Array[Finding] = []
  let mut previous = 0
  for i, step in self.scale {
    if !validate_positive_integer(step.size_px) {
      findings.push(Finding::{
        severity: Error,
        path: "scale[" + i.to_string() + "].size_px",
        message: "size must be positive",
      })
    }
    if step.size_px < previous {
      findings.push(Finding::{
        severity: Warning,
        path: "scale[" + i.to_string() + "].size_px",
        message: "type scale is not ascending",
      })
    }
    previous = step.size_px
  }
  AuditReport::from_findings(findings)
}

///|
pub fn Meta::is_complete(self : Meta) -> Bool {
  self.source.trim().to_owned() != "" && self.license.trim().to_owned() != ""
}