// Copyright (c) HashiCorp, Inc.
// Copyright (c) 2025 International Digital Economy Academy
// SPDX-License-Identifier: MPL-2.0

///|
/// Operator types for version constraints
pub enum Operator {
  Equal
  NotEqual
  GreaterThan
  LessThan
  GreaterThanEqual
  LessThanEqual
  Pessimistic
} derive(Eq, Show)

///|
/// Constraint represents a single constraint for a version, such as ">= 1.0"
pub struct Constraint {
  op : Operator
  version : Version
  original : String
} derive(Eq, Show)

///|
/// Constraints is an array of constraints
pub typealias Array[Constraint] as Constraints

///|
/// Create a new constraint from a string like ">= 1.0", "< 2.0", etc.
pub fn Constraint::new(
  constraint_str : String,
) -> Constraint raise VersionError {
  let trimmed = constraint_str.trim_space().to_string()

  // Check for empty constraint
  if trimmed.length() == 0 {
    raise VersionError(
      "malformed constraint: cannot parse empty constraint string",
    )
  }

  // Parse operator and version
  let (op, version_str) = if trimmed.strip_prefix(">=") is Some(remaining) {
    (Operator::GreaterThanEqual, remaining)
  } else if trimmed.strip_prefix("<=") is Some(remaining) {
    (Operator::LessThanEqual, remaining)
  } else if trimmed.strip_prefix("!=") is Some(remaining) {
    (Operator::NotEqual, remaining)
  } else if trimmed.strip_prefix("~>") is Some(remaining) {
    (Operator::Pessimistic, remaining)
  } else if trimmed.strip_prefix(">") is Some(remaining) {
    (Operator::GreaterThan, remaining)
  } else if trimmed.strip_prefix("<") is Some(remaining) {
    (Operator::LessThan, remaining)
  } else if trimmed.strip_prefix("=") is Some(remaining) {
    (Operator::Equal, remaining)
  } else {
    (Operator::Equal, trimmed)
  }
  let cleaned_version_str = version_str.trim_space().to_string()
  if cleaned_version_str.length() == 0 {
    raise VersionError(
      "malformed constraint '\{constraint_str}': missing version after operator",
    )
  }
  let version = Version::new(cleaned_version_str) catch {
    e => raise VersionError("malformed constraint '\{constraint_str}': \{e}")
  }
  { op, version, original: constraint_str }
}

///|
/// Create constraints from a comma-separated string like ">= 1.0, < 2.0"
pub fn constraints_new(
  constraints_str : String,
) -> Array[Constraint] raise VersionError {
  let parts = constraints_str.split(",")
  let constraints : Array[Constraint] = []
  for part in parts {
    let constraint = Constraint::new(part.trim_space().to_string())
    constraints.push(constraint)
  }
  constraints
}

///|
/// MustConstraints is a helper that wraps a call to constraints_new
/// and panics if error is non-nil
pub fn constraints_must(constraints_str : String) -> Array[Constraint] {
  constraints_new(constraints_str) catch {
    e => abort("Constraint parse error: \{e}")
  }
}

///|
/// Check if a version satisfies this constraint
pub fn Constraint::check(self : Constraint, version : Version) -> Bool {
  match self.op {
    Operator::Equal => version.equal(self.version)
    Operator::NotEqual => !version.equal(self.version)
    Operator::GreaterThan => check_greater_than(version, self.version)
    Operator::LessThan => check_less_than(version, self.version)
    Operator::GreaterThanEqual =>
      check_greater_than_equal(version, self.version)
    Operator::LessThanEqual => check_less_than_equal(version, self.version)
    Operator::Pessimistic => check_pessimistic(version, self.version)
  }
}

///|
/// Check if a version satisfies all constraints
pub fn constraints_check(
  constraints : Array[Constraint],
  version : Version,
) -> Bool {
  for constraint in constraints {
    if !constraint.check(version) {
      return false
    }
  }
  true
}

///|
/// Helper function for greater than constraint with prerelease handling
fn check_greater_than(version : Version, constraint_version : Version) -> Bool {
  if !prerelease_check(version, constraint_version) {
    return false
  }
  version.compare(constraint_version) > 0
}

///|
/// Helper function for less than constraint with prerelease handling
fn check_less_than(version : Version, constraint_version : Version) -> Bool {
  if !prerelease_check(version, constraint_version) {
    return false
  }
  version.compare(constraint_version) < 0
}

///|
/// Helper function for greater than or equal constraint with prerelease handling
fn check_greater_than_equal(
  version : Version,
  constraint_version : Version,
) -> Bool {
  if !prerelease_check(version, constraint_version) {
    return false
  }
  version.compare(constraint_version) >= 0
}

///|
/// Helper function for less than or equal constraint with prerelease handling
fn check_less_than_equal(
  version : Version,
  constraint_version : Version,
) -> Bool {
  if !prerelease_check(version, constraint_version) {
    return false
  }
  version.compare(constraint_version) <= 0
}

///|
/// Helper function for prerelease constraint checking (matches Go's prereleaseCheck)
/// Note: For pessimistic constraints, we use relaxed segment checking
fn prerelease_check_relaxed(
  version : Version,
  constraint_version : Version,
  is_pessimistic : Bool,
) -> Bool {
  let v_pre = version.prerelease() != ""
  let c_pre = constraint_version.prerelease() != ""
  match (c_pre, v_pre) {
    (true, true) =>
      if is_pessimistic {
        // For pessimistic constraints with prerelease, we only check that both have prerelease
        // The actual version bounds will be checked by the pessimistic logic
        true
      } else {
        // A constraint with a pre-release can only match a pre-release version
        // with the same base segments.
        version.equal_segments(constraint_version)
      }
    (false, true) =>
      // A constraint without a pre-release can only match a version without a
      // pre-release.
      false
    (true, false) =>
      // OK, except with the pessimistic operator (will be handled by caller)
      true
    (false, false) =>
      // OK
      true
  }
}

///|
/// Helper function for prerelease constraint checking (matches Go's prereleaseCheck)
fn prerelease_check(version : Version, constraint_version : Version) -> Bool {
  prerelease_check_relaxed(version, constraint_version, false)
}

///|
/// Helper function for pessimistic constraint (~>)
/// This allows patch-level changes if a patch version is specified,
/// or minor-level changes if no patch version is specified.
fn check_pessimistic(version : Version, constraint_version : Version) -> Bool {
  // Using a pessimistic constraint with a pre-release, restricts versions to pre-releases
  if !prerelease_check_relaxed(version, constraint_version, true) ||
    (constraint_version.prerelease() != "" && version.prerelease() == "") {
    return false
  }

  // If the version being checked is naturally less than the constraint, then there
  // is no way for the version to be valid against the constraint
  if version.less_than(constraint_version) {
    return false
  }
  let v_segments = version.segments64()
  let c_segments = constraint_version.segments64()

  // We'll use this more than once, so grab the length now so it's a little cleaner
  // to write the later checks
  let cs = c_segments.length()

  // If the version being checked has less specificity than the constraint, then there
  // is no way for the version to be valid against the constraint
  if cs > v_segments.length() {
    return false
  }

  // For pessimistic constraints, we need to check that:
  // 1. All segments except the last one in the constraint must match exactly
  // 2. The last segment in the constraint must be <= the corresponding segment in version
  // 
  // For ~> 1.2, constraint_version.si = 2, so we check:
  // - segment 0 (major) must be equal
  // - segment 1 (minor) can be >= but we need to ensure version doesn't exceed next major level

  // Check all segments from 0 to constraint_version.si-1 for exact match except the last one
  for i = 0; i < constraint_version.si - 1; i = i + 1 {
    if v_segments[i] != c_segments[i] {
      return false
    }
  }

  // For the last specified segment in constraint, version must be >= constraint
  if constraint_version.si > 0 &&
    c_segments[constraint_version.si - 1] >
    v_segments[constraint_version.si - 1] {
    return false
  }

  // Additional check: ensure version doesn't exceed the "next" version level
  // Based on Go's test cases, the semantics seem to be based on the precision specified:
  // ~> 1.0 means >= 1.0.0 and < 2.0.0 (segment 1 is 0, so increment major)
  // ~> 1.2 means >= 1.2.0 and < 1.3.0 (segment 1 is non-zero, so increment minor)
  // ~> 1.2.3 means >= 1.2.3 and < 1.3.0 (segment 2 is non-zero, but still increment minor)
  if constraint_version.si >= 1 {
    let next_version_segments = c_segments.copy()
    // Determine which level to increment based on the constraint's precision
    let increment_index = if c_segments.length() >= 2 && c_segments[1] != 0L {
      1 // If minor version is specified (non-zero), increment minor
    } else {
      0 // If only major is specified (minor is 0), increment major
    }
    next_version_segments[increment_index] = next_version_segments[increment_index] +
      1L
    // Set all segments after increment_index to 0
    for i = increment_index + 1; i < next_version_segments.length(); i = i + 1 {
      next_version_segments[i] = 0L
    }

    // Check if version >= next_version (if so, reject)
    // Use standard version comparison: returns -1, 0, 1 for <, =, >
    let mut compare_result = 0
    for i = 0
        i < next_version_segments.length() && i < v_segments.length()
        i = i + 1 {
      if v_segments[i] < next_version_segments[i] {
        compare_result = -1
        break
      } else if v_segments[i] > next_version_segments[i] {
        compare_result = 1
        break
      }
      // If equal, continue to next segment
    }

    // If all compared segments are equal, check array lengths
    if compare_result == 0 {
      if v_segments.length() < next_version_segments.length() {
        compare_result = -1
      } else if v_segments.length() > next_version_segments.length() {
        compare_result = 1
      }
      // If equal lengths and all segments equal, compare_result remains 0
    }

    // Reject if version >= next_version
    if compare_result >= 0 {
      return false
    }
  }

  // If nothing has rejected the version by now, it's valid
  true
}

///|
/// Convert constraint to string
pub fn Constraint::to_string(self : Constraint) -> String {
  self.original
}

///|
/// Convert constraints to string
pub fn constraints_to_string(constraints : Array[Constraint]) -> String {
  let parts : Array[String] = []
  for constraint in constraints {
    parts.push(constraint.to_string())
  }
  parts.join(", ")
}

///|
/// Check if constraint has prerelease
pub fn Constraint::has_prerelease(self : Constraint) -> Bool {
  self.version.prerelease().length() > 0
}

///|
/// Check if two constraints are equal
pub fn Constraint::equals(self : Constraint, other : Constraint) -> Bool {
  self.op == other.op && self.version.equal(other.version)
}

///|
/// Check if two constraint arrays are equal
pub fn constraints_equal(
  left : Array[Constraint],
  right : Array[Constraint],
) -> Bool {
  if left.length() != right.length() {
    return false
  }

  // Create sorted copies
  let left_sorted : Array[Constraint] = []
  let right_sorted : Array[Constraint] = []
  for c in left {
    left_sorted.push(c)
  }
  for c in right {
    right_sorted.push(c)
  }

  // Sort by operator first, then by version
  left_sorted.sort_by(fn(a, b) {
    let op_cmp = compare_operators(a.op, b.op)
    if op_cmp != 0 {
      op_cmp
    } else {
      a.version.compare(b.version)
    }
  })
  right_sorted.sort_by(fn(a, b) {
    let op_cmp = compare_operators(a.op, b.op)
    if op_cmp != 0 {
      op_cmp
    } else {
      a.version.compare(b.version)
    }
  })

  // Compare sorted arrays
  for i = 0; i < left_sorted.length(); i = i + 1 {
    if !left_sorted[i].equals(right_sorted[i]) {
      return false
    }
  }
  true
}

///|
/// Helper function to compare operators for sorting
fn compare_operators(a : Operator, b : Operator) -> Int {
  let a_val = match a {
    Operator::Equal => 0
    Operator::NotEqual => 1
    Operator::GreaterThan => 2
    Operator::LessThan => 3
    Operator::GreaterThanEqual => 4
    Operator::LessThanEqual => 5
    Operator::Pessimistic => 6
  }
  let b_val = match b {
    Operator::Equal => 0
    Operator::NotEqual => 1
    Operator::GreaterThan => 2
    Operator::LessThan => 3
    Operator::GreaterThanEqual => 4
    Operator::LessThanEqual => 5
    Operator::Pessimistic => 6
  }
  if a_val < b_val {
    -1
  } else if a_val > b_val {
    1
  } else {
    0
  }
}

///|
/// Create a constraint that accepts any version >= the given version
pub fn constraint_at_least(
  version_str : String,
) -> Constraint raise VersionError {
  Constraint::new(">= \{version_str}")
}

///|
/// Create a constraint that accepts any version < the given version
pub fn constraint_below(version_str : String) -> Constraint raise VersionError {
  Constraint::new("< \{version_str}")
}

///|
/// Create a constraint for an exact version match
pub fn constraint_exactly(
  version_str : String,
) -> Constraint raise VersionError {
  Constraint::new("= \{version_str}")
}

///|
/// Create a pessimistic constraint (~>) for the given version
pub fn constraint_pessimistic(
  version_str : String,
) -> Constraint raise VersionError {
  Constraint::new("~> \{version_str}")
}

///|
/// Create a range constraint ">=min,  Array[Constraint] raise VersionError {
  constraints_new(">= \{min_version}, < \{max_version}")
}

///|
/// Check if constraints allow any version (i.e., are not overly restrictive)
pub fn constraints_allow_any(constraints : Array[Constraint]) -> Bool {
  // Simple heuristic: if there are no constraints, any version is allowed
  constraints.length() == 0
}

///|
/// Get the strictest lower bound from a set of constraints
pub fn constraints_min_version(constraints : Array[Constraint]) -> Version? {
  let mut min_version : Version? = None
  for constraint in constraints {
    match constraint.op {
      Operator::GreaterThan | Operator::GreaterThanEqual =>
        match min_version {
          None => min_version = Some(constraint.version)
          Some(current_min) =>
            if constraint.version.greater_than(current_min) {
              min_version = Some(constraint.version)
            }
        }
      _ => continue
    }
  }
  min_version
}

///|
/// Get the strictest upper bound from a set of constraints
pub fn constraints_max_version(constraints : Array[Constraint]) -> Version? {
  let mut max_version : Version? = None
  for constraint in constraints {
    match constraint.op {
      Operator::LessThan | Operator::LessThanEqual =>
        match max_version {
          None => max_version = Some(constraint.version)
          Some(current_max) =>
            if constraint.version.less_than(current_max) {
              max_version = Some(constraint.version)
            }
        }
      _ => continue
    }
  }
  max_version
}