use std::{
    collections::BTreeMap,
    fmt::{self, Display},
};

use crate::types::{temporal, Class, KnownClass};
use time::{format_description, OffsetDateTime};
use url::Url;

use super::{Object, Property, PropertyList};

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("The feed was not declared with the correct type")]
    FeedWrongType,

    #[error("The feed did not have a declared name")]
    FeedMissingName,

    #[error("An entry of this feed was missing a name.")]
    ChildItemNeedsSingleName,

    #[error("An entry with the property {0:} doesn't have a datetime value set.")]
    ChildItemShouldHaveTimestamp(String),

    #[error("A feed was missing its root URL.")]
    FeedMissingUrl,

    #[cfg(feature = "atom_syndication")]
    #[error(transparent)]
    Atom(#[from] AtomError),

    #[cfg(feature = "atom_syndication")]
    #[error("The feed's category did not provide a name.")]
    FeedCategoryMissingName,

    #[cfg(feature = "atom_syndication")]
    #[error("The value {0:#?} could not be used as an ATOM datetime.")]
    InvalidPropertyValueForDate(Property),

    #[cfg(feature = "atom_syndication")]
    #[error("The feed needs a time at which it's been updated.")]
    FeedMissingUpdateTime,
}

#[derive(thiserror::Error, Debug)]
pub struct AtomError(String);

impl From<atom_syndication::Error> for AtomError {
    fn from(err: atom_syndication::Error) -> Self {
        Self(err.to_string())
    }
}

impl Display for AtomError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default)]
pub struct Feed(Object);

impl From<Object> for Feed {
    fn from(mut value: Object) -> Self {
        value.insert_context_uri();
        value.extract_references();
        value.insert("type".to_string(), "feed".to_string().try_into().unwrap());
        Self(value)
    }
}

impl From<Feed> for Object {
    fn from(feed: Feed) -> Self {
        feed.0
    }
}

impl std::ops::DerefMut for Feed {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl std::ops::Deref for Feed {
    type Target = Object;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl Feed {
    pub fn validate(&self) -> Result<(), crate::Error> {
        if self.0.r#type() != Class::Known(KnownClass::Feed) {
            return Err(Error::FeedWrongType.into());
        }

        if self.0.get("name").filter(|pv| pv.is_string()).is_none() {
            return Err(Error::FeedMissingName.into());
        }

        let children = self.0.children();

        if !children
            .iter()
            .all(|child| child.get("name").filter(|pv| pv.is_string()).is_some())
        {
            return Err(Error::ChildItemNeedsSingleName.into());
        }

        let uids = children
            .iter()
            .flat_map(|child| child.get("uid").filter(|pv| pv.is_string() || pv.is_url()))
            .cloned()
            .collect::<PropertyList>();

        if uids.len() != children.len() {
            // NOTE: This is a naive size check, not uniqueness check.
            return Err(Error::ChildItemNeedsSingleName.into());
        }

        if children.iter().all(|child| {
            let dtp_opt = child.get("updated");

            if let Some(Property::Temporal(temporal::Value::Timestamp(dtp))) = dtp_opt {
                dtp.is_stamp()
            } else {
                true
            }
        }) {
            return Err(Error::ChildItemShouldHaveTimestamp("published".to_string()).into());
        }

        if children.iter().all(|child| {
            let dtp_opt = child.get("published");

            if let Some(Property::Temporal(temporal::Value::Timestamp(dtp))) = dtp_opt {
                dtp.is_stamp()
            } else {
                true
            }
        }) {
            return Err(Error::ChildItemShouldHaveTimestamp("updated".to_string()).into());
        }

        Ok(())
    }

    fn references(&self) -> BTreeMap<String, Object> {
        self.get("references")
            .and_then(|ref_value| ref_value.as_object_list().cloned())
            .unwrap_or_default()
    }

    pub fn new(items: Vec<Object>) -> Self {
        let mut object = Object::default();
        object.set_children(items);
        Self(object)
    }
}

#[cfg(feature = "atom_syndication")]
impl TryInto<atom_syndication::Person> for Object {
    type Error = crate::Error;

    fn try_into(self) -> Result<atom_syndication::Person, Self::Error> {
        let mut builder = atom_syndication::PersonBuilder::default();

        if let Some(value) = self
            .get("name")
            .and_then(|name_value| name_value.as_string().to_owned())
        {
            builder.name(value);
        }

        let email_uri = self
            .get("url")
            .and_then(|value| value.as_list())
            .and_then(|url_values| {
                url_values
                    .iter()
                    .flat_map(|v| v.as_string())
                    .find(|value| value.starts_with("mailto:"))
                    .cloned()
                    .or_else(|| {
                        url_values
                            .iter()
                            .flat_map(|v| v.as_url())
                            .find(|value| value.scheme() == "mailto")
                            .map(|u| u.path().to_string())
                    })
            });

        builder.email(email_uri);

        let author_uri = self
            .get("url")
            .and_then(|value| value.as_list())
            .and_then(|url_values| {
                url_values
                    .iter()
                    .flat_map(|v| v.as_url())
                    .find(|value| value.scheme() == "http" || value.scheme() == "https")
                    .cloned()
            })
            .map(|u| u.to_string());

        builder.uri(author_uri);

        Ok(builder.build())
    }
}

#[cfg(feature = "atom_syndication")]
impl TryInto<atom_syndication::Category> for Object {
    type Error = crate::Error;

    fn try_into(self) -> Result<atom_syndication::Category, Self::Error> {
        let mut builder = atom_syndication::CategoryBuilder::default();

        if let Some(name) = self.get("name").and_then(|v| v.as_string()) {
            builder.term(name.to_owned());
        }

        builder.scheme(
            self.get("url")
                .and_then(|v| v.as_url())
                .map(|u| u.to_string()),
        );

        // NOTE: How to determine/set 'label'?
        Ok(builder.build())
    }
}

#[cfg(feature = "atom_syndication")]
impl TryInto<atom_syndication::Entry> for Object {
    type Error = crate::Error;

    fn try_into(self) -> Result<atom_syndication::Entry, Self::Error> {
        let mut builder = atom_syndication::EntryBuilder::default();

        let content_url = self
            .get("url")
            .and_then(|v| v.as_url())
            .cloned()
            .ok_or_else(|| crate::Error::Jf2Profile(Error::FeedMissingUrl))?;

        if let Some(name) = self.get("name").and_then(|v| v.as_string()) {
            let mut title = atom_syndication::TextBuilder::default();
            title.r#type(atom_syndication::TextType::Text);
            title.value(name);

            // TODO: title.lang(value);
            // TODO: title.base(base_url);

            builder.title(title.build());
        }

        builder.link(atom_syndication::Link {
            href: content_url.to_string(),
            rel: "canonical".to_string(),
            hreflang: None,
            mime_type: None,
            title: None,
            length: None,
        });

        // FIXME: Should be the UID of this.
        builder.id(content_url.to_string());

        if let Some(content) = self.get("content") {
            let (_ty, value) = if let Some(html) = content.as_html() {
                (atom_syndication::TextType::Html, html.to_owned())
            } else if let Some(text) = content.as_text() {
                (atom_syndication::TextType::Text, text.to_owned())
            } else {
                (atom_syndication::TextType::Text, Default::default())
            };

            if let Some(temporal::Value::Timestamp(updated_ts)) =
                self.get("updated").and_then(|dtpv| dtpv.as_temporal())
            {
                builder.updated(temporal_into_feed_date(updated_ts)?);
            }

            builder.content(if value.is_empty() {
                None
            } else {
                Some(
                    atom_syndication::ContentBuilder::default()
                        .src(Some(content_url.to_string()))
                        .value(value)
                        .content_type(Some("html".to_string()))
                        .build(),
                )
            });
        }

        if let Some(author) = self
            .get("author")
            .and_then(|author_value| author_value.as_object().cloned())
        {
            let author_person: atom_syndication::Person = author.try_into()?;
            builder.author(author_person);
        } else if let Some(authors) = self
            .get("author")
            .and_then(|authors_value| authors_value.as_list().cloned())
        {
            let authors_persons = authors.iter().try_fold(
                Vec::default(),
                |mut acc, val| -> Result<Vec<atom_syndication::Person>, crate::Error> {
                    if let Property::Subobject(obj) = val {
                        let person = obj.clone().try_into()?;
                        acc.push(person);
                    }

                    Ok(acc)
                },
            )?;
            builder.authors(authors_persons);
        }

        if let Some(category_value) = self.get("category").cloned().filter(|v| !v.is_empty()) {
            let categories = extract_categories(category_value)?;
            builder.categories(categories);
        }

        if let Some(temporal::Value::Timestamp(stamp)) =
            self.get("published").and_then(|v| v.as_temporal().cloned())
        {
            builder.published(temporal_into_feed_date(&stamp)?);
        }
        if let Some(temporal::Value::Timestamp(stamp)) =
            self.get("updated").and_then(|v| v.as_temporal().cloned())
        {
            builder.updated(temporal_into_feed_date(&stamp)?);
        }

        if let Some(summary_text) = self.get("summary").and_then(|v| v.as_text().cloned()) {
            // TODO: text.lang
            // TODO: text.base
            builder.summary(
                atom_syndication::TextBuilder::default()
                    .value(summary_text)
                    .build(),
            );
        }

        Ok(builder.build())
    }
}

#[cfg(feature = "atom_syndication")]
fn temporal_into_feed_date(
    stamp: &temporal::Stamp,
) -> Result<atom_syndication::FixedDateTime, crate::Error> {
    if stamp.is_stamp() {
        let concrete_dt: OffsetDateTime = stamp.clone().try_into()?;
        let concrete_dt_str = concrete_dt
            .format(&format_description::well_known::Rfc3339)
            .map_err(time::Error::Format)
            .map_err(temporal::Error::Time)
            .map_err(microformats_types::Error::Temporal)
            .map_err(crate::Error::Types)?;
        Ok(
            atom_syndication::FixedDateTime::parse_from_rfc3339(&concrete_dt_str)
                .map_err(|_| atom_syndication::Error::WrongDatetime(concrete_dt_str))
                .map_err(AtomError::from)
                .map_err(Error::Atom)?,
        )
    } else {
        Err(Error::Atom(atom_syndication::Error::WrongDatetime(stamp.to_string()).into()).into())
    }
}

#[cfg(feature = "atom_syndication")]
impl TryFrom<Feed> for atom_syndication::Feed {
    type Error = crate::Error;

    fn try_from(jf2_feed: Feed) -> Result<Self, Self::Error> {
        let mut atom_feed = atom_syndication::Feed::default();
        let uri = jf2_feed.url().ok_or(Error::FeedMissingUrl)?;
        atom_feed.set_id(uri);

        let photo_value = jf2_feed
            .get("photo")
            .and_then(|v| v.as_url())
            .map(|s| s.to_string());
        atom_feed.set_icon(photo_value.clone());
        atom_feed.set_logo(photo_value);

        let lang_value = jf2_feed.get("lang").and_then(|pv| pv.as_string()).cloned();
        atom_feed.set_lang(lang_value);

        if let Some(title_value) = jf2_feed.get("name").and_then(|pv| pv.as_string()) {
            atom_feed.set_title(title_value.to_owned());
        }

        if let Some(temporal::Value::Timestamp(stamp)) =
            jf2_feed.get("updated").and_then(|v| v.as_temporal())
        {
            atom_feed.set_updated(temporal_into_feed_date(stamp)?);
        } else {
            return Err(Error::FeedMissingUpdateTime.into());
        }

        if let Some(authors) = jf2_feed.get("author").cloned().filter(|v| !v.is_empty()) {
            let author_objs = authors.into_list().into_iter().try_fold(
                Vec::default(),
                |mut acc, author_value| -> Result<Vec<atom_syndication::Person>, crate::Error> {
                    let author_obj = if let Property::Url(author_url) = author_value {
                        jf2_feed.references().get(author_url.as_str()).cloned()
                    } else if let Property::Subobject(author_obj) = author_value {
                        Some(author_obj)
                    } else {
                        return Ok(acc);
                    };

                    if let Some(obj) = author_obj {
                        let author_person = obj.try_into()?;
                        acc.push(author_person);
                    }

                    Ok(acc)
                },
            )?;

            atom_feed.set_authors(author_objs);
        }

        if let Some(category_value) = jf2_feed.get("category").cloned().filter(|v| !v.is_empty()) {
            let categories = extract_categories(category_value)?;
            atom_feed.set_categories(categories);
        }

        let entries = jf2_feed.children().into_iter().try_fold(
            Vec::default(),
            |mut acc, child_value| -> Result<Vec<atom_syndication::Entry>, crate::Error> {
                acc.push(child_value.try_into()?);
                Ok(acc)
            },
        )?;

        atom_feed.set_entries(entries);

        Ok(atom_feed)
    }
}

fn extract_categories(value: Property) -> Result<Vec<atom_syndication::Category>, crate::Error> {
    value.into_list().into_iter().try_fold(
        Vec::default(),
        |mut acc, v| -> Result<Vec<atom_syndication::Category>, crate::Error> {
            let value = match v {
                Property::Url(u) => Some(
                    atom_syndication::CategoryBuilder::default()
                        .scheme(Some(u.to_string()))
                        .term(u.to_string())
                        .build(),
                ),
                Property::Subobject(obj) => Some(obj.try_into()?),
                Property::String(term) => Some(
                    atom_syndication::CategoryBuilder::default()
                        .term(term)
                        .build(),
                ),
                _ => None,
            };

            if let Some(vk) = value {
                acc.push(vk)
            };

            Ok(acc)
        },
    )
}

#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default)]
pub struct Card(Object);

impl From<Object> for Card {
    fn from(mut value: Object) -> Self {
        value.insert_context_uri();
        value.extract_references();
        Self(value)
    }
}

impl std::ops::DerefMut for Card {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl std::ops::Deref for Card {
    type Target = Object;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl Card {
    pub fn validate(&self) -> Result<(), crate::Error> {
        if self.0.r#type() != Class::Known(KnownClass::Card) {
            return Err(Error::FeedWrongType.into());
        }

        // TODO: Check if it provides a name.
        // TODO: Check if it provides a photo.
        // TODO: Check if it provides a url.

        Ok(())
    }

    pub fn name(&self) -> String {
        self.get("name")
            .and_then(|pv| pv.as_string())
            .cloned()
            .unwrap_or_default()
    }

    pub fn email(&self) -> Option<String> {
        self.get("email").and_then(|pv| pv.as_string()).cloned()
    }

    pub fn photo(&self) -> Option<Url> {
        self.get("photo").and_then(|pv| pv.as_url()).cloned()
    }

    pub fn url(&self) -> Option<Url> {
        self.get("url").and_then(|pv| pv.as_url()).cloned()
    }
}
