use crate::database::PsqlData;
use chrono::NaiveDate;
use futures_util::TryStreamExt;
use serde::{Deserialize, Serialize};
use sqlx::{self, postgres::PgPool, Pool, Postgres};
use std::{error::Error, path::Path};

#[derive(Debug, Serialize, Deserialize, PartialEq, PartialOrd, Clone, sqlx::Type)]
#[sqlx(type_name = "link_type", rename_all = "lowercase")]
pub enum LinkType {
    ARTICLE,
    BLOG,
}

#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, sqlx::FromRow)]
pub struct Link {
    pub id: i32,
    pub url: String,
    pub description: Option<String>,
    pub title: String,
    pub author: String,
    pub link_type: LinkType,
    pub date_added: NaiveDate,
}

impl Link {
    fn from_lines(mut lines: Vec<String>) -> Self {
        let mut url: Option<String> = None;
        let mut description: Option<String> = None;
        let mut title: Option<String> = None;
        let mut author: Option<String> = None;
        let mut link_type: Option<LinkType> = None;
        let mut date_added: Option<NaiveDate> = None;
        while lines.len() > 0 {
            let line: String = lines.pop().expect("Something went terribly wrong here");
            if line.contains("url: ") {
                url = Some(line.clone().replace("url: ", ""));
            } else if line.contains("title: ") {
                title = Some(line.clone().replace("title: ", ""));
            } else if line.contains("author: ") {
                author = Some(line.clone().replace("author: ", ""));
            } else if line.contains("type: ") {
                link_type = Some(match line.clone().replace("type: ", "").as_str() {
                    "blog" => LinkType::BLOG,
                    _ => LinkType::ARTICLE,
                });
            } else if line.contains("date_added: ") {
                date_added = Some(
                    NaiveDate::parse_from_str(&line.clone().replace("date: ", ""), "%Y-%m-%d").expect("not a date"),
                );
            } else if line.contains("description: ") {
                description = Some(line.clone().replace("description: ", ""));
            } else if line.eq("---") {
                break;
            }
        }
        Link {
            id: 0,
            url: url.unwrap(),
            title: title.unwrap(),
            author: author.unwrap(),
            date_added: date_added.unwrap(),
            link_type: link_type.unwrap(),
            description,
        }
    }

    pub async fn read_by_url(
        pool: &PgPool,
        url: &String,
    ) -> Result<Box<Self>, Box<dyn Error>> {
        let result = sqlx::query_as!(Link, "SELECT id,url,title,author,date_added,link_type AS \"link_type!: LinkType\",description FROM links WHERE url = $1;", url)
            .fetch_one(pool)
            .await?;

        Ok(Box::new(result))
    }
}

impl PsqlData for Link {
    async fn read_all(pool: &PgPool) -> Result<Vec<Box<Self>>, Box<dyn Error>> {
        let mut results = sqlx::query_as!(Link, "SELECT id,url,title,author,date_added,link_type AS \"link_type!: LinkType\",description FROM links;")
            .fetch(pool);

        let mut output = Vec::<Box<Link>>::new();

        while let Some(result) = results.try_next().await? {
            output.push(Box::new(result));
        }

        Ok(output)
    }

    async fn read(pool: &PgPool, id: i32) -> Result<Box<Self>, Box<dyn Error>> {
        let result = sqlx::query_as!(
            Link,
            "SELECT id,url,title,author,date_added,link_type AS \"link_type!: LinkType\",description FROM links WHERE id = $1",
            id
        )
        .fetch_one(pool)
        .await?;

        Ok(Box::new(result))
    }

    async fn insert(&self, pool: &PgPool) -> Result<(), Box<dyn Error>> {
        sqlx::query!(
            "INSERT INTO links (url, title, author, date_added, link_type, description) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id,url,title,author,date_added,link_type AS \"link_type!: LinkType\", description",
            self.url,
            self.title,
            self.author,
            self.date_added,
            self.link_type.clone() as LinkType,
            self.description,
        )
        .fetch_one(pool)
        .await?;

        Ok(())
    }

    async fn update(&self, pool: &PgPool) -> Result<(), Box<dyn Error>> {
        sqlx::query!(
            "UPDATE links SET url=$1, title=$2, author=$3, date_added=$4, link_type=$5, description=$6 WHERE id=$7",
            self.url,
            self.title,
            self.author,
            self.date_added,
            self.link_type.clone() as LinkType,
            self.description,
            self.id,
        )
        .execute(pool)
        .await?;

        Ok(())
    }

    async fn delete(&self, pool: &PgPool) -> Result<(), Box<dyn Error>> {
        let id = &self.id;
        crate::psql_delete!(id, pool, "links")
    }
}

fn split_into_links(mut lines: Vec<String>) -> Vec<Vec<String>> {
    let mut link_lines: Vec<Vec<String>> = Vec::new();
    let mut link: Vec<String> = Vec::new();
    while lines.len() > 0 {
        let line: String = lines
            .pop()
            .expect("This should not be trying to pop en empty Vec");
        if line.eq("---") {
            link_lines.push(link.clone());
            link = Vec::new();
        } else {
            link.push(line);
        }
    }
    link_lines.push(link.clone());
    link_lines
}

pub async fn load_links(pool: &Pool<Postgres>) -> Result<(), Box<dyn Error>> {
    let path: &Path = Path::new("links.txt");
    let mut links: Vec<Link> = split_into_links(super::read_lines(path))
        .iter()
        .map(|x: &Vec<String>| Link::from_lines(x.clone()))
        .collect();
    for link in links.iter_mut() {
        match Link::read_by_url(pool, &link.url).await {
            Ok(proj) => {
                link.id = proj.id;
                link.update(pool).await?
            }
            Err(_) => link.insert(pool).await?,
        }
    }
    Ok(())
}
