use crate::{
    args::{ColumnsArgs, ColumnsFlag},
    column::Columns,
    decks::{Decks, DecksCache},
    draft::Drafts,
    nav::{self, ProcessNavResult},
    onboarding::Onboarding,
    options::AppOptions,
    route::Route,
    storage,
    support::Support,
    timeline::{self, kind::ListKind, thread::Threads, TimelineCache, TimelineKind},
    timeline_loader::{TimelineLoader, TimelineLoaderMsg},
    ui::{self, DesktopSidePanel, SidePanelAction},
    view_state::ViewState,
    Result,
};
use egui_extras::{Size, StripBuilder};
use enostr::Pubkey;
use nostrdb::Transaction;
use notedeck::{
    tr, ui::is_compiled_as_mobile, ui::is_narrow, Accounts, AppAction, AppContext, AppResponse,
    DataPath, DataPathType, FilterState, Images, Localization, MediaJobSender, NotedeckOptions,
    SettingsHandler,
};
use notedeck_ui::{
    media::{MediaViewer, MediaViewerFlags, MediaViewerState},
    NoteOptions,
};
use std::collections::{BTreeSet, HashMap};
use std::path::Path;
use tracing::{error, info, warn};

/// Max timeline loader messages to process per frame to avoid UI stalls.
const MAX_TIMELINE_LOADER_MSGS_PER_FRAME: usize = 8;

#[derive(Debug, Eq, PartialEq, Clone)]
pub enum DamusState {
    Initializing,
    Initialized,
}

/// We derive Deserialize/Serialize so we can persist app state on shutdown.
pub struct Damus {
    state: DamusState,

    pub decks_cache: DecksCache,
    pub view_state: ViewState,
    pub drafts: Drafts,
    pub timeline_cache: TimelineCache,
    pub support: Support,
    pub threads: Threads,
    /// Background loader for initial timeline scans.
    timeline_loader: TimelineLoader,

    //frame_history: crate::frame_history::FrameHistory,

    // TODO: make these bitflags
    /// Were columns loaded from the commandline? If so disable persistence.
    pub options: AppOptions,
    pub note_options: NoteOptions,

    pub unrecognized_args: BTreeSet<String>,

    /// keep track of follow packs
    pub onboarding: Onboarding,

    /// Track which column is hovered for mouse back/forward navigation
    hovered_column: Option<usize>,
}

#[profiling::function]
fn handle_egui_events(
    input: &egui::InputState,
    columns: &mut Columns,
    hovered_column: Option<usize>,
    wants_keyboard_input: bool,
) {
    for event in &input.raw.events {
        match event {
            egui::Event::Key {
                key,
                pressed,
                modifiers,
                ..
            } if *pressed => {
                // Browser-like navigation: Cmd+Arrow (macOS) / Ctrl+Arrow (others)
                if !wants_keyboard_input
                    && (modifiers.ctrl || modifiers.command)
                    && !modifiers.shift
                    && !modifiers.alt
                {
                    match key {
                        egui::Key::ArrowLeft | egui::Key::H => {
                            columns.get_selected_router().go_back();
                            continue;
                        }
                        egui::Key::ArrowRight | egui::Key::L => {
                            columns.get_selected_router().go_forward();
                            continue;
                        }
                        _ => {}
                    }
                }

                match key {
                    egui::Key::J => {
                        //columns.select_down();
                        {}
                    }
                    /*
                    egui::Key::K => {
                        columns.select_up();
                    }
                    egui::Key::H => {
                        columns.select_left();
                    }
                    egui::Key::L => {
                        columns.select_left();
                    }
                    */
                    egui::Key::BrowserBack => {
                        columns.get_selected_router().go_back();
                    }
                    _ => {}
                }
            }

            egui::Event::PointerButton {
                button: egui::PointerButton::Extra1,
                pressed: true,
                ..
            } => {
                if let Some(col_idx) = hovered_column {
                    columns.column_mut(col_idx).router_mut().go_back();
                } else {
                    columns.get_selected_router().go_back();
                }
            }

            egui::Event::PointerButton {
                button: egui::PointerButton::Extra2,
                pressed: true,
                ..
            } => {
                if let Some(col_idx) = hovered_column {
                    columns.column_mut(col_idx).router_mut().go_forward();
                } else {
                    columns.get_selected_router().go_forward();
                }
            }

            egui::Event::InsetsChanged => {
                tracing::debug!("insets have changed!");
            }

            _ => {}
        }
    }
}

#[profiling::function]
fn try_process_event(
    damus: &mut Damus,
    app_ctx: &mut AppContext<'_>,
    ctx: &egui::Context,
) -> Result<()> {
    let current_columns =
        get_active_columns_mut(app_ctx.i18n, app_ctx.accounts, &mut damus.decks_cache);
    let wants_keyboard_input = ctx.wants_keyboard_input();
    ctx.input(|i| {
        handle_egui_events(
            i,
            current_columns,
            damus.hovered_column,
            wants_keyboard_input,
        )
    });

    // Handle Escape separately: only consume the key if there's a route to go back to,
    // otherwise let Chrome handle it (e.g. to open the side menu).
    // Don't consume if the media viewer is open — it should get priority.
    let media_viewer_open = damus
        .view_state
        .media_viewer
        .flags
        .contains(MediaViewerFlags::Open);
    let can_go_back = current_columns.get_selected_router().routes().len() > 1;
    if !media_viewer_open
        && can_go_back
        && ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape))
    {
        current_columns.get_selected_router().go_back();
    }

    let selected_account_pk = *app_ctx.accounts.selected_account_pubkey();
    for (kind, timeline) in &mut damus.timeline_cache {
        if timeline.subscription.dependers(&selected_account_pk) == 0 {
            continue;
        }

        if let FilterState::Ready(filter) = &timeline.filter {
            if timeline.kind.should_subscribe_locally()
                && timeline
                    .subscription
                    .get_local(&selected_account_pk)
                    .is_none()
            {
                timeline
                    .subscription
                    .try_add_local(selected_account_pk, app_ctx.ndb, filter);
            }
        }

        let is_ready = {
            let mut scoped_subs = app_ctx.remote.scoped_subs(app_ctx.accounts);
            timeline::is_timeline_ready(app_ctx.ndb, &mut scoped_subs, timeline, app_ctx.accounts)
        };

        if is_ready {
            schedule_timeline_load(
                &damus.timeline_loader,
                app_ctx.ndb,
                kind,
                timeline,
                app_ctx.accounts.selected_account_pubkey(),
            );
            let txn = Transaction::new(app_ctx.ndb).expect("txn");
            // only thread timelines are reversed
            let reversed = false;

            match timeline.poll_notes_into_view(
                &selected_account_pk,
                app_ctx.ndb,
                &txn,
                app_ctx.unknown_ids,
                app_ctx.note_cache,
                reversed,
            ) {
                Ok(new_note_keys) => {
                    if !new_note_keys.is_empty() && matches!(kind, TimelineKind::Notifications(_)) {
                        let muted = app_ctx.accounts.mute();
                        let has_unmuted = new_note_keys.iter().any(|key| {
                            app_ctx
                                .ndb
                                .get_note_by_key(&txn, *key)
                                .is_ok_and(|note| !muted.is_pk_muted(note.pubkey()))
                        });
                        if has_unmuted {
                            app_ctx
                                .sound
                                .play_debounced(notedeck::SoundEffect::Notification, 2000);
                        }

                        verify_new_zap_receipts(app_ctx, &txn, &new_note_keys);
                    }
                }
                Err(err) => {
                    error!("poll_notes_into_view: {err}");
                }
            }
        } else {
            // TODO: show loading?
            match kind {
                TimelineKind::List(ListKind::Contact(_))
                | TimelineKind::Algo(timeline::kind::AlgoTimeline::LastPerPubkey(
                    ListKind::Contact(_),
                )) => {
                    timeline::fetch_contact_list(timeline, app_ctx.accounts);
                }
                TimelineKind::List(ListKind::PeopleList(_))
                | TimelineKind::Algo(timeline::kind::AlgoTimeline::LastPerPubkey(
                    ListKind::PeopleList(_),
                )) => {
                    let txn = Transaction::new(app_ctx.ndb).expect("txn");
                    timeline::fetch_people_list(app_ctx.ndb, &txn, timeline);
                }
                _ => {}
            }
        }

        if let Some(follow_packs) = damus.onboarding.get_follow_packs_mut() {
            follow_packs.poll_for_notes(app_ctx.ndb, app_ctx.unknown_ids);
        }
    }

    Ok(())
}

/// Schedule an initial timeline load if it is not already in-flight or complete.
fn schedule_timeline_load(
    loader: &TimelineLoader,
    ndb: &nostrdb::Ndb,
    kind: &TimelineKind,
    timeline: &mut timeline::Timeline,
    account_pk: &Pubkey,
) {
    if timeline.initial_load != timeline::InitialLoadState::Pending {
        return;
    }

    let FilterState::Ready(filter) = timeline.filter.clone() else {
        return;
    };

    if timeline.kind.should_subscribe_locally() {
        timeline
            .subscription
            .try_add_local(*account_pk, ndb, &filter);
    }

    loader.load_timeline(kind.clone());
    timeline.initial_load = timeline::InitialLoadState::Loading;
}

/// Drain timeline loader messages and apply them to the timeline cache.
#[profiling::function]
fn handle_timeline_loader_messages(damus: &mut Damus, app_ctx: &mut AppContext<'_>) {
    let mut handled = 0;
    while handled < MAX_TIMELINE_LOADER_MSGS_PER_FRAME {
        let Some(msg) = damus.timeline_loader.try_recv() else {
            break;
        };
        handled += 1;

        match msg {
            TimelineLoaderMsg::TimelineBatch { kind, notes } => {
                let Some(timeline) = damus.timeline_cache.get_mut(&kind) else {
                    warn!("timeline loader batch for missing timeline {:?}", kind);
                    continue;
                };
                let txn = Transaction::new(app_ctx.ndb).expect("txn");
                if let Some(pks) =
                    timeline.insert_new(&txn, app_ctx.ndb, app_ctx.note_cache, &notes)
                {
                    pks.process(app_ctx.ndb, &txn, app_ctx.unknown_ids);
                }
            }
            TimelineLoaderMsg::TimelineFinished { kind } => {
                if let Some(timeline) = damus.timeline_cache.get_mut(&kind) {
                    timeline.initial_load = timeline::InitialLoadState::Complete;
                }
            }
            TimelineLoaderMsg::Failed { kind, error } => {
                warn!("timeline loader failed for {:?}: {}", kind, error);
                if let Some(timeline) = damus.timeline_cache.get_mut(&kind) {
                    timeline.initial_load = timeline::InitialLoadState::Pending;
                }
            }
        }
    }
}

#[profiling::function]
fn update_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ctx: &egui::Context) {
    app_ctx.img_cache.urls.cache.handle_io();

    damus
        .timeline_loader
        .start(ctx.clone(), app_ctx.ndb.clone());

    if damus.columns(app_ctx.accounts).columns().is_empty() {
        damus
            .columns_mut(app_ctx.i18n, app_ctx.accounts)
            .new_column_picker();
    }

    match damus.state {
        DamusState::Initializing => {
            damus.state = DamusState::Initialized;
            setup_selected_account_timeline_subs(&mut damus.timeline_cache, app_ctx);

            if !app_ctx.settings.welcome_completed() {
                damus
                    .columns_mut(app_ctx.i18n, app_ctx.accounts)
                    .get_selected_router()
                    .route_to(Route::Welcome);
            }

            if is_compiled_as_mobile() && !app_ctx.settings.tos_accepted() {
                damus
                    .columns_mut(app_ctx.i18n, app_ctx.accounts)
                    .get_selected_router()
                    .route_to(Route::TosAcceptance);
            }
        }

        DamusState::Initialized => (),
    };

    handle_timeline_loader_messages(damus, app_ctx);

    if let Err(err) = try_process_event(damus, app_ctx, ctx) {
        error!("error processing event: {}", err);
    }
}

pub(crate) fn setup_selected_account_timeline_subs(
    timeline_cache: &mut TimelineCache,
    app_ctx: &mut AppContext<'_>,
) {
    if let Err(err) = timeline::setup_initial_nostrdb_subs(
        app_ctx.ndb,
        timeline_cache,
        *app_ctx.accounts.selected_account_pubkey(),
    ) {
        warn!("update_damus init: {err}");
    }
}

fn render_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
    damus
        .note_options
        .set(NoteOptions::Wide, is_narrow(ui.ctx()));

    let app_resp = if notedeck::ui::is_narrow(ui.ctx()) {
        render_damus_mobile(damus, app_ctx, ui)
    } else {
        render_damus_desktop(damus, app_ctx, ui)
    };

    fullscreen_media_viewer_ui(
        ui,
        &mut damus.view_state.media_viewer,
        app_ctx.img_cache,
        app_ctx.media_jobs.sender(),
    );

    // We use this for keeping timestamps and things up to date
    //ui.ctx().request_repaint_after(Duration::from_secs(5));

    app_resp
}

/// Present a fullscreen media viewer if the FullscreenMedia AppOptions flag is set. This is
/// typically set by image carousels using a MediaAction's on_view_media callback when
/// an image is clicked
fn fullscreen_media_viewer_ui(
    ui: &mut egui::Ui,
    state: &mut MediaViewerState,
    img_cache: &mut Images,
    jobs: &MediaJobSender,
) {
    if !state.should_show(ui) {
        if state.scene_rect.is_some() {
            // if we shouldn't show yet we will have a scene
            // rect, then we should clear it for next time
            tracing::debug!("fullscreen_media_viewer_ui: resetting scene rect");
            state.scene_rect = None;
        }
        return;
    }

    let resp = MediaViewer::new(state)
        .fullscreen(true)
        .ui(img_cache, jobs, ui);

    if resp.clicked()
        || ui
            .ctx()
            .input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape))
    {
        fullscreen_media_close(state);
    }
}

/// Close the fullscreen media player. This also resets the scene_rect state
fn fullscreen_media_close(state: &mut MediaViewerState) {
    state.flags.set(MediaViewerFlags::Open, false);
}

/*
fn determine_key_storage_type() -> KeyStorageType {
    #[cfg(target_os = "macos")]
    {
        KeyStorageType::MacOS
    }

    #[cfg(target_os = "linux")]
    {
        KeyStorageType::Linux
    }

    #[cfg(not(any(target_os = "macos", target_os = "linux")))]
    {
        KeyStorageType::None
    }
}
*/

impl Damus {
    /// Called once before the first frame.
    pub fn new(app_context: &mut AppContext<'_>, args: &[String]) -> Self {
        // arg parsing

        let (parsed_args, unrecognized_args) =
            ColumnsArgs::parse(args, Some(app_context.accounts.selected_account_pubkey()));

        let account = app_context.accounts.selected_account_pubkey_bytes();

        let mut timeline_cache = TimelineCache::default();
        let mut options = AppOptions::default();
        let tmp_columns = !parsed_args.columns.is_empty();
        options.set(AppOptions::TmpColumns, tmp_columns);
        options.set(
            AppOptions::Debug,
            app_context.args.options.contains(NotedeckOptions::Debug),
        );
        options.set(
            AppOptions::SinceOptimize,
            parsed_args.is_flag_set(ColumnsFlag::SinceOptimize),
        );

        let decks_cache = if tmp_columns {
            info!("DecksCache: loading from command line arguments");
            let mut columns: Columns = Columns::new();
            let txn = Transaction::new(app_context.ndb).unwrap();
            for col in &parsed_args.columns {
                let timeline_kind = col.clone().into_timeline_kind();
                let mut scoped_subs = app_context.remote.scoped_subs(app_context.accounts);
                if let Some(add_result) = columns.add_new_timeline_column(
                    &mut timeline_cache,
                    &txn,
                    app_context.ndb,
                    app_context.note_cache,
                    &mut scoped_subs,
                    &timeline_kind,
                    *app_context.accounts.selected_account_pubkey(),
                ) {
                    add_result.process(
                        app_context.ndb,
                        app_context.note_cache,
                        &txn,
                        &mut timeline_cache,
                        app_context.unknown_ids,
                    );
                }
            }

            columns_to_decks_cache(app_context.i18n, columns, account)
        } else if let Some(decks_cache) = crate::storage::load_decks_cache(
            app_context.path,
            app_context.ndb,
            &mut timeline_cache,
            app_context.i18n,
        ) {
            info!(
                "DecksCache: loading from disk {}",
                crate::storage::DECKS_CACHE_FILE
            );
            decks_cache
        } else {
            info!("DecksCache: creating new with demo configuration");
            DecksCache::new_with_demo_config(&mut timeline_cache, app_context)
            //for (pk, _) in &app_context.accounts.cache {
            //    cache.add_deck_default(*pk);
            //}
        };

        let support = Support::new(app_context.path);
        let note_options = get_note_options(parsed_args, app_context.settings);
        let threads = Threads::default();

        Self {
            timeline_cache,
            drafts: Drafts::default(),
            state: DamusState::Initializing,
            note_options,
            options,
            //frame_history: FrameHistory::default(),
            view_state: ViewState::default(),
            support,
            decks_cache,
            unrecognized_args,
            threads,
            onboarding: Onboarding::default(),
            hovered_column: None,
            timeline_loader: TimelineLoader::default(),
        }
    }

    /// Scroll to the top of the currently selected column. This is called
    /// by the chrome when you click the toolbar
    pub fn scroll_to_top(&mut self) {
        self.options.insert(AppOptions::ScrollToTop)
    }

    pub fn columns_mut(&mut self, i18n: &mut Localization, accounts: &Accounts) -> &mut Columns {
        get_active_columns_mut(i18n, accounts, &mut self.decks_cache)
    }

    pub fn columns(&self, accounts: &Accounts) -> &Columns {
        get_active_columns(accounts, &self.decks_cache)
    }

    pub fn mock<P: AsRef<Path>>(data_path: P) -> Self {
        let mut i18n = Localization::default();
        let decks_cache = DecksCache::default_decks_cache(&mut i18n);

        let path = DataPath::new(&data_path);
        let imgcache_dir = path.path(DataPathType::Cache);
        let _ = std::fs::create_dir_all(imgcache_dir.clone());
        let options = AppOptions::default() | AppOptions::Debug | AppOptions::TmpColumns;

        let support = Support::new(&path);

        Self {
            timeline_cache: TimelineCache::default(),
            drafts: Drafts::default(),
            state: DamusState::Initializing,
            note_options: NoteOptions::default(),
            //frame_history: FrameHistory::default(),
            view_state: ViewState::default(),
            support,
            options,
            decks_cache,
            unrecognized_args: BTreeSet::default(),
            threads: Threads::default(),
            onboarding: Onboarding::default(),
            hovered_column: None,
            timeline_loader: TimelineLoader::default(),
        }
    }

    pub fn unrecognized_args(&self) -> &BTreeSet<String> {
        &self.unrecognized_args
    }

    /// Navigate to the Home (contact list) timeline.
    pub fn navigate_home(&mut self, ctx: &mut AppContext) {
        crate::toolbar::ToolbarAction::Home.process(self, ctx);
    }

    /// Navigate to the Search view.
    pub fn navigate_search(&mut self, ctx: &mut AppContext) {
        crate::toolbar::ToolbarAction::Search.process(self, ctx);
    }

    /// Navigate to the Notifications timeline.
    pub fn navigate_notifications(&mut self, ctx: &mut AppContext) {
        crate::toolbar::ToolbarAction::Notifications.process(self, ctx);
    }

    /// Check if there are unseen notifications.
    pub fn has_unseen_notifications(&mut self, accounts: &notedeck::Accounts) -> bool {
        let active_col = self.columns(accounts).selected as usize;
        crate::toolbar::unseen_notification(self, accounts, active_col)
    }

    /// Returns which toolbar tab matches the current active route.
    /// 0=Home, 1=Search, 2=Notifications, None=no match
    pub fn active_toolbar_tab(&self, accounts: &notedeck::Accounts) -> Option<u8> {
        use crate::timeline::kind::ListKind;
        use crate::timeline::TimelineKind;

        let cols = self.columns(accounts);
        let active_col = cols.selected as usize;
        let top = cols.column(active_col).router().top();
        let pk = accounts.get_selected_account().keypair().pubkey;

        match top {
            Route::Timeline(TimelineKind::List(ListKind::Contact(contact_pk)))
                if contact_pk == pk =>
            {
                Some(0)
            }
            Route::Search => Some(1),
            Route::Timeline(TimelineKind::Notifications(notif_pk)) if notif_pk == pk => Some(2),
            _ => None,
        }
    }
}

fn get_note_options(args: ColumnsArgs, settings_handler: &mut SettingsHandler) -> NoteOptions {
    let mut note_options = NoteOptions::default();

    note_options.set(
        NoteOptions::Textmode,
        args.is_flag_set(ColumnsFlag::Textmode),
    );
    note_options.set(
        NoteOptions::ScrambleText,
        args.is_flag_set(ColumnsFlag::Scramble),
    );
    note_options.set(
        NoteOptions::HideMedia,
        args.is_flag_set(ColumnsFlag::NoMedia),
    );
    note_options.set(
        NoteOptions::RepliesNewestFirst,
        settings_handler.show_replies_newest_first(),
    );
    note_options
}

/*
fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) {
    let stroke = ui.style().interact(&response).fg_stroke;
    let radius = egui::lerp(2.0..=3.0, openness);
    ui.painter()
        .circle_filled(response.rect.center(), radius, stroke.color);
}
*/

#[profiling::function]
fn render_damus_mobile(
    app: &mut Damus,
    app_ctx: &mut AppContext<'_>,
    ui: &mut egui::Ui,
) -> AppResponse {
    let mut can_take_drag_from = Vec::new();
    let active_col = app.columns_mut(app_ctx.i18n, app_ctx.accounts).selected as usize;
    let mut app_action: Option<AppAction> = None;

    let rect = ui.available_rect_before_wrap();
    if !app.columns(app_ctx.accounts).columns().is_empty() {
        let resp = nav::render_nav(
            active_col,
            ui.available_rect_before_wrap(),
            app,
            app_ctx,
            ui,
        );

        can_take_drag_from.extend(resp.can_take_drag_from());

        let r = resp.process_render_nav_response(app, app_ctx, ui);
        if let Some(r) = r {
            match r {
                ProcessNavResult::SwitchOccurred => {
                    if !app.options.contains(AppOptions::TmpColumns) {
                        storage::save_decks_cache(app_ctx.path, &app.decks_cache);
                    }
                }

                ProcessNavResult::PfpClicked => {
                    app_action = Some(AppAction::ToggleChrome);
                }

                ProcessNavResult::SwitchAccount(pubkey) => {
                    // Add as pubkey-only account if not already present
                    let kp = enostr::Keypair::only_pubkey(pubkey);
                    let _ = app_ctx.accounts.add_account(kp);

                    app_ctx.select_account(&pubkey);
                    setup_selected_account_timeline_subs(&mut app.timeline_cache, app_ctx);
                }

                ProcessNavResult::ExternalNoteAction(note_action) => {
                    app_action = Some(AppAction::Note(note_action));
                }
            }
        }
    }

    hovering_post_button(ui, app, app_ctx, rect);

    AppResponse::action(app_action).drag(can_take_drag_from)
}

fn hovering_post_button(
    ui: &mut egui::Ui,
    app: &mut Damus,
    app_ctx: &mut AppContext,
    mut rect: egui::Rect,
) {
    let should_show_compose = should_show_compose_button(&app.decks_cache, app_ctx.accounts);
    let btn_id = ui.id().with("hover_post_btn");
    let button_y = ui
        .ctx()
        .animate_bool_responsive(btn_id, should_show_compose);

    rect.min.x = rect.max.x - (if is_narrow(ui.ctx()) { 60.0 } else { 100.0 } * button_y);
    rect.min.y = rect.max.y - 100.0;
    rect.max.x += 48.0 * (1.0 - button_y);

    let darkmode = ui.ctx().style().visuals.dark_mode;

    // only show the compose button on profile pages and on home
    let compose_resp = ui
        .put(rect, ui::post::compose_note_button(darkmode))
        .on_hover_cursor(egui::CursorIcon::PointingHand);
    if compose_resp.clicked() && !app.columns(app_ctx.accounts).columns().is_empty() {
        // just use the some side panel logic as the desktop
        DesktopSidePanel::perform_action(
            &mut app.decks_cache,
            app_ctx.accounts,
            SidePanelAction::ComposeNote,
            app_ctx.i18n,
        );
    }
}

/// Should we show the compose button? When in threads we should hide it, etc
fn should_show_compose_button(decks: &DecksCache, accounts: &Accounts) -> bool {
    let Some(col) = decks.selected_column(accounts) else {
        return false;
    };

    match col.router().top() {
        Route::Timeline(timeline_kind) => {
            match timeline_kind {
                TimelineKind::List(list_kind) => match list_kind {
                    ListKind::Contact(_pk) => true,
                    ListKind::PeopleList(_) => true,
                },

                TimelineKind::Algo(_pk) => true,
                TimelineKind::Profile(_pk) => true,
                TimelineKind::Universe => true,
                TimelineKind::Generic(_) => true,
                TimelineKind::Hashtag(_) => true,

                // no!
                TimelineKind::Search(_) => false,
                TimelineKind::Notifications(_) => false,
            }
        }

        Route::Thread(_) => false,
        Route::Accounts(_) => false,
        Route::Reply(_) => false,
        Route::Quote(_) => false,
        Route::Relays => false,
        Route::Settings => false,
        Route::ComposeNote => false,
        Route::AddColumn(_) => false,
        Route::EditProfile(_) => false,
        Route::Support => false,
        Route::NewDeck => false,
        Route::Search => false,
        Route::EditDeck(_) => false,
        Route::Wallet(_) => false,
        Route::CustomizeZapAmount(_) => false,
        Route::RepostDecision(_) => false,
        Route::Following(_) => false,
        Route::FollowedBy(_) => false,
        Route::TosAcceptance => false,
        Route::Welcome => false,
        Route::Report(_) => false,
        Route::NoteDetails(_) => false,
    }
}

#[profiling::function]
fn render_damus_desktop(
    app: &mut Damus,
    app_ctx: &mut AppContext<'_>,
    ui: &mut egui::Ui,
) -> AppResponse {
    let screen_size = ui.ctx().screen_rect().width();
    let calc_panel_width = (screen_size
        / get_active_columns(app_ctx.accounts, &app.decks_cache).num_columns() as f32)
        - 30.0;
    let min_width = 320.0;
    let need_scroll = calc_panel_width < min_width;
    let panel_sizes = if need_scroll {
        Size::exact(min_width)
    } else {
        Size::remainder()
    };

    ui.spacing_mut().item_spacing.x = 0.0;

    if need_scroll {
        egui::ScrollArea::horizontal()
            .show(ui, |ui| timelines_view(ui, panel_sizes, app, app_ctx))
            .inner
    } else {
        timelines_view(ui, panel_sizes, app, app_ctx)
    }
}

fn timelines_view(
    ui: &mut egui::Ui,
    sizes: Size,
    app: &mut Damus,
    ctx: &mut AppContext<'_>,
) -> AppResponse {
    let num_cols = get_active_columns(ctx.accounts, &app.decks_cache).num_columns();
    let mut side_panel_action: Option<nav::SwitchingAction> = None;
    let mut responses = Vec::with_capacity(num_cols);

    let mut can_take_drag_from = Vec::new();

    StripBuilder::new(ui)
        .size(Size::exact(ui::side_panel::SIDE_PANEL_WIDTH))
        .sizes(sizes, num_cols)
        .clip(true)
        .horizontal(|mut strip| {
            strip.cell(|ui| {
                let rect = ui.available_rect_before_wrap();
                // Clone the route to avoid holding a borrow on app.decks_cache
                let current_route = get_active_columns(ctx.accounts, &app.decks_cache)
                    .selected()
                    .map(|col| col.router().top().clone());
                let side_panel = DesktopSidePanel::new(
                    ctx.accounts.get_selected_account(),
                    &app.decks_cache,
                    ctx.i18n,
                    ctx.ndb,
                    ctx.img_cache,
                    ctx.media_jobs.sender(),
                    current_route.as_ref(),
                    ctx.remote.relay_inspect(),
                )
                .show(ui);

                if let Some(side_panel) = side_panel {
                    if side_panel.response.clicked() || side_panel.response.secondary_clicked() {
                        if let Some(action) = DesktopSidePanel::perform_action(
                            &mut app.decks_cache,
                            ctx.accounts,
                            side_panel.action,
                            ctx.i18n,
                        ) {
                            side_panel_action = Some(action);
                        }
                    }
                }

                // debug
                /*
                ui.painter().rect(
                    rect,
                    0,
                    egui::Color32::RED,
                    egui::Stroke::new(1.0, egui::Color32::BLUE),
                    egui::StrokeKind::Inside,
                );
                */

                // vertical sidebar line
                ui.painter().vline(
                    rect.right(),
                    rect.y_range(),
                    ui.visuals().widgets.noninteractive.bg_stroke,
                );
            });

            app.hovered_column = None;

            for col_index in 0..num_cols {
                strip.cell(|ui| {
                    let rect = ui.available_rect_before_wrap();
                    let v_line_stroke = ui.visuals().widgets.noninteractive.bg_stroke;
                    let inner_rect = {
                        let mut inner = rect;
                        inner.set_right(rect.right() - v_line_stroke.width);
                        inner
                    };
                    let resp = nav::render_nav(col_index, inner_rect, app, ctx, ui);
                    can_take_drag_from.extend(resp.can_take_drag_from());
                    responses.push(resp);

                    // Track hovered column for mouse back/forward navigation
                    if ui.rect_contains_pointer(rect) {
                        app.hovered_column = Some(col_index);
                    }

                    // vertical line
                    ui.painter()
                        .vline(rect.right(), rect.y_range(), v_line_stroke);

                    // we need borrow ui context for processing, so proces
                    // responses in the last cell

                    if col_index == num_cols - 1 {}
                });

                //strip.cell(|ui| timeline::timeline_view(ui, app, timeline_ind));
            }
        });

    // process the side panel action after so we don't change the number of columns during
    // StripBuilder rendering
    let mut save_cols = false;
    if let Some(action) = side_panel_action {
        save_cols = save_cols || action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx);
    }

    let mut app_action: Option<AppAction> = None;

    for response in responses {
        let nav_result = response.process_render_nav_response(app, ctx, ui);

        if let Some(nr) = nav_result {
            match nr {
                ProcessNavResult::SwitchOccurred => save_cols = true,

                ProcessNavResult::PfpClicked => {
                    app_action = Some(AppAction::ToggleChrome);
                }

                ProcessNavResult::SwitchAccount(pubkey) => {
                    // Add as pubkey-only account if not already present
                    let kp = enostr::Keypair::only_pubkey(pubkey);
                    let _ = ctx.accounts.add_account(kp);

                    ctx.select_account(&pubkey);
                    setup_selected_account_timeline_subs(&mut app.timeline_cache, ctx);
                }

                ProcessNavResult::ExternalNoteAction(note_action) => {
                    app_action = Some(AppAction::Note(note_action));
                }
            }
        }
    }

    if app.options.contains(AppOptions::TmpColumns) {
        save_cols = false;
    }

    if save_cols {
        storage::save_decks_cache(ctx.path, &app.decks_cache);
    }

    AppResponse::action(app_action).drag(can_take_drag_from)
}

impl notedeck::App for Damus {
    #[profiling::function]
    fn update(&mut self, ctx: &mut AppContext<'_>, egui_ctx: &egui::Context) {
        update_damus(self, ctx, egui_ctx);
    }

    #[profiling::function]
    fn render(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> AppResponse {
        render_damus(self, ctx, ui)
    }

    fn kind_renderers(&self) -> Vec<Box<dyn notedeck::KindRenderer>> {
        vec![Box::new(crate::note_renderer::NoteKindRenderer)]
    }
}

pub fn get_active_columns<'a>(accounts: &Accounts, decks_cache: &'a DecksCache) -> &'a Columns {
    get_decks(accounts, decks_cache).active().columns()
}

pub fn get_decks<'a>(accounts: &Accounts, decks_cache: &'a DecksCache) -> &'a Decks {
    let key = accounts.selected_account_pubkey();
    decks_cache.decks(key)
}

pub fn get_active_columns_mut<'a>(
    i18n: &mut Localization,
    accounts: &Accounts,
    decks_cache: &'a mut DecksCache,
) -> &'a mut Columns {
    get_decks_mut(i18n, accounts, decks_cache)
        .active_mut()
        .columns_mut()
}

pub fn get_decks_mut<'a>(
    i18n: &mut Localization,
    accounts: &Accounts,
    decks_cache: &'a mut DecksCache,
) -> &'a mut Decks {
    decks_cache.decks_mut(i18n, accounts.selected_account_pubkey())
}

fn verify_new_zap_receipts(
    app_ctx: &mut AppContext<'_>,
    txn: &Transaction,
    new_note_keys: &[nostrdb::NoteKey],
) {
    for key in new_note_keys {
        if let Ok(note) = app_ctx.ndb.get_note_by_key(txn, *key) {
            if note.kind() == 9735 {
                app_ctx.zap_verifier.queue_verification(
                    app_ctx.ndb,
                    txn,
                    &note,
                    app_ctx.zaps.pay_cache(),
                );
            }
        }
    }
}

fn columns_to_decks_cache(i18n: &mut Localization, cols: Columns, key: &[u8; 32]) -> DecksCache {
    let mut account_to_decks: HashMap<Pubkey, Decks> = Default::default();
    let decks = Decks::new(crate::decks::Deck::new_with_columns(
        crate::decks::Deck::default_icon(),
        tr!(i18n, "My Deck", "Title for the user's deck"),
        cols,
    ));

    let account = Pubkey::new(*key);
    account_to_decks.insert(account, decks);
    DecksCache::new(account_to_decks, i18n)
}
