use std::vec;

/// Store is responsible for manageing the in-memory hashmap of questions by providing initialization read/write functions,
/// and file I/O operations to persist these questions
/// TODO - Results returning errors should use specified types, not strings
use self::{
    answer::{Answer, NewAnswer},
    question::{NewQuestion, Question, QuestionDTO},
    question_tag::QuestionTag,
    tag::Tag,
};
use crate::*;

#[derive(Debug)]
pub struct Store {
    pub connection: PgPool,
}

impl Store {
    /// Helper to deal with unwrapping postgres fields as u8 which we want. Not gracefull if you pass in the wrong field name - so do not do that
    fn id_to_u8(pg_row: &PgRow, id_name: &str) -> u8 {
        pg_row.try_get::<i32, _>(id_name).unwrap() as u8
    }

    pub async fn new(db_url: &str) -> Self {
        let db_pool: Pool<Postgres> = match PgPoolOptions::new()
            .max_connections(5)
            .connect(db_url)
            .await
        {
            Ok(pool) => pool,
            Err(_) => panic!("Could not establish database connection"),
        };
        sqlx::migrate!("./migrations").run(&db_pool).await.unwrap();
        Store {
            connection: db_pool,
        }
    }

    //Get the question tags for a particular question
    pub async fn get_tags_for_question(&self, question_id: u8) -> Option<Vec<Tag>> {
        let query = "SELECT tags.id, tags.label FROM tags 
        JOIN question_tag on tags.id = question_tag.tag_id 
        JOIN questions on question_tag.question_id = questions.id 
        WHERE questions.id = ($1)";
        let result = sqlx::query(query)
            .bind(i32::from(question_id))
            .fetch_all(&self.connection)
            .await;
        match result {
            Ok(pg_rows) => {
                let tags: Vec<Tag> = pg_rows
                    .iter()
                    .map(|pg_row| Tag::new(Store::id_to_u8(pg_row, "id"), pg_row.get("label")))
                    .collect::<Vec<Tag>>();
                Some(tags)
            }
            Err(e) => {
                println!("{}", e);
                None
            }
        }
    }

    //Remove a question/tag association
    pub async fn _remove_question_tag(
        &mut self,
        question_id: u8,
        tag_id: u8,
    ) -> Result<bool, String> {
        let result = sqlx::query("DELETE FROM question_tag WHERE question_id = $1 AND tag_id = $2")
            .bind(question_id.to_string())
            .bind(tag_id.to_string())
            .execute(&self.connection)
            .await;
        match result {
            Ok(_) => Ok(true),
            Err(e) => Err(e.to_string()),
        }
    }

    //Add a question/tag association
    pub async fn _add_question_tag(
        &mut self,
        question_id: u8,
        tag_id: u8,
    ) -> Result<QuestionTag, String> {
        let result =
            sqlx::query("INSERT INTO question_tag VALUES ($1,$2) RETURNING question_id, tag_id")
                .bind(question_id.to_string())
                .bind(tag_id.to_string())
                .fetch_one(&self.connection)
                .await;
        match result {
            Ok(pg_row) => Ok(QuestionTag::new(
                Store::id_to_u8(&pg_row, "question_id"),
                Store::id_to_u8(&pg_row, "tag_id"),
            )),
            Err(e) => Err(e.to_string()),
        }
    }

    // Add new tags to tags table, only if tags with existing label do not exist. If they already exist, just ignore that
    // Returns list of tags
    pub async fn add_tags(&mut self, tag_labels: Vec<String>) -> Result<Vec<Tag>, String> {
        let insert_query = "
        INSERT INTO tags (label)
        SELECT * FROM UNNEST(($1)::text[]) AS label
        ON CONFLICT (label) DO NOTHING
        ";

        //First run the insert query on the new labels
        let insert_result = sqlx::query(insert_query)
            .bind(&tag_labels)
            .execute(&self.connection)
            .await;
        match insert_result {
            Ok(_) => {
                //Then run the select query on the new labels (which may include already existing labels)
                let select_query = "
                    SELECT id, label FROM tags
                    WHERE label = ANY($1::text[]);
                ";
                match sqlx::query(select_query)
                    .bind(&tag_labels)
                    .fetch_all(&self.connection)
                    .await
                {
                    Ok(pg_rows) => {
                        let tags: Vec<Tag> = pg_rows
                            .iter()
                            .map(|pg_row| {
                                Tag::new(Store::id_to_u8(pg_row, "id"), pg_row.get("label"))
                            })
                            .collect();
                        Ok(tags)
                    }
                    Err(e) => Err(e.to_string()),
                }
            }
            Err(e) => Err(e.to_string()),
        }
    }

    /// Takes a question id and list of tags, and creates question_tag join associations
    /// Ignores if association already exists
    /// Returns list of the question tag associations
    pub async fn associate_tags(
        &mut self,
        question_id: u8,
        tags: &[Tag],
    ) -> Result<Vec<QuestionTag>, String> {
        let tag_ids_string: Vec<String> = tags.iter().map(|tag| tag.id.to_string()).collect();

        let query = "
        INSERT INTO question_tag (question_id, tag_id)
        SELECT $1::smallint, UNNEST($2::smallint[])
        ON CONFLICT DO NOTHING;
        ";
        let result = sqlx::query(query)
            .bind(question_id.to_string())
            .bind(tag_ids_string)
            .fetch_all(&self.connection)
            .await;
        match result {
            Ok(pg_rows) => {
                let question_tags: Vec<QuestionTag> = pg_rows
                    .iter()
                    .map(|pg_row| {
                        QuestionTag::new(
                            Store::id_to_u8(pg_row, "question_id"),
                            Store::id_to_u8(pg_row, "tag_id"),
                        )
                    })
                    .collect();
                Ok(question_tags)
            }
            Err(e) => Err(e.to_string()),
        }
    }

    // Takes a question id and a list of tags, and remove any existing question_tag join associations
    // Ignores if an association does not already exist
    // Returns Ok(true) on success
    pub async fn unassociate_tags(
        &mut self,
        question_id: u8,
        tags: Vec<Tag>,
    ) -> Result<bool, String> {
        let list = tags
            .iter()
            .map(|tag| format!("({},{})", question_id, tag.id))
            .collect::<Vec<String>>()
            .join(",");

        let query = format!(
            "DELETE FROM question_tag WHERE (question_id, tag_id) IN ({})",
            list
        ); // Not bulletproof to injection (still ok), but best we can do with sqlx aside from sequential hits
        match sqlx::query(&query).execute(&self.connection).await {
            Ok(_) => Ok(true),
            Err(e) => Err(e.to_string()),
        }
    }

    // Fetch a list of tags by either the tag id, or by the label. Up to the caller
    pub async fn _fetch_tags_by_property(
        &self,
        propert_id: u8,
        property_type: &str,
    ) -> Option<Vec<Tag>> {
        let query = format!("SELECT * FROM tags WHERE {} = ($2);", property_type).to_string(); //looks risky, but user does not get to control property type
        let result = sqlx::query(&query)
            .bind(property_type.to_string())
            .bind(propert_id.to_string())
            .fetch_all(&self.connection)
            .await;
        match result {
            Ok(pg_rows) => {
                let tags: Vec<Tag> = pg_rows
                    .iter()
                    .map(|pg_row| Tag::new(Store::id_to_u8(pg_row, "id"), pg_row.get("label")))
                    .collect();
                Some(tags)
            }
            Err(e) => {
                println!("err {}", e);
                None
            }
        }
    }

    //Add a new question - but also create the tags and question/tag associations as needed
    pub async fn add_question(&mut self, new_question: NewQuestion) -> Result<QuestionDTO, String> {
        let insert_question_result: Result<PgRow, sqlx::Error> = sqlx::query(
            "INSERT INTO questions (title, content) VALUES ($1, $2) RETURNING id, title, content",
        )
        .bind(new_question.title)
        .bind(new_question.content)
        .fetch_one(&self.connection)
        .await;
        // first, insert the new question
        match insert_question_result {
            Ok(pg_row) => {
                let inserted_question = Question::new(
                    Store::id_to_u8(&pg_row, "id"),
                    pg_row.get("title"),
                    pg_row.get("content"),
                );
                // Then create the new tags (if needed)
                let new_tags_result = &self.add_tags(new_question.tags).await;
                match new_tags_result {
                    Ok(new_tags_result) => {
                        let association = self
                            .associate_tags(inserted_question.id, new_tags_result)
                            .await;
                        // Finally, create the question/tag join
                        match association {
                            Ok(_) => {
                                // Now, return the QuestionDTO
                                Ok(QuestionDTO::new(
                                    inserted_question,
                                    new_tags_result.to_vec(),
                                ))
                            }
                            Err(e) => Err(e.to_string()),
                        }
                    }
                    Err(e) => Err(e.to_string()),
                }
            }
            Err(e) => Err(e.to_string()),
        }
    }

    // Delete the question tags association for the given question, then delete the question
    // Also clean up orphaned tags just in case
    pub async fn remove_question(&mut self, id: u8) -> Result<bool, String> {
        let delete_question_tag_query = "DELETE FROM question_tag WHERE question_id = $1";
        let delete_question_query = "DELETE FROM questions WHERE id = $1";
        // First, delete any possible question/tag associations
        match sqlx::query(delete_question_tag_query)
            .bind(i32::from(id))
            .execute(&self.connection)
            .await
        {
            Ok(_) => {
                //Now, delete the question
                match sqlx::query(delete_question_query)
                    .bind(i32::from(id))
                    .execute(&self.connection)
                    .await
                {
                    Ok(_) => {
                        // Finally remove any tags that may have been orphaned
                        match self.remove_orphan_tags().await {
                            Ok(_) => Ok(true),
                            Err(e) => Err(e.to_string()),
                        }
                    }
                    Err(e) => Err(e.to_string()),
                }
            }
            Err(e) => Err(e.to_string()),
        }
    }

    // Fetch one question, but do not worry about joining the tags
    pub async fn fetch_one_question_by_id(&self, id: u8) -> Option<Question> {
        let row_result = sqlx::query("SELECT id,title,content FROM questions WHERE id = $1")
            .bind(i32::from(id))
            .fetch_one(&self.connection)
            .await;
        match row_result {
            Ok(pg_row) => Some(Question::new(
                Store::id_to_u8(&pg_row, "id"),
                pg_row.get("title"),
                pg_row.get("content"),
            )),
            Err(e) => {
                println!("{}", e);
                None
            }
        }
    }

    // Fetch many questions - do not worry about joining the tags
    pub async fn fetch_many_questions(&self, start: i32, size: i32) -> Option<Vec<Question>> {
        let rows_result: Result<Vec<PgRow>, sqlx::Error> =
            sqlx::query("SELECT * FROM questions ORDER BY id LIMIT $1 OFFSET $2")
                .bind(size)
                .bind(start)
                .fetch_all(&self.connection)
                .await;

        match rows_result {
            Ok(pg_rows) => {
                let mut result: Vec<Question> = vec![];
                for pg_row in pg_rows {
                    result.push(Question::new(
                        Store::id_to_u8(&pg_row, "id"),
                        pg_row.get("title"),
                        pg_row.get("content"),
                    ))
                }
                Some(result)
            }
            Err(e) => {
                println!("{}", e);
                None
            }
        }
    }

    /// Remove tags from tags table, which have no question tag association
    /// Returns true on success
    pub async fn remove_orphan_tags(&mut self) -> Result<bool, String> {
        let result = sqlx::query(
            "DELETE FROM tags where id NOT IN (SELECT DISTINCT tag_id from question_tag)",
        )
        .execute(&self.connection)
        .await;
        match result {
            Ok(_) => Ok(true),
            Err(e) => Err(e.to_string()),
        }
    }

    //Update a question entity - just the question details (not tags)
    pub async fn update_question(
        &mut self,
        id: u8,
        title: String,
        content: String,
    ) -> Result<Question, String> {
        let query = "UPDATE questions
                    SET title = $1, content = $2 
                    WHERE id = $3 RETURNING id, title, content";
        let result = sqlx::query(query)
            .bind(title)
            .bind(content)
            .bind(i32::from(id))
            .fetch_one(&self.connection)
            .await;
        match result {
            Ok(pg_row) => Ok(Question::new(
                Store::id_to_u8(&pg_row, "id"),
                pg_row.get("title"),
                pg_row.get("content"),
            )),
            Err(e) => Err(e.to_string()),
        }
    }

    // Add an answer entity
    pub async fn add_answer(&mut self, new_answer: NewAnswer) -> Result<Answer, String> {
        let query = "INSERT INTO answers (content, question_id) VALUES ($1,$2) RETURNING id, content, question_id";
        let result = sqlx::query(query)
            .bind(new_answer.content)
            .bind(i32::from(new_answer.question_id))
            .fetch_one(&self.connection)
            .await;
        match result {
            Ok(pg_row) => Ok(Answer::new(
                Store::id_to_u8(&pg_row, "id"),
                pg_row.get("content"),
                Store::id_to_u8(&pg_row, "question_id"),
            )),
            Err(e) => Err(e.to_string()),
        }
    }
}
