package repository

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	domain "github.com/haydenhargreaves/Potion/internal/domain/recipe"
	"github.com/lib/pq"
)

type RecipeRepository struct {
	db *sql.DB
}

// Compile-time check to ensure the RecipeRepository implements domain.RecipeRepository
var _ domain.RecipeRepository = (*RecipeRepository)(nil)

// NewRecipeRepository creates a user repository object which is used by the user service to access
// the database. Any recipe related database operations will take place in this repository.
func NewRecipeRepository(db *sql.DB) domain.RecipeRepository {
	return &RecipeRepository{db: db}
}

// NOTE: This function modified the provided recipe with the new values, such as id and time stamp

// CreateRecipe creates a recipe in the database. The recipe provided should contain all data except
// time stamps and the ID; the database will fill them when the operation succeeds. Any errors will
// be bubbled to the caller. The recipe parameter is passed by reference and will therefore be updated
// directly and the new fields (ID, created) can be accessed upon success.
func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error {
	tx, err := r.db.Begin()
	if err != nil {
		tx.Rollback()
		return err
	}

	query := `INSERT INTO recipes (
			title, description, instructions, serves, difficulty,
			duration, category, ingredients, userid, modified, created
		) VALUES (
			$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
		) RETURNING id;`

	// NOTE: Data steps
	// cast duration to JSON
	// cast ingredients to JSON
	// cast category to string
	// use nil for the modified time

	durationJSON, err := json.Marshal(recipe.Duration)
	if err != nil {
		return err
	}

	ingredientsJSON, err := json.Marshal(recipe.Ingredients)
	if err != nil {
		return err
	}

	var id int
	if err = tx.QueryRow(
		query,
		recipe.Title,
		recipe.Description,
		pq.Array(recipe.Instructions),
		recipe.Serves,
		recipe.Difficulty,
		durationJSON,
		string(recipe.Category),
		ingredientsJSON,
		recipe.UserId,
		nil,
		recipe.Created,
	).Scan(&id); err != nil {
		tx.Rollback()
		return err
	}

	if err := tx.Commit(); err != nil {
		tx.Rollback()
		return err
	}

	// Set the new ID
	recipe.Id = id

	return nil
}

// GetRecipe gets a recipe from the database via its ID. The operation is wrapped in a transaction
// for added safety. The repository will not check for a nil result, instead the service will. Callers
// are responsible for protecting against double nil results. Any errors will be bubbled to the caller.
func (r *RecipeRepository) GetRecipe(id int, userId *int) (*domain.Recipe, error) {
	query := `
		SELECT 
			id, title, description, instructions, serves, difficulty, duration, category, ingredients, 
			userid, modified, created
		FROM recipes 
		WHERE id = $1
	`

	var durationBytes []byte
	var ingredientBytes []byte

	var recipe domain.Recipe
	if err := r.db.QueryRow(query, id).Scan(
		&recipe.Id,
		&recipe.Title,
		&recipe.Description,
		pq.Array(&recipe.Instructions),
		&recipe.Serves,
		&recipe.Difficulty,
		&durationBytes,
		&recipe.Category,
		&ingredientBytes,
		&recipe.UserId,
		&recipe.Modified,
		&recipe.Created,
	); err != nil {
		return nil, fmt.Errorf("Failed to location recipe in database: %s", err.Error())
	}

	// Parse duration
	if len(durationBytes) > 0 {
		var duration domain.RecipeDuration
		if err := json.Unmarshal(durationBytes, &duration); err != nil {
			return nil, fmt.Errorf("Failed to parse duration from database: %s", err.Error())
		}

		recipe.Duration = duration
	} else {
		recipe.Duration = domain.RecipeDuration{}
	}

	// Parse ingredient
	if len(ingredientBytes) > 0 {
		var ingredients []domain.RecipeIngredient
		if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil {
			return nil, fmt.Errorf("Failed to parse ingredients from database: %s", err.Error())
		}

		recipe.Ingredients = ingredients
	} else {
		recipe.Ingredients = []domain.RecipeIngredient{}
	}

	// Add tags
	if err := r.GetRecipeTags(&recipe); err != nil {
		fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
	}

	// Get favorite status, if user id is provided
	if userId != nil {
		if err := r.GetRecipeFavorite(&recipe, *userId); err != nil {
			fmt.Printf("ERROR getting recipe favorite status. %s\n", err.Error())
		}
	} else {
		recipe.Favorite = false
	}

	return &recipe, nil
}

// GetRecipes gets a list of recipes from the database via their ID. The operation is wrapped in a
// transaction for added safety. The repository will not check for a nil result, instead the service
// will. Callers are responsible for protecting against double nil results. Any errors will be bubbled
// to the caller.
func (r *RecipeRepository) GetRecipes(ids []int, userId *int) ([]domain.Recipe, error) {
	query := `
		SELECT 
			id, title, description, instructions, serves, difficulty, duration, category, ingredients, userid, modified, created
		FROM recipes 
		WHERE id = ANY($1) 
		ORDER BY array_position($1, id);
	`

	var recipes []domain.Recipe

	rows, err := r.db.Query(query, pq.Array(ids))
	if err != nil {
		return nil, fmt.Errorf("Failed to get recipes. %s", err.Error())
	}
	defer rows.Close()

	for rows.Next() {
		var recipe domain.Recipe
		var durationBytes []byte
		var ingredientBytes []byte

		if err := rows.Scan(
			&recipe.Id,
			&recipe.Title,
			&recipe.Description,
			pq.Array(&recipe.Instructions),
			&recipe.Serves,
			&recipe.Difficulty,
			&durationBytes,
			&recipe.Category,
			&ingredientBytes,
			&recipe.UserId,
			&recipe.Modified,
			&recipe.Created,
		); err != nil {
			return nil, fmt.Errorf("Failed to scan recipe from database: %s", err.Error())
		}

		// Parse duration
		if len(durationBytes) > 0 {
			var duration domain.RecipeDuration
			if err := json.Unmarshal(durationBytes, &duration); err != nil {
				return nil, fmt.Errorf("Failed to parse duration from database: %s", err.Error())
			}

			recipe.Duration = duration
		} else {
			recipe.Duration = domain.RecipeDuration{}
		}

		// Parse ingredient
		if len(ingredientBytes) > 0 {
			var ingredients []domain.RecipeIngredient
			if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil {
				return nil, fmt.Errorf("Failed to parse ingredients from database: %s", err.Error())
			}

			recipe.Ingredients = ingredients
		} else {
			recipe.Ingredients = []domain.RecipeIngredient{}
		}

		// Add tags
		if err := r.GetRecipeTags(&recipe); err != nil {
			fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
		}

		// Get favorite status, if user id is provided
		if userId != nil {
			if err := r.GetRecipeFavorite(&recipe, *userId); err != nil {
				fmt.Printf("ERROR getting recipe favorite status. %s\n", err.Error())
			}
		} else {
			recipe.Favorite = false
		}

		recipes = append(recipes, recipe)
	}

	return recipes, nil
}

// isBitActive returns true when the bit at pos (0 indexed) is true.
func isBitActive(bits, pos int) bool {
	return (bits>>pos)&1 == 1
}

// SearchRecipes will search the recipe table using the provided filters and return an unbound list
// of recipes. The filters are fairly complex, they are stored as bit masks. A more details
// description can be found in the recipe service implementation. Any errors will be bubbled to the
// caller.
//
// The favorites parameter is used to only return filters favorited by the userId provided.
//
// TODO: Pagination is required, to provide infinite scroll.
func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *int, favorites bool) ([]domain.Recipe, error) {
	// Compute meals type filters (there are 7 bits)
	var mealConditions []string
	for i := range 7 {
		if isBitActive(filters.MealType, i) {
			mealConditions = append(mealConditions, fmt.Sprintf("category = '%s'", domain.ParseMeal(i)))
		}
	}

	// Compute time filters (there are 5 bits)
	var timeConditions []string
	for i := range 5 {
		var cond string
		if isBitActive(filters.Time, i) {
			switch i {
			case 0:
				cond = "(duration->>'total')::int < 15"
			case 1:
				cond = "(duration->>'total')::int BETWEEN 15 AND 30"
			case 2:
				cond = "(duration->>'total')::int BETWEEN 30 AND 60"
			case 3:
				cond = "(duration->>'total')::int BETWEEN 60 AND 120"
			case 4:
				cond = "(duration->>'total')::int > 120"
			}
			timeConditions = append(timeConditions, cond)
		}
	}

	// Compute difficulty filters (there are 5 bits)
	var difficultyConditions []string
	for i := range 5 {
		if isBitActive(filters.Difficulty, i) {
			cond := fmt.Sprintf("difficulty = '%d'", i+1)
			difficultyConditions = append(difficultyConditions, cond)
		}
	}

	// Compute serving size filters (there are 5 bits)
	var servingConditions []string
	for i := range 5 {
		var cond string
		if isBitActive(filters.ServingSize, i) {
			switch i {
			case 0:
				cond = "serves BETWEEN 1 AND 2"
			case 1:
				cond = "serves BETWEEN 2 AND 4"
			case 2:
				cond = "serves BETWEEN 4 AND 6"
			case 3:
				cond = "serves BETWEEN 6 AND 8"
			case 4:
				cond = "serves > 8"
			}
			servingConditions = append(servingConditions, cond)
		}
	}

	// Merge condition strings
	mealString := fmt.Sprintf("(%s)", strings.Join(mealConditions, " OR "))
	timeString := fmt.Sprintf("(%s)", strings.Join(timeConditions, " OR "))
	difficultyString := fmt.Sprintf("(%s)", strings.Join(difficultyConditions, " OR "))
	servingString := fmt.Sprintf("(%s)", strings.Join(servingConditions, " OR "))

	// Combine condition strings
	var conditions []string
	if len(mealConditions) > 0 {
		conditions = append(conditions, mealString)
	}
	if len(timeConditions) > 0 {
		conditions = append(conditions, timeString)
	}
	if len(difficultyConditions) > 0 {
		conditions = append(conditions, difficultyString)
	}
	if len(servingConditions) > 0 {
		conditions = append(conditions, servingString)
	}

	// Define columns to select. More fields can be added if the full text search is required
	columns := []string{
		"r.id",
		"r.title",
		"r.description",
		"r.instructions",
		"r.serves",
		"r.difficulty",
		"r.duration",
		"r.category",
		"r.ingredients",
		"r.userid",
		"r.modified",
		"r.created",
	}

	// TODO: Need to add these to the query

	// FROM ... JOIN favorites f ON f.recipeId = r.id
	// WHERE ... AND f.userId = 3

	// Create search vector query
	var orderBy string = ""
	if filters.Search != "" {
		spl := strings.Split(filters.Search, " ")
		var cleaned []string

		// Use a string replacer, each word in the query will be passed through this
		replacer := strings.NewReplacer(
			"'", "",
			"-", "",
			"&", "",
			"|", "",
			"!", "",
		)

		for i := range len(spl) {
			q := strings.TrimSpace(replacer.Replace(spl[i]))

			if q != "" {
				cleaned = append(cleaned, q)
			}
		}

		vector_query := strings.Join(cleaned, " | ")

		conditions = append(
			conditions,
			fmt.Sprintf("r.search_vector @@ to_tsquery('english', '%s')", vector_query),
		)

		template := `
			ORDER BY
				ts_rank(r.search_vector, to_tsquery('english', '%s')) DESC,
				ts_rank_cd(r.search_vector, to_tsquery('english', '%s')) DESC
		`
		orderBy = fmt.Sprintf(template, vector_query, vector_query)
	}

	// Generate the query
	var query string
	if favorites && userId != nil {
		query = fmt.Sprintf(
			"SELECT %s FROM recipes r JOIN favorites f ON f.recipeId = r.id",
			strings.Join(columns, ","),
		)

		// Add new favorite condition to the conditions list
		conditions = append(conditions, fmt.Sprintf("f.userid = %d", *userId))
	} else {
		query = fmt.Sprintf("SELECT %s FROM recipes r", strings.Join(columns, ","))
	}

	// Convert and append conditions if provided
	if len(conditions) > 0 {
		conditionsString := fmt.Sprintf("WHERE %s", strings.Join(conditions, " AND "))
		query = fmt.Sprintf("%s %s", query, conditionsString)
	}

	// Append sorting order if exists
	if len(orderBy) > 0 {
		query = fmt.Sprintf("%s %s", query, orderBy)
	}

	// Finish it off with a colon!
	query += ";"

	// Execute the query
	rows, err := r.db.Query(query)
	if err != nil {
		return nil, fmt.Errorf("failed to query recipes: %w", err)
	}
	defer rows.Close()

	var recipes []domain.Recipe
	for rows.Next() {
		// Parsed values location
		var recipe domain.Recipe
		var durationBytes []byte
		var ingredientBytes []byte

		if err := rows.Scan(
			&recipe.Id,
			&recipe.Title,
			&recipe.Description,
			pq.Array(&recipe.Instructions),
			&recipe.Serves,
			&recipe.Difficulty,
			&durationBytes,
			&recipe.Category,
			&ingredientBytes,
			&recipe.UserId,
			&recipe.Modified,
			&recipe.Created,
		); err != nil {
			return nil, fmt.Errorf("failed to scan recipe row: %w", err)
		}

		// Parse duration from bytes
		if len(durationBytes) > 0 {
			var duration domain.RecipeDuration
			if err := json.Unmarshal(durationBytes, &duration); err != nil {
				return nil, fmt.Errorf("failed to parse duration for recipe ID %d: %w", recipe.Id, err)
			}
			recipe.Duration = duration
		} else {
			recipe.Duration = domain.RecipeDuration{}
		}

		// Parse ingredients from bytes
		if len(ingredientBytes) > 0 {
			var ingredients []domain.RecipeIngredient
			if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil {
				return nil, fmt.Errorf("failed to parse ingredients for recipe ID %d: %w", recipe.Id, err)
			}
			recipe.Ingredients = ingredients
		} else {
			recipe.Ingredients = []domain.RecipeIngredient{}
		}

		// Add tags
		if err := r.GetRecipeTags(&recipe); err != nil {
			fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
		}

		// Add recipe if not a favorite search
		if !favorites && userId != nil {
			if err := r.GetRecipeFavorite(&recipe, *userId); err != nil {
				fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
			}
		}

		if favorites {
			recipe.Favorite = true
		}

		recipes = append(recipes, recipe)
	}

	return recipes, nil
}

// CreateRecipeTags accepts a list of tags (names) and a recipe (already created by the DB) and
// creates the tags that do not exists, and adds those that do exist to the mapping table for the
// recipe. The result is records in the RecipeTags mapping table that represent all of the new
// and existing tags provided to this function. The recipe object must only contain an ID to call
// this function successfully, therefore, it must be an existing recipe. Any errors will be bubbled
// to the caller.
func (r *RecipeRepository) CreateRecipeTags(recipe domain.Recipe, tags []string) error {
	tx, err := r.db.Begin()
	if err != nil {
		tx.Rollback()
		return err
	}

	// Normalize the tag names (lower case with trimmed space)
	normalized := make(map[string]struct{}) // Use map to disallow duplicates
	for _, tag := range tags {
		normalized[strings.ToLower(strings.TrimSpace(tag))] = struct{}{}
	}

	// Insert the tags into the DB and return their IDS into the tag ID list
	var tagIds []int
	for tag := range normalized {
		var tagId int

		query := `
			INSERT INTO tags (name) VALUES ($1)
			ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
			RETURNING id;
		`

		err := tx.QueryRow(query, tag).Scan(&tagId)
		if err != nil {
			return fmt.Errorf("Failed to retrieve or create tag. %s\n", err.Error())
		}

		tagIds = append(tagIds, tagId)
	}

	// Using a prepared statement, execute the mapping insertions one-by-one
	for _, id := range tagIds {
		stmt, err := tx.Prepare("INSERT INTO RecipeTags (RecipeId, TagId) VALUES ($1, $2);")
		if err != nil {
			return fmt.Errorf("Failed to create statement for recipe tag mapping. %s\n", err.Error())
		}
		defer stmt.Close()

		if _, err := stmt.Exec(recipe.Id, id); err != nil {
			return fmt.Errorf("Failed to insert tag-recipe-mapping. %s\n", err.Error())
		}
	}

	if err := tx.Commit(); err != nil {
		tx.Rollback()
		return err
	}

	return nil
}

// GetUserRecipes gets a list of a users owned recipes. This function does not ensure the user is
// authenticated or exists. If nothing is found, a blank slice will be returned. The resulting list
// is sorted by the created dates, newest first. Any errors will be bubbled to the caller.
func (r *RecipeRepository) GetUserRecipes(id int) ([]domain.Recipe, error) {
	query := `
		SELECT id, title, description, instructions, serves, difficulty, duration, category, ingredients, 
			userid, modified, created 
		FROM recipes 
		WHERE userid = $1
		ORDER BY created DESC;
	`

	rows, err := r.db.Query(query, id)
	if err != nil {
		return nil, fmt.Errorf("Failed to query DB for user recipes. %s\n", err.Error())
	}
	defer rows.Close()

	// Prepare statement for tag query
	// tagQuery := `
	// `

	var recipes []domain.Recipe
	for rows.Next() {
		var recipe domain.Recipe
		var durationBytes []byte
		var ingredientBytes []byte

		// Scan results from recipe query onto recipe object
		if err := rows.Scan(
			&recipe.Id,
			&recipe.Title,
			&recipe.Description,
			pq.Array(&recipe.Instructions),
			&recipe.Serves,
			&recipe.Difficulty,
			&durationBytes,
			&recipe.Category,
			&ingredientBytes,
			&recipe.UserId,
			&recipe.Modified,
			&recipe.Created,
		); err != nil {
			return nil, fmt.Errorf("Failed to scan row onto recipe object. %s\n", err.Error())
		}

		// Parse duration
		if len(durationBytes) > 0 {
			var duration domain.RecipeDuration
			if err := json.Unmarshal(durationBytes, &duration); err != nil {
				return nil, fmt.Errorf("Failed to parse duration from database: %s", err.Error())
			}

			recipe.Duration = duration
		} else {
			recipe.Duration = domain.RecipeDuration{}
		}

		// Parse ingredient
		if len(ingredientBytes) > 0 {
			var ingredients []domain.RecipeIngredient
			if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil {
				return nil, fmt.Errorf("Failed to parse ingredients from database: %s", err.Error())
			}

			recipe.Ingredients = ingredients
		} else {
			recipe.Ingredients = []domain.RecipeIngredient{}
		}

		// Add tags
		if err := r.GetRecipeTags(&recipe); err != nil {
			fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
		}

		// Get favorite status
		if err := r.GetRecipeFavorite(&recipe, id); err != nil {
			fmt.Printf("ERROR getting recipe favorite status. %s\n", err.Error())
		}

		recipes = append(recipes, recipe)
	}

	return recipes, nil
}

// GetUserRecipes gets a list of a users favorited recipes. This function does not ensure the user is
// authenticated or exists. If nothing is found, a blank slice will be returned. The resulting list
// is sorted by the created dates, newest first. Any errors will be bubbled to the caller.
func (r *RecipeRepository) GetUserFavoriteRecipes(id int) ([]domain.Recipe, error) {
	query := `
		SELECT r.id, r.title, r.description, r.instructions, r.serves, r.difficulty, r.duration, r.category, r.ingredients, r.
			userid, r.modified, r.created
		FROM favorites f
		JOIN recipes r ON r.id = f.recipeid
		WHERE f.userid = $1
		ORDER BY f.created DESC;
	`
	rows, err := r.db.Query(query, id)
	if err != nil {
		return nil, fmt.Errorf("Failed to query DB for user recipes. %s\n", err.Error())
	}
	defer rows.Close()

	var recipes []domain.Recipe
	for rows.Next() {
		var recipe domain.Recipe
		var durationBytes []byte
		var ingredientBytes []byte

		// Scan results from recipe query onto recipe object
		if err := rows.Scan(
			&recipe.Id,
			&recipe.Title,
			&recipe.Description,
			pq.Array(&recipe.Instructions),
			&recipe.Serves,
			&recipe.Difficulty,
			&durationBytes,
			&recipe.Category,
			&ingredientBytes,
			&recipe.UserId,
			&recipe.Modified,
			&recipe.Created,
		); err != nil {
			return nil, fmt.Errorf("Failed to scan row onto recipe object. %s\n", err.Error())
		}

		// Parse duration
		if len(durationBytes) > 0 {
			var duration domain.RecipeDuration
			if err := json.Unmarshal(durationBytes, &duration); err != nil {
				return nil, fmt.Errorf("Failed to parse duration from database: %s", err.Error())
			}

			recipe.Duration = duration
		} else {
			recipe.Duration = domain.RecipeDuration{}
		}

		// Parse ingredient
		if len(ingredientBytes) > 0 {
			var ingredients []domain.RecipeIngredient
			if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil {
				return nil, fmt.Errorf("Failed to parse ingredients from database: %s", err.Error())
			}

			recipe.Ingredients = ingredients
		} else {
			recipe.Ingredients = []domain.RecipeIngredient{}
		}

		// Add tags
		if err := r.GetRecipeTags(&recipe); err != nil {
			fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
		}

		// Set favorite status (they're always true!)
		recipe.Favorite = true

		recipes = append(recipes, recipe)
	}

	return recipes, nil
}

// GetRecipeTags requires a recipe to be filled with at least an ID. This function will use the ID
// defined in the provided recipe to fill the Tags array with the recipe's tags from the database.
// The recipe is modified in place and is not returned. If the recipe is nil, the function will
// return nothing (skipping).  Any errors will be bubbled to the caller.
func (r *RecipeRepository) GetRecipeTags(recipe *domain.Recipe) error {
	if recipe == nil {
		return nil
	}

	recipe.Tags = []domain.Tag{}

	query := `
		SELECT t.* FROM tags t
		JOIN recipetags rt ON rt.tagid = t.id
		WHERE rt.recipeid = $1;
	`
	rows, err := r.db.Query(query, recipe.Id)
	if err != nil {
		return fmt.Errorf("Failed to get tags for recipe. %s\n", err.Error())
	}
	defer rows.Close()

	for rows.Next() {
		var tag domain.Tag

		err := rows.Scan(&tag.Id, &tag.Name, &tag.Created)
		if err != nil {
			return fmt.Errorf("Failed to scan tag onto domain model. %s\n", err.Error())
		}

		recipe.Tags = append(recipe.Tags, tag)
	}

	return nil
}

// GetRecipeFavorite requires a recipe to be filled with at least an ID. This function will use the
// ID defined in the provided recipe to fill the favorite status of the recipe, based on the provided
// userId. The recipe is modified in place and is not returned. If the recipe is nil, the function
// will return nothing (skipping). Any errors will be bubbled to the caller.
func (r *RecipeRepository) GetRecipeFavorite(recipe *domain.Recipe, userId int) error {
	if recipe == nil {
		return nil
	}

	query := `
		SELECT COUNT(*)
		FROM favorites
		WHERE recipeid = $1 AND userid = $2;
	`

	var count int
	if err := r.db.QueryRow(query, recipe.Id, userId).Scan(&count); err != nil {
		return fmt.Errorf("Failed to get recipe favorite. %s", err.Error())
	}

	recipe.Favorite = count > 0

	return nil
}

// GetRecipeOfTheWeek searches for the most recent recipe of the week. If there is not a value,
// the recipe will be nil. This function simply collects the most recent entry in the recipeoftheweek
// table and return it. If there is no entry, nil will be returned Any errors will be bubbled to
// the caller.
func (r *RecipeRepository) GetRecipeOfTheWeek(userId *int) (*domain.Recipe, error) {
	query := `
		SELECT 
			r.id, r.title, r.description, r.instructions, r.serves, r.difficulty, r.duration, r.category, 
			r.ingredients, r.userid, r.modified, r.created
		FROM recipes r
		JOIN recipeoftheweek rw ON rw.recipeid = r.id
		ORDER BY created DESC
		LIMIT 1;
	`

	var durationBytes []byte
	var ingredientBytes []byte

	var recipe domain.Recipe
	if err := r.db.QueryRow(query).Scan(
		&recipe.Id,
		&recipe.Title,
		&recipe.Description,
		pq.Array(&recipe.Instructions),
		&recipe.Serves,
		&recipe.Difficulty,
		&durationBytes,
		&recipe.Category,
		&ingredientBytes,
		&recipe.UserId,
		&recipe.Modified,
		&recipe.Created,
	); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, nil
		}
		return nil, fmt.Errorf("Failed to location recipe in database: %s", err.Error())
	}

	// Parse duration
	if len(durationBytes) > 0 {
		var duration domain.RecipeDuration
		if err := json.Unmarshal(durationBytes, &duration); err != nil {
			return nil, fmt.Errorf("Failed to parse duration from database: %s", err.Error())
		}

		recipe.Duration = duration
	} else {
		recipe.Duration = domain.RecipeDuration{}
	}

	// Parse ingredient
	if len(ingredientBytes) > 0 {
		var ingredients []domain.RecipeIngredient
		if err := json.Unmarshal(ingredientBytes, &ingredients); err != nil {
			return nil, fmt.Errorf("Failed to parse ingredients from database: %s", err.Error())
		}

		recipe.Ingredients = ingredients
	} else {
		recipe.Ingredients = []domain.RecipeIngredient{}
	}

	// Add tags
	if err := r.GetRecipeTags(&recipe); err != nil {
		fmt.Printf("ERROR getting recipe tags. %s\n", err.Error())
	}

	// Get favorite status, if user id is provided
	if userId != nil {
		if err := r.GetRecipeFavorite(&recipe, *userId); err != nil {
			fmt.Printf("ERROR getting recipe favorite status. %s\n", err.Error())
		}
	} else {
		recipe.Favorite = false
	}

	return &recipe, nil
}
