use std::cmp::Ordering;
use std::collections::HashMap;
use std::error::Error;
use std::ops::Sub;
use chrono::{DateTime, Duration, Utc};
use lemmy_db_schema::source::post::Post;
use serde_derive::{Deserialize, Serialize};
use url::Url;
use crate::{HTTP_CLIENT};
use crate::jnovel::PartInfo::{NoParts, Part};
use crate::jnovel::PostInfo::{Chapter, Volume};

static PAST_DAYS_ELIGIBLE: u8 = 4;

macro_rules! api_url {
    () => {
        "https://labs.j-novel.club/app/v1".to_string()
    };
}

macro_rules! jnc_base_url {
    () => {
        "https://j-novel.club".to_string()
    };
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct VolumesWrapper {
    volumes: Vec<VolumeDetail>,
    pagination: PaginationInfo,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct ChapterWrapper {
    parts: Vec<ChapterDetail>,
    pagination: PaginationInfo,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct PaginationInfo {
    limit: usize,
    skip: usize,
    #[serde(alias = "lastPage")]
    last_page: bool,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub(crate) struct Cover {
    #[serde(alias = "coverUrl")]
    pub(crate) cover: String,
    #[serde(alias = "thumbnailUrl")]
    pub(crate) thumbnail: String,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub(crate) struct VolumeDetail {
    pub(crate) title: String,
    pub(crate) slug: String,
    pub(crate) number: u8,
    pub(crate) publishing: String,
    #[serde(alias = "shortDescription")]
    pub(crate) short_description: String,
    pub(crate) cover: Cover,
}


#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub(crate) struct ChapterDetail {
    pub(crate) title: String,
    pub(crate) slug: String,
    pub(crate) launch: String,
    pub(crate) cover: Option<Cover>,
}

#[derive(Debug, Clone)]
pub(crate) struct LemmyPostInfo {
    title: String,
    url: Url,
}

#[derive(Debug, Copy, Clone)]
pub(crate) enum PartInfo {
    NoParts,
    Part(u8),
}

impl PartInfo {
    pub(crate) fn is_parts(&self) -> bool {
        match self {
            Part(_) => true,
            NoParts => false,
        }
    }

    pub(crate) fn is_no_parts(&self) -> bool {
        !self.is_parts()
    }
}

impl PartialEq for PartInfo {
    fn eq(&self, other: &Self) -> bool {
        let self_numeric = match self {
            Part(number) => number,
            NoParts => &0,
        };

        let other_numeric = match other {
            Part(number) => number,
            NoParts => &0,
        };

        self_numeric == other_numeric
    }
}

impl PartialOrd for PartInfo {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        if self.gt(other) { Some(Ordering::Greater) }
        else if self.eq(other) { Some(Ordering::Equal) }
        else { Some(Ordering::Less) }
    }

    fn lt(&self, other: &Self) -> bool {
        let self_numeric = match self {
            Part(number) => number,
            NoParts => &0,
        };

        let other_numeric = match other {
            Part(number) => number,
            NoParts => &0,
        };

        self_numeric < other_numeric
    }

    fn le(&self, other: &Self) -> bool {
        !self.gt(other)
    }

    fn gt(&self, other: &Self) -> bool {
        let self_numeric = match self {
            Part(number) => number,
            NoParts => &0,
        };

        let other_numeric = match other {
            Part(number) => number,
            NoParts => &0,
        };

        self_numeric > other_numeric
    }

    fn ge(&self, other: &Self) -> bool {
        !self.lt(other)
    }
}

#[derive(Debug, Clone)]
pub(crate) enum PostInfo {
    Chapter { part: PartInfo, volume: u8, lemmy_info: LemmyPostInfo },
    Volume { part: PartInfo, lemmy_info: LemmyPostInfo },
}

impl PartialEq for PostInfo {
    fn eq(&self, other: &Self) -> bool {
        let self_part = match self {
            Chapter {part, ..} => part,
            Volume {part, ..} => part,
        };

        let other_part = match other {
            Chapter {part, ..} => part,
            Volume {part, ..} => part,
        };

        self_part.eq(other_part)
    }
}

impl PartialOrd for PostInfo {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        if self.gt(other) { Some(Ordering::Greater) }
        else if self.eq(other) { Some(Ordering::Equal) }
        else { Some(Ordering::Less) }
    }

    fn lt(&self, other: &Self) -> bool {
        let self_part = match self {
            Chapter {part, ..} => part,
            Volume {part, ..} => part,
        };

        let other_part = match other {
            Chapter {part, ..} => part,
            Volume {part, ..} => part,
        };

        self_part < other_part
    }

    fn le(&self, other: &Self) -> bool {
        !self.gt(other)
    }

    fn gt(&self, other: &Self) -> bool {
        let self_part = match self {
            Chapter {part, ..} => part,
            Volume {part, ..} => part,
        };

        let other_part = match other {
            Chapter {part, ..} => part,
            Volume {part, ..} => part,
        };

        self_part > other_part
    }

    fn ge(&self, other: &Self) -> bool {
        !self.lt(other)
    }
}

pub(crate) async fn check_feed(series_slug: &str, series_has_parts: bool) -> Result<Vec<PostInfo>, Box<dyn Error>> {
     let response = HTTP_CLIENT
        .get(api_url!() + "/series/" + series_slug + "/volumes?format=json")
        .send()
        .await?
         .text()
         .await?;

    let mut volume_brief_data: VolumesWrapper = serde_json::from_str(&response)?;
    volume_brief_data.volumes.reverse(); // Makes breaking out of the volume loop easier

    // If no parts just use 0 as Part indicator as no Series with Parts has a Part 0
    let mut volume_map: HashMap<u8, PostInfo> = HashMap::new();
    let mut chapter_map: HashMap<u8, PostInfo> = HashMap::new();

    for volume in volume_brief_data.volumes.iter() {
        let publishing_date = DateTime::parse_from_rfc3339(&volume.publishing).unwrap();
        if publishing_date < Utc::now().sub(Duration::days(PAST_DAYS_ELIGIBLE as i64)) {
            match series_has_parts {
                true => continue,
                false => break,
            }
        }
        let new_part_info: PartInfo;

        if series_has_parts {
            let mut part_number: Option<u8> = None;
            let splits: Vec<&str> = volume.slug.split('-').collect();
            for (index, split) in splits.clone().into_iter().enumerate() {

                if split == "part" {
                    part_number = Some(splits[index +1]
                        .parse::<u8>()
                        .expect("Split Element after 'Part' should always be a number"));
                    break;
                }
            }

            match part_number {
                Some(number) => new_part_info = Part(number),
                None => {
                    println!("No Part found, assuming 1");
                    new_part_info = Part(1);
                },
            }
        }
        else {
            new_part_info = NoParts;
        }

        let post_url = format!("{}/series/{series_slug}#volume-{}", jnc_base_url!(), volume.number);
        let post_details = LemmyPostInfo {
            title: volume.title.clone(),
            url: Url::parse(&post_url).unwrap(),
        };

        let new_post_info = Volume {
            part: new_part_info,
            lemmy_info: post_details,
        };

        let part_id = match new_part_info {
            Part(number) => number,
            NoParts => 0,
        };

        if publishing_date <= Utc::now() {
            volume_map
                .entry(part_id)
                .and_modify(|val| {
                    if *val < new_post_info {
                        *val = new_post_info.clone()
                    }
                })
                .or_insert(new_post_info);
        }

        if let Some(part_info) = get_latest_part(&volume.slug).await? {
            let part_post_info = Chapter {
                part: new_part_info,
                volume: volume.number,
                lemmy_info: part_info
            };

            chapter_map
                .entry(part_id)
                .and_modify(|val| {
                    if *val < part_post_info {
                        *val = part_post_info.clone()
                    }
                })
                .or_insert(part_post_info);
        }
    }

    let mut result_vec: Vec<PostInfo> = volume_map.values().cloned().collect();
    let mut chapter_vec: Vec<PostInfo> = chapter_map.values().cloned().collect();
    result_vec.append(&mut chapter_vec);

    Ok(result_vec)
}

async fn get_latest_part(volume_slug: &str) -> Result<Option<LemmyPostInfo>, Box<dyn Error>> {
    let response = HTTP_CLIENT
        .get(api_url!() + "/volumes/" + volume_slug + "/parts?format=json")
        .send()
        .await?
        .text()
        .await?;

    let mut volume_parts_data: ChapterWrapper = serde_json::from_str(&response)?;
    volume_parts_data.parts.reverse(); // Makes breaking out of the parts loop easier

    let mut post_details: Option<LemmyPostInfo> = None;

    for part in volume_parts_data.parts.iter() {
        let publishing_date = DateTime::parse_from_rfc3339(&part.launch).unwrap();
        if publishing_date > Utc::now() {
            break
        }
        else if publishing_date < Utc::now().sub(Duration::days(PAST_DAYS_ELIGIBLE as i64)) {
            continue
        }

        let post_url = format!("{}/read/{}", jnc_base_url!(), part.slug);
        post_details = Some(LemmyPostInfo {
            title: part.title.clone(),
            url: Url::parse(&post_url).unwrap(),
        });

    }

    Ok(post_details)
}