package catalog

import (
	"errors"
	"net/http"
	"strconv"
	"strings"

	"github.com/TaruLab/keluargamart-backend/pkg/httpresponse"
	"github.com/TaruLab/keluargamart-backend/pkg/logger"
	"github.com/TaruLab/keluargamart-backend/pkg/middleware"
	"github.com/go-chi/chi/v5"
)

// Handler binds the catalog service to HTTP. Methods are exported so the
// router can attach RequirePermission per route. The auth package forms a
// cycle with catalog, so permission middleware is injected in main.
type Handler struct {
	svc *Service
}

func NewHandler(svc *Service) *Handler {
	return &Handler{svc: svc}
}

// --- categories

func (h *Handler) ListCategories(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	page, _ := strconv.Atoi(q.Get("page"))
	perPage, _ := strconv.Atoi(q.Get("per_page"))
	// Alias: ?q shorthand for ?search (frontend wiring mismatch).
	search := q.Get("search")
	if search == "" {
		search = q.Get("q")
	}
	f := CatalogFilter{
		Page:       page,
		PerPage:    perPage,
		Search:     search,
		OnlyActive: q.Get("active") == "1",
	}
	if f.Page < 1 {
		f.Page = 1
	}
	if f.PerPage < 1 || f.PerPage > 100 {
		f.PerPage = 20
	}
	list, total, err := h.svc.ListCategories(r.Context(), f)
	if err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	httpresponse.OKPaginated(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), list, f.Page, f.PerPage, total)
}

func (h *Handler) GetCategory(w http.ResponseWriter, r *http.Request) {
	c, err := h.svc.GetCategory(r.Context(), pathID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), c)
}

func (h *Handler) CreateCategory(w http.ResponseWriter, r *http.Request) {
	var in CategoryInput
	if !decodeJSON(w, r, &in) {
		return
	}
	c, err := h.svc.CreateCategory(r.Context(), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), c)
}

func (h *Handler) UpdateCategory(w http.ResponseWriter, r *http.Request) {
	id := pathID(r)
	var in CategoryInput
	if !decodeJSON(w, r, &in) {
		return
	}
	c, err := h.svc.UpdateCategory(r.Context(), id, in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), c)
}

func (h *Handler) DeleteCategory(w http.ResponseWriter, r *http.Request) {
	if err := h.svc.DeleteCategory(r.Context(), pathID(r)); err != nil {
		respondErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// --- units

func (h *Handler) ListUnits(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	page, _ := strconv.Atoi(q.Get("page"))
	perPage, _ := strconv.Atoi(q.Get("per_page"))
	// Alias: ?q shorthand for ?search (frontend wiring mismatch).
	search := q.Get("search")
	if search == "" {
		search = q.Get("q")
	}
	f := CatalogFilter{
		Page:    page,
		PerPage: perPage,
		Search:  search,
	}
	if f.Page < 1 {
		f.Page = 1
	}
	if f.PerPage < 1 || f.PerPage > 100 {
		f.PerPage = 20
	}
	list, total, err := h.svc.ListUnits(r.Context(), f)
	if err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	httpresponse.OKPaginated(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), list, f.Page, f.PerPage, total)
}

func (h *Handler) CreateUnit(w http.ResponseWriter, r *http.Request) {
	var in UnitInput
	if !decodeJSON(w, r, &in) {
		return
	}
	u, err := h.svc.CreateUnit(r.Context(), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), u)
}

func (h *Handler) UpdateUnit(w http.ResponseWriter, r *http.Request) {
	id := pathID(r)
	var in UnitInput
	if !decodeJSON(w, r, &in) {
		return
	}
	u, err := h.svc.UpdateUnit(r.Context(), id, in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), u)
}

func (h *Handler) DeleteUnit(w http.ResponseWriter, r *http.Request) {
	if err := h.svc.DeleteUnit(r.Context(), pathID(r)); err != nil {
		respondErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// --- products

func (h *Handler) ListProducts(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	page, _ := strconv.Atoi(q.Get("page"))
	perPage, _ := strconv.Atoi(q.Get("per_page"))
	// Alias: ?q shorthand for ?search (frontend wiring mismatch).
	search := q.Get("search")
	if search == "" {
		search = q.Get("q")
	}
	f := ProductFilter{
		Page:    page,
		PerPage: perPage,
		Search:  search,
	}
	if catID, err := strconv.ParseInt(q.Get("category_id"), 10, 64); err == nil && catID > 0 {
		f.CategoryID = &catID
	}
	if brandID, err := strconv.ParseInt(q.Get("brand_id"), 10, 64); err == nil && brandID > 0 {
		f.BrandID = &brandID
	}
	if s := q.Get("status"); s == "active" || s == "inactive" {
		act := s == "active"
		f.IsActive = &act
	}
	// ERP table sort: the data table sends the column and direction it wants.
	// Without this the headers accepted clicks and returned the same order.
	f.SortBy = strings.TrimSpace(q.Get("sort_by"))
	f.SortDir = strings.TrimSpace(q.Get("sort_dir"))

	// Server-side availability + sort for the storefront catalog.
	if q.Get("in_stock") == "1" || q.Get("in_stock") == "true" {
		act := true
		f.InStock = &act
	}
	f.Sort = q.Get("sort")
	list, total, err := h.svc.ListProducts(r.Context(), f)
	if err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	httpresponse.OKPaginated(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), list, f.Page, f.PerPage, total)
}

func (h *Handler) GetProduct(w http.ResponseWriter, r *http.Request) {
	p, err := h.svc.GetProduct(r.Context(), pathID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), p)
}

func (h *Handler) CreateProduct(w http.ResponseWriter, r *http.Request) {
	var in ProductInput
	if !decodeJSON(w, r, &in) {
		return
	}
	p, err := h.svc.CreateProduct(r.Context(), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), p)
}

func (h *Handler) UpdateProduct(w http.ResponseWriter, r *http.Request) {
	var in ProductInput
	if !decodeJSON(w, r, &in) {
		return
	}
	p, err := h.svc.UpdateProduct(r.Context(), pathID(r), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), p)
}

func (h *Handler) DeleteProduct(w http.ResponseWriter, r *http.Request) {
	if err := h.svc.DeleteProduct(r.Context(), pathID(r)); err != nil {
		respondErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// --- brands

func (h *Handler) ListBrands(w http.ResponseWriter, r *http.Request) {
	list, err := h.svc.ListBrands(r.Context(), r.URL.Query().Get("active") == "1")
	if err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), list)
}

func (h *Handler) GetBrand(w http.ResponseWriter, r *http.Request) {
	b, err := h.svc.GetBrand(r.Context(), pathID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), b)
}

func (h *Handler) CreateBrand(w http.ResponseWriter, r *http.Request) {
	var in BrandInput
	if !decodeJSON(w, r, &in) {
		return
	}
	b, err := h.svc.CreateBrand(r.Context(), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), b)
}

func (h *Handler) UpdateBrand(w http.ResponseWriter, r *http.Request) {
	var in BrandInput
	if !decodeJSON(w, r, &in) {
		return
	}
	b, err := h.svc.UpdateBrand(r.Context(), pathID(r), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), b)
}

func (h *Handler) DeleteBrand(w http.ResponseWriter, r *http.Request) {
	if err := h.svc.DeleteBrand(r.Context(), pathID(r)); err != nil {
		respondErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// --- configured product units

func (h *Handler) ListProductUnits(w http.ResponseWriter, r *http.Request) {
	list, err := h.svc.ListProductUnits(r.Context(), pathParamID(r, "id"), r.URL.Query().Get("active") == "1")
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), list)
}

func (h *Handler) CreateProductUnit(w http.ResponseWriter, r *http.Request) {
	var in ProductUnitInput
	if !decodeJSON(w, r, &in) {
		return
	}
	pu, err := h.svc.CreateProductUnit(r.Context(), pathParamID(r, "id"), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), pu)
}

func (h *Handler) UpdateProductUnit(w http.ResponseWriter, r *http.Request) {
	var in ProductUnitInput
	if !decodeJSON(w, r, &in) {
		return
	}
	pu, err := h.svc.UpdateProductUnit(r.Context(), pathParamID(r, "id"), pathParamID(r, "unitConfigID"), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), pu)
}

func (h *Handler) DeleteProductUnit(w http.ResponseWriter, r *http.Request) {
	if err := h.svc.DeleteProductUnit(r.Context(), pathParamID(r, "id"), pathParamID(r, "unitConfigID")); err != nil {
		respondErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// --- product barcodes

func (h *Handler) ListProductBarcodes(w http.ResponseWriter, r *http.Request) {
	list, err := h.svc.ListProductBarcodes(r.Context(), pathParamID(r, "id"))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), list)
}

func (h *Handler) CreateProductBarcode(w http.ResponseWriter, r *http.Request) {
	var in ProductBarcodeInput
	if !decodeJSON(w, r, &in) {
		return
	}
	b, err := h.svc.CreateProductBarcode(r.Context(), pathParamID(r, "id"), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), b)
}

func (h *Handler) DeleteProductBarcode(w http.ResponseWriter, r *http.Request) {
	if err := h.svc.DeleteProductBarcode(r.Context(), pathParamID(r, "id"), pathParamID(r, "barcodeID")); err != nil {
		respondErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) LookupBarcode(w http.ResponseWriter, r *http.Request) {
	v, err := h.svc.LookupBarcode(r.Context(), r.URL.Query().Get("barcode"))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), v)
}

// --- helpers

func pathID(r *http.Request) int64 {
	id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
	return id
}

func pathParamID(r *http.Request, name string) int64 {
	id, _ := strconv.ParseInt(chi.URLParam(r, name), 10, 64)
	return id
}

// decodeJSON answers 400 itself when the body would not decode and returns
// false, so a caller that gets false has already written the response.
func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) bool {
	return httpresponse.DecodeJSON(w, r, middleware.RequestIDFrom(r.Context()), dst)
}

func respondErr(w http.ResponseWriter, r *http.Request, err error) {
	rid := middleware.RequestIDFrom(r.Context())
	switch {
	case errors.Is(err, ErrNotFound):
		httpresponse.Error(w, http.StatusNotFound, rid, httpresponse.CodeNotFound, "Not found", nil)
	case errors.Is(err, ErrDuplicateName), errors.Is(err, ErrDuplicateCode),
		errors.Is(err, ErrDuplicateUnitConfig), errors.Is(err, ErrDuplicateBarcode):
		httpresponse.Error(w, http.StatusConflict, rid, httpresponse.CodeConflict, "Already exists", nil)
	case errors.Is(err, ErrInUse):
		httpresponse.Error(w, http.StatusConflict, rid, httpresponse.CodeConflict, "Resource is in use", nil)
	case errors.Is(err, ErrCategoryRequired), errors.Is(err, ErrUnitRequired),
		errors.Is(err, ErrBrandRequired), errors.Is(err, ErrInvalidConversion),
		errors.Is(err, ErrInvalidMinPurchase), errors.Is(err, ErrInvalidUnitPrice),
		errors.Is(err, ErrBaseUnitConversion), errors.Is(err, ErrUnitProductMismatch):
		httpresponse.Error(w, http.StatusUnprocessableEntity, rid, httpresponse.CodeBusinessViolation, err.Error(), nil)
	case err != nil:
		httpresponse.SafeError(w, rid, logger.FromContext(r.Context()), http.StatusBadRequest, httpresponse.CodeValidation, err)
	}
}
