#![cfg_attr(target_os = "android", allow(dead_code, unused_variables))]

use std::io;

use crate::Error;
use base64::{prelude::BASE64_URL_SAFE, Engine};
use ehttp::Request;
use nostrdb::{Note, NoteBuilder};
use notedeck::{
    media::images::fetch_binary_from_disk,
    platform::file::{MediaFrom, SelectedMedia},
};
use poll_promise::Promise;
use sha2::{Digest, Sha256};
use url::Url;

pub const NOSTR_BUILD_URL: fn() -> Url = || Url::parse("http://nostr.build").unwrap();
const NIP96_WELL_KNOWN: &str = ".well-known/nostr/nip96.json";

fn get_upload_url(nip96_url: Url) -> Promise<Result<String, Error>> {
    let request = Request::get(nip96_url);
    let (sender, promise) = Promise::new();

    ehttp::fetch(request, move |response| {
        let result = match response {
            Ok(resp) => {
                if resp.status == 200 {
                    if let Some(text) = resp.text() {
                        get_api_url_from_json(text)
                    } else {
                        Err(Error::Generic(
                            "ehttp::Response payload is not text".to_owned(),
                        ))
                    }
                } else {
                    Err(Error::Generic(format!(
                        "ehttp::Response status: {}",
                        resp.status
                    )))
                }
            }
            Err(e) => Err(Error::Generic(e)),
        };

        sender.send(result);
    });

    promise
}

fn get_api_url_from_json(json: &str) -> Result<String, Error> {
    match serde_json::from_str::<serde_json::Value>(json) {
        Ok(json) => {
            if let Some(url) = json
                .get("api_url")
                .and_then(|url| url.as_str())
                .map(|url| url.to_string())
            {
                Ok(url)
            } else {
                Err(Error::Generic(
                    "api_url key not found in ehttp::Response".to_owned(),
                ))
            }
        }
        Err(e) => Err(Error::Generic(e.to_string())),
    }
}

fn get_upload_url_from_provider(mut provider_url: Url) -> Promise<Result<String, Error>> {
    provider_url.set_path(NIP96_WELL_KNOWN);
    get_upload_url(provider_url)
}

pub fn get_nostr_build_upload_url() -> Promise<Result<String, Error>> {
    get_upload_url_from_provider(NOSTR_BUILD_URL())
}

fn create_nip98_note(seckey: &[u8; 32], upload_url: String, payload_hash: String) -> Note<'_> {
    NoteBuilder::new()
        .kind(27235)
        .start_tag()
        .tag_str("u")
        .tag_str(&upload_url)
        .start_tag()
        .tag_str("method")
        .tag_str("POST")
        .start_tag()
        .tag_str("payload")
        .tag_str(&payload_hash)
        .sign(seckey)
        .build()
        .expect("build note")
}

fn create_nip96_request(
    upload_url: &str,
    file_name: &str,
    media_type: &str,
    file_contents: Vec<u8>,
    nip98_base64: &str,
) -> ehttp::Request {
    let boundary = "----boundary";

    let mut body = format!(
        "--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{file_name}\"\r\nContent-Type: {media_type}\r\n\r\n",
    )
    .into_bytes();
    body.extend(file_contents);
    body.extend(format!("\r\n--{boundary}--\r\n").as_bytes());

    let headers = ehttp::Headers::new(&[
        (
            "Content-Type",
            format!("multipart/form-data; boundary={boundary}").as_str(),
        ),
        ("Authorization", format!("Nostr {nip98_base64}").as_str()),
    ]);

    Request {
        method: "POST".to_string(),
        url: upload_url.to_string(),
        headers,
        body,
    }
}

fn sha256_hex(contents: &Vec<u8>) -> String {
    let mut hasher = Sha256::new();
    hasher.update(contents);
    let hash = hasher.finalize();
    hex::encode(hash)
}

pub fn nip96_upload(
    seckey: [u8; 32],
    upload_url: String,
    selected_media: SelectedMedia,
) -> Promise<Result<Nip94Event, Error>> {
    internal_nip96_upload(seckey, upload_url, selected_media)
}

pub fn nostrbuild_nip96_upload(
    seckey: [u8; 32],
    selected_media: SelectedMedia,
) -> Promise<Result<Nip94Event, Error>> {
    let (sender, promise) = Promise::new();
    std::thread::spawn(move || {
        let upload_url = match get_nostr_build_upload_url().block_and_take() {
            Ok(url) => url,
            Err(e) => {
                sender.send(Err(Error::Generic(format!(
                    "could not get nostrbuild upload url: {e}"
                ))));
                return;
            }
        };

        let res = nip96_upload(seckey, upload_url, selected_media).block_and_take();
        sender.send(res);
    });
    promise
}

fn internal_nip96_upload(
    seckey: [u8; 32],
    upload_url: String,
    selected_media: SelectedMedia,
) -> Promise<Result<Nip94Event, Error>> {
    let file_name = selected_media.file_name;
    let mime_type = selected_media.media_type.to_mime();
    let bytes_res = bytes_from_media(selected_media.from);

    let file_contents = match bytes_res {
        Ok(bytes) => bytes,
        Err(e) => {
            return Promise::from_ready(Err(Error::Generic(format!(
                "could not read contents of file to upload: {e}"
            ))));
        }
    };

    let file_hash = sha256_hex(&file_contents);
    let nip98_note = create_nip98_note(&seckey, upload_url.to_owned(), file_hash);

    let nip98_base64 = match nip98_note.json() {
        Ok(json) => BASE64_URL_SAFE.encode(json),
        Err(e) => return Promise::from_ready(Err(Error::Generic(e.to_string()))),
    };

    let request = create_nip96_request(
        &upload_url,
        &file_name,
        mime_type,
        file_contents,
        &nip98_base64,
    );

    let (sender, promise) = Promise::new();

    ehttp::fetch(request, move |response| {
        let maybe_uploaded_media = match response {
            Ok(response) => {
                if response.ok {
                    match String::from_utf8(response.bytes.clone()) {
                        Ok(str_response) => find_nip94_ev_in_json(str_response),
                        Err(e) => Err(Error::Generic(e.to_string())),
                    }
                } else {
                    let kind = match response.status {
                        400..=499 => io::ErrorKind::InvalidInput,
                        500..=599 => io::ErrorKind::ConnectionAborted,
                        _ => io::ErrorKind::Other,
                    };
                    let err_msg = format!(
                        "ehttp Response was unsuccessful. Code {} with message: {}",
                        response.status, response.status_text
                    );

                    Err(Error::Io(io::Error::new(kind, err_msg)))
                }
            }
            Err(e) => Err(Error::Generic(e)),
        };

        sender.send(maybe_uploaded_media);
    });

    promise
}

fn find_nip94_ev_in_json(json: String) -> Result<Nip94Event, Error> {
    match serde_json::from_str::<serde_json::Value>(&json) {
        Ok(v) => {
            let tags = v["nip94_event"]["tags"].clone();
            let content = v["nip94_event"]["content"]
                .as_str()
                .unwrap_or_default()
                .to_string();
            match serde_json::from_value::<Vec<Vec<String>>>(tags) {
                Ok(tags) => Nip94Event::from_tags_and_content(tags, content)
                    .map_err(|e| Error::Generic(e.to_owned())),
                Err(e) => Err(Error::Generic(e.to_string())),
            }
        }
        Err(e) => Err(Error::Generic(e.to_string())),
    }
}

pub fn bytes_from_media(media: MediaFrom) -> Result<Vec<u8>, notedeck::Error> {
    match media {
        MediaFrom::PathBuf(full_path) => fetch_binary_from_disk(full_path.clone()),
        MediaFrom::Memory(bytes) => Ok(bytes),
    }
}

#[derive(Clone, Debug, serde::Deserialize)]
pub struct Nip94Event {
    pub url: String,
    pub ox: Option<String>,
    pub x: Option<String>,
    pub media_type: Option<String>,
    pub dimensions: Option<(u32, u32)>,
    pub blurhash: Option<String>,
    pub thumb: Option<String>,
    pub content: String,
}

impl Nip94Event {
    pub fn new(url: String, width: u32, height: u32) -> Self {
        Self {
            url,
            ox: None,
            x: None,
            media_type: None,
            dimensions: Some((width, height)),
            blurhash: None,
            thumb: None,
            content: String::new(),
        }
    }
}

const URL: &str = "url";
const OX: &str = "ox";
const X: &str = "x";
const M: &str = "m";
const DIM: &str = "dim";
const BLURHASH: &str = "blurhash";
const THUMB: &str = "thumb";

impl Nip94Event {
    fn from_tags_and_content(
        tags: Vec<Vec<String>>,
        content: String,
    ) -> Result<Self, &'static str> {
        let mut url = None;
        let mut ox = None;
        let mut x = None;
        let mut media_type = None;
        let mut dimensions = None;
        let mut blurhash = None;
        let mut thumb = None;

        for tag in tags {
            match tag.as_slice() {
                [key, value] if key == URL => url = Some(value.to_string()),
                [key, value] if key == OX => ox = Some(value.to_string()),
                [key, value] if key == X => x = Some(value.to_string()),
                [key, value] if key == M => media_type = Some(value.to_string()),
                [key, value] if key == DIM => {
                    if let Some((w, h)) = value.split_once('x') {
                        if let (Ok(w), Ok(h)) = (w.parse::<u32>(), h.parse::<u32>()) {
                            dimensions = Some((w, h));
                        }
                    }
                }
                [key, value] if key == BLURHASH => blurhash = Some(value.to_string()),
                [key, value] if key == THUMB => thumb = Some(value.to_string()),
                _ => {}
            }
        }

        Ok(Self {
            url: url.ok_or("Missing url")?,
            ox,
            x,
            media_type,
            dimensions,
            blurhash,
            thumb,
            content,
        })
    }
}

#[cfg(test)]
mod tests {
    use std::{fs, path::PathBuf, str::FromStr};

    use enostr::FullKeypair;

    use crate::media_upload::{
        get_upload_url_from_provider, nostrbuild_nip96_upload, SelectedMedia, NOSTR_BUILD_URL,
    };

    use super::internal_nip96_upload;

    #[test]
    fn test_nostrbuild_upload_url() {
        let promise = get_upload_url_from_provider(NOSTR_BUILD_URL());

        let url = promise.block_until_ready();

        assert!(url.is_ok());
    }

    #[test]
    #[ignore] // this test should not run automatically since it sends data to a real server
    fn test_internal_nip96() {
        // just a random image to test image upload
        let file_path = PathBuf::from_str("../../../assets/damus_rounded_80.png").unwrap();
        let selected_media = SelectedMedia::from_path(file_path).unwrap();
        let promise = get_upload_url_from_provider(NOSTR_BUILD_URL());
        let kp = FullKeypair::generate();
        println!("Using pubkey: {:?}", kp.pubkey);

        if let Ok(upload_url) = promise.block_until_ready() {
            let promise = internal_nip96_upload(
                kp.secret_key.secret_bytes(),
                upload_url.to_string(),
                selected_media,
            );
            let res = promise.block_until_ready();
            assert!(res.is_ok())
        } else {
            panic!()
        }
    }

    #[tokio::test]
    #[ignore] // this test should not run automatically since it sends data to a real server
    async fn test_nostrbuild_nip96() {
        // just a random image to test image upload
        let file_path =
            fs::canonicalize(PathBuf::from_str("../../assets/damus_rounded_80.png").unwrap())
                .unwrap();
        let selected_media = SelectedMedia::from_path(file_path).unwrap();
        let kp = FullKeypair::generate();
        println!("Using pubkey: {:?}", kp.pubkey);

        let promise = nostrbuild_nip96_upload(kp.secret_key.secret_bytes(), selected_media);

        let out = promise.block_and_take();
        assert!(out.is_ok());
    }
}
