///|
/// Sort order available to the local review page and other consumers.
pub(all) enum ReviewSort {
  PathAscending
  NameAscending
  SizeDescending
  SimilarityAscending
  CategoryThenPath
} derive(Eq)

///|
pub extend ReviewSort with Eq::{not_equal, equal}

///|
pub(all) struct ReviewQuery {
  text : String
  kind : Kind?
  minimum_size : Int32?
  maximum_size : Int32?
  sort : ReviewSort
  offset : Int32
  page_size : Int32
}

///|
pub(all) struct ReviewPage {
  rows : Array[ReviewRow]
  total_matches : Int32
  offset : Int32
  page_size : Int32
  page_count : Int32
  has_previous : Bool
  has_next : Bool
}

///|
/// Default query for a fresh report view: all rows by original path, first
/// 100 entries. Pagination is available to consumers that render large scans.
pub fn default_review_query() -> ReviewQuery {
  {
    text: "",
    kind: None,
    minimum_size: None,
    maximum_size: None,
    sort: PathAscending,
    offset: 0,
    page_size: 100,
  }
}

///|
fn query_matches(row : ReviewRow, query : ReviewQuery, needle : String) -> Bool {
  let matches_text = needle == "" ||
    row.path.to_lower().contains(needle) ||
    row.name.to_lower().contains(needle) ||
    row.kind.label().contains(needle)
  let matches_kind = match query.kind {
    Some(kind) => row.kind == kind
    None => true
  }
  let matches_minimum = match query.minimum_size {
    Some(minimum) => row.size >= minimum
    None => true
  }
  let matches_maximum = match query.maximum_size {
    Some(maximum) => row.size <= maximum
    None => true
  }
  matches_text && matches_kind && matches_minimum && matches_maximum
}

///|
fn kind_order(kind : Kind) -> Int32 {
  match kind {
    New => 0
    Similar => 1
    Conflict => 2
    Exact => 3
    Unreadable => 4
  }
}

///|
fn distance_order(row : ReviewRow) -> Int32 {
  match row.similarity_distance {
    Some(distance) => distance
    None => 178
  }
}

///|
fn review_row_precedes(
  left : ReviewRow,
  right : ReviewRow,
  sort : ReviewSort,
) -> Bool {
  match sort {
    PathAscending =>
      left.path.lexical_compare(right.path) < 0 ||
      (left.path == right.path && left.name.lexical_compare(right.name) < 0)
    NameAscending =>
      left.name.lexical_compare(right.name) < 0 ||
      (left.name == right.name && left.path.lexical_compare(right.path) < 0)
    SizeDescending =>
      left.size > right.size ||
      (left.size == right.size && left.path.lexical_compare(right.path) < 0)
    SimilarityAscending =>
      distance_order(left) < distance_order(right) ||
      (
        distance_order(left) == distance_order(right) &&
        left.path.lexical_compare(right.path) < 0
      )
    CategoryThenPath =>
      kind_order(left.kind) < kind_order(right.kind) ||
      (
        kind_order(left.kind) == kind_order(right.kind) &&
        left.path.lexical_compare(right.path) < 0
      )
  }
}

///|
/// Filter, sort and page plan rows. Negative size bounds are treated as zero;
/// an inverted range returns an empty page. Page sizes are clamped to 1–500.
pub fn query_review_rows(
  rows : Array[ReviewRow],
  query : ReviewQuery,
) -> ReviewPage {
  let minimum = match query.minimum_size {
    Some(value) => Some(value.max(0))
    None => None
  }
  let maximum = match query.maximum_size {
    Some(value) => Some(value.max(0))
    None => None
  }
  let normalized = {
    text: query.text,
    kind: query.kind,
    minimum_size: minimum,
    maximum_size: maximum,
    sort: query.sort,
    offset: query.offset.max(0),
    page_size: query.page_size.max(1).min(500),
  }
  let filtered : Array[ReviewRow] = []
  let needle = normalized.text.trim().to_lower().to_owned()
  let invalid_range = match (minimum, maximum) {
    (Some(minimum_size), Some(maximum_size)) => minimum_size > maximum_size
    _ => false
  }
  if !invalid_range {
    for row in rows {
      if query_matches(row, normalized, needle) {
        filtered.push(row)
      }
    }
  }
  filtered.sort_by((left, right) => {
    if review_row_precedes(left, right, normalized.sort) {
      -1
    } else if review_row_precedes(right, left, normalized.sort) {
      1
    } else {
      0
    }
  })
  let total = filtered.length()
  let page_size = normalized.page_size
  let start = normalized.offset.min(total)
  let end = (start + page_size).min(total)
  let selected : Array[ReviewRow] = []
  for index = start; index < end; index = index + 1 {
    selected.push(filtered[index])
  }
  {
    rows: selected,
    total_matches: total,
    offset: start,
    page_size,
    page_count: if total == 0 {
      0
    } else {
      (total + page_size - 1) / page_size
    },
    has_previous: start > 0,
    has_next: end < total,
  }
}

///|
/// A lightweight helper for consumers that want to show page numbers.
pub fn review_page_start(page_number : Int32, page_size : Int32) -> Int32 {
  (page_number.max(1) - 1) * page_size.max(1).min(500)
}

///|
/// Report the one-based page containing a row offset.
pub fn review_page_number(offset : Int32, page_size : Int32) -> Int32 {
  offset.max(0) / page_size.max(1).min(500) + 1
}