use std::error::Error;
use std::ffi::OsStr;
use std::path::Path;
use std::str::FromStr;

use http::Uri;

use gemini::{Doc, gemtext::Level as GtLevel};
use gnostr_types::{ContentSegment, ShatteredContent};
use mdiu::{Block, Content, Level, Link, Preformatted};
use nostr_sdk::prelude::*;
use regex::Regex;
use urlencoding::encode;

use super::options::NoteRenderOptions;
use crate::emoji::*;
use crate::nostru;
use crate::ts;
use crate::util::extract_first_words;

use rust_i18n::t;
rust_i18n::i18n!("locales");

pub fn invalid_content() -> Content {
    Content::new("Invalid content").unwrap()
}

pub fn h1(text: String) -> Block {
    Block::Heading(Level::One, Content::new(text).unwrap_or(invalid_content()))
}

pub fn h2(text: String) -> Block {
    Block::Heading(Level::Two, Content::new(text).unwrap_or(invalid_content()))
}

pub fn h3(text: String) -> Block {
    Block::Heading(
        Level::Three,
        Content::new(text).unwrap_or(invalid_content()),
    )
}

pub fn lnk(uri: Uri, opt_label: Option<String>) -> Block {
    if let Some(text) = opt_label {
        Block::Link(Link::new(
            uri,
            Some(Content::new(text).unwrap_or(invalid_content())),
        ))
    } else {
        Block::Link(Link::new(uri, None))
    }
}

/// Turn gemtext into a list of mdiu [`Block`]
pub fn blockify(gemtext: String) -> Vec<Block> {
    let mut blocks = Vec::new();

    if let Ok(builder) = gemini::parse::parse_gemtext(gemtext) {
        for element in builder {
            let block = match element {
                Doc::Text(text) => {
                    let Ok(content) = Content::new(text) else {
                        continue;
                    };

                    Block::Text(content)
                }
                Doc::Preformatted { alt, text } => {
                    Block::Preformatted(Preformatted::new(
                        text,
                        alt.and_then(|atext| Content::new(atext).ok()),
                    ))
                }
                Doc::Heading(level, text) => {
                    let lvl = match level {
                        GtLevel::One => Level::One,
                        GtLevel::Two => Level::Two,
                        GtLevel::Three => Level::Three,
                    };

                    let Ok(content) = Content::new(text) else {
                        continue;
                    };

                    Block::Heading(lvl, content)
                }
                Doc::Quote(quote) => {
                    let Ok(content) = Content::new(quote) else {
                        continue;
                    };

                    Block::Quote(content)
                }
                Doc::Link { to, name } => {
                    let Ok(uri) = Uri::from_str(&to) else {
                        continue;
                    };

                    Block::Link(Link::new(
                        uri,
                        name.and_then(|n| Content::new(n).ok()),
                    ))
                }
                Doc::ListItem(text) => {
                    let Ok(content) = Content::new(text) else {
                        continue;
                    };

                    Block::ListItem(content)
                }
                Doc::Blank => Block::Empty,
            };

            blocks.push(block);
        }
    }

    blocks
}

pub async fn pubkey_label(pubkey: &PublicKey, client: &Client) -> String {
    if let Some(meta) = nostru::get_user_metadata(client, *pubkey).await {
        if let Some(name) = meta.name
            && !name.is_empty()
        {
            name
        } else if let Some(dname) = meta.display_name
            && !dname.is_empty()
        {
            dname
        } else if let Some(nip05) = meta.nip05
            && !nip05.is_empty()
        {
            nip05
        } else {
            pubkey.to_bech32().unwrap()
        }
    } else {
        // Get some basic infos about this identity
        if client
            .subscribe(
                Filter::new()
                    .kind(Kind::Metadata)
                    .kind(Kind::RelayList)
                    .limit(5)
                    .author(*pubkey),
                None,
            )
            .await
            .is_err()
        {
            eprintln!("Subscription error (pubkey_label)");
        }

        pubkey.to_bech32().unwrap()
    }
}

pub async fn geminize_note(
    event: &Event,
    client: &Client,
) -> Vec<(Block, Option<Uri>)> {
    let mut vec: Vec<(Block, Option<Uri>)> = Vec::new();
    let mut links: Vec<(String, Uri)> = Vec::new();
    let mut link_no = 0;
    let ht_re = Regex::new(r"^#").unwrap();

    let shattered_content = ShatteredContent::new(event.content.clone());

    for segment in shattered_content.segments.iter() {
        match segment {
            ContentSegment::NostrUrl(nostr_url) => {
                let Ok(n21) = Nip21::parse(&format!("{nostr_url}")) else {
                    eprintln!("Invalid nip21 url: {nostr_url}");
                    continue;
                };

                match n21 {
                    Nip21::EventId(event_id) => {
                        let bech32 = event_id.to_bech32().unwrap();
                        let Ok(uri) =
                            Uri::from_str(&format!("/note/{}/thread", bech32))
                        else {
                            continue;
                        };

                        links.push((
                            t!("read_ref_note_with_bech", event_bech = bech32)
                                .to_string(),
                            uri,
                        ));
                    }
                    Nip21::Event(event) => {
                        let bech32 = event.event_id.to_bech32().unwrap();
                        let Ok(uri) =
                            Uri::from_str(&format!("/note/{}/thread", bech32))
                        else {
                            continue;
                        };

                        links.push((
                            t!("read_ref_note_with_bech", event_bech = bech32)
                                .to_string(),
                            uri,
                        ));
                    }
                    Nip21::Pubkey(pubkey) => {
                        let Ok(uri) = Uri::from_str(&format!(
                            "/p/{}",
                            pubkey.to_bech32().unwrap()
                        )) else {
                            continue;
                        };

                        links.push((
                            format!(
                                "{} {}",
                                EMOJI_KEY,
                                pubkey_label(&pubkey, client).await
                            ),
                            uri,
                        ));
                    }
                    Nip21::Profile(profile) => {
                        let Ok(uri) = Uri::from_str(&format!(
                            "/p/{}",
                            profile.public_key.to_bech32().unwrap()
                        )) else {
                            continue;
                        };

                        links.push((
                            format!(
                                "{} {}",
                                EMOJI_KEY,
                                pubkey_label(&profile.public_key, client).await
                            ),
                            uri,
                        ));
                    }

                    _ => (),
                }
            }
            ContentSegment::Hyperlink(span) => {
                let Some(chunk) = shattered_content.slice(span) else {
                    continue;
                };

                let Ok(url) = Url::parse(chunk) else {
                    continue;
                };

                let scheme = url.scheme();
                let filename = Path::new(url.path())
                    .file_name()
                    .unwrap_or(OsStr::new("/"));

                let mime_type = mime_guess::from_path(filename)
                    .first()
                    .unwrap_or(mime::TEXT_PLAIN);

                let Ok(orig_uri) = Uri::from_str(url.as_str()) else {
                    eprintln!("Invalid URL: {url}");
                    continue;
                };

                match scheme {
                    "http" | "https" => {
                        let mcat = mime_type.type_().as_str();
                        let Ok(get_uri) = Uri::builder()
                            .path_and_query(format!(
                                "/www/get/{}?{}",
                                filename.display(),
                                encode(url.as_ref())
                            ))
                            .build()
                        else {
                            continue;
                        };

                        match mcat {
                            // Image
                            "image" => {
                                links.push((
                                    format!(
                                        "{} {} ({})",
                                        EMOJI_FILE,
                                        t!("attached_image"),
                                        link_no
                                    ),
                                    get_uri,
                                ));
                            }
                            // Video
                            "video" => {
                                links.push((
                                    format!(
                                        "{} {} ({})",
                                        EMOJI_FILE,
                                        t!("attached_video"),
                                        link_no
                                    ),
                                    get_uri,
                                ));
                            }
                            // Audio
                            "audio" => {
                                links.push((
                                    format!(
                                        "{} {} ({})",
                                        EMOJI_FILE,
                                        t!("attached_audio"),
                                        link_no
                                    ),
                                    get_uri,
                                ));
                            }
                            // Anything else
                            _ => {
                                links.push((
                                    orig_uri.clone().to_string(),
                                    orig_uri.clone(),
                                ));
                            }
                        }

                        link_no += 1;
                    }
                    "ws" | "wss" | "ftp" | "gemini" | "nsite" | "payto" => {
                        links.push((
                            orig_uri.clone().to_string(),
                            orig_uri.clone(),
                        ));
                    }
                    scheme => println!("Unhandled URL scheme {scheme}"),
                }
            }
            ContentSegment::Plain(span) => {
                let Some(chunk) = shattered_content.slice(span) else {
                    continue;
                };
                for line in chunk.lines() {
                    // If there's an hashtag at the beginning of the line, remove it
                    let cline = ht_re.replace(line, "");
                    let Ok(content) = Content::new(cline) else {
                        continue;
                    };

                    vec.push((Block::Text(content), None));
                }
            }
            _ => (),
        }

        for (label, link) in &links {
            vec.push((lnk(link.clone(), Some(label.clone())), None));
        }

        links.clear();
    }

    vec
}

#[derive(Copy, Clone, PartialEq)]
pub enum NoteRenderMode {
    Tiny,
    Minimal,
    Normal,
    Verbose,
}

/// Renders a TextNote event as gemtext
/// Returns a list of mdiu gemtext [`Block`]
pub async fn render_note(
    client: &Client,
    event: &Event,
    metadata: Option<Metadata>,
    mode: NoteRenderOptions,
) -> Result<Vec<Block>, Box<dyn Error>> {
    let mut blocks = vec![];
    let mut header = String::new();
    let mut profile_header = String::new();

    let profile_uri = Uri::builder()
        .path_and_query(format!("/p/{}", event.pubkey.to_bech32()?))
        .build()?;

    let thread_uri = Uri::builder()
        .path_and_query(format!(
            "/note/{}/thread",
            event.id.to_bech32().unwrap()
        ))
        .build()?;

    let reply_uri = Uri::builder()
        .path_and_query(format!(
            "/note/{}/reply",
            event.id.to_bech32().unwrap()
        ))
        .build()?;

    let repost_uri = Uri::builder()
        .path_and_query(format!(
            "/note/{}/repost",
            event.id.to_bech32().unwrap()
        ))
        .build()?;

    let react_uri = Uri::builder()
        .path_and_query(format!(
            "/note/{}/react_select",
            event.id.to_bech32().unwrap()
        ))
        .build()?;

    let bookmark_uri = Uri::builder()
        .path_and_query(format!(
            "/note/{}/bookmark",
            event.id.to_bech32().unwrap()
        ))
        .build()?;

    let delete_uri = Uri::builder()
        .path_and_query(format!(
            "/note/{}/delete",
            event.id.to_bech32().unwrap()
        ))
        .build()?;

    let ts_diff = Timestamp::now() - event.created_at;
    let ts_ago = ts::time_ago(ts_diff.as_secs());

    if let Err(err) = client
        .subscribe(
            Filter::new()
                .kind(Kind::Reaction)
                .kind(Kind::Repost)
                .event(event.id),
            None,
        )
        .await
    {
        eprintln!("Reactions sub failed: {err}");
    }

    let rfilter = Filter::new().kind(Kind::Reaction).event(event.id);
    let Ok(reactions) = client.database().query(rfilter).await else {
        return Err(Box::from("Cannot fetch reactions"));
    };

    let Ok(reposts) = client
        .database()
        .query(Filter::new().kind(Kind::Repost).event(event.id))
        .await
    else {
        return Err(Box::from("Cannot fetch reposts"));
    };

    if let Some(sentence) = extract_first_words(event, 10) {
        header.push_str(EMOJI_SUN);
        header.push_str(&sentence);
    } else {
        header.push_str(&format!("{} {}", EMOJI_SUN, "Note"));
    }

    profile_header.push_str(EMOJI_SUN);

    if let Some(meta) = metadata {
        if let Some(name) = meta.name
            && !name.is_empty()
        {
            profile_header.push_str(name.as_str());
        }

        if let Some(display_name) = meta.display_name
            && !display_name.is_empty()
        {
            profile_header.push_str(format!(" ({})", display_name).as_str())
        }
    } else {
        profile_header.push_str(&event.pubkey.to_bech32().unwrap());
    }

    profile_header
        .push_str(format!(" {} {} ago", EMOJI_CLOCK, ts_ago).as_str());

    // Top heading for the note
    blocks.push(h2(header));

    // Reposts
    if !reposts.is_empty()
        && let Ok(content) = Content::new(nostru::reposts_counter(&reposts))
    {
        blocks.push(Block::Text(content));
    }

    // Reactions
    if !reactions.is_empty()
        && let Ok(content) = Content::new(nostru::reactions_summary(&reactions))
    {
        blocks.push(Block::Text(content));
    }

    blocks.push(lnk(profile_uri, Some(profile_header)));
    blocks.push(Block::Empty);

    match event.kind {
        Kind::TextNote => {
            for (block, _uri) in geminize_note(event, client).await {
                blocks.push(block);
            }
        }
        Kind::LongFormTextNote => {
            // Convert markdown to gemtext and map to mdiu blocks
            blocks.extend(blockify(md2gemtext::convert(&event.content)));
        }
        _ => (),
    }

    blocks.push(Block::Empty);

    if mode.contains(NoteRenderOptions::SHOW_HASHTAGS) {
        for htag in event.tags.hashtags() {
            if let Ok(uri) = Uri::builder()
                .path_and_query(format!("/hashtags/{}/notes", encode(htag)))
                .build()
            {
                blocks.push(lnk(
                    uri,
                    Some(
                        format!(
                            "{} Hashtag: {}",
                            EMOJI_LARGE_ORANGE_DIAMOND, htag
                        )
                        .to_string(),
                    ),
                ));
            }
        }

        // Mentioned public keys
        for pubkey in event.tags.public_keys() {
            if let Ok(uri) = Uri::builder()
                .path_and_query(format!("/p/{}", pubkey))
                .build()
            {
                blocks.push(lnk(
                    uri,
                    Some(
                        format!(
                            "{} {}: {}",
                            EMOJI_KEY,
                            t!("public_key"),
                            pubkey_label(pubkey, client).await
                        )
                        .to_string(),
                    ),
                ));
            }
        }
    }

    // Add links for the event tags
    for event_id in event.tags.event_ids() {
        if let Ok(uri) = Uri::builder()
            .path_and_query(format!(
                "/note/{}/thread",
                event_id.to_bech32().unwrap()
            ))
            .build()
        {
            blocks.push(lnk(
                uri,
                Some(
                    format!(
                        "{} {}",
                        EMOJI_LARGE_BLUE_SQUARE,
                        t!("mentioned_note")
                    )
                    .to_string(),
                ),
            ));
        }
    }

    blocks.push(lnk(
        thread_uri,
        Some(format!("{} {}", emo_thread(), t!("thread"))),
    ));
    blocks.push(lnk(
        reply_uri,
        Some(format!(
            "{} {}",
            emoji_by_code("speech_balloon"),
            t!("reply")
        )),
    ));
    blocks.push(lnk(
        repost_uri,
        Some(format!("{} {}", EMOJI_MEGAPHONE, t!("repost"))),
    ));

    if mode.contains(NoteRenderOptions::PUBLIC_BOOKMARK) {
        blocks.push(lnk(
            bookmark_uri,
            Some(format!("{} {}", EMOJI_BOOKMARK, t!("bookmark_public"))),
        ));
    }

    if mode.contains(NoteRenderOptions::DELETE) {
        blocks.push(lnk(
            delete_uri,
            Some(format!("{} {}", EMOJI_WASTEBASKET, t!("delete_note"))),
        ));
    }

    blocks.push(lnk(
        react_uri,
        Some(format!("{} {}", emoji_by_code("smile"), t!("react"))),
    ));
    blocks.push(Block::Empty);

    Ok(blocks)
}

pub async fn render_notes(
    events: Events,
    client: &Client,
    idx_start: Option<usize>,
    idx_end: Option<usize>,
    mode: NoteRenderOptions,
) -> Result<Vec<Block>, Box<dyn Error>> {
    let mut gemb = vec![];
    let evcount = events.len();

    let start = idx_start.unwrap_or_default();
    let end = match idx_end {
        Some(idx) => {
            if evcount < idx {
                evcount
            } else {
                idx
            }
        }
        None => evcount,
    };

    for event in &events.to_vec()[start..end] {
        let usermeta = nostru::get_user_metadata(client, event.pubkey).await;

        match render_note(client, event, usermeta, mode).await {
            Ok(mut blocks) => {
                gemb.append(&mut blocks);
            }
            Err(_err) => {
                gemb.push(Block::Text(
                    Content::new("Cannot process note").unwrap(),
                ));
            }
        }
    }

    Ok(gemb)
}
