use crate::{app::App, settings::GFeedOrder};

use diffus::{Diffable, Same, edit::Edit};
use news_flash::models::{Category, CategoryID, CategoryMapping, Feed, FeedMapping};
use std::{cmp::Ordering, fmt};

use super::FeedListItemID;

#[derive(Eq, Clone, Debug)]
pub struct FeedListItem {
    pub id: FeedListItemID,
    pub parent_id: CategoryID,
    pub label: String,
    pub item_count: u32,
    pub error_message: Option<String>,
    pub sort_index: i32,
    pub children: Vec<FeedListItem>,
}

impl FeedListItem {
    pub fn from_feed(feed: &Feed, mapping: &FeedMapping, item_count: u32) -> Self {
        Self {
            id: FeedListItemID::Feed(mapping.clone()),
            parent_id: mapping.category_id.clone(),
            label: feed.label.clone(),
            sort_index: mapping.sort_index.unwrap_or(i32::MAX),
            item_count,
            error_message: feed.error_message.clone(),
            children: Vec::new(),
        }
    }

    pub fn from_category(category: &Category, mapping: &CategoryMapping, item_count: u32) -> Self {
        Self {
            id: FeedListItemID::Category(category.category_id.clone()),
            parent_id: mapping.parent_id.clone(),
            label: category.label.clone(),
            sort_index: mapping.sort_index.unwrap_or(i32::MAX),
            item_count,
            error_message: None,
            children: Vec::new(),
        }
    }

    pub fn add_child(&mut self, item: FeedListItem) {
        let contains_item = self.children.iter().any(|i| i.id == item.id);
        if !contains_item {
            self.children.push(item);
            self.children.sort();
        } else {
            tracing::warn!(?self.id, ?item, "Category already contains item");
        }
    }
}

impl Same for FeedListItem {
    fn same(&self, other: &Self) -> bool {
        self.eq(other)
    }
}

impl PartialEq for FeedListItem {
    fn eq(&self, other: &Self) -> bool {
        self.id.eq(&other.id)
    }
}

impl Ord for FeedListItem {
    fn cmp(&self, other: &FeedListItem) -> Ordering {
        let order = App::default().settings().feed_list().order();
        match &self.id {
            FeedListItemID::Feed(_) => match &other.id {
                FeedListItemID::Feed(_) => match order {
                    GFeedOrder::Manual => self.sort_index.cmp(&other.sort_index),
                    GFeedOrder::Alphabetical => self.label.to_lowercase().cmp(&other.label.to_lowercase()),
                },
                FeedListItemID::Category(_) => match order {
                    GFeedOrder::Manual => self.sort_index.cmp(&other.sort_index),
                    GFeedOrder::Alphabetical => Ordering::Greater, // categories before feeds in case of alphabetical
                },
            },
            FeedListItemID::Category(_) => match &other.id {
                FeedListItemID::Feed(_) => match order {
                    GFeedOrder::Manual => self.sort_index.cmp(&other.sort_index),
                    GFeedOrder::Alphabetical => Ordering::Less, // categories before feeds in case of alphabetical
                },
                FeedListItemID::Category(_) => match order {
                    GFeedOrder::Manual => self.sort_index.cmp(&other.sort_index),
                    GFeedOrder::Alphabetical => self.label.to_lowercase().cmp(&other.label.to_lowercase()),
                },
            },
        }
    }
}

impl fmt::Display for FeedListItem {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}  (count: {}) (id: {:?})", self.label, self.item_count, self.id)
    }
}

impl PartialOrd for FeedListItem {
    fn partial_cmp(&self, other: &FeedListItem) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl<'a> Diffable<'a> for FeedListItem {
    type Diff = FeedListItemDiff<'a>;

    fn diff(&'a self, other: &'a Self) -> Edit<'a, Self> {
        let label = if self.label == other.label {
            None
        } else {
            Some(other.label.as_str())
        };

        let item_count = if self.item_count == other.item_count {
            None
        } else {
            Some(other.item_count)
        };

        let error_message = if self.error_message == other.error_message {
            None
        } else {
            Some(other.error_message.as_deref().unwrap_or_default())
        };

        let child_diff = self.children.diff(&other.children);
        let children_differ = matches!(child_diff, Edit::Change(_));

        if self == other && label.is_none() && item_count.is_none() && !children_differ && error_message.is_none() {
            Edit::Copy(self)
        } else {
            Edit::Change(FeedListItemDiff {
                id: self.id.clone(),
                label,
                item_count,
                error_message,
                child_diff,
            })
        }
    }
}

pub struct FeedListItemDiff<'a> {
    pub id: FeedListItemID,
    pub label: Option<&'a str>,
    pub item_count: Option<u32>,
    pub error_message: Option<&'a str>,
    pub child_diff: Edit<'a, Vec<FeedListItem>>,
}
