mod message;

use enostr::{FullKeypair, NormRelayUrl, Pubkey, SecretKey};
use hashbrown::HashSet;
pub(crate) use message::{send_conversation_message, SendMessageResult};
pub use nostr::secp256k1::rand::rngs::OsRng;
use nostr::secp256k1::rand::Rng;
use nostr::{
    event::{EventBuilder, Kind, Tag},
    key::PublicKey,
    nips::nip44,
    util::JsonUtil,
};
use nostrdb::{Filter, FilterBuilder, Ndb, Note, NoteBuilder, Transaction};
use notedeck::get_p_tags;

fn build_rumor_json(
    message: &str,
    participants: &[Pubkey],
    sender_pubkey: &Pubkey,
) -> Option<String> {
    let sender = nostrcrate_pk(sender_pubkey)?;
    let mut tags = Vec::new();
    for participant in participants {
        if let Some(pk) = nostrcrate_pk(participant) {
            tags.push(Tag::public_key(pk));
        } else {
            tracing::warn!("invalid participant {}", participant);
        }
    }

    let builder = EventBuilder::new(Kind::PrivateDirectMessage, message).tags(tags);
    Some(builder.build(sender).as_json())
}

pub fn giftwrap_message(
    rng: &mut OsRng,
    sender_secret: &SecretKey,
    recipient: &Pubkey,
    rumor_json: &str,
) -> Option<Note<'static>> {
    let Some(recipient_pk) = nostrcrate_pk(recipient) else {
        tracing::warn!("failed to convert recipient pubkey {}", recipient);
        return None;
    };

    let encrypted_rumor = match nip44::encrypt_with_rng(
        rng,
        sender_secret,
        &recipient_pk,
        rumor_json,
        nip44::Version::V2,
    ) {
        Ok(payload) => payload,
        Err(err) => {
            tracing::error!("failed to encrypt rumor for {recipient}: {err}");
            return None;
        }
    };

    let seal_created = randomized_timestamp(rng);
    let Some(seal_json) = build_seal_json(&encrypted_rumor, sender_secret, seal_created) else {
        tracing::error!("failed to build seal for recipient {}", recipient);
        return None;
    };

    let wrap_keys = FullKeypair::generate();
    let encrypted_seal = match nip44::encrypt_with_rng(
        rng,
        &wrap_keys.secret_key,
        &recipient_pk,
        &seal_json,
        nip44::Version::V2,
    ) {
        Ok(payload) => payload,
        Err(err) => {
            tracing::error!("failed to encrypt seal for wrap: {err}");
            return None;
        }
    };

    let wrap_created = randomized_timestamp(rng);
    build_giftwrap_note(&encrypted_seal, &wrap_keys, recipient, wrap_created)
}

fn build_seal_json(
    content_ciphertext: &str,
    sender_secret: &SecretKey,
    created_at: u64,
) -> Option<String> {
    let builder = NoteBuilder::new()
        .kind(13)
        .content(content_ciphertext)
        .created_at(created_at);

    builder
        .sign(&sender_secret.secret_bytes())
        .build()?
        .json()
        .ok()
}

fn build_giftwrap_note(
    content: &str,
    wrap_keys: &FullKeypair,
    recipient: &Pubkey,
    created_at: u64,
) -> Option<Note<'static>> {
    let builder = NoteBuilder::new()
        .kind(1059)
        .content(content)
        .created_at(created_at)
        .start_tag()
        .tag_str("p")
        .tag_str(&recipient.hex());

    builder.sign(&wrap_keys.secret_key.secret_bytes()).build()
}

fn nostrcrate_pk(pk: &Pubkey) -> Option<PublicKey> {
    PublicKey::from_slice(pk.bytes()).ok()
}

fn current_timestamp() -> u64 {
    use std::time::{SystemTime, UNIX_EPOCH};

    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs()
}

fn randomized_timestamp(rng: &mut OsRng) -> u64 {
    const MAX_SKEW_SECS: u64 = 2 * 24 * 60 * 60;
    let now = current_timestamp();
    let tweak = rng.gen_range(0..=MAX_SKEW_SECS);
    now.saturating_sub(tweak)
}

#[profiling::function]
pub fn get_participants<'a>(note: &Note<'a>) -> Vec<&'a [u8; 32]> {
    let mut participants = get_p_tags(note);
    let chat_message_sender = note.pubkey();
    if !participants.contains(&chat_message_sender) {
        // the chat message sender must be in the participants set
        participants.push(chat_message_sender);
    }
    participants
}

pub fn conversation_filter(cur_acc: &Pubkey) -> Vec<Filter> {
    vec![
        FilterBuilder::new()
            .kinds([14])
            .pubkey([cur_acc.bytes()])
            .build(),
        FilterBuilder::new()
            .kinds([14])
            .authors([cur_acc.bytes()])
            .build(),
    ]
}

/// Unfortunately this gives an OR across participants
pub fn chatroom_filter(participants: Vec<&[u8; 32]>, me: &[u8; 32]) -> Vec<Filter> {
    vec![FilterBuilder::new()
        .kinds([14])
        .authors(participants.clone())
        .pubkey([me])
        .build()]
}

/// Builds a filter for one participant's kind `10050` DM relay list.
pub fn participant_dm_relay_list_filter(participant: &Pubkey) -> Filter {
    FilterBuilder::new()
        .kinds([10050])
        .authors([participant.bytes()])
        .limit(1)
        .build()
}

/// Builds one no-limit filter for fetching participants' kind `10050` DM relay lists.
pub(crate) fn participant_dm_relay_list_prefetch_filter(participants: &[Pubkey]) -> Filter {
    let mut participants = participants.to_vec();
    participants.sort_unstable_by(|a, b| a.bytes().cmp(b.bytes()));
    participants.dedup();

    FilterBuilder::new()
        .kinds([10050])
        .authors(participants.iter().map(|participant| participant.bytes()))
        .build()
}

/// Returns `true` when `note` is a kind `10050` DM relay-list authored by `participant`.
pub fn is_participant_dm_relay_list(note: &Note<'_>, participant: &Pubkey) -> bool {
    note.kind() == 10050 && note.pubkey() == participant.bytes()
}

/// Queries NDB for presence of one participant's kind `10050` DM relay list.
pub fn has_participant_dm_relay_list(ndb: &Ndb, txn: &Transaction, participant: &Pubkey) -> bool {
    let filter = participant_dm_relay_list_filter(participant);
    let Ok(results) = ndb.query(txn, std::slice::from_ref(&filter), 1) else {
        return false;
    };

    !results.is_empty()
}

/// Default relay URLs used when creating a new kind `10050` DM relay-list note.
pub fn default_dm_relay_urls() -> &'static [&'static str] {
    &["wss://relay.damus.io", "wss://nos.lol"]
}

/// Builds a signed kind `10050` DM relay-list note using default relay URLs.
pub fn build_default_dm_relay_list_note(sender_secret: &SecretKey) -> Option<Note<'static>> {
    let mut builder = NoteBuilder::new().kind(10050).content("");

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

    builder.sign(&sender_secret.secret_bytes()).build()
}

/// Builds a signed kind `10050` DM relay-list note with `created_at = 1`.
///
/// The extremely old timestamp ensures that any real relay list the user has published
/// on any relay will naturally supersede this one (relays reject older replaceable events).
/// This is used as a fallback when relay-list EOSE times out, so the account can
/// immediately send/receive DMs via the default relays without waiting forever.
pub fn build_backdated_default_dm_relay_list_note(
    sender_secret: &SecretKey,
) -> Option<Note<'static>> {
    let mut builder = NoteBuilder::new().kind(10050).content("").created_at(1);

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

    builder.sign(&sender_secret.secret_bytes()).build()
}

/// Parses a kind `10050` note into unique websocket relay URLs.
pub fn parse_dm_relay_list_relays(note: &Note<'_>) -> Vec<NormRelayUrl> {
    if note.kind() != 10050 {
        return Vec::new();
    }

    let mut seen = HashSet::new();
    let mut relays = Vec::new();

    for tag in note.tags() {
        if tag.count() < 2 {
            continue;
        }

        let Some("relay") = tag.get_str(0) else {
            continue;
        };

        let Some(url) = tag.get_str(1) else {
            continue;
        };

        let Ok(norm_url) = NormRelayUrl::new(url) else {
            continue;
        };

        if !seen.insert(norm_url.clone()) {
            continue;
        }

        relays.push(norm_url);
    }

    relays
}

/// Queries NDB for one participant's latest kind `10050` relay list.
///
/// Returns explicit websocket relay URLs when available, else an empty vec.
#[profiling::function]
pub fn query_participant_dm_relays(
    ndb: &Ndb,
    txn: &Transaction,
    participant: &Pubkey,
) -> Vec<NormRelayUrl> {
    let filter = participant_dm_relay_list_filter(participant);
    let Ok(results) = ndb.query(txn, std::slice::from_ref(&filter), 1) else {
        tracing::debug!(
            "failed to query dm relay list for participant {}",
            participant
        );
        return Vec::new();
    };

    let Some(result) = results.first() else {
        tracing::debug!("no dm relay list found for participant {}", participant);
        return Vec::new();
    };

    let relays = parse_dm_relay_list_relays(&result.note);
    tracing::debug!(
        "selected dm relay list for participant {} at created_at={} with {} relay(s)",
        participant,
        result.note.created_at(),
        relays.len()
    );
    relays
}

/// Returns unique authors of locally stored kind `10050` notes, excluding the selected account.
#[profiling::function]
pub fn known_participant_dm_relay_list_authors(
    ndb: &Ndb,
    txn: &Transaction,
    selected_account: &Pubkey,
) -> Vec<Pubkey> {
    let filter = FilterBuilder::new().kinds([10050]).build();
    let Ok(results) = ndb.query(txn, std::slice::from_ref(&filter), 1_024) else {
        tracing::debug!(
            "failed to query known participant dm relay-list authors for selected_account={}",
            selected_account
        );
        return Vec::new();
    };

    let mut participants = HashSet::new();
    for result in results {
        let participant = Pubkey::new(*result.note.pubkey());
        if &participant != selected_account {
            participants.insert(participant);
        }
    }

    tracing::debug!(
        "found {} known participant dm relay-list author(s) for selected_account={}",
        participants.len(),
        selected_account
    );
    participants.into_iter().collect()
}

// easily retrievable from Note<'a>
pub struct Nip17ChatMessage<'a> {
    pub sender: &'a [u8; 32],
    pub p_tags: Vec<&'a [u8; 32]>,
    pub subject: Option<&'a str>,
    pub reply_to: Option<&'a [u8; 32]>, // NoteId
    pub message: &'a str,
    pub created_at: u64,
}

pub fn parse_chat_message<'a>(note: &Note<'a>) -> Option<Nip17ChatMessage<'a>> {
    if note.kind() != 14 {
        return None;
    }

    let mut p_tags = Vec::new();
    let mut subject = None;
    let mut reply_to = None;

    for tag in note.tags() {
        if tag.count() < 2 {
            continue;
        }
        let Some(first) = tag.get_str(0) else {
            continue;
        };

        if first == "p" {
            if let Some(id) = tag.get_id(1) {
                p_tags.push(id);
            }
        } else if first == "subject" {
            subject = tag.get_str(1);
        } else if first == "e" {
            reply_to = tag.get_id(1);
        }
    }

    Some(Nip17ChatMessage {
        sender: note.pubkey(),
        p_tags,
        subject,
        reply_to,
        message: note.content(),
        created_at: note.created_at(),
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use nostrdb::{Ndb, NoteBuilder, Transaction};
    use notedeck_testing::fixtures::test_config;
    use std::time::{Duration, Instant};
    use tempfile::tempdir;

    fn relay_note(relays: &[&str]) -> Note<'static> {
        let signer = FullKeypair::generate();
        relay_note_with_created_at(&signer, relays, 1_700_000_000)
    }

    fn relay_note_with_created_at(
        signer: &FullKeypair,
        relays: &[&str],
        created_at: u64,
    ) -> Note<'static> {
        let mut builder = NoteBuilder::new().kind(10050).content("");
        builder = builder.created_at(created_at);
        for relay in relays {
            builder = builder.start_tag().tag_str("relay").tag_str(relay);
        }

        builder
            .sign(&signer.secret_key.secret_bytes())
            .build()
            .expect("relay note")
    }

    /// Verifies the relay-list filter targets kind `10050`, the participant author, and limit `1`.
    #[test]
    fn participant_dm_relay_list_filter_is_stable() {
        let participant = Pubkey::new([0x22; 32]);
        let actual = participant_dm_relay_list_filter(&participant);
        let expected = FilterBuilder::new()
            .kinds([10050])
            .authors([participant.bytes()])
            .limit(1)
            .build();

        assert_eq!(
            actual.json().expect("actual filter json"),
            expected.json().expect("expected filter json")
        );
    }

    /// Verifies participant relay-list prefetch uses one combined no-limit author filter.
    #[test]
    fn participant_dm_relay_list_prefetch_filter_combines_authors_without_limit() {
        let participant_a = Pubkey::new([0x22; 32]);
        let participant_b = Pubkey::new([0x33; 32]);
        let participants = [participant_b, participant_a, participant_b];
        let actual = participant_dm_relay_list_prefetch_filter(&participants);
        let expected = FilterBuilder::new()
            .kinds([10050])
            .authors([participant_a.bytes(), participant_b.bytes()])
            .build();

        assert_eq!(
            actual.json().expect("actual filter json"),
            expected.json().expect("expected filter json")
        );
    }

    /// Verifies relay parsing ignores invalid URLs and deduplicates repeated relay tags.
    #[test]
    fn parse_dm_relay_list_relays_dedupes_and_skips_invalid_urls() {
        let note = relay_note(&[
            "wss://relay-a.example.com",
            "notaurl",
            "wss://relay-a.example.com",
            "wss://relay-b.example.com",
        ]);

        let parsed = parse_dm_relay_list_relays(&note);
        assert_eq!(parsed.len(), 2);

        let actual: HashSet<NormRelayUrl> = HashSet::from_iter(parsed);
        let expected = HashSet::from_iter(
            ["wss://relay-a.example.com", "wss://relay-b.example.com"]
                .into_iter()
                .map(|relay| NormRelayUrl::new(relay).expect("norm relay")),
        );

        assert_eq!(actual, expected);
    }

    /// Verifies default DM relay-list note construction emits kind `10050` and relay tags.
    #[test]
    fn build_default_dm_relay_list_note_contains_default_relays() {
        let signer = FullKeypair::generate();
        let note = build_default_dm_relay_list_note(&signer.secret_key).expect("relay list note");

        assert_eq!(note.kind(), 10050);
        let urls = parse_dm_relay_list_relays(&note);
        assert!(!urls.is_empty());
    }

    /// Verifies local participant relay-list author discovery ignores the selected account.
    #[test]
    fn known_participant_dm_relay_list_authors_skips_selected_account() {
        let tempdir = tempdir().expect("tempdir");
        let ndb = Ndb::new(tempdir.path().to_str().expect("db path"), &test_config()).expect("ndb");
        let selected = FullKeypair::generate();
        let participant_a = FullKeypair::generate();
        let participant_b = FullKeypair::generate();

        for note in [
            relay_note_with_created_at(&selected, &["wss://relay-selected.example.com"], 10),
            relay_note_with_created_at(&participant_a, &["wss://relay-a.example.com"], 11),
            relay_note_with_created_at(&participant_a, &["wss://relay-a-new.example.com"], 12),
            relay_note_with_created_at(&participant_b, &["wss://relay-b.example.com"], 13),
        ] {
            let note_json = note.json().expect("relay-list note json");
            ndb.process_client_event(&note_json)
                .expect("ingest relay-list note");
        }

        let expected = HashSet::from_iter([participant_a.pubkey, participant_b.pubkey]);
        let deadline = Instant::now() + Duration::from_secs(1);

        loop {
            let txn = Transaction::new(&ndb).expect("txn");
            let authors = known_participant_dm_relay_list_authors(&ndb, &txn, &selected.pubkey);
            let actual: HashSet<Pubkey> = authors.into_iter().collect();
            if actual == expected {
                break;
            }

            assert!(
                Instant::now() < deadline,
                "timed out waiting for known relay-list authors; actual={actual:?}, expected={expected:?}"
            );
            std::thread::sleep(Duration::from_millis(10));
        }
    }
}
