use std::io::Write;

use crossterm::{
  cursor::{MoveTo, SetCursorStyle, Show},
  queue,
  style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor},
  terminal::{Clear, ClearType},
};

use crate::layout::{Position, Size, bottom_third, centered_block_origin, centered_column, size};

const BOX_CHROME: u16 = 4; // "│ " + " │"
const MIN_INNER_WIDTH: u16 = 20;
const MAX_INNER_WIDTH: u16 = 60;
const BOX_HEIGHT: u16 = 6; // question + top border + content + bottom border + hint + blank

#[derive(Debug, Clone)]
pub struct InputBox {
  pub label: String,
  pub hint: String,
  pub inner_width: u16,
}

#[derive(Debug, Clone)]
pub struct InputBoxLayout {
  pub question_row: u16,
  pub box_top_row: u16,
  pub content_row: u16,
  pub box_bottom_row: u16,
  pub hint_row: u16,
  pub box_column: u16,
  pub inner_width: u16,
}

pub fn input_box(label: &str, hint: &str, terminal_width: u16) -> InputBox {
  let proportion = terminal_width.saturating_mul(3) / 10;
  let inner = proportion.clamp(MIN_INNER_WIDTH, MAX_INNER_WIDTH);

  InputBox {
    label: label.to_string(),
    hint: hint.to_string(),
    inner_width: inner,
  }
}

pub fn input_box_layout(input_box: &InputBox, terminal_size: Size) -> InputBoxLayout {
  let region = bottom_third(terminal_size);
  let outer_width = input_box.inner_width.saturating_add(BOX_CHROME);
  let block_size = size(outer_width, BOX_HEIGHT);
  let origin = centered_block_origin(region, block_size);

  InputBoxLayout {
    question_row: origin.row,
    box_top_row: origin.row.saturating_add(1),
    content_row: origin.row.saturating_add(2),
    box_bottom_row: origin.row.saturating_add(3),
    hint_row: origin.row.saturating_add(4),
    box_column: origin.column,
    inner_width: input_box.inner_width,
  }
}

pub fn visible_content(input_box: &InputBox, value: &str) -> String {
  let width = input_box.inner_width as usize;

  if value.is_empty() {
    return truncate_or_pad(&input_box.hint, width);
  }

  if value.len() <= width {
    return pad_right(value, width);
  }

  let visible = &value[value.len() - (width - 1)..];
  format!("\u{2026}{}", pad_right(visible, width - 1))
}

pub struct VisibleWindow {
  pub text: String,
  pub cursor_col: usize,
  pub selection: Option<(usize, usize)>,
}

pub fn visible_window(
  input_box: &InputBox,
  value: &str,
  cursor: usize,
  selection: Option<(usize, usize)>,
) -> VisibleWindow {
  let width = input_box.inner_width as usize;
  let chars: Vec<char> = value.chars().collect();
  let len = chars.len();

  if value.is_empty() {
    return VisibleWindow {
      text: truncate_or_pad(&input_box.hint, width),
      cursor_col: 0,
      selection: None,
    };
  }

  if len <= width {
    return VisibleWindow {
      text: pad_right(value, width),
      cursor_col: cursor.min(len),
      selection: clamp_selection(selection, 0, len),
    };
  }

  let (start, end) = compute_window(cursor, len, width);
  let window_text: String = chars[start..end].iter().collect();

  let display = if start > 0 {
    let trimmed: String = chars[start + 1..end].iter().collect();
    format!("\u{2026}{}", pad_right(&trimmed, width - 1))
  } else {
    pad_right(&window_text, width)
  };

  let cursor_col = cursor.saturating_sub(start);
  let sel = selection.map(|(s, e)| {
    let ws = s.saturating_sub(start).min(width);
    let we = e.saturating_sub(start).min(width);
    (ws, we)
  });

  VisibleWindow {
    text: display,
    cursor_col,
    selection: sel.filter(|(s, e)| s != e),
  }
}

fn compute_window(cursor: usize, len: usize, width: usize) -> (usize, usize) {
  if cursor <= width / 2 {
    (0, width.min(len))
  } else if cursor >= len.saturating_sub(width / 2) {
    let start = len.saturating_sub(width);
    (start, len)
  } else {
    let start = cursor.saturating_sub(width / 2);
    (start, (start + width).min(len))
  }
}

fn clamp_selection(sel: Option<(usize, usize)>, min: usize, max: usize) -> Option<(usize, usize)> {
  sel
    .map(|(s, e)| (s.max(min).min(max), e.max(min).min(max)))
    .filter(|(s, e)| s != e)
}

pub fn top_border(input_box: &InputBox) -> String {
  let label_display = format!(" {} ", input_box.label);
  let label_chars = label_display.chars().count();
  let fill_count = (input_box.inner_width as usize)
    .saturating_add(2)
    .saturating_sub(label_chars);

  let fill: String = "─".repeat(fill_count);
  format!("╭{label_display}{fill}╮")
}

pub fn bottom_border(inner_width: u16) -> String {
  let fill: String = "─".repeat(inner_width as usize + 2);
  format!("╰{fill}╯")
}

pub fn content_line(visible: &str) -> String {
  format!("│ {visible} │")
}

fn truncate_or_pad(text: &str, width: usize) -> String {
  if text.len() <= width {
    return pad_right(text, width);
  }

  let visible = &text[..width];
  visible.to_string()
}

fn pad_right(text: &str, width: usize) -> String {
  format!("{:<width$}", text, width = width)
}

#[cfg(not(tarpaulin_include))]
pub fn queue_question<W: Write>(writer: &mut W, text: &str, pos: Position) -> std::io::Result<()> {
  queue!(
    writer,
    MoveTo(pos.column, pos.row),
    Clear(ClearType::CurrentLine),
    SetForegroundColor(Color::White),
    SetAttribute(Attribute::Bold),
    Print(text),
    SetAttribute(Attribute::Reset),
    ResetColor
  )
}

#[cfg(not(tarpaulin_include))]
pub fn queue_input_box<W: Write>(
  writer: &mut W,
  layout: &InputBoxLayout,
  input_box: &InputBox,
  value: &str,
  label_width: u16,
) -> std::io::Result<()> {
  let visible = visible_content(input_box, value);
  let is_hint = value.is_empty();
  let bottom = bottom_border(layout.inner_width);

  queue_box_border(
    writer,
    &input_box.label,
    layout.inner_width,
    label_width,
    layout.box_column,
    layout.box_top_row,
  )?;
  queue_box_content(
    writer,
    &visible,
    is_hint,
    layout.box_column,
    layout.content_row,
  )?;
  queue_box_border_plain(writer, &bottom, layout.box_column, layout.box_bottom_row)
}

#[cfg(not(tarpaulin_include))]
pub fn queue_cursor<W: Write>(
  writer: &mut W,
  layout: &InputBoxLayout,
  cursor_col: u16,
) -> std::io::Result<()> {
  let abs_col = layout.box_column + 2 + cursor_col;
  queue!(
    writer,
    MoveTo(abs_col, layout.content_row),
    Show,
    SetCursorStyle::BlinkingBlock
  )
}

#[cfg(not(tarpaulin_include))]
#[allow(clippy::too_many_arguments)]
pub fn queue_input_box_with_cursor<W: Write>(
  writer: &mut W,
  layout: &InputBoxLayout,
  input_box: &InputBox,
  value: &str,
  label_width: u16,
  cursor: usize,
  selection: Option<(usize, usize)>,
) -> std::io::Result<u16> {
  let window = visible_window(input_box, value, cursor, selection);
  let cursor_col = window.cursor_col as u16;
  let is_hint = value.is_empty();
  let bottom = bottom_border(layout.inner_width);

  queue_box_border(
    writer,
    &input_box.label,
    layout.inner_width,
    label_width,
    layout.box_column,
    layout.box_top_row,
  )?;
  queue_box_content_with_cursor(
    writer,
    &window,
    is_hint,
    layout.box_column,
    layout.content_row,
  )?;
  queue_box_border_plain(writer, &bottom, layout.box_column, layout.box_bottom_row)?;
  Ok(cursor_col)
}

#[cfg(not(tarpaulin_include))]
fn queue_box_content_with_cursor<W: Write>(
  writer: &mut W,
  window: &VisibleWindow,
  is_hint: bool,
  column: u16,
  row: u16,
) -> std::io::Result<()> {
  queue!(
    writer,
    MoveTo(column, row),
    Clear(ClearType::CurrentLine),
    SetForegroundColor(Color::DarkGrey),
    Print("│ ")
  )?;

  if let Some((sel_start, sel_end)) = window.selection {
    let chars: Vec<char> = window.text.chars().collect();
    let before: String = chars[..sel_start].iter().collect();
    let selected: String = chars[sel_start..sel_end].iter().collect();
    let after: String = chars[sel_end..].iter().collect();

    let content_color = if is_hint {
      Color::DarkGrey
    } else {
      Color::White
    };
    queue!(
      writer,
      SetForegroundColor(content_color),
      Print(&before),
      SetAttribute(Attribute::Reverse),
      Print(&selected),
      SetAttribute(Attribute::Reset),
      SetForegroundColor(content_color),
      Print(&after)
    )?;
  } else {
    let content_color = if is_hint {
      Color::DarkGrey
    } else {
      Color::White
    };
    queue!(
      writer,
      SetForegroundColor(content_color),
      Print(&window.text)
    )?;
  }

  queue!(
    writer,
    SetForegroundColor(Color::DarkGrey),
    Print(" │"),
    ResetColor
  )
}

#[cfg(not(tarpaulin_include))]
fn queue_box_border<W: Write>(
  writer: &mut W,
  label: &str,
  inner_width: u16,
  label_width: u16,
  column: u16,
  row: u16,
) -> std::io::Result<()> {
  let total_fill = inner_width as usize + 2;

  if label.is_empty() {
    let fill: String = "─".repeat(total_fill);
    return queue!(
      writer,
      MoveTo(column, row),
      Clear(ClearType::CurrentLine),
      SetForegroundColor(Color::DarkGrey),
      Print("╭"),
      Print(&fill),
      Print("╮"),
      ResetColor
    );
  }

  let label_display = format!(" {label} ");
  let label_display_chars = label_display.chars().count();
  let left_fill_count = (label_width as usize + 2).saturating_sub(label_display_chars);
  let left_fill: String = "─".repeat(left_fill_count);
  let right_fill_count = total_fill.saturating_sub(left_fill_count + label_display_chars);
  let right_fill: String = "─".repeat(right_fill_count);

  queue!(
    writer,
    MoveTo(column, row),
    Clear(ClearType::CurrentLine),
    SetForegroundColor(Color::DarkGrey),
    Print("╭"),
    Print(&left_fill),
    SetForegroundColor(Color::Cyan),
    SetAttribute(Attribute::Bold),
    Print(&label_display),
    SetAttribute(Attribute::Reset),
    SetForegroundColor(Color::DarkGrey),
    Print(&right_fill),
    Print("╮"),
    ResetColor
  )
}

#[cfg(not(tarpaulin_include))]
fn queue_box_content<W: Write>(
  writer: &mut W,
  visible: &str,
  is_hint: bool,
  column: u16,
  row: u16,
) -> std::io::Result<()> {
  let content_color = if is_hint {
    Color::DarkGrey
  } else {
    Color::White
  };

  queue!(
    writer,
    MoveTo(column, row),
    Clear(ClearType::CurrentLine),
    SetForegroundColor(Color::DarkGrey),
    Print("│ "),
    SetForegroundColor(content_color),
    Print(visible),
    SetForegroundColor(Color::DarkGrey),
    Print(" │"),
    ResetColor
  )
}

#[cfg(not(tarpaulin_include))]
fn queue_box_border_plain<W: Write>(
  writer: &mut W,
  border: &str,
  column: u16,
  row: u16,
) -> std::io::Result<()> {
  queue!(
    writer,
    MoveTo(column, row),
    Clear(ClearType::CurrentLine),
    SetForegroundColor(Color::DarkGrey),
    Print(border),
    ResetColor
  )
}

#[cfg(not(tarpaulin_include))]
pub fn queue_hint_line<W: Write>(
  writer: &mut W,
  text: &str,
  tw: u16,
  row: u16,
  color: Color,
) -> std::io::Result<()> {
  let col = centered_column(tw, text.len() as u16);
  queue!(
    writer,
    MoveTo(col, row),
    Clear(ClearType::CurrentLine),
    SetForegroundColor(color),
    Print(text),
    ResetColor
  )
}

#[cfg(not(tarpaulin_include))]
pub fn queue_clear_prompt<W: Write>(
  writer: &mut W,
  layout: &InputBoxLayout,
) -> std::io::Result<()> {
  queue!(
    writer,
    MoveTo(0, layout.question_row),
    Clear(ClearType::CurrentLine),
    MoveTo(0, layout.box_top_row),
    Clear(ClearType::CurrentLine),
    MoveTo(0, layout.content_row),
    Clear(ClearType::CurrentLine),
    MoveTo(0, layout.box_bottom_row),
    Clear(ClearType::CurrentLine),
    MoveTo(0, layout.hint_row),
    Clear(ClearType::CurrentLine)
  )
}

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

  #[test]
  fn input_box_clamps_inner_width() {
    let narrow = input_box("X", "hint", 30);
    assert!(narrow.inner_width >= MIN_INNER_WIDTH);

    let wide = input_box("X", "hint", 200);
    assert!(wide.inner_width <= MAX_INNER_WIDTH);
  }

  #[test]
  fn input_box_layout_places_rows_sequentially() {
    let ib = input_box("VAR", "default", 80);
    let layout = input_box_layout(&ib, size(80, 30));

    assert_eq!(layout.box_top_row, layout.question_row + 1);
    assert_eq!(layout.content_row, layout.box_top_row + 1);
    assert_eq!(layout.box_bottom_row, layout.content_row + 1);
    assert_eq!(layout.hint_row, layout.box_bottom_row + 1);
  }

  #[test]
  fn input_box_layout_centers_horizontally() {
    let ib = input_box("VAR", "default", 120);
    let layout = input_box_layout(&ib, size(120, 30));

    assert!(layout.box_column > 0);
  }

  #[test]
  fn visible_content_shows_hint_when_empty() {
    let ib = input_box("X", "~/.config/blizz", 80);
    let content = visible_content(&ib, "");

    assert!(content.starts_with("~/.config/blizz"));
  }

  #[test]
  fn visible_content_shows_value_when_present() {
    let ib = input_box("X", "~/.config/blizz", 80);
    let content = visible_content(&ib, "/home");

    assert!(content.starts_with("/home"));
  }

  #[test]
  fn visible_content_truncates_long_input_with_ellipsis() {
    let ib = InputBox {
      label: "X".to_string(),
      hint: "hint".to_string(),
      inner_width: 10,
    };
    let content = visible_content(&ib, "abcdefghijklmnop");

    assert!(content.starts_with('\u{2026}'));
    assert_eq!(content.chars().count(), 10);
    assert!(content.contains("hijklmnop"));
  }

  #[test]
  fn visible_content_pads_short_input() {
    let ib = InputBox {
      label: "X".to_string(),
      hint: "hint".to_string(),
      inner_width: 20,
    };
    let content = visible_content(&ib, "hi");

    assert_eq!(content.len(), 20);
    assert!(content.starts_with("hi"));
  }

  #[test]
  fn top_border_includes_label_and_corners() {
    let ib = input_box("BLIZZ_HOME", "hint", 80);
    let border = top_border(&ib);

    assert!(border.starts_with("╭"));
    assert!(border.ends_with("╮"));
    assert!(border.contains("BLIZZ_HOME"));
  }

  #[test]
  fn bottom_border_has_correct_width() {
    let border = bottom_border(20);

    assert!(border.starts_with("╰"));
    assert!(border.ends_with("╯"));
  }

  #[test]
  fn content_line_wraps_with_box_chars() {
    let line = content_line("hello world         ");

    assert!(line.starts_with("│ "));
    assert!(line.ends_with(" │"));
  }

  #[test]
  fn visible_content_uses_hint_as_placeholder() {
    let ib = InputBox {
      label: "X".to_string(),
      hint: "type here".to_string(),
      inner_width: 20,
    };
    let content = visible_content(&ib, "");
    assert!(content.contains("type here"));
    assert_eq!(content.len(), 20);
  }

  #[test]
  fn top_border_with_empty_label_still_has_corners() {
    let ib = input_box("", "hint", 80);
    let border = top_border(&ib);
    assert!(border.starts_with("╭"));
    assert!(border.ends_with("╮"));
  }

  // ── visible_window ──────────────────────────────────────

  fn ib(width: u16) -> InputBox {
    InputBox {
      label: "X".into(),
      hint: "hint".into(),
      inner_width: width,
    }
  }

  #[test]
  fn visible_window_empty_input_shows_hint() {
    let w = visible_window(&ib(20), "", 0, None);
    assert!(w.text.contains("hint"));
    assert_eq!(w.cursor_col, 0);
    assert!(w.selection.is_none());
  }

  #[test]
  fn visible_window_short_input_fits_fully() {
    let w = visible_window(&ib(20), "hello", 5, None);
    assert!(w.text.starts_with("hello"));
    assert_eq!(w.text.chars().count(), 20);
    assert_eq!(w.cursor_col, 5);
    assert!(w.selection.is_none());
  }

  #[test]
  fn visible_window_short_input_with_selection() {
    let w = visible_window(&ib(20), "hello", 3, Some((1, 4)));
    assert_eq!(w.selection, Some((1, 4)));
  }

  #[test]
  fn visible_window_short_input_clamps_cursor() {
    let w = visible_window(&ib(20), "hi", 99, None);
    assert_eq!(w.cursor_col, 2);
  }

  #[test]
  fn visible_window_long_input_cursor_near_start() {
    let input = "abcdefghijklmnopqrstuvwxyz";
    let w = visible_window(&ib(10), input, 2, None);
    assert_eq!(w.text.chars().count(), 10);
    assert_eq!(w.cursor_col, 2);
  }

  #[test]
  fn visible_window_long_input_cursor_near_end() {
    let input = "abcdefghijklmnopqrstuvwxyz";
    let w = visible_window(&ib(10), input, 24, None);
    assert_eq!(w.text.chars().count(), 10);
  }

  #[test]
  fn visible_window_long_input_cursor_in_middle_shows_ellipsis() {
    let input = "abcdefghijklmnopqrstuvwxyz";
    let w = visible_window(&ib(10), input, 13, None);
    assert_eq!(w.text.chars().count(), 10);
    assert!(w.text.starts_with('\u{2026}'));
  }

  #[test]
  fn visible_window_long_input_with_selection() {
    let input = "abcdefghijklmnopqrstuvwxyz";
    let w = visible_window(&ib(10), input, 13, Some((10, 16)));
    assert!(w.selection.is_some());
    let (s, e) = w.selection.unwrap();
    assert!(s < e);
  }

  #[test]
  fn visible_window_selection_collapsed_is_none() {
    let input = "abcdefghijklmnopqrstuvwxyz";
    let w = visible_window(&ib(10), input, 2, Some((5, 5)));
    assert!(w.selection.is_none());
  }

  // ── compute_window ────────────────────────────────────────

  #[test]
  fn compute_window_cursor_at_start() {
    let (start, end) = compute_window(2, 30, 10);
    assert_eq!(start, 0);
    assert_eq!(end, 10);
  }

  #[test]
  fn compute_window_cursor_at_end() {
    let (start, end) = compute_window(28, 30, 10);
    assert_eq!(start, 20);
    assert_eq!(end, 30);
  }

  #[test]
  fn compute_window_cursor_in_middle() {
    let (start, end) = compute_window(15, 30, 10);
    assert_eq!(start, 10);
    assert_eq!(end, 20);
  }

  // ── clamp_selection ───────────────────────────────────────

  #[test]
  fn clamp_selection_within_range() {
    assert_eq!(clamp_selection(Some((2, 5)), 0, 10), Some((2, 5)));
  }

  #[test]
  fn clamp_selection_clamps_to_bounds() {
    assert_eq!(clamp_selection(Some((0, 15)), 2, 10), Some((2, 10)));
  }

  #[test]
  fn clamp_selection_collapsed_is_none() {
    assert_eq!(clamp_selection(Some((5, 5)), 0, 10), None);
  }

  #[test]
  fn clamp_selection_none_input() {
    assert_eq!(clamp_selection(None, 0, 10), None);
  }

  #[cfg(not(tarpaulin_include))]
  #[test]
  fn queue_question_writes_text() {
    let mut buffer = Vec::new();
    let pos = Position { column: 0, row: 0 };

    queue_question(&mut buffer, "Where to install?", pos).unwrap();
    let output = String::from_utf8(buffer).unwrap();

    assert!(output.contains("Where to install?"));
  }

  #[cfg(not(tarpaulin_include))]
  #[test]
  fn queue_input_box_writes_all_parts() {
    let mut buffer = Vec::new();
    let ib = input_box("VAR", "default", 80);
    let layout = input_box_layout(&ib, size(80, 30));

    queue_input_box(
      &mut buffer,
      &layout,
      &ib,
      "",
      ib.label.chars().count() as u16,
    )
    .unwrap();
    let output = String::from_utf8(buffer).unwrap();

    assert!(output.contains("VAR"));
  }

  #[cfg(not(tarpaulin_include))]
  #[test]
  fn queue_clear_prompt_clears_all_rows() {
    let mut buffer = Vec::new();
    let ib = input_box("VAR", "default", 80);
    let layout = input_box_layout(&ib, size(80, 30));

    queue_clear_prompt(&mut buffer, &layout).unwrap();

    assert!(!buffer.is_empty());
  }
}
