/// All API route handlers of the application
use self::{
    answer::NewAnswer,
    pg_store::Store,
    question::{NewQuestion, QuestionDTO},
};
use crate::*;
// sqlx wants 'bigint' = i32
const DEFAULT_PAGE: i32 = 0;
const DEFAULT_PAGE_SIZE: i32 = 10;

/// Fetches a single question using a provided id. First we fetch the question, then the questions tags, then build that into a response DTO
/// # Parameters
/// `id`: Path parmater Id of the question to lookup
/// # Returns
/// If the question is found, it converts the question to its Data Transfer Object (DTO) format and returns it.
/// If the question is not found, it returns a 404 NOT_FOUND response with the error message.
pub async fn read_question(
    State(store): State<Arc<RwLock<Store>>>,
    Path(id): Path<u8>,
) -> Response {
    //First, fetch the question
    let question_result = store.read().await.fetch_one_question_by_id(id).await;
    match question_result {
        Some(question) => {
            //Then fetch the tags for this question, if there are any
            let tags_result = store.read().await.get_tags_for_question(question.id).await;
            match tags_result {
                Some(tags) => QuestionDTO::new(question, tags).into_response(),
                None => QuestionDTO::new(question, vec![]).into_response(),
            }
        }
        None => (StatusCode::NOT_FOUND).into_response(),
    }
}

/// Fetches all questions
/// Load the questions, then load the tags for each question and build the json response
/// # Parameters
/// 'page' and 'size' as query paramateres
/// # Returns
/// A list of questions, empty if none exist
pub async fn read_questions(
    State(store): State<Arc<RwLock<Store>>>,
    Query(pagination): Query<Pagination>,
) -> Response {
    let page: i32 = pagination.page.unwrap_or(DEFAULT_PAGE);
    let size: i32 = pagination.size.unwrap_or(DEFAULT_PAGE_SIZE);
    let start: i32 = page * size;
    let questions_option = store.read().await.fetch_many_questions(start, size).await;
    match questions_option {
        Some(questions) => {
            let mut response_vec_dto: Vec<QuestionDTO> = vec![];
            for question in questions {
                //Not ideal - hitting the database serially for the tags of each invidual question. Can be optimized with more complex sql
                let tags_option = store.read().await.get_tags_for_question(question.id).await;
                match tags_option {
                    Some(tags) => {
                        let question_dto: QuestionDTO = QuestionDTO::new(question, tags);
                        response_vec_dto.push(question_dto)
                    }
                    None => {
                        let question_dto: QuestionDTO = QuestionDTO::new(question, vec![]);
                        response_vec_dto.push(question_dto)
                    }
                }
            }
            (StatusCode::OK, Json(response_vec_dto)).into_response()
        }
        None => StatusCode::NO_CONTENT.into_response(),
    }
}

/// Creates a new question
/// # Parameters
/// `QuestionDTO` A JSON representation of a Question including its id
/// # Returns
/// Http Created 201 and the newly created question are return upon success
/// Http Confilct 409 and a message are returned if a question with that `id`` already exists
/// Http Unprocessable Entity 422 is returned (implicitlly) for a malformed body
pub async fn create_question(
    State(store): State<Arc<RwLock<Store>>>,
    Json(new_question): Json<NewQuestion>,
) -> Response {
    match store.write().await.add_question(new_question).await {
        Ok(question) => (StatusCode::CREATED, Json(&question)).into_response(),
        Err(e) => (StatusCode::CONFLICT, e).into_response(),
    }
}

/// Updates an existing question
/// TODO: Better handle errors, responses in the tag updating flow - it is getting complex
/// # Parameters
/// `QuestionDTO` A JSON representation of a Question including its id
/// # Returns
/// Http Ok 200 and the updated question are return upon success
/// Http Not Found 404 and a message are returned if a question with that `id` does not already exist
/// Http Unprocessable Entity 422 is returned (implicitlly) for a malformed body
pub async fn update_question(
    State(store): State<Arc<RwLock<Store>>>,
    Json(question): Json<QuestionDTO>,
) -> Response {
    // 1: Update the question entity
    let updated_question_result = store
        .write()
        .await
        .update_question(question.id, question.title, question.content)
        .await;
    match updated_question_result {
        Ok(updated_question) => {
            // 2: Get the list of tags currently associated with this question
            let current_tags_option = store
                .read()
                .await
                .get_tags_for_question(updated_question.id)
                .await;
            match current_tags_option {
                Some(current_tags) => {
                    let incoming_tag_labels = question.tags;
                    // 3: Create (ignored if they already exist, new tags for the incoming tags)
                    let incoming_tags = store
                        .write()
                        .await
                        .add_tags(incoming_tag_labels)
                        .await
                        .unwrap();
                    // 4: Unassociated all current tags with this question
                    if !current_tags.is_empty() {
                        let _remove_tags = store
                            .write()
                            .await
                            .unassociate_tags(updated_question.id, current_tags)
                            .await
                            .unwrap();
                    }

                    // 5: Associated all of the incoming tags (now newly created if needed) with the question
                    let _updated_tags = store
                        .write()
                        .await
                        .associate_tags(updated_question.id, &incoming_tags)
                        .await
                        .unwrap();
                    //6: Return the updated question with the updated tags
                    QuestionDTO::new(updated_question, incoming_tags).into_response()
                }
                None => (StatusCode::OK).into_response(),
            }
        }
        Err(e) => (StatusCode::CONFLICT, e).into_response(),
    }
}

/// Delete an existing question
/// # Parameters
/// `id`: Path parmater Id of the question to delete
/// # Returns
/// Http Ok 200 and the deleted question are return upon success
/// Http Not Found 404 and a message are returned if a question with that `id` does not exist
pub async fn delete_question(
    State(store): State<Arc<RwLock<Store>>>,
    Path(id): Path<u8>,
) -> Response {
    match store.write().await.remove_question(id).await {
        Ok(_) => (StatusCode::OK, format!("Question with id {} deleted", id)).into_response(),
        Err(e) => (StatusCode::NOT_FOUND, e).into_response(),
    }
}

/// Create an Answer
/// # Parameters
/// `answer_dto` Form URL encoded answer DTO
/// # Returns
/// Status Created 201 and the created answer upon success
/// Status Conflict 409 and message if answer with ID already exists
/// Status Unprocessable Entity 422 is returned (implicitlly) for a malformed body
pub async fn create_answer(
    State(store): State<Arc<RwLock<Store>>>,
    Form(new_answer): Form<NewAnswer>,
) -> Response {
    match store.write().await.add_answer(new_answer).await {
        Ok(answer) => (StatusCode::CREATED, Json(answer)).into_response(),
        Err(e) => (StatusCode::CONFLICT, e).into_response(),
    }
}

#[derive(Debug, Deserialize)]
pub struct Pagination {
    page: Option<i32>,
    size: Option<i32>,
}
