use enostr::{FilledKeypair, FullKeypair, ProfileState, Pubkey};
use nostrdb::{Ndb, Note, NoteBuildOptions, NoteBuilder, Transaction};

use notedeck::{
    builder_from_note, note::publish::publish_note_builder, send_mute_event, Accounts,
    ContactState, DataPath, Localization, ProfileContext, PublishApi, RelayType, RemoteApi,
};
use tracing::info;

use crate::{column::Column, nav::RouterAction, route::Route, storage, Damus};

pub struct SaveProfileChanges {
    pub kp: FullKeypair,
    pub state: ProfileState,
}

impl SaveProfileChanges {
    pub fn new(kp: FullKeypair, state: ProfileState) -> Self {
        Self { kp, state }
    }
    pub fn to_note(&self) -> Note<'_> {
        let sec = &self.kp.secret_key.to_secret_bytes();
        add_client_tag(NoteBuilder::new())
            .kind(0)
            .content(&self.state.to_json())
            .options(NoteBuildOptions::default().created_at(true).sign(sec))
            .build()
            .expect("should build")
    }
}

fn add_client_tag(builder: NoteBuilder<'_>) -> NoteBuilder<'_> {
    builder
        .start_tag()
        .tag_str("client")
        .tag_str("Damus Notedeck")
}

pub enum ProfileAction {
    Edit(FullKeypair),
    SaveChanges(SaveProfileChanges),
    Follow(Pubkey),
    Unfollow(Pubkey),
    Context(ProfileContext),
}

impl ProfileAction {
    #[allow(clippy::too_many_arguments)]
    pub fn process_profile_action(
        &self,
        app: &mut Damus,
        path: &DataPath,
        i18n: &mut Localization,
        ctx: &egui::Context,
        ndb: &Ndb,
        remote: &mut RemoteApi<'_>,
        accounts: &Accounts,
    ) -> Option<RouterAction> {
        match self {
            ProfileAction::Edit(kp) => Some(RouterAction::route_to(Route::EditProfile(kp.pubkey))),
            ProfileAction::SaveChanges(changes) => {
                let note = changes.to_note();
                let Ok(event) = enostr::ClientMessage::event(&note) else {
                    tracing::error!("could not serialize profile note?");
                    return None;
                };

                let Ok(json) = event.to_json() else {
                    tracing::error!("could not serialize profile note?");
                    return None;
                };

                // TODO(jb55): do this in a more centralized place
                let _ = ndb.process_event_with(&json, nostrdb::IngestMetadata::new().client(true));

                info!("sending {}", &json);
                let mut publisher = remote.publisher(accounts);
                publisher.publish_note(&note, RelayType::AccountsWrite);

                Some(RouterAction::GoBack)
            }
            ProfileAction::Follow(target_key) => {
                let mut publisher = remote.publisher(accounts);
                Self::send_follow_user_event(ndb, &mut publisher, accounts, target_key);
                None
            }
            ProfileAction::Unfollow(target_key) => {
                let mut publisher = remote.publisher(accounts);
                Self::send_unfollow_user_event(ndb, &mut publisher, accounts, target_key);
                None
            }
            ProfileAction::Context(profile_context) => {
                use notedeck::ProfileContextSelection;
                match &profile_context.selection {
                    ProfileContextSelection::ViewAs => {
                        Some(RouterAction::SwitchAccount(profile_context.profile))
                    }
                    ProfileContextSelection::AddProfileColumn => {
                        let timeline_route = Route::Timeline(
                            crate::timeline::TimelineKind::Profile(profile_context.profile),
                        );

                        let missing_column = {
                            let deck_columns = app.columns(accounts).columns();
                            let router_head = std::slice::from_ref(&timeline_route);
                            !deck_columns
                                .iter()
                                .any(|column| column.router.routes().starts_with(router_head))
                        };

                        if missing_column {
                            let column = Column::new(vec![timeline_route]);

                            app.columns_mut(i18n, accounts).add_column(column);

                            storage::save_decks_cache(path, &app.decks_cache);
                        }

                        None
                    }
                    ProfileContextSelection::MuteUser => {
                        let kp = accounts.get_selected_account().key.to_full()?;
                        let muted = accounts.mute();
                        let txn = Transaction::new(ndb).expect("txn");
                        let publisher = &mut remote.publisher(accounts);
                        if muted.is_pk_muted(profile_context.profile.bytes()) {
                            notedeck::send_unmute_event(
                                ndb,
                                &txn,
                                publisher,
                                kp,
                                &muted,
                                &profile_context.profile,
                            );
                        } else {
                            send_mute_event(
                                ndb,
                                &txn,
                                publisher,
                                kp,
                                &muted,
                                &profile_context.profile,
                            );
                        }
                        None
                    }
                    ProfileContextSelection::ReportUser => {
                        let target = notedeck::ReportTarget {
                            pubkey: profile_context.profile,
                            note_id: None,
                        };
                        Some(RouterAction::route_to_sheet(
                            Route::Report(target),
                            egui_nav::Split::AbsoluteFromBottom(340.0),
                        ))
                    }
                    _ => {
                        profile_context
                            .selection
                            .process(ctx, &profile_context.profile);
                        None
                    }
                }
            }
        }
    }

    fn send_follow_user_event(
        ndb: &Ndb,
        publisher: &mut PublishApi<'_, '_>,
        accounts: &Accounts,
        target_key: &Pubkey,
    ) {
        send_kind_3_event(ndb, publisher, accounts, FollowAction::Follow(target_key));
    }

    fn send_unfollow_user_event(
        ndb: &Ndb,
        publisher: &mut PublishApi<'_, '_>,
        accounts: &Accounts,
        target_key: &Pubkey,
    ) {
        send_kind_3_event(ndb, publisher, accounts, FollowAction::Unfollow(target_key));
    }
}

enum FollowAction<'a> {
    Follow(&'a Pubkey),
    Unfollow(&'a Pubkey),
}

fn send_kind_3_event(
    ndb: &Ndb,
    publisher: &mut PublishApi<'_, '_>,
    accounts: &Accounts,
    action: FollowAction,
) {
    let Some(kp) = accounts.get_selected_account().key.to_full() else {
        return;
    };

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

    let ContactState::Received {
        contacts: _,
        note_key,
        timestamp: _,
    } = accounts.get_selected_account().data.contacts.get_state()
    else {
        return;
    };

    let contact_note = match ndb.get_note_by_key(&txn, *note_key).ok() {
        Some(n) => n,
        None => {
            tracing::error!(
                "Somehow we are in state ContactState::Received but the contact note key doesn't exist"
            );
            return;
        }
    };

    if contact_note.kind() != 3 {
        tracing::error!(
            "Something very wrong just occured. The key for the supposed contact note yielded a note which was not a contact..."
        );
        return;
    }

    let builder = match action {
        FollowAction::Follow(pubkey) => {
            builder_from_note(contact_note, None::<fn(&nostrdb::Tag<'_>) -> bool>)
                .start_tag()
                .tag_str("p")
                .tag_str(&pubkey.hex())
        }
        FollowAction::Unfollow(pubkey) => builder_from_note(
            contact_note,
            Some(|tag: &nostrdb::Tag<'_>| {
                if tag.count() < 2 {
                    return false;
                }

                let Some("p") = tag.get_str(0) else {
                    return false;
                };

                let Some(cur_val) = tag.get_id(1) else {
                    return false;
                };

                cur_val == pubkey.bytes()
            }),
        ),
    };

    publish_note_builder(builder, ndb, publisher, kp);
}

pub fn send_new_contact_list(
    kp: FilledKeypair,
    ndb: &Ndb,
    publisher: &mut PublishApi<'_, '_>,
    mut pks_to_follow: Vec<Pubkey>,
) {
    if !pks_to_follow.contains(kp.pubkey) {
        pks_to_follow.push(*kp.pubkey);
    }

    let builder = construct_contact_list_note(pks_to_follow);

    publish_note_builder(builder, ndb, publisher, kp);
}

/// Builds a kind-3 contact-list note for the provided pubkeys.
fn construct_contact_list_note<'a>(pks: Vec<Pubkey>) -> NoteBuilder<'a> {
    let mut builder = NoteBuilder::new()
        .content("")
        .kind(3)
        .options(NoteBuildOptions::default());

    for pk in pks {
        builder = builder.start_tag().tag_str("p").tag_str(&pk.hex());
    }

    builder
}

pub fn send_default_dms_relay_list(
    kp: FilledKeypair<'_>,
    ndb: &Ndb,
    publisher: &mut PublishApi<'_, '_>,
) {
    publish_note_builder(construct_default_dms_relay_list(), ndb, publisher, kp);
}

fn construct_default_dms_relay_list<'a>() -> NoteBuilder<'a> {
    let mut builder = NoteBuilder::new()
        .content("")
        .kind(10050)
        .options(NoteBuildOptions::default());

    for relay in default_dms_relays() {
        builder = builder.start_tag().tag_str("relay").tag_str(relay);
    }

    builder
}

fn default_dms_relays() -> Vec<&'static str> {
    vec!["wss://relay.damus.io", "wss://nos.lol"]
}

#[cfg(test)]
mod tests {
    use super::construct_contact_list_note;
    use enostr::FullKeypair;

    #[test]
    fn construct_contact_list_note_emits_expected_tags() {
        let owner = FullKeypair::generate();
        let first = FullKeypair::generate();
        let second = FullKeypair::generate();

        let note = construct_contact_list_note(vec![first.pubkey, second.pubkey])
            .sign(&owner.secret_key.secret_bytes())
            .build()
            .expect("contact list note");

        assert_eq!(note.kind(), 3);
        assert!(note.tags().into_iter().any(|tag| {
            tag.get_str(0) == Some("p") && tag.get_id(1) == Some(first.pubkey.bytes())
        }));
        assert!(note.tags().into_iter().any(|tag| {
            tag.get_str(0) == Some("p") && tag.get_id(1) == Some(second.pubkey.bytes())
        }));
    }
}
