///|
pub(all) enum ControlDomain {
  OwnerEvidence
  SegmentEvidence
  ScheduleEvidence
  MetricEvidence
  BlastEvidence
  RollbackEvidence
  ReleaseDomain
} derive(Eq, Debug)

///|
pub(all) struct ControlRule {
  id : String
  domain : ControlDomain
  title : String
  check_text : String
  evidence_text : String
  repair_text : String
  weight : Int
} derive(Eq, Debug)

///|
pub fn ControlRule::ControlRule(
  id? : String = "",
  domain? : ControlDomain = OwnerEvidence,
  title? : String = "",
  check_text? : String = "",
  evidence_text? : String = "",
  repair_text? : String = "",
  weight? : Int = 1,
) -> ControlRule {
  { id, domain, title, check_text, evidence_text, repair_text, weight }
}

///|
pub(all) struct ControlEvaluation {
  rule_id : String
  domain : ControlDomain
  status : Severity
  score : Int
  title : String
  message : String
  repair : String
} derive(Eq, Debug)

///|
pub(all) struct ControlReview {
  verdict : String
  score : Int
  passed : Int
  warned : Int
  failed : Int
  evaluations : Array[ControlEvaluation]
} derive(Eq, Debug)

///|
pub fn default_control_rules() -> Array[ControlRule] {
  [
    ControlRule(
      id="POL-001",
      domain=OwnerEvidence,
      title="Repository owner is fixed",
      check_text="The rollout plan points to one repository owner.",
      evidence_text="owner/repository/package fields",
      repair_text="Use one GitHub owner and one Mooncakes namespace.",
      weight=3,
    ),
    ControlRule(
      id="POL-002",
      domain=OwnerEvidence,
      title="Package namespace is aligned",
      check_text="The package name uses the same namespace as the submitted account.",
      evidence_text="moon.mod package name",
      repair_text="Set moon.mod name before publishing.",
      weight=4,
    ),
    ControlRule(
      id="POL-003",
      domain=OwnerEvidence,
      title="Repository URL is reviewer friendly",
      check_text="The repository URL is concrete and does not rely on local credentials.",
      evidence_text="plan.repository",
      repair_text="Use an HTTPS GitHub URL that can be opened publicly.",
      weight=3,
    ),
    ControlRule(
      id="POL-004",
      domain=OwnerEvidence,
      title="Runtime output avoids private contact details",
      check_text="Library examples should not print phone numbers or personal email addresses.",
      evidence_text="public package output",
      repair_text="Keep personal contact details in the separate proposal only.",
      weight=2,
    ),
    ControlRule(
      id="POL-005",
      domain=OwnerEvidence,
      title="One submission identity is used",
      check_text="Evidence should not mix multiple repository owners or package owners.",
      evidence_text="owner field and package namespace",
      repair_text="Replace stale owners before committing.",
      weight=4,
    ),
    ControlRule(
      id="POL-006",
      domain=SegmentEvidence,
      title="At least one target segment exists",
      check_text="A canary plan should define who can enter the rollout.",
      evidence_text="plan.segments",
      repair_text="Add a named SegmentRule.",
      weight=3,
    ),
    ControlRule(
      id="POL-007",
      domain=SegmentEvidence,
      title="Segments match real sample users",
      check_text="The example population should exercise the segment rules.",
      evidence_text="count_segment_matches",
      repair_text="Adjust fixtures or segment rules so tests cover selected users.",
      weight=4,
    ),
    ControlRule(
      id="POL-008",
      domain=SegmentEvidence,
      title="High-risk users can be excluded",
      check_text="Segment rules should support exclusion tags or exclusion tiers.",
      evidence_text="exclude_tags/exclude_tiers",
      repair_text="Add exclusion boundaries for VIP or unsupported traffic.",
      weight=3,
    ),
    ControlRule(
      id="POL-009",
      domain=SegmentEvidence,
      title="Minimum version is modeled",
      check_text="Protocol-sensitive rollouts should be able to require a client version.",
      evidence_text="min_version",
      repair_text="Set min_version on at least one segment when protocol behavior changes.",
      weight=2,
    ),
    ControlRule(
      id="POL-010",
      domain=SegmentEvidence,
      title="Bucket prefilter is available",
      check_text="Segments can cap membership before phase percentage is applied.",
      evidence_text="max_bucket_bp",
      repair_text="Use max_bucket_bp for staff rings or small seed rings.",
      weight=2,
    ),
    ControlRule(
      id="POL-011",
      domain=ScheduleEvidence,
      title="Phase windows are non-overlapping",
      check_text="Only one rollout phase should be active at a given minute.",
      evidence_text="phase_overlap",
      repair_text="Move start_minute or duration_minutes to avoid overlaps.",
      weight=5,
    ),
    ControlRule(
      id="POL-012",
      domain=ScheduleEvidence,
      title="Phase durations are positive",
      check_text="Every phase must have time to collect evidence.",
      evidence_text="duration_minutes",
      repair_text="Use duration_minutes greater than zero.",
      weight=4,
    ),
    ControlRule(
      id="POL-013",
      domain=ScheduleEvidence,
      title="Large live phases need approval",
      check_text="At least 50% live exposure should require a manual checkpoint.",
      evidence_text="require_manual_approval",
      repair_text="Mark large live phases as manually approved.",
      weight=4,
    ),
    ControlRule(
      id="POL-014",
      domain=ScheduleEvidence,
      title="Shadow and dry-run phases are represented",
      check_text="The schedule should contain a low-risk phase before live traffic.",
      evidence_text="mode values",
      repair_text="Add Shadow or DryRun phases before Live phases.",
      weight=3,
    ),
    ControlRule(
      id="POL-015",
      domain=ScheduleEvidence,
      title="Phase percentages are bounded",
      check_text="Each phase uses basis points inside 0..10000.",
      evidence_text="percent_bp",
      repair_text="Clamp percent_bp to the allowed range.",
      weight=4,
    ),
    ControlRule(
      id="POL-016",
      domain=MetricEvidence,
      title="Metric samples exist",
      check_text="Gate decisions require at least one MetricsSnapshot.",
      evidence_text="metrics array",
      repair_text="Record synthetic or production metric snapshots.",
      weight=4,
    ),
    ControlRule(
      id="POL-017",
      domain=MetricEvidence,
      title="Error budget is enforced",
      check_text="The gate compares error_per_million against the budget.",
      evidence_text="evaluate_gate",
      repair_text="Lower exposure or roll back when error rate exceeds budget.",
      weight=5,
    ),
    ControlRule(
      id="POL-018",
      domain=MetricEvidence,
      title="Latency budget is enforced",
      check_text="The gate compares latency_p95_ms against the budget.",
      evidence_text="evaluate_gate",
      repair_text="Hold or roll back when latency exceeds the p95 ceiling.",
      weight=4,
    ),
    ControlRule(
      id="POL-019",
      domain=MetricEvidence,
      title="Synthetic checks are honored",
      check_text="Synthetic failures can block an unsafe advance.",
      evidence_text="synthetic_pass",
      repair_text="Keep require_synthetic_pass true for critical paths.",
      weight=4,
    ),
    ControlRule(
      id="POL-020",
      domain=MetricEvidence,
      title="Dependency failure budget exists",
      check_text="Dependency failures are part of the release gate.",
      evidence_text="max_dependency_failures",
      repair_text="Set a strict dependency failure budget.",
      weight=3,
    ),
    ControlRule(
      id="POL-021",
      domain=BlastEvidence,
      title="User blast radius is computed",
      check_text="Reports include exposed user count and user share.",
      evidence_text="blast_radius_for_users",
      repair_text="Provide the user population fixture for simulation.",
      weight=4,
    ),
    ControlRule(
      id="POL-022",
      domain=BlastEvidence,
      title="Traffic blast radius is computed",
      check_text="High-volume users should increase exposure risk.",
      evidence_text="traffic_units",
      repair_text="Set realistic traffic_units on sample users.",
      weight=4,
    ),
    ControlRule(
      id="POL-023",
      domain=BlastEvidence,
      title="Region spread is tracked",
      check_text="Reports count how many regions are touched.",
      evidence_text="region_count",
      repair_text="Keep region coverage visible in blast reports.",
      weight=3,
    ),
    ControlRule(
      id="POL-024",
      domain=BlastEvidence,
      title="Tier spread is tracked",
      check_text="Reports count how many account tiers are touched.",
      evidence_text="tier_count",
      repair_text="Keep tier coverage visible in blast reports.",
      weight=3,
    ),
    ControlRule(
      id="POL-025",
      domain=BlastEvidence,
      title="Blast budget can stop release",
      check_text="The gate should hold or roll back when blast exceeds budget.",
      evidence_text="max_blast_radius_bp",
      repair_text="Lower percent_bp or tighten segments.",
      weight=5,
    ),
    ControlRule(
      id="POL-026",
      domain=RollbackEvidence,
      title="Rollback path is explicit",
      check_text="The plan should contain a kill switch or documented rollback action.",
      evidence_text="KillSwitch phase or incident actions",
      repair_text="Add a KillSwitch phase or keep a generated rollback report.",
      weight=4,
    ),
    ControlRule(
      id="POL-027",
      domain=RollbackEvidence,
      title="Hold decision blocks auto advance",
      check_text="Warnings should not silently advance to the next live ring.",
      evidence_text="GateDecision Hold",
      repair_text="Require manual review before advance after warning.",
      weight=3,
    ),
    ControlRule(
      id="POL-028",
      domain=RollbackEvidence,
      title="Rollback reasons are printable",
      check_text="Generated reports should include human-readable gate reasons.",
      evidence_text="GateResult.reasons",
      repair_text="Keep reason strings attached to every gate.",
      weight=3,
    ),
    ControlRule(
      id="POL-029",
      domain=RollbackEvidence,
      title="Rollback preserves previous healthy window",
      check_text="Trend gates should compare previous and current snapshots.",
      evidence_text="metric_trend_gate",
      repair_text="Provide adjacent metric windows.",
      weight=3,
    ),
    ControlRule(
      id="POL-030",
      domain=RollbackEvidence,
      title="Failure output is actionable",
      check_text="Incident reports should include operator actions.",
      evidence_text="incident actions",
      repair_text="Use incident_report_markdown for failed simulations.",
      weight=3,
    ),
    ControlRule(
      id="POL-031",
      domain=ReleaseDomain,
      title="README can cite runnable examples",
      check_text="The package should expose a summary and markdown report for examples.",
      evidence_text="canaryring_summary and plan_report_markdown",
      repair_text="Keep examples/basic and cmd/main executable.",
      weight=3,
    ),
    ControlRule(
      id="POL-032",
      domain=ReleaseDomain,
      title="Simulation exports JSON",
      check_text="Automation can consume machine-readable reports.",
      evidence_text="SimulationReport::to_json",
      repair_text="Keep JSON fields stable across patch releases.",
      weight=3,
    ),
    ControlRule(
      id="POL-033",
      domain=ReleaseDomain,
      title="Simulation exports Markdown",
      check_text="Reviewers can inspect results without custom tooling.",
      evidence_text="SimulationReport::to_markdown",
      repair_text="Keep Markdown report deterministic.",
      weight=3,
    ),
    ControlRule(
      id="POL-034",
      domain=ReleaseDomain,
      title="Release evidence review exists",
      check_text="Package metadata can be checked before final acceptance.",
      evidence_text="release_evidence_review",
      repair_text="Run release evidence review before publishing.",
      weight=3,
    ),
    ControlRule(
      id="POL-035",
      domain=ReleaseDomain,
      title="Runbook review exists",
      check_text="Operational notes can be reviewed with deterministic rules.",
      evidence_text="runbook_review",
      repair_text="Keep runbook records in docs/test-record.md.",
      weight=2,
    ),
  ]
}

///|
pub fn control_domain_text(domain : ControlDomain) -> String {
  match domain {
    OwnerEvidence => "owner"
    SegmentEvidence => "segment"
    ScheduleEvidence => "schedule"
    MetricEvidence => "metric"
    BlastEvidence => "blast"
    RollbackEvidence => "rollback"
    ReleaseDomain => "release"
  }
}

///|
pub fn evaluate_control_rule(
  rule : ControlRule,
  plan : RolloutPlan,
  users : Array[UserProfile],
  metrics : Array[MetricsSnapshot],
) -> ControlEvaluation {
  let status = domain_status(rule.domain, plan, users, metrics)
  let score = if status == Pass {
    rule.weight
  } else if status == Warn {
    rule.weight / 2
  } else {
    0
  }
  {
    rule_id: rule.id,
    domain: rule.domain,
    status,
    score,
    title: rule.title,
    message: rule.check_text + " Evidence: " + rule.evidence_text,
    repair: if status == Pass {
      ""
    } else {
      rule.repair_text
    },
  }
}

///|
pub fn review_controls(
  plan : RolloutPlan,
  users : Array[UserProfile],
  metrics : Array[MetricsSnapshot],
) -> ControlReview {
  let rules = default_control_rules()
  let evaluations = Array::new(capacity=rules.length())
  let mut possible = 0
  let mut earned = 0
  let mut passed = 0
  let mut warned = 0
  let mut failed = 0
  for rule in rules {
    let evaluation = evaluate_control_rule(rule, plan, users, metrics)
    possible += rule.weight
    earned += evaluation.score
    if evaluation.status == Pass {
      passed += 1
    } else if evaluation.status == Warn {
      warned += 1
    } else {
      failed += 1
    }
    evaluations.push(evaluation)
  }
  let score = if possible == 0 { 0 } else { earned * 100 / possible }
  let verdict = if failed > 0 {
    "blocked"
  } else if warned > 0 || score < 90 {
    "review"
  } else {
    "ready"
  }
  { verdict, score, passed, warned, failed, evaluations }
}

///|
fn domain_status(
  domain : ControlDomain,
  plan : RolloutPlan,
  users : Array[UserProfile],
  metrics : Array[MetricsSnapshot],
) -> Severity {
  match domain {
    OwnerEvidence => owner_control_status(plan)
    SegmentEvidence => segment_control_status(plan, users)
    ScheduleEvidence => schedule_control_status(plan)
    MetricEvidence => metric_control_status(plan, users, metrics)
    BlastEvidence => blast_control_status(plan, users)
    RollbackEvidence => rollback_control_status(plan, metrics)
    ReleaseDomain => release_control_status(plan)
  }
}

///|
fn owner_control_status(plan : RolloutPlan) -> Severity {
  if plan.owner == "EJJ-ai-nb" &&
    plan.package_name == "EJJ-ai-nb/canaryring" &&
    contains_ci(plan.repository, "github.com/EJJ-ai-nb/canaryring") {
    Pass
  } else if has_text(plan.owner) &&
    has_text(plan.package_name) &&
    has_text(plan.repository) {
    Warn
  } else {
    Fail
  }
}

///|
fn segment_control_status(
  plan : RolloutPlan,
  users : Array[UserProfile],
) -> Severity {
  if plan.segments.length() == 0 {
    return Warn
  }
  let mut matched = 0
  let mut has_exclusion = false
  let mut has_version = false
  let mut has_prefilter = false
  for segment in plan.segments {
    matched += count_segment_matches(users, segment, salt=plan.salt)
    if segment.exclude_tags.length() > 0 ||
      segment.exclude_tiers.length() > 0 ||
      segment.exclude_regions.length() > 0 {
      has_exclusion = true
    }
    if has_text(segment.min_version) {
      has_version = true
    }
    if segment.max_bucket_bp < 10000 {
      has_prefilter = true
    }
  }
  if matched == 0 {
    Fail
  } else if has_exclusion && has_version && has_prefilter {
    Pass
  } else {
    Warn
  }
}

///|
fn schedule_control_status(plan : RolloutPlan) -> Severity {
  let findings = validate_plan(plan)
  let mut has_schedule_fail = false
  let mut has_live = false
  let mut has_safe_phase = false
  for finding in findings {
    if finding.kind == ScheduleGate && finding.severity == Fail {
      has_schedule_fail = true
    }
  }
  for phase in plan.phases {
    if phase.mode == Live {
      has_live = true
    }
    if phase.mode == Shadow || phase.mode == DryRun {
      has_safe_phase = true
    }
  }
  if has_schedule_fail || plan.phases.length() == 0 {
    Fail
  } else if has_live && has_safe_phase {
    Pass
  } else {
    Warn
  }
}

///|
fn metric_control_status(
  plan : RolloutPlan,
  users : Array[UserProfile],
  metrics : Array[MetricsSnapshot],
) -> Severity {
  if metrics.length() == 0 {
    return Fail
  }
  let minute = plan_duration(plan) - 1
  let blast = blast_radius_for_users(plan, users, minute)
  let gate = evaluate_gate(plan.budget, nearest_metric(metrics, minute), blast)
  gate.severity
}

///|
fn blast_control_status(
  plan : RolloutPlan,
  users : Array[UserProfile],
) -> Severity {
  if users.length() == 0 {
    return Fail
  }
  let minute = plan_duration(plan) - 1
  let blast = blast_radius_for_users(plan, users, minute)
  if blast.percent_bp <= plan.budget.max_blast_radius_bp {
    Pass
  } else if blast.percent_bp <= plan.budget.max_blast_radius_bp + 1500 {
    Warn
  } else {
    Fail
  }
}

///|
fn rollback_control_status(
  plan : RolloutPlan,
  metrics : Array[MetricsSnapshot],
) -> Severity {
  let mut has_kill = false
  let mut has_manual_large_live = false
  for phase in plan.phases {
    if phase.mode == KillSwitch {
      has_kill = true
    }
    if phase.mode == Live &&
      phase.percent_bp >= 5000 &&
      phase.require_manual_approval {
      has_manual_large_live = true
    }
  }
  if has_kill {
    Pass
  } else if has_manual_large_live && metrics.length() > 1 {
    Warn
  } else {
    Fail
  }
}

///|
fn release_control_status(plan : RolloutPlan) -> Severity {
  if plan.package_name == "EJJ-ai-nb/canaryring" &&
    contains_ci(plan.repository, "github.com/EJJ-ai-nb/canaryring") &&
    has_text(plan.name) {
    Pass
  } else if has_text(plan.package_name) && has_text(plan.repository) {
    Warn
  } else {
    Fail
  }
}

///|
fn nearest_metric(
  metrics : Array[MetricsSnapshot],
  minute : Int,
) -> MetricsSnapshot {
  let mut chosen = MetricsSnapshot::MetricsSnapshot(minute~)
  let mut best_delta = 2147483
  for metric in metrics {
    let delta = if metric.minute > minute {
      metric.minute - minute
    } else {
      minute - metric.minute
    }
    if delta < best_delta {
      chosen = metric
      best_delta = delta
    }
  }
  chosen
}

///|
pub fn ControlReview::to_markdown(self : ControlReview) -> String {
  let out = StringBuilder()
  out.write_string("# CanaryRing Control Review\n\n")
  out.write_string("- Verdict: " + self.verdict + "\n")
  out.write_string("- Score: " + self.score.to_string() + "/100\n")
  out.write_string("- Passed: " + self.passed.to_string() + "\n")
  out.write_string("- Warnings: " + self.warned.to_string() + "\n")
  out.write_string("- Failed: " + self.failed.to_string() + "\n\n")
  for item in self.evaluations {
    out.write_string("- [" + severity_text(item.status) + "] ")
    out.write_string(
      item.rule_id + " " + control_domain_text(item.domain) + " ",
    )
    out.write_string(item.title)
    if item.repair != "" {
      out.write_string(" Fix: " + item.repair)
    }
    out.write_string("\n")
  }
  out.to_string()
}

///|
pub fn ControlReview::to_json(self : ControlReview) -> String {
  let out = StringBuilder()
  out.write_string("{")
  out.write_string("\"verdict\":\"" + json_escape(self.verdict) + "\",")
  out.write_string("\"score\":" + self.score.to_string() + ",")
  out.write_string("\"passed\":" + self.passed.to_string() + ",")
  out.write_string("\"warned\":" + self.warned.to_string() + ",")
  out.write_string("\"failed\":" + self.failed.to_string() + ",")
  out.write_string("\"evaluations\":[")
  for i in 0.. 0 {
      out.write_string(",")
    }
    out.write_string("{")
    out.write_string("\"id\":\"" + json_escape(item.rule_id) + "\",")
    out.write_string("\"domain\":\"" + control_domain_text(item.domain) + "\",")
    out.write_string("\"status\":\"" + severity_text(item.status) + "\",")
    out.write_string("\"score\":" + item.score.to_string())
    out.write_string("}")
  }
  out.write_string("]}")
  out.to_string()
}