package model

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

type Step struct {
	Id          string `json:"id"`
	Index       string `json:"index"`
	Text        string `json:"text"`
	Recipe      string `json:"recipe"`
	Ingredients []Ingredient `json:"ingredients"`
}

func (s Step) String() string {
	b, _ := json.MarshalIndent(s, "", "    ")
	return string(b)
}

func (s *Step) Validate() error {
	var err error

	if s.Id != "" {
		err = isPositiveOrZeroInt(s.Id)
		if err != nil {
			return fmt.Errorf("Invalid step ID: %w", err)
		}
	}

	if s.Index != "" {
		err = isPositiveOrZeroInt(s.Index)
		if err != nil {
			return fmt.Errorf("Invalid step index: %w", err)
		}
	}

	if s.Text == "" {
		return fmt.Errorf("Step text must not be empty")
	}

	err = isPositiveOrZeroInt(s.Recipe)
	if err != nil {
		return fmt.Errorf("Step does not reference a valid recipe ID: %w", err)
	}

	return nil
}

func (s *Step) Create(tx *sql.Tx) error {
	if s.Id != "" {
		return fmt.Errorf("Cannot create step if ID is given")
	}

	err := s.Validate()
	if err != nil {
		return err
	}

	cmd := `
INSERT INTO steps
    ('index', text, recipe)
VALUES
    (?, ?, ?)
`

	result, err := tx.Exec(cmd, s.Index, s.Text, s.Recipe)
	if err != nil {
		return err
	}

	id, err := result.LastInsertId()
	if err != nil {
		return err
	}

	s.Id = fmt.Sprint(id)

	for i := range s.Ingredients {
		s.Ingredients[i].Step = s.Id
		s.Ingredients[i].Index = fmt.Sprint(i)
		err = s.Ingredients[i].Create(tx)
		if err != nil {
			return err
		}
	}

	return nil
}

func (s *Step) getIngredientIds(tx *sql.Tx) ([]Ingredient, error) {
	retval := make([]Ingredient, 0)

	cmd := `
SELECT
    id
FROM
    ingredients
WHERE
    step = ?
ORDER BY
    'index' ASC
`

	rows, err := tx.Query(cmd, s.Id)
	if err != nil {
		return retval, err
	}
	defer rows.Close()

	for rows.Next() {
		i := Ingredient{}

		err = rows.Scan(&i.Id)
		if err != nil {
			return retval, err
		}

		retval = append(retval, i)
	}

	return retval, nil
}

func (s *Step) Read(tx *sql.Tx) error {
	cmd := `
SELECT
    "index", text, recipe
FROM
    steps
WHERE
    id = ?
`

	rows, err := tx.Query(cmd, s.Id)
	if err != nil {
		return err
	}
	defer rows.Close()

	if !rows.Next() {
		return sql.ErrNoRows
	}

	err = rows.Scan(&s.Index, &s.Text, &s.Recipe)
	if err != nil {
		return err
	}

	s.Ingredients, err = s.getIngredientIds(tx)
	if err != nil {
		return err
	}

	for i := range s.Ingredients {
		err = s.Ingredients[i].Read(tx)
		if err != nil {
			return err
		}
	}

	return s.Validate()
}

func (s *Step) Update(tx *sql.Tx) error {
	err := s.Validate()
	if err != nil {
		return err
	}

	oldIngredients, err := s.getIngredientIds(tx)
	if err != nil {
		return err
	}

	for i := range oldIngredients {
		err = oldIngredients[i].Delete(tx)
		if err != nil {
			return err
		}
	}

	for i := range s.Ingredients {
		s.Ingredients[i].Index = fmt.Sprint(i)
		s.Ingredients[i].Step = s.Id
		err = s.Ingredients[i].Create(tx)
		if err != nil {
			return err
		}
	}

	cmd := `
UPDATE
    steps
SET
    index = ?,
    text = ?,
    recipe = ?
WHERE
    id = ?`

	res, err := tx.Exec(cmd, s.Index, s.Text, s.Recipe, s.Id)
	if err != nil {
		return err
	}

	affected, err := res.RowsAffected()
	if err != nil {
		return err
	}
	if affected != 1 {
		return fmt.Errorf("Step update affected %d rows instead of 1", affected)
	}

	return nil
}

func (s *Step) Delete(tx *sql.Tx) error {
	oldIngredients, err := s.getIngredientIds(tx)
	if err != nil {
		return err
	}

	for i := range oldIngredients {
		err = oldIngredients[i].Delete(tx)
		if err != nil {
			return err
		}
	}

	cmd := `
DELETE FROM
    steps
WHERE
    id = ?
`

	result, err := tx.Exec(cmd, s.Id)
	if err != nil {
		return err
	}

	rows, err := result.RowsAffected()
	if rows != 1 {
		return errors.New("Step deletion did not affect exactly one row")
	}

	return nil
}