///|
/// Validation policy for component contracts.
pub struct ComponentPolicy {
  min_anatomy_parts : Int
  min_states : Int
  min_tokens : Int
  require_purpose : Bool
  require_focus_state : Bool
  require_disabled_state : Bool
} derive(Debug, Eq)

///|
pub fn ComponentPolicy::default() -> ComponentPolicy {
  {
    min_anatomy_parts: 1,
    min_states: 1,
    min_tokens: 1,
    require_purpose: true,
    require_focus_state: false,
    require_disabled_state: false,
  }
}

///|
pub fn ComponentPolicy::strict() -> ComponentPolicy {
  {
    min_anatomy_parts: 2,
    min_states: 2,
    min_tokens: 1,
    require_purpose: true,
    require_focus_state: false,
    require_disabled_state: false,
  }
}

///|
pub fn ComponentPolicy::with_minimums(
  self : ComponentPolicy,
  anatomy : Int,
  states : Int,
  tokens : Int,
) -> ComponentPolicy {
  {
    ..self,
    min_anatomy_parts: if anatomy < 0 {
      0
    } else {
      anatomy
    },
    min_states: if states < 0 {
      0
    } else {
      states
    },
    min_tokens: if tokens < 0 {
      0
    } else {
      tokens
    },
  }
}

///|
pub fn ComponentPolicy::requiring_focus(
  self : ComponentPolicy,
  required : Bool,
) -> ComponentPolicy {
  { ..self, require_focus_state: required }
}

///|
pub fn ComponentPolicy::requiring_disabled(
  self : ComponentPolicy,
  required : Bool,
) -> ComponentPolicy {
  { ..self, require_disabled_state: required }
}

///|
pub fn ComponentSpec::has_state(self : ComponentSpec, state : String) -> Bool {
  let target = normalize_identifier(state)
  self.states.any(fn(value) { normalize_identifier(value) == target })
}

///|
pub fn ComponentSpec::has_token(self : ComponentSpec, token : String) -> Bool {
  let target = normalize_identifier(token)
  self.tokens.any(fn(value) { normalize_identifier(value) == target })
}

///|
pub fn ComponentSpec::state_names(self : ComponentSpec) -> Array[String] {
  self.states
}

///|
pub fn ComponentSpec::token_names(self : ComponentSpec) -> Array[String] {
  self.tokens
}

///|
pub fn ComponentSpec::anatomy_names(self : ComponentSpec) -> Array[String] {
  self.anatomy
}

///|
pub fn ComponentSpec::slug(self : ComponentSpec) -> String {
  normalize_identifier(self.name)
}

///|
pub fn ComponentSpec::is_documented(self : ComponentSpec) -> Bool {
  self.name.trim().to_owned() != "" && self.purpose.trim().to_owned() != ""
}

///|
pub fn ComponentSpec::quality_score(self : ComponentSpec) -> Int {
  let mut score = 0
  if self.name.trim().to_owned() != "" {
    score += 20
  }
  if self.purpose.trim().to_owned() != "" {
    score += 20
  }
  if self.anatomy.length() >= 2 {
    score += 20
  } else if self.anatomy.length() == 1 {
    score += 10
  }
  if self.tokens.length() >= 1 {
    score += 20
  }
  if self.states.length() >= 2 {
    score += 20
  } else if self.states.length() == 1 {
    score += 10
  }
  score
}

///|
pub fn ComponentSpec::audit(
  self : ComponentSpec,
  policy : ComponentPolicy,
  path : String,
) -> AuditReport {
  let findings : Array[Finding] = []
  if self.name.trim().to_owned() == "" {
    findings.push(Finding::{
      severity: Error,
      path: path + ".name",
      message: "component name cannot be empty",
    })
  }
  if policy.require_purpose && self.purpose.trim().to_owned() == "" {
    findings.push(Finding::{
      severity: Error,
      path: path + ".purpose",
      message: "component purpose is required",
    })
  }
  if self.anatomy.length() < policy.min_anatomy_parts {
    findings.push(Finding::{
      severity: Error,
      path: path + ".anatomy",
      message: "component anatomy does not describe enough parts",
    })
  }
  if self.states.length() < policy.min_states {
    findings.push(Finding::{
      severity: Error,
      path: path + ".states",
      message: "component must document enough interaction states",
    })
  }
  if self.tokens.length() < policy.min_tokens {
    findings.push(Finding::{
      severity: Error,
      path: path + ".tokens",
      message: "component must reference at least one design token",
    })
  }
  if policy.require_focus_state &&
    !self.has_state("focus") &&
    !self.has_state("focus-visible") {
    findings.push(Finding::{
      severity: Error,
      path: path + ".states",
      message: "interactive component must define focus behavior",
    })
  }
  if policy.require_disabled_state && !self.has_state("disabled") {
    findings.push(Finding::{
      severity: Error,
      path: path + ".states",
      message: "interactive component must define disabled behavior",
    })
  }
  AuditReport::from_findings(findings)
}

///|
pub fn ComponentSpec::contract_markdown(self : ComponentSpec) -> String {
  let lines : Array[String] = [
    "## Contract: " + self.name,
    "",
    self.purpose,
    "",
    "### Anatomy",
  ]
  for part in self.anatomy {
    lines.push("- " + part)
  }
  lines.push("")
  lines.push("### Tokens")
  for token in self.tokens {
    lines.push("- `" + token + "`")
  }
  lines.push("")
  lines.push("### States")
  for state in self.states {
    lines.push("- " + state)
  }
  lines.join("\n")
}

///|
pub fn ComponentSpec::contract_json(self : ComponentSpec) -> String {
  "{\"name\":\"" +
  json_escape(self.name) +
  "\",\"purpose\":\"" +
  json_escape(self.purpose) +
  "\",\"anatomy\":[" +
  string_array_json(self.anatomy) +
  "],\"tokens\":[" +
  string_array_json(self.tokens) +
  "],\"states\":[" +
  string_array_json(self.states) +
  "]}"
}

///|
pub fn ComponentSpec::state_matrix(self : ComponentSpec) -> Array[String] {
  self.states.map(fn(state) { self.name + " / " + state })
}

///|
pub fn ComponentSpec::token_usage(
  self : ComponentSpec,
  token : String,
) -> String {
  if self.has_token(token) {
    "required by " + self.name
  } else {
    "not referenced by " + self.name
  }
}

///|
pub fn BrandBook::audit_components(
  self : BrandBook,
  policy : ComponentPolicy,
) -> AuditReport {
  let findings : Array[Finding] = []
  if self.components.length() == 0 {
    findings.push(Finding::{
      severity: Error,
      path: "components",
      message: "at least one component contract is required",
    })
  }
  for index, component in self.components {
    let report = component.audit(
      policy,
      "components[" + index.to_string() + "]",
    )
    for finding in report.findings {
      findings.push(finding)
    }
  }
  for left_index in 0.. Int {
  if self.components.length() == 0 {
    0
  } else {
    self.components.fold(init=0, fn(total, component) {
      total + component.quality_score()
    }) /
    self.components.length()
  }
}

///|
pub fn BrandBook::component_contracts_markdown(self : BrandBook) -> String {
  let sections : Array[String] = ["# Component contracts", ""]
  for component in self.components {
    sections.push(component.contract_markdown())
    sections.push("")
  }
  sections.join("\n")
}

///|
pub fn BrandBook::component_state_names(self : BrandBook) -> Array[String] {
  let result : Array[String] = []
  for component in self.components {
    for state in component.states {
      if !result.contains(state) {
        result.push(state)
      }
    }
  }
  result
}

///|
pub fn BrandBook::component_token_names(self : BrandBook) -> Array[String] {
  let result : Array[String] = []
  for component in self.components {
    for token in component.tokens {
      if !result.contains(token) {
        result.push(token)
      }
    }
  }
  result
}

///|
pub fn BrandBook::find_components_by_token(
  self : BrandBook,
  token : String,
) -> Array[ComponentSpec] {
  self.components.filter(fn(component) { component.has_token(token) })
}

///|
pub fn BrandBook::find_components_by_state(
  self : BrandBook,
  state : String,
) -> Array[ComponentSpec] {
  self.components.filter(fn(component) { component.has_state(state) })
}

///|
fn string_array_json(values : Array[String]) -> String {
  values.map(fn(value) { "\"" + json_escape(value) + "\"" }).join(",")
}