package payment

import (
	"context"
	"fmt"
	ht "html/template"
	"log"
	"net/http"
	tt "text/template"
	"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/mail"
	"dev.tandem.ws/tandem/camper/pkg/redsys"
	"dev.tandem.ws/tandem/camper/pkg/template"
	"dev.tandem.ws/tandem/camper/pkg/uuid"
)

const (
	StatusCompleted        = "completed"
	StatusPreAuthenticated = "preauth"
)

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 paymentSlug string
		paymentSlug, r.URL.Path = httplib.ShiftPath(r.URL.Path)

		if !uuid.Valid(paymentSlug) {
			http.NotFound(w, r)
			return
		}
		payment, err := fetchPayment(r.Context(), conn, paymentSlug)
		if err != nil {
			if database.ErrorIsNotFound(err) {
				http.NotFound(w, r)
				return
			}
			panic(err)
		}

		var head string
		head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
		switch head {
		case "":
			switch r.Method {
			case http.MethodGet:
				page := newPaymentPage(payment)
				page.MustRender(w, r, user, company, conn)
			default:
				httplib.MethodNotAllowed(w, r, http.MethodGet)
			}
		case "success":
			handleSuccessfulPayment(w, r, user, company, conn, payment)
		case "failure":
			handleFailedPayment(w, r, user, company, conn, payment)
		case "notification":
			handleNotification(w, r, user, company, conn, payment)
		default:
			http.NotFound(w, r)
		}
	})
}

func fetchPayment(ctx context.Context, conn *database.Conn, paymentSlug string) (*Payment, error) {
	row := conn.QueryRow(ctx, `
	select payment_id
	     , payment.slug::text
	     , payment.reference
	     , payment.created_at
	     , to_price(total, decimal_digits)
	     , to_price(payment.down_payment, decimal_digits)
	from payment
	join currency using (currency_code)
	where payment.slug = $1
	  and payment_status <> 'draft'
`, paymentSlug)
	payment := &Payment{}
	if err := row.Scan(&payment.ID, &payment.Slug, &payment.Reference, &payment.CreateTime, &payment.Total, &payment.DownPayment); err != nil {
		return nil, err
	}
	return payment, nil
}

type Payment struct {
	ID          int
	Slug        string
	Reference   string
	Total       string
	DownPayment string
	CreateTime  time.Time
}

func (payment *Payment) createRequest(r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) (*redsys.SignedRequest, error) {
	baseURL := publicBaseURL(r, user, payment.Slug)
	request := redsys.Request{
		TransactionType:  redsys.TransactionTypePreauth,
		Amount:           payment.DownPayment,
		OrderNumber:      payment.Reference,
		Product:          user.Locale.Pgettext("Campsite Booking", "order product name"),
		SuccessURL:       fmt.Sprintf("%s/success", baseURL),
		FailureURL:       fmt.Sprintf("%s/failure", baseURL),
		NotificationURL:  fmt.Sprintf("%s/notification", baseURL),
		ConsumerLanguage: user.Locale.Language,
	}
	return request.Sign(r.Context(), conn, company)
}

func publicBaseURL(r *http.Request, user *auth.User, slug string) string {
	schema := httplib.Protocol(r)
	authority := httplib.Host(r)
	return fmt.Sprintf("%s://%s/%s/payments/%s", schema, authority, user.Locale.Language, slug)
}

type paymentPage struct {
	*template.PublicPage
	Environment string
	Payment     *Payment
	Request     *redsys.SignedRequest
}

func newPaymentPage(payment *Payment) *paymentPage {
	return &paymentPage{
		PublicPage: template.NewPublicPage(),
		Payment:    payment,
	}
}

func (p *paymentPage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
	p.Setup(r, user, company, conn)
	request, err := p.Payment.createRequest(r, user, company, conn)
	if err != nil {
		panic(err)
	}
	p.Request = request
	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)
	}
	template.MustRenderPublicFiles(w, r, user, company, p, "payment/request.gohtml", "payment/details.gohtml")
}
func handleSuccessfulPayment(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, payment *Payment) {
	var head string
	head, r.URL.Path = httplib.ShiftPath(r.URL.Path)

	switch head {
	case "":
		switch r.Method {
		case http.MethodGet:
			page := newSuccessfulPaymentPage(payment)
			page.MustRender(w, r, user, company, conn)
		default:
			httplib.MethodNotAllowed(w, r, http.MethodGet)
		}
	default:
		http.NotFound(w, r)
	}
}

type successfulPaymentPage struct {
	*template.PublicPage
	Payment *Payment
}

func newSuccessfulPaymentPage(payment *Payment) *successfulPaymentPage {
	return &successfulPaymentPage{
		PublicPage: template.NewPublicPage(),
		Payment:    payment,
	}
}

func (p *successfulPaymentPage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
	p.Setup(r, user, company, conn)
	template.MustRenderPublicFiles(w, r, user, company, p, "payment/success.gohtml", "payment/details.gohtml")
}

func handleFailedPayment(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, payment *Payment) {
	var head string
	head, r.URL.Path = httplib.ShiftPath(r.URL.Path)

	switch head {
	case "":
		switch r.Method {
		case http.MethodGet:
			page := newFailedPaymentPage(payment)
			page.MustRender(w, r, user, company, conn)
		default:
			httplib.MethodNotAllowed(w, r, http.MethodGet)
		}
	default:
		http.NotFound(w, r)
	}
}

type failedPaymentPage struct {
	*template.PublicPage
	Payment *Payment
}

func newFailedPaymentPage(payment *Payment) *failedPaymentPage {
	return &failedPaymentPage{
		PublicPage: template.NewPublicPage(),
		Payment:    payment,
	}
}

func (p *failedPaymentPage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
	p.Setup(r, user, company, conn)
	template.MustRenderPublicFiles(w, r, user, company, p, "payment/failure.gohtml", "payment/details.gohtml")
}

func handleNotification(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, payment *Payment) {
	var head string
	head, r.URL.Path = httplib.ShiftPath(r.URL.Path)

	switch head {
	case "":
		switch r.Method {
		case http.MethodPost:
			if err := r.ParseForm(); err != nil {
				http.Error(w, err.Error(), http.StatusBadRequest)
				return
			}
			signed := redsys.SignedResponse{
				MerchantParameters: r.Form.Get("Ds_MerchantParameters"),
				Signature:          r.Form.Get("Ds_Signature"),
				SignatureVersion:   r.Form.Get("Ds_SignatureVersion"),
			}
			response, err := signed.Decode(r.Context(), conn, company)
			if err != nil {
				panic(err)
			}
			if response == nil {
				http.Error(w, "Invalid response", http.StatusBadRequest)
				return
			}
			if response.OrderNumber != payment.Reference {
				http.Error(w, "Response for a different payment", http.StatusBadRequest)
				return
			}
			status, err := response.Process(r.Context(), conn, payment.Slug)
			if err != nil {
				http.Error(w, err.Error(), http.StatusBadRequest)
				return
			}
			switch status {
			case StatusPreAuthenticated:
				if err := sendEmail(r, conn, payment, company, user.Locale); err != nil {
					log.Println("Could not send email:", err)
				}
				// TODO: use email to send marshalled payment
				if _, err := conn.Exec(r.Context(), "select unmarshal_booking(marshal_payment($1))", payment.ID); err != nil {
					log.Println("Could not marshal payment:", err)
				}
			default:
			}
			w.WriteHeader(http.StatusOK)
		default:
			httplib.MethodNotAllowed(w, r, http.MethodPost)
		}
	default:
		http.NotFound(w, r)
	}
}

type CompletedEmail struct {
	CurrentLocale     string
	CustomerFullName  string
	PaymentReference  string
	AccommodationName string
	ArrivalDate       string
	DepartureDate     string
	Total             string
	DownPayment       string
	CompanyAddress    *address
}

type NotificationEmail struct {
	CompanyName string
	BaseURL     string
	Details     *paymentDetails
}

type address struct {
	TradeName  string
	Address    string
	PostalCode string
	Province   string
	City       string
	Country    string
}

func sendEmail(r *http.Request, conn *database.Conn, payment *Payment, company *auth.Company, locale *locale.Locale) error {
	email := &CompletedEmail{
		CurrentLocale:    locale.Language.String(),
		PaymentReference: payment.Reference,
		Total:            template.FormatPrice(payment.Total, locale.Language, locale.CurrencyPattern, company.DecimalDigits, company.CurrencySymbol),
		DownPayment:      template.FormatPrice(payment.DownPayment, locale.Language, locale.CurrencyPattern, company.DecimalDigits, company.CurrencySymbol),
		CompanyAddress:   &address{},
	}

	var fromAddress string
	var toAddress string
	if err := conn.QueryRow(r.Context(), `
		select company.email::text
		     , customer.email::text
		     , customer.full_name
		     , coalesce(i18n.name, campsite_type.name)
		     , to_char(arrival_date, 'DD/MM/YYYY')
		     , to_char(departure_date, 'DD/MM/YYYY')
		     , company.trade_name
		     , company.address
		     , company.postal_code
		     , company.province
		     , company.city
		     , coalesce(country_i18n.name, country.name) as country_name
		from payment
			join payment_customer as customer using (payment_id)
			join campsite_type using (campsite_type_id)
		    left join campsite_type_i18n as i18n on i18n.campsite_type_id = campsite_type.campsite_type_id and i18n.lang_tag = $2
			join company on company.company_id = payment.company_id
			join country on country.country_code = company.country_code
			left join country_i18n on country.country_code = country_i18n.country_code and country_i18n.lang_tag = $2
		where payment_id = $1
		`, payment.ID, locale.Language).Scan(&fromAddress,
		&toAddress,
		&email.CustomerFullName,
		&email.AccommodationName,
		&email.ArrivalDate,
		&email.DepartureDate,
		&email.CompanyAddress.TradeName,
		&email.CompanyAddress.Address,
		&email.CompanyAddress.PostalCode,
		&email.CompanyAddress.Province,
		&email.CompanyAddress.City,
		&email.CompanyAddress.Country,
	); err != nil {
		return err
	}

	details, notificationErr := fetchPaymentDetails(r.Context(), conn, payment.Slug, company.DefaultLocale())
	if notificationErr == nil {
		schema := httplib.Protocol(r)
		authority := httplib.Host(r)
		notification := &NotificationEmail{
			CompanyName: email.CompanyAddress.TradeName,
			BaseURL:     fmt.Sprintf("%s://%s/admin/payments/%s", schema, authority, payment.Slug),
			Details:     details,
		}
		notificationErr = sendEmailTemplate(fromAddress, fromAddress, notification, company, "details.go", false, company.DefaultLocale())
	}
	if err := sendEmailTemplate(fromAddress, toAddress, email, company, "body.go", true, locale); err != nil {
		return err
	}
	return notificationErr
}

func sendEmailTemplate(fromAddress string, toAddress string, data interface{}, company *auth.Company, baseTemplate string, addAlternativeHTML bool, locale *locale.Locale) error {
	m := mail.NewMsg()
	if err := m.From(fromAddress); err != nil {
		return err
	}
	if err := m.ReplyTo(fromAddress); err != nil {
		return err
	}
	if err := m.To(toAddress); err != nil {
		return err
	}
	m.Subject(locale.Pgettext("Booking payment successfully received", "subject"))

	baseFilename := "web/templates/mail/payment/" + baseTemplate
	body, err := tt.New(baseTemplate + "txt").Funcs(tt.FuncMap{
		"gettext":  locale.Get,
		"pgettext": locale.GetC,
		"formatPrice": func(price string) string {
			return template.FormatPrice(price, locale.Language, locale.CurrencyPattern, company.DecimalDigits, company.CurrencySymbol)
		},
	}).ParseFiles(baseFilename + "txt")
	if err != nil {
		return err
	}
	if err := m.SetBodyTextTemplate(body, data); err != nil {
		return err
	}
	if addAlternativeHTML {
		alternative, err := ht.New(baseTemplate + "html").Funcs(tt.FuncMap{
			"gettext":  locale.Get,
			"pgettext": locale.GetC,
			"raw":      func(s string) ht.HTML { return ht.HTML(s) },
		}).ParseFiles(baseFilename + "html")
		if err != nil {
			return err
		}
		if err := m.AddAlternativeHTMLTemplate(alternative, data); err != nil {
			return err
		}
	}
	return m.WriteToSendmail()
}