/*
 * SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
 * SPDX-License-Identifier: AGPL-3.0-only
 */

package types

import (
	"context"
	"fmt"
	"net/http"

	"github.com/jackc/pgx/v4"

	"dev.tandem.ws/tandem/camper/pkg/auth"
	"dev.tandem.ws/tandem/camper/pkg/database"
	"dev.tandem.ws/tandem/camper/pkg/form"
	httplib "dev.tandem.ws/tandem/camper/pkg/http"
	"dev.tandem.ws/tandem/camper/pkg/locale"
	"dev.tandem.ws/tandem/camper/pkg/template"
	"dev.tandem.ws/tandem/camper/pkg/uuid"
)

type AdminHandler struct {
}

func NewAdminHandler() *AdminHandler {
	return &AdminHandler{}
}

func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var head string
		head, r.URL.Path = httplib.ShiftPath(r.URL.Path)

		switch head {
		case "new":
			switch r.Method {
			case http.MethodGet:
				f, err := newTypeForm(r.Context(), company, conn)
				if err != nil {
					panic(err)
				}
				f.MustRender(w, r, user, company)
			default:
				httplib.MethodNotAllowed(w, r, http.MethodGet)
			}
		case "":
			switch r.Method {
			case http.MethodGet:
				serveTypeIndex(w, r, user, company, conn)
			case http.MethodPost:
				addType(w, r, user, company, conn)
			default:
				httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost)
			}
		case "order":
			switch r.Method {
			case http.MethodPost:
				orderTypes(w, r, user, company, conn)
			default:
				httplib.MethodNotAllowed(w, r, http.MethodPost)
			}
		default:
			if !uuid.Valid(head) {
				http.NotFound(w, r)
				return
			}
			f, err := newTypeForm(r.Context(), company, conn)
			if err != nil {
				panic(err)
			}
			if err := f.FillFromDatabase(r.Context(), conn, head); err != nil {
				if database.ErrorIsNotFound(err) {
					http.NotFound(w, r)
					return
				}
				panic(err)
			}
			h.typeHandler(user, company, conn, f).ServeHTTP(w, r)
		}
	})
}

func (h *AdminHandler) typeHandler(user *auth.User, company *auth.Company, conn *database.Conn, f *typeForm) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var head string
		head, r.URL.Path = httplib.ShiftPath(r.URL.Path)

		switch head {
		case "":
			switch r.Method {
			case http.MethodGet:
				f.MustRender(w, r, user, company)
			case http.MethodPut:
				editType(w, r, user, company, conn, f)
			default:
				httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
			}
		case "features":
			h.featuresHandler(user, company, conn, f.Slug).ServeHTTP(w, r)
		case "options":
			h.optionsHandler(user, company, conn, f.Slug).ServeHTTP(w, r)
		case "slides":
			h.carouselHandler(user, company, conn, f.Slug).ServeHTTP(w, r)
		default:
			http.NotFound(w, r)
		}
	})
}

func serveTypeIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
	types, err := collectTypeEntries(r.Context(), company, conn)
	if err != nil {
		panic(err)
	}
	page := &typeIndex{
		Types: types,
	}
	page.MustRender(w, r, user, company)
}

type typeEntry struct {
	Slug   string
	Name   string
	Active bool
}

func collectTypeEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*typeEntry, error) {
	rows, err := conn.Query(ctx, `
		select campsite_type.slug
		     , campsite_type.name
		     , campsite_type.active
		from campsite_type
		where campsite_type.company_id = $1
		order by position, name
	`, company.ID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var types []*typeEntry
	for rows.Next() {
		entry := &typeEntry{}
		if err = rows.Scan(&entry.Slug, &entry.Name, &entry.Active); err != nil {
			return nil, err
		}
		types = append(types, entry)
	}

	return types, nil
}

type typeIndex struct {
	Types []*typeEntry
}

func (page *typeIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
	template.MustRenderAdmin(w, r, user, company, "campsite/type/index.gohtml", page)
}

func addType(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
	f, err := newTypeForm(r.Context(), company, conn)
	if err != nil {
		panic(err)
	}
	processTypeForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error {
		var err error
		f.Slug, err = tx.AddCampsiteType(ctx, company.ID, f.Media.Int(), f.Name[f.DefaultLang].Val, f.Spiel[f.DefaultLang].Val, f.Info[f.DefaultLang].Val, f.Facilities[f.DefaultLang].Val, f.Description[f.DefaultLang].Val, f.AdditionalInfo[f.DefaultLang].Val, f.CheckIn[f.DefaultLang].Val, f.CheckOut[f.DefaultLang].Val, f.MaxCampers.Int(), f.MinNights.Int(), f.MaxNights.Int(), f.OverflowAllowed.Checked, f.AskZonePreferences.Checked)
		if err != nil {
			return err
		}
		if err := translateTypes(ctx, tx, company, f); err != nil {
			return err
		}
		if err := setTypeDogsPrice(ctx, tx, f); err != nil {
			return err
		}
		return setTypePrices(ctx, tx, f.Slug, f.Prices)
	})
}

func translateTypes(ctx context.Context, tx *database.Tx, company *auth.Company, f *typeForm) error {
	for lang := range company.Locales {
		l := lang.String()
		if l == f.DefaultLang {
			continue
		}
		if err := tx.TranslateCampsiteType(ctx, f.Slug, lang, f.Name[l].Val, f.Spiel[l].Val, f.Info[l].Val, f.Facilities[l].Val, f.Description[l].Val, f.AdditionalInfo[l].Val, f.CheckIn[l].Val, f.CheckOut[l].Val); err != nil {
			return err
		}
	}
	return nil
}

func setTypeDogsPrice(ctx context.Context, tx *database.Tx, f *typeForm) error {
	if f.DogsAllowed.Checked {
		return tx.SetCampsitePetTypeCost(ctx, f.Slug, f.DogsPrice.Val)
	} else {
		return tx.UnsetCampsitePetTypeCost(ctx, f.Slug)
	}
}

func setTypePrices(ctx context.Context, tx *database.Tx, slug string, prices map[int]*typePriceForm) error {
	for seasonID, p := range prices {
		if err := tx.SetCampsiteTypeCost(ctx, slug, seasonID, p.PricePerNight.Val, p.PricePerAdult.Val, p.PricePerTeenager.Val, p.PricePerChild.Val); err != nil {
			return err
		}
	}
	return nil
}

func editType(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *typeForm) {
	processTypeForm(w, r, user, company, conn, f, func(ctx context.Context, tx *database.Tx) error {
		if _, err := tx.EditCampsiteType(ctx, f.Slug, f.Media.Int(), f.Name[f.DefaultLang].Val, f.Spiel[f.DefaultLang].Val, f.Info[f.DefaultLang].Val, f.Facilities[f.DefaultLang].Val, f.Description[f.DefaultLang].Val, f.AdditionalInfo[f.DefaultLang].Val, f.CheckIn[f.DefaultLang].Val, f.CheckOut[f.DefaultLang].Val, f.MaxCampers.Int(), f.MinNights.Int(), f.MaxNights.Int(), f.OverflowAllowed.Checked, f.AskZonePreferences.Checked, f.Active.Checked); err != nil {
			return err
		}
		if err := translateTypes(ctx, tx, company, f); err != nil {
			return err
		}
		if err := setTypeDogsPrice(ctx, tx, f); err != nil {
			return err
		}
		return setTypePrices(ctx, tx, f.Slug, f.Prices)
	})
}

func orderTypes(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
	if err := r.ParseForm(); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := user.VerifyCSRFToken(r); err != nil {
		http.Error(w, err.Error(), http.StatusForbidden)
		return
	}
	slugs := r.PostForm["slug"]
	if len(slugs) > 0 {
		for _, slug := range slugs {
			if !uuid.Valid(slug) {
				w.WriteHeader(http.StatusUnprocessableEntity)
				return
			}
		}
		if err := conn.OrderCampsiteTypes(r.Context(), slugs); err != nil {
			panic(err)
		}
	}
	serveTypeIndex(w, r, user, company, conn)
}

func processTypeForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *typeForm, act func(ctx context.Context, tx *database.Tx) error) {
	if err := f.Parse(r); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := user.VerifyCSRFToken(r); err != nil {
		http.Error(w, err.Error(), http.StatusForbidden)
		return
	}
	if ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil {
		panic(err)
	} else if !ok {
		if !httplib.IsHTMxRequest(r) {
			w.WriteHeader(http.StatusUnprocessableEntity)
		}
		f.MustRender(w, r, user, company)
		return
	}

	tx := conn.MustBegin(r.Context())
	if err := act(r.Context(), tx); err == nil {
		if err := tx.Commit(r.Context()); err != nil {
			panic(err)
		}
	} else {
		if err := tx.Rollback(r.Context()); err != nil {
			panic(err)
		}
		panic(err)
	}
	httplib.Redirect(w, r, "/admin/campsites/types", http.StatusSeeOther)
}

type typeForm struct {
	DefaultLang        string
	Slug               string
	Active             *form.Checkbox
	Media              *form.Media
	Name               form.I18nInput
	MaxCampers         *form.Input
	MinNights          *form.Input
	MaxNights          *form.Input
	DogsAllowed        *form.Checkbox
	DogsPrice          *form.Input
	OverflowAllowed    *form.Checkbox
	AskZonePreferences *form.Checkbox
	Spiel              form.I18nInput
	Info               form.I18nInput
	Facilities         form.I18nInput
	Description        form.I18nInput
	AdditionalInfo     form.I18nInput
	CheckIn            form.I18nInput
	CheckOut           form.I18nInput
	Prices             map[int]*typePriceForm
}

type typePriceForm struct {
	SeasonName       string
	PricePerNight    *form.Input
	PricePerAdult    *form.Input
	PricePerTeenager *form.Input
	PricePerChild    *form.Input
}

func newTypeForm(ctx context.Context, company *auth.Company, conn *database.Conn) (*typeForm, error) {
	f := &typeForm{
		DefaultLang: company.DefaultLanguage.String(),
		Active: &form.Checkbox{
			Name:    "active",
			Checked: true,
		},
		Media: &form.Media{
			Input: &form.Input{
				Name: "media",
			},
			Label:  locale.PgettextNoop("Cover image", "input"),
			Prompt: locale.PgettextNoop("Set campsite type cover", "action"),
		},
		Name: form.NewI18nInput(company.Locales, "name"),
		MaxCampers: &form.Input{
			Name: "max_campers",
		},
		MinNights: &form.Input{
			Name: "min_nights",
			Val:  "1",
		},
		MaxNights: &form.Input{
			Name: "max_nights",
			Val:  "7",
		},
		DogsAllowed: &form.Checkbox{
			Name: "dogs_allowed",
		},
		DogsPrice: &form.Input{
			Name: "dogs_price",
		},
		OverflowAllowed: &form.Checkbox{
			Name: "overflow_allowed",
		},
		AskZonePreferences: &form.Checkbox{
			Name: "ask_zone_preferences",
		},
		Spiel:          form.NewI18nInput(company.Locales, "spiel"),
		Info:           form.NewI18nInput(company.Locales, "info"),
		Facilities:     form.NewI18nInput(company.Locales, "facilities"),
		Description:    form.NewI18nInput(company.Locales, "description"),
		AdditionalInfo: form.NewI18nInput(company.Locales, "additional_info"),
		CheckIn:        form.NewI18nInput(company.Locales, "check_in"),
		CheckOut:       form.NewI18nInput(company.Locales, "check_out"),
	}

	rows, err := conn.Query(ctx, "select season_id, name from season where active and company_id = $1", company.ID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	f.Prices = make(map[int]*typePriceForm)
	for rows.Next() {
		var id int
		var name string
		if err = rows.Scan(&id, &name); err != nil {
			return nil, err
		}
		f.Prices[id] = &typePriceForm{
			SeasonName: name,
			PricePerNight: &form.Input{
				Name: fmt.Sprintf("season.%d.price_per_night", id),
				Val:  "0",
			},
			PricePerAdult: &form.Input{
				Name: fmt.Sprintf("season.%d.price_per_adult", id),
				Val:  "0",
			},
			PricePerTeenager: &form.Input{
				Name: fmt.Sprintf("season.%d.price_per_teenager", id),
				Val:  "0",
			},
			PricePerChild: &form.Input{
				Name: fmt.Sprintf("season.%d.price_per_child", id),
				Val:  "0",
			},
		}
	}

	return f, nil
}

func (f *typeForm) FillFromDatabase(ctx context.Context, conn *database.Conn, slug string) error {
	f.Slug = slug
	var name database.RecordArray
	var spiel database.RecordArray
	var info database.RecordArray
	var facilities database.RecordArray
	var description database.RecordArray
	var additionalInfo database.RecordArray
	var checkIn database.RecordArray
	var checkOut database.RecordArray
	row := conn.QueryRow(ctx, `
		select campsite_type.name
		     , campsite_type.spiel::text
		     , campsite_type.info::text
		     , campsite_type.facilities::text
		     , campsite_type.description::text
		     , campsite_type.additional_info::text
		     , campsite_type.check_in::text
		     , campsite_type.check_out::text
		     , media_id::text
		     , max_campers::text
		     , lower(bookable_nights)::text
		     , (upper(bookable_nights) - 1)::text
		     , pet.cost_per_night is not null as dogs_allowed
		     , to_price(coalesce(pet.cost_per_night, 0)) as pet_cost
		     , overflow_allowed
		     , ask_zone_preferences
		     , active
		     , array_agg((lang_tag, i18n.name))
		     , array_agg((lang_tag, i18n.spiel::text))
		     , array_agg((lang_tag, i18n.info::text))
		     , array_agg((lang_tag, i18n.facilities::text))
		     , array_agg((lang_tag, i18n.description::text))
		     , array_agg((lang_tag, i18n.additional_info::text))
		     , array_agg((lang_tag, i18n.check_in))
		     , array_agg((lang_tag, i18n.check_out))
		from campsite_type
		    left join campsite_type_pet_cost as pet using (campsite_type_id)
        	left join campsite_type_i18n as i18n using (campsite_type_id)
		where slug = $1
		group by campsite_type.name
		       , campsite_type.spiel::text
		       , campsite_type.info::text
		       , campsite_type.facilities::text
		       , campsite_type.description::text
		       , campsite_type.additional_info::text
		       , campsite_type.check_in
		       , campsite_type.check_out
		       , media_id::text
		       , max_campers::text
		       , bookable_nights
		       , pet.cost_per_night
		       , overflow_allowed
		       , ask_zone_preferences
		       , active
	`, pgx.QueryResultFormats{pgx.BinaryFormatCode}, slug)
	if err := row.Scan(
		&f.Name[f.DefaultLang].Val,
		&f.Spiel[f.DefaultLang].Val,
		&f.Info[f.DefaultLang].Val,
		&f.Facilities[f.DefaultLang].Val,
		&f.Description[f.DefaultLang].Val,
		&f.AdditionalInfo[f.DefaultLang].Val,
		&f.CheckIn[f.DefaultLang].Val,
		&f.CheckOut[f.DefaultLang].Val,
		&f.Media.Val,
		&f.MaxCampers.Val,
		&f.MinNights.Val,
		&f.MaxNights.Val,
		&f.DogsAllowed.Checked,
		&f.DogsPrice.Val,
		&f.OverflowAllowed.Checked,
		&f.AskZonePreferences.Checked,
		&f.Active.Checked,
		&name,
		&spiel,
		&info,
		&facilities,
		&description,
		&additionalInfo,
		&checkIn,
		&checkOut,
	); err != nil {
		return err
	}
	if err := f.Name.FillArray(name); err != nil {
		return err
	}
	if err := f.Spiel.FillArray(spiel); err != nil {
		return err
	}
	if err := f.Info.FillArray(info); err != nil {
		return err
	}
	if err := f.Facilities.FillArray(facilities); err != nil {
		return err
	}
	if err := f.Description.FillArray(description); err != nil {
		return err
	}
	if err := f.AdditionalInfo.FillArray(additionalInfo); err != nil {
		return err
	}
	if err := f.CheckIn.FillArray(checkIn); err != nil {
		return err
	}
	if err := f.CheckOut.FillArray(checkOut); err != nil {
		return err
	}

	rows, err := conn.Query(ctx, `
		select season_id
		     , to_price(cost_per_night)
		     , to_price(cost_per_adult)
		     , to_price(cost_per_teenager)
		     , to_price(cost_per_child)
		from campsite_type
		join campsite_type_cost using (campsite_type_id)
		where slug = $1
	`, slug)
	if err != nil {
		return err
	}
	defer rows.Close()

	for rows.Next() {
		var seasonID int
		var pricePerNight string
		var pricePerAdult string
		var pricePerTeenager string
		var pricePerChild string
		if err = rows.Scan(&seasonID, &pricePerNight, &pricePerAdult, &pricePerTeenager, &pricePerChild); err != nil {
			return err
		}
		if p, ok := f.Prices[seasonID]; ok {
			p.PricePerNight.Val = pricePerNight
			p.PricePerAdult.Val = pricePerAdult
			p.PricePerTeenager.Val = pricePerTeenager
			p.PricePerChild.Val = pricePerChild
		}
	}
	return rows.Err()
}

func (f *typeForm) Parse(r *http.Request) error {
	if err := r.ParseForm(); err != nil {
		return err
	}
	f.Active.FillValue(r)
	f.Name.FillValue(r)
	f.MaxCampers.FillValue(r)
	f.MinNights.FillValue(r)
	f.MaxNights.FillValue(r)
	f.DogsAllowed.FillValue(r)
	f.DogsPrice.FillValue(r)
	f.OverflowAllowed.FillValue(r)
	f.AskZonePreferences.FillValue(r)
	f.Spiel.FillValue(r)
	f.Info.FillValue(r)
	f.Facilities.FillValue(r)
	f.Description.FillValue(r)
	f.AdditionalInfo.FillValue(r)
	f.CheckIn.FillValue(r)
	f.CheckOut.FillValue(r)
	f.Media.FillValue(r)
	for _, p := range f.Prices {
		p.PricePerNight.FillValue(r)
		p.PricePerAdult.FillValue(r)
		p.PricePerTeenager.FillValue(r)
		p.PricePerChild.FillValue(r)
	}
	return nil
}

func (f *typeForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) {
	v := form.NewValidator(l)
	if v.CheckRequired(f.Name[f.DefaultLang], l.GettextNoop("Name can not be empty.")) {
		v.CheckMinLength(f.Name[f.DefaultLang], 1, l.GettextNoop("Name must have at least one letter."))
	}
	v.CheckRequired(f.CheckIn[f.DefaultLang], l.GettextNoop("Check-in can not be empty."))
	v.CheckRequired(f.CheckOut[f.DefaultLang], l.GettextNoop("Check-out can not be empty."))
	if v.CheckRequired(f.Media.Input, l.GettextNoop("Cover image can not be empty.")) {
		if _, err := v.CheckImageMedia(ctx, conn, f.Media.Input, l.GettextNoop("Cover image must be an image media type.")); err != nil {
			return false, err
		}
	}
	if v.CheckRequired(f.MaxCampers, l.GettextNoop("Maximum number of campers can not be empty.")) {
		if v.CheckValidInteger(f.MaxCampers, l.GettextNoop("Maximum number of campers must be an integer number.")) {
			v.CheckMinInteger(f.MaxCampers, 1, l.GettextNoop("Maximum number of campers must be one or greater."))
		}
	}
	if v.CheckRequired(f.MinNights, l.GettextNoop("Minimum number of nights can not be empty.")) {
		if v.CheckValidInteger(f.MinNights, l.GettextNoop("Minimum number of nights must be an integer.")) {
			v.CheckMinInteger(f.MinNights, 1, l.GettextNoop("Minimum number of nights must be one or greater."))
		}
	}
	if v.CheckRequired(f.MaxNights, l.GettextNoop("Maximum number of nights can not be empty.")) {
		if v.CheckValidInteger(f.MaxNights, l.GettextNoop("Maximum number of nights must be an integer.")) && f.MinNights.Error == nil {
			v.CheckMinInteger(f.MaxNights, f.MinNights.Int(), l.GettextNoop("Maximum number of nights must be equal or greater than the minimum."))
		}
	}
	if f.DogsAllowed.Checked {
		if v.CheckRequired(f.DogsPrice, l.GettextNoop("Dogs price can not be empty when dogs are allowed.")) {
			if v.CheckValidDecimal(f.DogsPrice, l.GettextNoop("Dogs price must be a decimal number.")) {
				v.CheckMinDecimal(f.DogsPrice, 0.0, l.GettextNoop("Dogs price must be zero or greater."))
			}
		}
	}
	for _, p := range f.Prices {
		if v.CheckRequired(p.PricePerNight, l.GettextNoop("Price per night can not be empty.")) {
			if v.CheckValidDecimal(p.PricePerNight, l.GettextNoop("Price per night must be a decimal number.")) {
				v.CheckMinDecimal(p.PricePerNight, 0.0, l.GettextNoop("Price per night must be zero or greater."))
			}
		}
		if v.CheckRequired(p.PricePerAdult, l.GettextNoop("Price per adult can not be empty.")) {
			if v.CheckValidDecimal(p.PricePerAdult, l.GettextNoop("Price per adult must be a decimal number.")) {
				v.CheckMinDecimal(p.PricePerAdult, 0.0, l.GettextNoop("Price per adult must be zero or greater."))
			}
		}
		if v.CheckRequired(p.PricePerTeenager, l.GettextNoop("Price per teenager can not be empty.")) {
			if v.CheckValidDecimal(p.PricePerTeenager, l.GettextNoop("Price per teenager must be a decimal number.")) {
				v.CheckMinDecimal(p.PricePerTeenager, 0.0, l.GettextNoop("Price per teenager must be zero or greater."))
			}
		}
		if v.CheckRequired(p.PricePerChild, l.GettextNoop("Price per child can not be empty.")) {
			if v.CheckValidDecimal(p.PricePerChild, l.GettextNoop("Price per child must be a decimal number.")) {
				v.CheckMinDecimal(p.PricePerChild, 0.0, l.GettextNoop("Price per child must be zero or greater."))
			}
		}
	}
	return v.AllOK, nil
}

func (f *typeForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
	template.MustRenderAdmin(w, r, user, company, "campsite/type/form.gohtml", f)
}
