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

package booking

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"strconv"
	"time"

	"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"
)

type PublicHandler struct {
}

func NewPublicHandler() *PublicHandler {
	return &PublicHandler{}
}

func (h *PublicHandler) 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 "":
			switch r.Method {
			case http.MethodGet:
				page, err := newPublicPage(r, company, conn, user.Locale)
				if err != nil {
					http.Error(w, err.Error(), http.StatusBadRequest)
					return
				}
				page.MustRender(w, r, user, company, conn)
			case http.MethodPost:
				requestPayment(w, r, user, company, conn)
			default:
				httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost)
			}
		default:
			http.NotFound(w, r)
		}
	})
}

type publicPage struct {
	*template.PublicPage
	Form        *bookingForm
	HasErrors   bool
	Environment string
}

func newPublicPage(r *http.Request, company *auth.Company, conn *database.Conn, l *locale.Locale) (*publicPage, error) {
	f, err := newBookingForm(r, company, conn, l)
	if err != nil {
		return nil, err
	}
	return newPublicPageWithForm(f), nil
}

func newPublicPageWithForm(form *bookingForm) *publicPage {
	return &publicPage{
		PublicPage: template.NewPublicPage(),
		Form:       form,
	}
}

func (p *publicPage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
	p.Setup(r, user, company, conn)
	if err := conn.QueryRow(r.Context(), "select environment from redsys where company_id = $1", company.ID).Scan(&p.Environment); err != nil && !database.ErrorIsNotFound(err) {
		panic(err)
	}
	if httplib.IsHTMxRequest(r) {
		template.MustRenderPublicNoLayout(w, r, user, company, "booking/fields.gohtml", p)
	} else {
		template.MustRenderPublicFiles(w, r, user, company, p, "booking/page.gohtml", "booking/fields.gohtml")
	}
}

type bookingForm struct {
	CampsiteType *form.Select
	PaymentSlug  *form.Input
	Dates        *DateFields
	Guests       *bookingGuestFields
	Options      *bookingOptionFields
	Customer     *bookingCustomerFields
	Cart         *bookingCart
}

type DateFields struct {
	MinNights     int
	MaxNights     int
	ArrivalDate   *bookingDateInput
	DepartureDate *bookingDateInput
}

type bookingDateInput struct {
	*form.Input
	MinDate time.Time
	MaxDate time.Time
}

type bookingGuestFields struct {
	MaxGuests       int
	OverflowAllowed bool
	Overflow        bool
	NumberAdults    *form.Input
	NumberTeenagers *form.Input
	NumberChildren  *form.Input
	NumberDogs      *form.Input
	ACSICard        *form.Checkbox
	Error           error
}

type bookingOptionFields struct {
	Legend          string
	ZonePreferences *form.Input
	Options         []*campsiteTypeOption
}

type campsiteTypeOption struct {
	ID       int
	Label    string
	Min      int
	Max      int
	Input    *form.Input
	Subtotal string
}

type bookingCustomerFields struct {
	FullName   *form.Input
	Address    *form.Input
	PostalCode *form.Input
	City       *form.Input
	Country    *form.Select
	Email      *form.Input
	Phone      *form.Input
	Agreement  *form.Checkbox
}

func newEmptyBookingForm(ctx context.Context, conn *database.Conn, company *auth.Company, l *locale.Locale) *bookingForm {
	return &bookingForm{
		CampsiteType: &form.Select{
			Name:    "campsite_type",
			Options: form.MustGetOptions(ctx, conn, "select type.slug, coalesce(i18n.name, type.name) as l10n_name from campsite_type as type left join campsite_type_i18n as i18n on type.campsite_type_id = i18n.campsite_type_id and i18n.lang_tag = $1 where company_id = $2 and active order by position, l10n_name", l.Language, company.ID),
		},
		PaymentSlug: &form.Input{
			Name: "payment_slug",
		},
	}
}

func newBookingForm(r *http.Request, company *auth.Company, conn *database.Conn, l *locale.Locale) (*bookingForm, error) {
	if err := r.ParseForm(); err != nil {
		return nil, err
	}

	f := newEmptyBookingForm(r.Context(), conn, company, l)
	f.CampsiteType.FillValue(r)
	f.PaymentSlug.FillValue(r)
	campsiteType := f.CampsiteType.String()
	if campsiteType == "" {
		return f, nil
	}
	if !f.CampsiteType.ValidOptionsSelected() {
		f.CampsiteType.Error = errors.New(l.Gettext("Selected campsite type is not valid."))
		return f, nil
	}

	var err error
	f.Dates, err = NewDateFields(r.Context(), conn, campsiteType)
	if err != nil {
		return nil, err
	}
	f.Dates.FillValues(r, l)
	if f.Dates.ArrivalDate.Val == "" || f.Dates.ArrivalDate.Error != nil || f.Dates.DepartureDate.Val == "" || f.Dates.DepartureDate.Error != nil {
		return f, nil
	}

	f.Guests, err = newBookingGuestFields(r.Context(), conn, campsiteType, f.Dates.ArrivalDate.Val, f.Dates.DepartureDate.Val)
	if err != nil {
		return nil, err
	}
	f.Guests.FillValues(r, l)

	f.Options, err = newBookingOptionFields(r.Context(), conn, campsiteType, l)
	if err != nil {
		return nil, err
	}
	if f.Options != nil {
		f.Options.FillValues(r)
	}

	f.Customer = newBookingCustomerFields(r.Context(), conn, l)
	f.Customer.FillValues(r)

	f.Cart, err = newBookingCart(r.Context(), conn, f, campsiteType)
	if err != nil {
		return nil, err
	}

	return f, nil
}

func (f *bookingForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) {
	v := form.NewValidator(l)

	if f.Dates == nil {
		return false, errors.New("no booking date fields")
	}
	if f.Guests == nil {
		return false, errors.New("no guests fields")
	}
	if f.Customer == nil {
		return false, errors.New("no customer fields")
	}
	if f.Cart == nil {
		return false, errors.New("no booking cart")
	}

	v.CheckSelectedOptions(f.CampsiteType, l.GettextNoop("Selected campsite type is not valid."))
	f.Dates.Valid(v, l)
	f.Guests.Valid(v, l)
	if err := f.Customer.Valid(ctx, conn, v, l); err != nil {
		return false, err
	}
	if f.Options != nil {
		f.Options.Valid(v, l)
	}
	return v.AllOK, nil
}

func NewDateFields(ctx context.Context, conn *database.Conn, campsiteType string) (*DateFields, error) {
	row := conn.QueryRow(ctx, `
		select lower(bookable_nights),
		       upper(bookable_nights) - 1,
		       greatest(min(lower(season_range)), lower(operating_dates), current_timestamp::date),
		       least(max(upper(season_range)), upper(operating_dates))
		from campsite_type
		         join campsite_type_cost using (campsite_type_id)
		         join season_calendar using (season_id)
		where campsite_type.slug = $1
		  and season_range >> daterange(date_trunc('year', current_timestamp)::date, date_trunc('year', current_timestamp)::date + 1)
		group by bookable_nights, operating_dates
	`, campsiteType)
	f := &DateFields{
		ArrivalDate: &bookingDateInput{
			Input: &form.Input{Name: "arrival_date"},
		},
		DepartureDate: &bookingDateInput{
			Input: &form.Input{Name: "departure_date"},
		},
	}
	if err := row.Scan(&f.MinNights, &f.MaxNights, &f.ArrivalDate.MinDate, &f.DepartureDate.MaxDate); err != nil {
		return nil, err
	}
	f.ArrivalDate.MaxDate = f.DepartureDate.MaxDate.AddDate(0, 0, -f.MinNights)
	f.DepartureDate.MinDate = f.ArrivalDate.MinDate.AddDate(0, 0, f.MinNights)

	return f, nil
}

func (f *DateFields) FillValues(r *http.Request, l *locale.Locale) {
	f.ArrivalDate.FillValue(r)
	f.DepartureDate.FillValue(r)
	f.AdjustValues(l)
}

func (f *DateFields) AdjustValues(l *locale.Locale) {
	if f.ArrivalDate.Val != "" {
		arrivalDate, err := time.Parse(database.ISODateFormat, f.ArrivalDate.Val)
		if err != nil {
			f.ArrivalDate.Error = errors.New(l.Gettext("Arrival date must be a valid date."))
			return
		}
		f.DepartureDate.MinDate = arrivalDate.AddDate(0, 0, f.MinNights)
		maxDate := arrivalDate.AddDate(0, 0, f.MaxNights)
		if maxDate.Before(f.DepartureDate.MaxDate) {
			f.DepartureDate.MaxDate = maxDate
		}

		if f.DepartureDate.Val == "" {
			f.DepartureDate.Val = f.DepartureDate.MinDate.Format(database.ISODateFormat)
		} else {
			departureDate, err := time.Parse(database.ISODateFormat, f.DepartureDate.Val)
			if err != nil {
				f.DepartureDate.Error = errors.New(l.Gettext("Departure date must be a valid date."))
				return
			}
			if departureDate.Before(f.DepartureDate.MinDate) {
				f.DepartureDate.Val = f.DepartureDate.MinDate.Format(database.ISODateFormat)
			} else if departureDate.After(f.DepartureDate.MaxDate) {
				f.DepartureDate.Val = f.DepartureDate.MaxDate.Format(database.ISODateFormat)
			}
		}
	}
}

func (f *DateFields) Valid(v *form.Validator, l *locale.Locale) {
	var validBefore bool
	if v.CheckRequired(f.ArrivalDate.Input, l.GettextNoop("Arrival date can not be empty")) {
		if v.CheckValidDate(f.ArrivalDate.Input, l.GettextNoop("Arrival date must be a valid date.")) {
			if v.CheckMinDate(f.ArrivalDate.Input, f.ArrivalDate.MinDate, fmt.Sprintf(l.Gettext("Arrival date must be %s or after."), f.ArrivalDate.MinDate.Format(database.ISODateFormat))) {
				v.CheckMaxDate(f.ArrivalDate.Input, f.ArrivalDate.MaxDate, fmt.Sprintf(l.Gettext("Arrival date must be %s or before."), f.ArrivalDate.MaxDate.Format(database.ISODateFormat)))
			}
		}
	}
	if v.CheckRequired(f.DepartureDate.Input, l.GettextNoop("Departure date can not be empty")) {
		if v.CheckValidDate(f.DepartureDate.Input, l.GettextNoop("Departure date must be a valid date.")) && validBefore {
			if v.CheckMinDate(f.DepartureDate.Input, f.DepartureDate.MinDate, fmt.Sprintf(l.Gettext("Departure date must be %s or after."), f.DepartureDate.MinDate.Format(database.ISODateFormat))) {
				v.CheckMaxDate(f.DepartureDate.Input, f.DepartureDate.MaxDate, fmt.Sprintf(l.Gettext("Departure date must be %s or before."), f.DepartureDate.MaxDate.Format(database.ISODateFormat)))
			}
		}
	}
}

func newBookingGuestFields(ctx context.Context, conn *database.Conn, campsiteType string, arrivalDate string, departureDate string) (*bookingGuestFields, error) {
	f := &bookingGuestFields{
		NumberAdults:    &form.Input{Name: "number_adults"},
		NumberTeenagers: &form.Input{Name: "number_teenagers"},
		NumberChildren:  &form.Input{Name: "number_children"},
	}
	row := conn.QueryRow(ctx, `
		select max_campers
		     , overflow_allowed
		     , pet.cost_per_night is not null as dogs_allowed
		     , exists (
		         select 1 from acsi_calendar
				 where acsi_calendar.campsite_type_id = campsite_type.campsite_type_id
				   and acsi_range && daterange($2::date, $3::date)
			 ) as acsi_allowed
		from campsite_type
			left join campsite_type_pet_cost as pet using (campsite_type_id)
		where slug = $1
	`, campsiteType, arrivalDate, departureDate)
	var dogsAllowed bool
	var ACSIAllowed bool
	if err := row.Scan(&f.MaxGuests, &f.OverflowAllowed, &dogsAllowed, &ACSIAllowed); err != nil {
		return nil, err
	}
	if dogsAllowed {
		f.NumberDogs = &form.Input{Name: "number_dogs"}
	}
	if ACSIAllowed {
		f.ACSICard = &form.Checkbox{Name: "acsi_card"}
	}
	return f, nil
}

func (f *bookingGuestFields) FillValues(r *http.Request, l *locale.Locale) {
	numGuests := 0
	numGuests += fillNumericField(f.NumberAdults, r, 1)
	numGuests += fillNumericField(f.NumberTeenagers, r, 0)
	numGuests += fillNumericField(f.NumberChildren, r, 0)
	if f.NumberDogs != nil {
		fillNumericField(f.NumberDogs, r, 0)
	}
	if f.ACSICard != nil {
		f.ACSICard.FillValue(r)
	}
	f.AdjustValues(numGuests, l)
}

func (f *bookingGuestFields) AdjustValues(numGuests int, l *locale.Locale) {
	if numGuests > f.MaxGuests {
		if f.OverflowAllowed {
			f.Overflow = true
		} else {
			f.Error = fmt.Errorf(l.Gettext("There can be at most %d guests in this accommodation."), f.MaxGuests)
		}
	}
}

func fillNumericField(input *form.Input, r *http.Request, min int) int {
	input.FillValue(r)
	if input.Val == "" {
		input.Val = strconv.Itoa(min)
		return min
	}
	val, err := strconv.Atoi(input.Val)
	if err != nil {
		input.Val = strconv.Itoa(min)
		val = min
	}
	return val
}

func (f *bookingGuestFields) Valid(v *form.Validator, l *locale.Locale) {
	if v.CheckRequired(f.NumberAdults, l.GettextNoop("Number of adults can not be empty")) {
		if v.CheckValidInteger(f.NumberAdults, l.GettextNoop("Number of adults must be an integer.")) {
			v.CheckMinInteger(f.NumberAdults, 1, l.GettextNoop("There must be at least one adult."))
		}
	}
	if v.CheckRequired(f.NumberTeenagers, l.GettextNoop("Number of teenagers can not be empty")) {
		if v.CheckValidInteger(f.NumberTeenagers, l.GettextNoop("Number of teenagers must be an integer.")) {
			v.CheckMinInteger(f.NumberTeenagers, 0, l.GettextNoop("Number of teenagers can not be negative."))
		}
	}
	if v.CheckRequired(f.NumberChildren, l.GettextNoop("Number of children can not be empty")) {
		if v.CheckValidInteger(f.NumberChildren, l.GettextNoop("Number of children must be an integer.")) {
			v.CheckMinInteger(f.NumberChildren, 0, l.GettextNoop("Number of children can not be negative."))
		}
	}
	if f.NumberDogs != nil && v.CheckRequired(f.NumberDogs, l.GettextNoop("Number of dogs can not be empty")) {
		if v.CheckValidInteger(f.NumberDogs, l.GettextNoop("Number of dogs must be an integer.")) {
			v.CheckMinInteger(f.NumberDogs, 0, l.GettextNoop("Number of dogs can not be negative."))
		}
	}
}

func newBookingOptionFields(ctx context.Context, conn *database.Conn, campsiteType string, l *locale.Locale) (*bookingOptionFields, error) {
	f := &bookingOptionFields{}

	row := conn.QueryRow(ctx, `
		select coalesce(i18n.name, campsite_type.name)
		     , ask_zone_preferences
		from campsite_type
		left join campsite_type_i18n as i18n on i18n.campsite_type_id = campsite_type.campsite_type_id and i18n.lang_tag = $1
		where slug = $2
	`, l.Language, campsiteType)
	var askZonePreferences bool
	if err := row.Scan(&f.Legend, &askZonePreferences); err != nil {
		return nil, err
	}
	if askZonePreferences {
		f.ZonePreferences = &form.Input{Name: "zone_preferences"}
	}

	rows, err := conn.Query(ctx, `
		select option.campsite_type_option_id
		     , 'campsite_type_option_' || option.campsite_type_option_id
		     , coalesce(i18n.name, option.name) as l10_name
		     , lower(range)::text
		     , lower(range)
		     , upper(range)
		from campsite_type_option as option
		join campsite_type using (campsite_type_id)
		left join campsite_type_option_i18n as i18n on i18n.campsite_type_option_id = option.campsite_type_option_id and i18n.lang_tag = $1
		where slug = $2
		  and campsite_type.active
		order by option.position, l10_name
`, l.Language, campsiteType)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	for rows.Next() {
		option := &campsiteTypeOption{
			Input: &form.Input{},
		}
		if err = rows.Scan(&option.ID, &option.Input.Name, &option.Label, &option.Input.Val, &option.Min, &option.Max); err != nil {
			return nil, err
		}
		f.Options = append(f.Options, option)
	}
	if rows.Err() != nil {
		return nil, rows.Err()
	}

	if f.ZonePreferences == nil && len(f.Options) == 0 {
		return nil, nil
	}
	return f, nil
}

func (f *bookingOptionFields) FillValues(r *http.Request) {
	if f.ZonePreferences != nil {
		f.ZonePreferences.FillValue(r)
	}
	for _, option := range f.Options {
		fillNumericField(option.Input, r, option.Min)
	}
}

func (f *bookingOptionFields) FillFromDatabase(ctx context.Context, conn *database.Conn, bookingID int) error {
	rows, err := conn.Query(ctx, `
	select campsite_type_option.campsite_type_option_id
	     , coalesce(units, lower(range))::text
	     , to_price(coalesce(subtotal, 0), decimal_digits)
    from booking
    	join campsite_type_option using (campsite_type_id)
		left join booking_option
			 on booking.booking_id = booking_option.booking_id
			and booking_option.campsite_type_option_id = campsite_type_option.campsite_type_option_id
    	join currency using (currency_code)
		where booking.booking_id = $1
`, bookingID)
	if err != nil {
		return err
	}
	defer rows.Close()

	for rows.Next() {
		var id int
		var units string
		var subtotal string
		if err = rows.Scan(&id, &units, &subtotal); err != nil {
			return err
		}
		for _, option := range f.Options {
			if option.ID == id {
				option.Input.Val = units
				option.Subtotal = subtotal
				break
			}
		}
	}
	return nil
}

func (f *bookingOptionFields) Valid(v *form.Validator, l *locale.Locale) {
	for _, option := range f.Options {
		if v.CheckRequired(option.Input, fmt.Sprintf(l.Gettext("%s can not be empty"), option.Label)) {
			if v.CheckValidInteger(option.Input, fmt.Sprintf(l.Gettext("%s must be an integer."), option.Label)) {
				if v.CheckMinInteger(option.Input, option.Min, fmt.Sprintf(l.Gettext("%s must be %d or greater."), option.Label, option.Min)) {
					v.CheckMaxInteger(option.Input, option.Max, fmt.Sprintf(l.Gettext("%s must be at most %d."), option.Label, option.Max))
				}
			}
		}
	}
}

func newBookingCustomerFields(ctx context.Context, conn *database.Conn, l *locale.Locale) *bookingCustomerFields {
	return &bookingCustomerFields{
		FullName: &form.Input{
			Name: "full_name",
		},
		Address: &form.Input{
			Name: "address",
		},
		PostalCode: &form.Input{
			Name: "postal_code",
		},
		City: &form.Input{
			Name: "city",
		},
		Country: &form.Select{
			Name:    "country",
			Options: form.MustGetCountryOptions(ctx, conn, l),
		},
		Email: &form.Input{
			Name: "email",
		},
		Phone: &form.Input{
			Name: "phone",
		},
		Agreement: &form.Checkbox{
			Name: "agreement",
		},
	}
}

func (f *bookingCustomerFields) FillValues(r *http.Request) {
	f.FullName.FillValue(r)
	f.Address.FillValue(r)
	f.PostalCode.FillValue(r)
	f.City.FillValue(r)
	f.Country.FillValue(r)
	f.Email.FillValue(r)
	f.Phone.FillValue(r)
	f.Agreement.FillValue(r)
}

func (f *bookingCustomerFields) Valid(ctx context.Context, conn *database.Conn, v *form.Validator, l *locale.Locale) error {
	var country string
	if v.CheckSelectedOptions(f.Country, l.GettextNoop("Selected country is not valid.")) {
		country = f.Country.Selected[0]
	}

	if v.CheckRequired(f.FullName, l.GettextNoop("Full name can not be empty.")) {
		v.CheckMinLength(f.FullName, 1, l.GettextNoop("Full name must have at least one letter."))
	}

	v.CheckRequired(f.Address, l.GettextNoop("Address can not be empty."))
	v.CheckRequired(f.City, l.GettextNoop("Town or village can not be empty."))
	if v.CheckRequired(f.PostalCode, l.GettextNoop("Postcode can not be empty.")) && country != "" {
		if _, err := v.CheckValidPostalCode(ctx, conn, f.PostalCode, country, l.GettextNoop("This postcode is not valid.")); err != nil {
			return err
		}
	}
	if v.CheckRequired(f.Email, l.GettextNoop("Email can not be empty.")) {
		v.CheckValidEmail(f.Email, l.GettextNoop("This email is not valid. It should be like name@domain.com."))
	}
	if v.CheckRequired(f.Phone, l.GettextNoop("Phone can not be empty.")) && country != "" {
		if _, err := v.CheckValidPhone(ctx, conn, f.Phone, country, l.GettextNoop("This phone number is not valid.")); err != nil {
			return err
		}
	}

	v.Check(f.Agreement, f.Agreement.Checked, l.GettextNoop("It is mandatory to agree to the reservation conditions."))
	return nil
}
