// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 clbbbb

///|
pub struct Policy {
  allowed : Array[String]
  denied : Array[String]
  allow_copyleft : Bool
  require_osi : Bool
} derive(Debug, Eq)

///|
pub fn permissive_policy() -> Policy {
  {
    allowed: [
      "MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "Zlib", "0BSD",
    ],
    denied: [],
    allow_copyleft: false,
    require_osi: true,
  }
}

///|
pub fn strict_policy(allowed : Array[String]) -> Policy {
  {
    allowed: normalize_license_list(allowed),
    denied: [],
    allow_copyleft: false,
    require_osi: true,
  }
}

///|
pub fn normalize_license_list(items : Array[String]) -> Array[String] {
  let result : Array[String] = []
  for item in items {
    result.push(canonical_license(item))
  }
  unique_strings(result)
}

///|
pub fn policy_errors(text : String, policy : Policy) -> Array[String] {
  match parse(text).expr {
    Some(expr) => policy_errors_expr(expr, policy)
    None => ["parse error: " + parse(text).error]
  }
}

///|
pub fn policy_errors_expr(expr : Expr, policy : Policy) -> Array[String] {
  match expr {
    License(id) => policy_errors_license(id, policy)
    LicenseWithException(id, ex) => policy_errors_with_exception(id, ex, policy)
    And(left, right) =>
      unique_strings(
        policy_errors_expr(left, policy) + policy_errors_expr(right, policy),
      )
    Or(left, right) => {
      let left_errors = policy_errors_expr(left, policy)
      let right_errors = policy_errors_expr(right, policy)
      if left_errors.is_empty() || right_errors.is_empty() {
        []
      } else {
        unique_strings(
          prefix_errors("left option: ", left_errors) +
          prefix_errors("right option: ", right_errors),
        )
      }
    }
  }
}

///|
pub fn policy_errors_with_exception(
  id : String,
  ex : String,
  policy : Policy,
) -> Array[String] {
  let errors = policy_errors_license(id, policy)
  if !is_known_exception(ex) {
    errors.push("unknown exception: " + ex)
  }
  errors
}

///|
pub fn policy_errors_license(id : String, policy : Policy) -> Array[String] {
  let errors : Array[String] = []
  if !policy.allowed.is_empty() && !contains_string(policy.allowed, id) {
    errors.push("license not allowed: " + id)
  }
  if contains_string(policy.denied, id) {
    errors.push("license denied: " + id)
  }
  if policy.require_osi && !is_osi_license(id) {
    errors.push("license is not OSI-approved in catalog: " + id)
  }
  if !policy.allow_copyleft && is_copyleft_license(id) {
    errors.push("copyleft license not allowed: " + id)
  }
  errors
}

///|
pub fn prefix_errors(prefix : String, errors : Array[String]) -> Array[String] {
  let result : Array[String] = []
  for item in errors {
    result.push(prefix + item)
  }
  result
}

///|
pub fn policy_report(text : String, policy : Policy) -> String {
  let errors = policy_errors(text, policy)
  if errors.is_empty() {
    "ok"
  } else {
    join_lines(errors)
  }
}

///|
pub fn policy_allows(text : String, policy : Policy) -> Bool {
  policy_errors(text, policy).is_empty()
}

///|
pub fn policy_option_expressions(expr : Expr) -> Array[Expr] {
  match expr {
    Or(left, right) =>
      policy_option_expressions(left) + policy_option_expressions(right)
    And(left, right) =>
      combine_policy_options(
        policy_option_expressions(left),
        policy_option_expressions(right),
      )
    _ => [expr]
  }
}

///|
pub fn combine_policy_options(
  left : Array[Expr],
  right : Array[Expr],
) -> Array[Expr] {
  let rows : Array[Expr] = []
  for left_expr in left {
    for right_expr in right {
      rows.push(And(left_expr, right_expr))
    }
  }
  rows
}

///|
pub fn allowed_policy_options(text : String, policy : Policy) -> Array[String] {
  match parse(text).expr {
    Some(expr) => {
      let rows : Array[String] = []
      for option in policy_option_expressions(expr) {
        if policy_errors_expr(option, policy).is_empty() {
          rows.push(format(option))
        }
      }
      rows
    }
    None => []
  }
}

///|
pub fn policy_option_report(text : String, policy : Policy) -> String {
  match parse(text).expr {
    None => "parse error: " + parse(text).error
    Some(expr) => {
      let rows : Array[String] = ["options:"]
      for option in policy_option_expressions(expr) {
        let errors = policy_errors_expr(option, policy)
        if errors.is_empty() {
          rows.push("[x] " + format(option))
        } else {
          rows.push("[ ] " + format(option) + " - " + join_with(errors, "; "))
        }
      }
      join_lines(rows)
    }
  }
}