package server

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"strconv"
	"time"

	"github.com/a-h/templ"
	"github.com/go-chi/chi/v5"
	u "github.com/rjNemo/underscore"

	"github.com/rjNemo/rentease/internal/config"
	"github.com/rjNemo/rentease/internal/constant"
	"github.com/rjNemo/rentease/internal/service/booking"
	"github.com/rjNemo/rentease/internal/view"
	myTime "github.com/rjNemo/rentease/pkg/time"
)

func handleBookingListPage(bs *booking.Service, hc *config.Host) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		search := r.URL.Query().Get("search")

		var bookings []*booking.Line
		if search != "" {
			bookings = bs.Search(search)
		} else {
			bookings = bs.All()
		}

		slog.Info("serving bookings", slog.Int("bookings_length", len(bookings)))

		bvm := u.Map(bookings, func(b *booking.Line) *view.ListBookingsViewModel {
			return &view.ListBookingsViewModel{
				Id:       b.InvoiceNumber(hc),
				Url:      templ.SafeURL(fmt.Sprintf("%s/%d", constant.RouteBooking, b.ID)),
				From:     b.From.Format(time.DateOnly),
				To:       b.To.Format(time.DateOnly),
				Platform: b.Platform,
				Name:     b.CustomerName,
				Total:    strconv.FormatFloat(b.Total, 'f', 2, 64),
				Canceled: b.Canceled,
			}
		})

		var err error
		switch {
		case hxRequest(r) && !hxBoosted(r):
			err = renderTempl(w, http.StatusOK, view.BookingLines(bvm))
		default:
			err = renderTempl(w, http.StatusOK, view.ListBookings(bvm))
		}

		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	}
}

func paymentViewModelFromBookingPayment(p booking.Payment) *view.PaymentViewModel {
	stripeStatus := ""
	if p.StripeStatus != nil {
		stripeStatus = *p.StripeStatus
	}

	return &view.PaymentViewModel{
		Amount:        strconv.FormatFloat(p.Amount, 'f', 2, 64),
		PaymentMethod: string(p.PaymentMethod),
		PaymentUrl:    fmt.Sprintf("%s/%d", constant.RoutePayment, p.ID),
		StripeStatus:  stripeStatus,
	}
}

func handleBookingList(bs *booking.Service) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		search := r.URL.Query().Get("search")

		var bookings []*booking.Line
		if search != "" {
			bookings = bs.Search(search)
		} else {
			bookings = bs.All()
		}

		w.Header().Set("Content-Type", "application/json")
		if err := json.NewEncoder(w).Encode(bookings); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	}
}

func handleBookingCreatePage(hc *config.Host) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if err := renderTempl(w, http.StatusOK, view.NewBooking(u.Map(hc.Platforms, func(p config.Platform) string {
			return string(p)
		}))); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	}
}

func handleBookingCreate(bs *booking.Service) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if err := r.ParseForm(); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		customerNumber, err := strconv.Atoi(r.FormValue("customer_number"))
		if err != nil {
			http.Error(w, "invalid customer number", http.StatusBadRequest)
			return
		}

		platformFees := 0.0
		if v := r.FormValue("platform_fees"); v != "" {
			platformFees, err = strconv.ParseFloat(v, 64)
			if err != nil {
				http.Error(w, "invalid platform fees", http.StatusBadRequest)
				return
			}
		}

		externalID := r.FormValue("external_id")
		var externalPtr *string
		if externalID != "" {
			externalPtr = &externalID
		}

		from, _ := myTime.ParseFromForm(r.FormValue("from"))
		to, _ := myTime.ParseFromForm(r.FormValue("to"))

		b := bs.Create(
			from,
			to,
			r.FormValue("name"),
			r.FormValue("phone_number"),
			r.FormValue("email"),
			r.FormValue("platform"),
			customerNumber,
			platformFees,
			externalPtr,
		)

		http.Redirect(w, r, fmt.Sprintf("%s/%d", constant.RouteBooking, b.ID), http.StatusSeeOther)
	}
}

func handleBookingPage(bs *booking.Service, hc *config.Host) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		idStr := chi.URLParam(r, "id")
		id, err := strconv.Atoi(idStr)
		if err != nil {
			http.Error(w, "invalid booking id", http.StatusBadRequest)
			return
		}

		b := bs.One(id)

		var eid string
		if b.ExternalID == nil {
			eid = ""
		} else {
			eid = *b.ExternalID
		}
		bvm := &view.BookingViewModel{
			Id:                   b.InvoiceNumber(hc),
			Name:                 b.Name,
			PhoneNumber:          b.PhoneNumber,
			CustomerNumber:       strconv.Itoa(b.CustomerNumber),
			Email:                b.Email,
			From:                 b.From.Format(time.DateOnly),
			To:                   b.To.Format(time.DateOnly),
			Platform:             string(b.Platform),
			ExternalId:           eid,
			Canceled:             b.Canceled,
			PlatformFees:         strconv.FormatFloat(b.PlatformFees, 'f', 2, 64),
			Url:                  templ.EscapeString(fmt.Sprintf("%s/%d", constant.RouteBooking, b.ID)),
			PdfUrl:               templ.SafeURL(fmt.Sprintf("%s/pdf/%d", constant.RouteBooking, b.ID)),
			CancelUrl:            fmt.Sprintf("%s/%d/cancel", constant.RouteBooking, b.ID),
			PaymentUrl:           fmt.Sprintf("%s/%d", constant.RoutePayment, b.ID),
			StripePaymentLinkUrl: fmt.Sprintf("%s/%d/stripe/payment-link", constant.RouteBooking, b.ID),
			Items: view.ItemListViewModel{
				Items: u.Map(b.Items, func(i booking.Item) view.ItemViewModel {
					return view.ItemViewModel{
						Id:            strconv.Itoa(i.ID),
						Item:          i.Item,
						Quantity:      strconv.Itoa(i.Quantity),
						Price:         strconv.FormatFloat(i.Price, 'f', 2, 64),
						SubTotal:      strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64),
						PaymentStatus: i.PaymentStatus,
						ItemUrl:       fmt.Sprintf("%s/%d", constant.RouteItem, i.ID),
					}
				}),
				Payments: u.Map(b.Payments, func(p booking.Payment) view.PaymentViewModel {
					return *paymentViewModelFromBookingPayment(p)
				}),
			},
			Total: strconv.FormatFloat(u.Reduce(b.Items, func(i booking.Item, sum float64) float64 {
				return sum + i.Price*float64(i.Quantity)
			}, 0.0), 'f', 2, 64),
			Platforms: u.Map(hc.Platforms, func(p config.Platform) string { return string(p) }),
			ItemList: u.OrderBy(func(items map[string]config.HostItem) (out []string) { // TODO: return the full item to prefill the form
				for _, item := range items {
					out = append(out, item.Name)
				}
				return out
			}(hc.Items),
				func(l, r string) bool { return l > r },
			),
			PaymentMethods: hc.PaymentMethods,
		}

		if err := renderTempl(w, http.StatusOK, view.BookingById(bvm)); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	}
}

func handleBookingStripePaymentLink(bs *booking.Service) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		idStr := chi.URLParam(r, "id")
		id, err := strconv.Atoi(idStr)
		if err != nil {
			http.Error(w, "invalid booking id", http.StatusBadRequest)
			return
		}

		url, err := bs.CreateStripePaymentLink(r.Context(), id)
		if err != nil {
			switch {
			case errors.Is(err, booking.ErrStripeClientNotConfigured):
				http.Error(w, "stripe is not configured", http.StatusBadRequest)
			case errors.Is(err, booking.ErrBookingNotFound):
				http.Error(w, "booking not found", http.StatusNotFound)
			case errors.Is(err, booking.ErrNoOutstandingBalance):
				http.Error(w, "booking has no outstanding balance", http.StatusBadRequest)
			default:
				http.Error(w, fmt.Sprintf("failed to create payment link: %v", err), http.StatusInternalServerError)
			}
			return
		}

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusCreated)
		if err := json.NewEncoder(w).Encode(map[string]string{"url": url}); err != nil {
			slog.Error("failed to write stripe payment link response", slog.Any("error", err))
		}
	}
}

func handleBookingUpdate(bs *booking.Service, hc *config.Host) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if err := r.ParseForm(); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		id, err := strconv.Atoi(chi.URLParam(r, "id"))
		if err != nil {
			http.Error(w, "invalid booking id", http.StatusBadRequest)
			return
		}

		customerNumber, err := strconv.Atoi(r.FormValue("customer_number"))
		if err != nil {
			http.Error(w, "invalid customer number", http.StatusBadRequest)
			return
		}

		platformFees := 0.0
		if v := r.FormValue("platform_fees"); v != "" {
			platformFees, err = strconv.ParseFloat(v, 64)
			if err != nil {
				http.Error(w, "invalid platform fees", http.StatusBadRequest)
				return
			}
		}

		externalID := r.FormValue("external_id")
		var externalPtr *string
		if externalID != "" {
			externalPtr = &externalID
		}

		from, _ := myTime.ParseFromForm(r.FormValue("from"))
		to, _ := myTime.ParseFromForm(r.FormValue("to"))

		b := bs.Update(
			id,
			from,
			to,
			r.FormValue("name"),
			r.FormValue("phone_number"),
			r.FormValue("email"),
			r.FormValue("platform"),
			customerNumber,
			platformFees,
			externalPtr,
		)

		externalValue := ""
		if b.ExternalID != nil {
			externalValue = *b.ExternalID
		}

		form := view.BookingForm(view.BookingViewModel{
			Id:             b.InvoiceNumber(hc),
			Name:           b.Name,
			PhoneNumber:    b.PhoneNumber,
			CustomerNumber: strconv.Itoa(b.CustomerNumber),
			Email:          b.Email,
			From:           b.From.Format(time.DateOnly),
			To:             b.To.Format(time.DateOnly),
			Canceled:       b.Canceled,
			Platform:       string(b.Platform),
			ExternalId:     externalValue,
			Platforms:      u.Map(hc.Platforms, func(p config.Platform) string { return string(p) }),
			PlatformFees:   strconv.FormatFloat(b.PlatformFees, 'f', 2, 64),
			PaymentMethods: hc.PaymentMethods,
			Url:            templ.EscapeString(fmt.Sprintf("%s/%d", constant.RouteBooking, b.ID)),
			PdfUrl:         templ.SafeURL(fmt.Sprintf("%s/pdf/%d", constant.RouteBooking, b.ID)),
		})

		if err := renderTempl(w, http.StatusOK, form); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	}
}

func handleLineItemForm(bs *booking.Service) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		idStr := chi.URLParam(r, "id")
		id, err := strconv.Atoi(idStr)
		if err != nil {
			http.Error(w, "invalid item id", http.StatusBadRequest)
			return
		}

		i := bs.OneItem(id)
		form := view.LineItemForm(&view.ItemViewModel{
			Id:            strconv.Itoa(i.ID),
			Item:          i.Item,
			Quantity:      strconv.Itoa(i.Quantity),
			Price:         strconv.FormatFloat(i.Price, 'f', 2, 64),
			PaymentStatus: i.PaymentStatus,
			SubTotal:      strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64),
			ItemUrl:       fmt.Sprintf("%s/%d", constant.RouteItem, i.ID),
		})

		if err := renderTempl(w, http.StatusOK, form); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	}
}

func handleCreateItem(bs *booking.Service, hc *config.Host) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		bookingIdStr := chi.URLParam(r, "id")
		bid, err := strconv.Atoi(bookingIdStr)
		if err != nil {
			http.Error(w, "invalid booking id", http.StatusBadRequest)
			return
		}

		if err := r.ParseForm(); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		b := bs.One(bid)

		itemName := r.FormValue("item")
		itm, ok := hc.Items[itemName]
		if !ok {
			http.Error(w, fmt.Sprintf("invalid item name %q", itemName), http.StatusBadRequest)
			return
		}

		quantity, err := strconv.Atoi(r.FormValue("quantity"))
		if err != nil {
			http.Error(w, "invalid quantity", http.StatusBadRequest)
			return
		}

		price := 0.0
		if v := r.FormValue("price"); v != "" {
			price, err = strconv.ParseFloat(v, 64)
			if err != nil {
				http.Error(w, "invalid price", http.StatusBadRequest)
				return
			}
		}

		newItems := bs.CreateItem(b.ID, itm, quantity, price, r.FormValue("method"), b.CustomerNumber, string(b.Platform))

		var buf bytes.Buffer
		for _, i := range newItems {
			component := view.LineItem(&view.ItemViewModel{
				Id:            strconv.Itoa(i.ID),
				Item:          i.Item,
				Quantity:      strconv.Itoa(i.Quantity),
				Price:         strconv.FormatFloat(i.Price, 'f', 2, 64),
				PaymentStatus: i.PaymentStatus,
				SubTotal:      strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64),
				ItemUrl:       fmt.Sprintf("%s/%d", constant.RouteItem, i.ID),
			})
			if err := component.Render(context.Background(), &buf); err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
		}

		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		w.WriteHeader(http.StatusCreated)
		if _, err := buf.WriteTo(w); err != nil {
			slog.Error("failed to write item response", slog.Any("error", err))
		}
	}
}

func handleItemPay(bs *booking.Service) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		itemIdStr := chi.URLParam(r, "id")
		itemId, err := strconv.Atoi(itemIdStr)
		if err != nil {
			http.Error(w, "invalid item id", http.StatusBadRequest)
			return
		}

		i := bs.PayItem(itemId)
		if err := renderTempl(w, http.StatusOK, view.LineItem(&view.ItemViewModel{
			Id:            itemIdStr,
			Item:          i.Item,
			Quantity:      strconv.Itoa(i.Quantity),
			Price:         strconv.FormatFloat(i.Price, 'f', 2, 64),
			PaymentStatus: i.PaymentStatus,
			SubTotal:      strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64),
			ItemUrl:       fmt.Sprintf("%s/%d", constant.RouteItem, i.ID),
		})); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	}
}

func handleItemUpdate(bs *booking.Service) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if err := r.ParseForm(); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		id, err := strconv.Atoi(chi.URLParam(r, "id"))
		if err != nil {
			http.Error(w, "invalid item id", http.StatusBadRequest)
			return
		}

		quantity, err := strconv.Atoi(r.FormValue("quantity"))
		if err != nil {
			http.Error(w, "invalid quantity", http.StatusBadRequest)
			return
		}

		price := 0.0
		if v := r.FormValue("price"); v != "" {
			price, err = strconv.ParseFloat(v, 64)
			if err != nil {
				http.Error(w, "invalid price", http.StatusBadRequest)
				return
			}
		}

		i := bs.UpdateItem(id, r.FormValue("item"), quantity, price, r.FormValue("paymentMethod"), r.FormValue("paymentStatus"))

		if err := renderTempl(w, http.StatusCreated, view.LineItem(&view.ItemViewModel{
			Id:            strconv.Itoa(id),
			Item:          i.Item,
			Quantity:      strconv.Itoa(i.Quantity),
			Price:         strconv.FormatFloat(i.Price, 'f', 2, 64),
			PaymentStatus: i.PaymentStatus,
			SubTotal:      strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64),
			ItemUrl:       fmt.Sprintf("%s/%d", constant.RouteItem, i.ID),
		})); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	}
}

func handlePaymentUpdate(bs *booking.Service) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if err := r.ParseForm(); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		id, err := strconv.Atoi(chi.URLParam(r, "id"))
		if err != nil {
			http.Error(w, "invalid payment id", http.StatusBadRequest)
			return
		}

		amount := 0.0
		if v := r.FormValue("amount"); v != "" {
			amount, err = strconv.ParseFloat(v, 64)
			if err != nil {
				http.Error(w, "invalid amount", http.StatusBadRequest)
				return
			}
		}

		p := bs.UpdatePayment(id, amount, r.FormValue("paymentMethod"))

		if err := renderTempl(w, http.StatusOK, view.PaymentLine(paymentViewModelFromBookingPayment(*p))); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	}
}

func handleBookingCancel(bs *booking.Service) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		idStr := chi.URLParam(r, "id")
		id, err := strconv.Atoi(idStr)
		if err != nil {
			http.Error(w, "invalid booking id", http.StatusBadRequest)
			return
		}

		bs.Cancel(id)

		component := templ.ComponentFunc(func(ctx context.Context, writer io.Writer) error {
			_, err := io.WriteString(writer, " <span>Canceled</span>")
			return err
		})

		if err := renderTempl(w, http.StatusOK, component); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	}
}
