///|
/// Build a diagnostic message.
#alias(new, deprecated="Use `Diagnostic()` instead")
pub fn Diagnostic::Diagnostic(
  severity : Severity,
  path : StringView,
  message : StringView,
) -> Diagnostic {
  { severity, path: path.to_owned(), message: message.to_owned() }
}

///|
/// Build an error diagnostic.
pub fn Diagnostic::error(path : StringView, message : StringView) -> Diagnostic {
  Diagnostic(Error, path, message)
}

///|
/// Build a warning diagnostic.
pub fn Diagnostic::warning(
  path : StringView,
  message : StringView,
) -> Diagnostic {
  Diagnostic(Warning, path, message)
}

///|
/// Return true if diagnostics contain at least one error.
pub fn has_errors(diagnostics : ArrayView[Diagnostic]) -> Bool {
  for diagnostic in diagnostics {
    if diagnostic.severity is Error {
      return true
    }
  }
  false
}

///|
/// Validate a single flag without segment cross-reference checks.
pub fn validate_flag(flag : Flag) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  validate_flag_shape(flag, diagnostics)
  validate_rules(flag, Map([]), false, diagnostics)
  diagnostics
}

///|
/// Validate a flag set including segment references.
pub fn validate_set(set : FlagSet) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  set.flags.each((_key, flag) => {
    validate_flag_shape(flag, diagnostics)
    validate_rules(flag, set.segments, true, diagnostics)
  })
  set.segments.each((_key, segment) => validate_segment(segment, diagnostics))
  diagnostics
}

///|
fn validate_flag_shape(flag : Flag, diagnostics : Array[Diagnostic]) -> Unit {
  let path = "flags." + flag.key
  if flag.key.is_empty() {
    diagnostics.push(Diagnostic::error("flags", "flag key must not be empty"))
  }
  if flag.variants.is_empty() {
    diagnostics.push(
      Diagnostic::error(path, "flag must define at least one variant"),
    )
  }
  push_duplicate_warnings(path + ".variants", flag.variants, diagnostics)
  if !flag.variants.contains(flag.default_variant) {
    diagnostics.push(
      Diagnostic::error(
        path + ".default_variant",
        "default variant must be listed in variants",
      ),
    )
  }
  if !flag.variants.contains(flag.off_variant) {
    diagnostics.push(
      Diagnostic::error(
        path + ".off_variant",
        "off variant must be listed in variants",
      ),
    )
  }
  match flag.fallthrough_variant {
    Some(variant) =>
      if !flag.variants.contains(variant) {
        diagnostics.push(
          Diagnostic::error(
            path + ".fallthrough_variant",
            "fallthrough variant must be listed in variants",
          ),
        )
      }
    None => ()
  }
  match flag.fallthrough_rollout {
    Some(rollout) =>
      validate_rollout(
        path + ".fallthrough_rollout",
        rollout,
        flag,
        diagnostics,
      )
    None => ()
  }
}

///|
fn validate_rules(
  flag : Flag,
  segments : Map[String, Segment],
  check_segments : Bool,
  diagnostics : Array[Diagnostic],
) -> Unit {
  let seen : Map[String, Bool] = Map([])
  for i, rule in flag.rules {
    let path = "flags." + flag.key + ".rules[" + i.to_string() + "]"
    if rule.key.is_empty() {
      diagnostics.push(Diagnostic::error(path, "rule key must not be empty"))
    } else if seen.contains(rule.key) {
      diagnostics.push(Diagnostic::warning(path, "rule key is duplicated"))
    } else {
      seen[rule.key] = true
    }
    if rule.conditions.is_empty() && rule.segments.is_empty() {
      diagnostics.push(
        Diagnostic::warning(
          path, "rule has no conditions or segments and will match every context",
        ),
      )
    }
    match rule.variant {
      Some(variant) =>
        if !flag.variants.contains(variant) {
          diagnostics.push(
            Diagnostic::error(
              path + ".variant",
              "rule variant must be listed in flag variants",
            ),
          )
        }
      None => ()
    }
    match rule.rollout {
      Some(rollout) =>
        validate_rollout(path + ".rollout", rollout, flag, diagnostics)
      None =>
        if rule.variant is None {
          diagnostics.push(
            Diagnostic::warning(
              path, "rule has neither variant nor rollout and will use default",
            ),
          )
        }
    }
    if rule.variant is Some(_) && rule.rollout is Some(_) {
      diagnostics.push(
        Diagnostic::warning(
          path, "rule defines both variant and rollout; rollout takes precedence",
        ),
      )
    }
    if check_segments {
      for segment_key in rule.segments {
        if !segments.contains(segment_key) {
          diagnostics.push(
            Diagnostic::error(
              path + ".segments",
              "unknown segment: " + segment_key,
            ),
          )
        }
      }
    }
  }
}

///|
fn validate_rollout(
  path : String,
  rollout : Rollout,
  flag : Flag,
  diagnostics : Array[Diagnostic],
) -> Unit {
  if rollout.allocations.is_empty() {
    diagnostics.push(Diagnostic::error(path, "rollout must define allocations"))
    return
  }
  let mut total = 0
  for i, allocation in rollout.allocations {
    let item_path = path + ".allocations[" + i.to_string() + "]"
    if allocation.variant.is_empty() {
      diagnostics.push(
        Diagnostic::error(item_path, "allocation variant must not be empty"),
      )
    }
    if !flag.variants.contains(allocation.variant) {
      diagnostics.push(
        Diagnostic::error(
          item_path, "allocation variant must be listed in flag variants",
        ),
      )
    }
    if allocation.weight < 0 {
      diagnostics.push(
        Diagnostic::error(item_path, "allocation weight must be >= 0"),
      )
    }
    if allocation.weight > 10000 {
      diagnostics.push(
        Diagnostic::error(item_path, "allocation weight must be <= 10000"),
      )
    }
    total = total + allocation.weight
  }
  if total > 10000 {
    diagnostics.push(
      Diagnostic::error(path, "rollout total weight must be <= 10000"),
    )
  } else if total < 10000 {
    diagnostics.push(
      Diagnostic::warning(
        path, "rollout total weight is below 10000; uncovered buckets use default variant",
      ),
    )
  }
}

///|
fn validate_segment(segment : Segment, diagnostics : Array[Diagnostic]) -> Unit {
  let path = "segments." + segment.key
  if segment.key.is_empty() {
    diagnostics.push(
      Diagnostic::error("segments", "segment key must not be empty"),
    )
  }
  if segment.include_users.is_empty() && segment.conditions.is_empty() {
    diagnostics.push(
      Diagnostic::warning(
        path, "segment has no include users or conditions and only matches contexts explicitly excluded from nothing",
      ),
    )
  }
  for user in segment.include_users {
    if segment.exclude_users.contains(user) {
      diagnostics.push(
        Diagnostic::error(
          path,
          "user " + user + " appears in both include and exclude lists",
        ),
      )
    }
  }
  push_duplicate_warnings(
    path + ".include_users",
    segment.include_users,
    diagnostics,
  )
  push_duplicate_warnings(
    path + ".exclude_users",
    segment.exclude_users,
    diagnostics,
  )
}

///|
fn push_duplicate_warnings(
  path : String,
  values : Array[String],
  diagnostics : Array[Diagnostic],
) -> Unit {
  let seen : Map[String, Bool] = Map([])
  for value in values {
    if seen.contains(value) {
      diagnostics.push(Diagnostic::warning(path, "duplicate value: " + value))
    } else {
      seen[value] = true
    }
  }
}