package inventory

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

	"github.com/TaruLab/keluargamart-backend/internal/auth"
	"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"
	"github.com/shopspring/decimal"
)

// Handler binds the inventory service to HTTP. Methods are exported so the
// router can attach RequirePermission per route in main.
type Handler struct {
	svc *Service
}

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

func (h *Handler) ListStock(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	sortBy := q.Get("sort_by")
	sortDir := q.Get("sort_dir")
	if sortBy == "" && q.Get("sort") != "" {
		parts := strings.Split(q.Get("sort"), ":")
		sortBy = parts[0]
		if len(parts) > 1 {
			sortDir = parts[1]
		}
	}
	f := StockFilter{
		Page:      queryInt(q.Get("page")),
		PerPage:   queryInt(q.Get("per_page")),
		Search:    q.Get("search"),
		Warehouse: parseInt64(q.Get("warehouse_id")),
		LowStock:  q.Get("low_stock") == "1",
		SortBy:    sortBy,
		SortDir:   sortDir,
	}
	list, total, err := h.svc.ListStock(r.Context(), f)
	if err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	summary, err := h.svc.StockSummary(r.Context(), f)
	if err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	httpresponse.OKPaginatedWithMeta(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), list, pageOrDefault(f.Page), perPageOrDefault(f.PerPage), total, map[string]any{
		"summary": summary,
	})
}

func (h *Handler) ListMovements(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	search := q.Get("search")
	if search == "" {
		search = q.Get("q")
	}
	f := MovementFilter{
		Page:        queryInt(q.Get("page")),
		PerPage:     queryInt(q.Get("per_page")),
		ProductID:   parseInt64(q.Get("product_id")),
		WarehouseID: parseInt64(q.Get("warehouse_id")),
		Type:        q.Get("type"),
		DateFrom:    q.Get("date_from"),
		DateTo:      q.Get("date_to"),
		Search:      search,
	}
	list, total, err := h.svc.ListMovements(r.Context(), f)
	if err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	summary, err := h.svc.MovementSummary(r.Context(), f)
	if err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	httpresponse.OKPaginatedWithMeta(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), list, pageOrDefault(f.Page), perPageOrDefault(f.PerPage), total, map[string]any{"summary": summary})
}

func (h *Handler) StockIn(w http.ResponseWriter, r *http.Request) {
	var in StockInInput
	if !decode(w, r, &in) {
		return
	}
	m, err := h.svc.StockIn(WithMovementRequest(r.Context(), r.Header.Get("Idempotency-Key"), "stock_in", in), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), m)
}

func (h *Handler) StockOut(w http.ResponseWriter, r *http.Request) {
	var in StockOutInput
	if !decode(w, r, &in) {
		return
	}
	m, err := h.svc.StockOut(WithMovementRequest(r.Context(), r.Header.Get("Idempotency-Key"), "stock_out", in), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), m)
}

func (h *Handler) CreateOpname(w http.ResponseWriter, r *http.Request) {
	var in OpnameInput
	if !decode(w, r, &in) {
		return
	}
	o, err := h.svc.OpnameCreate(r.Context(), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), o)
}

func (h *Handler) ListOpnames(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	f := OpnameFilter{Page: queryInt(q.Get("page")), PerPage: queryInt(q.Get("per_page")), Status: q.Get("status"), WarehouseID: parseInt64(q.Get("warehouse_id"))}
	list, total, err := h.svc.ListOpnames(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, pageOrDefault(f.Page), perPageOrDefault(f.PerPage), total)
}

func (h *Handler) GetOpname(w http.ResponseWriter, r *http.Request) {
	o, err := h.svc.OpnameByID(r.Context(), pathID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	items, err := h.svc.OpnameItems(r.Context(), o.ID)
	if err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	o.Items = items
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), o)
}

func (h *Handler) UpdateOpnamePhysical(w http.ResponseWriter, r *http.Request) {
	var in struct {
		Items []OpnameItemInput `json:"items"`
	}
	if !decode(w, r, &in) {
		return
	}
	if err := h.svc.OpnameSetPhysical(r.Context(), pathID(r), in.Items); err != nil {
		respondErr(w, r, err)
		return
	}
	o, err := h.svc.OpnameByID(r.Context(), pathID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), o)
}

func (h *Handler) FinalizeOpname(w http.ResponseWriter, r *http.Request) {
	id := pathID(r)
	// Optional spec body: {items:[{stock_opname_item_id, product_id, physical_quantity, notes}]}.
	if r.ContentLength != 0 {
		var in struct {
			Items []struct {
				StockOpnameItemID int64            `json:"stock_opname_item_id"`
				ProductID         int64            `json:"product_id"`
				PhysicalQuantity  *decimal.Decimal `json:"physical_quantity"`
				Notes             *string          `json:"notes"`
			} `json:"items"`
		}
		if !decode(w, r, &in) {
			return
		}
		if len(in.Items) > 0 {
			items, err := h.svc.OpnameItems(r.Context(), id)
			if err != nil {
				respondErr(w, r, err)
				return
			}
			byID := make(map[int64]int64, len(items))
			byProductID := make(map[int64]int64, len(items))
			for _, it := range items {
				byID[it.ID] = it.ProductID
				byProductID[it.ProductID] = it.ProductID
			}
			inputs := make([]OpnameItemInput, 0, len(in.Items))
			for _, fi := range in.Items {
				pid, ok := byID[fi.StockOpnameItemID]
				if !ok && fi.ProductID > 0 {
					pid, ok = byProductID[fi.ProductID]
				}
				if !ok {
					httpresponse.Error(w, http.StatusNotFound, middleware.RequestIDFrom(r.Context()),
						httpresponse.CodeNotFound, "Not found", nil)
					return
				}
				inputs = append(inputs, OpnameItemInput{ProductID: pid, PhysicalQty: fi.PhysicalQuantity, Notes: fi.Notes})
			}
			if err := h.svc.OpnameSetPhysical(r.Context(), id, inputs); err != nil {
				respondErr(w, r, err)
				return
			}
		}
	}
	if _, err := h.svc.OpnameFinalize(r.Context(), id); err != nil {
		respondErr(w, r, err)
		return
	}
	o, err := h.svc.OpnameByID(r.Context(), id)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	if o.Items, err = h.svc.OpnameItems(r.Context(), id); err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), o)
}

func (h *Handler) CreateTransfer(w http.ResponseWriter, r *http.Request) {
	var in TransferInput
	if !decode(w, r, &in) {
		return
	}
	t, err := h.svc.TransferCreate(r.Context(), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), t)
}

func (h *Handler) ListTransfers(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	f := TransferFilter{
		Page:    queryInt(q.Get("page")),
		PerPage: queryInt(q.Get("per_page")),
		Status:  q.Get("status"),
		SortBy:  q.Get("sort_by"),
		SortDir: q.Get("sort_dir"),
	}
	list, total, err := h.svc.ListTransfers(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, pageOrDefault(f.Page), perPageOrDefault(f.PerPage), total)
}

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

func (h *Handler) ApproveTransfer(w http.ResponseWriter, r *http.Request) {
	id := pathID(r)
	if id <= 0 {
		httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
			httpresponse.CodeValidation, "invalid transfer id", nil)
		return
	}
	var opID int64
	if u := auth.UserFrom(r.Context()); u != nil {
		opID = u.ID
	}
	t, err := h.svc.TransferApprove(r.Context(), id, opID)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), t)
}

// --- helpers

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

func decode(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, ErrInsufficient):
		httpresponse.Error(w, http.StatusConflict, rid, httpresponse.CodeBusinessViolation, "Stock tidak mencukupi", nil)
	case errors.Is(err, ErrStockFrozen):
		// 409 with a sentence the operator can act on: an opname is open and
		// its baseline must not move underneath the counting team.
		httpresponse.Error(w, http.StatusConflict, rid, httpresponse.CodeBusinessViolation, err.Error(), nil)
	case errors.Is(err, ErrOnlyDestinationWarehouse):
		httpresponse.Error(w, http.StatusForbidden, rid, httpresponse.CodeForbidden, "Hanya gudang tujuan yang berhak menyelesaikan penerimaan barang", nil)
	case errors.Is(err, ErrForbiddenWarehouse):
		httpresponse.Error(w, http.StatusForbidden, rid, httpresponse.CodeForbidden, "Gudang di luar outlet Anda", nil)
	case errors.Is(err, ErrUnsupportedState):
		httpresponse.Error(w, http.StatusConflict, rid, httpresponse.CodeBusinessViolation, err.Error(), nil)
	case errors.Is(err, ErrAlreadyFinalized):
		httpresponse.Error(w, http.StatusConflict, rid, httpresponse.CodeBusinessViolation, "Opname sudah selesai", nil)
	case err != nil:
		httpresponse.SafeError(w, rid, logger.FromContext(r.Context()), http.StatusBadRequest, httpresponse.CodeValidation, err)
	}
}

func queryInt(s string) int {
	n, _ := strconv.Atoi(s)
	return n
}

func parseInt64(s string) int64 {
	n, _ := strconv.ParseInt(s, 10, 64)
	return n
}

func pageOrDefault(n int) int {
	if n < 1 {
		return 1
	}
	return n
}

func perPageOrDefault(n int) int {
	if n < 1 {
		return 20
	}
	if n > 1000 {
		return 1000
	}
	return n
}
