use core::cmp::Ordering;
use std::cell::RefCell;
use std::sync::Arc;

use rpds::HashTrieSet;

use crate::calcit::{Calcit, CalcitErr, CalcitErrKind, CalcitList, CalcitTuple};
use crate::util::number::f64_to_usize;

use crate::builtins;
use crate::call_stack::CallStackList;
use crate::runner;

pub fn new_list(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  Ok(Calcit::List(Arc::new(xs.into())))
}

pub fn count(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 1 {
    return CalcitErr::err_nodes(
      CalcitErrKind::Arity,
      "&list:count requires exactly 1 argument (a list), but received:",
      xs,
    );
  }
  match &xs[0] {
    Calcit::List(ys) => Ok(Calcit::Number(ys.len() as f64)),
    a => {
      let msg = format!(
        "&list:count requires a list as argument, but received a value of type: {}",
        crate::builtins::meta::type_of(&[a.clone()])?.lisp_str()
      );
      let hint = crate::calcit::format_proc_examples_hint(&crate::calcit::CalcitProc::NativeListCount).unwrap_or_else(|| {
        String::from(
          "💡 Hint: Use `count` function which works on multiple types including lists, or ensure you're passing a list to &list:count",
        )
      });
      CalcitErr::err_str_with_hint(CalcitErrKind::Type, msg, hint)
    }
  }
}

pub fn nth(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 2 {
    let msg = format!(
      "&list:nth requires exactly 2 arguments (list and index), but received {} arguments",
      xs.len()
    );
    let hint = String::from(
      "💡 Correct usage: `&list:nth ([] 1 2 3) 0` => 1\n  First argument: a list\n  Second argument: numeric index (0-based)",
    );
    return CalcitErr::err_nodes_with_hint(CalcitErrKind::Arity, msg, xs, hint);
  }
  match (&xs[0], &xs[1]) {
    (Calcit::List(ys), Calcit::Number(n)) => match f64_to_usize(*n) {
      Ok(idx) => match ys.get(idx) {
        Some(v) => Ok((*v).to_owned()),
        None => Ok(Calcit::Nil),
      },
      Err(e) => CalcitErr::err_str(
        CalcitErrKind::Type,
        format!("&list:nth requires a valid non-negative integer index, {e}"),
      ),
    },
    (_, _) => {
      let msg = format!(
        "&list:nth requires (list, number) but received: ({}, {})",
        crate::builtins::meta::type_of(&[xs[0].clone()])?.lisp_str(),
        crate::builtins::meta::type_of(&[xs[1].clone()])?.lisp_str()
      );
      let hint =
        String::from("💡 Correct usage: `&list:nth ([] 1 2 3) 0` => 1\n  Or use higher-level `nth` which works on multiple types");
      CalcitErr::err_str_with_hint(CalcitErrKind::Type, msg, hint)
    }
  }
}

pub fn slice(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 2 && xs.len() != 3 {
    return CalcitErr::err_str(
      CalcitErrKind::Arity,
      format!("&list:slice expected 2 or 3 arguments, but received: {}", CalcitList::from(xs)),
    );
  }
  match (&xs[0], &xs[1]) {
    (Calcit::List(ys), Calcit::Number(from)) => {
      let from_idx = f64_to_usize(*from)?;
      let to_idx = match xs.get(2) {
        Some(Calcit::Number(to)) => f64_to_usize(*to)?,
        Some(a) => {
          return CalcitErr::err_str(
            CalcitErrKind::Type,
            format!("&list:slice expected a number for index, but received: {a}"),
          );
        }
        None => ys.len(),
      };

      Ok(Calcit::List(Arc::new(ys.slice(from_idx, to_idx)?)))
    }
    (a, b) => CalcitErr::err_str(
      CalcitErrKind::Type,
      format!("&list:slice expected a list and numbers for indexes, but received: {a} {b}"),
    ),
  }
}

pub fn last(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 1 {
    return CalcitErr::err_nodes(CalcitErrKind::Arity, "&list:last expected 1 argument (a list), but received:", xs);
  }
  match &xs[0] {
    Calcit::List(ys) => {
      let n = ys.len();
      if n == 0 {
        Ok(Calcit::Nil)
      } else {
        match ys.get(n - 1) {
          Some(v) => Ok((*v).to_owned()),
          None => Ok(Calcit::Nil),
        }
      }
    }
    a => CalcitErr::err_str(CalcitErrKind::Type, format!("&list:last expected a list, but received: {a}")),
  }
}

pub fn append(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 2 {
    return CalcitErr::err_str(
      CalcitErrKind::Arity,
      format!("&list:append expected 2 arguments, but received: {}", CalcitList::from(xs)),
    );
  }
  match &xs[0] {
    Calcit::List(ys) => Ok(Calcit::List(Arc::new(ys.push_right(xs[1].to_owned())))),
    a => CalcitErr::err_str(CalcitErrKind::Type, format!("&list:append expected a list, but received: {a}")),
  }
}

pub fn prepend(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match (xs.first(), xs.get(1)) {
    (Some(Calcit::List(ys)), Some(a)) => Ok(Calcit::List(Arc::new(ys.push_left(a.to_owned())))),
    (Some(a), _) => CalcitErr::err_str(CalcitErrKind::Type, format!("&list:prepend expected a list, but received: {a}")),
    (None, _) => CalcitErr::err_str(CalcitErrKind::Arity, "&list:prepend expected 2 arguments, but received none"),
  }
}

pub fn rest(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 1 {
    let hint = String::from("💡 Usage: `&list:rest ([] 1 2 3)` => ([] 2 3)\n  Returns all elements except the first one");
    return CalcitErr::err_nodes_with_hint(
      CalcitErrKind::Arity,
      "&list:rest requires exactly 1 argument (a list), but received:",
      xs,
      hint,
    );
  }
  match &xs[0] {
    Calcit::List(ys) => {
      if ys.is_empty() {
        Ok(Calcit::Nil)
      } else {
        Ok(Calcit::List(Arc::new(ys.drop_left())))
      }
    }
    a => {
      let msg = format!(
        "&list:rest requires a list as argument, but received: {}",
        crate::builtins::meta::type_of(&[a.clone()])?.lisp_str()
      );
      let hint = String::from(
        "💡 Hint: Use the higher-level `rest` function which works on multiple types,\n  or ensure you're passing a list to &list:rest",
      );
      CalcitErr::err_str_with_hint(CalcitErrKind::Type, msg, hint)
    }
  }
}

pub fn butlast(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 1 {
    return CalcitErr::err_nodes(CalcitErrKind::Arity, "&list:butlast expected a list, but received:", xs);
  }
  match &xs[0] {
    Calcit::Nil => Ok(Calcit::Nil),
    Calcit::List(ys) => {
      if ys.is_empty() {
        Ok(Calcit::Nil)
      } else {
        Ok(Calcit::List(Arc::new(ys.butlast()?)))
      }
    }
    a => CalcitErr::err_str(CalcitErrKind::Type, format!("&list:butlast expected a list, but received: {a}")),
  }
}

pub fn concat(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  let mut total_size = 0;
  for x in xs {
    if let Calcit::List(zs) = x {
      total_size += zs.len();
    } else {
      return CalcitErr::err_str(
        CalcitErrKind::Type,
        format!("&list:concat expected list arguments, but received: {x}"),
      );
    }
  }

  let mut ys = Vec::with_capacity(total_size);
  for x in xs {
    if let Calcit::List(zs) = x {
      // Use extend to efficiently append elements from the inner list
      ys.extend(zs.iter().map(|v| v.to_owned()));
    }
    // no need for else, already checked
  }
  Ok(Calcit::List(Arc::new(CalcitList::Vector(ys))))
}

pub fn range(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.is_empty() || xs.len() > 3 {
    return CalcitErr::err_nodes(CalcitErrKind::Arity, "&list:range expected 1 to 3 arguments, but received:", xs);
  }
  let (base, bound) = match (&xs[0], xs.get(1)) {
    (Calcit::Number(bound), None) => (0.0, *bound),
    (Calcit::Number(base), Some(Calcit::Number(bound))) => (*base, *bound),
    (a, b) => {
      return CalcitErr::err_str(
        CalcitErrKind::Type,
        format!("&list:range expected numbers for base and bound, but received: {a} {b:?}"),
      );
    }
  };

  let step = match xs.get(2) {
    Some(Calcit::Number(n)) => *n,
    Some(a) => {
      return CalcitErr::err_str(
        CalcitErrKind::Type,
        format!("&list:range expected a number for step, but received: {a}"),
      );
    }
    None => 1.0,
  };

  if (bound - base).abs() < f64::EPSILON {
    return Ok(Calcit::from(CalcitList::default()));
  }

  if step == 0.0 || (bound > base && step < 0.0) || (bound < base && step > 0.0) {
    return CalcitErr::err_str(
      CalcitErrKind::Unexpected,
      "&list:range cannot construct list with a step of 0 or invalid step direction",
    );
  }

  let mut ys = vec![];
  let mut i = base;
  if step > 0.0 {
    while i < bound {
      ys.push(Calcit::Number(i));
      i += step;
    }
  } else {
    while i > bound {
      ys.push(Calcit::Number(i));
      i += step;
    }
  }
  Ok(Calcit::from(ys))
}

pub fn reverse(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 1 {
    return CalcitErr::err_nodes(CalcitErrKind::Arity, "&list:reverse expected a list, but received:", xs);
  }
  match &xs[0] {
    Calcit::Nil => Ok(Calcit::Nil),
    Calcit::List(ys) => Ok(Calcit::from(ys.reverse())),
    a => CalcitErr::err_str(CalcitErrKind::Type, format!("&list:reverse expected a list, but received: {a}")),
  }
}

/// foldl using syntax for performance, it's supposed to be a function
pub fn foldl(xs: &[Calcit], call_stack: &CallStackList) -> Result<Calcit, CalcitErr> {
  if xs.len() == 3 {
    let mut ret = xs[1].to_owned();

    match (&xs[0], &xs[2]) {
      // dirty since only functions being call directly then we become fast
      (Calcit::List(xs), Calcit::Fn { info, .. }) => {
        for x in xs.iter() {
          ret = runner::run_fn(&[ret, (*x).to_owned()], info, call_stack)?;
        }
        Ok(ret)
      }
      (Calcit::List(xs), Calcit::Proc(proc)) => {
        for x in xs.iter() {
          // println!("foldl args, {} {}", ret, x.to_owned());
          ret = builtins::handle_proc(*proc, &[ret, (*x).to_owned()], call_stack)?;
        }
        Ok(ret)
      }
      // also handles set
      (Calcit::Set(xs), Calcit::Fn { info, .. }) => {
        for x in xs {
          ret = runner::run_fn(&[ret, x.to_owned()], info, call_stack)?;
        }
        Ok(ret)
      }
      (Calcit::Set(xs), Calcit::Proc(proc)) => {
        for x in xs {
          // println!("foldl args, {} {}", ret, x.to_owned());
          ret = builtins::handle_proc(*proc, &[ret, x.to_owned()], call_stack)?;
        }
        Ok(ret)
      }
      // also handles map
      (Calcit::Map(xs), Calcit::Fn { info, .. }) => {
        for (k, x) in xs {
          ret = runner::run_fn(
            &[ret, Calcit::from(CalcitList::from(&[k.to_owned(), x.to_owned()]))],
            info,
            call_stack,
          )?;
        }
        Ok(ret)
      }
      (Calcit::Map(xs), Calcit::Proc(proc)) => {
        for (k, x) in xs {
          // println!("foldl args, {} {}", ret, x.to_owned());
          ret = builtins::handle_proc(
            *proc,
            &[ret, Calcit::from(CalcitList::from(&[k.to_owned(), x.to_owned()]))],
            call_stack,
          )?;
        }
        Ok(ret)
      }

      (a, b) => Err(CalcitErr::use_msg_stack_location(
        CalcitErrKind::Type,
        format!("&list:foldl expected a list and a function, but received: {a} {b}"),
        call_stack,
        a.get_location().or_else(|| b.get_location()),
      )),
    }
  } else {
    Err(CalcitErr::use_msg_stack(
      CalcitErrKind::Arity,
      format!("&list:foldl expected 3 arguments, but received: {}", CalcitList::from(xs)),
      call_stack,
    ))
  }
}

/// foldl-shortcut using syntax for performance, it's supposed to be a function
/// by returning `:: bool acc`, bool indicates where performace a shortcut return
pub fn foldl_shortcut(xs: &[Calcit], call_stack: &CallStackList) -> Result<Calcit, CalcitErr> {
  if xs.len() == 4 {
    let acc = &xs[1];
    let default_value = &xs[2];
    match (&xs[0], &xs[3]) {
      // dirty since only functions being call directly then we become fast
      (Calcit::List(xs), Calcit::Fn { info, .. }) => {
        let mut state = acc.to_owned();
        for x in xs.iter() {
          let pair = runner::run_fn(&[state.to_owned(), (*x).to_owned()], info, call_stack)?;
          match pair {
            Calcit::Tuple(CalcitTuple { tag: x0, extra, .. }) => match &*x0 {
              Calcit::Bool(b) => {
                let x1 = extra.first().ok_or(CalcitErr::use_msg_stack_location(
                  CalcitErrKind::Arity,
                  "&list:foldl-shortcut expected a value in the tuple",
                  call_stack,
                  x0.get_location(),
                ))?;
                if *b {
                  return Ok((*x1).to_owned());
                } else {
                  x1.clone_into(&mut state)
                }
              }
              a => {
                return Err(CalcitErr::use_msg_stack_location(
                  CalcitErrKind::Type,
                  format!("&list:foldl-shortcut return value must be a boolean, but received: {a}"),
                  call_stack,
                  a.get_location(),
                ));
              }
            },
            _ => {
              return Err(CalcitErr::use_msg_stack(
                CalcitErrKind::Type,
                format!("&list:foldl-shortcut return value must be `:: boolean accumulator`, but received: {pair}"),
                call_stack,
              ));
            }
          }
        }
        Ok(default_value.to_owned())
      }
      // almost identical body, except for the type
      (Calcit::Set(xs), Calcit::Fn { info, .. }) => {
        let mut state = acc.to_owned();
        for x in xs {
          let pair = runner::run_fn(&[state.to_owned(), x.to_owned()], info, call_stack)?;
          match pair {
            Calcit::Tuple(CalcitTuple { tag: x0, extra, .. }) => match &*x0 {
              Calcit::Bool(b) => {
                let x1 = extra.first().ok_or(CalcitErr::use_msg_stack_location(
                  CalcitErrKind::Arity,
                  "&list:foldl-shortcut expected a value in the tuple",
                  call_stack,
                  x0.get_location(),
                ))?;
                if *b {
                  return Ok((*x1).to_owned());
                } else {
                  x1.clone_into(&mut state)
                }
              }
              a => {
                return Err(CalcitErr::use_msg_stack_location(
                  CalcitErrKind::Type,
                  format!("&list:foldl-shortcut return value must be a boolean, but received: {a}"),
                  call_stack,
                  a.get_location(),
                ));
              }
            },
            _ => {
              return Err(CalcitErr::use_msg_stack(
                CalcitErrKind::Type,
                format!("&list:foldl-shortcut return value must be `:: boolean accumulator`, but received: {pair}"),
                call_stack,
              ));
            }
          }
        }
        Ok(default_value.to_owned())
      }
      // almost identical body, escept for the type
      (Calcit::Map(xs), Calcit::Fn { info, .. }) => {
        let mut state = acc.to_owned();
        for (k, x) in xs {
          let pair = runner::run_fn(
            &[state.to_owned(), Calcit::from(CalcitList::from(&[k.to_owned(), x.to_owned()]))],
            info,
            call_stack,
          )?;
          match pair {
            Calcit::Tuple(CalcitTuple { tag: x0, extra, .. }) => match &*x0 {
              Calcit::Bool(b) => {
                let x1 = extra.first().ok_or(CalcitErr::use_msg_stack_location(
                  CalcitErrKind::Arity,
                  "&list:foldl-shortcut expected a value in the tuple",
                  call_stack,
                  x0.get_location(),
                ))?;
                if *b {
                  return Ok((*x1).to_owned());
                } else {
                  (*x1).clone_into(&mut state)
                }
              }
              a => {
                return Err(CalcitErr::use_msg_stack_location(
                  CalcitErrKind::Type,
                  format!("&list:foldl-shortcut return value must be a boolean, but received: {a}"),
                  call_stack,
                  a.get_location(),
                ));
              }
            },
            _ => {
              return Err(CalcitErr::use_msg_stack(
                CalcitErrKind::Type,
                format!("&list:foldl-shortcut return value must be `:: boolean accumulator`, but received: {pair}"),
                call_stack,
              ));
            }
          }
        }
        Ok(default_value.to_owned())
      }

      (a, b) => Err(CalcitErr::use_msg_stack_location(
        CalcitErrKind::Type,
        format!("&list:foldl-shortcut expected a list and a function, but received: {a} {b}"),
        call_stack,
        a.get_location().or_else(|| b.get_location()),
      )),
    }
  } else {
    Err(CalcitErr::use_msg_stack(
      CalcitErrKind::Arity,
      format!(
        "&list:foldl-shortcut expected 4 arguments (list, state, default, fn), but received: {}",
        CalcitList::from(xs)
      ),
      call_stack,
    ))
  }
}

pub fn foldr_shortcut(xs: &[Calcit], call_stack: &CallStackList) -> Result<Calcit, CalcitErr> {
  if xs.len() == 4 {
    // let xs = runner::evaluate_expr(&expr[0], scope, file_ns)?;
    let acc = &xs[1];
    let default_value = &xs[2];
    // let f = runner::evaluate_expr(&expr[3], scope, file_ns)?;
    match (&xs[0], &xs[3]) {
      // dirty since only functions being call directly then we become fast
      (Calcit::List(xs), Calcit::Fn { info, .. }) => {
        let mut state = acc.to_owned();
        let size = xs.len();
        for i in 0..size {
          let x = xs[size - 1 - i].to_owned();
          let pair = runner::run_fn(&[state.to_owned(), x.to_owned()], info, call_stack)?;
          match pair {
            Calcit::Tuple(CalcitTuple { tag: x0, extra, .. }) => match &*x0 {
              Calcit::Bool(b) => {
                let x1 = extra.first().ok_or(CalcitErr::use_msg_stack_location(
                  CalcitErrKind::Arity,
                  "&list:foldr-shortcut expected a value in the tuple",
                  call_stack,
                  x0.get_location(),
                ))?;
                if *b {
                  return Ok((*x1).to_owned());
                } else {
                  (*x1).clone_into(&mut state)
                }
              }
              a => {
                return Err(CalcitErr::use_msg_stack_location(
                  CalcitErrKind::Type,
                  format!("&list:foldr-shortcut return value must be a boolean, but received: {a}"),
                  call_stack,
                  a.get_location(),
                ));
              }
            },
            _ => {
              return Err(CalcitErr::use_msg_stack(
                CalcitErrKind::Type,
                format!("&list:foldr-shortcut return value must be `:: boolean accumulator`, but received: {pair}"),
                call_stack,
              ));
            }
          }
        }
        Ok(default_value.to_owned())
      }

      (a, b) => Err(CalcitErr::use_msg_stack_location(
        CalcitErrKind::Type,
        format!("&list:foldr-shortcut expected a list and a function, but received: {a} {b}"),
        call_stack,
        a.get_location().or_else(|| b.get_location()),
      )),
    }
  } else {
    Err(CalcitErr::use_msg_stack(
      CalcitErrKind::Arity,
      format!(
        "&list:foldr-shortcut expected 4 arguments (list, state, default, fn), but received: {}",
        CalcitList::from(xs)
      ),
      call_stack,
    ))
  }
}

pub fn sort(xs: &[Calcit], call_stack: &CallStackList) -> Result<Calcit, CalcitErr> {
  let comparator_result_to_ordering = |result: Result<Calcit, CalcitErr>, error_slot: &RefCell<Option<CalcitErr>>| -> Ordering {
    match result {
      Ok(Calcit::Number(x)) if x < 0.0 => Ordering::Less,
      Ok(Calcit::Number(x)) if x > 0.0 => Ordering::Greater,
      Ok(Calcit::Number(_)) => Ordering::Equal,
      Ok(v) => {
        *error_slot.borrow_mut() = Some(CalcitErr::use_msg_stack(
          CalcitErrKind::Type,
          format!("&list:sort comparator must return a number, but received: {v}"),
          call_stack,
        ));
        Ordering::Equal
      }
      Err(e) => {
        *error_slot.borrow_mut() = Some(CalcitErr::use_msg_stack(
          CalcitErrKind::Unexpected,
          format!("&list:sort failed: {e}"),
          call_stack,
        ));
        Ordering::Equal
      }
    }
  };

  match xs {
    [Calcit::List(values)] => {
      let mut next_values: Vec<Calcit> = values.to_vec();
      next_values.sort();
      Ok(Calcit::List(Arc::new(CalcitList::Vector(next_values))))
    }
    [Calcit::List(values), Calcit::Fn { info, .. }] => {
      let mut next_values: Vec<Calcit> = values.to_vec();
      let sorting_error: RefCell<Option<CalcitErr>> = RefCell::new(None);
      next_values.sort_by(|a, b| -> Ordering {
        if sorting_error.borrow().is_some() {
          return Ordering::Equal;
        }
        let result = runner::run_fn(&[(*a).to_owned(), (*b).to_owned()], info, call_stack);
        comparator_result_to_ordering(result, &sorting_error)
      });
      if let Some(e) = sorting_error.into_inner() {
        return Err(e);
      }
      Ok(Calcit::List(Arc::new(CalcitList::Vector(next_values))))
    }
    [Calcit::List(values), Calcit::Proc(proc)] => {
      let mut next_values: Vec<Calcit> = values.to_vec();
      let sorting_error: RefCell<Option<CalcitErr>> = RefCell::new(None);
      next_values.sort_by(|a, b| -> Ordering {
        if sorting_error.borrow().is_some() {
          return Ordering::Equal;
        }
        let result = builtins::handle_proc(*proc, &[(*a).to_owned(), (*b).to_owned()], call_stack);
        comparator_result_to_ordering(result, &sorting_error)
      });
      if let Some(e) = sorting_error.into_inner() {
        return Err(e);
      }
      Ok(Calcit::List(Arc::new(CalcitList::Vector(next_values))))
    }
    [a, b] => Err(CalcitErr::use_msg_stack_location(
      CalcitErrKind::Type,
      format!("&list:sort expected a list and a function, but received: {a} {b}"),
      call_stack,
      a.get_location().or_else(|| b.get_location()),
    )),
    _ => Err(CalcitErr::use_msg_stack(
      CalcitErrKind::Arity,
      format!(
        "&list:sort expected 1 or 2 arguments, but received: {}",
        Calcit::List(Arc::new(xs.into()))
      ),
      call_stack,
    )),
  }
}

pub fn first(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 1 {
    return CalcitErr::err_str(CalcitErrKind::Arity, "&list:first expected 1 argument, but received none");
  }
  match &xs[0] {
    Calcit::List(ys) => {
      if ys.is_empty() {
        Ok(Calcit::Nil)
      } else {
        Ok((ys[0]).to_owned())
      }
    }
    a => CalcitErr::err_str(CalcitErrKind::Type, format!("&list:first expected a list, but received: {a}")),
  }
}

// real implementation relies of ternary-tree
pub fn assoc_before(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 3 {
    return CalcitErr::err_nodes(CalcitErrKind::Arity, "&list:assoc-before expected 3 arguments, but received:", xs);
  }
  match (&xs[0], &xs[1]) {
    (Calcit::List(zs), Calcit::Number(n)) => match f64_to_usize(*n) {
      Ok(idx) => {
        // let ys = insert(zs, idx, xs[2].to_owned());
        Ok(Calcit::List(Arc::new(zs.assoc_before(idx, xs[2].to_owned())?)))
      }
      Err(e) => CalcitErr::err_str(CalcitErrKind::Type, format!("&list:assoc-before expected a valid index, {e}")),
    },
    (a, b) => CalcitErr::err_str(
      CalcitErrKind::Type,
      format!("&list:assoc-before expected a list and an index, but received: {a} {b}"),
    ),
  }
}

pub fn assoc_after(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 3 {
    return CalcitErr::err_nodes(CalcitErrKind::Arity, "&list:assoc-after expected 3 arguments, but received:", xs);
  }
  match (&xs[0], &xs[1]) {
    (Calcit::List(zs), Calcit::Number(n)) => match f64_to_usize(*n) {
      Ok(idx) => {
        // let ys = insert(zs, idx + 1, xs[2].to_owned());
        Ok(Calcit::from(zs.assoc_after(idx, xs[2].to_owned())?))
      }
      Err(e) => CalcitErr::err_str(CalcitErrKind::Type, format!("&list:assoc-after expected a valid index, {e}")),
    },
    (a, b) => CalcitErr::err_str(
      CalcitErrKind::Type,
      format!("&list:assoc-after expected a list and an index, but received: {a} {b}"),
    ),
  }
}

pub fn list_ques(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 1 {
    return CalcitErr::err_nodes(CalcitErrKind::Arity, "list? expects exactly 1 argument, but received:", xs);
  }
  Ok(Calcit::Bool(matches!(&xs[0], Calcit::List(_))))
}

pub fn empty_ques(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 1 {
    return CalcitErr::err_nodes(CalcitErrKind::Arity, "&list:empty? expected a list, but received:", xs);
  }
  match &xs[0] {
    Calcit::List(ys) => Ok(Calcit::Bool(ys.is_empty())),
    a => CalcitErr::err_str(CalcitErrKind::Type, format!("&list:empty? expected a list, but received: {a}")),
  }
}

pub fn contains_ques(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 2 {
    return CalcitErr::err_nodes(
      CalcitErrKind::Arity,
      "&list:contains? expected a list and an index, but received:",
      xs,
    );
  }
  match (&xs[0], &xs[1]) {
    (Calcit::List(xs), Calcit::Number(n)) => match f64_to_usize(*n) {
      Ok(idx) => Ok(Calcit::Bool(idx < xs.len())),
      Err(_) => Ok(Calcit::Bool(false)),
    },
    (a, b) => CalcitErr::err_str(
      CalcitErrKind::Type,
      format!("&list:contains? expected a list and an index, but received: {a} {b}"),
    ),
  }
}

pub fn includes_ques(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  match (xs.first(), xs.get(1)) {
    (Some(Calcit::List(xs)), Some(a)) => Ok(Calcit::Bool(xs.index_of(a).is_some())),
    (Some(a), ..) => CalcitErr::err_str(
      CalcitErrKind::Type,
      format!("&list:includes? expected a list and a value, but received: {a}"),
    ),
    (None, ..) => CalcitErr::err_nodes(CalcitErrKind::Arity, "&list:includes? expected 2 arguments, but received:", xs),
  }
}

pub fn assoc(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 3 {
    return CalcitErr::err_nodes(CalcitErrKind::Arity, "&list:assoc expected 3 arguments, but received:", xs);
  }
  match (&xs[0], &xs[1]) {
    (Calcit::List(zs), Calcit::Number(n)) => match f64_to_usize(*n) {
      Ok(idx) => {
        if idx < zs.len() {
          let mut ys: CalcitList = (**zs).to_owned();
          // ys[idx] = xs[2].to_owned();
          ys = ys.assoc(idx, xs[2].to_owned())?;
          Ok(Calcit::from(ys))
        } else {
          Ok(Calcit::List(Arc::new(xs.into())))
        }
      }
      Err(e) => CalcitErr::err_str(CalcitErrKind::Type, e),
    },
    (a, b) => CalcitErr::err_str(
      CalcitErrKind::Type,
      format!("&list:assoc expected a list and an index, but received: {a} {b}"),
    ),
  }
}

pub fn dissoc(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 2 {
    return CalcitErr::err_nodes(CalcitErrKind::Arity, "&list:dissoc expected 2 arguments, but received:", xs);
  }
  match (&xs[0], &xs[1]) {
    (Calcit::List(xs), Calcit::Number(n)) => match f64_to_usize(*n) {
      Ok(at) => Ok(Calcit::from(xs.dissoc(at)?)),
      Err(e) => CalcitErr::err_str(CalcitErrKind::Type, format!("&list:dissoc expected a valid index, {e}")),
    },
    (Calcit::List(_xs), a) => CalcitErr::err_str(
      CalcitErrKind::Type,
      format!("&list:dissoc expected a number for index, but received: {a}"),
    ),
    (a, ..) => CalcitErr::err_str(CalcitErrKind::Type, format!("&list:dissoc expected a list, but received: {a}")),
  }
}

pub fn list_to_set(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 1 {
    return CalcitErr::err_nodes(CalcitErrKind::Arity, "&list:to-set expected 1 argument, but received:", xs);
  }
  match &xs[0] {
    Calcit::List(ys) => {
      let mut zs = rpds::HashTrieSet::new_sync();
      ys.traverse(&mut |y| {
        zs.insert_mut(y.to_owned());
      });
      Ok(Calcit::Set(zs))
    }
    a => CalcitErr::err_str(CalcitErrKind::Type, format!("&list:to-set expected a list, but received: {a}")),
  }
}

pub fn distinct(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 1 {
    return CalcitErr::err_nodes(CalcitErrKind::Arity, "&list:distinct expected 1 argument, but received:", xs);
  }
  match &xs[0] {
    Calcit::List(ys) => {
      let mut seen = HashTrieSet::new_sync();
      let mut zs = CalcitList::new_inner();
      ys.traverse(&mut |y| {
        if !seen.contains(y) {
          seen.insert_mut(y.to_owned());
          zs = zs.push_right(y.to_owned());
        }
      });
      Ok(Calcit::from(CalcitList::List(zs)))
    }
    a => CalcitErr::err_str(CalcitErrKind::Type, format!("&list:distinct expected a list, but received: {a}")),
  }
}

// === BufList (mutable append-only list) ===

use std::sync::Mutex;

/// Create a new empty BufList: `(&buf-list:new)`
pub fn buf_list_new(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if !xs.is_empty() {
    return CalcitErr::err_str(CalcitErrKind::Arity, format!("&buf-list:new expects 0 args, got {}", xs.len()));
  }
  Ok(Calcit::BufList(Arc::new(Mutex::new(Vec::new()))))
}

/// Push a single element: `(&buf-list:push buf item)` — mutates and returns the same buf
pub fn buf_list_push(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 2 {
    return CalcitErr::err_str(CalcitErrKind::Arity, format!("&buf-list:push expects 2 args, got {}", xs.len()));
  }
  match &xs[0] {
    Calcit::BufList(buf) => {
      let mut items = buf.lock().expect("BufList lock");
      items.push(xs[1].to_owned());
      Ok(xs[0].to_owned())
    }
    a => CalcitErr::err_str(CalcitErrKind::Type, format!("&buf-list:push expects a buf-list, got: {a}")),
  }
}

/// Append all elements from a list: `(&buf-list:concat buf list)` — mutates and returns the same buf
pub fn buf_list_concat(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 2 {
    return CalcitErr::err_str(CalcitErrKind::Arity, format!("&buf-list:concat expects 2 args, got {}", xs.len()));
  }
  match (&xs[0], &xs[1]) {
    (Calcit::BufList(buf), Calcit::List(list)) => {
      let mut items = buf.lock().expect("BufList lock");
      for item in list.iter() {
        items.push(item.to_owned());
      }
      Ok(xs[0].to_owned())
    }
    (Calcit::BufList(_), b) => CalcitErr::err_str(CalcitErrKind::Type, format!("&buf-list:concat expects list as 2nd arg, got: {b}")),
    (a, _) => CalcitErr::err_str(CalcitErrKind::Type, format!("&buf-list:concat expects a buf-list, got: {a}")),
  }
}

/// Freeze into an immutable list: `(&buf-list:to-list buf)`
pub fn buf_list_to_list(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 1 {
    return CalcitErr::err_str(CalcitErrKind::Arity, format!("&buf-list:to-list expects 1 arg, got {}", xs.len()));
  }
  match &xs[0] {
    Calcit::BufList(buf) => {
      let items = buf.lock().expect("BufList lock");
      Ok(Calcit::from(items.clone()))
    }
    a => CalcitErr::err_str(CalcitErrKind::Type, format!("&buf-list:to-list expects a buf-list, got: {a}")),
  }
}

/// Get element count: `(&buf-list:count buf)`
pub fn buf_list_count(xs: &[Calcit]) -> Result<Calcit, CalcitErr> {
  if xs.len() != 1 {
    return CalcitErr::err_str(CalcitErrKind::Arity, format!("&buf-list:count expects 1 arg, got {}", xs.len()));
  }
  match &xs[0] {
    Calcit::BufList(buf) => {
      let items = buf.lock().expect("BufList lock");
      Ok(Calcit::Number(items.len() as f64))
    }
    a => CalcitErr::err_str(CalcitErrKind::Type, format!("&buf-list:count expects a buf-list, got: {a}")),
  }
}

#[cfg(test)]
mod tests {
  use super::*;
  use crate::calcit::CalcitProc;

  #[test]
  fn sort_returns_error_when_comparator_not_number() {
    let values = Calcit::from(vec![Calcit::Number(3.0), Calcit::Number(1.0), Calcit::Number(2.0)]);
    let comparator = Calcit::Proc(CalcitProc::Not);

    let result = sort(&[values, comparator], &CallStackList::default());
    assert!(result.is_err());
  }

  #[test]
  fn sort_accepts_single_argument_natural_order() {
    let values = Calcit::from(vec![Calcit::Number(3.0), Calcit::Number(1.0), Calcit::Number(2.0)]);

    let result = sort(&[values], &CallStackList::default()).expect("sort should support single list argument");
    assert_eq!(
      result,
      Calcit::from(vec![Calcit::Number(1.0), Calcit::Number(2.0), Calcit::Number(3.0)])
    );
  }
}
