use crossterm::{
    event::{
        self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers,
        MouseButton, MouseEvent, MouseEventKind,
    },
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
    backend::CrosstermBackend,
    layout::{Constraint, Direction, Layout, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph},
    Frame, Terminal,
};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};

use crate::cli::Args;
use crate::config::Config;
use crate::config::FavoriteEntry;
use crate::lookup;
use crate::tlds::{apply_top_tlds, default_list_name, get_tlds_or_default, list_names};
use crate::types::{DomainResult, DomainStatus, ErrorKind};

// note : this will be the worst shitshow of code you will probably have looked at in youre entire life
// it works and is somewhat stable but i didnt feel like sorting it into nice modules and all mostly just relying
// on copy pasting shit where i need it
//
// have fun and may you forgive me for this extremly large ahh file.

// names and labels
const APP_NAME: &str = "hoardom";
const APP_DESC: &str = "Domain hoarding made less painful";
const CLOSE_BUTTON_LABEL: &str = "[X]";
const EXPORT_BUTTON_LABEL: &str = "[Export](F2)";
const HELP_BUTTON_LABEL: &str = "[Help](F1)";
const SEARCH_BUTTON_LABEL: &str = "[Search]";
const STOP_BUTTON_LABEL: &str = "[Stop](s)";
const CLEAR_BUTTON_LABEL: &str = "[Clear](C)";

// Layout tuning constants
const TOPBAR_HEIGHT: u16 = 1;
const SEARCH_PANEL_HEIGHT: u16 = 3;
const CONTENT_MIN_HEIGHT: u16 = 5;

const SIDEBAR_TARGET_WIDTH_PERCENT: u16 = 30;
const SIDEBAR_MIN_WIDTH: u16 = 24;
const SIDEBAR_MAX_WIDTH: u16 = 26;

const SCRATCHPAD_TARGET_WIDTH_PERCENT: u16 = 30;
const SCRATCHPAD_MIN_WIDTH: u16 = 20;
const SCRATCHPAD_MAX_WIDTH: u16 = 32;

const RESULTS_MIN_WIDTH: u16 = 24;

const FAVORITES_MIN_HEIGHT: u16 = 4;
const SETTINGS_PANEL_HEIGHT: u16 = 8;
const SETTINGS_PANEL_MIN_HEIGHT: u16 = SETTINGS_PANEL_HEIGHT;
const SETTINGS_PANEL_MAX_HEIGHT: u16 = SETTINGS_PANEL_HEIGHT;

const DROPDOWN_MAX_WIDTH: u16 = 26;
const DROPDOWN_MAX_HEIGHT: u16 = 10;
const EXPORT_POPUP_WIDTH: u16 = 82;
const EXPORT_POPUP_HEIGHT: u16 = 10;

const MIN_UI_WIDTH: u16 = SIDEBAR_MIN_WIDTH + RESULTS_MIN_WIDTH + SCRATCHPAD_MIN_WIDTH;
const MIN_UI_HEIGHT: u16 = TOPBAR_HEIGHT + SEARCH_PANEL_HEIGHT + CONTENT_MIN_HEIGHT + 5;

#[derive(Debug, Clone, PartialEq)]
enum Focus {
    Search,
    Scratchpad,
    Results,
    Favorites,
    Settings,
}

fn escape_csv(value: &str) -> String {
    if value.contains([',', '"', '\n']) {
        format!("\"{}\"", value.replace('"', "\"\""))
    } else {
        value.to_string()
    }
}

fn export_favorites_txt(path: &Path, favorites: &[FavoriteEntry]) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)
            .map_err(|e| format!("Failed to create export directory: {}", e))?;
    }
    let text: Vec<&str> = favorites.iter().map(|f| f.domain.as_str()).collect();
    std::fs::write(path, text.join("\n")).map_err(|e| format!("Failed to export favorites: {}", e))
}

fn export_results_csv(path: &Path, results: &[&DomainResult]) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)
            .map_err(|e| format!("Failed to create export directory: {}", e))?;
    }

    let mut lines = vec!["domain,status,details".to_string()];
    for result in results {
        lines.push(format!(
            "{},{},{}",
            escape_csv(&result.full),
            escape_csv(result.status_str()),
            escape_csv(&result.note_str()),
        ));
    }

    std::fs::write(path, lines.join("\n")).map_err(|e| format!("Failed to export results: {}", e))
}

#[derive(Debug, Clone, PartialEq)]
enum DropdownState {
    Closed,
    Open(usize), // which option is highlighted
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ExportMode {
    FavoritesTxt,
    ResultsCsv,
}

impl ExportMode {
    fn default_file_name(self) -> &'static str {
        match self {
            ExportMode::FavoritesTxt => "hoardom-favorites.txt",
            ExportMode::ResultsCsv => "hoardom-results.csv",
        }
    }

    fn toggled(self) -> Self {
        match self {
            ExportMode::FavoritesTxt => ExportMode::ResultsCsv,
            ExportMode::ResultsCsv => ExportMode::FavoritesTxt,
        }
    }
}

#[derive(Debug, Clone)]
struct ExportPopup {
    mode: ExportMode,
    selected_row: usize,
    path: String,
    cursor_pos: usize,
    status: Option<String>,
    status_success: bool,
    confirm_overwrite: bool,
    close_at: Option<Instant>,
}

struct App {
    search_input: String,
    cursor_pos: usize,
    results: Vec<(usize, DomainResult)>,
    results_state: ListState,
    favorites: Vec<FavoriteEntry>,
    favorites_state: ListState,
    focus: Focus,
    show_unavailable: bool,
    clear_on_search: bool,
    tld_list_name: String,
    settings_selected: Option<usize>,
    show_notes_panel: bool,
    last_fav_export_path: String,
    last_res_export_path: String,
    scratchpad: String,
    scratchpad_cursor: usize,
    dropdown: DropdownState,
    searching: bool,
    search_progress: (usize, usize),
    search_started_at: Option<Instant>,
    last_search_duration: Option<Duration>,
    status_msg: Option<String>,
    config_path: PathBuf,
    can_save: bool,
    cache_path: Option<PathBuf>,
    force_refresh: bool,
    mouse_enabled: bool,
    top_tlds: Option<Vec<String>>,
    only_top: Option<Vec<String>>,
    imported_lists: Vec<crate::config::ImportedFilter>,
    cache_settings: crate::config::CacheSettings,
    verbose: bool,
    delay: f64,
    retries: u32,
    jobs: u8,
    panel_rects: PanelRects, // updated each frame for mouse hit detection
    stream_rx: Option<tokio::sync::mpsc::Receiver<lookup::StreamMsg>>,
    stream_task: Option<tokio::task::JoinHandle<()>>,
    patch: crate::tlds::WhoisOverrides,
    noretry: Vec<ErrorKind>,
    should_quit: bool,
    show_help: bool,
    export_popup: Option<ExportPopup>,
    fav_check_rx: Option<tokio::sync::mpsc::Receiver<lookup::StreamMsg>>,
    fav_check_task: Option<tokio::task::JoinHandle<()>>,
    checking_favorites: bool,
    backups_enabled: bool,
    backup_count: u32,
    no_color: bool,
    no_unicode: bool,
}

#[derive(Debug, Clone, Default)]
struct PanelRects {
    topbar: Option<Rect>,
    export_button: Option<Rect>,
    help_button: Option<Rect>,
    help_popup: Option<Rect>,
    dropdown: Option<Rect>,
    search: Option<Rect>,
    search_button: Option<Rect>,
    cancel_button: Option<Rect>,
    clear_button: Option<Rect>,
    export_popup: Option<Rect>,
    export_mode_favorites: Option<Rect>,
    export_mode_results: Option<Rect>,
    export_path: Option<Rect>,
    export_cancel: Option<Rect>,
    export_save: Option<Rect>,
    scratchpad: Option<Rect>,
    results: Option<Rect>,
    favorites: Option<Rect>,
    fav_check_button: Option<Rect>,
    settings: Option<Rect>,
}

impl App {
    fn new(
        args: &Args,
        config: &Config,
        config_path: PathBuf,
        can_save: bool,
        cache_path: Option<PathBuf>,
        force_refresh: bool,
        whois_overrides: crate::tlds::WhoisOverrides,
        noretry: Vec<ErrorKind>,
    ) -> Self {
        let tld_list_name = args
            .tld_list
            .as_ref()
            .map(|s| s.to_lowercase())
            .unwrap_or_else(|| config.settings.tld_list.clone());

        Self {
            search_input: String::new(),
            cursor_pos: 0,
            results: Vec::new(),
            results_state: ListState::default(),
            favorites: config.favorites.clone(),
            favorites_state: ListState::default(),
            focus: Focus::Search,
            show_unavailable: config.settings.show_all || args.show_all,
            clear_on_search: config.settings.clear_on_search,
            tld_list_name,
            settings_selected: Some(0),
            show_notes_panel: config.settings.show_notes_panel,
            last_fav_export_path: config.settings.last_fav_export_path.clone(),
            last_res_export_path: config.settings.last_res_export_path.clone(),
            scratchpad: config.scratchpad.clone(),
            scratchpad_cursor: config.scratchpad.len(),
            dropdown: DropdownState::Closed,
            searching: false,
            search_progress: (0, 0),
            search_started_at: None,
            last_search_duration: None,
            status_msg: None,
            config_path,
            can_save,
            cache_path,
            force_refresh,
            mouse_enabled: !args.no_mouse,
            top_tlds: args.top_tlds.clone(),
            only_top: args.only_top.clone(),
            imported_lists: config.imported_filters.clone(),
            cache_settings: config.cache.clone(),
            verbose: args.verbose,
            delay: args.effective_delay(),
            retries: args.effective_retry(),
            jobs: if args.jobs.is_some() {
                args.effective_jobs()
            } else {
                config.settings.jobs.max(1)
            },
            panel_rects: PanelRects::default(),
            stream_rx: None,
            stream_task: None,
            patch: whois_overrides,
            noretry,
            should_quit: false,
            show_help: false,
            export_popup: None,
            fav_check_rx: None,
            fav_check_task: None,
            checking_favorites: false,
            backups_enabled: config.settings.backups,
            backup_count: config.settings.backup_count,
            no_color: args.no_color,
            no_unicode: args.no_unicode,
        }
    }

    /// style with fg color, or plain style if no_color is on
    fn fg(&self, color: Color) -> Style {
        if self.no_color {
            Style::default()
        } else {
            Style::default().fg(color)
        }
    }

    /// style with fg color + bold
    fn fg_bold(&self, color: Color) -> Style {
        if self.no_color {
            Style::default().add_modifier(Modifier::BOLD)
        } else {
            Style::default().fg(color).add_modifier(Modifier::BOLD)
        }
    }

    /// resolve a color: returns Color::Reset when no_color is on
    /// use this when chaining .bg() or .fg() on existing styles
    fn c(&self, color: Color) -> Color {
        if self.no_color { Color::Reset } else { color }
    }

    // unicode symbol helpers
    fn sym_check(&self) -> &'static str {
        if self.no_unicode { "+" } else { "\u{2713}" }
    }
    fn sym_cross(&self) -> &'static str {
        if self.no_unicode { "x" } else { "\u{2717}" }
    }
    fn sym_sep(&self) -> &'static str {
        if self.no_unicode { "|" } else { "\u{2502}" }
    }
    fn sym_bar_filled(&self) -> &'static str {
        if self.no_unicode { "#" } else { "\u{2588}" }
    }
    fn sym_bar_empty(&self) -> &'static str {
        if self.no_unicode { "-" } else { "\u{2591}" }
    }

    /// preconfigured Block with ASCII or unicode borders
    fn block(&self) -> Block<'static> {
        let mut b = Block::default().borders(Borders::ALL);
        if self.no_unicode {
            b = b.border_type(BorderType::Plain);
        }
        b
    }

    fn get_effective_tlds(&self) -> Vec<&'static str> {
        let mut tld_vec = self.base_tlds_for_selection();

        if let Some(ref only) = self.only_top {
            tld_vec = only
                .iter()
                .filter(|s| !s.is_empty())
                .map(|s| -> &'static str { Box::leak(s.clone().into_boxed_str()) })
                .collect();
        }

        if let Some(ref top) = self.top_tlds {
            tld_vec = apply_top_tlds(tld_vec, top);
        }

        tld_vec
    }

    fn base_tlds_for_selection(&self) -> Vec<&'static str> {
        if let Some(tlds) = crate::tlds::get_tlds(&self.tld_list_name) {
            tlds
        } else if let Some(imported) = self
            .imported_lists
            .iter()
            .find(|list| list.name == self.tld_list_name)
        {
            imported
                .tlds
                .iter()
                .map(|s| -> &'static str { Box::leak(s.clone().into_boxed_str()) })
                .collect()
        } else {
            get_tlds_or_default(default_list_name())
        }
    }

    fn list_options(&self) -> Vec<String> {
        let mut options: Vec<String> = list_names().iter().map(|s| s.to_string()).collect();
        for imported in &self.imported_lists {
            if !options.contains(&imported.name) {
                options.push(imported.name.clone());
            }
        }
        options
    }

    fn parsed_queries(&self) -> Vec<String> {
        self.search_input
            .split(|c: char| c.is_whitespace() || c == ',')
            .map(str::trim)
            .filter(|s| !s.is_empty())
            .map(ToOwned::to_owned)
            .collect()
    }

    fn default_export_dir() -> PathBuf {
        #[cfg(debug_assertions)]
        {
            if let Ok(dir) = std::env::current_dir() {
                return dir;
            }
        }

        dirs::home_dir().unwrap_or_else(|| PathBuf::from("."))
    }

    fn suggested_export_path(&self, mode: ExportMode) -> String {
        let last_export_path = match mode {
            ExportMode::FavoritesTxt => &self.last_fav_export_path,
            ExportMode::ResultsCsv => &self.last_res_export_path,
        };

        let base = if last_export_path.is_empty() {
            Self::default_export_dir().join(mode.default_file_name())
        } else {
            let last = PathBuf::from(last_export_path);
            if last.is_dir() {
                last.join(mode.default_file_name())
            } else {
                last
            }
        };
        base.display().to_string()
    }

    fn open_export_popup(&mut self) {
        let mode = ExportMode::FavoritesTxt;
        let path = self.suggested_export_path(mode);
        self.export_popup = Some(ExportPopup {
            mode,
            selected_row: 0,
            cursor_pos: path.len(),
            path,
            status: None,
            status_success: false,
            confirm_overwrite: false,
            close_at: None,
        });
    }

    fn set_export_mode(&mut self, mode: ExportMode) {
        let suggested_path = self.suggested_export_path(mode);
        if let Some(popup) = &mut self.export_popup {
            popup.mode = mode;
            popup.path = suggested_path;
            popup.cursor_pos = popup.path.len();
            popup.status = None;
            popup.status_success = false;
            popup.confirm_overwrite = false;
            popup.close_at = None;
        }
    }

    fn clear_results(&mut self) {
        self.results.clear();
        self.results_state.select(None);
        self.search_progress = (0, 0);
        self.status_msg = Some("Results cleared".to_string());
    }
    fn save_config(&self) {
        if !self.can_save {
            return;
        }
        let config = Config {
            settings: crate::config::Settings {
                tld_list: self.tld_list_name.clone(),
                show_all: self.show_unavailable,
                clear_on_search: self.clear_on_search,
                show_notes_panel: self.show_notes_panel,
                last_fav_export_path: self.last_fav_export_path.clone(),
                last_res_export_path: self.last_res_export_path.clone(),
                top_tlds: self.top_tlds.clone().unwrap_or_default(),
                jobs: self.jobs,
                noretry: self
                    .noretry
                    .iter()
                    .map(|k| k.to_config_str().to_string())
                    .collect(),
                backups: self.backups_enabled,
                backup_count: self.backup_count,
            },
            favorites: self.favorites.clone(),
            imported_filters: self.imported_lists.clone(),
            cache: self.cache_settings.clone(),
            scratchpad: self.scratchpad.clone(),
        };
        let _ = config.save(&self.config_path);
    }

    fn add_favorite(&mut self, domain: &str) {
        let d = domain.to_lowercase();
        if !self.favorites.iter().any(|f| f.domain == d) {
            // check if we just looked this domain up - inherit its status
            let status = self
                .results
                .iter()
                .find(|(_, r)| r.full.to_lowercase() == d)
                .map(|(_, r)| r.status_str().to_string())
                .unwrap_or_else(|| "unknown".to_string());
            let checked = if status != "unknown" {
                chrono::Utc::now().to_rfc3339()
            } else {
                String::new()
            };
            self.favorites.push(FavoriteEntry {
                domain: d,
                status,
                checked,
                changed: false,
            });
            self.save_config();
        }
    }

    fn remove_focused_favorite(&mut self) {
        if let Some(idx) = self.favorites_state.selected() {
            if idx < self.favorites.len() {
                self.favorites.remove(idx);
                // adjust selection
                if !self.favorites.is_empty() {
                    if idx >= self.favorites.len() {
                        self.favorites_state.select(Some(self.favorites.len() - 1));
                    }
                } else {
                    self.favorites_state.select(None);
                }
                self.save_config();
            }
        }
    }

    fn visible_results(&self) -> Vec<&DomainResult> {
        if self.show_unavailable {
            self.results.iter().map(|(_, r)| r).collect()
        } else {
            self.results
                .iter()
                .filter(|(_, r)| r.is_available())
                .map(|(_, r)| r)
                .collect()
        }
    }

    fn set_tld_list_by_index(&mut self, idx: usize) {
        let options = self.list_options();
        if let Some(selected) = options.get(idx) {
            self.tld_list_name = selected.clone();
        } else {
            self.tld_list_name = default_list_name().to_string();
        }
        self.save_config();
    }

    fn tld_list_index(&self) -> usize {
        self.list_options()
            .iter()
            .position(|option| option == &self.tld_list_name)
            .unwrap_or(0)
    }

    fn set_focus(&mut self, focus: Focus) {
        self.focus = focus;
        if self.focus == Focus::Settings && self.settings_selected.is_none() {
            self.settings_selected = Some(0);
        }
    }

    fn panel_at(&self, col: u16, row: u16) -> Option<Focus> {
        let pos = (col, row);
        if let Some(r) = self.panel_rects.search {
            if contains_pos(r, pos) {
                return Some(Focus::Search);
            }
        }
        if let Some(r) = self.panel_rects.scratchpad {
            if contains_pos(r, pos) {
                return Some(Focus::Scratchpad);
            }
        }
        if let Some(r) = self.panel_rects.results {
            if contains_pos(r, pos) {
                return Some(Focus::Results);
            }
        }
        if let Some(r) = self.panel_rects.favorites {
            if contains_pos(r, pos) {
                return Some(Focus::Favorites);
            }
        }
        if let Some(r) = self.panel_rects.settings {
            if contains_pos(r, pos) {
                return Some(Focus::Settings);
            }
        }
        None
    }
}

fn contains_pos(rect: Rect, pos: (u16, u16)) -> bool {
    pos.0 >= rect.x
        && pos.0 < rect.x + rect.width
        && pos.1 >= rect.y
        && pos.1 < rect.y + rect.height
}

fn clamp_panel_size(value: u16, min: u16, max: u16) -> u16 {
    value.max(min).min(max)
}

fn export_popup_rect(area: Rect) -> Rect {
    let width = EXPORT_POPUP_WIDTH.min(area.width.saturating_sub(4));
    let height = EXPORT_POPUP_HEIGHT.min(area.height.saturating_sub(4));
    Rect {
        x: area.x + (area.width.saturating_sub(width)) / 2,
        y: area.y + (area.height.saturating_sub(height)) / 2,
        width,
        height,
    }
}

fn help_popup_rect(area: Rect) -> Rect {
    let width = if area.width > 54 {
        area.width.saturating_sub(6).min(58)
    } else {
        area.width.saturating_sub(2)
    };
    let height = if area.height > 22 {
        20
    } else {
        area.height.saturating_sub(2)
    };

    Rect {
        x: area.x + (area.width.saturating_sub(width)) / 2,
        y: area.y + (area.height.saturating_sub(height)) / 2,
        width,
        height,
    }
}

pub async fn run_tui(
    args: &Args,
    config: &Config,
    paths: crate::config::HoardomPaths,
    cache_file: Option<PathBuf>,
    force_refresh: bool,
    whois_overrides: crate::tlds::WhoisOverrides,
    noretry: Vec<ErrorKind>,
) -> io::Result<()> {
    // terminal setup
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    if !args.no_mouse {
        execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
    } else {
        execute!(stdout, EnterAlternateScreen)?;
    }
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    let mut app = App::new(
        args,
        config,
        paths.config_file.clone(),
        paths.can_save,
        cache_file,
        force_refresh,
        whois_overrides,
        noretry,
    );

    if !paths.can_save {
        app.status_msg = Some("Warning: favorites and settings wont be saved".to_string());
    }

    let result = run_app(&mut terminal, &mut app).await;

    // put the terminal back to normal
    if !args.no_mouse {
        execute!(terminal.backend_mut(), DisableMouseCapture)?;

        while event::poll(std::time::Duration::from_millis(0))? {
            let _ = event::read();
        }

        execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
    } else {
        execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
    }
    terminal.backend_mut().flush()?;
    disable_raw_mode()?;
    terminal.show_cursor()?;

    result
}

async fn run_app(
    terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
    app: &mut App,
) -> io::Result<()> {
    loop {
        if app.should_quit {
            return Ok(());
        }

        if let Some(popup) = app.export_popup.as_ref() {
            if popup
                .close_at
                .is_some_and(|deadline| Instant::now() >= deadline)
            {
                app.export_popup = None;
            }
        }

        // poll streaming results if a search is in progress
        if let Some(ref mut rx) = app.stream_rx {
            // drain all available messages without blocking
            loop {
                match rx.try_recv() {
                    Ok(lookup::StreamMsg::Result { result, sort_index }) => {
                        // insert in sorted position to maintain list order
                        let pos = app.results.partition_point(|(idx, _)| *idx < sort_index);
                        app.results.insert(pos, (sort_index, result));
                        // auto-select first result
                        if app.results.len() == 1 {
                            app.results_state.select(Some(0));
                        }
                    }
                    Ok(lookup::StreamMsg::Progress { current, total }) => {
                        app.search_progress = (current, total);
                    }
                    Ok(lookup::StreamMsg::Error(msg)) => {
                        app.status_msg = Some(format!("Error: {}", msg));
                    }
                    Ok(lookup::StreamMsg::Done) => {
                        app.searching = false;
                        app.last_search_duration = app.search_started_at.map(|t| t.elapsed());
                        app.search_started_at = None;
                        app.status_msg = None;
                        app.stream_rx = None;
                        app.stream_task = None;
                        break;
                    }
                    Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
                        app.searching = false;
                        app.last_search_duration = app.search_started_at.map(|t| t.elapsed());
                        app.search_started_at = None;
                        app.status_msg = None;
                        app.stream_rx = None;
                        app.stream_task = None;
                        break;
                    }
                }
            }
        }

        // poll favorites check results
        if let Some(ref mut rx) = app.fav_check_rx {
            loop {
                match rx.try_recv() {
                    Ok(lookup::StreamMsg::Result { result, .. }) => {
                        // Update the matching favorite's status
                        let domain_lower = result.full.to_lowercase();
                        if let Some(fav) =
                            app.favorites.iter_mut().find(|f| f.domain == domain_lower)
                        {
                            let new_status = result.status_str().to_string();
                            if fav.status != new_status && fav.status != "unknown" {
                                fav.changed = true;
                            }
                            fav.status = new_status;
                            fav.checked = chrono::Utc::now().to_rfc3339();
                        }
                    }
                    Ok(lookup::StreamMsg::Done) => {
                        app.checking_favorites = false;
                        app.fav_check_rx = None;
                        app.fav_check_task = None;
                        app.save_config();
                        break;
                    }
                    Ok(_) => {} // progress/error - dont care for fav checks
                    Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break,
                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
                        app.checking_favorites = false;
                        app.fav_check_rx = None;
                        app.fav_check_task = None;
                        app.save_config();
                        break;
                    }
                }
            }
        }

        terminal.draw(|f| draw_ui(f, app))?;

        // poll for events with a timeout so we can update during searches
        if event::poll(std::time::Duration::from_millis(50))? {
            match event::read()? {
                Event::Key(key) if key.kind == KeyEventKind::Press => {
                    if matches!(key.code, KeyCode::F(1)) {
                        app.show_help = !app.show_help;
                        continue;
                    }

                    if matches!(key.code, KeyCode::F(2)) {
                        if app.export_popup.is_some() {
                            app.export_popup = None;
                        } else {
                            app.open_export_popup();
                        }
                        continue;
                    }

                    if key.modifiers.contains(KeyModifiers::CONTROL)
                        && matches!(key.code, KeyCode::Char('c'))
                    {
                        quit_app(app);
                        continue;
                    }

                    if app.export_popup.is_some() {
                        handle_export_popup_key(app, key.code);
                        continue;
                    }

                    if app.searching && matches!(key.code, KeyCode::Char('s' | 'S')) {
                        cancel_search(app);
                        continue;
                    }

                    if !app.searching
                        && !app.clear_on_search
                        && matches!(key.code, KeyCode::Char('C'))
                    {
                        app.clear_results();
                        continue;
                    }

                    // close dropdown on any key if its open (unless its the dropdown interaction)
                    if app.dropdown != DropdownState::Closed && app.focus != Focus::Settings {
                        app.dropdown = DropdownState::Closed;
                    }

                    match key.code {
                        KeyCode::Esc => {
                            if app.show_help {
                                app.show_help = false;
                                continue;
                            }
                            handle_escape_for_panel(app);
                            continue;
                        }
                        // tab between panels
                        KeyCode::Tab => {
                            app.dropdown = DropdownState::Closed;
                            app.set_focus(match app.focus {
                                Focus::Search => {
                                    if app.show_notes_panel {
                                        Focus::Scratchpad
                                    } else {
                                        Focus::Results
                                    }
                                }
                                Focus::Scratchpad => Focus::Results,
                                Focus::Results => Focus::Favorites,
                                Focus::Favorites => Focus::Settings,
                                Focus::Settings => Focus::Search,
                            });
                        }
                        KeyCode::BackTab => {
                            app.dropdown = DropdownState::Closed;
                            app.set_focus(match app.focus {
                                Focus::Search => Focus::Settings,
                                Focus::Scratchpad => Focus::Search,
                                Focus::Results => {
                                    if app.show_notes_panel {
                                        Focus::Scratchpad
                                    } else {
                                        Focus::Search
                                    }
                                }
                                Focus::Favorites => Focus::Results,
                                Focus::Settings => Focus::Favorites,
                            });
                        }
                        _ => {
                            handle_key_for_panel(app, key.code).await;
                        }
                    }
                }
                Event::Mouse(mouse) => {
                    if app.mouse_enabled {
                        handle_mouse(app, mouse);
                    }
                }
                _ => {}
            }
        }
    }
}

async fn handle_key_for_panel(app: &mut App, key: KeyCode) {
    match app.focus {
        Focus::Search => handle_search_key(app, key).await,
        Focus::Scratchpad => handle_scratchpad_key(app, key),
        Focus::Results => handle_results_key(app, key),
        Focus::Favorites => handle_favorites_key(app, key),
        Focus::Settings => handle_settings_key(app, key),
    }
}

fn run_export(app: &mut App) {
    let Some(popup) = app.export_popup.as_ref() else {
        return;
    };

    let mode = popup.mode;
    let path_text = popup.path.clone();
    let path = PathBuf::from(&path_text);

    if path.is_file() && !popup.confirm_overwrite {
        if let Some(popup) = app.export_popup.as_mut() {
            popup.status = Some("File exists. Press Enter again to overwrite.".to_string());
            popup.status_success = false;
            popup.confirm_overwrite = true;
            popup.close_at = None;
        }
        return;
    }

    let result = match mode {
        ExportMode::FavoritesTxt => export_favorites_txt(&path, &app.favorites),
        ExportMode::ResultsCsv => {
            let visible = app.visible_results();
            if visible.is_empty() {
                Err("No results to export".to_string())
            } else {
                export_results_csv(&path, &visible)
            }
        }
    };

    match result {
        Ok(()) => {
            match mode {
                ExportMode::FavoritesTxt => app.last_fav_export_path = path_text.clone(),
                ExportMode::ResultsCsv => app.last_res_export_path = path_text.clone(),
            }
            if let Some(popup) = app.export_popup.as_mut() {
                popup.status = Some("Success".to_string());
                popup.status_success = true;
                popup.confirm_overwrite = false;
                popup.close_at = Some(Instant::now() + Duration::from_secs(2));
            }
            app.save_config();
        }
        Err(err) => {
            if let Some(popup) = app.export_popup.as_mut() {
                popup.status = Some(err);
                popup.status_success = false;
                popup.confirm_overwrite = false;
                popup.close_at = None;
            }
        }
    }
}

fn handle_export_popup_key(app: &mut App, key: KeyCode) {
    let Some(popup) = app.export_popup.as_mut() else {
        return;
    };

    match key {
        KeyCode::Esc => {
            app.export_popup = None;
        }
        KeyCode::Tab | KeyCode::Down => {
            popup.selected_row = (popup.selected_row + 1) % 4;
        }
        KeyCode::BackTab | KeyCode::Up => {
            popup.selected_row = if popup.selected_row == 0 {
                3
            } else {
                popup.selected_row - 1
            };
        }
        KeyCode::Left => {
            if popup.selected_row == 0 {
                let mode = popup.mode.toggled();
                app.set_export_mode(mode);
            } else if popup.selected_row == 3 {
                popup.selected_row = 2;
            } else if popup.selected_row == 1 && popup.cursor_pos > 0 {
                popup.cursor_pos -= 1;
            }
        }
        KeyCode::Right => {
            if popup.selected_row == 0 {
                let mode = popup.mode.toggled();
                app.set_export_mode(mode);
            } else if popup.selected_row == 2 {
                popup.selected_row = 3;
            } else if popup.selected_row == 1 && popup.cursor_pos < popup.path.len() {
                popup.cursor_pos += 1;
            }
        }
        KeyCode::Home => {
            if popup.selected_row == 1 {
                popup.cursor_pos = 0;
            }
        }
        KeyCode::End => {
            if popup.selected_row == 1 {
                popup.cursor_pos = popup.path.len();
            }
        }
        KeyCode::Backspace => {
            if popup.selected_row == 1 && popup.cursor_pos > 0 {
                popup.cursor_pos -= 1;
                popup.path.remove(popup.cursor_pos);
                popup.status = None;
                popup.status_success = false;
                popup.confirm_overwrite = false;
                popup.close_at = None;
            }
        }
        KeyCode::Delete => {
            if popup.selected_row == 1 && popup.cursor_pos < popup.path.len() {
                popup.path.remove(popup.cursor_pos);
                popup.status = None;
                popup.status_success = false;
                popup.confirm_overwrite = false;
                popup.close_at = None;
            }
        }
        KeyCode::Char(c) => {
            if popup.selected_row == 1 {
                popup.path.insert(popup.cursor_pos, c);
                popup.cursor_pos += 1;
                popup.status = None;
                popup.status_success = false;
                popup.confirm_overwrite = false;
                popup.close_at = None;
            }
        }
        KeyCode::Enter => match popup.selected_row {
            0 => {
                let mode = popup.mode.toggled();
                app.set_export_mode(mode);
            }
            1 => run_export(app),
            2 => app.export_popup = None,
            3 => run_export(app),
            _ => {}
        },
        _ => {}
    }
}

fn handle_scratchpad_key(app: &mut App, key: KeyCode) {
    match key {
        KeyCode::Char(c) => {
            app.scratchpad.insert(app.scratchpad_cursor, c);
            app.scratchpad_cursor += 1;
            app.save_config();
        }
        KeyCode::Enter => {
            app.scratchpad.insert(app.scratchpad_cursor, '\n');
            app.scratchpad_cursor += 1;
            app.save_config();
        }
        KeyCode::Backspace => {
            if app.scratchpad_cursor > 0 {
                app.scratchpad_cursor -= 1;
                app.scratchpad.remove(app.scratchpad_cursor);
                app.save_config();
            }
        }
        KeyCode::Delete => {
            if app.scratchpad_cursor < app.scratchpad.len() {
                app.scratchpad.remove(app.scratchpad_cursor);
                app.save_config();
            }
        }
        KeyCode::Left => {
            if app.scratchpad_cursor > 0 {
                app.scratchpad_cursor -= 1;
            }
        }
        KeyCode::Right => {
            if app.scratchpad_cursor < app.scratchpad.len() {
                app.scratchpad_cursor += 1;
            }
        }
        KeyCode::Up => {
            move_scratchpad_cursor_vertical(app, -1);
        }
        KeyCode::Down => {
            move_scratchpad_cursor_vertical(app, 1);
        }
        KeyCode::Home => {
            app.scratchpad_cursor = 0;
        }
        KeyCode::End => {
            app.scratchpad_cursor = app.scratchpad.len();
        }
        _ => {}
    }
}

async fn handle_search_key(app: &mut App, key: KeyCode) {
    if app.searching {
        return;
    }
    match key {
        KeyCode::Enter => {
            if !app.search_input.is_empty() && !app.searching {
                start_search(app);
            }
        }
        KeyCode::Char(c) => {
            // only allow valid domain chars (alphanumeric, hyphen, dot, space for multi query)
            if c.is_ascii_alphanumeric() || c == '-' || c == '.' || c == ' ' {
                app.search_input.insert(app.cursor_pos, c);
                app.cursor_pos += c.len_utf8();
            }
        }
        KeyCode::Backspace => {
            if app.cursor_pos > 0 {
                // Find the previous char boundary
                let prev = app.search_input[..app.cursor_pos]
                    .char_indices()
                    .next_back()
                    .map(|(i, _)| i)
                    .unwrap_or(0);
                app.search_input.remove(prev);
                app.cursor_pos = prev;
            }
        }
        KeyCode::Delete => {
            if app.cursor_pos < app.search_input.len()
                && app.search_input.is_char_boundary(app.cursor_pos)
            {
                app.search_input.remove(app.cursor_pos);
            }
        }
        KeyCode::Left => {
            if app.cursor_pos > 0 {
                app.cursor_pos = app.search_input[..app.cursor_pos]
                    .char_indices()
                    .next_back()
                    .map(|(i, _)| i)
                    .unwrap_or(0);
            }
        }
        KeyCode::Right => {
            if app.cursor_pos < app.search_input.len() {
                app.cursor_pos = app.search_input[app.cursor_pos..]
                    .char_indices()
                    .nth(1)
                    .map(|(i, _)| app.cursor_pos + i)
                    .unwrap_or(app.search_input.len());
            }
        }
        KeyCode::Home => {
            app.cursor_pos = 0;
        }
        KeyCode::End => {
            app.cursor_pos = app.search_input.len();
        }
        _ => {}
    }
}

fn handle_results_key(app: &mut App, key: KeyCode) {
    let visible = app.visible_results();
    let len = visible.len();
    if len == 0 {
        return;
    }

    match key {
        KeyCode::Up => {
            let i = match app.results_state.selected() {
                Some(i) => {
                    if i > 0 {
                        i - 1
                    } else {
                        0
                    }
                }
                None => 0,
            };
            app.results_state.select(Some(i));
        }
        KeyCode::Down => {
            let i = match app.results_state.selected() {
                Some(i) => {
                    if i + 1 < len {
                        i + 1
                    } else {
                        i
                    }
                }
                None => 0,
            };
            app.results_state.select(Some(i));
        }
        KeyCode::Enter => {
            // add focused domain to favorites
            if let Some(idx) = app.results_state.selected() {
                let visible = app.visible_results();
                if let Some(result) = visible.get(idx) {
                    let domain = result.full.clone();
                    app.add_favorite(&domain);
                }
            }
        }
        _ => {}
    }
}

fn handle_favorites_key(app: &mut App, key: KeyCode) {
    let len = app.favorites.len();
    if len == 0 {
        return;
    }

    match key {
        KeyCode::Up => {
            let i = match app.favorites_state.selected() {
                Some(i) => {
                    if i > 0 {
                        i - 1
                    } else {
                        0
                    }
                }
                None => 0,
            };
            app.favorites_state.select(Some(i));
        }
        KeyCode::Down => {
            let i = match app.favorites_state.selected() {
                Some(i) => {
                    if i + 1 < len {
                        i + 1
                    } else {
                        i
                    }
                }
                None => 0,
            };
            app.favorites_state.select(Some(i));
        }
        KeyCode::Enter => {
            // acknowledge status change - clear the ! marker
            if let Some(idx) = app.favorites_state.selected() {
                if let Some(fav) = app.favorites.get_mut(idx) {
                    if fav.changed {
                        fav.changed = false;
                        app.save_config();
                    }
                }
            }
        }
        KeyCode::Backspace | KeyCode::Delete => {
            app.remove_focused_favorite();
        }
        KeyCode::Char('c') | KeyCode::Char('C') => {
            start_fav_check(app);
        }
        _ => {}
    }
}

fn handle_settings_key(app: &mut App, key: KeyCode) {
    match &app.dropdown {
        DropdownState::Open(current) => {
            let current = *current;
            let option_count = app.list_options().len();
            match key {
                KeyCode::Up => {
                    let new = if option_count == 0 {
                        0
                    } else if current > 0 {
                        current - 1
                    } else {
                        option_count - 1
                    };
                    app.dropdown = DropdownState::Open(new);
                }
                KeyCode::Down => {
                    let new = if option_count == 0 {
                        0
                    } else if current + 1 < option_count {
                        current + 1
                    } else {
                        0
                    };
                    app.dropdown = DropdownState::Open(new);
                }
                KeyCode::Enter => {
                    app.set_tld_list_by_index(current);
                    app.dropdown = DropdownState::Closed;
                    app.settings_selected = Some(0);
                }
                KeyCode::Esc => {
                    app.dropdown = DropdownState::Closed;
                }
                _ => {}
            }
        }
        DropdownState::Closed => {
            match key {
                KeyCode::Up => {
                    app.settings_selected = Some(match app.settings_selected.unwrap_or(0) {
                        0 => 4,
                        n => n - 1,
                    });
                }
                KeyCode::Down => {
                    app.settings_selected = Some(match app.settings_selected.unwrap_or(0) {
                        0 => 1,
                        1 => 2,
                        2 => 3,
                        3 => 4,
                        _ => 0,
                    });
                }
                KeyCode::Enter => {
                    match app.settings_selected.unwrap_or(0) {
                        0 => {
                            app.dropdown = DropdownState::Open(app.tld_list_index());
                        }
                        1 => {
                            app.show_unavailable = !app.show_unavailable;
                            app.save_config();
                        }
                        2 => {
                            app.show_notes_panel = !app.show_notes_panel;
                            if !app.show_notes_panel && app.focus == Focus::Scratchpad {
                                app.set_focus(Focus::Results);
                            }
                            app.save_config();
                        }
                        3 => {
                            app.clear_on_search = !app.clear_on_search;
                            app.save_config();
                        }
                        4 => {
                            // increment jobs (wrap at 99 -> 1)
                            app.jobs = if app.jobs >= 99 { 1 } else { app.jobs + 1 };
                            app.save_config();
                        }
                        _ => {}
                    }
                }
                KeyCode::Char(' ') => match app.settings_selected.unwrap_or(0) {
                    1 => {
                        app.show_unavailable = !app.show_unavailable;
                        app.save_config();
                    }
                    2 => {
                        app.show_notes_panel = !app.show_notes_panel;
                        if !app.show_notes_panel && app.focus == Focus::Scratchpad {
                            app.set_focus(Focus::Results);
                        }
                        app.save_config();
                    }
                    3 => {
                        app.clear_on_search = !app.clear_on_search;
                        app.save_config();
                    }
                    _ => {}
                },
                KeyCode::Char('+') | KeyCode::Char('=') => {
                    if app.settings_selected == Some(4) {
                        app.jobs = if app.jobs >= 99 { 99 } else { app.jobs + 1 };
                        app.save_config();
                    }
                }
                KeyCode::Char('-') => {
                    if app.settings_selected == Some(4) {
                        app.jobs = if app.jobs <= 1 { 1 } else { app.jobs - 1 };
                        app.save_config();
                    }
                }
                KeyCode::Left => {
                    if app.settings_selected == Some(4) {
                        app.jobs = if app.jobs <= 1 { 1 } else { app.jobs - 1 };
                        app.save_config();
                    }
                }
                KeyCode::Right => {
                    if app.settings_selected == Some(4) {
                        app.jobs = if app.jobs >= 99 { 99 } else { app.jobs + 1 };
                        app.save_config();
                    }
                }
                _ => {}
            }
        }
    }
}

fn handle_mouse(app: &mut App, mouse: MouseEvent) {
    match mouse.kind {
        MouseEventKind::Down(MouseButton::Left) => {
            let col = mouse.column;
            let row = mouse.row;

            if let Some(help_popup) = app.panel_rects.help_popup {
                if !contains_pos(help_popup, (col, row)) {
                    app.show_help = false;
                }
                return;
            }

            if let Some(export_popup) = app.panel_rects.export_popup {
                if !contains_pos(export_popup, (col, row)) {
                    app.export_popup = None;
                    return;
                }

                if let Some(mode_rect) = app.panel_rects.export_mode_favorites {
                    if contains_pos(mode_rect, (col, row)) {
                        app.set_export_mode(ExportMode::FavoritesTxt);
                        if let Some(popup) = app.export_popup.as_mut() {
                            popup.selected_row = 0;
                        }
                        return;
                    }
                }

                if let Some(mode_rect) = app.panel_rects.export_mode_results {
                    if contains_pos(mode_rect, (col, row)) {
                        app.set_export_mode(ExportMode::ResultsCsv);
                        if let Some(popup) = app.export_popup.as_mut() {
                            popup.selected_row = 0;
                        }
                        return;
                    }
                }

                if let Some(path_rect) = app.panel_rects.export_path {
                    if contains_pos(path_rect, (col, row)) {
                        if let Some(popup) = app.export_popup.as_mut() {
                            popup.selected_row = 1;
                            let clicked = col.saturating_sub(path_rect.x + 1) as usize;
                            popup.cursor_pos = clicked.min(popup.path.len());
                        }
                        return;
                    }
                }

                if let Some(cancel_rect) = app.panel_rects.export_cancel {
                    if contains_pos(cancel_rect, (col, row)) {
                        app.export_popup = None;
                        return;
                    }
                }

                if let Some(save_rect) = app.panel_rects.export_save {
                    if contains_pos(save_rect, (col, row)) {
                        if let Some(popup) = app.export_popup.as_mut() {
                            popup.selected_row = 3;
                        }
                        run_export(app);
                        return;
                    }
                }

                return;
            }

            if let Some(dropdown_rect) = app.panel_rects.dropdown {
                if contains_pos(dropdown_rect, (col, row)) {
                    let item_row = row.saturating_sub(dropdown_rect.y + 1) as usize;
                    let options = app.list_options();
                    if item_row < options.len() {
                        app.set_tld_list_by_index(item_row);
                    }
                    app.dropdown = DropdownState::Closed;
                    return;
                }

                app.dropdown = DropdownState::Closed;
                app.panel_rects.dropdown = None;
            }

            if let Some(topbar) = app.panel_rects.topbar {
                if contains_pos(topbar, (col, row)) {
                    let close_end = topbar.x + 2;
                    if col <= close_end {
                        return;
                    }
                    if let Some(export_button) = app.panel_rects.export_button {
                        if contains_pos(export_button, (col, row)) {
                            app.open_export_popup();
                            return;
                        }
                    }
                    if let Some(help_button) = app.panel_rects.help_button {
                        if contains_pos(help_button, (col, row)) {
                            app.show_help = !app.show_help;
                            return;
                        }
                    }
                    return;
                }
            }

            if let Some(search_rect) = app.panel_rects.search {
                if contains_pos(search_rect, (col, row)) {
                    app.set_focus(Focus::Search);

                    if let Some(search_button) = app.panel_rects.search_button {
                        if contains_pos(search_button, (col, row)) {
                            if !app.searching && !app.search_input.is_empty() {
                                start_search(app);
                            }
                            return;
                        }
                    }

                    if let Some(cancel_button) = app.panel_rects.cancel_button {
                        if contains_pos(cancel_button, (col, row)) {
                            if app.searching {
                                cancel_search(app);
                            }
                            return;
                        }
                    }

                    if let Some(clear_button) = app.panel_rects.clear_button {
                        if contains_pos(clear_button, (col, row)) {
                            app.clear_results();
                            return;
                        }
                    }

                    return;
                }
            }

            if let Some(scratchpad_rect) = app.panel_rects.scratchpad {
                if contains_pos(scratchpad_rect, (col, row)) {
                    app.set_focus(Focus::Scratchpad);
                    app.scratchpad_cursor = app.scratchpad.len();
                    return;
                }
            }

            // check if clicking inside settings panel for interactive elements
            if let Some(settings_rect) = app.panel_rects.settings {
                if contains_pos(settings_rect, (col, row)) {
                    app.set_focus(Focus::Settings);

                    // row offsets within settings panel (1 = border)
                    let local_row = row.saturating_sub(settings_rect.y + 1);

                    if app.dropdown == DropdownState::Closed {
                        // row 0 = TLD list line, row 1 = checkbox line
                        if local_row == 0 {
                            app.settings_selected = Some(0);
                            // open TLD dropdown
                            app.dropdown = DropdownState::Open(app.tld_list_index());
                        } else if local_row == 1 {
                            app.settings_selected = Some(1);
                            // toggle show unavailable checkbox
                            app.show_unavailable = !app.show_unavailable;
                            app.save_config();
                        } else if local_row == 2 {
                            app.settings_selected = Some(2);
                            app.show_notes_panel = !app.show_notes_panel;
                            if !app.show_notes_panel && app.focus == Focus::Scratchpad {
                                app.set_focus(Focus::Results);
                            }
                            app.save_config();
                        } else if local_row == 3 {
                            app.settings_selected = Some(3);
                            app.clear_on_search = !app.clear_on_search;
                            app.save_config();
                        } else if local_row == 4 {
                            app.settings_selected = Some(4);
                            // clicking on jobs row increments (use keyboard -/+ for fine control)
                            app.jobs = if app.jobs >= 99 { 1 } else { app.jobs + 1 };
                            app.save_config();
                        }
                    }
                    return;
                }
            }

            // check if clicking inside results panel if so select that row
            if let Some(results_rect) = app.panel_rects.results {
                if contains_pos(results_rect, (col, row)) {
                    app.set_focus(Focus::Results);
                    let visible_len = app.visible_results().len();
                    let content_start = results_rect.y + 1;
                    let progress_offset = if app.searching && app.search_progress.1 > 0 {
                        1
                    } else {
                        0
                    };
                    let header_offset = if visible_len > 0 { 1 } else { 0 };
                    let list_start = content_start + progress_offset + header_offset;

                    if row < list_start {
                        return;
                    }

                    let clicked_idx = row.saturating_sub(list_start) as usize;
                    if clicked_idx < visible_len {
                        app.results_state.select(Some(clicked_idx));
                    }
                    return;
                }
            }

            // check if clicking the fav check button
            if let Some(btn_rect) = app.panel_rects.fav_check_button {
                if contains_pos(btn_rect, (col, row)) {
                    start_fav_check(app);
                    return;
                }
            }

            // check if clicking inside favorites panel - select that row
            if let Some(fav_rect) = app.panel_rects.favorites {
                if contains_pos(fav_rect, (col, row)) {
                    app.set_focus(Focus::Favorites);
                    let content_start = fav_rect.y + 1;
                    let clicked_idx = (row.saturating_sub(content_start)) as usize;
                    if clicked_idx < app.favorites.len() {
                        app.favorites_state.select(Some(clicked_idx));
                    }
                    return;
                }
            }

            // default: just switch focus to clicked panel
            if let Some(panel) = app.panel_at(col, row) {
                app.set_focus(panel);
            }
        }
        MouseEventKind::Up(MouseButton::Left) => {
            let col = mouse.column;
            let row = mouse.row;

            if let Some(topbar) = app.panel_rects.topbar {
                if contains_pos(topbar, (col, row)) {
                    let close_end = topbar.x + 2;
                    if col <= close_end {
                        quit_app(app);
                        return;
                    }
                }
            }
        }
        MouseEventKind::ScrollUp => {
            // scroll up in focused list
            match app.focus {
                Focus::Results => {
                    if let Some(i) = app.results_state.selected() {
                        if i > 0 {
                            app.results_state.select(Some(i - 1));
                        }
                    }
                }
                Focus::Favorites => {
                    if let Some(i) = app.favorites_state.selected() {
                        if i > 0 {
                            app.favorites_state.select(Some(i - 1));
                        }
                    }
                }
                _ => {}
            }
        }
        MouseEventKind::ScrollDown => {
            // scroll down in focused list
            match app.focus {
                Focus::Results => {
                    let len = app.visible_results().len();
                    if let Some(i) = app.results_state.selected() {
                        if i + 1 < len {
                            app.results_state.select(Some(i + 1));
                        }
                    } else if len > 0 {
                        app.results_state.select(Some(0));
                    }
                }
                Focus::Favorites => {
                    let len = app.favorites.len();
                    if let Some(i) = app.favorites_state.selected() {
                        if i + 1 < len {
                            app.favorites_state.select(Some(i + 1));
                        }
                    } else if len > 0 {
                        app.favorites_state.select(Some(0));
                    }
                }
                _ => {}
            }
        }
        _ => {}
    }
}

fn cancel_search(app: &mut App) {
    if let Some(handle) = app.stream_task.take() {
        handle.abort();
    }
    app.stream_rx = None;
    app.searching = false;
    app.search_progress = (0, 0);
    app.search_started_at = None;
    app.last_search_duration = None;
    app.status_msg = Some("Search canceled".to_string());
}

fn quit_app(app: &mut App) {
    if let Some(handle) = app.stream_task.take() {
        handle.abort();
    }
    if let Some(handle) = app.fav_check_task.take() {
        handle.abort();
    }
    app.stream_rx = None;
    app.fav_check_rx = None;

    // backup on shutdown if enabled
    if app.can_save && app.backups_enabled {
        let _ = Config::create_backup(&app.config_path, app.backup_count);
    }

    app.should_quit = true;
}

/// kick off checking all favorites availability in the bg
fn start_fav_check(app: &mut App) {
    if app.checking_favorites || app.favorites.is_empty() {
        return;
    }

    // Cancel any previous fav check
    if let Some(handle) = app.fav_check_task.take() {
        handle.abort();
    }
    app.fav_check_rx = None;

    app.checking_favorites = true;

    // Build a batch: each favorite is "name.tld" -> lookup (name, [tld])
    let batches: lookup::LookupBatch = app
        .favorites
        .iter()
        .filter_map(|fav| {
            let parts: Vec<&str> = fav.domain.splitn(2, '.').collect();
            if parts.len() == 2 {
                Some((parts[0].to_string(), vec![parts[1].to_string()]))
            } else {
                None
            }
        })
        .collect();

    if batches.is_empty() {
        app.checking_favorites = false;
        return;
    }

    let stream = lookup::lookup_many_streaming(
        batches,
        app.delay,
        app.retries,
        app.verbose,
        app.cache_path.clone(),
        false, // dont force refresh
        app.jobs,
        app.patch.clone(),
        app.noretry.clone(),
    );

    app.fav_check_task = Some(stream.handle);
    app.fav_check_rx = Some(stream.receiver);
}

fn handle_escape_for_panel(app: &mut App) {
    if app.dropdown != DropdownState::Closed {
        app.dropdown = DropdownState::Closed;
        return;
    }

    match app.focus {
        Focus::Scratchpad => {}
        Focus::Results => {
            app.results_state.select(None);
        }
        Focus::Favorites => {
            app.favorites_state.select(None);
        }
        Focus::Settings => {
            app.settings_selected = None;
        }
        Focus::Search => {}
    }
}

fn start_search(app: &mut App) {
    if app.searching {
        cancel_search(app);
    }

    let search_terms = app.parsed_queries();
    if search_terms.is_empty() {
        return;
    }

    app.searching = true;
    if app.clear_on_search {
        app.results.clear();
        app.results_state.select(None);
    }
    app.search_progress = (0, 0);
    app.search_started_at = Some(Instant::now());
    app.last_search_duration = None;
    app.status_msg = Some("Searching...".to_string());

    let default_tlds = app.get_effective_tlds();
    if default_tlds.is_empty() {
        app.status_msg = Some("No TLDs to search".to_string());
        app.searching = false;
        return;
    }

    let search_batches: lookup::LookupBatch = search_terms
        .into_iter()
        .map(|term| {
            if term.contains('.') {
                let mut parts = term.splitn(2, '.');
                let name = parts.next().unwrap_or_default().to_string();
                let tld = parts.next().unwrap_or_default().to_string();
                (name, vec![tld])
            } else {
                (
                    term,
                    default_tlds.iter().map(|tld| (*tld).to_string()).collect(),
                )
            }
        })
        .filter(|(name, tlds)| !name.is_empty() && !tlds.is_empty())
        .collect();

    if search_batches.is_empty() {
        app.status_msg = Some("No valid search terms".to_string());
        app.searching = false;
        return;
    }

    let stream = lookup::lookup_many_streaming(
        search_batches,
        app.delay,
        app.retries,
        app.verbose,
        app.cache_path.clone(),
        app.force_refresh,
        app.jobs,
        app.patch.clone(),
        app.noretry.clone(),
    );

    // only force refresh on first search
    app.force_refresh = false;
    app.stream_task = Some(stream.handle);
    app.stream_rx = Some(stream.receiver);
}


fn draw_ui(f: &mut Frame, app: &mut App) {
    let size = f.area();

    if terminal_too_small(size) {
        app.panel_rects = PanelRects::default();
        draw_terminal_too_small(f, app, size);
        return;
    }

    // main layout: top bar + content area + search bar at bottom
    let main_chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(TOPBAR_HEIGHT),
            Constraint::Min(CONTENT_MIN_HEIGHT),
            Constraint::Length(SEARCH_PANEL_HEIGHT),
        ])
        .split(size);

    let content_area = main_chunks[1];
    let desired_sidebar = content_area
        .width
        .saturating_mul(SIDEBAR_TARGET_WIDTH_PERCENT)
        / 100;
    let mut sidebar_width = clamp_panel_size(desired_sidebar, SIDEBAR_MIN_WIDTH, SIDEBAR_MAX_WIDTH)
        .min(content_area.width.saturating_sub(RESULTS_MIN_WIDTH));
    if sidebar_width == 0 {
        sidebar_width = SIDEBAR_MIN_WIDTH.min(content_area.width);
    }

    let sidebar_chunk = Rect {
        x: content_area.x + content_area.width.saturating_sub(sidebar_width),
        y: content_area.y,
        width: sidebar_width,
        height: content_area.height,
    };

    let (scratchpad_chunk, results_chunk) = if app.show_notes_panel {
        let center_width = content_area.width.saturating_sub(sidebar_width);
        let desired_scratchpad = content_area
            .width
            .saturating_mul(SCRATCHPAD_TARGET_WIDTH_PERCENT)
            / 100;
        let mut scratchpad_width = clamp_panel_size(
            desired_scratchpad,
            SCRATCHPAD_MIN_WIDTH,
            SCRATCHPAD_MAX_WIDTH,
        )
        .min(center_width.saturating_sub(RESULTS_MIN_WIDTH));
        if scratchpad_width == 0 {
            scratchpad_width = SCRATCHPAD_MIN_WIDTH.min(center_width);
        }

        (
            Some(Rect {
                x: content_area.x,
                y: content_area.y,
                width: scratchpad_width,
                height: content_area.height,
            }),
            Rect {
                x: content_area.x + scratchpad_width,
                y: content_area.y,
                width: center_width.saturating_sub(scratchpad_width),
                height: content_area.height,
            },
        )
    } else {
        (
            None,
            Rect {
                x: content_area.x,
                y: content_area.y,
                width: content_area.width.saturating_sub(sidebar_width),
                height: content_area.height,
            },
        )
    };

    let settings_height = clamp_panel_size(
        SETTINGS_PANEL_HEIGHT,
        SETTINGS_PANEL_MIN_HEIGHT,
        SETTINGS_PANEL_MAX_HEIGHT,
    )
    .min(sidebar_chunk.height.saturating_sub(FAVORITES_MIN_HEIGHT));
    let settings_chunk = Rect {
        x: sidebar_chunk.x,
        y: sidebar_chunk.y + sidebar_chunk.height.saturating_sub(settings_height),
        width: sidebar_chunk.width,
        height: settings_height,
    };
    let favorites_chunk = Rect {
        x: sidebar_chunk.x,
        y: sidebar_chunk.y,
        width: sidebar_chunk.width,
        height: sidebar_chunk.height.saturating_sub(settings_height),
    };

    // store rects for mouse detection
    app.panel_rects.topbar = Some(main_chunks[0]);
    let help_width = HELP_BUTTON_LABEL.chars().count() as u16;
    let export_width = EXPORT_BUTTON_LABEL.chars().count() as u16;
    let help_x = main_chunks[0].x + main_chunks[0].width.saturating_sub(help_width);
    let export_x = help_x.saturating_sub(export_width + 1);
    app.panel_rects.export_button = Some(Rect {
        x: export_x,
        y: main_chunks[0].y,
        width: export_width,
        height: 1,
    });
    app.panel_rects.help_button = Some(Rect {
        x: help_x,
        y: main_chunks[0].y,
        width: help_width,
        height: 1,
    });
    app.panel_rects.dropdown = None;
    app.panel_rects.help_popup = None;
    app.panel_rects.search_button = None;
    app.panel_rects.cancel_button = None;
    app.panel_rects.clear_button = None;
    app.panel_rects.export_popup = None;
    app.panel_rects.export_mode_favorites = None;
    app.panel_rects.export_mode_results = None;
    app.panel_rects.export_path = None;
    app.panel_rects.export_cancel = None;
    app.panel_rects.export_save = None;
    app.panel_rects.scratchpad = scratchpad_chunk;
    app.panel_rects.results = Some(results_chunk);
    app.panel_rects.favorites = Some(favorites_chunk);
    app.panel_rects.settings = Some(settings_chunk);
    app.panel_rects.search = Some(main_chunks[2]);

    // draw each panel
    draw_topbar(f, app, main_chunks[0]);
    if let Some(scratchpad_rect) = scratchpad_chunk {
        draw_scratchpad(f, app, scratchpad_rect);
    }
    draw_results(f, app, results_chunk);
    draw_favorites(f, app, favorites_chunk);
    draw_settings(f, app, settings_chunk);
    draw_search(f, app, main_chunks[2]);

    // draw dropdown overlay if open
    if let DropdownState::Open(selected) = &app.dropdown {
        draw_dropdown(f, app, settings_chunk, *selected);
    }

    if app.show_help {
        draw_help_overlay(f, app, size);
    }

    if app.export_popup.is_some() {
        draw_export_popup(f, app, size);
    }
}

fn terminal_too_small(area: Rect) -> bool {
    area.width < MIN_UI_WIDTH || area.height < MIN_UI_HEIGHT
}

fn draw_terminal_too_small(f: &mut Frame, app: &App, area: Rect) {
    let block = app.block()
        .border_style(app.fg(Color::Red))
        .title(" hoardom ");

    let inner = block.inner(area);
    f.render_widget(block, area);
    let content_width = inner.width as usize;

    let text = vec![
        Line::from(Span::styled(
            fit_cell_center("HELP ! HELP ! HELP !", content_width),
            app.fg_bold(Color::White),
        )),
        Line::from(Span::styled(
            fit_cell_center("I AM BEING CRUSHED!", content_width),
            app.fg_bold(Color::White),
        )),
        Line::from(fit_cell_center("", content_width)),
        Line::from(Span::styled(
            fit_cell_center("Im claustrophobic! :'(", content_width),
            app.fg(Color::White),
        )),
        Line::from(Span::styled(
            fit_cell_center(
                &format!("Need {}x{} of space", MIN_UI_WIDTH, MIN_UI_HEIGHT),
                content_width,
            ),
            app.fg(Color::White),
        )),
        Line::from(Span::styled(
            fit_cell_center(
                &format!("Current: {}x{}", area.width, area.height),
                content_width,
            ),
            app.fg(Color::DarkGray),
        )),
        Line::from(fit_cell_center("", content_width)),
        Line::from(Span::styled(
            fit_cell_center("REFUSING TO WORK TILL YOU", content_width),
            app.fg_bold(Color::White),
        )),
        Line::from(Span::styled(
            fit_cell_center("GIVE ME BACK MY SPACE! >:(", content_width),
            app.fg_bold(Color::White),
        )),
    ];

    f.render_widget(Paragraph::new(text), inner);
}

fn draw_topbar(f: &mut Frame, app: &App, area: Rect) {
    let title = format!("{} - {}", APP_NAME, APP_DESC);
    let width = area.width as usize;
    let left = format!("{} {}", CLOSE_BUTTON_LABEL, title);
    let right = format!("{} {}", EXPORT_BUTTON_LABEL, HELP_BUTTON_LABEL);
    let gap = width.saturating_sub(left.chars().count() + right.chars().count());
    let paragraph = Paragraph::new(Line::from(vec![
        Span::styled(
            CLOSE_BUTTON_LABEL,
            app.fg_bold(Color::Red).bg(app.c(Color::Gray)),
        ),
        Span::styled(
            format!(" {}", title),
            app.fg_bold(Color::White).bg(app.c(Color::Red)),
        ),
        Span::styled(
            " ".repeat(gap),
            Style::default().bg(app.c(Color::Red)).add_modifier(Modifier::BOLD),
        ),
        Span::styled(
            EXPORT_BUTTON_LABEL,
            app.fg_bold(Color::LightGreen).bg(app.c(Color::Red)),
        ),
        Span::styled(
            " ",
            Style::default().bg(app.c(Color::Red)).add_modifier(Modifier::BOLD),
        ),
        Span::styled(
            HELP_BUTTON_LABEL,
            app.fg_bold(Color::LightGreen).bg(app.c(Color::Red)),
        ),
    ]))
    .style(
        app.fg_bold(Color::White).bg(app.c(Color::Red)),
    );
    f.render_widget(paragraph, area);
}

// Help Overlay size is here you goofus
fn draw_help_overlay(f: &mut Frame, app: &mut App, area: Rect) {
    let popup = help_popup_rect(area);
    app.panel_rects.help_popup = Some(popup);

    let text = vec![
        Line::from(Span::styled(" ", app.fg(Color::White))),
        Line::from(Span::styled("Global :", app.fg(Color::White))),
        Line::from(Span::styled(
            "F1 or Help button   Toggle this help",
            app.fg(Color::White),
        )),
        Line::from(Span::styled(
            "F2 or Export button Open export popup",
            app.fg(Color::White),
        )),
        Line::from(Span::styled(
            "Ctrl+C              Quit the app",
            app.fg(Color::White),
        )),
        Line::from(Span::styled(
            "s                   Stop/cancel running search",
            app.fg(Color::White),
        )),
        Line::from(Span::styled(
            "Esc                 Clear selection or close help",
            app.fg(Color::White),
        )),
        Line::from(Span::styled(
            "Tab or Shift+Tab    Move between panels",
            app.fg(Color::White),
        )),
        Line::from(Span::styled(
            "Up and Down arrows  Navigate results",
            app.fg(Color::White),
        )),
        Line::from(Span::styled(" ", app.fg(Color::White))),
        Line::from(Span::styled(
            "Mouse               Click Elements duh",
            app.fg(Color::White),
        )),
        Line::from(Span::styled(
            "Scrolling           Scroll through elements (yea)",
            app.fg(Color::White),
        )),
        Line::from(Span::styled(" ", app.fg(Color::White))),
        Line::from(Span::styled(
            "In Results :",
            app.fg(Color::White),
        )),
        Line::from(Span::styled(
            "Enter               Add highlighted result to Favorites",
            app.fg(Color::White),
        )),
        Line::from(Span::styled(
            "In Favorites :",
            app.fg(Color::White),
        )),
        Line::from(Span::styled(
            "Backspace or Delete Remove focused favorite",
            app.fg(Color::White),
        )),
    ];

    let block = app.block()
        .border_style(app.fg(Color::Red))
        .title(" Help ");

    f.render_widget(Clear, popup);
    f.render_widget(Paragraph::new(text).block(block), popup);
}

fn draw_export_popup(f: &mut Frame, app: &mut App, area: Rect) {
    let Some(popup_state) = app.export_popup.as_ref() else {
        return;
    };

    let popup = export_popup_rect(area);
    app.panel_rects.export_popup = Some(popup);

    let block = app.block()
        .border_style(app.fg(Color::Red))
        .title(" Export ");

    let inner = block.inner(popup);
    f.render_widget(Clear, popup);
    f.render_widget(block, popup);

    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(1),
            Constraint::Length(1),
            Constraint::Length(3),
            Constraint::Length(1),
            Constraint::Length(1),
            Constraint::Min(0),
        ])
        .split(inner);

    let white_style = app.fg(Color::White);
    let mode_style = |mode: ExportMode| {
        let mut style = white_style;
        if popup_state.mode == mode {
            style = style.add_modifier(Modifier::REVERSED | Modifier::BOLD);
        } else if popup_state.selected_row == 0 {
            style = style.add_modifier(Modifier::BOLD);
        }
        style
    };

    let subtitle = fit_cell_center(
        "Choose what to export and where to save it.",
        chunks[0].width as usize,
    );
    f.render_widget(
        Paragraph::new(subtitle).style(app.fg(Color::DarkGray)),
        chunks[0],
    );

    let favorites_label = "[Favorites TXT]";
    let results_label = "[Results CSV]";
    let mode_spacing = "  ";
    let mode_text = format!("{}{}{}", favorites_label, mode_spacing, results_label);
    let mode_pad = chunks[1]
        .width
        .saturating_sub(mode_text.chars().count() as u16)
        / 2;
    let mode_x = chunks[1].x + mode_pad;
    app.panel_rects.export_mode_favorites = Some(Rect {
        x: mode_x,
        y: chunks[1].y,
        width: favorites_label.chars().count() as u16,
        height: 1,
    });
    app.panel_rects.export_mode_results = Some(Rect {
        x: mode_x + favorites_label.chars().count() as u16 + mode_spacing.chars().count() as u16,
        y: chunks[1].y,
        width: results_label.chars().count() as u16,
        height: 1,
    });
    let mode_line = Line::from(vec![
        Span::raw(" ".repeat(mode_pad as usize)),
        Span::styled(favorites_label, mode_style(ExportMode::FavoritesTxt)),
        Span::raw(mode_spacing),
        Span::styled(results_label, mode_style(ExportMode::ResultsCsv)),
    ]);
    f.render_widget(Paragraph::new(mode_line), chunks[1]);

    let path_block = app.block()
        .border_style(if popup_state.selected_row == 1 {
            app.fg(Color::Red)
        } else {
            app.fg(Color::DarkGray)
        })
        .title(" Save to ");
    let path_inner = path_block.inner(chunks[2]);
    app.panel_rects.export_path = Some(chunks[2]);
    f.render_widget(path_block, chunks[2]);
    f.render_widget(
        Paragraph::new(popup_state.path.as_str()).style(app.fg(Color::White)),
        path_inner,
    );

    let status_style = if popup_state.status_success {
        app.fg_bold(Color::Green)
    } else if popup_state.confirm_overwrite {
        app.fg_bold(Color::Yellow)
    } else if popup_state.status.is_some() {
        app.fg(Color::Red)
    } else {
        app.fg(Color::DarkGray)
    };
    let status_text = popup_state.status.as_deref().unwrap_or(" ");
    f.render_widget(
        Paragraph::new(fit_cell_center(status_text, chunks[3].width as usize)).style(status_style),
        chunks[3],
    );

    let cancel_label = "[Cancel]";
    let button_gap = "  ";
    let save_label = "[Save]";
    let button_text = format!("{}{}{}", cancel_label, button_gap, save_label);
    let button_pad = chunks[4]
        .width
        .saturating_sub(button_text.chars().count() as u16);
    let buttons_x = chunks[4].x + button_pad;
    app.panel_rects.export_cancel = Some(Rect {
        x: buttons_x,
        y: chunks[4].y,
        width: cancel_label.chars().count() as u16,
        height: 1,
    });
    app.panel_rects.export_save = Some(Rect {
        x: buttons_x + cancel_label.chars().count() as u16 + button_gap.chars().count() as u16,
        y: chunks[4].y,
        width: save_label.chars().count() as u16,
        height: 1,
    });
    let button_line = Line::from(vec![
        Span::raw(" ".repeat(button_pad as usize)),
        Span::styled(
            cancel_label,
            if popup_state.selected_row == 2 {
                app.fg_bold(Color::Green).bg(app.c(Color::DarkGray))
            } else {
                app.fg_bold(Color::Green)
            },
        ),
        Span::raw(button_gap),
        Span::styled(
            save_label,
            if popup_state.selected_row == 3 {
                app.fg_bold(Color::Green).bg(app.c(Color::DarkGray))
            } else {
                app.fg_bold(Color::Green)
            },
        ),
    ]);
    f.render_widget(Paragraph::new(button_line), chunks[4]);

    if popup_state.selected_row == 1 {
        let max_x = path_inner.width.saturating_sub(1);
        let x = path_inner.x + (popup_state.cursor_pos as u16).min(max_x);
        let y = path_inner.y;
        f.set_cursor_position((x, y));
    }
}

fn draw_results(f: &mut Frame, app: &mut App, area: Rect) {
    let focused = app.focus == Focus::Results;
    let border_style = if focused {
        app.fg(Color::Red)
    } else {
        app.fg(Color::DarkGray)
    };

    // show progress in title when searching
    let title = if app.searching {
        let (cur, tot) = app.search_progress;
        if tot > 0 {
            format!(" Results [{}/{}] ", cur, tot)
        } else {
            " Results (loading...) ".to_string()
        }
    } else if app.results.is_empty() {
        " Results ".to_string()
    } else {
        let avail = app.results.iter().filter(|(_, r)| r.is_available()).count();
        let duration_str = match app.last_search_duration {
            Some(d) => format!(" | Took: {:.1}s", d.as_secs_f64()),
            None => String::new(),
        };
        format!(
            " Results ({} available / {} total{}) ",
            avail,
            app.results.len(),
            duration_str
        )
    };

    let block = app.block()
        .border_style(border_style)
        .title(title);

    // If searching and have progress, show a gauge bar at top of results area
    if app.searching && app.search_progress.1 > 0 {
        let inner = block.inner(area);
        f.render_widget(block, area);

        // split: 1 line for progress bar, rest for results
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Length(1), Constraint::Min(1)])
            .split(inner);

        // draw progress gauge
        let (cur, tot) = app.search_progress;
        let pct = (cur as f64 / tot as f64 * 100.0) as u16;
        let filled = (chunks[0].width as u32 * cur as u32 / tot as u32) as u16;
        let bar: String = app.sym_bar_filled().repeat(filled as usize)
            + &app.sym_bar_empty().repeat((chunks[0].width.saturating_sub(filled)) as usize);
        let bar_text = format!(" {}% ", pct);
        let gauge_line = Line::from(vec![
            Span::styled(bar, app.fg(Color::Red)),
            Span::styled(bar_text, app.fg(Color::DarkGray)),
        ]);
        f.render_widget(Paragraph::new(gauge_line), chunks[0]);

        // draw results list in remaining space
        draw_results_list(f, app, chunks[1]);
    } else {
        let inner = block.inner(area);
        f.render_widget(block, area);
        draw_results_list(f, app, inner);
    }
}

fn draw_results_list(f: &mut Frame, app: &mut App, area: Rect) {
    let show_note_column = app.show_unavailable;
    let selected_idx = app.results_state.selected();
    let selected_bg = Color::Black;
    let sep = format!(" {} ", app.sym_sep());

    // collect visible results
    let visible_data: Vec<(String, String, String, DomainStatus)> = if app.show_unavailable {
        app.results
            .iter()
            .map(|(_, r)| {
                (
                    r.full.clone(),
                    r.status_str().to_string(),
                    r.note_str(),
                    r.status.clone(),
                )
            })
            .collect()
    } else {
        app.results
            .iter()
            .filter(|(_, r)| r.is_available())
            .map(|(_, r)| {
                (
                    r.full.clone(),
                    r.status_str().to_string(),
                    r.note_str(),
                    r.status.clone(),
                )
            })
            .collect()
    };

    if visible_data.is_empty() && !app.searching {
        let msg = if app.results.is_empty() {
            "Type a domain suffix or full domain name then press Enter"
        } else {
            "No available domains found"
        };
        let p = Paragraph::new(msg).style(app.fg(Color::DarkGray));
        f.render_widget(p, area);
        return;
    }

    // calculate adaptive column widths from available space
    let total_w = area.width.saturating_sub(1) as usize;
    let marker_w = 3usize;
    let sep_w = if show_note_column { 9usize } else { 6usize };
    let mut status_w = 10usize.min(total_w.saturating_sub(12)).max(6);
    let mut note_w = if show_note_column {
        (total_w / 3).clamp(8, 28)
    } else {
        0
    };
    let mut domain_w = total_w.saturating_sub(status_w + note_w + marker_w + sep_w);

    if domain_w < 10 {
        let needed = 10 - domain_w;
        if show_note_column {
            let shrink_note = needed.min(note_w.saturating_sub(8));
            note_w = note_w.saturating_sub(shrink_note);
        }
        domain_w = total_w.saturating_sub(status_w + note_w + marker_w + sep_w);
    }
    if domain_w < 10 {
        let needed = 10 - domain_w;
        let shrink_status = needed.min(status_w.saturating_sub(6));
        status_w = status_w.saturating_sub(shrink_status);
        domain_w = total_w.saturating_sub(status_w + note_w + marker_w + sep_w);
    }
    domain_w = domain_w.max(6);

    let (list_area, header_area) = if !visible_data.is_empty() {
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Length(1), Constraint::Min(1)])
            .split(area);
        (chunks[1], Some(chunks[0]))
    } else {
        (area, None)
    };

    if let Some(header_area) = header_area {
        let mut header_spans = vec![
            Span::styled(
                format!(" {}", fit_cell("Domain", domain_w)),
                app.fg_bold(Color::Gray),
            ),
            Span::styled(sep.clone(), app.fg(Color::DarkGray)),
            Span::styled(
                fit_cell("Status", status_w),
                app.fg_bold(Color::Gray),
            ),
        ];

        if show_note_column {
            header_spans.push(Span::styled(sep.clone(), app.fg(Color::DarkGray)));
            header_spans.push(Span::styled(
                fit_cell("Details", note_w),
                app.fg_bold(Color::Gray),
            ));
        }

        header_spans.push(Span::styled(sep.clone(), app.fg(Color::DarkGray)));
        header_spans.push(Span::styled(
            format!(" {} ", app.sym_check()),
            app.fg_bold(Color::Gray),
        ));

        f.render_widget(Paragraph::new(Line::from(header_spans)), header_area);
    }

    let items: Vec<ListItem> = visible_data
        .iter()
        .enumerate()
        .map(|(idx, (full, status_str, note, status))| {
            let is_selected = selected_idx == Some(idx);
            let selection_bg = if is_selected { Some(selected_bg) } else { None };

            let status_style = match status {
                DomainStatus::Available => app.fg(Color::Green),
                DomainStatus::Registered { .. } => app.fg(Color::Red),
                DomainStatus::Error { kind, .. } => match kind {
                    ErrorKind::InvalidTld => app.fg(Color::Yellow),
                    _ => app.fg(Color::Blue),
                },
            };

            let domain_style = match status {
                DomainStatus::Available => app.fg(Color::Green),
                DomainStatus::Registered { .. } => app.fg(Color::Red),
                DomainStatus::Error { kind, .. } => match kind {
                    ErrorKind::InvalidTld => app.fg(Color::Yellow),
                    _ => app.fg(Color::Blue),
                },
            };

            let apply_bg = |style: Style| {
                if let Some(bg) = selection_bg {
                    style.bg(bg).add_modifier(Modifier::BOLD)
                } else {
                    style
                }
            };

            let mut spans = vec![
                Span::styled(
                    format!(" {}", fit_cell(full, domain_w)),
                    apply_bg(domain_style),
                ),
                Span::styled(sep.clone(), apply_bg(app.fg(Color::Gray))),
                Span::styled(fit_cell(status_str, status_w), apply_bg(status_style)),
            ];

            if show_note_column {
                spans.push(Span::styled(
                    sep.clone(),
                    apply_bg(app.fg(Color::Gray)),
                ));
                spans.push(Span::styled(
                    fit_cell(note, note_w),
                    apply_bg(app.fg(Color::White)),
                ));
            }

            spans.push(Span::styled(
                sep.clone(),
                apply_bg(app.fg(Color::Gray)),
            ));
            spans.push(match status {
                DomainStatus::Available => {
                    Span::styled(format!(" {} ", app.sym_check()), apply_bg(app.fg(Color::Green)))
                }
                DomainStatus::Registered { .. } => {
                    Span::styled(format!(" {} ", app.sym_cross()), apply_bg(app.fg(Color::Red)))
                }
                DomainStatus::Error { kind, .. } => match kind {
                    ErrorKind::InvalidTld => {
                        Span::styled(" ? ", apply_bg(app.fg(Color::Yellow)))
                    }
                    _ => Span::styled(" ! ", apply_bg(app.fg(Color::Blue))),
                },
            });

            let line = Line::from(spans);
            ListItem::new(line)
        })
        .collect();

    let list = List::new(items);

    f.render_stateful_widget(list, list_area, &mut app.results_state);
}

fn fit_cell(value: &str, width: usize) -> String {
    if width == 0 {
        return String::new();
    }

    let len = value.chars().count();
    if len <= width {
        return format!("{:<width$}", value, width = width);
    }

    if width == 1 {
        return "…".to_string();
    }

    let truncated: String = value.chars().take(width - 1).collect();
    format!("{:<width$}", format!("{}…", truncated), width = width)
}

fn fit_cell_center(value: &str, width: usize) -> String {
    if width == 0 {
        return String::new();
    }

    let rendered = if value.chars().count() <= width {
        value.to_string()
    } else if width == 1 {
        "…".to_string()
    } else {
        let truncated: String = value.chars().take(width - 1).collect();
        format!("{}…", truncated)
    };

    let len = rendered.chars().count();
    let left = width.saturating_sub(len) / 2;
    let right = width.saturating_sub(len + left);
    format!("{}{}{}", " ".repeat(left), rendered, " ".repeat(right))
}

fn wrap_text_lines(text: &str, width: u16) -> Vec<String> {
    if width == 0 {
        return Vec::new();
    }

    let width = width as usize;
    let mut wrapped = Vec::new();

    for line in text.split('\n') {
        if line.is_empty() {
            wrapped.push(String::new());
            continue;
        }

        let chars: Vec<char> = line.chars().collect();
        for chunk in chars.chunks(width) {
            wrapped.push(chunk.iter().collect());
        }
    }

    if text.ends_with('\n') {
        wrapped.push(String::new());
    }

    wrapped
}

fn scratchpad_cursor_positions(text: &str, width: u16) -> Vec<(usize, u16, u16)> {
    if width == 0 {
        return vec![(0, 0, 0)];
    }

    let mut positions = Vec::with_capacity(text.chars().count() + 1);
    let mut line = 0u16;
    let mut col = 0u16;
    let mut byte_idx = 0usize;
    positions.push((byte_idx, line, col));

    for ch in text.chars() {
        byte_idx += ch.len_utf8();
        if ch == '\n' {
            line += 1;
            col = 0;
        } else {
            col += 1;
            if col >= width {
                line += 1;
                col = 0;
            }
        }
        positions.push((byte_idx, line, col));
    }

    positions
}

fn cursor_line_col(text: &str, cursor: usize, width: u16) -> (u16, u16) {
    let clamped_cursor = cursor.min(text.len());
    scratchpad_cursor_positions(text, width)
        .into_iter()
        .take_while(|(idx, _, _)| *idx <= clamped_cursor)
        .last()
        .map(|(_, line, col)| (line, col))
        .unwrap_or((0, 0))
}

fn cursor_index_for_line_col(text: &str, target_line: u16, target_col: u16, width: u16) -> usize {
    let positions = scratchpad_cursor_positions(text, width);
    let mut best_on_line = None;

    for (idx, line, col) in positions {
        if line == target_line {
            best_on_line = Some(idx);
            if col >= target_col {
                return idx;
            }
        } else if line > target_line {
            return best_on_line.unwrap_or(idx);
        }
    }

    best_on_line.unwrap_or(text.len())
}

fn scratchpad_inner_width(app: &App) -> u16 {
    app.panel_rects
        .scratchpad
        .map(|rect| rect.width.saturating_sub(2).max(1))
        .unwrap_or(1)
}

fn move_scratchpad_cursor_vertical(app: &mut App, line_delta: i16) {
    let width = scratchpad_inner_width(app);
    let (line, col) = cursor_line_col(&app.scratchpad, app.scratchpad_cursor, width);
    let target_line = if line_delta.is_negative() {
        line.saturating_sub(line_delta.unsigned_abs())
    } else {
        line.saturating_add(line_delta as u16)
    };
    app.scratchpad_cursor = cursor_index_for_line_col(&app.scratchpad, target_line, col, width);
}

fn draw_scratchpad(f: &mut Frame, app: &mut App, area: Rect) {
    let focused = app.focus == Focus::Scratchpad;
    let border_style = if focused {
        app.fg(Color::Red)
    } else {
        app.fg(Color::DarkGray)
    };

    let block = app.block()
        .border_style(border_style)
        .title(" Scratchpad ");

    let inner = block.inner(area);
    let wrapped_lines = wrap_text_lines(&app.scratchpad, inner.width);
    let text: Vec<Line> = if wrapped_lines.is_empty() {
        vec![Line::raw(String::new())]
    } else {
        wrapped_lines.into_iter().map(Line::raw).collect()
    };
    f.render_widget(block, area);
    f.render_widget(
        Paragraph::new(text).style(app.fg(Color::White)),
        inner,
    );

    if focused && app.export_popup.is_none() {
        let (line, col) = cursor_line_col(&app.scratchpad, app.scratchpad_cursor, inner.width);
        let max_x = inner.width.saturating_sub(1);
        let max_y = inner.height.saturating_sub(1);
        f.set_cursor_position((inner.x + col.min(max_x), inner.y + line.min(max_y)));
    }
}

fn draw_favorites(f: &mut Frame, app: &mut App, area: Rect) {
    let focused = app.focus == Focus::Favorites;
    let border_style = if focused {
        app.fg(Color::Red)
    } else {
        app.fg(Color::DarkGray)
    };

    let title = if app.checking_favorites {
        " Favorites (checking...) "
    } else {
        " Favorites "
    };

    let block = app.block()
        .border_style(border_style)
        .title(title);

    let inner = block.inner(area);

    // Reserve 1 row at the bottom for the check button
    let list_area = Rect {
        x: inner.x,
        y: inner.y,
        width: inner.width,
        height: inner.height.saturating_sub(1),
    };
    let button_area = Rect {
        x: inner.x,
        y: inner.y + list_area.height,
        width: inner.width,
        height: 1.min(inner.height),
    };
    app.panel_rects.fav_check_button = Some(button_area);

    let items: Vec<ListItem> = app
        .favorites
        .iter()
        .map(|fav| {
            let status_color = match fav.status.as_str() {
                "available" => Color::Green,
                "registered" => Color::Red,
                "error" => Color::DarkGray,
                _ => Color::White, // unknown
            };
            let mut spans = vec![Span::styled(
                fav.domain.as_str(),
                app.fg(status_color),
            )];
            if fav.changed {
                spans.push(Span::styled(" !", app.fg(Color::Yellow)));
            }
            ListItem::new(Line::from(spans))
        })
        .collect();

    let list = List::new(items).highlight_style(Style::default().add_modifier(Modifier::REVERSED));

    f.render_widget(block, area);
    f.render_stateful_widget(list, list_area, &mut app.favorites_state);

    // Draw the check button at the bottom
    let btn_label = if app.checking_favorites {
        "checking..."
    } else {
        "[c]heck all"
    };
    let btn_style = if app.checking_favorites {
        app.fg(Color::DarkGray)
    } else {
        app.fg(Color::Green)
    };
    f.render_widget(
        Paragraph::new(Line::from(Span::styled(btn_label, btn_style)))
            .alignment(ratatui::layout::Alignment::Center),
        button_area,
    );
}

fn draw_settings(f: &mut Frame, app: &mut App, area: Rect) {
    let focused = app.focus == Focus::Settings;
    let border_style = if focused {
        app.fg(Color::Red)
    } else {
        app.fg(Color::DarkGray)
    };

    let block = app.block()
        .border_style(border_style)
        .title(" Settings ");

    let unavail_check = if app.show_unavailable { "[x]" } else { "[ ]" };
    let notes_check = if app.show_notes_panel { "[x]" } else { "[ ]" };
    let clear_check = if app.clear_on_search { "[x]" } else { "[ ]" };
    let jobs_str = format!("{:>2}", app.jobs);

    let selected = if focused { app.settings_selected } else { None };
    let checkbox_style = |row: usize, checked: bool| {
        let style = if checked {
            app.fg(Color::Green)
        } else {
            app.fg(Color::DarkGray)
        };

        if selected == Some(row) {
            style.add_modifier(Modifier::REVERSED | Modifier::BOLD)
        } else {
            style
        }
    };

    let label_style = |row: usize| {
        if selected == Some(row) {
            Style::default().add_modifier(Modifier::REVERSED)
        } else {
            app.fg(Color::White)
        }
    };

    let tld_row_style = if selected == Some(0) {
        Style::default()
            .bg(app.c(Color::DarkGray))
            .add_modifier(Modifier::BOLD)
    } else {
        Style::default()
    };

    let jobs_row_style = if selected == Some(4) {
        Style::default()
            .bg(app.c(Color::DarkGray))
            .add_modifier(Modifier::BOLD)
    } else {
        Style::default()
    };

    let text = vec![
        Line::from(vec![
            Span::raw(" "),
            Span::styled("TLD List: [", tld_row_style.fg(app.c(Color::White))),
            Span::styled(app.tld_list_name.as_str(), tld_row_style.fg(app.c(Color::Cyan))),
            Span::styled("] ", tld_row_style.fg(app.c(Color::White))),
            Span::styled("V", tld_row_style.fg(app.c(Color::Green))),
        ]),
        Line::from(vec![
            Span::raw(" "),
            Span::styled(unavail_check, checkbox_style(1, app.show_unavailable)),
            Span::styled(" Show Unavailable", label_style(1)),
        ]),
        Line::from(vec![
            Span::raw(" "),
            Span::styled(notes_check, checkbox_style(2, app.show_notes_panel)),
            Span::styled(" Show Notes Panel", label_style(2)),
        ]),
        Line::from(vec![
            Span::raw(" "),
            Span::styled(clear_check, checkbox_style(3, app.clear_on_search)),
            Span::styled(" Clear on Search", label_style(3)),
        ]),
        Line::from(vec![
            Span::raw(" "),
            Span::styled("Jobs: [", jobs_row_style.fg(app.c(Color::White))),
            Span::styled(jobs_str, jobs_row_style.fg(app.c(Color::Cyan))),
            Span::styled("] ", jobs_row_style.fg(app.c(Color::White))),
            Span::styled("-/+", jobs_row_style.fg(app.c(Color::Green))),
        ]),
    ];

    let paragraph = Paragraph::new(text).block(block);
    f.render_widget(paragraph, area);
}

fn draw_search(f: &mut Frame, app: &mut App, area: Rect) {
    let focused = app.focus == Focus::Search;
    let border_style = if focused {
        app.fg(Color::Red)
    } else {
        app.fg(Color::DarkGray)
    };

    let title = match &app.status_msg {
        Some(msg) => format!(" Search - {} ", msg),
        None => " Search (Enter to lookup) ".to_string(),
    };

    let block = app.block()
        .border_style(border_style)
        .title(title);

    let inner = block.inner(area);
    f.render_widget(block, area);

    let search_button_width = SEARCH_BUTTON_LABEL.chars().count() as u16;
    let clear_button_width = CLEAR_BUTTON_LABEL.chars().count() as u16;
    let stop_button_width = STOP_BUTTON_LABEL.chars().count() as u16;
    let chunks = if app.clear_on_search {
        Layout::default()
            .direction(Direction::Horizontal)
            .constraints([
                Constraint::Min(1),
                Constraint::Length(search_button_width),
                Constraint::Length(1),
                Constraint::Length(stop_button_width),
            ])
            .split(inner)
    } else {
        Layout::default()
            .direction(Direction::Horizontal)
            .constraints([
                Constraint::Min(1),
                Constraint::Length(search_button_width),
                Constraint::Length(1),
                Constraint::Length(stop_button_width),
                Constraint::Length(1),
                Constraint::Length(clear_button_width),
            ])
            .split(inner)
    };

    app.panel_rects.search_button = Some(chunks[1]);
    if app.clear_on_search {
        app.panel_rects.clear_button = None;
        app.panel_rects.cancel_button = Some(chunks[3]);
    } else {
        app.panel_rects.cancel_button = Some(chunks[3]);
        app.panel_rects.clear_button = Some(chunks[5]);
    }

    let input_chunk = chunks[0];
    let visible_input = fit_cell(&app.search_input, input_chunk.width as usize);
    let input = Paragraph::new(visible_input).style(app.fg(Color::White));
    f.render_widget(input, input_chunk);

    let search_enabled = !app.searching && !app.search_input.is_empty();
    let cancel_enabled = app.searching;

    let search_style = if search_enabled {
        app.fg_bold(Color::Black).bg(app.c(Color::Green))
    } else {
        app.fg(Color::DarkGray).bg(app.c(Color::Black))
    };
    let stop_style = if cancel_enabled {
        app.fg_bold(Color::Black).bg(app.c(Color::Yellow))
    } else {
        app.fg(Color::DarkGray).bg(app.c(Color::Black))
    };
    let clear_style = app.fg_bold(Color::White).bg(app.c(Color::Red));

    f.render_widget(
        Paragraph::new(SEARCH_BUTTON_LABEL).style(search_style),
        chunks[1],
    );
    if app.clear_on_search {
        f.render_widget(
            Paragraph::new(STOP_BUTTON_LABEL).style(stop_style),
            chunks[3],
        );
    } else {
        f.render_widget(
            Paragraph::new(STOP_BUTTON_LABEL).style(stop_style),
            chunks[3],
        );
        f.render_widget(
            Paragraph::new(CLEAR_BUTTON_LABEL).style(clear_style),
            chunks[5],
        );
    }

    // show cursor in search bar when focused
    if focused && app.export_popup.is_none() {
        let max_cursor = input_chunk.width.saturating_sub(1) as usize;
        let x = input_chunk.x + app.cursor_pos.min(max_cursor) as u16;
        let y = input_chunk.y;
        f.set_cursor_position((x, y));
    }
}

fn draw_dropdown(f: &mut Frame, app: &mut App, settings_area: Rect, selected: usize) {
    let options = app.list_options();

    // position dropdown below the TLD list line in settings
    let dropdown_full = Rect {
        x: settings_area.x + 1,
        y: settings_area.y + 1,
        width: settings_area
            .width
            .saturating_sub(2)
            .min(DROPDOWN_MAX_WIDTH),
        height: (options.len() as u16 + 2).min(DROPDOWN_MAX_HEIGHT),
    };

    app.panel_rects.dropdown = Some(dropdown_full);

    // clear the area behind the dropdown
    f.render_widget(Clear, dropdown_full);

    let items: Vec<ListItem> = options
        .iter()
        .map(|opt| {
            ListItem::new(Line::from(Span::styled(
                format!(" {} ", opt),
                app.fg(Color::White),
            )))
        })
        .collect();

    let block = app.block()
        .border_style(app.fg(Color::Red))
        .title(" TLD List ");

    f.render_widget(Clear, dropdown_full);
    let list = List::new(items).block(block).highlight_style(
        app.fg_bold(Color::White).bg(app.c(Color::Red)),
    );
    let mut state = ListState::default();
    state.select(Some(selected));
    f.render_stateful_widget(list, dropdown_full, &mut state);
}
