///|
pub(all) enum RuleTopic {
  Harmony
  Melody
  Rhythm
  Cadence
  VoiceLeading
} derive(Debug, Eq, ToJson)

///|
pub(all) enum TeachingLevel {
  Foundation
  Intermediate
  Advanced
} derive(Debug, Eq, ToJson)

///|
pub(all) struct RuleCard {
  code : String
  rule : RuleKind
  topic : String
  level : String
  title : String
  rationale : String
  remedy : String
  example : String
} derive(Debug, Eq, ToJson)

///|
pub(all) struct Recommendation {
  code : String
  priority : Int
  title : String
  action : String
  evidence : String
} derive(Debug, Eq, ToJson)

///|
/// Each card documents one analyzer rule with an actionable domain-specific repair.
pub fn rule_cards() -> Array[RuleCard] {
  [
    {
      code: "parallel-perfect",
      rule: ParallelPerfect,
      topic: "harmony",
      level: "foundation",
      title: "Avoid consecutive perfect intervals",
      rationale: "Repeated fifths and octaves can make two lines sound fused instead of independent.",
      remedy: "Keep the stable sonority in one voice and move the other by contrary or oblique motion.",
      example: "Replace the second perfect interval with a third while preserving the melodic contour.",
    },
    {
      code: "hidden-perfect",
      rule: HiddenPerfect,
      topic: "harmony",
      level: "intermediate",
      title: "Approach perfect intervals with care",
      rationale: "Similar motion into a perfect interval is strongest when the upper voice leaps.",
      remedy: "Approach the octave or fifth by contrary motion, or use a step in the upper voice.",
      example: "Keep the cadence pitch and rewrite the penultimate upper note as a stepwise approach.",
    },
    {
      code: "voice-crossing",
      rule: VoiceCrossing,
      topic: "voice-leading",
      level: "foundation",
      title: "Keep the voices in their registral order",
      rationale: "Crossing voices obscures the identity of the lines and weakens readable counterpoint.",
      remedy: "Move the crossing note by a step or transpose the local fragment into the intended register.",
      example: "Compare the crossing index with the nearest legal pitch in the same melodic direction.",
    },
    {
      code: "dissonance-placement",
      rule: DissonancePlacement,
      topic: "harmony",
      level: "intermediate",
      title: "Resolve dissonance through a prepared motion",
      rationale: "A dissonance is most intelligible when its beat strength and neighboring motion explain it.",
      remedy: "Place the dissonance on a weak beat, prepare it by step, and resolve it by step.",
      example: "Turn an accented semitone clash into a passing tone between two consonances.",
    },
    {
      code: "melody-range",
      rule: MelodyRange,
      topic: "melody",
      level: "foundation",
      title: "Keep the line within its intended span",
      rationale: "A compact range keeps a teaching phrase singable and makes its contour easier to hear.",
      remedy: "Move an outlying pitch by an octave-equivalent local step or revise the preceding leap.",
      example: "Preserve the highest point and bring the lowest event inside the configured span.",
    },
    {
      code: "large-leap",
      rule: LargeLeap,
      topic: "melody",
      level: "intermediate",
      title: "Limit unsupported melodic leaps",
      rationale: "Large leaps draw attention away from the phrase unless their register and direction are deliberate.",
      remedy: "Reduce the interval or place the leap at a structural point with a clear arrival.",
      example: "Replace a twelfth with a sixth and retain the same cadence target.",
    },
    {
      code: "leap-recovery",
      rule: LeapRecovery,
      topic: "melody",
      level: "intermediate",
      title: "Balance a leap with stepwise recovery",
      rationale: "A step in the opposite direction after a leap helps the line recover its tonal center.",
      remedy: "Make the next event move by step in the opposite direction, or reduce the original leap.",
      example: "After an upward sixth, choose a descending step before continuing the phrase.",
    },
    {
      code: "species-rhythm",
      rule: SpeciesRhythm,
      topic: "rhythm",
      level: "foundation",
      title: "Match events to the selected species",
      rationale: "Species-specific durations turn rhythmic constraints into predictable analysis and practice units.",
      remedy: "Use the expected subdivision for the species and mark suspensions with explicit ties.",
      example: "For second species, pair two counterpoint events with each cantus event.",
    },
    {
      code: "cadence-shape",
      rule: CadenceShape,
      topic: "cadence",
      level: "advanced",
      title: "Make the final approach structurally clear",
      rationale: "A cadence communicates closure through its final sonority and the direction of the approach.",
      remedy: "Reserve the final sonority for the last event and approach it with controlled contrary motion.",
      example: "Inspect the final two aligned events before changing any earlier material.",
    },
  ]
}

///|
pub fn rule_card_count() -> Int {
  rule_cards().length()
}

///|
pub fn cards_for_rule(rule : RuleKind) -> Array[RuleCard] {
  let result : Array[RuleCard] = []
  for card in rule_cards() {
    if card.rule == rule {
      result.push(card)
    }
  }
  result
}

///|
pub fn cards_for_topic(topic : String) -> Array[RuleCard] {
  let result : Array[RuleCard] = []
  for card in rule_cards() {
    if card.topic == topic {
      result.push(card)
    }
  }
  result
}

///|
pub fn cards_for_level(level : String) -> Array[RuleCard] {
  let result : Array[RuleCard] = []
  for card in rule_cards() {
    if card.level == level {
      result.push(card)
    }
  }
  result
}

///|
pub fn rule_card(rule : RuleKind, index : Int) -> RuleCard? {
  let cards = cards_for_rule(rule)
  if index < 0 || index >= cards.length() {
    None
  } else {
    Some(cards[index])
  }
}

///|
pub fn rule_topic_name(topic : RuleTopic) -> String {
  match topic {
    Harmony => "harmony"
    Melody => "melody"
    Rhythm => "rhythm"
    Cadence => "cadence"
    VoiceLeading => "voice-leading"
  }
}

///|
pub fn teaching_level_name(level : TeachingLevel) -> String {
  match level {
    Foundation => "foundation"
    Intermediate => "intermediate"
    Advanced => "advanced"
  }
}

///|
pub fn rule_family(rule : RuleKind) -> String {
  match rule {
    ParallelPerfect | HiddenPerfect | DissonancePlacement => "harmony"
    VoiceCrossing => "voice-leading"
    MelodyRange | LargeLeap | LeapRecovery => "melody"
    SpeciesRhythm => "rhythm"
    CadenceShape => "cadence"
  }
}

///|
pub fn rule_explanation(rule : RuleKind) -> String {
  match rule_card(rule, 0) {
    Some(card) => card.rationale
    None => "No explanation is registered."
  }
}

///|
pub fn rule_remedy(rule : RuleKind) -> String {
  match rule_card(rule, 0) {
    Some(card) => card.remedy
    None => "Inspect the finding in context."
  }
}

///|
pub fn recommendation_for_finding(finding : Finding) -> Recommendation {
  let card = match rule_card(finding.rule, 0) {
    Some(card) => card
    None =>
      {
        code: "general-review",
        rule: finding.rule,
        topic: rule_family(finding.rule),
        level: "foundation",
        title: "Review the local phrase",
        rationale: "Every finding should be evaluated in its musical context.",
        remedy: "Compare the current note with its neighbors.",
        example: "Make one controlled edit and run the analysis again.",
      }
  }
  {
    code: card.code,
    priority: severity_rank(finding.severity) * 10 + finding.index,
    title: card.title,
    action: card.remedy,
    evidence: finding.message,
  }
}

///|
pub fn recommendations(analysis : Analysis) -> Array[Recommendation] {
  let result : Array[Recommendation] = []
  for finding in analysis.findings {
    result.push(recommendation_for_finding(finding))
  }
  result
}

///|
pub fn top_recommendations(
  analysis : Analysis,
  limit : Int,
) -> Array[Recommendation] {
  let all = recommendations(analysis)
  let result : Array[Recommendation] = []
  let count = if limit < 0 {
    0
  } else if limit > all.length() {
    all.length()
  } else {
    limit
  }
  for i in 0.. String {
  "- [\{recommendation.priority}] \{recommendation.title}: \{recommendation.action} (\{recommendation.evidence})"
}

///|
pub fn recommendations_markdown(analysis : Analysis) -> String {
  let lines : Array[String] = ["## Next steps", ""]
  for recommendation in top_recommendations(analysis, 12) {
    lines.push(recommendation_markdown(recommendation))
  }
  lines.join("\n")
}

///|
pub fn recommendation_count_for_rule(
  analysis : Analysis,
  rule : RuleKind,
) -> Int {
  finding_count(analysis, rule)
}

///|
pub fn catalog_topics() -> Array[String] {
  ["harmony", "melody", "rhythm", "cadence", "voice-leading"]
}

///|
pub fn catalog_levels() -> Array[String] {
  ["foundation", "intermediate", "advanced"]
}