mod error;
#[cfg(test)]
mod tests;

use self::error::{OpmlError, OpmlErrorKind};
use crate::models::{Category, CategoryID, CategoryType, Feed, FeedID, FeedMapping, Url, NEWSFLASH_TOPLEVEL};
use crate::util::feed_parser::{self, ParsedUrl};
use failure::ResultExt;
use log::{error, warn};
use reqwest::Client;
use std::str;
use sxd_document::{dom, parser, writer, Package};

pub struct OpmlResult {
    pub categories: Vec<Category>,
    pub feeds: Vec<Feed>,
    pub feed_mappings: Vec<FeedMapping>,
}

pub fn generate_opml(categories: &[Category], feeds: &[Feed], mappings: &[FeedMapping]) -> Result<String, OpmlError> {
    let package = Package::new();
    let doc = package.as_document();

    let opml_node = doc.create_element("opml");
    opml_node.set_attribute_value("version", "1.0");
    doc.root().append_child(opml_node);

    let head_node = doc.create_element("head");
    opml_node.append_child(head_node);

    let title_node = doc.create_element("title");
    head_node.append_child(title_node);

    let title_text = doc.create_text("NewsFlash OPML export");
    title_node.append_child(title_text);

    let body_node = doc.create_element("body");
    opml_node.append_child(body_node);

    let toplevel_id = NEWSFLASH_TOPLEVEL.clone();
    write_categories(categories, feeds, mappings, &toplevel_id, &body_node, &doc);

    let mut output: Vec<u8> = Vec::new();
    writer::format_document(&doc, &mut output).context(OpmlErrorKind::Xml)?;
    let output = str::from_utf8(&output).context(OpmlErrorKind::Utf8)?;

    Ok(output.to_owned())
}

pub fn write_categories(
    categories: &[Category],
    feeds: &[Feed],
    mappings: &[FeedMapping],
    parent_id: &CategoryID,
    parent_node: &dom::Element<'_>,
    doc: &dom::Document<'_>,
) {
    let filtered_categories: Vec<&Category> = categories.iter().filter(|category| &category.parent_id == parent_id).collect();

    for category in filtered_categories {
        if category.category_type == CategoryType::Generated {
            continue;
        }

        let category_node = doc.create_element("outline");
        category_node.set_attribute_value("title", &category.label);
        category_node.set_attribute_value("text", &category.label);
        parent_node.append_child(category_node);

        write_categories(categories, feeds, mappings, &category.category_id, &category_node, doc);
    }

    let feed_ids: Vec<&FeedID> = mappings
        .iter()
        .filter(|mapping| &mapping.category_id == parent_id)
        .map(|mapping| &mapping.feed_id)
        .collect();

    let feeds: Vec<&Feed> = feeds.iter().filter(|feed| feed_ids.contains(&&feed.feed_id)).collect();

    for feed in feeds {
        if let Some(ref xml_url) = &feed.feed_url {
            let feed_node = doc.create_element("outline");
            feed_node.set_attribute_value("title", &feed.label);
            feed_node.set_attribute_value("text", &feed.label);
            feed_node.set_attribute_value("type", "rss");
            feed_node.set_attribute_value("xmlUrl", &format!("{}", xml_url));
            if let Some(ref website) = &feed.website {
                feed_node.set_attribute_value("htmlUrl", &format!("{}", website));
            }
            parent_node.append_child(feed_node);
        }
    }
}

pub async fn parse_opml(opml_string: &str, parse_all_feeds: bool, client: &Client) -> Result<OpmlResult, OpmlError> {
    let package = parser::parse(opml_string).map_err(|_| {
        error!("Failed to parse opml file to xml document");
        OpmlErrorKind::Xml
    })?;
    let document = package.as_document();
    let root = document.root();

    let opml_tag = root.children();
    let mut category_vec: Vec<Category> = Vec::new();
    let mut feed_vec: Vec<Feed> = Vec::new();
    let mut mapping_vec: Vec<FeedMapping> = Vec::new();

    for element in opml_tag {
        if let Some(head_and_body) = parse_opml_tag(element) {
            for element in head_and_body {
                if let Some(outlines) = parse_body(element) {
                    let mut category_sort_index: i32 = 0;
                    let mut feed_sort_index: i32 = 0;
                    for outline in outlines {
                        parse_outline(
                            outline,
                            &NEWSFLASH_TOPLEVEL,
                            &mut category_sort_index,
                            &mut feed_sort_index,
                            &mut category_vec,
                            &mut feed_vec,
                            &mut mapping_vec,
                        );
                    }

                    let mut parsed_feeds: Vec<Feed> = Vec::new();

                    for feed in feed_vec {
                        if parse_all_feeds || feed.website.is_none() {
                            let xml_url = match &feed.feed_url {
                                Some(url) => url.clone(),
                                None => continue,
                            };

                            match feed_parser::download_and_parse_feed(&xml_url, &feed.feed_id, Some(feed.label.clone()), feed.sort_index, client).await {
                                Ok(ParsedUrl::SingleFeed(parsed_feed)) => parsed_feeds.push(parsed_feed),
                                Ok(ParsedUrl::MultipleFeeds(_)) => {
                                    warn!("Parsing of feed '{}' resulted in multiple available feeds", xml_url);
                                    parsed_feeds.push(feed);
                                },
                                Err(error) => {
                                    warn!("Parsing of feed '{}' failed, falling back to data from opml: {}", xml_url, error);
                                    parsed_feeds.push(feed);
                                },
                            }
                        } else {
                            parsed_feeds.push(feed);
                        }
                    }

                    return Ok(OpmlResult {
                        categories: category_vec,
                        feeds: parsed_feeds,
                        feed_mappings: mapping_vec,
                    });
                }
                // only one body outline allowed, so return after parsing the first one
                // ignoring any further outline children
            }
            warn!("Opml: No outline tag inside of body found");
            return Err(OpmlErrorKind::Empty)?;
        }
    }

    Err(OpmlErrorKind::Body)?
}

#[allow(clippy::too_many_arguments)]
fn parse_outline(
    outline: dom::ChildOfElement<'_>,
    category_id: &CategoryID,
    category_sort_index: &mut i32,
    feed_sort_index: &mut i32,
    category_vec: &mut Vec<Category>,
    feed_vec: &mut Vec<Feed>,
    mapping_vec: &mut Vec<FeedMapping>,
) {
    // could either be a category or a feed
    if let Some((category, outlines)) = parse_category(outline, category_sort_index) {
        let category_id = category.category_id.clone();
        category_vec.push(category);
        for outline in outlines {
            parse_outline(
                outline,
                &category_id,
                category_sort_index,
                feed_sort_index,
                category_vec,
                feed_vec,
                mapping_vec,
            );
        }
    } else if let Some((feed, mapping)) = parse_feed(outline, category_id, feed_sort_index) {
        feed_vec.push(feed);
        mapping_vec.push(mapping);
    }
}

fn parse_opml_tag(opml_tag: dom::ChildOfRoot<'_>) -> Option<Vec<dom::ChildOfElement<'_>>> {
    let opml_tag = opml_tag.element()?;
    if "opml" == opml_tag.name().local_part() {
        return Some(opml_tag.children());
    }
    None
}

fn parse_body(body: dom::ChildOfElement<'_>) -> Option<Vec<dom::ChildOfElement<'_>>> {
    let body_tag = body.element()?;
    if "body" == body_tag.name().local_part() {
        return Some(body_tag.children());
    }
    None
}

fn parse_category<'a>(outline: dom::ChildOfElement<'a>, sort_index: &mut i32) -> Option<(Category, Vec<dom::ChildOfElement<'a>>)> {
    let category_outline = outline.element()?;
    if "outline" == category_outline.name().local_part() && None == category_outline.attribute_value("xmlUrl") {
        let title = match category_outline.attribute_value("title") {
            Some(title) => title,
            None => category_outline.attribute_value("text")?,
        };
        let category = Category {
            category_id: CategoryID::new(title),
            label: title.to_owned(),
            sort_index: Some(*sort_index),
            parent_id: NEWSFLASH_TOPLEVEL.clone(),
            category_type: CategoryType::Default,
        };
        *sort_index += 1;
        return Some((category, category_outline.children()));
    }
    None
}

fn parse_feed(outline: dom::ChildOfElement<'_>, category_id: &CategoryID, sort_index: &mut i32) -> Option<(Feed, FeedMapping)> {
    let feed_outline = outline.element()?;
    if "outline" == feed_outline.name().local_part() {
        let type_attribute = feed_outline.attribute_value("type");
        if Some("rss") == type_attribute || type_attribute.is_none() {
            // prefer optional "title" attribute, fall back to mandatory "text" attribute
            let title = match feed_outline.attribute_value("title") {
                Some(title) => title,
                None => match feed_outline.attribute_value("text") {
                    Some(title) => title,
                    None => "No Title",
                },
            };
            let xml_url = feed_outline.attribute_value("xmlUrl")?;
            let feed_id = FeedID::new(xml_url);
            let xml_url = Url::parse(xml_url).ok()?;
            let mapping = FeedMapping {
                feed_id: feed_id.clone(),
                category_id: category_id.clone(),
            };
            let website = match feed_outline.attribute_value("htmlUrl") {
                Some(html_url) => {
                    let html_url = Url::parse(html_url).ok()?;
                    Some(html_url)
                }
                None => None,
            };

            let feed = Feed {
                feed_id: feed_id.clone(),
                label: title.to_owned(),
                website,
                feed_url: Some(xml_url.clone()),
                icon_url: None,
                sort_index: Some(*sort_index),
            };

            *sort_index += 1;
            return Some((feed, mapping));
        }
    }
    None
}