package payment

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

	"dev.tandem.ws/tandem/camper/pkg/auth"
	"dev.tandem.ws/tandem/camper/pkg/database"
	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"
	"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 "":
			switch r.Method {
			case http.MethodGet:
				servePaymentIndex(w, r, user, company, conn)
			default:
				httplib.MethodNotAllowed(w, r, http.MethodGet)
			}
		case "settings":
			head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
			switch head {
			case "":
				switch r.Method {
				case http.MethodGet:
					f := newSettingsForm(user.Locale)
					if err := f.FillFromDatabase(r.Context(), company, conn); err != nil {
						if !database.ErrorIsNotFound(err) {
							panic(err)
						}
					}
					f.MustRender(w, r, user, company)
				case http.MethodPut:
					updatePaymentSettings(w, r, user, company, conn)
				default:
					httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
				}
			default:
				http.NotFound(w, r)
			}
		default:
			if !uuid.Valid(head) {
				http.NotFound(w, r)
				return
			}
			payment, err := fetchPaymentDetails(r.Context(), conn, head, user.Locale)
			if err != nil {
				if database.ErrorIsNotFound(err) {
					http.NotFound(w, r)
					return
				}
				panic(err)
			}
			h.paymentHandler(user, company, conn, payment).ServeHTTP(w, r)
		}
	})
}

func (h *AdminHandler) paymentHandler(user *auth.User, company *auth.Company, conn *database.Conn, payment *paymentDetails) 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:
				if payment.Status == StatusPreAuthenticated {
					var err error
					if err = conn.QueryRow(r.Context(), "select environment from redsys where company_id = $1", company.ID).Scan(&payment.Environment); err != nil && !database.ErrorIsNotFound(err) {
						panic(err)
					}
					payment.AcceptPreauthRequest, err = payment.createRequest(r, user, company, conn, redsys.TransactionTypePreauthConfirm)
					if err != nil {
						panic(err)
					}
					payment.VoidPreauthRequest, err = payment.createRequest(r, user, company, conn, redsys.TransactionTypePreauthVoid)
					if err != nil {
						panic(err)
					}
				}
				payment.MustRender(w, r, user, company)
			default:
				httplib.MethodNotAllowed(w, r, http.MethodGet)
			}
		default:
			http.NotFound(w, r)
		}
	})
}

func servePaymentIndex(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
	filters := newFilterForm(r.Context(), conn, company, user.Locale)
	if err := filters.Parse(r); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	payments, err := collectPaymentEntries(r.Context(), company, conn, user.Locale, filters)
	if err != nil {
		panic(err)
	}
	page := &paymentIndex{
		Payments: filters.buildCursor(payments),
		Filters:  filters,
	}
	page.MustRender(w, r, user, company)
}

type paymentEntry struct {
	ID          int
	URL         string
	Reference   string
	DownPayment string
	Total       string
	Status      string
	StatusLabel string
	CreatedAt   time.Time
}

func collectPaymentEntries(ctx context.Context, company *auth.Company, conn *database.Conn, locale *locale.Locale, filters *filterForm) ([]*paymentEntry, error) {
	where, args := filters.BuildQuery([]interface{}{locale.Language})
	rows, err := conn.Query(ctx, fmt.Sprintf(`
		select payment_id
		     , '/admin/payments/' || payment.slug
		     , payment.reference
		     , to_price(payment.down_payment, decimal_digits)
		     , to_price(total, decimal_digits)
		     , payment.payment_status
		     , coalesce(payment_status_i18n.name, payment_status.name)
		     , created_at
		from payment
			join currency using (currency_code)
		    join payment_status using (payment_status)
        	left join payment_status_i18n on payment_status_i18n.payment_status = payment.payment_status
				and payment_status_i18n.lang_tag = $1
		where (%s)
		order by created_at desc
		       , payment_id
		limit %d
	`, where, filters.PerPage()+1), args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var payments []*paymentEntry
	for rows.Next() {
		entry := &paymentEntry{}
		if err = rows.Scan(
			&entry.ID,
			&entry.URL,
			&entry.Reference,
			&entry.DownPayment,
			&entry.Total,
			&entry.Status,
			&entry.StatusLabel,
			&entry.CreatedAt,
		); err != nil {
			return nil, err
		}
		payments = append(payments, entry)
	}

	return payments, nil
}

type paymentIndex struct {
	Payments []*paymentEntry
	Filters  *filterForm
}

func (page *paymentIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
	if httplib.IsHTMxRequest(r) && page.Filters.Paginated() {
		template.MustRenderAdminNoLayout(w, r, user, company, "payment/results.gohtml", page)
	} else {
		template.MustRenderAdminFiles(w, r, user, company, page, "payment/index.gohtml", "payment/results.gohtml")
	}
}

type paymentDetails struct {
	ID                   int
	Slug                 string
	Reference            string
	CampsiteType         string
	ArrivalDate          time.Time
	DepartureDate        time.Time
	NumNights            int
	NumAdults            int
	NumTeenagers         int
	NumChildren          int
	NumDogs              int
	SubtotalTouristTax   string
	Total                string
	DownPaymentPercent   int
	DownPayment          string
	ZonePreferences      string
	ACSICard             bool
	Status               string
	StatusLabel          string
	CreatedAt            time.Time
	UpdatedAt            time.Time
	Options              []*paymentOption
	Customer             *paymentCustomer
	Environment          string
	AcceptPreauthRequest *redsys.SignedRequest
	VoidPreauthRequest   *redsys.SignedRequest
}

type paymentOption struct {
	Label string
	Units int
}

type paymentCustomer struct {
	FullName   string
	Address    string
	PostalCode string
	City       string
	Country    string
	Email      string
	Phone      string
	Language   string
}

func fetchPaymentDetails(ctx context.Context, conn *database.Conn, slug string, locale *locale.Locale) (*paymentDetails, error) {
	row := conn.QueryRow(ctx, `
		select payment_id
		     , payment.slug
		     , payment.reference
		     , coalesce(campsite_type_i18n.name, campsite_type.name)
		     , arrival_date
		     , departure_date
		     , departure_date - arrival_date
		     , number_adults
		     , number_teenagers
		     , number_children
		     , number_dogs
		     , to_price(subtotal_tourist_tax, decimal_digits)
		     , to_price(total, decimal_digits)
		     , (down_payment_percent * 100)::integer
		     , to_price(payment.down_payment, decimal_digits)
		     , zone_preferences
		     , acsi_card
		     , payment.payment_status
		     , coalesce(payment_status_i18n.name, payment_status.name)
		     , created_at
		     , updated_at
		from payment
		    join currency using (currency_code)
		    join campsite_type using (campsite_type_id)
		    join payment_status using (payment_status)
        	left join payment_status_i18n on payment_status_i18n.payment_status = payment.payment_status
				and payment_status_i18n.lang_tag = $2
        	left join campsite_type_i18n on campsite_type_i18n.campsite_type_id = campsite_type.campsite_type_id
				and campsite_type_i18n.lang_tag = $2
		where payment.slug = $1
	`, slug, locale.Language)
	details := &paymentDetails{}
	if err := row.Scan(
		&details.ID,
		&details.Slug,
		&details.Reference,
		&details.CampsiteType,
		&details.ArrivalDate,
		&details.DepartureDate,
		&details.NumNights,
		&details.NumAdults,
		&details.NumTeenagers,
		&details.NumChildren,
		&details.NumDogs,
		&details.SubtotalTouristTax,
		&details.Total,
		&details.DownPaymentPercent,
		&details.DownPayment,
		&details.ZonePreferences,
		&details.ACSICard,
		&details.Status,
		&details.StatusLabel,
		&details.CreatedAt,
		&details.UpdatedAt,
	); err != nil {
		return nil, err
	}

	var err error
	details.Options, err = fetchPaymentOptions(ctx, conn, details.ID, locale)
	if err != nil && !database.ErrorIsNotFound(err) {
		return nil, err
	}

	details.Customer, err = fetchPaymentCustomer(ctx, conn, details.ID, locale)
	if err != nil && !database.ErrorIsNotFound(err) {
		return nil, err
	}

	return details, nil
}

func fetchPaymentOptions(ctx context.Context, conn *database.Conn, paymentID int, locale *locale.Locale) ([]*paymentOption, error) {
	rows, err := conn.Query(ctx, `
		select coalesce(option_i18n.name, option.name)
		     , units
		from payment_option
			join campsite_type_option as option using (campsite_type_option_id)
			left join campsite_type_option_i18n as option_i18n on option_i18n.campsite_type_option_id = option.campsite_type_option_id
				and option_i18n.lang_tag = $2
		where payment_id = $1
`, paymentID, locale.Language)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var options []*paymentOption
	for rows.Next() {
		option := &paymentOption{}
		if err = rows.Scan(&option.Label, &option.Units); err != nil {
			return nil, err
		}
		options = append(options, option)
	}
	return options, nil
}

func fetchPaymentCustomer(ctx context.Context, conn *database.Conn, paymentID int, locale *locale.Locale) (*paymentCustomer, error) {
	row := conn.QueryRow(ctx, `
		select full_name
		     , address
		     , postal_code
		     , city
		     , coalesce(country_i18n.name, country.name)
		     , email
		     , phone::text
		     , language.endonym
		from payment_customer
			join country using (country_code)
			left join country_i18n on country.country_code = country_i18n.country_code
				and country_i18n.lang_tag = $2
			join language on payment_customer.lang_tag = language.lang_tag
		where payment_id = $1
`, paymentID, locale.Language)
	customer := &paymentCustomer{}
	if err := row.Scan(
		&customer.FullName,
		&customer.Address,
		&customer.PostalCode,
		&customer.City,
		&customer.Country,
		&customer.Email,
		&customer.Phone,
		&customer.Language,
	); err != nil {
		return nil, err
	}
	return customer, nil
}

func (f *paymentDetails) createRequest(r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, transactionType redsys.TransactionType) (*redsys.SignedRequest, error) {
	schema := httplib.Protocol(r)
	authority := httplib.Host(r)
	paymentURL := fmt.Sprintf("%s://%s/admin/payments/%s", schema, authority, f.Slug)
	request := redsys.Request{
		TransactionType:  transactionType,
		Amount:           f.DownPayment,
		OrderNumber:      f.Reference,
		SuccessURL:       paymentURL,
		FailureURL:       paymentURL,
		NotificationURL:  fmt.Sprintf("%s/notification", publicBaseURL(r, user, f.Slug)),
		ConsumerLanguage: user.Locale.Language,
	}
	return request.Sign(r.Context(), conn, company)
}

func (f *paymentDetails) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
	template.MustRenderAdmin(w, r, user, company, "payment/details.gohtml", f)
}