//! Common utilities shared between CLI handlers

use super::cirru_validator;
use cirru_parser::Cirru;
use std::fs;
use std::sync::Arc;

// Error message constants
pub const ERR_MULTIPLE_INPUT_SOURCES: &str = "Multiple input sources provided. Use only one of: --file/-f, --code/-e, or --json/-j.";

pub const ERR_CONFLICTING_INPUT_FLAGS: &str = "Conflicting input flags: --leaf cannot be used with --json-input.";

pub const ERR_CODE_INPUT_REQUIRED: &str = "Code input required: use --file, --code, or --json";

pub const ERR_JSON_OBJECTS_NOT_SUPPORTED: &str = "JSON objects not supported, use arrays";

/// Convert JSON Value to Cirru syntax tree
pub fn json_value_to_cirru(json: &serde_json::Value) -> Result<Cirru, String> {
  match json {
    serde_json::Value::String(s) => Ok(Cirru::Leaf(Arc::from(s.as_str()))),
    serde_json::Value::Number(n) => Ok(Cirru::Leaf(Arc::from(n.to_string()))),
    serde_json::Value::Bool(b) => Ok(Cirru::Leaf(Arc::from(b.to_string()))),
    serde_json::Value::Null => Ok(Cirru::Leaf(Arc::from("nil"))),
    serde_json::Value::Array(arr) => {
      let items: Result<Vec<Cirru>, String> = arr.iter().map(json_value_to_cirru).collect();
      Ok(Cirru::List(items?))
    }
    serde_json::Value::Object(_) => Err(ERR_JSON_OBJECTS_NOT_SUPPORTED.to_string()),
  }
}

/// Convert JSON string to Cirru syntax tree
pub fn json_to_cirru(json_str: &str) -> Result<Cirru, String> {
  let json_value: serde_json::Value = serde_json::from_str(json_str).map_err(|e| format!("Failed to parse JSON: {e}"))?;
  json_value_to_cirru(&json_value)
}

/// Convert Cirru syntax tree to JSON value (internal)
pub fn cirru_to_json_value(c: &Cirru) -> serde_json::Value {
  match c {
    Cirru::Leaf(s) => serde_json::Value::String(s.to_string()),
    Cirru::List(items) => serde_json::Value::Array(items.iter().map(cirru_to_json_value).collect()),
  }
}

/// Convert Cirru syntax tree to JSON string
pub fn cirru_to_json(node: &Cirru) -> String {
  serde_json::to_string_pretty(&cirru_to_json_value(node)).unwrap_or_else(|_| "[]".to_string())
}

pub fn format_path_with_separator(path: &[usize], separator: &str) -> String {
  path.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(separator)
}

pub fn format_path(path: &[usize]) -> String {
  format_path_with_separator(path, ".")
}

fn is_shell_sensitive_char(ch: char) -> bool {
  matches!(
    ch,
    '>' | '<' | '|' | '&' | ';' | '(' | ')' | '$' | '*' | '?' | '[' | ']' | '{' | '}' | '!' | '`'
  )
}

pub fn shell_quote(raw: &str) -> String {
  format!("'{}'", raw.replace('\'', "'\"'\"'"))
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DefinitionLookup {
  pub resolved: String,
  pub warning: Option<String>,
}

pub fn resolve_definition_lookup<'a, I>(
  namespace: &str,
  requested: &str,
  definitions: I,
  auto_correct: bool,
) -> Result<DefinitionLookup, String>
where
  I: IntoIterator<Item = &'a str>,
{
  let definition_names: Vec<&str> = definitions.into_iter().collect();

  if definition_names.contains(&requested) {
    return Ok(DefinitionLookup {
      resolved: requested.to_string(),
      warning: None,
    });
  }

  let shell_candidates: Vec<(&str, char)> = definition_names
    .into_iter()
    .filter_map(|candidate| {
      let rest = candidate.strip_prefix(requested)?;
      let next_char = rest.chars().next()?;
      if is_shell_sensitive_char(next_char) {
        Some((candidate, next_char))
      } else {
        None
      }
    })
    .collect();

  if shell_candidates.is_empty() {
    return Err(format!("Definition '{requested}' not found in namespace '{namespace}'"));
  }

  let mut lines = vec![format!("Definition '{requested}' not found in namespace '{namespace}'.")];
  lines.push("Possible cause: your shell may have interpreted part of the definition name before calcit received it.".to_string());
  lines.push("This often happens with characters like >, <, |, &, $, *, ?, (, or ).".to_string());

  if shell_candidates.len() == 1 {
    let (candidate, shell_char) = shell_candidates[0];
    lines.push(format!(
      "Detected a likely intended definition: '{candidate}' (the next character after '{requested}' is shell-sensitive: '{shell_char}')."
    ));
    lines.push(format!(
      "Try quoting the full target, for example: {}",
      shell_quote(&format!("{namespace}/{candidate}"))
    ));

    if auto_correct {
      lines.push(format!("Auto-correcting to '{candidate}' for this read-only command."));
      return Ok(DefinitionLookup {
        resolved: candidate.to_string(),
        warning: Some(lines.join("\n")),
      });
    }
  } else {
    let preview = shell_candidates
      .iter()
      .take(4)
      .map(|(candidate, _)| format!("'{candidate}'"))
      .collect::<Vec<_>>()
      .join(", ");
    lines.push(format!(
      "Found multiple shell-sensitive candidates starting with '{requested}': {preview}"
    ));
    lines.push(format!(
      "Quote the full target to disambiguate, for example: {}",
      shell_quote(&format!("{namespace}/{}", shell_candidates[0].0))
    ));
  }

  Err(lines.join("\n"))
}

pub fn print_cli_warning_block(message: &str) {
  let mut lines = message.lines();
  if let Some(first) = lines.next() {
    eprintln!("\n⚠️  Warning: {first}");
    for line in lines {
      eprintln!("   {line}");
    }
    eprintln!();
  }
}

pub fn emit_cli_output(content: &str, to_stderr: bool) {
  if to_stderr {
    eprint!("{content}");
    if !content.ends_with('\n') {
      eprintln!();
    }
  } else {
    print!("{content}");
    if !content.ends_with('\n') {
      println!();
    }
  }
}

pub fn format_path_bracketed(path: &[usize]) -> String {
  if path.is_empty() {
    "root".to_string()
  } else {
    format!("[{}]", format_path(path))
  }
}

/// Parse path string like "2.1.0" to Vec<usize>
pub fn parse_path(path_str: &str) -> Result<Vec<usize>, String> {
  if path_str.is_empty() {
    return Ok(vec![]);
  }

  if path_str.contains(',') {
    return Err(format!(
      "Invalid path '{path_str}': comma separator is no longer supported. Use dot-separated coordinates, e.g. '2.1.0'."
    ));
  }

  path_str
    .split('.')
    .map(|s| s.trim().parse::<usize>().map_err(|e| format!("Invalid path index '{s}': {e}")))
    .collect()
}

/// Validate input flag conflicts
pub fn validate_input_flags(leaf_input: bool, json_input: bool) -> Result<(), String> {
  if leaf_input && json_input {
    return Err(ERR_CONFLICTING_INPUT_FLAGS.to_string());
  }
  Ok(())
}

pub fn validate_input_sources(sources: &[bool]) -> Result<(), String> {
  if sources.iter().filter(|&&enabled| enabled).count() > 1 {
    Err(ERR_MULTIPLE_INPUT_SOURCES.to_string())
  } else {
    Ok(())
  }
}

/// Read code input from file, inline code, or json option.
/// Exactly one input source should be used.
pub fn read_code_input(file: &Option<String>, code: &Option<String>, json: &Option<String>) -> Result<Option<String>, String> {
  let sources = [file.is_some(), code.is_some(), json.is_some()];
  validate_input_sources(&sources)?;

  if let Some(path) = file {
    let content = fs::read_to_string(path).map_err(|e| format!("Failed to read file '{path}': {e}"))?;
    Ok(Some(content.trim().to_string()))
  } else if let Some(s) = code {
    if s.contains('\n') {
      eprintln!("\n⚠️  Note: Inline code contains newlines. Multi-line code in shell can be error-prone.");
      eprintln!("   Consider writing to a temporary file and using --file/-f instead.");
      eprintln!();
    }
    Ok(Some(s.trim().to_string()))
  } else if let Some(j) = json {
    Ok(Some(j.clone()))
  } else {
    Ok(None)
  }
}

/// Check if a Cirru node is a single-element list containing only a string leaf,
/// which might confuse LLM thinking it's a leaf node when it's actually an expression.
pub fn warn_if_single_string_expression(node: &Cirru, input_source: &str) {
  if let Cirru::List(items) = node {
    if items.len() == 1 {
      if let Some(Cirru::Leaf(_)) = items.first() {
        eprintln!("\n⚠️  Note: Cirru one-liner input '{input_source}' was parsed as an expression (list with one element).");
        eprintln!("   In Cirru syntax, this creates a list containing one element.");
        eprintln!("   If you want a leaf node (plain string), use --leaf parameter.");
        eprintln!("   Example: --leaf -e '{input_source}' creates a leaf, not an expression.\n");
      }
    }
  }
}

/// Warn when a Cirru one-liner input is wrapped by parentheses and emit a JSON validation payload
fn warn_if_wrapped_by_parentheses(raw: &str, node: &Cirru) {
  let t = raw.trim();
  if t.starts_with('(') && t.ends_with(')') {
    eprintln!("\n⚠️  Warning: One-liner input appears wrapped by top-level parentheses.");
    eprintln!("   Cirru typically avoids wrapping the entire top-level expression with '()'.");
    eprintln!("   This extra layer changes call semantics. Prefer removing the outer parentheses.\n");
    eprintln!("   JSON echo:");
    eprintln!("{}", cirru_to_json(node));
    eprintln!();
  }
}

/// Determine input mode and parse raw input string into a `Cirru` node.
/// Precedence (highest to lowest):
/// - `--json <string>` (inline JSON)
/// - `--leaf` (treat raw input as a Cirru leaf)
/// - `--json-input` (parse JSON -> Cirru)
/// - Cirru one-liner (default)
pub fn parse_input_to_cirru(
  raw: &str,
  inline_json: &Option<String>,
  json_input: bool,
  leaf: bool,
  auto_json: bool,
) -> Result<Cirru, String> {
  // Validate conflicting flags early (keep error messages user-friendly)
  validate_input_flags(leaf, json_input)?;

  // If inline JSON provided, use it (takes precedence)
  if let Some(j) = inline_json {
    if j.len() > 2000 {
      eprintln!("\n⚠️  Note: JSON input is very large ({} chars).", j.len());
      eprintln!("   For large definitions, consider using placeholders and submitting in segments.");
      eprintln!();
    }
    let node = json_to_cirru(j)?;
    if leaf {
      match node {
        Cirru::Leaf(_) => Ok(node),
        _ => Err("--leaf expects a JSON string (leaf node), but got a non-leaf JSON value.".to_string()),
      }
    } else {
      Ok(node)
    }
  } else if leaf {
    // --leaf: automatically treat raw input as a Cirru leaf node
    Ok(Cirru::Leaf(Arc::from(raw)))
  } else if json_input {
    if raw.len() > 2000 {
      eprintln!("\n⚠️  Note: JSON input is very large ({} chars).", raw.len());
      eprintln!("   For large definitions, consider using placeholders and submitting in segments.");
      eprintln!();
    }
    json_to_cirru(raw)
  } else {
    // If input comes from inline `--code/-e`, it's typically single-line.
    // Auto-detect JSON arrays/strings so users don't need `-J` for inline JSON.
    if auto_json {
      let trimmed = raw.trim();
      let looks_like_json_string = trimmed.starts_with('"') && trimmed.ends_with('"');
      // Heuristic for `-e/--code`:
      // - If it is a JSON string: starts/ends with quotes -> JSON
      // - If it is a JSON array: starts with '[' and ends with ']' AND contains at least one '"' -> JSON
      //   (This avoids ambiguity with Cirru list syntax like `[]` or `[] 1 2 3`.)
      let looks_like_json_array = trimmed.starts_with('[') && trimmed.ends_with(']') && trimmed.contains('"');

      // If it looks like JSON, treat it as JSON.
      // Do NOT fall back to Cirru one-liner on JSON parse failure, otherwise invalid JSON
      // can be silently accepted as a Cirru expression.
      if looks_like_json_array || looks_like_json_string {
        if trimmed.len() > 2000 {
          eprintln!("\n⚠️  Note: JSON input is very large ({} chars).", trimmed.len());
          eprintln!("   For large definitions, consider using placeholders and submitting in segments.");
          eprintln!();
        }
        return json_to_cirru(trimmed).map_err(|e| format!("Failed to parse JSON from -e/--code: {e}"));
      }

      // Inline `-e/--code` defaults to Cirru one-liner expr when it's not JSON.
      if trimmed.is_empty() {
        return Err("Input is empty. Please provide Cirru code or use -j for JSON input.".to_string());
      }
      if raw.contains('\t') {
        return Err(
          "Input contains tab characters. Cirru requires spaces for indentation.\n\
           Please replace tabs with 2 spaces.\n\
           Tip: Use `cat -A file` to check for tabs (shown as ^I)."
            .to_string(),
        );
      }

      if raw.len() > 1000 {
        eprintln!("\n⚠️  Note: Cirru one-liner input is very large ({} chars).", raw.len());
        eprintln!("   For large definitions, consider using placeholders and submitting in segments.");
        eprintln!();
      }

      let result = cirru_parser::parse_expr_one_liner(raw).map_err(|e| format!("Failed to parse Cirru one-liner expression: {e}"))?;
      warn_if_wrapped_by_parentheses(raw, &result);
      warn_if_single_string_expression(&result, raw);
      // Validate basic Cirru syntax
      cirru_validator::validate_cirru_syntax(&result)?;
      return Ok(result);
    }

    // Check for common mistakes before parsing
    let trimmed = raw.trim();

    // Check for empty input
    if trimmed.is_empty() {
      return Err("Input is empty. Please provide Cirru code or use -j for JSON input.".to_string());
    }

    // Detect JSON input without --json-input flag
    // JSON arrays look like: ["item", ...] or [ "item", ...]
    // Cirru [] syntax looks like: [] 1 2 3 or []
    // Key difference: JSON has ["..." at start, Cirru has [] followed by space or newline
    if trimmed.starts_with('[') && trimmed.ends_with(']') {
      // Check if it looks like JSON (starts with [" or [ ")
      let after_bracket = &trimmed[1..];
      let is_likely_json = after_bracket.starts_with('"')
        || after_bracket.starts_with(' ') && after_bracket.trim_start().starts_with('"')
        || after_bracket.starts_with('\n') && after_bracket.trim_start().starts_with('"');

      // Also check: Cirru [] is followed by space then non-quote content
      let is_cirru_list = after_bracket.starts_with(']') // empty []
      || (after_bracket.starts_with(' ') && !after_bracket.trim_start().starts_with('"'));

      if is_likely_json && !is_cirru_list {
        return Err(
          "Input appears to be JSON format (starts with '[\"').\n\
         If you want to use JSON input, use one of:\n\
         - inline JSON: cr edit def ns/name -j '[\"defn\", ...]'\n\
         - inline code: cr edit def ns/name -e '[\"defn\", ...]'\n\
         - file JSON: add -J or --json-input (e.g. -f code.json -J).\n\
         Note: Cirru's [] list syntax (e.g. '[] 1 2 3') is different and will be parsed correctly."
            .to_string(),
        );
      }
    }

    // Detect tabs in input
    if raw.contains('\t') {
      return Err(
        "Input contains tab characters. Cirru requires spaces for indentation.\n\
       Please replace tabs with 2 spaces.\n\
       Tip: Use `cat -A file` to check for tabs (shown as ^I)."
          .to_string(),
      );
    }

    // Default: parse as cirru text
    let parsed = cirru_parser::parse(raw).map_err(|e| {
      let err_str = e.to_string();
      let mut msg = format!("Failed to parse Cirru text: {err_str}");

      // Provide specific hints based on error type
      if err_str.contains("odd indentation") {
        msg.push_str("\n\nCirru requires 2-space indentation. Each nesting level must use exactly 2 spaces.");
        msg.push_str("\nExample:\n  defn my-fn (x)\n    &+ x 1");
      } else if err_str.contains("unexpected end of file") {
        msg.push_str("\n\nPossible cause: missing closing quotes or unclosed structural pattern.");
      }
      msg
    })?;

    // Return the expressions
    if parsed.len() == 1 {
      let result = parsed.into_iter().next().unwrap();
      warn_if_single_string_expression(&result, raw);
      // Validate basic Cirru syntax
      cirru_validator::validate_cirru_syntax(&result)?;
      Ok(result)
    } else if parsed.is_empty() {
      Err("Input parsed as an empty Cirru structure.".to_string())
    } else {
      // Validate basic Cirru syntax for each node
      for node in &parsed {
        cirru_validator::validate_cirru_syntax(node)?;
      }
      Ok(Cirru::List(parsed))
    }
  }
}

#[cfg(test)]
mod tests {
  use super::{format_path, format_path_bracketed, format_path_with_separator, parse_path, resolve_definition_lookup, shell_quote};

  #[test]
  fn rejects_comma_separated_paths() {
    let err = parse_path("3,2,1").unwrap_err();
    assert!(err.contains("comma separator is no longer supported"));
  }

  #[test]
  fn parses_dot_separated_paths() {
    assert_eq!(parse_path("3.2.1").unwrap(), vec![3, 2, 1]);
  }

  #[test]
  fn rejects_mixed_separators() {
    assert!(parse_path("3,2.1").is_err());
  }

  #[test]
  fn formats_paths_with_dot_by_default() {
    assert_eq!(format_path(&[3, 2, 1]), "3.2.1");
    assert_eq!(format_path_bracketed(&[3, 2, 1]), "[3.2.1]");
    assert_eq!(format_path_with_separator(&[3, 2, 1], ","), "3,2,1");
  }

  #[test]
  fn quotes_shell_targets_with_single_quotes() {
    assert_eq!(shell_quote("app.main/element->node"), "'app.main/element->node'");
  }

  #[test]
  fn auto_corrects_unique_shell_truncated_definition() {
    let lookup = resolve_definition_lookup("respo.render.html", "element-", vec!["element->node", "render-app"], true).unwrap();

    assert_eq!(lookup.resolved, "element->node");
    let warning = lookup.warning.unwrap();
    assert!(warning.contains("Possible cause: your shell may have interpreted part of the definition name"));
    assert!(warning.contains("Auto-correcting to 'element->node'"));
  }

  #[test]
  fn keeps_plain_not_found_when_no_shell_candidate_exists() {
    let err = resolve_definition_lookup("app.main", "missing", vec!["main", "helper"], false).unwrap_err();
    assert_eq!(err, "Definition 'missing' not found in namespace 'app.main'");
  }

  #[test]
  fn reports_ambiguous_shell_truncated_definition() {
    let err = resolve_definition_lookup("app.main", "value-", vec!["value->text", "value->debug", "other"], false).unwrap_err();

    assert!(err.contains("Found multiple shell-sensitive candidates starting with 'value-'"));
    assert!(err.contains("'value->text'"));
    assert!(err.contains("'value->debug'"));
  }
}
