use std::{fmt, ops::Deref, sync::OnceLock};

use log::warn;

use crate::config::KdashConfig;
use crate::event::Key;

// using a macro so that we can automatically generate an iterable vector for bindings. This beats reflection :)
macro_rules! generate_keybindings {
  ($($field:ident),+) => {
    #[derive(Clone)]
    pub struct KeyBindings { $(pub $field: KeyBinding),+ }
    impl KeyBindings {
      pub fn as_iter(&self) -> Vec<&KeyBinding> {
        vec![
            $(&self.$field),+
        ]
      }

      pub fn with_overrides(&self, config: &KdashConfig) -> (Self, Vec<String>) {
        let mut updated = self.clone();
        let mut warnings = vec![];

        if let Some(overrides) = &config.keybindings {
          $(
            if let Some(value) = overrides.values.get(stringify!($field)) {
              match value.parse::<Key>() {
                Ok(key) => updated.$field.key = key,
                Err(error) => warnings.push(format!(
                  "Invalid keybinding override for {}: {} ({})",
                  stringify!($field),
                  value,
                  error
                )),
              }
            }
          )+

          for field in overrides.values.keys() {
            if ![ $(stringify!($field)),+ ].contains(&field.as_str()) {
              warnings.push(format!("Unknown keybinding override: {}", field));
            }
          }
        }

        (updated, warnings)
      }
    }
  };
}

generate_keybindings! {
  // order here is shown as is in Help
  quit,
  esc,
  help,
  submit,
  filter,
  refresh,
  toggle_theme,
  cycle_theme_prev,
  cycle_main_views,
  cycle_main_views_prev,
  reset_navigation,
  jump_to_current_context,
  jump_to_all_context,
  jump_to_utilization,
  jump_to_troubleshoot,
  copy_to_clipboard,
  dump_error_log,
  pg_up,
  pg_down,
  home,
  end,
  up,
  down,
  left,
  right,
  toggle_info,
  shell_exec,
  log_auto_scroll,
  select_all_namespace,
  open_action_menu,
  delete_resource,
  restart_resource,
  previous_logs,
  jump_to_namespace,
  describe_resource,
  resource_yaml,
  edit_resource,
  decode_secret,
  jump_to_pods,
  jump_to_services,
  jump_to_nodes,
  jump_to_configmaps,
  jump_to_statefulsets,
  jump_to_replicasets,
  jump_to_deployments,
  jump_to_jobs,
  jump_to_daemonsets,
  jump_to_more_resources,
  jump_to_dynamic_resources,
  aggregate_logs,
  port_forward,
  port_forwards_list,
  cycle_group_by,
  toggle_wide_columns,
  toggle_log_timestamps,
  toggle_log_wrap
}

#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug)]
pub enum HContext {
  General,
  Overview,
  Utilization,
  Logs,
}

impl fmt::Display for HContext {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    write!(f, "{:?}", self)
  }
}

impl HContext {
  /// Section heading shown on the help page.
  fn label(self) -> &'static str {
    match self {
      HContext::General => "General",
      HContext::Overview => "Resource Views",
      HContext::Utilization => "Utilization",
      HContext::Logs => "Logs",
    }
  }
}

#[derive(Clone)]
pub struct KeyBinding {
  pub key: Key,
  pub alt: Option<Key>,
  pub desc: &'static str,
  pub context: HContext,
}

const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
  quit: KeyBinding {
    key: Key::Ctrl('c'),
    alt: Some(Key::Char('q')),
    desc: "Quit",
    context: HContext::General,
  },
  esc: KeyBinding {
    key: Key::Esc,
    alt: None,
    desc: "Close child page/Go back",
    context: HContext::General,
  },
  help: KeyBinding {
    key: Key::Char('?'),
    alt: None,
    desc: "Help page",
    context: HContext::General,
  },
  submit: KeyBinding {
    key: Key::Enter,
    alt: None,
    desc: "Select table row",
    context: HContext::General,
  },
  filter: KeyBinding {
    key: Key::Char('/'),
    alt: None,
    desc: "Filter current view",
    context: HContext::General,
  },
  refresh: KeyBinding {
    key: Key::Ctrl('r'),
    alt: None,
    desc: "Refresh data",
    context: HContext::General,
  },
  toggle_theme: KeyBinding {
    key: Key::Char('t'),
    alt: None,
    desc: "Cycle theme (next)",
    context: HContext::General,
  },
  cycle_theme_prev: KeyBinding {
    key: Key::Alt('t'),
    alt: None,
    desc: "Cycle theme (previous)",
    context: HContext::General,
  },
  jump_to_current_context: KeyBinding {
    key: Key::Shift('a'),
    alt: None,
    desc: "Switch to active context view",
    context: HContext::General,
  },
  jump_to_all_context: KeyBinding {
    key: Key::Shift('c'),
    alt: None,
    desc: "Switch to all contexts view",
    context: HContext::General,
  },
  jump_to_utilization: KeyBinding {
    key: Key::Shift('u'),
    alt: None,
    desc: "Switch to resource utilization view",
    context: HContext::General,
  },
  jump_to_troubleshoot: KeyBinding {
    key: Key::Shift('t'),
    alt: None,
    desc: "Switch to troubleshoot view",
    context: HContext::General,
  },
  cycle_main_views: KeyBinding {
    key: Key::Tab,
    alt: None,
    desc: "Cycle through main views",
    context: HContext::General,
  },
  cycle_main_views_prev: KeyBinding {
    key: Key::BackTab,
    alt: None,
    desc: "Cycle through main views (previous)",
    context: HContext::General,
  },
  reset_navigation: KeyBinding {
    key: Key::Ctrl('h'),
    alt: None,
    desc: "Reset navigation to root view",
    context: HContext::General,
  },
  copy_to_clipboard: KeyBinding {
    key: Key::Char('c'),
    alt: None,
    desc: "Copy log/output to clipboard",
    context: HContext::General,
  },
  dump_error_log: KeyBinding {
    key: Key::Shift('d'),
    alt: None,
    desc: "Dump recent errors to file",
    context: HContext::General,
  },
  down: KeyBinding {
    key: Key::Down,
    alt: Some(Key::Char('j')),
    desc: "Next row/Scroll down",
    context: HContext::General,
  },
  up: KeyBinding {
    key: Key::Up,
    alt: Some(Key::Char('k')),
    desc: "Previous row/Scroll up",
    context: HContext::General,
  },
  pg_up: KeyBinding {
    key: Key::PageUp,
    alt: None,
    desc: "Scroll page up",
    context: HContext::General,
  },
  pg_down: KeyBinding {
    key: Key::PageDown,
    alt: None,
    desc: "Scroll page down",
    context: HContext::General,
  },
  home: KeyBinding {
    key: Key::Home,
    alt: None,
    desc: "Scroll to top",
    context: HContext::General,
  },
  end: KeyBinding {
    key: Key::End,
    alt: None,
    desc: "Scroll to bottom",
    context: HContext::General,
  },
  left: KeyBinding {
    key: Key::Left,
    alt: Some(Key::Char('h')),
    desc: "Next resource tab",
    context: HContext::Overview,
  },
  right: KeyBinding {
    key: Key::Right,
    alt: Some(Key::Char('l')),
    desc: "Previous resource tab",
    context: HContext::Overview,
  },
  toggle_info: KeyBinding {
    key: Key::Char('i'),
    alt: None,
    desc: "Show/Hide info bar",
    context: HContext::Overview,
  },
  shell_exec: KeyBinding {
    key: Key::Char('s'),
    alt: None,
    desc: "Open shell in selected container",
    context: HContext::Overview,
  },
  log_auto_scroll: KeyBinding {
    key: Key::Char('s'),
    alt: None,
    desc: "Toggle log auto scroll",
    context: HContext::Overview,
  },
  jump_to_namespace: KeyBinding {
    key: Key::Char('n'),
    alt: None,
    desc: "Select namespace block",
    context: HContext::Overview,
  },
  select_all_namespace: KeyBinding {
    key: Key::Char('a'),
    alt: None,
    desc: "Select all namespaces",
    context: HContext::Overview,
  },
  open_action_menu: KeyBinding {
    key: Key::Char('m'),
    alt: None,
    desc: "Show actions menu for selected resource",
    context: HContext::Overview,
  },
  delete_resource: KeyBinding {
    key: Key::Ctrl('d'),
    alt: None,
    desc: "Delete selected resource",
    context: HContext::Overview,
  },
  restart_resource: KeyBinding {
    key: Key::Char('r'),
    alt: None,
    desc: "Rollout restart workload (deploy/sts/ds)",
    context: HContext::Overview,
  },
  previous_logs: KeyBinding {
    key: Key::Char('p'),
    alt: None,
    desc: "Show previous (restarted) container logs",
    context: HContext::Overview,
  },
  describe_resource: KeyBinding {
    key: Key::Char('d'),
    alt: None,
    desc: "Describe resource",
    context: HContext::Overview,
  },
  resource_yaml: KeyBinding {
    key: Key::Char('y'),
    alt: None,
    desc: "Get resource YAML",
    context: HContext::Overview,
  },
  edit_resource: KeyBinding {
    key: Key::Char('e'),
    alt: None,
    desc: "Edit resource in $EDITOR",
    context: HContext::Overview,
  },
  decode_secret: KeyBinding {
    key: Key::Char('x'),
    alt: None,
    desc: "Decode secret",
    context: HContext::Overview,
  },
  jump_to_pods: KeyBinding {
    key: Key::Char('1'),
    alt: None,
    desc: "Select pods tab",
    context: HContext::Overview,
  },
  jump_to_services: KeyBinding {
    key: Key::Char('2'),
    alt: None,
    desc: "Select services tab",
    context: HContext::Overview,
  },
  jump_to_nodes: KeyBinding {
    key: Key::Char('3'),
    alt: None,
    desc: "Select nodes tab",
    context: HContext::Overview,
  },
  jump_to_configmaps: KeyBinding {
    key: Key::Char('4'),
    alt: None,
    desc: "Select configmaps tab",
    context: HContext::Overview,
  },
  jump_to_statefulsets: KeyBinding {
    key: Key::Char('5'),
    alt: None,
    desc: "Select replicasets tab",
    context: HContext::Overview,
  },
  jump_to_replicasets: KeyBinding {
    key: Key::Char('6'),
    alt: None,
    desc: "Select statefulsets tab",
    context: HContext::Overview,
  },
  jump_to_deployments: KeyBinding {
    key: Key::Char('7'),
    alt: None,
    desc: "Select deployments tab",
    context: HContext::Overview,
  },
  jump_to_jobs: KeyBinding {
    key: Key::Char('8'),
    alt: None,
    desc: "Select jobs tab",
    context: HContext::Overview,
  },
  jump_to_daemonsets: KeyBinding {
    key: Key::Char('9'),
    alt: None,
    desc: "Select daemon sets tab",
    context: HContext::Overview,
  },
  jump_to_more_resources: KeyBinding {
    key: Key::Char('0'),
    alt: None,
    desc: "Select more resources",
    context: HContext::Overview,
  },
  jump_to_dynamic_resources: KeyBinding {
    key: Key::Char('-'),
    alt: None,
    desc: "Select dynamic resources",
    context: HContext::Overview,
  },
  aggregate_logs: KeyBinding {
    key: Key::Shift('l'),
    alt: None,
    desc: "Aggregate logs for resource",
    context: HContext::Overview,
  },
  port_forward: KeyBinding {
    key: Key::Char('f'),
    alt: None,
    desc: "Port-forward selected pod/service",
    context: HContext::Overview,
  },
  port_forwards_list: KeyBinding {
    key: Key::Shift('f'),
    alt: None,
    desc: "List/stop active port-forwards",
    context: HContext::Overview,
  },
  cycle_group_by: KeyBinding {
    key: Key::Char('g'),
    alt: None,
    desc: "Cycle through grouping",
    context: HContext::Utilization,
  },
  toggle_wide_columns: KeyBinding {
    key: Key::Char('w'),
    alt: None,
    desc: "Toggle wide view (show all columns)",
    context: HContext::General,
  },
  toggle_log_timestamps: KeyBinding {
    key: Key::Char('t'),
    alt: None,
    desc: "Toggle log timestamps",
    context: HContext::Logs,
  },
  toggle_log_wrap: KeyBinding {
    key: Key::Char('w'),
    alt: None,
    desc: "Toggle log line wrap",
    context: HContext::Logs,
  },
};

static ACTIVE_KEYBINDINGS: OnceLock<KeyBindings> = OnceLock::new();

pub struct ActiveKeyBindings;

impl Deref for ActiveKeyBindings {
  type Target = KeyBindings;

  fn deref(&self) -> &Self::Target {
    ACTIVE_KEYBINDINGS.get_or_init(|| DEFAULT_KEYBINDINGS.clone())
  }
}

pub static DEFAULT_KEYBINDING: ActiveKeyBindings = ActiveKeyBindings;

pub fn initialize_keybindings(config: &KdashConfig) -> Vec<String> {
  let (keybindings, warnings) = DEFAULT_KEYBINDINGS.with_overrides(config);
  let _ = ACTIVE_KEYBINDINGS.set(keybindings);

  for warning in &warnings {
    warn!("{}", warning);
  }

  warnings
}

/// A help-page section: a context-group title plus its `(keys, description)`
/// rows. Drives the grouped, multi-column help page.
pub struct HelpSection {
  pub title: &'static str,
  pub context: HContext,
  pub rows: Vec<(String, String)>,
}

/// Build the help-page contents grouped by keybinding context, in display order.
/// Empty groups are dropped.
pub fn get_help_sections() -> Vec<HelpSection> {
  let items = DEFAULT_KEYBINDING.as_iter();
  [
    HContext::General,
    HContext::Overview,
    HContext::Utilization,
    HContext::Logs,
  ]
  .into_iter()
  .filter_map(|ctx| {
    let rows: Vec<(String, String)> = items
      .iter()
      .filter(|b| b.context == ctx)
      .map(|b| (key_label(b), b.desc.to_string()))
      .collect();
    (!rows.is_empty()).then_some(HelpSection {
      title: ctx.label(),
      context: ctx,
      rows,
    })
  })
  .collect()
}

/// Compact glyph keys for a binding, e.g. `Ctrl+c,q` or `↑,k`.
fn key_label(item: &KeyBinding) -> String {
  match item.alt {
    Some(alt) => format!("{},{}", item.key.symbol(), alt.symbol()),
    None => item.key.symbol(),
  }
}

#[cfg(test)]
mod tests {
  use std::collections::BTreeMap;

  use super::{DEFAULT_KEYBINDINGS, *};
  use crate::config::{KdashConfig, KeybindingOverrides};

  #[test]
  fn test_as_iter() {
    assert!(DEFAULT_KEYBINDING.as_iter().len() >= 28);
  }

  #[test]
  fn test_with_overrides_updates_known_binding() {
    let config = KdashConfig {
      keybindings: Some(KeybindingOverrides {
        values: BTreeMap::from([("quit".into(), "ctrl+q".into())]),
      }),
      ..Default::default()
    };

    let (keybindings, warnings) = DEFAULT_KEYBINDINGS.with_overrides(&config);

    assert_eq!(keybindings.quit.key, Key::Ctrl('q'));
    assert!(warnings.is_empty());
  }

  #[test]
  fn test_with_overrides_treats_uppercase_and_shift_equally() {
    let uppercase = KdashConfig {
      keybindings: Some(KeybindingOverrides {
        values: BTreeMap::from([("dump_error_log".into(), "D".into())]),
      }),
      ..Default::default()
    };
    let shifted = KdashConfig {
      keybindings: Some(KeybindingOverrides {
        values: BTreeMap::from([("dump_error_log".into(), "shift+d".into())]),
      }),
      ..Default::default()
    };

    let (uppercase_bindings, uppercase_warnings) = DEFAULT_KEYBINDINGS.with_overrides(&uppercase);
    let (shifted_bindings, shifted_warnings) = DEFAULT_KEYBINDINGS.with_overrides(&shifted);

    assert_eq!(uppercase_bindings.dump_error_log.key, Key::Shift('d'));
    assert_eq!(
      uppercase_bindings.dump_error_log.key,
      shifted_bindings.dump_error_log.key
    );
    assert!(uppercase_warnings.is_empty());
    assert!(shifted_warnings.is_empty());
  }

  #[test]
  fn test_with_overrides_warns_on_invalid_or_unknown_binding() {
    let config = KdashConfig {
      keybindings: Some(KeybindingOverrides {
        values: BTreeMap::from([
          ("quit".into(), "boguskey".into()),
          ("made_up".into(), "x".into()),
        ]),
      }),
      ..Default::default()
    };

    let (_, warnings) = DEFAULT_KEYBINDINGS.with_overrides(&config);

    assert_eq!(warnings.len(), 2);
    assert!(warnings.iter().any(|warning| warning.contains("quit")));
    assert!(warnings.iter().any(|warning| warning.contains("made_up")));
  }
}
