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

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

#[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;
        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()
                .map(|v| match v {
                    PropertyValue::Plain(plain) => Ok(plain),
                    other => 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) => match image.alt {
                        Some(alt) => Ok(Image::Accessible {
                            src: image.value,
                            alt,
                        }),
                        None => Ok(Image::Plain(image.value))
                    },
                    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.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<time::OffsetDateTime>,
    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.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: time::OffsetDateTime,
    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;
        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<time::OffsetDateTime, Error> {
                        match v {
                            PropertyValue::Temporal(Temporal::Timestamp(ref dt)) => {
                                // This is incredibly sketchy.
                                let (date, time, offset) = (
                                    dt.date.to_owned().ok_or_else(|| Error::WrongValueType {
                                        expected: "timestamp (date, time, offset)",
                                        got: v.clone()
                                    })?.data,
                                    dt.time.to_owned().ok_or_else(|| Error::WrongValueType {
                                        expected: "timestamp (date, time, offset)",
                                        got: v.clone()
                                    })?.data,
                                    dt.offset.to_owned().ok_or_else(|| Error::WrongValueType {
                                        expected: "timestamp (date, time, offset)",
                                        got: v.clone()
                                    })?.data,
                                );

                                Ok(date.with_time(time).assume_offset(offset))
                            }
                            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(|section| {
                        section
                            .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(&time::format_description::well_known::Rfc2822)
                                                    .unwrap()
                                            )
                                            .date_time(self.published.format(&time::format_description::well_known::Rfc3339).unwrap())
                                            .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);
                }

                // XXX .text() and .push() are completely equivalent
                // since .text() does no escaping
                main.push(self.content.0.html)
            })
            .footer(|footer| footer)
    }
}