use crate::models::{ArticleID, FeedID, Url};
use crate::schema::{article_contents, articles};
use chrono::{DateTime, Utc};
use diesel::backend::Backend;
use diesel::deserialize;
use diesel::deserialize::FromSql;
use diesel::serialize::{self, IsNull};
use diesel::serialize::{Output, ToSql};
use diesel::sql_types::Integer;
use diesel::sqlite::Sqlite;
use serde::{Deserialize, Serialize};
use std::io::{Error, ErrorKind, Write};
use std::path::PathBuf;

#[derive(Identifiable, Insertable, Selectable, Queryable, AsChangeset, Clone, Eq, PartialEq, Debug)]
#[diesel(primary_key(article_id))]
#[diesel(belongs_to(Feed))]
#[diesel(treat_none_as_default_value = false)]
#[diesel(table_name = articles)]
#[diesel(check_for_backend(Sqlite))]
pub struct Article {
    pub article_id: ArticleID,
    pub title: Option<String>,
    pub author: Option<String>,
    pub feed_id: FeedID,
    pub url: Option<Url>,
    pub date: DateTime<Utc>,
    pub synced: DateTime<Utc>,
    pub summary: Option<String>,
    pub direction: Option<Direction>,
    pub unread: Read,
    pub marked: Marked,
    pub thumbnail_url: Option<String>,
    pub updated: Option<DateTime<Utc>>,
}

//------------------------------------------------------------------

#[derive(Insertable, Selectable, Queryable, Clone, Eq, PartialEq, Debug)]
#[diesel(primary_key(article_id))]
#[diesel(belongs_to(Article))]
#[diesel(treat_none_as_default_value = false)]
#[diesel(table_name = article_contents)]
#[diesel(check_for_backend(Sqlite))]
pub struct ArticleContent {
    pub article_id: ArticleID,
    pub html: Option<String>,
    pub plain_text: Option<String>,
    pub scraped_content: Option<String>,
}

//------------------------------------------------------------------

#[derive(Clone, Eq, PartialEq, Debug)]
pub struct FatArticle {
    pub article_id: ArticleID,
    pub title: Option<String>,
    pub author: Option<String>,
    pub feed_id: FeedID,
    pub url: Option<Url>,
    pub date: DateTime<Utc>,
    pub synced: DateTime<Utc>,
    pub html: Option<String>,
    pub summary: Option<String>,
    pub direction: Option<Direction>,
    pub unread: Read,
    pub marked: Marked,
    pub scraped_content: Option<String>,
    pub plain_text: Option<String>,
    pub thumbnail_url: Option<String>,
    pub updated: Option<DateTime<Utc>>,
}

impl FatArticle {
    pub fn from_table_entries(entries: (Article, Option<ArticleContent>)) -> Self {
        let (article, content) = entries;
        let (html, plain_text, scraped_content) = if let Some(content) = content {
            (content.html, content.plain_text, content.scraped_content)
        } else {
            (None, None, None)
        };

        FatArticle {
            article_id: article.article_id,
            title: article.title,
            author: article.author,
            feed_id: article.feed_id,
            url: article.url,
            date: article.date,
            synced: article.synced,
            html,
            summary: article.summary,
            direction: article.direction,
            unread: article.unread,
            marked: article.marked,
            scraped_content,
            plain_text,
            thumbnail_url: article.thumbnail_url,
            updated: article.updated,
        }
    }

    pub fn to_article(&self) -> Article {
        Article {
            article_id: self.article_id.clone(),
            title: self.title.clone(),
            author: self.author.clone(),
            feed_id: self.feed_id.clone(),
            url: self.url.clone(),
            date: self.date,
            synced: self.synced,
            summary: self.summary.clone(),
            direction: self.direction,
            unread: self.unread,
            marked: self.marked,
            thumbnail_url: self.thumbnail_url.clone(),
            updated: self.updated,
        }
    }

    pub fn to_content(&self) -> ArticleContent {
        ArticleContent {
            article_id: self.article_id.clone(),
            html: self.html.clone(),
            plain_text: self.plain_text.clone(),
            scraped_content: self.scraped_content.clone(),
        }
    }

    pub fn save_html(&self, path: &PathBuf) -> Result<(), Error> {
        if let Some(ref html) = self.html
            && let Ok(()) = std::fs::create_dir_all(path)
        {
            let mut file_name = match self.title.clone() {
                Some(file_name) => file_name,
                None => "Unknown Title".to_owned(),
            };
            file_name.push_str(".html");
            let path = path.join(file_name);
            let mut html_file = std::fs::File::create(path)?;
            html_file.write_all(html.as_bytes())?;
        }

        Err(Error::new(ErrorKind::NotFound, "No HTML for article"))
    }
}

//------------------------------------------------------------------

#[derive(Eq, PartialEq, Copy, Clone, Debug, Default, AsExpression, FromSqlRow, Serialize, Deserialize)]
#[diesel(sql_type = Integer)]
pub enum Direction {
    #[default]
    LeftToRight,
    RightToLeft,
}

impl Direction {
    pub fn to_int(self) -> i32 {
        self as i32
    }

    pub fn from_int(int: i32) -> Self {
        match int {
            0 => Direction::LeftToRight,
            1 => Direction::RightToLeft,

            _ => Direction::LeftToRight,
        }
    }
}

impl FromSql<Integer, Sqlite> for Direction {
    fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
        let int = i32::from_sql(bytes)?;
        Ok(Direction::from_int(int))
    }
}

impl ToSql<Integer, Sqlite> for Direction {
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
        out.set_value(*self as i32);
        Ok(IsNull::No)
    }
}

//------------------------------------------------------------------

#[derive(Eq, PartialEq, Copy, Clone, Debug, Default, AsExpression, FromSqlRow, Serialize, Deserialize)]
#[diesel(sql_type = Integer)]
pub enum Read {
    Read,
    #[default]
    Unread,
}

impl Read {
    pub fn to_int(self) -> i32 {
        self as i32
    }

    pub fn from_int(int: i32) -> Self {
        match int {
            0 => Read::Read,
            1 => Read::Unread,

            _ => Read::Read,
        }
    }

    pub fn invert(&self) -> Read {
        match self {
            Read::Read => Read::Unread,
            Read::Unread => Read::Read,
        }
    }
}

impl FromSql<Integer, Sqlite> for Read {
    fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
        let int = i32::from_sql(bytes)?;
        Ok(Read::from_int(int))
    }
}

impl ToSql<Integer, Sqlite> for Read {
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
        out.set_value(*self as i32);
        Ok(IsNull::No)
    }
}

//------------------------------------------------------------------

#[derive(Eq, PartialEq, Copy, Clone, Debug, Default, AsExpression, FromSqlRow, Serialize, Deserialize)]
#[diesel(sql_type = Integer)]
pub enum Marked {
    Marked,
    #[default]
    Unmarked,
}

impl Marked {
    pub fn to_int(self) -> i32 {
        self as i32
    }

    pub fn from_int(int: i32) -> Self {
        match int {
            0 => Marked::Marked,
            1 => Marked::Unmarked,

            _ => Marked::Unmarked,
        }
    }

    pub fn invert(&self) -> Marked {
        match self {
            Marked::Marked => Marked::Unmarked,
            Marked::Unmarked => Marked::Marked,
        }
    }
}

impl FromSql<Integer, Sqlite> for Marked {
    fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
        let int = i32::from_sql(bytes)?;
        Ok(Marked::from_int(int))
    }
}

impl ToSql<Integer, Sqlite> for Marked {
    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
        out.set_value(*self as i32);
        Ok(IsNull::No)
    }
}

//------------------------------------------------------------------

#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)]
pub enum ArticleOrder {
    #[default]
    NewestFirst,
    OldestFirst,
}

impl ArticleOrder {
    pub fn invert(&self) -> ArticleOrder {
        match self {
            ArticleOrder::NewestFirst => ArticleOrder::OldestFirst,
            ArticleOrder::OldestFirst => ArticleOrder::NewestFirst,
        }
    }
}

//------------------------------------------------------------------

#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)]
pub enum OrderBy {
    #[default]
    Updated,
    Published,
}
