#[cfg(feature = "arboard")]
use arboard::Clipboard;
use color_eyre::eyre::{Result, eyre};
use crossterm::event::{KeyEvent, MouseEvent, MouseEventKind};
use ratatui::{
  Frame,
  layout::{Constraint, Direction, Layout, Position},
  prelude::Rect,
  style::{Color, Style},
  text::Line,
  widgets::{Block, Borders, Clear, Padding, Paragraph, Tabs, Wrap},
};
use sqlparser::ast::Statement;
use strum::IntoEnumIterator;
use tokio::sync::mpsc::{self};

use crate::{
  action::{Action, ExportFormat, MenuItemKind, MenuPreview},
  cli::{Cli, Driver},
  completion::{CompletionCoordinator, CompletionDatabaseEvent},
  components::{
    Component, ComponentImpls,
    data::{Data, DataComponent},
    editor::Editor,
    favorites::{FavoriteEntries, Favorites},
    history::History,
    menu::{Menu, MenuComponent},
  },
  config::{Config, KeyBindings},
  database::{self, Database, DbTaskResult, ExecutionType, Rows},
  external_editor,
  focus::Focus,
  popups::{
    PopUp, PopUpPayload, confirm_bypass::ConfirmBypass, confirm_export::ConfirmExport,
    confirm_query::ConfirmQuery, confirm_tx::ConfirmTx, confirm_yank::ConfirmYank,
    exporting::Exporting, name_favorite::NameFavorite,
  },
  tui,
  ui::center,
};

const DEFAULT_MENU_WIDTH_PERCENT: u16 = 25;
const DEFAULT_TABS_HEIGHT_PERCENT: u16 = 45;
const MIN_SECTION_PERCENT: u16 = 10;
const MAX_SECTION_PERCENT: u16 = 90;
const SECTION_RESIZE_STEP: i16 = 5;

fn resized_percent(percent: u16, delta: i16) -> u16 {
  (percent as i16 + delta).clamp(MIN_SECTION_PERCENT as i16, MAX_SECTION_PERCENT as i16) as u16
}

pub struct HistoryEntry {
  pub query_lines: Vec<String>,
  pub timestamp: chrono::DateTime<chrono::Local>,
}

pub struct AppState {
  pub driver: Driver,
  pub focus: Focus,
  pub history: Vec<HistoryEntry>,
  pub favorites: FavoriteEntries,
  pub last_query_start: Option<chrono::DateTime<chrono::Utc>>,
  pub last_query_end: Option<chrono::DateTime<chrono::Utc>>,
  pub query_task_running: bool,
}

pub struct Components<'a> {
  pub menu: Box<dyn MenuComponent<'a>>,
  pub editor: Box<dyn Component>,
  pub history: Box<dyn Component>,
  pub data: Box<dyn DataComponent<'a>>,
  pub favorites: Box<dyn Component>,
}

pub struct App {
  pub mouse_mode_override: Option<bool>,
  pub config: Config,
  pub components: Components<'static>,
  pub should_quit: bool,
  pub last_tick_key_events: Vec<KeyEvent>,
  pub last_frame_mouse_event: Option<MouseEvent>,
  pub state: AppState,
  last_focused_tab: Focus,
  last_focused_component: Focus,
  popup: Option<Box<dyn PopUp>>,
  completion: CompletionCoordinator,
  menu_width_percent: u16,
  tabs_height_percent: u16,
}

fn format_footer_key_hint(hint: &str) -> String {
  let Some(inner) = hint.strip_prefix('<').and_then(|hint| hint.strip_suffix('>')) else {
    return hint.to_owned();
  };
  if inner.chars().count() == 1 {
    inner.to_owned()
  } else {
    format!("<{}>", inner.replace('+', " + "))
  }
}

fn footer_action_hints(keybindings: &KeyBindings, focus: Focus, action: &Action) -> Vec<String> {
  keybindings
    .hints_for_action(focus, action)
    .iter()
    .map(|hint| format_footer_key_hint(hint))
    .collect()
}

fn footer_help_text(config: &Config, focus: Focus, query_task_running: bool) -> String {
  let abort_hints = footer_action_hints(&config.keybindings, focus, &Action::AbortQuery).join("|");
  let abort = if query_task_running && focus != Focus::PopUp && !abort_hints.is_empty() {
    format!("[{abort_hints}] abort ")
  } else {
    String::new()
  };

  let contextual = match focus {
    Focus::Menu =>
      "[R] refresh [j|↓] down [k|↑] up [l|<enter>] table list [h|󰁮 ] schema list [y] copy name [/] search [g] top [G] bottom".to_owned(),
    Focus::Editor if !query_task_running => {
      let mut execute_hints = vec!["<alt + enter>".to_owned()];
      for hint in footer_action_hints(&config.keybindings, focus, &Action::SubmitEditorQuery) {
        if !execute_hints.contains(&hint) {
          execute_hints.push(hint);
        }
      }
      let execute_hints = execute_hints.join("|");
      let external_editor_hints =
        footer_action_hints(&config.keybindings, focus, &Action::RequestExternalEditor).join("|");
      let external_editor = if external_editor_hints.is_empty() {
        String::new()
      } else {
        format!(" [{external_editor_hints}] external editor")
      };
      format!(
        "[{execute_hints}] execute query{external_editor} [<ctrl + f>|<alt + f>] save query to favorites"
      )
    },
    Focus::History =>
      "[j|↓] down [k|↑] up [y] copy query [I] edit query [D] clear history".to_owned(),
    Focus::Favorites =>
      "[j|↓] down [k|↑] up [y] copy query [I] edit query [D] delete entry [/] search [<esc>] clear search".to_owned(),
    Focus::Data if !query_task_running =>
      "[P] export [j|↓] next row [k|↑] prev row [w|e] next col [b] prev col [v] select field [V] select row [y] copy [Y] copy all [g] top [G] bottom [0] first col [$] last col".to_owned(),
    Focus::PopUp => "[<esc>] cancel".to_owned(),
    _ => String::new(),
  };

  format!("{abort}{contextual}")
}

impl App {
  pub fn new(mouse_mode_override: Option<bool>, config: Config) -> Result<Self> {
    let focus = Focus::Menu;
    let (completion, completion_client) = CompletionCoordinator::new();
    let menu = Menu::new();
    let editor = Editor::with_completion_channels(completion_client);
    let history = History::new();
    let data = Data::new();
    let favorites = Favorites::new();
    let favorite_entries = FavoriteEntries::new(&config.config._favorites_dir)?;

    Ok(Self {
      components: Components {
        menu: Box::new(menu),
        editor: Box::new(editor),
        history: Box::new(history),
        data: Box::new(data),
        favorites: Box::new(favorites),
      },
      should_quit: false,
      mouse_mode_override,
      config,
      last_tick_key_events: Vec::new(),
      last_frame_mouse_event: None,
      state: AppState {
        driver: Driver::Postgres,
        focus,
        history: vec![],
        last_query_start: None,
        last_query_end: None,
        favorites: favorite_entries,
        query_task_running: false,
      },
      last_focused_tab: Focus::Editor,
      last_focused_component: focus,
      popup: None,
      completion,
      menu_width_percent: DEFAULT_MENU_WIDTH_PERCENT,
      tabs_height_percent: DEFAULT_TABS_HEIGHT_PERCENT,
    })
  }

  fn add_to_history(&mut self, query_lines: Vec<String>) {
    self.state.history.insert(0, HistoryEntry { query_lines, timestamp: chrono::Local::now() });
    if self.state.history.len() > 50 {
      self.state.history.pop();
    }
  }

  fn clear_history(&mut self) {
    self.state.history = vec![];
  }

  fn set_data_state(&mut self, data: Option<Result<Rows>>, statement_type: Option<Statement>) {
    let result_rows = data
      .as_ref()
      .and_then(|result| result.as_ref().ok())
      .filter(|_| !matches!(statement_type.as_ref(), Some(Statement::Explain { .. })));
    self.completion.add_result_rows(result_rows);
    self.components.data.set_data_state(data, statement_type);
  }

  fn set_data_loading(&mut self) {
    self.components.data.set_loading();
  }

  fn set_data_cancelled(&mut self) {
    self.components.data.set_cancelled();
  }

  fn set_focus(&mut self, focus: Focus) {
    self.state.focus = focus;
    if focus != Focus::PopUp {
      self.popup = None;
      self.last_focused_component = focus;
    }
    if focus == Focus::Editor || focus == Focus::History || focus == Focus::Favorites {
      self.last_focused_tab = focus;
    }
  }

  fn set_popup(&mut self, popup: Box<dyn PopUp>) {
    self.popup = Some(popup);
    self.set_focus(Focus::PopUp);
  }

  fn last_focused_tab(&mut self) {
    match self.last_focused_tab {
      Focus::Editor => self.set_focus(Focus::Editor),
      Focus::History => self.set_focus(Focus::History),
      Focus::Favorites => self.set_focus(Focus::Favorites),
      _ => {},
    }
  }

  fn resize_focused_section(&mut self, delta: i16) {
    match self.state.focus {
      Focus::Menu => {
        self.menu_width_percent = resized_percent(self.menu_width_percent, delta);
      },
      Focus::Editor | Focus::History | Focus::Favorites => {
        self.tabs_height_percent = resized_percent(self.tabs_height_percent, delta);
      },
      // the stored percentage is the upper section's share, so growing
      // the results pane means shrinking the stored value
      Focus::Data => {
        self.tabs_height_percent = resized_percent(self.tabs_height_percent, -delta);
      },
      Focus::PopUp => {},
    }
  }

  fn last_focused_component(&mut self) {
    match self.last_focused_component {
      Focus::Menu => self.set_focus(Focus::Menu),
      Focus::Editor => self.set_focus(Focus::Editor),
      Focus::Data => self.set_focus(Focus::Data),
      Focus::History => self.set_focus(Focus::History),
      Focus::Favorites => self.set_focus(Focus::Favorites),
      Focus::PopUp => {},
    }
  }

  pub async fn run(&mut self, driver: Driver, args: Cli) -> Result<()> {
    self.state.driver = driver;
    let mut database: Box<dyn Database> = match driver {
      Driver::Postgres => Box::new(database::PostgresDriver::new()),
      Driver::MySql => Box::new(database::MySqlDriver::new()),
      Driver::Sqlite => Box::new(database::SqliteDriver::new()),
      #[cfg(feature = "oracle")]
      Driver::Oracle => Box::new(database::OracleDriver::new()),
      #[cfg(feature = "duckdb")]
      Driver::DuckDb => Box::new(database::DuckDbDriver::new()),
    };
    self.completion.set_builtin_functions(database.builtin_functions());
    let default_title = database.init(args.clone()).await?;
    let terminal_title = args.connection_name.clone().unwrap_or(default_title);
    tracing::Span::current().record("conn", terminal_title.as_str());
    let (action_tx, mut action_rx) = mpsc::unbounded_channel();
    log::info!("driver: {driver:?}, connection: {terminal_title}");

    let mut tui = tui::Tui::new()?
      .mouse(self.mouse_mode_override.or(self.config.settings.mouse_mode))
      .title(Some(format!("rainfrog({})", terminal_title)));
    tui.enter()?;

    #[allow(unused_mut)]
    #[cfg(feature = "arboard")]
    let mut clipboard = Clipboard::new();

    self.components.menu.register_action_handler(action_tx.clone())?;
    self.components.editor.register_action_handler(action_tx.clone())?;
    self.components.history.register_action_handler(action_tx.clone())?;
    self.components.data.register_action_handler(action_tx.clone())?;
    self.components.favorites.register_action_handler(action_tx.clone())?;

    self.components.menu.register_config_handler(self.config.clone())?;
    self.components.editor.register_config_handler(self.config.clone())?;
    self.components.history.register_config_handler(self.config.clone())?;
    self.components.data.register_config_handler(self.config.clone())?;
    self.components.favorites.register_config_handler(self.config.clone())?;

    let size = tui.size()?;
    self.components.menu.init(Rect { width: size.width, height: size.height, x: 0, y: 0 })?;
    self.components.editor.init(Rect { width: size.width, height: size.height, x: 0, y: 0 })?;
    self.components.history.init(Rect { width: size.width, height: size.height, x: 0, y: 0 })?;
    self.components.data.init(Rect { width: size.width, height: size.height, x: 0, y: 0 })?;
    self.components.favorites.init(Rect { width: size.width, height: size.height, x: 0, y: 0 })?;

    action_tx.send(Action::LoadMenu)?;

    loop {
      self.completion.poll(
        std::time::Duration::from_millis(
          self.config.settings.autocomplete_debounce_ms.unwrap_or(100),
        ),
        self.config.settings.autocomplete_trigger_len.unwrap_or(1),
      );
      self.completion.start_missing_columns(database.as_ref())?;
      for event in self.completion.poll_database().await {
        match event {
          CompletionDatabaseEvent::MenuLoadFinished(result) => {
            self.components.menu.set_table_list(Some(result));
          },
        }
      }
      if self.popup.is_some() {
        self.set_focus(Focus::PopUp);
      }
      match database.get_query_results().await? {
        DbTaskResult::Finished(results) => {
          let statement_type = results.data_statement_type();
          self.set_data_state(Some(results.results), statement_type);
          self.state.last_query_end = Some(chrono::Utc::now());
          self.state.query_task_running = false;
        },
        DbTaskResult::ConfirmTx(rows_affected, statement, display_statement) => {
          self.state.last_query_end = Some(chrono::Utc::now());
          self.set_popup(Box::new(ConfirmTx::new(rows_affected, statement, display_statement)));
          self.state.query_task_running = true;
        },
        DbTaskResult::Pending => {
          self.state.query_task_running = true;
        },
        DbTaskResult::NoTask => {
          self.state.query_task_running = false;
        },
      }
      if let Some(e) = tui.next().await {
        let mut event_consumed = false;
        match e {
          tui::Event::Quit => action_tx.send(Action::Quit)?,
          tui::Event::Tick => action_tx.send(Action::Tick)?,
          tui::Event::Render => action_tx.send(Action::Render)?,
          tui::Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?,
          tui::Event::Mouse(event) => self.last_frame_mouse_event = Some(event),
          tui::Event::Paste(ref text) => {
            if let Some(popup) = &mut self.popup {
              popup.handle_paste_events(text, &mut self.state)?;
              event_consumed = true;
            }
          },
          tui::Event::Key(key) => {
            if let Some(keymap) = self.config.keybindings.get(&self.state.focus) {
              if let Some(action) = keymap.get(&vec![key]) {
                log::info!("Got action: {action:?}");
                action_tx.send(action.clone())?;
                event_consumed = true;
              } else if let Some(popup) = &mut self.popup {
                // popup captures all inputs. if it returns a payload, that means
                // it is finished and should be closed
                let payload = popup.handle_key_events(key, &mut self.state)?;
                match payload {
                  Some(PopUpPayload::SetDataTable(result, statement)) => {
                    self.set_data_state(result, statement);
                    self.set_focus(Focus::Editor);
                  },
                  Some(PopUpPayload::ConfirmQuery(query)) => {
                    action_tx.send(Action::Query(vec![query], true, false))?;
                    self.set_focus(Focus::Editor);
                  },
                  Some(PopUpPayload::ConfirmBypass(query)) => {
                    action_tx.send(Action::Query(vec![query], true, true))?;
                    self.set_focus(Focus::Editor);
                  },
                  Some(PopUpPayload::ConfirmExport(confirmed)) => {
                    if confirmed {
                      action_tx.send(Action::ExportData(ExportFormat::CSV))?;
                      self.set_popup(Box::new(Exporting::new()));
                    } else {
                      self.set_focus(Focus::Data);
                    }
                  },
                  Some(PopUpPayload::ConfirmYank(confirmed)) => {
                    if confirmed {
                      action_tx.send(Action::YankAll)?;
                    } else {
                      self.set_focus(Focus::Data);
                    }
                  },
                  Some(PopUpPayload::Cancel) => {
                    self.last_focused_component();
                  },
                  Some(PopUpPayload::NamedFavorite(name, query_lines)) => {
                    self.state.favorites.add_entry(name, query_lines);
                    self.set_focus(Focus::Editor);
                  },
                  Some(PopUpPayload::CommitTx) => {
                    let response = database.commit_tx().await?;
                    self.state.last_query_end = Some(chrono::Utc::now());
                    if let Some(results) = response {
                      let statement_type = results.data_statement_type();
                      self.set_data_state(Some(results.results), statement_type);
                      self.set_focus(Focus::Editor);
                    }
                  },
                  Some(PopUpPayload::RollbackTx) => {
                    database.rollback_tx().await?;
                    self.state.last_query_end = Some(chrono::Utc::now());
                    self.set_data_state(
                      Some(Ok(Rows { headers: vec![], rows: vec![], rows_affected: None })),
                      Some(Statement::Rollback { chain: false, savepoint: None }),
                    );
                    self.set_focus(Focus::Editor);
                  },
                  None => {},
                }
                event_consumed = true;
              } else {
                // If the key was not handled as a single key action,
                // then consider it for multi-key combinations.
                self.last_tick_key_events.push(key);

                // Check for multi-key combinations
                if let Some(action) = keymap.get(&self.last_tick_key_events) {
                  log::info!("Got action: {action:?}");
                  action_tx.send(action.clone())?;
                  event_consumed = true;
                }
              }
            }
          },
          _ => {},
        }
        if !event_consumed {
          for i in ComponentImpls::iter() {
            let action = match i {
              ComponentImpls::Menu => self.components.menu.handle_events(
                Some(e.clone()),
                self.last_tick_key_events.clone(),
                &self.state,
              )?,
              ComponentImpls::Editor => self.components.editor.handle_events(
                Some(e.clone()),
                self.last_tick_key_events.clone(),
                &self.state,
              )?,
              ComponentImpls::History => self.components.history.handle_events(
                Some(e.clone()),
                self.last_tick_key_events.clone(),
                &self.state,
              )?,
              ComponentImpls::Data => self.components.data.handle_events(
                Some(e.clone()),
                self.last_tick_key_events.clone(),
                &self.state,
              )?,
              ComponentImpls::Favorites => self.components.favorites.handle_events(
                Some(e.clone()),
                self.last_tick_key_events.clone(),
                &self.state,
              )?,
            };
            if let Some(action) = action {
              action_tx.send(action)?;
            }
          }
        }
      }

      while let Ok(action) = action_rx.try_recv() {
        if action != Action::Tick && action != Action::Render {
          log::debug!("{action:?}");
        }
        let action_consumed = false;
        match &action {
          Action::NoOp => {},
          Action::Tick => {
            self.last_tick_key_events.drain(..);
          },
          Action::Quit => self.should_quit = true,
          Action::Resize(w, h) => {
            tui.resize(Rect::new(0, 0, *w, *h))?;
            tui.draw(|f| {
              self.draw_layout(f, action_tx.clone()).expect("Couldn't draw layout");
            })?;
          },
          Action::Render => {
            tui.draw(|f| {
              self.draw_layout(f, action_tx.clone()).expect("Couldn't draw layout");
            })?;
            self.last_frame_mouse_event = None;
          },
          Action::FocusMenu => self.set_focus(Focus::Menu),
          Action::FocusEditor => self.set_focus(Focus::Editor),
          Action::FocusData => self.set_focus(Focus::Data),
          Action::FocusHistory => self.set_focus(Focus::History),
          Action::FocusFavorites => self.set_focus(Focus::Favorites),
          Action::CycleFocusForwards => match self.state.focus {
            Focus::Menu => self.set_focus(Focus::Editor),
            Focus::Editor => self.set_focus(Focus::Data),
            Focus::Data => self.set_focus(Focus::History),
            Focus::History => self.set_focus(Focus::Favorites),
            Focus::Favorites => self.set_focus(Focus::Menu),
            Focus::PopUp => {},
          },
          Action::CycleFocusBackwards => match self.state.focus {
            Focus::History => self.set_focus(Focus::Data),
            Focus::Data => self.set_focus(Focus::Editor),
            Focus::Editor => self.set_focus(Focus::Menu),
            Focus::Menu => self.set_focus(Focus::Favorites),
            Focus::Favorites => self.set_focus(Focus::History),
            Focus::PopUp => {},
          },
          Action::IncreaseSectionSize => {
            self.resize_focused_section(SECTION_RESIZE_STEP);
          },
          Action::DecreaseSectionSize => {
            self.resize_focused_section(-SECTION_RESIZE_STEP);
          },
          Action::LoadMenu => {
            self.components.menu.set_table_list(None);
            self.completion.start_menu_load(database.as_ref())?;
          },
          Action::EditQueryExternally(lines) => {
            let query = lines.join("\n");
            tui.exit()?;
            let edit_result = external_editor::edit_query(query).await;
            let resume_result = tui.enter();
            resume_result?;

            match edit_result {
              Ok(edited) => {
                action_tx.send(Action::QueryToEditor(external_editor::query_lines(&edited)))?;
              },
              Err(error) => {
                self.set_data_state(
                  Some(Err(error.wrap_err("Failed to edit query externally"))),
                  None,
                );
              },
            }
          },
          Action::Query(query_lines, confirmed, bypass) => 'query_action: {
            if self.state.query_task_running {
              break 'query_action;
            }
            let query_string = query_lines.clone().join(" \n");
            if query_string.is_empty() {
              break 'query_action;
            }
            self.add_to_history(query_lines.clone());
            if *bypass && !confirmed {
              log::warn!("Bypassing parser");
              self.set_popup(Box::new(ConfirmBypass::new(query_string.clone())));
              break 'query_action;
            }
            let execution_info = match *bypass && *confirmed {
              true => Ok((ExecutionType::Normal, None)),
              false => database::get_execution_type(query_string.clone(), *confirmed, driver),
            };
            match execution_info {
              Ok((ExecutionType::Transaction, _)) => {
                self.set_data_loading();
                database.start_tx(query_string.clone()).await?;
                self.completion.queue_columns_for_text(query_string);
                self.state.last_query_start = Some(chrono::Utc::now());
                self.state.last_query_end = None;
              },
              Ok((ExecutionType::Confirm, Some(statement_type))) => {
                self.set_popup(Box::new(ConfirmQuery::new(query_string.clone(), statement_type)));
              },
              Ok((ExecutionType::Normal, _)) => {
                self.set_data_loading();
                database.start_query(query_string.clone(), *bypass).await?;
                self.completion.queue_columns_for_text(query_string);
                self.state.last_query_start = Some(chrono::Utc::now());
                self.state.last_query_end = None;
              },
              Err(e) => self.set_data_state(Some(Err(e)), None),
              _ => {
                self
                  .set_data_state(Some(Err(eyre!("Missing statement type but not bypass"))), None);
              },
            }
          },
          Action::AbortQuery => match database.abort_query().await {
            Ok(true) => {
              self.set_data_cancelled();
              self.state.last_query_end = Some(chrono::Utc::now());
            },
            Ok(false) => {},
            Err(e) => {
              self.set_data_state(Some(Err(e)), None);
            },
          },
          Action::MenuPreview(preview_type, target) => {
            let preview_query = match preview_type {
              MenuPreview::Rows => match target.kind {
                MenuItemKind::Function => {
                  "select 'Row preview is not available for functions' as message".to_owned()
                },
                _ => database.preview_rows_query(target.schema.as_str(), target.name.as_str()),
              },
              MenuPreview::Columns => {
                database.preview_columns_query(target.schema.as_str(), target.name.as_str())
              },
              MenuPreview::Constraints => {
                database.preview_constraints_query(target.schema.as_str(), target.name.as_str())
              },
              MenuPreview::Indexes => {
                database.preview_indexes_query(target.schema.as_str(), target.name.as_str())
              },
              MenuPreview::Policies => {
                database.preview_policies_query(target.schema.as_str(), target.name.as_str())
              },
              MenuPreview::Definition => match target.kind {
                MenuItemKind::View { materialized } => database.preview_view_definition_query(
                  target.schema.as_str(),
                  target.name.as_str(),
                  materialized,
                ),
                MenuItemKind::Function => database
                  .preview_function_definition_query(target.schema.as_str(), target.name.as_str()),
                MenuItemKind::Table => {
                  "select 'Definition preview is only available for views' as message".to_owned()
                },
              },
            };
            action_tx.send(Action::QueryToEditor(
              preview_query.clone().split('\n').map(|s| s.trim().to_string()).collect(),
            ))?;
            action_tx.send(Action::FocusEditor)?;
            action_tx.send(Action::FocusMenu)?;
            action_tx.send(Action::Query(vec![preview_query.clone()], false, false))?;
          },

          Action::RequestSaveFavorite(query_lines) => {
            self.set_popup(Box::new(NameFavorite::new(
              self.state.favorites.iter().map(|f| f.get_name().to_string()).collect(),
              query_lines.clone(),
            )));
          },
          Action::DeleteFavorite(name) => {
            self.state.favorites.delete_entry(name.clone());
          },
          Action::ClearHistory => {
            self.clear_history();
          },
          Action::CopyData(data) => {
            #[cfg(feature = "arboard")]
            {
              clipboard.as_mut().map_or_else(
                |e| {
                  log::error!("{e:?}");
                },
                |clipboard| {
                  clipboard.set_text(data).unwrap_or_else(|e| {
                    log::error!("{e:?}");
                  })
                },
              );
            }
            self.last_focused_component();
          },
          Action::RequestExportData(row_count) => {
            self.set_popup(Box::new(ConfirmExport::new(*row_count)));
          },
          Action::ExportDataFinished => {
            self.set_focus(Focus::Data);
          },
          Action::RequestYankAll(row_count) => {
            self.set_popup(Box::new(ConfirmYank::new(*row_count)));
          },
          _ => {},
        }
        if !action_consumed {
          for i in ComponentImpls::iter() {
            let action = match i {
              ComponentImpls::Menu => self.components.menu.update(action.clone(), &self.state)?,
              ComponentImpls::Editor => {
                self.components.editor.update(action.clone(), &self.state)?
              },
              ComponentImpls::History => {
                self.components.history.update(action.clone(), &self.state)?
              },
              ComponentImpls::Data => self.components.data.update(action.clone(), &self.state)?,
              ComponentImpls::Favorites => {
                self.components.favorites.update(action.clone(), &self.state)?
              },
            };
            if let Some(action) = action {
              log::info!("{action:?}");
              action_tx.send(action)?;
            }
          }
        }
      }

      if self.last_frame_mouse_event.is_some() {
        tui.draw(|f| {
          self.draw_layout(f, action_tx.clone()).expect("Couldn't draw layout");
        })?;
      }
      if self.should_quit {
        self.completion.cancel_all();
        database.abort_query().await?;
        tui.stop()?;
        break;
      }
    }
    tui.exit()?;
    Ok(())
  }

  fn draw_layout(&mut self, f: &mut Frame, action_tx: mpsc::UnboundedSender<Action>) -> Result<()> {
    let hints_layout = Layout::default()
      .direction(Direction::Vertical)
      .constraints(match f.area().width {
        x if x < 160 => [Constraint::Fill(1), Constraint::Length(2)],
        _ => [Constraint::Fill(1), Constraint::Length(1)],
      })
      .split(f.area());
    let root_layout = Layout::default()
      .direction(Direction::Horizontal)
      .constraints([
        Constraint::Percentage(self.menu_width_percent),
        Constraint::Percentage(100 - self.menu_width_percent),
      ])
      .split(hints_layout[0]);
    let right_layout = Layout::default()
      .direction(Direction::Vertical)
      .constraints([
        Constraint::Percentage(self.tabs_height_percent),
        Constraint::Percentage(100 - self.tabs_height_percent),
      ])
      .split(root_layout[1]);
    let tabs_layout = Layout::default()
      .direction(Direction::Vertical)
      .constraints([Constraint::Length(1), Constraint::Fill(1)])
      .split(right_layout[0]);

    if let Some(event) = &self.last_frame_mouse_event
      && self.popup.is_none()
      && event.kind != MouseEventKind::Moved
      && !matches!(event.kind, MouseEventKind::Down(_))
    {
      let position = Position::new(event.column, event.row);
      let menu_target = root_layout[0];
      let tabs_target = tabs_layout[0];
      let tab_content_target = tabs_layout[1];
      let data_target = right_layout[1];
      if menu_target.contains(position) {
        self.set_focus(Focus::Menu);
      } else if data_target.contains(position) {
        self.set_focus(Focus::Data);
      } else if tab_content_target.contains(position) {
        self.last_focused_tab();
      } else if tabs_target.contains(position) {
        match self.state.focus {
          Focus::Editor => {
            if matches!(event.kind, MouseEventKind::Up(_)) {
              self.set_focus(Focus::History);
            }
          },
          Focus::History => {
            if matches!(event.kind, MouseEventKind::Up(_)) {
              self.set_focus(Focus::Favorites);
            }
          },
          Focus::Favorites => {
            if matches!(event.kind, MouseEventKind::Up(_)) {
              self.set_focus(Focus::Editor);
            }
          },
          Focus::PopUp => {},
          _ => {
            self.state.focus = self.last_focused_tab;
          },
        }
        self.last_frame_mouse_event = None;
      }
    }
    let focus_hint = |action, preferred_hint| {
      self
        .config
        .keybindings
        .hint_for_action(self.state.focus, action, preferred_hint)
        .map_or_else(String::new, |hint| format!(" {hint}"))
    };
    let tabs = Tabs::new(vec![
      format!(" 󰤏 query{}", focus_hint(&Action::FocusEditor, "<alt+2>")),
      format!("   history{}", focus_hint(&Action::FocusHistory, "<alt+4>")),
      format!("   favorites{}", focus_hint(&Action::FocusFavorites, "<alt+5>")),
    ])
    .highlight_style(Style::new().fg(self.state.focus.tab_color()).reversed())
    .select(self.last_focused_tab.tab_index())
    .padding(" ", "")
    .divider(" ");

    let state = &self.state;

    f.render_widget(tabs, tabs_layout[0]);
    f.render_widget(Clear, tabs_layout[1]);

    match self.last_focused_tab {
      Focus::Editor => {
        self.components.editor.draw(f, tabs_layout[1], state).unwrap();
      },
      Focus::History => {
        self.components.history.draw(f, tabs_layout[1], state).unwrap();
      },
      Focus::Favorites => {
        self.components.favorites.draw(f, tabs_layout[1], state).unwrap();
      },
      Focus::Menu | Focus::Data | Focus::PopUp => (),
    };

    self.components.menu.draw(f, root_layout[0], state).unwrap();
    self.components.data.draw(f, right_layout[1], state).unwrap();
    self.render_hints(f, hints_layout[1]);

    if let Some(popup) = &self.popup {
      self.render_popup(f, popup.as_ref());
    }
    Ok(())
  }

  fn render_hints(&self, frame: &mut Frame, area: Rect) {
    let block = Block::default().style(Style::default().fg(Color::Blue));
    let help_text = footer_help_text(&self.config, self.state.focus, self.state.query_task_running);
    let paragraph =
      Paragraph::new(Line::from(help_text).centered()).block(block).wrap(Wrap { trim: true });
    frame.render_widget(paragraph, area);
  }

  fn render_popup(&self, frame: &mut Frame, popup: &dyn PopUp) {
    let area = center(frame.area(), Constraint::Percentage(50), Constraint::Percentage(50));
    let block = Block::default()
      .borders(Borders::ALL)
      .border_style(Style::default().fg(Color::Yellow))
      .title(Line::from(" Confirm Action ").centered())
      .padding(Padding::uniform(1));
    let layout = Layout::default()
      .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
      .direction(Direction::Vertical)
      .split(block.inner(area));

    let popup_cta = Paragraph::new(Line::from(popup.get_cta_text(&self.state)).centered())
      .wrap(Wrap { trim: false });
    let popup_actions = Paragraph::new(Line::from(popup.get_actions_text(&self.state)).centered());
    frame.render_widget(Clear, area);
    frame.render_widget(block, area);
    frame.render_widget(popup_cta, layout[0]);
    frame.render_widget(
      popup_actions,
      center(layout[1], Constraint::Fill(1), Constraint::Percentage(50)),
    );
  }
}

#[cfg(test)]
mod tests {
  use pretty_assertions::assert_eq;

  use super::*;
  use crate::components::app_state_with_focus;
  use crate::config::default_config_contents;

  fn test_app(focus: Focus) -> App {
    let (completion, completion_client) = CompletionCoordinator::new();
    App {
      components: Components {
        menu: Box::new(Menu::new()),
        editor: Box::new(Editor::with_completion_channels(completion_client)),
        history: Box::new(History::new()),
        data: Box::new(Data::new()),
        favorites: Box::new(Favorites::new()),
      },
      should_quit: false,
      mouse_mode_override: None,
      config: Config::default(),
      last_tick_key_events: Vec::new(),
      last_frame_mouse_event: None,
      state: app_state_with_focus(focus),
      last_focused_tab: Focus::Editor,
      last_focused_component: focus,
      popup: None,
      completion,
      menu_width_percent: DEFAULT_MENU_WIDTH_PERCENT,
      tabs_height_percent: DEFAULT_TABS_HEIGHT_PERCENT,
    }
  }

  #[test]
  fn test_resized_percent_steps_and_clamps() {
    assert_eq!(resized_percent(25, 5), 30);
    assert_eq!(resized_percent(25, -5), 20);
    assert_eq!(resized_percent(90, 5), 90);
    assert_eq!(resized_percent(10, -5), 10);
  }

  #[test]
  fn test_resize_focused_section_menu() {
    let mut app = test_app(Focus::Menu);
    app.resize_focused_section(SECTION_RESIZE_STEP);
    assert_eq!(app.menu_width_percent, 30);
    assert_eq!(app.tabs_height_percent, DEFAULT_TABS_HEIGHT_PERCENT);
    app.resize_focused_section(-SECTION_RESIZE_STEP);
    assert_eq!(app.menu_width_percent, DEFAULT_MENU_WIDTH_PERCENT);
  }

  #[test]
  fn test_resize_focused_section_tabs() {
    for focus in [Focus::Editor, Focus::History, Focus::Favorites] {
      let mut app = test_app(focus);
      app.resize_focused_section(SECTION_RESIZE_STEP);
      assert_eq!(app.tabs_height_percent, 50, "focus: {focus:?}");
      assert_eq!(app.menu_width_percent, DEFAULT_MENU_WIDTH_PERCENT, "focus: {focus:?}");
    }
  }

  #[test]
  fn test_resize_focused_section_data_inverts() {
    let mut app = test_app(Focus::Data);
    app.resize_focused_section(SECTION_RESIZE_STEP);
    assert_eq!(app.tabs_height_percent, 40);
    app.resize_focused_section(-SECTION_RESIZE_STEP);
    assert_eq!(app.tabs_height_percent, DEFAULT_TABS_HEIGHT_PERCENT);
  }

  #[test]
  fn test_draw_layout_applies_resized_percentages() {
    use ratatui::{Terminal, backend::TestBackend};

    // returns (right region start column, data block top row): the tab-content
    // block's top-left corner marks the menu width, and the first corner below
    // it marks where the results section begins
    fn markers(
      app: &mut App,
      terminal: &mut Terminal<TestBackend>,
      tx: &mpsc::UnboundedSender<Action>,
    ) -> (u16, u16) {
      terminal.draw(|f| app.draw_layout(f, tx.clone()).unwrap()).unwrap();
      let buf = terminal.backend().buffer();
      let right_start = (0u16..100)
        .find(|&x| buf.cell(Position::new(x, 1)).unwrap().symbol() == "┌")
        .expect("tab content block top border not found");
      let data_top = (2u16..42)
        .find(|&y| buf.cell(Position::new(right_start, y)).unwrap().symbol() == "┌")
        .expect("data block top border not found");
      (right_start, data_top)
    }

    let (tx, _rx) = mpsc::unbounded_channel();
    let mut app = test_app(Focus::Menu);
    // 100x42: each width percent is one column, and with the two hint rows the
    // 40-row main area makes each 5% step exactly two rows
    let mut terminal = Terminal::new(TestBackend::new(100, 42)).unwrap();

    assert_eq!(markers(&mut app, &mut terminal, &tx), (25, 18));

    app.resize_focused_section(SECTION_RESIZE_STEP);
    app.resize_focused_section(SECTION_RESIZE_STEP);
    app.resize_focused_section(SECTION_RESIZE_STEP);
    assert_eq!(markers(&mut app, &mut terminal, &tx), (40, 18));

    app.state.focus = Focus::Editor;
    app.resize_focused_section(SECTION_RESIZE_STEP);
    assert_eq!(markers(&mut app, &mut terminal, &tx), (40, 20));

    app.state.focus = Focus::Data;
    app.resize_focused_section(SECTION_RESIZE_STEP);
    app.resize_focused_section(SECTION_RESIZE_STEP);
    assert_eq!(markers(&mut app, &mut terminal, &tx), (40, 16));
  }

  #[test]
  fn test_resize_focused_section_popup_noop() {
    let mut app = test_app(Focus::PopUp);
    app.resize_focused_section(SECTION_RESIZE_STEP);
    assert_eq!(app.menu_width_percent, DEFAULT_MENU_WIDTH_PERCENT);
    assert_eq!(app.tabs_height_percent, DEFAULT_TABS_HEIGHT_PERCENT);
  }

  #[test]
  fn default_config_preserves_editor_footer_hints() {
    let config: Config = toml::from_str(default_config_contents()).unwrap();

    assert_eq!(
      footer_help_text(&config, Focus::Editor, false),
      "[<alt + enter>|<f5>] execute query [<alt + e>|<f6>] external editor [<ctrl + f>|<alt + f>] save query to favorites"
    );
    assert_eq!(footer_help_text(&config, Focus::Editor, true), "[<alt + q>] abort ");
  }

  #[test]
  fn footer_uses_configured_action_hints_and_keeps_component_hints_static() {
    let config: Config = toml::from_str(
      r#"
        [keybindings.Editor]
        "<Alt-q>" = ""
        "z" = "AbortQuery"
        "<F5>" = ""
        "<Alt-enter>" = "SubmitEditorQuery"
        "x" = "SubmitEditorQuery"
        "<Alt-e>" = ""
        "<F6>" = ""
        "<F2>" = "RequestExternalEditor"
      "#,
    )
    .unwrap();

    assert_eq!(
      footer_help_text(&config, Focus::Editor, false),
      "[<alt + enter>|x] execute query [<f2>] external editor [<ctrl + f>|<alt + f>] save query to favorites"
    );
    assert_eq!(footer_help_text(&config, Focus::Editor, true), "[z] abort ");
  }
}
