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

package booking

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"net/http"

	"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/redsys"
	"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 := newPublicPage(r.Context(), company, conn, user.Locale)
				_ = r.ParseForm()
				page.Form.CampsiteType.FillValue(r)
				page.Form.ArrivalDate.FillValue(r)
				page.Form.DepartureDate.FillValue(r)
				page.MustRender(w, r, user, company, conn)
			case http.MethodPost:
				makeReservation(w, r, user, company, conn)
			default:
				httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost)
			}
		case "success":
			handleSuccessfulPayment(w, r, user, company, conn)
		case "failure":
			handleFailedPayment(w, r, user, company, conn)
		default:
			http.NotFound(w, r)
		}
	})
}

func makeReservation(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
	f := newBookingForm(r.Context(), company, conn, user.Locale)
	if err := f.Parse(r); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		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)
		}
		page := newPublicPageWithForm(f)
		page.MustRender(w, r, user, company, conn)
		return
	}

	schema := httplib.Protocol(r)
	authority := httplib.Host(r)
	request := &redsys.Request{
		TransactionType:  redsys.TransactionTypeCharge,
		Amount:           "12.34",
		OrderNumber:      randomOrderNumber(),
		Product:          "Test Booking",
		SuccessURL:       fmt.Sprintf("%s://%s/%s/booking/success", schema, authority, user.Locale.Language),
		FailureURL:       fmt.Sprintf("%s://%s/%s/booking/failure", schema, authority, user.Locale.Language),
		NotificationURL:  fmt.Sprintf("%s://%s/%s/booking/notification", schema, authority, user.Locale.Language),
		ConsumerLanguage: user.Locale.Language,
	}
	signed, err := redsys.SignRequest(r.Context(), conn, company, request)
	if err != nil {
		panic(err)
	}
	page := newPaymentPage(signed)
	page.MustRender(w, r, user, company, conn)
}

func randomOrderNumber() string {
	bytes := make([]byte, 6)
	if _, err := rand.Read(bytes); err != nil {
		panic(err)
	}
	return hex.EncodeToString(bytes)
}

type publicPage struct {
	*template.PublicPage
	Form *bookingForm
}

func newPublicPage(ctx context.Context, company *auth.Company, conn *database.Conn, l *locale.Locale) *publicPage {
	return newPublicPageWithForm(newBookingForm(ctx, company, conn, l))
}

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)
	template.MustRenderPublic(w, r, user, company, "booking.gohtml", p)
}

type bookingForm struct {
	FullName            *form.Input
	Address             *form.Input
	PostalCode          *form.Input
	City                *form.Input
	Country             *form.Select
	Email               *form.Input
	Phone               *form.Input
	CampsiteType        *form.Select
	CampsiteTypeOptions map[string][]*campsiteTypeOption
	ArrivalDate         *form.Input
	DepartureDate       *form.Input
	AreaPreferences     *form.Input
	ACSICard            *form.Checkbox
	Agreement           *form.Checkbox
}

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

func newBookingForm(ctx context.Context, company *auth.Company, conn *database.Conn, l *locale.Locale) *bookingForm {
	f := &bookingForm{
		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.MustGetOptions(ctx, conn, "select country.country_code, coalesce(i18n.name, country.name) as l10n_name from country left join country_i18n as i18n on country.country_code = i18n.country_code and i18n.lang_tag = $1 order by l10n_name", l.Language),
		},
		Email: &form.Input{
			Name: "email",
		},
		Phone: &form.Input{
			Name: "phone",
		},
		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 order by position, l10n_name", l.Language, company.ID),
		},
		CampsiteTypeOptions: make(map[string][]*campsiteTypeOption),
		ArrivalDate: &form.Input{
			Name: "arrival_date",
		},
		DepartureDate: &form.Input{
			Name: "departure_date",
		},
		AreaPreferences: &form.Input{
			Name: "area_preferences",
		},
		ACSICard: &form.Checkbox{
			Name: "acsi_card",
		},
		Agreement: &form.Checkbox{
			Name: "agreement",
		},
	}
	rows, err := conn.Query(ctx, `
		select 'campsite_type_option_' || option.campsite_type_option_id
		     , slug
		     , 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 company_id = $2
		order by option.position, l10_name
`, l.Language, company.ID)
	if err != nil {
		panic(err)
	}
	for rows.Next() {
		var slug string
		option := &campsiteTypeOption{
			Input: &form.Input{},
		}
		if err := rows.Scan(&option.Input.Name, &slug, &option.Label, &option.Input.Val, &option.Min, &option.Max); err != nil {
			panic(err)
		}
		f.CampsiteTypeOptions[slug] = append(f.CampsiteTypeOptions[slug], option)
	}
	if rows.Err() != nil {
		panic(rows.Err())
	}
	return f
}
func (f *bookingForm) Parse(r *http.Request) error {
	if err := r.ParseForm(); err != nil {
		return err
	}
	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.CampsiteType.FillValue(r)
	f.ArrivalDate.FillValue(r)
	f.DepartureDate.FillValue(r)
	f.AreaPreferences.FillValue(r)
	f.ACSICard.FillValue(r)
	f.Agreement.FillValue(r)
	for _, options := range f.CampsiteTypeOptions {
		for _, option := range options {
			option.Input.FillValue(r)
		}
	}
	return nil
}

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

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

	if country != "" && f.PostalCode.Val != "" {
		if _, err := v.CheckValidPostalCode(ctx, conn, f.PostalCode, country, l.GettextNoop("This postal code is not valid.")); err != nil {
			return false, 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.")) {
		if _, err := v.CheckValidPhone(ctx, conn, f.Phone, country, l.GettextNoop("This phone number is not valid.")); err != nil {
			return false, err
		}
	}
	var validBefore bool
	v.CheckSelectedOptions(f.CampsiteType, l.GettextNoop("Selected campsite type is not valid."))
	if v.CheckRequired(f.ArrivalDate, l.GettextNoop("Arrival date can not be empty")) {
		if v.CheckValidDate(f.ArrivalDate, l.GettextNoop("Arrival date must be a valid date.")) {
			validBefore = true
		}
	}
	if v.CheckRequired(f.DepartureDate, l.GettextNoop("Departure date can not be empty")) {
		if v.CheckValidDate(f.DepartureDate, l.GettextNoop("Departure date must be a valid date.")) && validBefore {
			v.CheckDateAfter(f.DepartureDate, f.ArrivalDate, l.GettextNoop("The departure date must be after the arrival date."))
		}
	}
	v.Check(f.Agreement, f.Agreement.Checked, l.GettextNoop("It is mandatory to agree to the reservation conditions."))
	for _, options := range f.CampsiteTypeOptions {
		for _, option := range 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))
					}
				}
			}
		}
	}
	return v.AllOK, nil
}
