package repository

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

	sq "github.com/Masterminds/squirrel"
	domain "github.com/haydenhargreaves/Potion/internal/domain/recipe"
	"github.com/jmoiron/sqlx"
	"github.com/lib/pq"
)

type RecipeRepository struct {
	db *sqlx.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 *sqlx.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 {
	// Convert data into a readable format
	durationJSON, err := json.Marshal(recipe.Duration)
	if err != nil {
		return err
	}

	ingredientsStore := domain.RecipeIngredientStore{
		Sections:    recipe.Sections,
		Ingredients: recipe.Ingredients,
	}

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

	instructions := make([]string, len(recipe.Instructions))
	for i, instruction := range recipe.Instructions {
		instructions[i] = instruction.Content
	}

	psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)

	query := psql.
		Insert("recipes").
		Columns(
			"title",
			"description",
			"instructions",
			"serves",
			"difficulty",
			"duration",
			"category",
			"ingredients",
			"userid",
			"modified",
			"created",
		).
		Values(
			recipe.Title,
			recipe.Description,
			pq.Array(instructions),
			recipe.Serves,
			recipe.Difficulty,
			durationJSON,
			string(recipe.Category),
			ingredientsJSON,
			recipe.UserId,
			nil,
			recipe.Created,
		).
		Suffix("RETURNING id")

	_sql, args, err := query.ToSql()
	if err != nil {
		return fmt.Errorf("Failed to construct query: %w", err)
	}

	var id int
	if err := r.db.Get(&id, _sql, args...); err != nil {
		return fmt.Errorf("Failed to create recipe: %w", err)
	}

	// Set the new ID
	recipe.Id = id

	return nil
}

// EditRecipe updates a recipe in the database. The recipe provided must contain an ID, otherwise this
// function will fail - it will not know what recipe to edit.
func (r *RecipeRepository) EditRecipe(recipe *domain.Recipe, userId int) error {
	if recipe.Id <= 0 {
		return fmt.Errorf("Recipe must contain an ID. Cannot edit unknown recipe.")
	}

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

	ingredientsStore := domain.RecipeIngredientStore{
		Sections:    recipe.Sections,
		Ingredients: recipe.Ingredients,
	}

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

	instructions := make([]string, len(recipe.Instructions))
	for i, instruction := range recipe.Instructions {
		instructions[i] = instruction.Content
	}

	psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)

	query := psql.
		Update("recipes").
		Set("title", recipe.Title).
		Set("description", recipe.Description).
		Set("instructions", pq.Array(instructions)).
		Set("serves", recipe.Serves).
		Set("difficulty", recipe.Difficulty).
		Set("duration", durationJSON).
		Set("category", string(recipe.Category)).
		Set("ingredients", ingredientsJSON).
		Set("modified", time.Now().UTC()).
		Where(sq.Eq{
			"id":     recipe.Id,
			"userid": userId,
		})

	_sql, args, err := query.ToSql()
	if err != nil {
		return fmt.Errorf("Failed to construct query: %w", err)
	}

	result, err := r.db.Exec(_sql, args...)
	if err != nil {
		return fmt.Errorf("Failed to update recipe: %w", err)
	}

	if rows, err := result.RowsAffected(); err != nil {
		return err
	} else if rows != 1 {
		return fmt.Errorf("Modified an unexpected number of rows. Expected 1, modified %d.", rows)
	}

	return nil
}

// DeleteRecipe deletes a recipe in the database. This is done by setting the deleted field to true.
// This will create a "soft delete" effect. This function does not validate that the user is the owner,
// so the caller should validate the owner. If any errors occur, they will be returned to the caller.
func (r *RecipeRepository) DeleteRecipe(recipeId, userId int) error {
	psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)

	query := psql.
		Update("recipes").
		Set("deleted", true).
		Set("modified", time.Now().UTC()).
		Where(sq.Eq{
			"id":      recipeId,
			"userid":  userId,
			"deleted": false,
		})

	sql, args, err := query.ToSql()
	if err != nil {
		return fmt.Errorf("Failed to build delete query: %w", err)
	}

	result, err := r.db.Exec(sql, args...)
	if err != nil {
		return fmt.Errorf("Failed to delete recipe: %w", err)
	}

	rows, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("Failed to get rows affects: %w", err)
	}
	if rows != 1 {
		return fmt.Errorf("Incorrect number of rows modified. Expected 1, received %d.", rows)
	}

	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.
//
// This function will only return recipes that are not deleted. Any recipes marked deleted will be ignored
// and the standard "not-found" error will be returned.
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, deleted
		FROM recipes 
		WHERE id = $1 AND deleted = false;
	`

	var durationBytes []byte
	var instructions pq.StringArray
	var ingredientBytes []byte

	var recipe domain.Recipe
	if err := r.db.QueryRow(query, id).Scan(
		&recipe.Id,
		&recipe.Title,
		&recipe.Description,
		&instructions,
		&recipe.Serves,
		&recipe.Difficulty,
		&durationBytes,
		&recipe.Category,
		&ingredientBytes,
		&recipe.UserId,
		&recipe.Modified,
		&recipe.Created,
		&recipe.Deleted,
	); 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 store domain.RecipeIngredientStore
		if err := json.Unmarshal(ingredientBytes, &store); err != nil {
			// Check for unmarshal to support backwards compatability
			return nil, fmt.Errorf("Failed to parse ingredients from database: %s", err.Error())
		}

		recipe.Ingredients = store.Ingredients
		recipe.Sections = store.Sections
	} else {
		recipe.Ingredients = []domain.RecipeIngredient{}
	}

	// Add instructions
	for _, instruction := range instructions {
		recipe.Instructions = append(recipe.Instructions, domain.RecipeInstruction{Content: instruction})
	}

	// 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.
//
// This function calls a function that only returns recipes that are not deleted. Any recipes marked
// deleted will be ignored and the standard "not-found" error will be returned.
func (r *RecipeRepository) GetRecipes(ids []int, userId *int) ([]domain.Recipe, error) {
	var recipes []domain.Recipe

	for _, id := range ids {
		recipe, err := r.GetRecipe(id, userId)
		if err != nil {
			return nil, err
		}

		// Skip any un-found recipes...?
		if recipe != nil {
			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.
//
// 12/28/25: This function has changed, now longer returns the recipes, but their IDs for fetching
// elsewhere.
//
// 2/3/26: Refactored this large function to use Squirrel for simpler generation. Reduced line count by 50,
// but this is still insane. We need to clean this up.
//
// This function will only return recipes that are not deleted. Any recipes marked deleted will be ignored
// and the standard "not-found" error will be returned.
func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *int, favorites bool) ([]int, error) {
	// Begin creating the query
	psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
	query := psql.Select("r.id").From("recipes r")

	// Only select fields where the recipe ID can be found in the favorites table (mapped to user ID)
	if favorites && userId != nil {
		query = query.
			Join("favorites f ON f.recipeId = r.id").
			Where(sq.Eq{"f.userid": *userId})
	}

	// Compute and add meal type filters (7 bit options)
	var mealCategories []string
	for i := range 7 {
		if isBitActive(filters.MealType, i) {
			mealCategories = append(mealCategories, string(domain.ParseMeal(i)))
		}
	}

	if len(mealCategories) > 0 {
		query = query.Where(sq.Eq{"category": mealCategories})
	}

	// Compute and add time filters (5 bit options)
	var timeOr sq.Or
	for i := range 5 {
		if isBitActive(filters.Time, i) {
			switch i {
			case 0:
				timeOr = append(timeOr, sq.Lt{"(duration->>'total')::int": 15})
			case 1:
				timeOr = append(timeOr, sq.Expr("(duration->>'total')::int BETWEEN 15 AND 30"))
			case 2:
				timeOr = append(timeOr, sq.Expr("(duration->>'total')::int BETWEEN 30 AND 60"))
			case 3:
				timeOr = append(timeOr, sq.Expr("(duration->>'total')::int BETWEEN 60 AND 120"))
			case 4:
				timeOr = append(timeOr, sq.Gt{"(duration->>'total')::int": 120})
			}
		}
	}

	if len(timeOr) > 0 {
		query = query.Where(timeOr)
	}

	// Compute and add difficulty filters (5 bit options)
	var difficulties []int
	for i := range 5 {
		if isBitActive(filters.Difficulty, i) {
			difficulties = append(difficulties, i+1)
		}
	}

	if len(difficulties) > 0 {
		query = query.Where(sq.Eq{"difficulty": difficulties})
	}

	// Compute and add serving size filters (5 bit options)
	var servingOr sq.Or
	for i := range 5 {
		if isBitActive(filters.ServingSize, i) {
			switch i {
			case 0:
				servingOr = append(servingOr, sq.Expr("serves BETWEEN 1 AND 2"))
			case 1:
				servingOr = append(servingOr, sq.Expr("serves BETWEEN 2 AND 4"))
			case 2:
				servingOr = append(servingOr, sq.Expr("serves BETWEEN 4 AND 6"))
			case 3:
				servingOr = append(servingOr, sq.Expr("serves BETWEEN 6 AND 8"))
			case 4:
				servingOr = append(servingOr, sq.Gt{"serves": 8})
			}
		}
	}

	if len(servingOr) > 0 {
		query = query.Where(servingOr)
	}

	// Handle search with full-text search and ILIKE fallback
	if filters.Search != "" {
		spl := strings.Split(filters.Search, " ")
		var cleaned []string

		// Sanitize search terms
		replacer := strings.NewReplacer(
			"'", "",
			"-", "",
			"&", "",
			"|", "",
			"!", "",
			":", "",
			"(", "",
			")", "",
		)

		for _, term := range spl {
			q := strings.TrimSpace(replacer.Replace(term))
			if q != "" {
				cleaned = append(cleaned, q+":*") // Add prefix matching
			}
		}

		if len(cleaned) > 0 {
			vectorQuery := strings.Join(cleaned, " | ")

			// Build search condition as raw SQL expression
			// We'll use sq.Expr for the entire OR clause
			var searchConditions []string
			var searchArgs []interface{}

			// Full-text search
			searchConditions = append(searchConditions, "r.search_vector @@ to_tsquery('english', ?)")
			searchArgs = append(searchArgs, vectorQuery)

			// ILIKE fallback for substring matching
			for _, term := range spl {
				cleanTerm := strings.TrimSpace(replacer.Replace(term))
				if cleanTerm != "" {
					searchConditions = append(searchConditions, "r.title ILIKE ?")
					searchArgs = append(searchArgs, "%"+cleanTerm+"%")

					searchConditions = append(searchConditions, "r.description ILIKE ?")
					searchArgs = append(searchArgs, "%"+cleanTerm+"%")
				}
			}

			// Combine all conditions with OR
			searchExpr := fmt.Sprintf("(%s)", strings.Join(searchConditions, " OR "))
			query = query.Where(sq.Expr(searchExpr, searchArgs...))

			// Add ordering for search results
			query = query.
				OrderBy(fmt.Sprintf("CASE WHEN r.search_vector @@ to_tsquery('english', '%s') THEN 1 ELSE 2 END", vectorQuery)).
				OrderBy(fmt.Sprintf("ts_rank(r.search_vector, to_tsquery('english', '%s')) DESC", vectorQuery)).
				OrderBy(fmt.Sprintf("ts_rank_cd(r.search_vector, to_tsquery('english', '%s')) DESC", vectorQuery))
		}
	}

	// Exclude deleted recipes
	query = query.Where(sq.Eq{"deleted": false})

	sql, args, err := query.ToSql()
	if err != nil {
		return nil, fmt.Errorf("Failed to build query: %w", err)
	}

	// Execute query using SQLX
	var ids []int
	if err = r.db.Select(&ids, sql, args...); err != nil {
		return nil, fmt.Errorf("Failed to query recipes: %w", err)
	}

	return ids, 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 {
		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
}

// UpdateRecipeTags replaces all existing tags for a recipe with a new list of tags.
// It removes all current tag associations, creates any new tags that don't exist,
// and creates new associations for the provided tags. The recipe object must contain
// a valid ID. Any errors will be bubbled to the caller.
func (r *RecipeRepository) UpdateRecipeTags(recipe domain.Recipe, tags []string) error {
	tx, err := r.db.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback() // Rollback if we don't commit

	if recipe.Id <= 0 {
		return fmt.Errorf("[ERROR] Recipe must have a valid ID")
	}

	// Step 1: Delete all existing tag associations for this recipe
	deleteQuery := `DELETE FROM RecipeTags WHERE RecipeId = $1;`
	if _, err := tx.Exec(deleteQuery, recipe.Id); err != nil {
		return fmt.Errorf("[ERROR] Failed to delete existing recipe tags: %w", err)
	}

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

	// If no tags provided, we're done (all tags removed)
	if len(normalized) == 0 {
		if err := tx.Commit(); err != nil {
			return err
		}
		return nil
	}

	// Step 3: 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("[ERROR] Failed to retrieve or create tag: %w", err)
		}
		tagIds = append(tagIds, tagId)
	}

	// Step 4: Insert the new tag associations
	// Use a single prepared statement for all inserts
	stmt, err := tx.Prepare("INSERT INTO RecipeTags (RecipeId, TagId) VALUES ($1, $2);")
	if err != nil {
		return fmt.Errorf("[ERROR] Failed to create statement for recipe tag mapping: %w", err)
	}
	defer stmt.Close()

	for _, id := range tagIds {
		if _, err := stmt.Exec(recipe.Id, id); err != nil {
			return fmt.Errorf("[ERROR] Failed to insert tag-recipe mapping: %w", err)
		}
	}

	// Commit the transaction
	if err := tx.Commit(); err != nil {
		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.
//
// 12/28/25: This now returns just the IDs, the service can handle fetching them.
//
// This function will only return recipes that are not deleted. Any recipes marked deleted will be ignored
// and the standard "not-found" error will be returned.
func (r *RecipeRepository) GetUserRecipesIds(userId int) ([]int, error) {
	psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)

	query := psql.
		Select("id").
		From("recipes").
		Where(sq.Eq{
			"userid":  userId,
			"deleted": false,
		}).
		OrderBy("created DESC")

	_sql, args, err := query.ToSql()
	if err != nil {
		return []int{}, fmt.Errorf("Failed to construct SQL query: %w", err)
	}

	var ids []int
	if err := r.db.Select(&ids, _sql, args...); err != nil {
		return []int{}, fmt.Errorf("Failed to get user recipes: %w", err)
	}

	return ids, 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.
//
// 12/28/25: This now just returns the IDs, so the service can handle the fetching.
//
// This function will only return recipes that are not deleted. Any recipes marked deleted will be ignored
// and the standard "not-found" error will be returned.
func (r *RecipeRepository) GetUserFavoriteRecipesIds(userId int) ([]int, error) {
	psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)

	query := psql.
		Select("r.id").
		From("favorites f").
		Join("recipes r on r.id = f.recipeid").
		Where(sq.Eq{
			"f.userid": userId,
			"deleted":  false,
		}).
		OrderBy("f.created DESC")

	_sql, args, err := query.ToSql()
	if err != nil {
		return []int{}, fmt.Errorf("Failed to construct SQL query: %w", err)
	}

	fmt.Println(_sql)

	var ids []int
	if err := r.db.Select(&ids, _sql, args...); err != nil {
		return []int{}, fmt.Errorf("Failed to get users' favorite recipes: %w", err)
	}

	return ids, 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
	}

	psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)

	query := psql.
		Select("COUNT(*)").
		From("favorites").
		Where(sq.Eq{
			"recipeid": recipe.Id,
			"userid":   userId,
		})

	_sql, args, err := query.ToSql()
	if err != nil {
		return fmt.Errorf("Failed to construct SQL query: %w", err)
	}

	var count int
	if err := r.db.Get(&count, _sql, args...); err != nil {
		return fmt.Errorf("Failed to get recipe favorite status: %w", err)
	}

	recipe.Favorite = count > 0

	return nil
}

// GetRecipeOfTheWeekId 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. All that is returned is the recipe ID, that way the caller can handle the fetching.
func (r *RecipeRepository) GetRecipeOfTheWeekId(userId *int) (*int, error) {
	psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)

	query := psql.
		Select("r.id").
		From("recipes r").
		Join("recipeoftheweek rw ON rw.recipeid = r.id").
		Where(sq.Eq{"r.deleted": false}).
		OrderBy("rw.created DESC").
		Limit(1)

	_sql, args, err := query.ToSql()
	if err != nil {
		return nil, fmt.Errorf("Failed to build SQL query: %w", err)
	}

	var recipeId int
	if err := r.db.Get(&recipeId, _sql, args...); err != nil {
		if err == sql.ErrNoRows {
			return nil, nil
		}
		return nil, fmt.Errorf("Failed to locate recipe in database: %s", err.Error())
	}

	return &recipeId, nil
}

// IsRecipeOwner takes two required arguments: a user id and a recipe id. This function queries the DB
// to check if the user is the owner of the provided recipe. Any error will be bubbled to the caller.
func (r *RecipeRepository) IsRecipeOwner(userId, recipeId int) (bool, error) {
	psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)

	query := psql.
		Select("userid").
		From("recipes").
		Where(sq.Eq{
			"id":      recipeId,
			"deleted": false,
		})

	_sql, args, err := query.ToSql()
	if err != nil {
		return false, err
	}

	var recipeOwnerId int
	if err := r.db.Get(&recipeOwnerId, _sql, args...); err != nil {
		if err == sql.ErrNoRows {
			return false, nil
		}
		return false, fmt.Errorf("Failed to get recipe owner id: %w", err)
	}

	return recipeOwnerId == userId, nil
}
