///|
pub(all) struct Attribute {
  key : String
  value : String
} derive(Eq, Debug)

///|
pub(all) struct EvalContext {
  subject : String
  environment : String
  attributes : Array[Attribute]
} derive(Eq, Debug)

///|
pub(all) enum Operator {
  Eq
  Ne
  Gt
  Lt
  Ge
  Le
  Contains
  In
  NotIn
} derive(Eq, Debug)

///|
pub(all) enum Rule {
  Allow(String)
  Deny(String)
  When(String, Operator, String)
  AllowVariant(String, String)
  WhenVariant(String, Operator, String, String)
} derive(Eq, Debug)

///|
pub(all) struct Variant {
  name : String
  weight : Int
} derive(Eq, Debug)

///|
pub(all) struct Flag {
  name : String
  enabled : Bool
  default_enabled : Bool
  rollout : Int
  salt : String
  rules : Array[Rule]
  is_multivariate : Bool
  variants : Array[Variant]
  default_variant : String
} derive(Eq, Debug)

///|
pub(all) struct Decision {
  flag : String
  enabled : Bool
  variant : String
  reason : String
  audit_trail : Array[String]
} derive(Eq, Debug)

///|
pub(all) struct FlagDiagnostic {
  kind : String
  flag : String
  message : String
} derive(Eq, Debug)

///|
pub(all) enum FlagError {
  ParseError(Int, String)
  UnknownFlag(Int, String)
} derive(Eq, Debug)

///|
pub fn evaluate(flag : Flag, context : EvalContext) -> Decision {
  let audit : Array[String] = []
  audit.push("Evaluating flag " + flag.name)

  if !flag.enabled {
    audit.push(
      "Flag is disabled, returning default variant " + flag.default_variant,
    )
    let def_enabled = flag.default_enabled
    return {
      flag: flag.name,
      enabled: def_enabled,
      variant: flag.default_variant,
      reason: "flag disabled",
      audit_trail: audit,
    }
  }

  // 1. Evaluate Deny rules first
  for rule in flag.rules {
    match rule {
      Deny(subject) =>
        if subject == context.subject {
          audit.push("Subject denied by rule: " + subject)
          let variant = if flag.is_multivariate {
            flag.default_variant
          } else {
            "off"
          }
          return {
            flag: flag.name,
            enabled: false,
            variant,
            reason: "subject denied by rule",
            audit_trail: audit,
          }
        }
      _ => ()
    }
  }

  // 2. Evaluate Allow rules next
  for rule in flag.rules {
    match rule {
      Allow(subject) =>
        if subject == context.subject {
          audit.push("Subject allowed by rule: " + subject)
          let variant = if flag.is_multivariate {
            if flag.variants.length() > 0 {
              flag.variants[0].name
            } else {
              "on"
            }
          } else {
            "on"
          }
          return {
            flag: flag.name,
            enabled: true,
            variant,
            reason: "subject allowed by rule",
            audit_trail: audit,
          }
        }
      AllowVariant(subject, variant) =>
        if subject == context.subject {
          audit.push("Subject allowed by rule for variant " + variant)
          return {
            flag: flag.name,
            enabled: true,
            variant,
            reason: "subject allowed variant rule",
            audit_trail: audit,
          }
        }
      _ => ()
    }
  }

  // 3. Evaluate Activation Conditions (When rules)
  for rule in flag.rules {
    match rule {
      When(key, op, val) => {
        let matched = match lookup_context(context, key) {
          Some(ctx_val) => eval_operator(ctx_val, op, val)
          None => false
        }
        if !matched {
          audit.push(
            "Activation condition not matched: " +
            key +
            " " +
            op_to_str(op) +
            " " +
            val,
          )
          let variant = if flag.is_multivariate {
            flag.default_variant
          } else {
            "off"
          }
          return {
            flag: flag.name,
            enabled: false,
            variant,
            reason: "condition not matched",
            audit_trail: audit,
          }
        }
        audit.push(
          "Activation condition matched: " +
          key +
          " " +
          op_to_str(op) +
          " " +
          val,
        )
      }
      _ => ()
    }
  }

  // 4. Evaluate WhenVariant rules
  for rule in flag.rules {
    match rule {
      WhenVariant(key, op, val, variant) => {
        let matched = match lookup_context(context, key) {
          Some(ctx_val) => eval_operator(ctx_val, op, val)
          None => false
        }
        if matched {
          audit.push(
            "Condition variant matched rule: " +
            key +
            " " +
            op_to_str(op) +
            " " +
            val +
            " -> " +
            variant,
          )
          let is_enabled = variant != flag.default_variant
          return {
            flag: flag.name,
            enabled: is_enabled,
            variant,
            reason: "condition variant matched rule",
            audit_trail: audit,
          }
        }
      }
      _ => ()
    }
  }

  // 5. Percentage Rollout evaluation
  if flag.is_multivariate {
    let score = bucket(context.subject, flag.name + ":" + flag.salt)
    audit.push("Subject bucket score: " + score.to_string())
    let mut sum = 0
    for v in flag.variants {
      sum += v.weight
      if score < sum {
        audit.push("Subject falls into variant bucket: " + v.name)
        let is_enabled = v.name != flag.default_variant
        return {
          flag: flag.name,
          enabled: is_enabled,
          variant: v.name,
          reason: "rollout bucket selection",
          audit_trail: audit,
        }
      }
    }
    audit.push(
      "Subject score outside all variant buckets, returning default variant " +
      flag.default_variant,
    )
    return {
      flag: flag.name,
      enabled: false,
      variant: flag.default_variant,
      reason: "rollout bucket selection fallback",
      audit_trail: audit,
    }
  } else {
    if flag.rollout <= 0 {
      audit.push("Boolean rollout is 0%, flag disabled")
      return {
        flag: flag.name,
        enabled: false,
        variant: "off",
        reason: "rollout bucket closed",
        audit_trail: audit,
      }
    }
    if flag.rollout >= 100 {
      audit.push("Boolean rollout is 100%, flag enabled")
      return {
        flag: flag.name,
        enabled: true,
        variant: "on",
        reason: "rollout bucket opened",
        audit_trail: audit,
      }
    }
    let score = bucket(context.subject, flag.name + ":" + flag.salt)
    audit.push(
      "Subject bucket score: " +
      score.to_string() +
      ", rollout: " +
      flag.rollout.to_string(),
    )
    if score < flag.rollout {
      return {
        flag: flag.name,
        enabled: true,
        variant: "on",
        reason: "rollout bucket opened",
        audit_trail: audit,
      }
    } else {
      return {
        flag: flag.name,
        enabled: false,
        variant: "off",
        reason: "rollout bucket closed",
        audit_trail: audit,
      }
    }
  }
}

///|
pub fn evaluate_all(
  flags : Array[Flag],
  context : EvalContext,
) -> Array[Decision] {
  let decisions : Array[Decision] = []
  for flag in flags {
    decisions.push(evaluate(flag, context))
  }
  decisions
}

///|
pub fn bucket(key : String, salt : String) -> Int {
  let input = key + "|" + salt
  let mut hash = 5381
  for c in input.iter() {
    hash = (hash * 33 + c.to_int()) % 1000003
    if hash < 0 {
      hash = 0 - hash
    }
  }
  hash % 100
}

///|
pub fn parse_flags(input : String) -> Result[Array[Flag], FlagError] {
  let mut flags : Array[Flag] = []
  let lines = input.split("\n").to_array()
  for i in 0..
        match parse_flag_line(tokens, line_number) {
          Ok(flag) => flags.push(flag)
          Err(err) => return Err(err)
        }
      "allow" =>
        match attach_rule(flags, tokens, line_number, "allow") {
          Ok(next) => flags = next
          Err(err) => return Err(err)
        }
      "deny" =>
        match attach_rule(flags, tokens, line_number, "deny") {
          Ok(next) => flags = next
          Err(err) => return Err(err)
        }
      "when" =>
        match attach_rule(flags, tokens, line_number, "when") {
          Ok(next) => flags = next
          Err(err) => return Err(err)
        }
      "allow_variant" =>
        match attach_rule(flags, tokens, line_number, "allow_variant") {
          Ok(next) => flags = next
          Err(err) => return Err(err)
        }
      "when_variant" =>
        match attach_rule(flags, tokens, line_number, "when_variant") {
          Ok(next) => flags = next
          Err(err) => return Err(err)
        }
      other => return Err(ParseError(line_number, "unknown directive " + other))
    }
  }
  Ok(flags)
}

///|
pub fn validate_flags(flags : Array[Flag]) -> Array[FlagDiagnostic] {
  let diagnostics : Array[FlagDiagnostic] = []
  for i in 0.. 0 {
        diagnostics.push({
          kind: "duplicate",
          flag: flag.name,
          message: "duplicate flag name " + flag.name,
        })
      }
    }
    if flag.rollout < 0 || flag.rollout > 100 {
      diagnostics.push({
        kind: "invalid_rollout",
        flag: flag.name,
        message: "rollout must be between 0 and 100",
      })
    }
    if flag.name.length() == 0 {
      diagnostics.push({
        kind: "empty_name",
        flag: flag.name,
        message: "flag name cannot be empty",
      })
    }
    if flag.is_multivariate {
      if flag.variants.length() == 0 {
        diagnostics.push({
          kind: "no_variants",
          flag: flag.name,
          message: "multivariate flag must define variants",
        })
      } else {
        let mut sum = 0
        for v in flag.variants {
          sum += v.weight
          if v.weight < 0 || v.weight > 100 {
            diagnostics.push({
              kind: "invalid_variant_weight",
              flag: flag.name,
              message: "variant " + v.name + " weight must be between 0 and 100",
            })
          }
        }
        if sum != 100 {
          diagnostics.push({
            kind: "invalid_weight_sum",
            flag: flag.name,
            message: "sum of variant weights must be exactly 100, got " +
            sum.to_string(),
          })
        }
      }
    }
  }
  diagnostics
}

///|
pub fn explain(decision : Decision) -> String {
  let state = if decision.enabled { "on" } else { "off" }
  let base = decision.flag +
    " " +
    state +
    " (variant: " +
    decision.variant +
    "): " +
    decision.reason
  if decision.audit_trail.length() > 0 {
    let mut trail = base + "\n  Audit Trail:"
    for step in decision.audit_trail {
      trail = trail + "\n    - " + step
    }
    trail
  } else {
    base
  }
}

///|
pub fn format_error(err : FlagError) -> String {
  match err {
    ParseError(line, message) => "line \{line}: " + message
    UnknownFlag(line, name) => "line \{line}: unknown flag " + name
  }
}

///|
fn lookup_context(context : EvalContext, key : String) -> String? {
  if key == "environment" || key == "env" {
    return Some(context.environment)
  }
  for attr in context.attributes {
    if attr.key == key {
      return Some(attr.value)
    }
  }
  None
}

///|
fn words(line : String) -> Array[String] {
  let parts : Array[String] = []
  for part in line.split(" ") {
    let token = part.trim().to_owned()
    if token.length() > 0 {
      parts.push(token)
    }
  }
  parts
}

///|
fn parse_flag_line(
  tokens : Array[String],
  line : Int,
) -> Result[Flag, FlagError] {
  if tokens.length() < 4 {
    return Err(ParseError(line, "flag requires name enabled and rollout"))
  }
  let name = tokens[1]
  if !tokens[2].has_prefix("enabled=") {
    return Err(ParseError(line, "expected enabled=true|false token"))
  }
  let enabled_text = after(tokens[2], "enabled=")
  let enabled = if enabled_text == "true" {
    true
  } else if enabled_text == "false" {
    false
  } else {
    return Err(ParseError(line, "expected enabled=true|false token"))
  }
  if !tokens[3].has_prefix("rollout=") {
    return Err(ParseError(line, "expected rollout=0..100 token"))
  }
  let rollout_text = after(tokens[3], "rollout=")
  let rollout = match parse_int(rollout_text) {
    Some(value) => value
    None => return Err(ParseError(line, "rollout must be an integer"))
  }
  let mut salt = name
  let mut is_multivariate = false
  let mut default_variant = if enabled { "on" } else { "off" }
  let variants : Array[Variant] = []

  for i in 4.. {
            let weight = match parse_int(v_weight_str.to_owned()) {
              Some(w) => w
              None =>
                return Err(
                  ParseError(line, "variant weight must be an integer"),
                )
            }
            variants.push({ name: v_name.to_owned(), weight })
          }
          None =>
            return Err(
              ParseError(line, "variants must be formatted as name:weight"),
            )
        }
      }
    }
  }

  Ok({
    name,
    enabled,
    default_enabled: false,
    rollout,
    salt,
    rules: [],
    is_multivariate,
    variants,
    default_variant,
  })
}

///|
fn attach_rule(
  flags : Array[Flag],
  tokens : Array[String],
  line : Int,
  directive : String,
) -> Result[Array[Flag], FlagError] {
  if tokens.length() < 3 {
    return Err(ParseError(line, directive + " requires flag name and value"))
  }
  let flag_name = tokens[1]

  let rule = match directive {
    "allow" => Allow(tokens[2])
    "deny" => Deny(tokens[2])
    "when" =>
      match parse_condition(tokens[2], line) {
        Ok(cond) => When(cond.0, cond.1, cond.2)
        Err(err) => return Err(err)
      }
    "allow_variant" => {
      if tokens.length() < 4 {
        return Err(
          ParseError(
            line, "allow_variant requires flag name, subject and variant name",
          ),
        )
      }
      AllowVariant(tokens[2], tokens[3])
    }
    "when_variant" => {
      if tokens.length() < 4 {
        return Err(
          ParseError(
            line, "when_variant requires flag name, condition and variant name",
          ),
        )
      }
      match parse_condition(tokens[2], line) {
        Ok(cond) => WhenVariant(cond.0, cond.1, cond.2, tokens[3])
        Err(err) => return Err(err)
      }
    }
    _ => return Err(ParseError(line, "unknown rule directive"))
  }

  let out : Array[Flag] = []
  let mut found = false
  for flag in flags {
    if flag.name == flag_name {
      let rules : Array[Rule] = []
      for existing in flag.rules {
        rules.push(existing)
      }
      rules.push(rule)
      out.push({
        name: flag.name,
        enabled: flag.enabled,
        default_enabled: flag.default_enabled,
        rollout: flag.rollout,
        salt: flag.salt,
        rules,
        is_multivariate: flag.is_multivariate,
        variants: flag.variants,
        default_variant: flag.default_variant,
      })
      found = true
    } else {
      out.push(flag)
    }
  }
  if !found {
    return Err(UnknownFlag(line, flag_name))
  }
  Ok(out)
}

///|
fn parse_condition(
  token : String,
  line : Int,
) -> Result[(String, Operator, String), FlagError] {
  if token.contains(">=") {
    let parts = split_once(token, ">=")
    return Ok((parts.0, Ge, parts.1))
  }
  if token.contains("<=") {
    let parts = split_once(token, "<=")
    return Ok((parts.0, Le, parts.1))
  }
  if token.contains("!=") {
    let parts = split_once(token, "!=")
    return Ok((parts.0, Ne, parts.1))
  }
  if token.contains("~=") {
    let parts = split_once(token, "~=")
    return Ok((parts.0, Contains, parts.1))
  }
  if token.contains("=") {
    let parts = split_once(token, "=")
    return Ok((parts.0, Eq, parts.1))
  }
  if token.contains(">") {
    let parts = split_once(token, ">")
    return Ok((parts.0, Gt, parts.1))
  }
  if token.contains("<") {
    let parts = split_once(token, "<")
    return Ok((parts.0, Lt, parts.1))
  }
  if token.contains(":in:") {
    let parts = split_once(token, ":in:")
    return Ok((parts.0, In, parts.1))
  }
  if token.contains(":not_in:") {
    let parts = split_once(token, ":not_in:")
    return Ok((parts.0, NotIn, parts.1))
  }

  Err(ParseError(line, "invalid condition format " + token))
}

///|
fn split_once(token : String, sep : String) -> (String, String) {
  match token.split_once(sep[:]) {
    Some((a, b)) => (a.to_owned(), b.to_owned())
    None => (token, "")
  }
}

///|
fn after(input : String, prefix : String) -> String {
  input[prefix.length():].to_owned()
}

///|
fn parse_int(input : String) -> Int? {
  if input.length() == 0 {
    return None
  }
  let mut value = 0
  for c in input.iter() {
    if c < '0' || c > '9' {
      return None
    }
    value = value * 10 + c.to_int() - '0'.to_int()
  }
  Some(value)
}

///|
fn op_to_str(op : Operator) -> String {
  match op {
    Eq => "="
    Ne => "!="
    Gt => ">"
    Lt => "<"
    Ge => ">="
    Le => "<="
    Contains => "~="
    In => "in"
    NotIn => "not_in"
  }
}

///|
fn eval_operator(ctx_val : String, op : Operator, rule_val : String) -> Bool {
  match op {
    Eq => ctx_val == rule_val
    Ne => ctx_val != rule_val
    Gt => ctx_val > rule_val
    Lt => ctx_val < rule_val
    Ge => ctx_val >= rule_val
    Le => ctx_val <= rule_val
    Contains => ctx_val.contains(rule_val[:])
    In => {
      let mut found = false
      for part in rule_val.split(",") {
        if part.trim().to_owned() == ctx_val {
          found = true
          break
        }
      }
      found
    }
    NotIn => {
      let mut found = false
      for part in rule_val.split(",") {
        if part.trim().to_owned() == ctx_val {
          found = true
          break
        }
      }
      !found
    }
  }
}

///|
pub trait ConfigProvider {
  fn load_flags(self : Self) -> Result[Array[Flag], String]
}

///|
pub struct JsonConfigProvider {
  json_string : String
}

///|
pub fn JsonConfigProvider::new(json_string : String) -> JsonConfigProvider {
  { json_string, }
}

///|
fn parse_op_str(op_str : String) -> Operator? {
  match op_str {
    "Eq" | "=" => Some(Eq)
    "Ne" | "!=" => Some(Ne)
    "Gt" | ">" => Some(Gt)
    "Lt" | "<" => Some(Lt)
    "Ge" | ">=" => Some(Ge)
    "Le" | "<=" => Some(Le)
    "Contains" | "~=" => Some(Contains)
    "In" => Some(In)
    "NotIn" => Some(NotIn)
    _ => None
  }
}

///|
fn get_string(json : Json, key : String) -> String? {
  match json {
    Object(m) =>
      match m.get(key) {
        Some(String(s)) => Some(s)
        _ => None
      }
    _ => None
  }
}

///|
fn get_bool(json : Json, key : String) -> Bool? {
  match json {
    Object(m) =>
      match m.get(key) {
        Some(True) => Some(true)
        Some(False) => Some(false)
        _ => None
      }
    _ => None
  }
}

///|
fn get_int(json : Json, key : String) -> Int? {
  match json {
    Object(m) =>
      match m.get(key) {
        Some(Number(d, ..)) => Some(d.to_int())
        _ => None
      }
    _ => None
  }
}

///|
fn get_array(json : Json, key : String) -> Array[Json]? {
  match json {
    Object(m) =>
      match m.get(key) {
        Some(Array(arr)) => Some(arr)
        _ => None
      }
    _ => None
  }
}

///|
pub impl ConfigProvider for JsonConfigProvider with fn load_flags(
  self : JsonConfigProvider,
) -> Result[Array[Flag], String] {
  let json = @json.parse(self.json_string[:]) catch {
    e => return Err("JSON parse error: \{e}")
  }

  let flags : Array[Flag] = []

  let flags_json = match get_array(json, "flags") {
    Some(arr) => arr
    None => return Err("JSON config missing 'flags' array")
  }

  for flag_j in flags_json {
    let name = match get_string(flag_j, "name") {
      Some(s) => s
      None => return Err("flag missing 'name' field")
    }
    let enabled = match get_bool(flag_j, "enabled") {
      Some(b) => b
      None => true
    }
    let default_enabled = match get_bool(flag_j, "default_enabled") {
      Some(b) => b
      None => false
    }
    let rollout = match get_int(flag_j, "rollout") {
      Some(r) => r
      None => 100
    }
    let salt = match get_string(flag_j, "salt") {
      Some(s) => s
      None => name
    }
    let is_multivariate = match get_bool(flag_j, "is_multivariate") {
      Some(b) => b
      None => false
    }
    let default_variant = match get_string(flag_j, "default_variant") {
      Some(s) => s
      None => "off"
    }

    let variants : Array[Variant] = []
    if is_multivariate {
      match get_array(flag_j, "variants") {
        Some(variants_j) =>
          for v_j in variants_j {
            let v_name = match get_string(v_j, "name") {
              Some(s) => s
              None => return Err("variant missing 'name' field")
            }
            let v_weight = match get_int(v_j, "weight") {
              Some(w) => w
              None => return Err("variant missing 'weight' field")
            }
            variants.push({ name: v_name, weight: v_weight })
          }
        None => ()
      }
    }

    let rules : Array[Rule] = []
    match get_array(flag_j, "rules") {
      Some(rules_j) =>
        for rule_j in rules_j {
          let r_type = match get_string(rule_j, "type") {
            Some(s) => s
            None => return Err("rule missing 'type' field")
          }
          match r_type {
            "Allow" => {
              let subject = match get_string(rule_j, "subject") {
                Some(s) => s
                None => return Err("Allow rule missing 'subject'")
              }
              rules.push(Allow(subject))
            }
            "Deny" => {
              let subject = match get_string(rule_j, "subject") {
                Some(s) => s
                None => return Err("Deny rule missing 'subject'")
              }
              rules.push(Deny(subject))
            }
            "AllowVariant" => {
              let subject = match get_string(rule_j, "subject") {
                Some(s) => s
                None => return Err("AllowVariant rule missing 'subject'")
              }
              let variant = match get_string(rule_j, "variant") {
                Some(s) => s
                None => return Err("AllowVariant rule missing 'variant'")
              }
              rules.push(AllowVariant(subject, variant))
            }
            "When" => {
              let key = match get_string(rule_j, "key") {
                Some(s) => s
                None => return Err("When rule missing 'key'")
              }
              let op_str = match get_string(rule_j, "op") {
                Some(s) => s
                None => return Err("When rule missing 'op'")
              }
              let op = match parse_op_str(op_str) {
                Some(o) => o
                None =>
                  return Err("When rule has invalid operator '" + op_str + "'")
              }
              let value = match get_string(rule_j, "value") {
                Some(s) => s
                None => return Err("When rule missing 'value'")
              }
              rules.push(When(key, op, value))
            }
            "WhenVariant" => {
              let key = match get_string(rule_j, "key") {
                Some(s) => s
                None => return Err("WhenVariant rule missing 'key'")
              }
              let op_str = match get_string(rule_j, "op") {
                Some(s) => s
                None => return Err("WhenVariant rule missing 'op'")
              }
              let op = match parse_op_str(op_str) {
                Some(o) => o
                None =>
                  return Err(
                    "WhenVariant rule has invalid operator '" + op_str + "'",
                  )
              }
              let value = match get_string(rule_j, "value") {
                Some(s) => s
                None => return Err("WhenVariant rule missing 'value'")
              }
              let variant = match get_string(rule_j, "variant") {
                Some(s) => s
                None => return Err("WhenVariant rule missing 'variant'")
              }
              rules.push(WhenVariant(key, op, value, variant))
            }
            _ => return Err("unknown rule type '" + r_type + "'")
          }
        }
      None => ()
    }

    flags.push({
      name,
      enabled,
      default_enabled,
      rollout,
      salt,
      rules,
      is_multivariate,
      variants,
      default_variant,
    })
  }

  Ok(flags)
}