package controllers

import javax.inject._
import play.api.mvc._
import play.api.libs.json.{JsValue, JsLookupResult}

import models.{Daily}
import models.actions.{AuthenticatedUserAction, AuthenticationRequest}
import models.exceptions.{ConflictException, NotFoundException, InvalidRequestBodyException, InvalidQueryParameterException}

import scala.concurrent.TimeoutException
import org.bson.types.ObjectId

/**
 * This controller handles all the Daily endpoints.
 */
@Singleton
class DailyController @Inject()(val controllerComponents: ControllerComponents, authenticatedUserAction: AuthenticatedUserAction) 
  extends BaseController {

  /**
   * Create an Action to fetch all the Dailies in the DB.
   */
  def getAll() = authenticatedUserAction {
    println("DailyController:getAll")

    try {
        val result: Seq[Daily] = Daily.getAllDailiesAsync()
        val jsonResult: JsValue = Daily.toJson(result)
        Ok(jsonResult)
    } catch {
        case _: TimeoutException => BadRequest("Request timed out")
        case _: Throwable => BadRequest("Exception raised")
    }
  }

  /**
   * Create an Action to fetch the user's Dailies in the DB.
   * 
   * @param userId The ID of the user to get the dailies for.
   */
  def getUserDailies(userId: String) = authenticatedUserAction {
    println("DailyController:getUserDailies")

    try {
        if (!ObjectId.isValid(userId)) throw new InvalidQueryParameterException("Invalid query parameter ID format: userId")

        val result: Seq[Daily] = Daily.getUserDailiesAsync(new ObjectId(userId))
        val jsonResult: JsValue = Daily.toJson(result)
        Ok(jsonResult)
    } catch {
        case _: TimeoutException => BadRequest("Request timed out")
        case ex: InvalidQueryParameterException => BadRequest(ex.getMessage())
        case _: Throwable => BadRequest("Exception raised")
    }
  }

  /**
   * Create an Action to fetch the user's Feed.
   * 
   * @param userId The ID of the user to get the feed for.
   */
  def getUserFeed(userId: String, questionId: String) = authenticatedUserAction { implicit request: AuthenticationRequest[AnyContent] =>
    println("DailyController:getUserFeed")

    try {
        if (!ObjectId.isValid(userId)) throw new InvalidRequestBodyException("Invalid query parameter ID format: userId")

        val result: Seq[Daily] = Daily.getUserFeedAsync(new ObjectId(userId), new ObjectId(questionId), request.jwt)
        val jsonResult: JsValue = Daily.toJson(result)
        Ok(jsonResult)
    } catch {
        case _: TimeoutException => BadRequest("Request timed out")
        case ex: InvalidQueryParameterException => BadRequest(ex.getMessage())
        case _: Throwable => BadRequest("Exception raised")
    }
  }

  /**
   * Create an Action to create a Daily.
   */
  def create() = authenticatedUserAction { implicit request: AuthenticationRequest[AnyContent] =>
    println("DailyController:create")

    try {
        val (userId, questionId, content) = fetchCreateRequestBody(request.body)

        val result: Daily = Daily.createDailyAsync(userId, questionId, content)
        val jsonResult: JsValue = Daily.toJson(result)
        Ok(jsonResult)
    } catch {
        case _: TimeoutException => BadRequest("Request timed out")
        case ex: InvalidRequestBodyException => BadRequest(ex.getMessage())
        case _: Throwable => BadRequest("Exception raised")
    }
  }

  /**
   * Create an Action to like a Daily.
   */
  def like() = authenticatedUserAction { implicit request: AuthenticationRequest[AnyContent] =>
    println("DailyController:like")

    try {
        val (dailyId, likerId) = fetchLikeRequestBody(request.body)
        
        Daily.likeAsync(dailyId, likerId, request.jwt)
        Ok("Daily liked.")
    } catch {
        case _: TimeoutException => BadRequest("Request timed out")
        case ex: InvalidRequestBodyException => BadRequest(ex.getMessage())
        case ex: ConflictException => BadRequest(ex.getMessage())
        case ex: NotFoundException => BadRequest(ex.getMessage())
        case _: Throwable => BadRequest("Exception raised")
    }
  }

   /**
   * Create an Action to unlike a Daily.
   */
  def unlike() = authenticatedUserAction { implicit request: AuthenticationRequest[AnyContent] =>
    println("DailyController:unlike")

    try {
        val (dailyId, likerId) = fetchLikeRequestBody(request.body)

        Daily.unlikeAsync(dailyId, likerId, request.jwt)
        Ok("Daily unliked.")
    } catch {
        case _: TimeoutException => BadRequest("Request timed out")
        case ex: InvalidRequestBodyException => BadRequest(ex.getMessage())
        case ex: ConflictException => BadRequest(ex.getMessage())
        case ex: NotFoundException => BadRequest(ex.getMessage())
        case _: Throwable => BadRequest("Exception raised")
    }
  }

  /**
   * Fetch the needed values from the request body for the creating a Daily endpoint.
   * 
   * @param requestBody The request's body.
   */
  def fetchCreateRequestBody(requestBody: AnyContent): (ObjectId, ObjectId, String) = {
    if (!requestBody.asJson.isDefined) throw new InvalidRequestBodyException("Request body must be in JSON format.")

    val bodyJson = requestBody.asJson.get

    val userId: ObjectId = fetchJsonBodyObjectId(bodyJson, "userId")
    val questionId: ObjectId = fetchJsonBodyObjectId(bodyJson, "questionId")
    val content: String = fetchJsonBodyString(bodyJson, "content")

    (userId, questionId, content)
  }

  /**
   * Fetch the needed values from the request body for the liking/unliking a Daily endpoint.
   * 
   * @param requestBody The request's body.
   */
  def fetchLikeRequestBody(requestBody: AnyContent): (ObjectId, ObjectId) = {
    if (!requestBody.asJson.isDefined) throw new InvalidRequestBodyException("Request body must be in JSON format.")

    val bodyJson = requestBody.asJson.get

    val dailyId: ObjectId  = fetchJsonBodyObjectId(bodyJson, "dailyId")
    val likerId: ObjectId  = fetchJsonBodyObjectId(bodyJson, "likerId")

    (dailyId, likerId)
  }

  /**
   * Fetch the value of the given field name from the JSON.
   * 
   * @param bodyJson The JSON.
   * @param fieldName The field name.
   */
  def fetchJsonBodyValue(bodyJson: JsValue, fieldName: String): JsValue = {
    val value: JsLookupResult = (bodyJson \ fieldName)
    if (!value.isDefined) throw new InvalidRequestBodyException("Missing parameter: " + fieldName)
    value.get
  }

  /**
   * Fetch the String value of the field name from the JSON.
   * 
   * @param bodyJson The JSON.
   * @param fieldName The field name.
   */
  def fetchJsonBodyString(bodyJson: JsValue, fieldName: String): String = {
    fetchJsonBodyValue(bodyJson, fieldName).as[String]
  }

  /**
   * Fetch the ObjectId value of the field name from the JSON.
   * 
   * @param bodyJson The JSON.
   * @param fieldName The field name.
   * 
   * @throws InvalidRequestBodyException if the value is not a valid ID.
   */
  def fetchJsonBodyObjectId(bodyJson: JsValue, fieldName: String): ObjectId = {
    val value: String = fetchJsonBodyValue(bodyJson, fieldName).as[String]
    if (!ObjectId.isValid(value)) throw new InvalidRequestBodyException("Invalid ID format: " + fieldName)
    new ObjectId(value)
  }
}