pub mod data;

pub mod builtins;
pub mod calcit;
pub mod call_graph_diff;
pub mod call_stack;
pub mod call_tree;
pub mod cli_args;
pub mod codegen;
pub mod def_diff;
pub mod detailed_snapshot;
pub mod program;
pub mod program_diff;
pub mod runner;
pub mod snapshot;
pub mod util;

use calcit::{CalcitErrKind, LocatedWarning};
use call_stack::CallStackList;
use std::cell::RefCell;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

pub use calcit::{
  Calcit, CalcitErr, CalcitFnTypeAnnotation, CalcitProc, CalcitSyntax, CalcitTypeAnnotation, ProcTypeSignature, SyntaxTypeSignature,
};

use crate::util::string::strip_shebang;

pub const DEFAULT_SNAPSHOT_FILE: &str = "calcit.cirru";
pub const LEGACY_SNAPSHOT_FILE: &str = "compact.cirru";

static QUIET_TOOL_OUTPUT: AtomicBool = AtomicBool::new(false);

pub fn set_quiet_tool_output(v: bool) {
  QUIET_TOOL_OUTPUT.store(v, Ordering::Relaxed);
}

pub fn quiet_tool_output() -> bool {
  QUIET_TOOL_OUTPUT.load(Ordering::Relaxed)
}

fn core_snapshot_schema_needs_fallback(snapshot: &snapshot::Snapshot) -> bool {
  let Some(core_file) = snapshot.files.get("calcit.core") else {
    return true;
  };
  let Some(map_entry) = core_file.defs.get("map") else {
    return true;
  };
  let CalcitTypeAnnotation::Fn(fn_annot) = map_entry.schema.as_ref() else {
    return true;
  };

  fn_annot.where_bounds.is_empty() || !matches!(fn_annot.arg_types.get(1).map(|arg| arg.as_ref()), Some(CalcitTypeAnnotation::Fn(_)))
}

fn load_core_snapshot_from_embedded_source() -> Result<snapshot::Snapshot, String> {
  let content = include_str!("../src/cirru/calcit-core.cirru");
  let data = cirru_edn::parse(content).map_err(|e| format!("Failed to parse embedded core snapshot source: {e}"))?;
  snapshot::load_snapshot_data(&data, "calcit-internal://calcit-core.cirru")
}

fn overlay_core_schemas_from_source(snapshot: &mut snapshot::Snapshot) -> Result<(), String> {
  let source_snapshot = load_core_snapshot_from_embedded_source()?;

  for (ns, source_file) in &source_snapshot.files {
    let Some(target_file) = snapshot.files.get_mut(ns) else {
      continue;
    };

    for (def, source_entry) in &source_file.defs {
      let Some(target_entry) = target_file.defs.get_mut(def) else {
        continue;
      };

      if !matches!(source_entry.schema.as_ref(), CalcitTypeAnnotation::Dynamic) {
        target_entry.schema = source_entry.schema.clone();
      }
    }
  }

  Ok(())
}

pub fn load_core_snapshot() -> Result<snapshot::Snapshot, String> {
  // load core libs
  let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/calcit-core.rmp"));
  let mut snapshot = snapshot::decode_binary_snapshot(bytes).map_err(|e| {
    eprintln!("\n{e}");
    "Failed to deserialize core snapshot".to_string()
  })?;
  if core_snapshot_schema_needs_fallback(&snapshot) {
    overlay_core_schemas_from_source(&mut snapshot)?;
  }
  let path = "calcit-internal://calcit-core.cirru";
  let meta_ns = format!("{}.$meta", snapshot.package);
  snapshot.files.insert(meta_ns.to_owned(), snapshot::gen_meta_ns(&meta_ns, path));
  Ok(snapshot)
}

#[derive(Clone, Debug)]
pub struct ProgramEntries {
  pub init_fn: Arc<str>,
  pub init_ns: Arc<str>,
  pub init_def: Arc<str>,
  pub reload_fn: Arc<str>,
  pub reload_ns: Arc<str>,
  pub reload_def: Arc<str>,
}

pub fn run_program(init_ns: Arc<str>, init_def: Arc<str>, params: &[Calcit]) -> Result<Calcit, CalcitErr> {
  run_program_with_docs(init_ns, init_def, params)
}

pub fn resolve_snapshot_path_alias(path: &Path) -> PathBuf {
  if path.exists() {
    return path.to_path_buf();
  }

  match path.file_name().and_then(|name| name.to_str()) {
    Some(DEFAULT_SNAPSHOT_FILE) => {
      let fallback = path.with_file_name(LEGACY_SNAPSHOT_FILE);
      if fallback.exists() { fallback } else { path.to_path_buf() }
    }
    _ => path.to_path_buf(),
  }
}

fn module_path_candidates(path: &str) -> Vec<String> {
  if path.ends_with('/') {
    vec![format!("{path}{DEFAULT_SNAPSHOT_FILE}"), format!("{path}{LEGACY_SNAPSHOT_FILE}")]
  } else if path.ends_with(DEFAULT_SNAPSHOT_FILE) {
    vec![
      path.to_string(),
      format!(
        "{}{}",
        path.strip_suffix(DEFAULT_SNAPSHOT_FILE).unwrap_or(path),
        LEGACY_SNAPSHOT_FILE
      ),
    ]
  } else {
    vec![path.to_string()]
  }
}

fn materialize_module_path(file_path: &str, base_dir: &Path, module_folder: &Path) -> PathBuf {
  if file_path.starts_with("./") {
    base_dir.join(file_path)
  } else if file_path.starts_with('/') {
    Path::new(file_path).to_owned()
  } else {
    module_folder.join(file_path)
  }
}

fn module_candidate_display_path(file_path: &str, fullpath: &Path, module_folder: &Path) -> String {
  if file_path.starts_with("./") {
    file_path.to_string()
  } else if file_path.starts_with('/') {
    if let Ok(stripped) = fullpath.strip_prefix(module_folder) {
      format!("<mods>/{}", stripped.display())
    } else {
      file_path.to_string()
    }
  } else {
    format!("<mods>/{file_path}")
  }
}

pub fn resolve_module_snapshot_candidates(path: &str, base_dir: &Path, module_folder: &Path) -> Vec<(String, PathBuf, String)> {
  let candidates = module_path_candidates(path);
  let mut items = candidates
    .iter()
    .map(|candidate| {
      let fullpath = materialize_module_path(candidate, base_dir, module_folder);
      let display_path = module_candidate_display_path(candidate, &fullpath, module_folder);
      (candidate.clone(), fullpath, display_path)
    })
    .collect::<Vec<_>>();

  if !items.iter().any(|(_, fullpath, _)| fullpath.exists())
    && let Some((file_path, fullpath, display_path)) = items.first().cloned()
  {
    return vec![(file_path, fullpath, display_path)];
  }

  items.retain(|(_, fullpath, _)| fullpath.exists());
  items
}

pub fn resolve_module_snapshot_path(path: &str, base_dir: &Path, module_folder: &Path) -> (String, PathBuf, String) {
  resolve_module_snapshot_candidates(path, base_dir, module_folder)
    .into_iter()
    .next()
    .unwrap_or_else(|| {
      let fullpath = materialize_module_path(path, base_dir, module_folder);
      let display_path = module_candidate_display_path(path, &fullpath, module_folder);
      (path.to_string(), fullpath, display_path)
    })
}

pub fn run_program_with_docs(init_ns: Arc<str>, init_def: Arc<str>, params: &[Calcit]) -> Result<Calcit, CalcitErr> {
  let check_warnings = RefCell::new(LocatedWarning::default_list());

  match runner::preprocess::ensure_ns_def_compiled(&init_ns, &init_def, &check_warnings, &CallStackList::default()) {
    Ok(()) => {}
    Err(failure) => {
      eprintln!("\nfailed preprocessing, {failure}");
      let headline = failure.headline();
      call_stack::display_stack_with_docs(&headline, &failure.stack, failure.location.as_ref(), failure.hint.as_deref())?;
      return CalcitErr::err_str(failure.kind, headline);
    }
  };

  let warnings = check_warnings.borrow();
  if !warnings.is_empty() {
    return Err(CalcitErr {
      kind: CalcitErrKind::Unexpected,
      msg: format!("Found {} warnings, runner blocked", warnings.len()),
      code: None,
      warnings: Box::new(warnings.to_owned()),
      stack: CallStackList::default(),
      location: None,
      hint: None,
    });
  }

  match runner::evaluate_symbol_from_program(&init_def, &init_ns, None, &CallStackList::default()) {
    Ok(entry) => match entry {
      Calcit::Fn { info, .. } => {
        let result = runner::run_fn(params, &info, &CallStackList::default());
        match result {
          Ok(v) => Ok(v),
          Err(failure) => {
            call_stack::display_stack_with_docs(&failure.msg, &failure.stack, failure.location.as_ref(), failure.hint.as_deref())?;
            Err(failure)
          }
        }
      }
      _ => CalcitErr::err_str(CalcitErrKind::Type, format!("expected function entry, got: {entry}")),
    },
    Err(failure) => {
      call_stack::display_stack_with_docs(&failure.msg, &failure.stack, failure.location.as_ref(), failure.hint.as_deref())?;
      Err(failure)
    }
  }
}

pub fn load_module(path: &str, base_dir: &Path, module_folder: &Path) -> Result<snapshot::Snapshot, String> {
  let candidates = resolve_module_snapshot_candidates(path, base_dir, module_folder);
  let mut last_error: Option<String> = None;

  for (_, fullpath, display_path) in candidates {
    let mut content = match fs::read_to_string(&fullpath) {
      Ok(content) => content,
      Err(e) => {
        last_error = Some(format!("Failed to read {}: {e}", fullpath.display()));
        continue;
      }
    };

    strip_shebang(&mut content);
    let data = match cirru_edn::parse(&content) {
      Ok(data) => data,
      Err(e) => {
        last_error = Some(format!("Failed to parse file '{}': {e}", fullpath.display()));
        continue;
      }
    };

    match snapshot::load_snapshot_data(&data, &fullpath.display().to_string()) {
      Ok(snapshot) => {
        if !quiet_tool_output() {
          println!("loading: {display_path}");
        }
        return Ok(snapshot);
      }
      Err(e) => {
        last_error = Some(format!("Failed to load snapshot '{}': {e}", fullpath.display()));
      }
    }
  }

  Err(last_error.unwrap_or_else(|| format!("expected Cirru snapshot for module path: {path}")))
}
