use std::collections::{HashMap, HashSet};

use async_stream::try_stream;
use serde::{Deserialize, Serialize};

use crate::{
    config::Session,
    data::{RequestArgumenter, RequestExt},
};

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct LinkedPixivUser {
    #[serde(deserialize_with = "super::de_str_to_u64")]
    #[expect(unused)]
    pub user_id: u64,
    pub name: String,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct FetchPostCover {
    #[expect(unused)]
    r#type: String,
    #[expect(unused)]
    url: String,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct FetchPost {
    #[serde(deserialize_with = "super::de_str_to_u64")]
    pub id: u64,
    pub title: String,
    pub fee_required: u64,
    pub published_datetime: chrono::DateTime<chrono::FixedOffset>,
    pub updated_datetime: chrono::DateTime<chrono::FixedOffset>,
    #[expect(unused)]
    pub tags: Vec<String>,

    #[expect(unused)]
    pub is_liked: bool,
    #[expect(unused)]
    pub like_count: usize,
    #[expect(unused)]
    pub is_commenting_restricted: bool,
    #[expect(unused)]
    pub comment_count: usize,

    pub is_restricted: bool,

    #[expect(unused)]
    pub user: Option<LinkedPixivUser>,
    pub creator_id: String,
    #[expect(unused)]
    pub has_adult_content: bool,
    #[expect(unused)]
    pub cover: Option<FetchPostCover>,
    #[expect(unused)]
    pub excerpt: String,
    #[expect(unused)]
    pub is_pinned: bool,
}

#[derive(Deserialize, Serialize, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum FetchPostBlock {
    #[serde(rename = "p")]
    Paragraph {
        text: String,
    },

    Header {
        text: String,
    },

    #[serde(rename_all = "camelCase")]
    File {
        file_id: String,
    },

    #[serde(rename_all = "camelCase")]
    Image {
        image_id: String,
    },

    #[serde(rename_all = "camelCase")]
    Embed {
        embed_id: String,
        content: Option<serde_json::Value>,
    },

    #[serde(rename_all = "camelCase")]
    UrlEmbed {
        url_embed_id: String,
        content: Option<serde_json::Value>,
    },
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct FetchPostImage {
    pub id: String,
    pub extension: String,
    pub width: u64,
    pub height: u64,
    pub original_url: String,
    #[expect(unused)]
    pub thumbnail_url: String,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct FetchPostFile {
    pub id: String,
    pub name: String,
    pub extension: String,
    pub size: u64,
    pub url: String,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct FetchPostBodyRichRaw {
    pub blocks: Vec<FetchPostBlock>,
    pub image_map: HashMap<String, FetchPostImage>,
    pub file_map: HashMap<String, FetchPostFile>,
    pub embed_map: HashMap<String, serde_json::Value>,
    pub url_embed_map: HashMap<String, serde_json::Value>,
}

#[derive(Debug, Deserialize)]
#[serde(try_from = "FetchPostBodyRichRaw")]
pub struct FetchPostBodyRich {
    pub blocks: Vec<FetchPostBlock>,
    pub images: Vec<(usize, FetchPostImage)>,
    pub files: Vec<(usize, FetchPostFile)>,
}

#[derive(thiserror::Error, Debug)]
pub enum FetchPostBodyConversionError {
    #[error("Unmatched image ID: {0}")]
    UnmatchedImageId(String),
    #[error("Unmatched file ID: {0}")]
    UnmatchedFileId(String),
    #[error("Unmatched embed ID: {0}")]
    UnmatchedEmbedId(String),
    #[error("Unmatched url_embed ID: {0}")]
    UnmatchedUrlEmbedId(String),
    #[error(
        "Extra unmapped items exist: image: {image:?} file: {file:?} embed: {embed:?} url_embed: {url_embed:?}"
    )]
    Extra {
        image: bool,
        file: bool,
        embed: bool,
        url_embed: bool,
    },
}

impl TryFrom<FetchPostBodyRichRaw> for FetchPostBodyRich {
    type Error = FetchPostBodyConversionError;

    fn try_from(mut raw: FetchPostBodyRichRaw) -> Result<Self, Self::Error> {
        tracing::debug!("Converting rich body: {:#?}", raw);
        let mut images = Vec::new();
        let mut files = Vec::new();

        for (idx, block) in raw.blocks.iter_mut().enumerate() {
            match block {
                FetchPostBlock::Image { image_id } => {
                    let inner = raw.image_map.remove(image_id).ok_or_else(|| {
                        FetchPostBodyConversionError::UnmatchedImageId(image_id.clone())
                    })?;
                    images.push((idx, inner));
                }
                FetchPostBlock::File { file_id } => {
                    let inner = raw.file_map.remove(file_id).ok_or_else(|| {
                        FetchPostBodyConversionError::UnmatchedFileId(file_id.clone())
                    })?;
                    files.push((idx, inner));
                }
                FetchPostBlock::Embed { embed_id, content } => {
                    let inner = raw.embed_map.remove(embed_id).ok_or_else(|| {
                        FetchPostBodyConversionError::UnmatchedEmbedId(embed_id.clone())
                    })?;
                    assert!(content.is_none());
                    *content = Some(inner);
                }
                FetchPostBlock::UrlEmbed {
                    url_embed_id,
                    content,
                } => {
                    let inner = raw.url_embed_map.remove(url_embed_id).ok_or_else(|| {
                        FetchPostBodyConversionError::UnmatchedUrlEmbedId(url_embed_id.clone())
                    })?;
                    assert!(content.is_none());
                    *content = Some(inner);
                }
                _ => {}
            }
        }

        let extra_embed = raw.embed_map.len() != 0;
        let extra_url_embed = raw.url_embed_map.len() != 0;
        let extra_image = raw.image_map.len() != 0;
        let extra_file = raw.file_map.len() != 0;
        if extra_embed || extra_url_embed || extra_image || extra_file {
            return Err(FetchPostBodyConversionError::Extra {
                embed: extra_embed,
                url_embed: extra_url_embed,
                image: extra_image,
                file: extra_file,
            });
        }

        tracing::debug!(
            "Conversion successful: images: {:#?}, files: {:#?}",
            images,
            files
        );

        Ok(Self {
            blocks: raw.blocks,
            images,
            files,
        })
    }
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct FetchPostBodySimple {
    pub text: String,
    #[serde(default)]
    pub images: Vec<FetchPostImage>,
    #[serde(default)]
    pub files: Vec<FetchPostFile>,
}

#[derive(Deserialize, Debug)]
pub struct FetchPostBodyLegacy {
    pub html: String,

    #[serde(skip)]
    pub scraper: Option<scraper::Html>,
    #[serde(skip)]
    pub images: Option<Vec<FetchPostImage>>,
    #[serde(skip)]
    pub files: Option<Vec<FetchPostFile>>,
}

impl FetchPostBodyLegacy {
    fn parse_filename(filename: &str) -> Option<(&str, &str)> {
        let mut segs = filename.splitn(2, ".");
        let id = segs.next()?;
        let ext = segs.next()?;
        if segs.next().is_some() {
            return None;
        }
        Some((id, ext))
    }

    fn try_parse_file_filename(content: &str) -> Option<&str> {
        // Content should be formatted like <filename> - <size>
        let mut content_parts = content.rsplitn(2, " - ");
        let size = content_parts.next().unwrap();
        if let Some(filename) = content_parts.next() {
            // Ignore size because it's unreliable
            tracing::warn!("Unreliable size in legacy post: {}: {}", filename, size);
            return Some(filename);
        }

        // Also try <filename> (Size)
        if content.ends_with(')') {
            let mut content_parts = content.rsplitn(2, " (");
            let size = content_parts.next().unwrap();
            if let Some(filename) = content_parts.next() {
                tracing::warn!(
                    "Unreliable size in legacy post: {}: {}",
                    filename,
                    &size[..(size.len() - 1)]
                );
                return Some(filename);
            }
        }

        return None;
    }

    fn parse(&mut self) {
        if self.scraper.is_none() {
            self.scraper = Some(scraper::Html::parse_document(&self.html));
        }

        let scraper = self.scraper.as_ref().unwrap();

        if self.images.is_none() {
            let parsed = scraper
                .select(&scraper::Selector::parse("img").unwrap())
                .filter_map(|img| {
                    let src = img.value().attr("src")?.trim();
                    if !src.starts_with("https://downloads.fanbox.cc")
                        && !src.starts_with("http://downloads.fanbox.cc")
                    {
                        return None;
                    }
                    let last_seg = src.rsplit('/').next().unwrap();
                    let (id, ext) = Self::parse_filename(last_seg).unwrap();

                    // Try to parse width and height from img
                    let width = img
                        .value()
                        .attr("width")
                        .and_then(|e| e.parse::<u64>().ok())
                        .unwrap_or(0);
                    let height = img
                        .value()
                        .attr("height")
                        .and_then(|e| e.parse::<u64>().ok())
                        .unwrap_or(0);

                    Some(FetchPostImage {
                        id: id.to_string(),
                        extension: ext.to_string(),
                        width,
                        height,
                        original_url: src.to_string(),
                        thumbnail_url: src.to_string(),
                    })
                })
                .collect();
            self.images = Some(parsed);
        }

        let file_image_ids: HashSet<_> = self
            .images
            .as_ref()
            .unwrap()
            .iter()
            .map(|e| e.id.as_str())
            .collect();

        tracing::debug!("All image ids: {:?}", file_image_ids);

        if self.files.is_none() {
            let parsed = scraper
                .select(&scraper::Selector::parse("a").unwrap())
                .filter_map(|a| {
                    let href = a.value().attr("href")?.trim();
                    if !href.starts_with("https://downloads.fanbox.cc")
                        && !href.starts_with("http://downloads.fanbox.cc")
                    {
                        return None;
                    }
                    tracing::debug!("Href: {}", href);
                    // Skipping anchors that are actually images
                    let last_seg: &str = href.rsplit('/').next().unwrap();
                    let (id, ext) = Self::parse_filename(last_seg).unwrap();
                    if file_image_ids.contains(id) {
                        return None;
                    }

                    // Assert that this element only has one text node
                    let mut children = a.children();
                    let text = children.next().unwrap();
                    tracing::debug!("Children: {:?}", text);
                    assert!(children.next().is_none());
                    assert!(text.value().is_text());
                    let content = text.value().as_text().unwrap().trim();
                    let filename = if let Some(filename) = Self::try_parse_file_filename(content) {
                        filename
                    } else {
                        tracing::warn!(
                            "Unable to parse filename from {}, using {}",
                            content,
                            last_seg
                        );
                        last_seg
                    };

                    Some(FetchPostFile {
                        id: id.to_string(),
                        url: href.to_string(),
                        name: filename.to_string(),
                        extension: ext.to_string(),
                        size: 0,
                    })
                })
                .collect();
            self.files = Some(parsed);
        }
    }

    pub fn images_iter(&mut self) -> impl Iterator<Item = (usize, &FetchPostImage)> {
        self.parse();
        self.images.as_ref().unwrap().iter().enumerate()
    }

    pub fn files_iter(&mut self) -> impl Iterator<Item = (usize, &FetchPostFile)> {
        self.parse();
        let img_len = self.images.as_ref().unwrap().len();
        self.files
            .as_ref()
            .unwrap()
            .iter()
            .enumerate()
            .map(move |(i, file)| (i + img_len, file))
    }
}

#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub enum FetchPostBody {
    Rich(FetchPostBodyRich),
    Simple(FetchPostBodySimple),
    Legacy(FetchPostBodyLegacy),
}

impl FetchPostBody {
    pub fn images<'a>(&'a mut self) -> Box<dyn Iterator<Item = (usize, &'a FetchPostImage)> + 'a> {
        match self {
            FetchPostBody::Rich(rich) => Box::new(rich.images.iter().map(|(idx, img)| (*idx, img))),
            FetchPostBody::Simple(simple) => Box::new(simple.images.iter().enumerate()),
            FetchPostBody::Legacy(legacy) => Box::new(legacy.images_iter()),
        }
    }

    pub fn files<'a>(&'a mut self) -> Box<dyn Iterator<Item = (usize, &'a FetchPostFile)> + 'a> {
        match self {
            FetchPostBody::Rich(rich) => {
                Box::new(rich.files.iter().map(|(idx, file)| (*idx, file)))
            }
            FetchPostBody::Simple(simple) => {
                let img_len = simple.images.len();
                Box::new(
                    simple
                        .files
                        .iter()
                        .enumerate()
                        .map(move |(i, file)| (i + img_len, file)),
                )
            }
            FetchPostBody::Legacy(legacy) => Box::new(legacy.files_iter()),
        }
    }

    pub fn text_repr(&self) -> anyhow::Result<String> {
        let txt = match self {
            FetchPostBody::Rich(rich) => serde_json::to_string(&rich.blocks)?,
            FetchPostBody::Simple(simple) => simple.text.clone(),
            FetchPostBody::Legacy(legacy) => legacy.html.clone(),
        };
        Ok(txt)
    }

    pub fn is_rich(&self) -> bool {
        matches!(self, FetchPostBody::Rich(_))
    }
}

#[derive(Deserialize, Debug)]
pub struct FetchPostDetail {
    #[serde(flatten)]
    pub post: FetchPost,

    pub body: Option<FetchPostBody>,
}

pub struct FanboxRequest<'a>(pub &'a Session);

impl<'a> RequestArgumenter for FanboxRequest<'a> {
    fn argument(self, req: wreq::RequestBuilder) -> anyhow::Result<wreq::RequestBuilder> {
        if let Some(ref full) = self.0.fanbox_header_full {
            let mut updated = req;
            for (hdr, val) in full.iter() {
                updated = updated.header(hdr, val);
            }
            return Ok(updated);
        }

        let updated = if let Some(ref cookie) = self.0.fanbox {
            req.header("Cookie", format!("FANBOXSESSID={};", cookie.cookie))
        } else {
            return Err(anyhow::anyhow!("Fanbox session is required"));
        };

        let updated = updated
            .header("Origin", "https://www.fanbox.cc")
            .header("Referer", "https://www.fanbox.cc/")
            .emulation(wreq_util::Emulation::Chrome140);
        Ok(updated)
    }
}

#[derive(Deserialize)]
#[serde(untagged)]
pub enum Response<T> {
    Errored { error: String },
    Success { body: T },
}

impl<T> Response<T> {
    pub fn into_body(self) -> anyhow::Result<T> {
        match self {
            Response::Errored { error } => Err(anyhow::anyhow!("Fanbox API error: {}", error)),
            Response::Success { body } => Ok(body),
        }
    }
}

pub async fn get_author_paginates(
    session: &Session,
    author_id: &str,
) -> anyhow::Result<Vec<String>> {
    let url = format!(
        "https://api.fanbox.cc/post.paginateCreator?creatorId={}&sort=newest",
        author_id
    );

    let json: Response<Vec<String>> = crate::fetch::fetch(|client| {
        Ok(client
            .get(&url)
            .prepare_with(FanboxRequest(session))?
            .build()?)
    })
    .await?;

    json.into_body()
}

pub fn fetch_author_posts(
    session: &Session,
    author_id: &str,
    skip_pages: usize,
) -> impl futures::Stream<Item = anyhow::Result<FetchPost>> {
    try_stream! {
        let paginates = get_author_paginates(session, author_id).await?;
        for (page, url) in paginates.iter().enumerate() {
            if page < skip_pages {
                tracing::info!("Skipping page {}/{}", page + 1, paginates.len());
                continue;
            }

            // FIXME: assert url format
            tracing::info!("Fetching page {}/{}", page + 1, paginates.len());

            let posts: Response<Vec<FetchPost>> = crate::fetch::fetch(|client| {
                Ok(client.get(url).prepare_with(FanboxRequest(session))?.build()?)
            }).await?;
            for post in posts.into_body()? {
                yield post;
            }
        }
    }
}

pub async fn fetch_post(session: &Session, post_id: u64) -> anyhow::Result<FetchPostDetail> {
    let url = format!("https://api.fanbox.cc/post.info?postId={}", post_id);

    let json: Response<FetchPostDetail> = crate::fetch::fetch(|client| {
        Ok(client
            .get(&url)
            .prepare_with(FanboxRequest(session))?
            .build()?)
    })
    .await?;
    json.into_body()
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SupportedCreator {
    #[serde(deserialize_with = "super::de_str_to_u64")]
    #[expect(unused)]
    pub id: u64,
    pub creator_id: String,
    pub user: Option<LinkedPixivUser>,
    #[expect(unused)]
    pub has_adult_content: bool,

    // Supporting plan
    #[expect(unused)]
    pub fee: u64,
    #[expect(unused)]
    pub title: String,
    #[expect(unused)]
    pub description: String,
}

pub async fn fetch_supporting_list(session: &Session) -> anyhow::Result<Vec<SupportedCreator>> {
    let url = "https://api.fanbox.cc/plan.listSupporting";
    let json: Response<Vec<SupportedCreator>> = crate::fetch::fetch(|client| {
        Ok(client
            .get(url)
            .prepare_with(FanboxRequest(session))?
            .build()?)
    })
    .await?;
    json.into_body()
}
