use std::{collections::HashMap, borrow::Cow};

use html::{media::builders, inline_text::Anchor, content::builders::ArticleBuilder};
use microformats::types::{Class, KnownClass, Item, PropertyValue, temporal::Value as Temporal, Fragment};

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("wrong mf2 class, expected {expected:?}, got {got:?}")]
    WrongClass {
        expected: Vec<KnownClass>,
        got: Vec<Class>
    },
    #[error("entry lacks `uid` property")]
    NoUid,
    #[error("unexpected type of property value: expected {expected}, got {got:?}")]
    WrongValueType {
        expected: &'static str,
        got: PropertyValue
    },
    #[error("missing property: {0}")]
    MissingProperty(&'static str),
}

pub enum Image {
    Plain(url::Url),
    Accessible {
        src: url::Url,
        alt: String
    }
}

impl Image {
    pub fn build(self, img: &mut html::media::builders::ImageBuilder) -> &mut html::media::builders::ImageBuilder {
        match self {
            Image::Plain(url) => img.src(String::from(url)),
            Image::Accessible { src, alt } => img.src(String::from(src)).alt(alt)
        }
    }
}

pub struct Card {
    uid: url::Url,
    urls: Vec<url::Url>,
    name: String,
    note: Option<String>,
    photo: Image,
    pronouns: Vec<String>
}

impl TryFrom<Item> for Card {
    type Error = Error;

    fn try_from(card: Item) -> Result<Self, Self::Error> {
        if card.r#type.as_slice() != [Class::Known(KnownClass::Card)] {
            return Err(Error::WrongClass {
                expected: vec![KnownClass::Card],
                got: card.r#type
            })
        }

        let mut props = card.properties.take();
        let uid = {
            let uids = props.remove("uid").ok_or(Error::NoUid)?;
            if let Some(PropertyValue::Url(uid)) = uids.into_iter().take(1).next() {
                uid
            } else {
                return Err(Error::NoUid)
            }
        };

        Ok(Self {
            uid,
            urls: props.remove("url").unwrap_or_default().into_iter()
                .filter_map(|v| if let PropertyValue::Url(url) = v {
                    Some(url)
                } else {
                    None
                }).collect(),
            name: props.remove("name")
                .unwrap_or_default()
                .into_iter()
                .next()
                .ok_or(Error::MissingProperty("name"))
                .and_then(|v| match v {
                    PropertyValue::Plain(plain) => Ok(plain),
                    other => Err(Error::WrongValueType {
                        expected: "string",
                        got: other
                    })
                })?,
            note: props.remove("note")
                .unwrap_or_default()
                .into_iter()
                .next()
                .and_then(|v| match v {
                    PropertyValue::Plain(plain) => Some(Ok(plain)),
                    other => Some(Err(Error::WrongValueType {
                        expected: "string",
                        got: other
                    }))
                })
                .transpose()?,
            photo: props.remove("photo")
                .unwrap_or_default()
                .into_iter()
                .next()
                .ok_or(Error::MissingProperty("photo"))
                .and_then(|v| match v {
                    PropertyValue::Url(url) => Ok(Image::Plain(url)),
                    PropertyValue::Image(image) => Ok(Image::Accessible {
                        src: image.src,
                        alt: image.alt
                    }),
                    other => Err(Error::WrongValueType {
                        expected: "string",
                        got: other
                    })
                })?,
            pronouns: props.remove("pronoun")
                .unwrap_or_default()
                .into_iter()
                .map(|v| match v {
                    PropertyValue::Plain(plain) => Ok(plain),
                    other => Err(Error::WrongValueType {
                        expected: "string",
                        got: other
                    })
                })
                .collect::<Result<Vec<String>, _>>()?
        })
    }
}

impl Card {
    pub fn build_section(self, section: &mut html::content::builders::SectionBuilder) -> &mut html::content::builders::SectionBuilder {
        section
            .class("mini-h-card")
            .anchor(|a| a
                .class("larger u-author")
                .href(String::from(self.uid))
                .image(move |img| self.photo.build(img).loading("lazy"))
                .text(self.name)
            )
    }

    pub fn build(self, article: &mut html::content::builders::ArticleBuilder) -> &mut html::content::builders::ArticleBuilder {
        let urls: Vec<_> = self.urls.into_iter().filter(|u| *u != self.uid).collect();

        article
            .class("h-card")
            .image(move |builder| self.photo.build(builder))
            .heading_1(move |builder| {
                builder.anchor(|builder| builder
                    .class("u-url u-uid p-name")
                    .href(String::from(self.uid))
                    .text(self.name)
                )
            });

        if !self.pronouns.is_empty() {
            article.span(move |span| {
                span.text("(");
                self.pronouns.into_iter().for_each(|p| { span.text(p); });
                span.text(")")
            });
        }

        if let Some(note) = self.note {
            article.paragraph(move |p| p.class("p-note").text(note));
        }

        if !urls.is_empty() {
            article.paragraph(|p| p.text("Can be found elsewhere at:"));
            article.unordered_list(move |ul| {
                for url in urls {
                    let url = String::from(url);
                    ul.list_item(move |li| li.push({
                        Anchor::builder()
                            .href(url.clone())
                            .text(url)
                            .build()
                    }));
                }

                ul
            });
        }

        article
    }
}

impl TryFrom<PropertyValue> for Card {
    type Error = Error;

    fn try_from(v: PropertyValue) -> Result<Self, Self::Error> {
        match v {
            PropertyValue::Item(item) => item.take().try_into(),
            other => Err(Error::WrongValueType { expected: "h-card", got: other })
        }
    }
}

pub struct Cite {
    uid: url::Url,
    url: Vec<url::Url>,
    in_reply_to: Option<Vec<Citation>>,
    author: Card,
    published: Option<chrono::DateTime<chrono::FixedOffset>>,
    content: Content
}

impl TryFrom<Item> for Cite {
    type Error = Error;

    fn try_from(cite: Item) -> Result<Self, Self::Error> {
        if cite.r#type.as_slice() != [Class::Known(KnownClass::Cite)] {
            return Err(Error::WrongClass {
                expected: vec![KnownClass::Cite],
                got: cite.r#type
            })
        }

        todo!()
    }

}

pub enum Citation {
    Brief(url::Url),
    Full(Cite)
}

impl TryFrom<PropertyValue> for Citation {
    type Error = Error;
    fn try_from(v: PropertyValue) -> Result<Self, Self::Error> {
        match v {
            PropertyValue::Url(url) => Ok(Self::Brief(url)),
            PropertyValue::Item(item) => Ok(Self::Full(item.take().try_into()?)),
            other => Err(Error::WrongValueType {
                expected: "url or h-cite",
                got: other
            })
        }
    }
}

pub struct Content(Fragment);

impl From<Content> for html::content::Main {
    fn from(content: Content) -> Self {
        let mut builder = Self::builder();
        builder
            .class("e-content")
            .text(content.0.html);
        if let Some(lang) = content.0.lang {
            builder.lang(Cow::Owned(lang));
        }
        builder.build()
    }
}

pub struct Entry {
    uid: url::Url,
    url: Vec<url::Url>,
    in_reply_to: Option<Citation>,
    author: Card,
    category: Vec<String>,
    syndication: Vec<url::Url>,
    published: chrono::DateTime<chrono::FixedOffset>,
    content: Content
}


impl TryFrom<Item> for Entry {
    type Error = Error;
    fn try_from(entry: Item) -> Result<Self, Self::Error> {
        if entry.r#type.as_slice() != [Class::Known(KnownClass::Entry)] {
            return Err(Error::WrongClass {
                expected: vec![KnownClass::Entry],
                got: entry.r#type
            })
        }

        let mut props = entry.properties.take();
        let uid = {
            let uids = props.remove("uid").ok_or(Error::NoUid)?;
            if let Some(PropertyValue::Url(uid)) = uids.into_iter().take(1).next() {
                uid
            } else {
                return Err(Error::NoUid)
            }
        };
        Ok(Entry {
            uid,
            url: props.remove("url").unwrap_or_default().into_iter()
                .filter_map(|v| if let PropertyValue::Url(url) = v {
                    Some(url)
                } else {
                    None
                }).collect(),
            in_reply_to: props.remove("in-reply-to")
                .unwrap_or_default()
                .into_iter()
                .next()
                .map(|v| v.try_into())
                .transpose()?,
            author: props.remove("author")
                .unwrap_or_default()
                .into_iter()
                .next()
                .map(|v| v.try_into())
                .transpose()?
                .ok_or(Error::MissingProperty("author"))?,
            category: props.remove("category")
                .unwrap_or_default()
                .into_iter()
                .map(|v| match v {
                    PropertyValue::Plain(string) => Ok(string),
                    other => Err(Error::WrongValueType {
                        expected: "string",
                        got: other
                    })
                })
                .collect::<Result<Vec<_>, _>>()?,
            syndication: props.remove("syndication")
                .unwrap_or_default()
                .into_iter()
                .map(|v| match v {
                    PropertyValue::Url(url) => Ok(url),
                    other => Err(Error::WrongValueType {
                        expected: "link",
                        got: other
                    })
                })
                .collect::<Result<Vec<_>, _>>()?,
            published: props.remove("published")
                .unwrap_or_default()
                .into_iter()
                .next()
                .map(|v| -> Result<chrono::DateTime<chrono::FixedOffset>, Error> {
                    match v {
                        PropertyValue::Temporal(Temporal::Timestamp(dt)) => {
                            // This is incredibly sketchy.
                            let (date, time, offset) = (dt.date.clone().unwrap().data, dt.as_time().unwrap().data.clone(), dt.as_time().unwrap().offset.unwrap().data);

                            date.and_time(time).and_local_timezone(offset).single().ok_or_else(|| Error::WrongValueType {
                                expected: "datetime with timezone",
                                got: PropertyValue::Temporal(Temporal::Timestamp(dt))
                            })
                        },
                        other => Err(Error::WrongValueType {
                            expected: "timestamp",
                            got: other
                        })
                    }
                })
                .ok_or(Error::MissingProperty("published"))??,
            content: props.remove("content")
                .unwrap_or_default()
                .into_iter()
                .next()
                .ok_or(Error::MissingProperty("content"))
                .and_then(|v| match v {
                    PropertyValue::Fragment(fragment) => Ok(Content(fragment)),
                    other => Err(Error::WrongValueType {
                        expected: "html",
                        got: other
                    })
                })?,
        })
    }
}

impl Entry {
    pub fn build(self, article: &mut ArticleBuilder) -> &mut ArticleBuilder {
        article
            .class("h-entry")
            .header(|header| header
                .class("metadata")
                .section(|section| self.author.build_section(section))
                .section(|div| {
                    div
                        .division(|div| div
                            .anchor(|a| a
                                .class("u-url u-uid")
                                .href(String::from(self.uid))
                                .push(html::inline_text::Time::builder()
                                    .text(self.published.format("%Y-%m-%d %a %H:%M:%S %z").to_string())
                                    .date_time(self.published.to_rfc3339_opts(
                                        chrono::SecondsFormat::Secs, false
                                    ))
                                    .build()
                            )))
                        .division(|div| div
                            .text("Tagged")
                            .unordered_list(|ul| {
                                for category in self.category {
                                    ul.list_item(|li| li
                                        .class("p-category")
                                        .text(category)
                                    );
                                }

                                ul
                            })
                        )
                    })
            )
            .main(|main| {
                if let Some(lang) = self.content.0.lang {
                    main.lang(lang);
                }

                main.push(self.content.0.html)
            })
            
    }
}