use crate::{
    accounts::{render_accounts_route, AccountsAction, AccountsResponse, AccountsRoute},
    app::{get_active_columns_mut, get_decks_mut, setup_selected_account_timeline_subs},
    column::ColumnsAction,
    deck_state::DeckState,
    decks::{Deck, DecksAction, DecksCache},
    options::AppOptions,
    profile::{ProfileAction, SaveProfileChanges},
    repost::RepostAction,
    route::{cleanup_popped_route, ColumnsRouter, Route, SingletonRouter},
    timeline::{
        route::{render_thread_route, render_timeline_route},
        TimelineCache,
    },
    ui::{
        self,
        add_column::render_add_column_routes,
        column::NavTitle,
        configure_deck::ConfigureDeckView,
        edit_deck::{EditDeckResponse, EditDeckView},
        note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType},
        profile::EditProfileView,
        repost::RepostDecisionView,
        search::{FocusState, SearchView},
        settings::SettingsAction,
        support::SupportView,
        wallet::{get_default_zap_state, WalletAction, WalletState, WalletView},
        RelayView, SettingsView,
    },
    Damus,
};

use egui_nav::{
    Nav, NavAction, NavResponse, NavUiType, PopupResponse, PopupSheet, RouteResponse, Split,
};
use enostr::ProfileState;
use nostrdb::{Filter, Ndb, Transaction};
use notedeck::{
    get_current_default_msats, nav::DragResponse, tr, ui::is_narrow, Accounts, AppContext,
    FilterState, NoteAction, NoteCache, NoteContext, NoteDetail, RelayAction,
};
use notedeck_ui::{ContactsListAction, ContactsListView, NoteOptions};
use tracing::error;

/// The result of processing a nav response
pub enum ProcessNavResult {
    SwitchOccurred,
    PfpClicked,
    SwitchAccount(enostr::Pubkey),
    /// A note action that should be forwarded to Chrome as an AppAction
    ExternalNoteAction(notedeck::NoteAction),
}

impl ProcessNavResult {
    pub fn switch_occurred(&self) -> bool {
        matches!(self, Self::SwitchOccurred)
    }
}

#[allow(clippy::enum_variant_names)]
pub enum RenderNavAction {
    Back,
    RemoveColumn,
    /// The response when the user interacts with a pfp in the nav header
    PfpClicked,
    PostAction(NewPostAction),
    NoteAction(NoteAction),
    ProfileAction(ProfileAction),
    SwitchingAction(SwitchingAction),
    WalletAction(WalletAction),
    RelayAction(RelayAction),
    SettingsAction(SettingsAction),
    RepostAction(RepostAction),
    ShowFollowing(enostr::Pubkey),
    ShowFollowers(enostr::Pubkey),
    RefreshTimeline(crate::timeline::TimelineKind),
}

pub enum SwitchingAction {
    Accounts(AccountsAction),
    Columns(ColumnsAction),
    Decks(crate::decks::DecksAction),
}

impl SwitchingAction {
    /// process the action, and return whether switching occured
    pub fn process(
        &self,
        timeline_cache: &mut TimelineCache,
        decks_cache: &mut DecksCache,
        ctx: &mut AppContext<'_>,
    ) -> bool {
        match &self {
            SwitchingAction::Accounts(account_action) => match account_action {
                AccountsAction::Switch(switch_action) => {
                    ctx.select_account(&switch_action.switch_to);

                    if switch_action.switching_to_new {
                        decks_cache.add_deck_default(ctx, timeline_cache, switch_action.switch_to);
                    }

                    setup_selected_account_timeline_subs(timeline_cache, ctx);

                    // pop nav after switch
                    get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache)
                        .column_mut(switch_action.source_column)
                        .router_mut()
                        .go_back();
                }
                AccountsAction::Remove(to_remove) => 's: {
                    if !ctx.remove_account(to_remove) {
                        break 's;
                    }

                    let mut scoped_subs = ctx.remote.scoped_subs(ctx.accounts);
                    decks_cache.remove(
                        ctx.i18n,
                        to_remove,
                        timeline_cache,
                        ctx.ndb,
                        &mut scoped_subs,
                    );
                }
            },
            SwitchingAction::Columns(columns_action) => match *columns_action {
                ColumnsAction::Remove(index) => {
                    let kinds_to_pop = get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache)
                        .delete_column(index);
                    for kind in &kinds_to_pop {
                        let mut scoped_subs = ctx.remote.scoped_subs(ctx.accounts);
                        if let Err(err) = timeline_cache.pop(kind, ctx.ndb, &mut scoped_subs) {
                            error!("error popping timeline: {err}");
                        }
                    }
                }

                ColumnsAction::Switch(from, to) => {
                    get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache).move_col(from, to);
                }
            },
            SwitchingAction::Decks(decks_action) => match *decks_action {
                DecksAction::Switch(index) => {
                    get_decks_mut(ctx.i18n, ctx.accounts, decks_cache).set_active(index)
                }
                DecksAction::Removing(index) => {
                    let mut scoped_subs = ctx.remote.scoped_subs(ctx.accounts);
                    get_decks_mut(ctx.i18n, ctx.accounts, decks_cache).remove_deck(
                        index,
                        timeline_cache,
                        ctx.ndb,
                        &mut scoped_subs,
                    );
                }
            },
        }
        true
    }
}

impl From<PostAction> for RenderNavAction {
    fn from(post_action: PostAction) -> Self {
        match post_action {
            PostAction::QuotedNoteAction(note_action) => Self::NoteAction(note_action),
            PostAction::NewPostAction(new_post) => Self::PostAction(new_post),
        }
    }
}

impl From<NewPostAction> for RenderNavAction {
    fn from(post_action: NewPostAction) -> Self {
        Self::PostAction(post_action)
    }
}

impl From<NoteAction> for RenderNavAction {
    fn from(note_action: NoteAction) -> RenderNavAction {
        Self::NoteAction(note_action)
    }
}

enum NotedeckNavResponse {
    Popup(Box<PopupResponse<Option<RenderNavAction>>>),
    Nav(Box<NavResponse<Option<RenderNavAction>>>),
}

pub struct RenderNavResponse {
    column: usize,
    response: NotedeckNavResponse,
}

impl RenderNavResponse {
    #[allow(private_interfaces)]
    pub fn new(column: usize, response: NotedeckNavResponse) -> Self {
        RenderNavResponse { column, response }
    }

    pub fn can_take_drag_from(&self) -> Vec<egui::Id> {
        match &self.response {
            NotedeckNavResponse::Popup(_) => Vec::new(), // TODO(kernelkind): upgrade once popup supports drag ids
            NotedeckNavResponse::Nav(nav_response) => nav_response.can_take_drag_from.clone(),
        }
    }

    #[must_use = "Make sure to save columns if result is true"]
    #[profiling::function]
    pub fn process_render_nav_response(
        self,
        app: &mut Damus,
        ctx: &mut AppContext<'_>,
        ui: &mut egui::Ui,
    ) -> Option<ProcessNavResult> {
        match self.response {
            NotedeckNavResponse::Popup(nav_action) => {
                process_popup_resp(*nav_action, app, ctx, ui, self.column)
            }
            NotedeckNavResponse::Nav(nav_response) => {
                process_nav_resp(app, ctx, ui, *nav_response, self.column)
            }
        }
    }
}

fn process_popup_resp(
    action: PopupResponse<Option<RenderNavAction>>,
    app: &mut Damus,
    ctx: &mut AppContext<'_>,
    ui: &mut egui::Ui,
    col: usize,
) -> Option<ProcessNavResult> {
    let mut process_result: Option<ProcessNavResult> = None;
    if let Some(nav_action) = action.response {
        process_result = process_render_nav_action(app, ctx, ui, col, nav_action);
    }

    if let Some(NavAction::Returned(_)) = action.action {
        let column = app.columns_mut(ctx.i18n, ctx.accounts).column_mut(col);
        if let Some(after_action) = column.sheet_router.after_action.clone() {
            column.router_mut().route_to(after_action);
        }
        column.sheet_router.clear();
    } else if let Some(NavAction::Navigating) = action.action {
        let column = app.columns_mut(ctx.i18n, ctx.accounts).column_mut(col);
        column.sheet_router.navigating = false;
    }

    process_result
}

#[profiling::function]
fn process_nav_resp(
    app: &mut Damus,
    ctx: &mut AppContext<'_>,
    ui: &mut egui::Ui,
    response: NavResponse<Option<RenderNavAction>>,
    col: usize,
) -> Option<ProcessNavResult> {
    let mut process_result: Option<ProcessNavResult> = None;

    if let Some(action) = response.response.or(response.title_response) {
        // start returning when we're finished posting

        process_result = process_render_nav_action(app, ctx, ui, col, action);
    }

    if let Some(action) = response.action {
        match action {
            NavAction::Returned(return_type) => {
                //ctx.sound.play(notedeck::SoundEffect::Closed);

                let r = app
                    .columns_mut(ctx.i18n, ctx.accounts)
                    .column_mut(col)
                    .router_mut()
                    .pop();

                // Clean up resources for the popped route
                if let Some(route) = &r {
                    cleanup_popped_route(
                        route,
                        &mut app.timeline_cache,
                        &mut app.threads,
                        &mut app.onboarding,
                        &mut app.view_state,
                        ctx.ndb,
                        &mut ctx.remote.scoped_subs(ctx.accounts),
                        return_type,
                        col,
                    );
                }

                process_result = Some(ProcessNavResult::SwitchOccurred);
            }

            NavAction::Navigated => {
                handle_navigating_edit_profile(ctx.ndb, ctx.accounts, app, col);
                {
                    let mut scoped_subs = ctx.remote.scoped_subs(ctx.accounts);
                    handle_navigating_timeline(
                        ctx.ndb,
                        ctx.note_cache,
                        &mut scoped_subs,
                        ctx.accounts,
                        app,
                        col,
                    );
                }

                let cur_router = app
                    .columns_mut(ctx.i18n, ctx.accounts)
                    .column_mut(col)
                    .router_mut();
                cur_router.navigating_mut(false);
                if cur_router.is_replacing() {
                    cur_router.remove_previous_routes();
                }

                process_result = Some(ProcessNavResult::SwitchOccurred);
            }

            NavAction::Dragging => {}
            NavAction::Returning(_) => {}
            NavAction::Resetting => {}
            NavAction::Navigating => {
                // since we are navigating, we should set this column as
                // the selected one
                app.columns_mut(ctx.i18n, ctx.accounts)
                    .select_column(col as i32);

                handle_navigating_edit_profile(ctx.ndb, ctx.accounts, app, col);
                {
                    let mut scoped_subs = ctx.remote.scoped_subs(ctx.accounts);
                    handle_navigating_timeline(
                        ctx.ndb,
                        ctx.note_cache,
                        &mut scoped_subs,
                        ctx.accounts,
                        app,
                        col,
                    );
                }
            }
        }
    }

    process_result
}

/// We are navigating to edit profile, prepare the profile state
/// if we don't have it
fn handle_navigating_edit_profile(ndb: &Ndb, accounts: &Accounts, app: &mut Damus, col: usize) {
    let pk = {
        let Route::EditProfile(pk) = app.columns(accounts).column(col).router().top() else {
            return;
        };

        if app.view_state.pubkey_to_profile_state.contains_key(pk) {
            return;
        }

        pk.to_owned()
    };

    let txn = Transaction::new(ndb).expect("txn");
    app.view_state.pubkey_to_profile_state.insert(pk, {
        let filter = Filter::new_with_capacity(1)
            .kinds([0])
            .authors([pk.bytes()])
            .build();

        if let Ok(results) = ndb.query(&txn, &[filter], 1) {
            if let Some(result) = results.first() {
                tracing::debug!(
                    "refreshing profile state for edit view: {}",
                    result.note.content()
                );
                ProfileState::from_note_contents(result.note.content())
            } else {
                ProfileState::default()
            }
        } else {
            ProfileState::default()
        }
    });
}

fn handle_navigating_timeline(
    ndb: &Ndb,
    note_cache: &mut NoteCache,
    scoped_subs: &mut notedeck::ScopedSubApi<'_, '_>,
    accounts: &Accounts,
    app: &mut Damus,
    col: usize,
) {
    let account_pk = accounts.selected_account_pubkey();
    let kind = {
        let Route::Timeline(kind) = app.columns(accounts).column(col).router().top() else {
            return;
        };

        if let Some(timeline) = app.timeline_cache.get(kind) {
            if timeline.subscription.dependers(account_pk) > 0 {
                return;
            }
        }

        kind.to_owned()
    };

    let txn = Transaction::new(ndb).expect("txn");
    app.timeline_cache.open(
        ndb,
        note_cache,
        &txn,
        scoped_subs,
        &kind,
        *account_pk,
        false,
    );
}

pub enum RouterAction {
    GoBack,
    /// We clicked on a pfp in a route. We currently don't carry any
    /// information about the pfp since we only use it for toggling the
    /// chrome atm
    PfpClicked,
    RouteTo(Route, RouterType),
    CloseSheetThenRoute(Route),
    Overlay {
        route: Route,
        make_new: bool,
    },
    SwitchAccount(enostr::Pubkey),
}

pub enum RouterType {
    Sheet(Split),
    Stack,
}

fn go_back(stack: &mut ColumnsRouter<Route>, sheet: &mut SingletonRouter<Route>) {
    if sheet.route().is_some() {
        sheet.go_back();
    } else {
        stack.go_back();
    }
}

impl RouterAction {
    pub fn process_router_action(
        self,
        stack_router: &mut ColumnsRouter<Route>,
        sheet_router: &mut SingletonRouter<Route>,
        sound: &notedeck::SoundManager,
    ) -> Option<ProcessNavResult> {
        match self {
            RouterAction::GoBack => {
                go_back(stack_router, sheet_router);

                None
            }

            RouterAction::PfpClicked => {
                if stack_router.routes().len() == 1 {
                    // if we're at the top level and we click a profile pic,
                    // bubble it up so that it can be handled by the chrome
                    // to open the sidebar
                    Some(ProcessNavResult::PfpClicked)
                } else {
                    // Otherwise just execute a back action
                    go_back(stack_router, sheet_router);

                    None
                }
            }

            RouterAction::RouteTo(route, router_type) => {
                sound.play(notedeck::SoundEffect::Opened);
                match router_type {
                    RouterType::Sheet(percent) => {
                        sheet_router.route_to(route, percent);
                        None
                    }
                    RouterType::Stack => {
                        stack_router.route_to(route);
                        None
                    }
                }
            }
            RouterAction::Overlay { route, make_new } => {
                sound.play(notedeck::SoundEffect::Opened);
                if make_new {
                    stack_router.route_to_overlaid_new(route);
                } else {
                    stack_router.route_to_overlaid(route);
                }
                None
            }
            RouterAction::CloseSheetThenRoute(route) => {
                sheet_router.go_back();
                sheet_router.after_action = Some(route);
                None
            }
            RouterAction::SwitchAccount(pubkey) => Some(ProcessNavResult::SwitchAccount(pubkey)),
        }
    }

    pub fn route_to(route: Route) -> Self {
        RouterAction::RouteTo(route, RouterType::Stack)
    }

    pub fn route_to_sheet(route: Route, split: Split) -> Self {
        RouterAction::RouteTo(route, RouterType::Sheet(split))
    }
}

#[profiling::function]
fn process_render_nav_action(
    app: &mut Damus,
    ctx: &mut AppContext<'_>,
    ui: &mut egui::Ui,
    col: usize,
    action: RenderNavAction,
) -> Option<ProcessNavResult> {
    let router_action = match action {
        RenderNavAction::Back => Some(RouterAction::GoBack),
        RenderNavAction::PfpClicked => Some(RouterAction::PfpClicked),
        RenderNavAction::RemoveColumn => {
            let kinds_to_pop = app.columns_mut(ctx.i18n, ctx.accounts).delete_column(col);

            for kind in &kinds_to_pop {
                let mut scoped_subs = ctx.remote.scoped_subs(ctx.accounts);
                if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, &mut scoped_subs) {
                    error!("error popping timeline: {err}");
                }
            }

            return Some(ProcessNavResult::SwitchOccurred);
        }
        RenderNavAction::PostAction(new_post_action) => {
            let txn = Transaction::new(ctx.ndb).expect("txn");
            let mut publisher = ctx.remote.publisher(ctx.accounts);
            match new_post_action.execute(ctx.ndb, &txn, &mut publisher, &mut app.drafts, col) {
                Err(err) => tracing::error!("Error executing post action: {err}"),
                Ok(_) => tracing::debug!("Post action executed"),
            }

            Some(RouterAction::GoBack)
        }
        RenderNavAction::NoteAction(note_action) => {
            // SummarizeThread is handled by Chrome/Dave, not Columns
            if let notedeck::NoteAction::Context(ref ctx_sel) = note_action {
                if matches!(
                    ctx_sel.action,
                    notedeck::NoteContextSelection::SummarizeThread(_)
                ) {
                    return Some(ProcessNavResult::ExternalNoteAction(note_action));
                }
            }

            let txn = Transaction::new(ctx.ndb).expect("txn");

            crate::actionbar::execute_and_process_note_action(
                note_action,
                ctx.ndb,
                get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
                col,
                &mut app.timeline_cache,
                &mut app.threads,
                ctx.note_cache,
                &mut ctx.remote,
                &txn,
                ctx.unknown_ids,
                ctx.accounts,
                ctx.global_wallet,
                ctx.zaps,
                ctx.img_cache,
                &mut app.view_state,
                ctx.media_jobs.sender(),
                ui,
            )
        }
        RenderNavAction::SwitchingAction(switching_action) => {
            if switching_action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx) {
                return Some(ProcessNavResult::SwitchOccurred);
            } else {
                return None;
            }
        }
        RenderNavAction::ProfileAction(profile_action) => profile_action.process_profile_action(
            app,
            ctx.path,
            ctx.i18n,
            ui.ctx(),
            ctx.ndb,
            &mut ctx.remote,
            ctx.accounts,
        ),
        RenderNavAction::WalletAction(wallet_action) => {
            wallet_action.process(ctx.accounts, ctx.global_wallet)
        }
        RenderNavAction::RelayAction(action) => {
            ctx.process_relay_action(action);
            None
        }
        RenderNavAction::SettingsAction(action) => {
            action.process_settings_action(app, ctx, ui.ctx())
        }
        RenderNavAction::RepostAction(action) => action.process(
            ctx.ndb,
            &ctx.accounts.get_selected_account().key,
            ctx.accounts,
            &mut ctx.remote,
        ),
        RenderNavAction::ShowFollowing(pubkey) => Some(RouterAction::RouteTo(
            crate::route::Route::Following(pubkey),
            RouterType::Stack,
        )),
        RenderNavAction::ShowFollowers(pubkey) => Some(RouterAction::RouteTo(
            crate::route::Route::FollowedBy(pubkey),
            RouterType::Stack,
        )),
        RenderNavAction::RefreshTimeline(kind) => {
            let txn = Transaction::new(ctx.ndb).expect("txn");

            // Regenerate the filter (runs new reservoir sampling for algo feeds)
            let new_filter = kind.filters(&txn, ctx.ndb);

            if let Some(timeline) = app.timeline_cache.get_mut(&kind) {
                // Clear existing notes
                timeline.reset_views();
                // Reset load state so the timeline loader picks it up
                timeline.initial_load = crate::timeline::InitialLoadState::Pending;

                // Apply the new filter and update the remote subscription
                if let FilterState::Ready(ref filter) = new_filter {
                    let mut scoped_subs = ctx.remote.scoped_subs(ctx.accounts);
                    crate::timeline::update_remote_timeline_subscription(
                        timeline,
                        filter.remote().to_vec(),
                        &mut scoped_subs,
                    );
                }
                timeline.filter = new_filter;
            }
            None
        }
    };

    if let Some(action) = router_action {
        let cols =
            get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache).column_mut(col);
        let router = &mut cols.router;
        let sheet_router = &mut cols.sheet_router;

        action.process_router_action(router, sheet_router, ctx.sound)
    } else {
        None
    }
}

/// Extract the sender pubkey from a zap receipt (kind 9735).
/// The sender is the author of the zap request (kind 9734) embedded
/// in the receipt's "description" tag, not the receipt's own pubkey
/// (which is the LNURL server).
fn zap_sender_pubkey(zap_receipt: &nostrdb::Note<'_>) -> Option<enostr::Pubkey> {
    for tag in zap_receipt.tags() {
        if tag.count() >= 2 && tag.get_str(0) == Some("description") {
            let desc = tag.get_str(1)?;
            let zap_req = enostr::Note::from_json(desc).ok()?;
            return Some(zap_req.pubkey);
        }
    }
    None
}

/// Check if a zap receipt has been verified via nostrdb metadata flags.
/// NDB_NOTE_META_FLAG_ZAP_VERIFIED = 4
fn is_zap_verified(ndb: &Ndb, txn: &Transaction, note_id: &[u8; 32]) -> bool {
    if let Ok(mut meta) = ndb.get_note_metadata(txn, note_id) {
        *meta.flags() & 4 != 0
    } else {
        false
    }
}

/// Cached zap detail list split by verification status.
#[derive(Clone)]
struct ZapDetailCache {
    verified: Vec<enostr::Pubkey>,
    unverified: Vec<enostr::Pubkey>,
}

fn note_detail_ui(
    ui: &mut egui::Ui,
    ndb: &Ndb,
    detail: &NoteDetail,
    note_context: &mut NoteContext<'_>,
) -> DragResponse<RenderNavAction> {
    let note_id = match detail {
        NoteDetail::Reactions(id) => id,
        NoteDetail::Reposts(id) => id,
        NoteDetail::Zaps(id) => id,
    };

    if matches!(detail, NoteDetail::Zaps(_)) {
        return zap_detail_ui(ui, ndb, note_id, note_context);
    }

    let cache_id = egui::Id::new(("note_detail_cache", note_id, std::mem::discriminant(detail)));

    let contacts = ui
        .ctx()
        .data_mut(|d| d.get_temp::<Vec<enostr::Pubkey>>(cache_id));

    let (txn, contacts) = if let Some(cached) = contacts {
        let txn = nostrdb::Transaction::new(ndb).expect("txn");
        (txn, cached)
    } else {
        let txn = nostrdb::Transaction::new(ndb).expect("txn");

        let kinds: &[u64] = match detail {
            NoteDetail::Reactions(_) => &[7],
            NoteDetail::Reposts(_) => &[6, 16],
            NoteDetail::Zaps(_) => unreachable!(),
        };

        let filter = nostrdb::Filter::new()
            .kinds(kinds.iter().copied())
            .event(note_id.bytes())
            .limit(500)
            .build();

        let mut seen = std::collections::HashSet::new();
        let mut contacts = vec![];

        if let Ok(results) = ndb.query(&txn, &[filter], 500) {
            for result in &results {
                let pk = enostr::Pubkey::new(*result.note.pubkey());
                if seen.insert(pk) {
                    contacts.push(pk);
                }
            }
        }

        ui.ctx()
            .data_mut(|d| d.insert_temp(cache_id, contacts.clone()));
        (txn, contacts)
    };

    ContactsListView::new(
        &contacts,
        note_context.jobs,
        note_context.ndb,
        note_context.img_cache,
        &txn,
        note_context.i18n,
    )
    .ui(ui)
    .map_output(|action| match action {
        ContactsListAction::Select(pk) => RenderNavAction::NoteAction(NoteAction::Profile(pk)),
    })
}

fn zap_detail_ui(
    ui: &mut egui::Ui,
    ndb: &Ndb,
    note_id: &enostr::NoteId,
    note_context: &mut NoteContext<'_>,
) -> DragResponse<RenderNavAction> {
    let cache_id = egui::Id::new(("zap_detail_cache", note_id));

    let cached = ui
        .ctx()
        .data_mut(|d| d.get_temp::<ZapDetailCache>(cache_id));

    let (txn, zap_cache) = if let Some(cached) = cached {
        let txn = nostrdb::Transaction::new(ndb).expect("txn");
        (txn, cached)
    } else {
        let txn = nostrdb::Transaction::new(ndb).expect("txn");

        let filter = nostrdb::Filter::new()
            .kinds([9735])
            .event(note_id.bytes())
            .limit(500)
            .build();

        let mut seen = std::collections::HashSet::new();
        let mut verified = vec![];
        let mut unverified = vec![];

        if let Ok(results) = ndb.query(&txn, &[filter], 500) {
            for result in &results {
                if let Some(pk) = zap_sender_pubkey(&result.note) {
                    if seen.insert(pk) {
                        if is_zap_verified(ndb, &txn, result.note.id()) {
                            verified.push(pk);
                        } else {
                            unverified.push(pk);
                        }
                    }
                }
            }
        }

        let zap_cache = ZapDetailCache {
            verified,
            unverified,
        };

        ui.ctx()
            .data_mut(|d| d.insert_temp(cache_id, zap_cache.clone()));
        (txn, zap_cache)
    };

    let mut action = None;

    egui::ScrollArea::vertical().show(ui, |ui| {
        for pk in &zap_cache.verified {
            let profile = ndb.get_profile_by_pubkey(&txn, pk.bytes()).ok();
            if notedeck_ui::profile_row(
                ui,
                profile.as_ref(),
                false,
                note_context.img_cache,
                note_context.jobs,
                note_context.i18n,
            ) {
                action = Some(RenderNavAction::NoteAction(NoteAction::Profile(*pk)));
            }
        }

        if !zap_cache.unverified.is_empty() {
            ui.add_space(8.0);
            ui.separator();
            ui.add_space(4.0);
            ui.label(
                egui::RichText::new("Unverified")
                    .size(13.0)
                    .color(ui.visuals().weak_text_color()),
            );
            ui.add_space(4.0);

            for pk in &zap_cache.unverified {
                let profile = ndb.get_profile_by_pubkey(&txn, pk.bytes()).ok();
                if notedeck_ui::profile_row(
                    ui,
                    profile.as_ref(),
                    false,
                    note_context.img_cache,
                    note_context.jobs,
                    note_context.i18n,
                ) {
                    action = Some(RenderNavAction::NoteAction(NoteAction::Profile(*pk)));
                }
            }
        }
    });

    DragResponse::output(action)
}

fn render_nav_body(
    ui: &mut egui::Ui,
    app: &mut Damus,
    ctx: &mut AppContext,
    top: &Route,
    depth: usize,
    col: usize,
    inner_rect: egui::Rect,
) -> DragResponse<RenderNavAction> {
    let mut note_context = NoteContext {
        ndb: ctx.ndb,
        accounts: ctx.accounts,
        img_cache: ctx.img_cache,
        note_cache: ctx.note_cache,
        zaps: ctx.zaps,
        jobs: ctx.media_jobs.sender(),
        unknown_ids: ctx.unknown_ids,
        nip05_cache: ctx.nip05_cache,
        clipboard: ctx.clipboard,
        i18n: ctx.i18n,
        global_wallet: ctx.global_wallet,
        sound: ctx.sound,
        registries: ctx.registries,
        app_actions: ctx.app_actions,
    };
    match top {
        Route::Timeline(kind) => {
            // did something request scroll to top for the selection column?
            let scroll_to_top = app
                .decks_cache
                .selected_column_index(ctx.accounts)
                .is_some_and(|ind| ind == col)
                && app.options.contains(AppOptions::ScrollToTop);

            let resp = render_timeline_route(
                &mut app.timeline_cache,
                kind,
                col,
                app.note_options,
                depth,
                ui,
                &mut note_context,
                scroll_to_top,
            );

            app.timeline_cache.set_fresh(kind);

            // always clear the scroll_to_top request
            if scroll_to_top {
                app.options.remove(AppOptions::ScrollToTop);
            }

            resp
        }
        Route::Thread(selection) => render_thread_route(
            &mut app.threads,
            selection,
            col,
            app.note_options,
            ui,
            &mut note_context,
        ),
        Route::Accounts(amr) => {
            let resp = render_accounts_route(
                ui,
                ctx,
                &mut app.view_state.login,
                &mut app.onboarding,
                &mut app.view_state.follow_packs,
                *amr,
            );

            resp.map_output_maybe(|action| match action {
                AccountsResponse::ViewProfile(pubkey) => {
                    Some(RenderNavAction::NoteAction(NoteAction::Profile(pubkey)))
                }
                AccountsResponse::Account(accounts_route_response) => {
                    let mut action = accounts_route_response.process(ctx, app, col);

                    let txn = Transaction::new(ctx.ndb).expect("txn");
                    action.process_action(ctx.unknown_ids, ctx.ndb, &txn);
                    action
                        .accounts_action
                        .map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f)))
                }
            })
        }
        Route::Relays => RelayView::new(
            ctx.remote.relay_inspect(),
            ctx.accounts.selected_account_advertised_relays(),
            ctx.accounts.selected_account_private_relay_set(),
            &mut app.view_state.id_string_map,
            ctx.i18n,
        )
        .ui(ui)
        .map_output(RenderNavAction::RelayAction),

        Route::Settings => {
            let db_path = ctx.args.db_path(ctx.path);
            SettingsView::new(
                ctx.settings.get_settings_mut(),
                &mut note_context,
                &db_path,
                &mut app.view_state.compact,
            )
            .ui(ui)
            .map_output(RenderNavAction::SettingsAction)
        }

        Route::Reply(id) => {
            let txn = if let Ok(txn) = Transaction::new(ctx.ndb) {
                txn
            } else {
                ui.label(tr!(
                    note_context.i18n,
                    "Reply to unknown note",
                    "Error message when reply note cannot be found"
                ));
                return DragResponse::none();
            };

            let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) {
                note
            } else {
                ui.label(tr!(
                    note_context.i18n,
                    "Reply to unknown note",
                    "Error message when reply note cannot be found"
                ));
                return DragResponse::none();
            };

            let Some(poster) = ctx.accounts.selected_filled() else {
                return DragResponse::none();
            };

            let resp = {
                let draft = app.drafts.reply_mut(note.id());

                let mut options = app.note_options;
                options.set(NoteOptions::Wide, false);

                ui::PostReplyView::new(
                    &mut note_context,
                    poster,
                    draft,
                    &note,
                    inner_rect,
                    options,
                    col,
                )
                .show(ui)
            };

            resp.map_output_maybe(|o| Some(o.action?.into()))
        }
        Route::Quote(id) => {
            let txn = Transaction::new(ctx.ndb).expect("txn");

            let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) {
                note
            } else {
                ui.label(tr!(
                    note_context.i18n,
                    "Quote of unknown note",
                    "Error message when quote note cannot be found"
                ));
                return DragResponse::none();
            };

            let Some(poster) = ctx.accounts.selected_filled() else {
                return DragResponse::none();
            };

            let draft = app.drafts.quote_mut(note.id());

            let response = crate::ui::note::QuoteRepostView::new(
                &mut note_context,
                poster,
                draft,
                &note,
                inner_rect,
                app.note_options,
                col,
            )
            .show(ui);

            response.map_output_maybe(|o| Some(o.action?.into()))
        }
        Route::ComposeNote => {
            let Some(kp) = ctx.accounts.get_selected_account().key.to_full() else {
                return DragResponse::none();
            };
            let navigating =
                get_active_columns_mut(note_context.i18n, ctx.accounts, &mut app.decks_cache)
                    .column(col)
                    .router()
                    .navigating();
            let draft = app.drafts.compose_mut(col);

            if navigating {
                draft.focus_state = FocusState::Navigating
            } else if draft.focus_state == FocusState::Navigating {
                draft.focus_state = FocusState::ShouldRequestFocus;
            }

            let txn = Transaction::new(ctx.ndb).expect("txn");
            let post_response = ui::PostView::new(
                &mut note_context,
                draft,
                PostType::New,
                kp,
                inner_rect,
                app.note_options,
            )
            .ui(&txn, ui);

            post_response.map_output_maybe(|o| Some(o.action?.into()))
        }
        Route::AddColumn(route) => {
            render_add_column_routes(ui, app, ctx, col, route);

            DragResponse::none()
        }
        Route::Support => {
            app.support.refresh();
            SupportView::new(&mut app.support, ctx.i18n).show(ui);
            DragResponse::none()
        }
        Route::Search => {
            let id = ui.id().with(("search", depth, col));
            let navigating =
                get_active_columns_mut(note_context.i18n, ctx.accounts, &mut app.decks_cache)
                    .column(col)
                    .router()
                    .navigating();
            let search_buffer = app.view_state.searches.entry(id).or_default();
            let txn = Transaction::new(ctx.ndb).expect("txn");

            if navigating {
                search_buffer.focus_state = FocusState::Navigating
            } else if search_buffer.focus_state == FocusState::Navigating {
                // we're not navigating but our last search buffer state
                // says we were navigating. This means that navigating has
                // stopped. Let's make sure to focus the input field
                search_buffer.focus_state = FocusState::ShouldRequestFocus;
                tracing::debug!("requesting search focus");
            }

            SearchView::new(&txn, app.note_options, search_buffer, &mut note_context)
                .show(ui)
                .map_output(RenderNavAction::NoteAction)
        }
        Route::NewDeck => {
            let id = ui.id().with("new-deck");
            let new_deck_state = app.view_state.id_to_deck_state.entry(id).or_default();
            let mut resp = None;
            if let Some(config_resp) = ConfigureDeckView::new(new_deck_state, ctx.i18n).ui(ui) {
                let cur_acc = ctx.accounts.selected_account_pubkey();
                app.decks_cache
                    .add_deck(*cur_acc, Deck::new(config_resp.icon, config_resp.name));

                // set new deck as active
                let cur_index = get_decks_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
                    .decks()
                    .len()
                    - 1;
                resp = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks(
                    DecksAction::Switch(cur_index),
                )));

                new_deck_state.clear();
                get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
                    .get_selected_router()
                    .go_back();
            }

            DragResponse::output(resp)
        }
        Route::EditDeck(index) => {
            let mut action = None;
            let cur_deck = get_decks_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
                .decks_mut()
                .get_mut(*index)
                .expect("index wasn't valid");
            let id = ui
                .id()
                .with(("edit-deck", ctx.accounts.selected_account_pubkey(), index));
            let deck_state = app
                .view_state
                .id_to_deck_state
                .entry(id)
                .or_insert_with(|| DeckState::from_deck(cur_deck));
            if let Some(resp) = EditDeckView::new(deck_state, ctx.i18n).ui(ui) {
                match resp {
                    EditDeckResponse::Edit(configure_deck_response) => {
                        cur_deck.edit(configure_deck_response);
                    }
                    EditDeckResponse::Delete => {
                        action = Some(RenderNavAction::SwitchingAction(SwitchingAction::Decks(
                            DecksAction::Removing(*index),
                        )));
                    }
                }
                get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
                    .get_selected_router()
                    .go_back();
            }

            DragResponse::output(action)
        }
        Route::EditProfile(pubkey) => {
            let Some(kp) = ctx.accounts.get_full(pubkey) else {
                error!("Pubkey in EditProfile route did not have an nsec attached in Accounts");
                return DragResponse::none();
            };

            let Some(state) = app.view_state.pubkey_to_profile_state.get_mut(kp.pubkey) else {
                tracing::error!(
                    "No profile state when navigating to EditProfile... was handle_navigating_edit_profile not called?"
                );
                return DragResponse::none();
            };

            EditProfileView::new(
                ctx.i18n,
                state,
                ctx.img_cache,
                ctx.clipboard,
                ctx.media_jobs.sender(),
            )
            .ui(ui)
            .map_output_maybe(|save| {
                if save {
                    app.view_state
                        .pubkey_to_profile_state
                        .get(kp.pubkey)
                        .map(|state| {
                            RenderNavAction::ProfileAction(ProfileAction::SaveChanges(
                                SaveProfileChanges::new(kp.to_full(), state.clone()),
                            ))
                        })
                } else {
                    None
                }
            })
        }
        Route::Following(pubkey) => {
            let cache_id = egui::Id::new(("following_contacts_cache", pubkey));

            let contacts = ui
                .ctx()
                .data_mut(|d| d.get_temp::<Vec<enostr::Pubkey>>(cache_id));

            let (txn, contacts) = if let Some(cached) = contacts {
                let txn = nostrdb::Transaction::new(ctx.ndb).expect("txn");
                (txn, cached)
            } else {
                let txn = nostrdb::Transaction::new(ctx.ndb).expect("txn");
                let filter = nostrdb::Filter::new()
                    .authors([pubkey.bytes()])
                    .kinds([3])
                    .limit(1)
                    .build();

                let mut contacts = vec![];
                if let Ok(results) = ctx.ndb.query(&txn, &[filter], 1) {
                    if let Some(result) = results.first() {
                        for tag in result.note.tags() {
                            if tag.count() >= 2 {
                                if let Some("p") = tag.get_str(0) {
                                    if let Some(pk_bytes) = tag.get_id(1) {
                                        contacts.push(enostr::Pubkey::new(*pk_bytes));
                                    }
                                }
                            }
                        }
                    }
                }

                contacts.sort_by_cached_key(|pk| {
                    ctx.ndb
                        .get_profile_by_pubkey(&txn, pk.bytes())
                        .ok()
                        .and_then(|p| {
                            notedeck::name::get_display_name(Some(&p))
                                .display_name
                                .map(|s| s.to_lowercase())
                        })
                        .unwrap_or_else(|| "zzz".to_string())
                });

                ui.ctx()
                    .data_mut(|d| d.insert_temp(cache_id, contacts.clone()));
                (txn, contacts)
            };

            ContactsListView::new(
                &contacts,
                note_context.jobs,
                note_context.ndb,
                note_context.img_cache,
                &txn,
                note_context.i18n,
            )
            .ui(ui)
            .map_output(|action| match action {
                ContactsListAction::Select(pk) => {
                    RenderNavAction::NoteAction(NoteAction::Profile(pk))
                }
            })
        }
        Route::FollowedBy(_pubkey) => DragResponse::none(),
        Route::NoteDetails(detail) => note_detail_ui(ui, ctx.ndb, detail, &mut note_context),
        Route::TosAcceptance => {
            let resp = ui::tos::TosAcceptanceView::new(
                ctx.i18n,
                &mut app.view_state.tos_age_confirmed,
                &mut app.view_state.tos_confirmed,
            )
            .show(ui);

            if let Some(ui::tos::TosAcceptanceResponse::Accept) = resp {
                ctx.settings.accept_tos();
                app.view_state.tos_age_confirmed = false;
                app.view_state.tos_confirmed = false;
                return DragResponse::output(Some(RenderNavAction::Back));
            }

            DragResponse::none()
        }
        Route::Welcome => {
            let resp = ui::welcome::WelcomeView::new(ctx.i18n).show(ui);

            if let Some(welcome_resp) = resp {
                ctx.settings.complete_welcome();
                match welcome_resp {
                    ui::welcome::WelcomeResponse::CreateAccount => {
                        app.columns_mut(ctx.i18n, ctx.accounts)
                            .column_mut(col)
                            .router_mut()
                            .route_to(Route::Accounts(AccountsRoute::Onboarding));
                    }
                    ui::welcome::WelcomeResponse::Login => {
                        app.columns_mut(ctx.i18n, ctx.accounts)
                            .column_mut(col)
                            .router_mut()
                            .route_to(Route::Accounts(AccountsRoute::AddAccount));
                    }
                    ui::welcome::WelcomeResponse::Browse => {
                        return DragResponse::output(Some(RenderNavAction::Back));
                    }
                }
            }

            DragResponse::none()
        }
        Route::Wallet(wallet_type) => {
            let state = match wallet_type {
                notedeck::WalletType::Auto => 's: {
                    if let Some(cur_acc_wallet) = ctx.accounts.get_selected_wallet_mut() {
                        let default_zap_state =
                            get_default_zap_state(&mut cur_acc_wallet.default_zap);
                        break 's WalletState::Wallet {
                            wallet: &mut cur_acc_wallet.wallet,
                            default_zap_state,
                            can_create_local_wallet: false,
                        };
                    }

                    let Some(wallet) = &mut ctx.global_wallet.wallet else {
                        break 's WalletState::NoWallet {
                            state: &mut ctx.global_wallet.ui_state,
                            show_local_only: true,
                        };
                    };

                    let default_zap_state = get_default_zap_state(&mut wallet.default_zap);
                    WalletState::Wallet {
                        wallet: &mut wallet.wallet,
                        default_zap_state,
                        can_create_local_wallet: true,
                    }
                }
                notedeck::WalletType::Local => 's: {
                    let cur_acc = ctx.accounts.get_selected_wallet_mut();
                    let Some(wallet) = cur_acc else {
                        break 's WalletState::NoWallet {
                            state: &mut ctx.global_wallet.ui_state,
                            show_local_only: false,
                        };
                    };

                    let default_zap_state = get_default_zap_state(&mut wallet.default_zap);
                    WalletState::Wallet {
                        wallet: &mut wallet.wallet,
                        default_zap_state,
                        can_create_local_wallet: false,
                    }
                }
            };

            DragResponse::output(WalletView::new(state, ctx.i18n, ctx.clipboard).ui(ui))
                .map_output(RenderNavAction::WalletAction)
        }
        Route::CustomizeZapAmount(target) => {
            let txn = Transaction::new(ctx.ndb).expect("txn");
            let default_msats = get_current_default_msats(ctx.accounts, ctx.global_wallet);
            DragResponse::output(
                CustomZapView::new(
                    ctx.i18n,
                    ctx.img_cache,
                    ctx.ndb,
                    &txn,
                    &target.zap_recipient,
                    default_msats,
                    ctx.media_jobs.sender(),
                )
                .ui(ui),
            )
            .map_output(|msats| {
                get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
                    .column_mut(col)
                    .router_mut()
                    .go_back();
                RenderNavAction::NoteAction(NoteAction::Zap(notedeck::ZapAction::Send(
                    notedeck::note::ZapTargetAmount {
                        target: target.clone(),
                        specified_msats: Some(msats),
                    },
                )))
            })
        }
        Route::RepostDecision(note_id) => {
            DragResponse::output(RepostDecisionView::new(note_id).show(ui))
                .map_output(RenderNavAction::RepostAction)
        }
        Route::Report(target) => {
            let Some(kp) = ctx.accounts.selected_filled() else {
                return DragResponse::output(Some(RenderNavAction::Back));
            };

            let resp =
                ui::report::ReportView::new(&mut app.view_state.selected_report_type).show(ui);

            if let Some(report_type) = resp {
                notedeck::send_report_event(
                    ctx.ndb,
                    &mut ctx.remote.publisher(ctx.accounts),
                    kp,
                    target,
                    report_type,
                );
                app.view_state.selected_report_type = None;
                return DragResponse::output(Some(RenderNavAction::Back));
            }

            DragResponse::none()
        }
    }
}

#[must_use = "RenderNavResponse must be handled by calling .process_render_nav_response(..)"]
#[profiling::function]
pub fn render_nav(
    col: usize,
    inner_rect: egui::Rect,
    app: &mut Damus,
    ctx: &mut AppContext<'_>,
    ui: &mut egui::Ui,
) -> RenderNavResponse {
    let narrow = is_narrow(ui.ctx());

    if let Some(sheet_route) = app
        .columns(ctx.accounts)
        .column(col)
        .sheet_router
        .route()
        .clone()
    {
        let navigating = app
            .columns(ctx.accounts)
            .column(col)
            .sheet_router
            .navigating;
        let returning = app.columns(ctx.accounts).column(col).sheet_router.returning;
        let split = app.columns(ctx.accounts).column(col).sheet_router.split;
        let bg_route = app
            .columns(ctx.accounts)
            .column(col)
            .router()
            .routes()
            .last()
            .cloned();
        if let Some(bg_route) = bg_route {
            let resp = PopupSheet::new(&bg_route, &sheet_route)
                .id_source(egui::Id::new(("nav", col)))
                .navigating(navigating)
                .returning(returning)
                .with_split(split)
                .show_mut(ui, |ui, typ, route| match typ {
                    NavUiType::Title => NavTitle::new(
                        ctx.ndb,
                        ctx.img_cache,
                        get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
                        std::slice::from_ref(route),
                        col,
                        ctx.i18n,
                        ctx.media_jobs.sender(),
                    )
                    .show_move_button(!narrow)
                    .show_delete_button(!narrow)
                    .show(ui),
                    NavUiType::Body => {
                        render_nav_body(ui, app, ctx, route, 1, col, inner_rect).output
                    }
                });

            return RenderNavResponse::new(col, NotedeckNavResponse::Popup(Box::new(resp)));
        }
    };

    let routes = app
        .columns(ctx.accounts)
        .column(col)
        .router()
        .routes()
        .clone();
    let nav = Nav::new(&routes).id_source(egui::Id::new(("nav", col)));

    let nav_response = nav
        .navigating(
            app.columns_mut(ctx.i18n, ctx.accounts)
                .column_mut(col)
                .router_mut()
                .navigating(),
        )
        .returning(
            app.columns_mut(ctx.i18n, ctx.accounts)
                .column_mut(col)
                .router_mut()
                .returning(),
        )
        .animate_transitions(ctx.settings.get_settings_mut().animate_nav_transitions)
        .show_mut(ui, |ui, render_type, nav| match render_type {
            NavUiType::Title => {
                let action = NavTitle::new(
                    ctx.ndb,
                    ctx.img_cache,
                    get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
                    nav.routes(),
                    col,
                    ctx.i18n,
                    ctx.media_jobs.sender(),
                )
                .show_move_button(!narrow)
                .show_delete_button(!narrow)
                .show(ui);
                RouteResponse {
                    response: action,
                    can_take_drag_from: Vec::new(),
                }
            }

            NavUiType::Body => {
                let resp = if let Some(top) = nav.routes().last() {
                    render_nav_body(ui, app, ctx, top, nav.routes().len(), col, inner_rect)
                } else {
                    DragResponse::none()
                };

                RouteResponse {
                    response: resp.output,
                    can_take_drag_from: resp.drag_id.map(|d| vec![d]).unwrap_or(Vec::new()),
                }
            }
        });

    RenderNavResponse::new(col, NotedeckNavResponse::Nav(Box::new(nav_response)))
}
