///|
/// A focused content-health check for brand guidance that is not captured by
/// token schema validation alone.
pub struct ContentPolicy {
  min_principles : Int
  min_misuse_rules : Int
  require_imagery_guidance : Bool
  require_tagline : Bool
  forbid_placeholder_words : Bool
} derive(Debug, Eq)

///|
pub fn ContentPolicy::default() -> ContentPolicy {
  {
    min_principles: 1,
    min_misuse_rules: 1,
    require_imagery_guidance: false,
    require_tagline: true,
    forbid_placeholder_words: true,
  }
}

///|
pub fn ContentPolicy::strict() -> ContentPolicy {
  {
    min_principles: 3,
    min_misuse_rules: 3,
    require_imagery_guidance: true,
    require_tagline: true,
    forbid_placeholder_words: true,
  }
}

///|
pub fn ContentPolicy::with_minimums(
  self : ContentPolicy,
  principles : Int,
  misuse_rules : Int,
) -> ContentPolicy {
  {
    ..self,
    min_principles: if principles < 0 {
      0
    } else {
      principles
    },
    min_misuse_rules: if misuse_rules < 0 {
      0
    } else {
      misuse_rules
    },
  }
}

///|
pub fn ContentPolicy::requiring_imagery(
  self : ContentPolicy,
  required : Bool,
) -> ContentPolicy {
  { ..self, require_imagery_guidance: required }
}

///|
pub fn ContentPolicy::allowing_placeholders(
  self : ContentPolicy,
  allowed : Bool,
) -> ContentPolicy {
  { ..self, forbid_placeholder_words: !allowed }
}

///|
pub fn Voice::is_complete(self : Voice) -> Bool {
  self.tone.trim().to_owned() != "" && self.writing_principles.length() > 0
}

///|
pub fn Voice::principle_count(self : Voice) -> Int {
  self.writing_principles.length()
}

///|
pub fn Voice::has_principle(self : Voice, phrase : String) -> Bool {
  self.writing_principles.any(fn(value) { value.contains(phrase) })
}

///|
pub fn Voice::to_json(self : Voice) -> String {
  "{\"tone\":\"" +
  json_escape(self.tone) +
  "\",\"principles\":[" +
  self.writing_principles
  .map(fn(value) { "\"" + json_escape(value) + "\"" })
  .join(",") +
  "]}"
}

///|
pub fn Voice::to_markdown(self : Voice) -> String {
  let lines : Array[String] = ["### Voice", "", "Tone: " + self.tone, ""]
  for principle in self.writing_principles {
    lines.push("- " + principle)
  }
  lines.join("\n")
}

///|
pub fn MisuseRule::is_actionable(self : MisuseRule) -> Bool {
  self.title.trim().to_owned() != "" &&
  self.reason.trim().to_owned() != "" &&
  self.replacement.trim().to_owned() != ""
}

///|
pub fn MisuseRule::to_json(self : MisuseRule) -> String {
  "{\"title\":\"" +
  json_escape(self.title) +
  "\",\"reason\":\"" +
  json_escape(self.reason) +
  "\",\"replacement\":\"" +
  json_escape(self.replacement) +
  "\"}"
}

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

///|
pub fn Meta::summary(self : Meta) -> String {
  self.source + " — maintained by " + self.maintainer + " — " + self.license
}

///|
pub fn Meta::to_json(self : Meta) -> String {
  "{\"source\":\"" +
  json_escape(self.source) +
  "\",\"maintainer\":\"" +
  json_escape(self.maintainer) +
  "\",\"license\":\"" +
  json_escape(self.license) +
  "\"}"
}

///|
pub struct ContentReport {
  score : Int
  findings : Array[Finding]
  suggestions : Array[String]
} derive(Debug, Eq)

///|
pub fn BrandBook::audit_content(
  self : BrandBook,
  policy : ContentPolicy,
) -> ContentReport {
  let findings : Array[Finding] = []
  let suggestions : Array[String] = []
  if policy.require_tagline && self.tagline.trim().to_owned() == "" {
    findings.push(Finding::{
      severity: Error,
      path: "tagline",
      message: "a concise product tagline is required",
    })
  }
  if self.voice.tone.trim().to_owned() == "" {
    findings.push(Finding::{
      severity: Error,
      path: "voice.tone",
      message: "voice tone is required",
    })
  }
  if self.voice.writing_principles.length() < policy.min_principles {
    findings.push(Finding::{
      severity: Error,
      path: "voice.writing_principles",
      message: "voice needs more writing principles",
    })
  }
  if self.forbidden.length() < policy.min_misuse_rules {
    findings.push(Finding::{
      severity: Error,
      path: "forbidden",
      message: "misuse guidance does not cover enough failure modes",
    })
  }
  for index, rule in self.forbidden {
    if !rule.is_actionable() {
      findings.push(Finding::{
        severity: Error,
        path: "forbidden[" + index.to_string() + "]",
        message: "misuse guidance needs reason and replacement",
      })
    }
  }
  if policy.require_imagery_guidance &&
    self.assets.imagery_style.trim().to_owned() == "" {
    findings.push(Finding::{
      severity: Error,
      path: "assets.imagery_style",
      message: "imagery guidance is required",
    })
  }
  if policy.forbid_placeholder_words {
    for term in ["todo", "lorem", "placeholder", "tbd"] {
      if normalize_identifier(self.name).contains(term) ||
        normalize_identifier(self.tagline).contains(term) ||
        normalize_identifier(self.voice.tone).contains(term) {
        findings.push(Finding::{
          severity: Error,
          path: "content",
          message: "placeholder language is not publishable: " + term,
        })
      }
    }
  }
  if self.voice.writing_principles.length() < 3 {
    suggestions.push("Add a principle covering examples and edge cases.")
  }
  if self.forbidden.length() < 3 {
    suggestions.push(
      "Document at least one misuse rule for color and one for assets.",
    )
  }
  let score = 100 - findings.length() * 20
  { score: if score < 0 { 0 } else { score }, findings, suggestions }
}

///|
pub fn ContentReport::has_errors(self : ContentReport) -> Bool {
  self.findings.any(fn(finding) { finding.severity == Error })
}

///|
pub fn ContentReport::summary(self : ContentReport) -> String {
  self.score.to_string() +
  "/100, " +
  self.findings.length().to_string() +
  " findings"
}

///|
pub fn ContentReport::to_markdown(self : ContentReport) -> String {
  let lines : Array[String] = [
    "# Content health",
    "",
    "Score: " + self.summary(),
    "",
  ]
  for finding in self.findings {
    lines.push(
      "- **" +
      finding.severity.label() +
      "** `" +
      finding.path +
      "`: " +
      finding.message,
    )
  }
  if self.suggestions.length() > 0 {
    lines.push("")
    lines.push("## Suggestions")
    for suggestion in self.suggestions {
      lines.push("- " + suggestion)
    }
  }
  lines.join("\n")
}

///|
pub fn ContentReport::to_json(self : ContentReport) -> String {
  "{\"score\":" +
  self.score.to_string() +
  ",\"findings\":[" +
  self.findings
  .map(fn(finding) {
    "{\"severity\":\"" +
    finding.severity.label() +
    "\",\"path\":\"" +
    json_escape(finding.path) +
    "\",\"message\":\"" +
    json_escape(finding.message) +
    "\"}"
  })
  .join(",") +
  "]}"
}

///|
pub fn BrandBook::content_score(self : BrandBook) -> Int {
  self.audit_content(ContentPolicy::default()).score
}

///|
pub fn BrandBook::content_is_ready(self : BrandBook) -> Bool {
  !self.audit_content(ContentPolicy::strict()).has_errors()
}

///|
pub fn BrandBook::content_markdown(self : BrandBook) -> String {
  let sections : Array[String] = [
    self.voice.to_markdown(),
    "",
    "### Misuse guidance",
    "",
  ]
  for rule in self.forbidden {
    sections.push(rule.to_markdown())
    sections.push("")
  }
  sections.join("\n")
}

///|
pub fn BrandBook::content_json(self : BrandBook) -> String {
  "{\"name\":\"" +
  json_escape(self.name) +
  "\",\"voice\":" +
  self.voice.to_json() +
  ",\"metadata\":" +
  self.metadata.to_json() +
  ",\"misuse\":[" +
  self.forbidden.map(fn(rule) { rule.to_json() }).join(",") +
  "]}"
}

///|
pub fn BrandBook::principles(self : BrandBook) -> Array[String] {
  self.voice.writing_principles
}

///|
pub fn BrandBook::misuse_titles(self : BrandBook) -> Array[String] {
  self.forbidden.map(fn(rule) { rule.title })
}

///|
pub fn BrandBook::find_misuse(self : BrandBook, title : String) -> MisuseRule? {
  for rule in self.forbidden {
    if normalize_identifier(rule.title) == normalize_identifier(title) {
      return Some(rule)
    }
  }
  None
}