package lhpp

import (
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"strconv"
	"strings"

	"github.com/go-chi/chi/v5"

	"github.com/TaruLab/keluargamart-backend/internal/auth"
	"github.com/TaruLab/keluargamart-backend/internal/finance"
	"github.com/TaruLab/keluargamart-backend/pkg/httpresponse"
	"github.com/TaruLab/keluargamart-backend/pkg/logger"
	"github.com/TaruLab/keluargamart-backend/pkg/middleware"
)

// Handler binds the LHPP 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) List(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	f := ListFilter{
		Page:    queryInt(q.Get("page")),
		PerPage: queryInt(q.Get("per_page")),
		Search:  q.Get("search"),
		Status:  q.Get("status"),
	}
	rows, total, err := h.svc.List(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()), rows,
		normOr(f.Page, 1), perPageOrDefault(f.PerPage), total)
}

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

func (h *Handler) RecordPayment(w http.ResponseWriter, r *http.Request) {
	var in PaymentInput
	defer r.Body.Close()
	if !httpresponse.DecodeJSON(w, r, middleware.RequestIDFrom(r.Context()), &in) {
		return
	}
	group, err := h.svc.RecordPayment(r.Context(), pathID(r), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), group)
}

// --- group maintenance --------------------------------------------------------

// Merge folds several sheets into one (POST /lhpp/groups/merge).
func (h *Handler) Merge(w http.ResponseWriter, r *http.Request) {
	var in MergeInput
	defer r.Body.Close()
	if !httpresponse.DecodeJSON(w, r, middleware.RequestIDFrom(r.Context()), &in) {
		return
	}
	group, err := h.svc.Merge(r.Context(), in, actorID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), group)
}

// Split pulls DOs out of a sheet into a new one (POST /lhpp/groups/{id}/split).
func (h *Handler) Split(w http.ResponseWriter, r *http.Request) {
	var in SplitInput
	defer r.Body.Close()
	if !httpresponse.DecodeJSON(w, r, middleware.RequestIDFrom(r.Context()), &in) {
		return
	}
	group, err := h.svc.Split(r.Context(), pathID(r), in, actorID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), group)
}

// Void retires a sheet and releases its DOs (POST /lhpp/groups/{id}/void).
func (h *Handler) Void(w http.ResponseWriter, r *http.Request) {
	var in VoidInput
	defer r.Body.Close()
	// An empty body is a legitimate "void it" — the reason is optional.
	if err := json.NewDecoder(r.Body).Decode(&in); err != nil && !errors.Is(err, io.EOF) {
		// An empty body is tolerated above, so this path is reached with a
		// body that is present but unreadable: SafeMessage keeps the Go type the
		// decoder names off the wire.
		httpresponse.Error(w, http.StatusBadRequest, middleware.RequestIDFrom(r.Context()),
			httpresponse.CodeValidation, httpresponse.SafeMessage(err), nil)
		return
	}
	if err := h.svc.Void(r.Context(), pathID(r), in, actorID(r)); err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), map[string]any{"voided": true})
}

// UpdateGroup re-points a single-DO sheet at another driver
// (PATCH /lhpp/groups/{id}). The number never changes.
func (h *Handler) UpdateGroup(w http.ResponseWriter, r *http.Request) {
	var in UpdateGroupInput
	defer r.Body.Close()
	if !httpresponse.DecodeJSON(w, r, middleware.RequestIDFrom(r.Context()), &in) {
		return
	}
	group, err := h.svc.ChangeDriver(r.Context(), pathID(r), in, actorID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), group)
}

// Assign hands the sheet to a collector — POST /lhpp/groups/{id}/assign,
// body {assignee_id?, reason?}. nil assignee takes the sheet back. Custody
// only: money still posts through finance.PayInvoice.
func (h *Handler) Assign(w http.ResponseWriter, r *http.Request) {
	var in AssignInput
	defer r.Body.Close()
	if !httpresponse.DecodeJSON(w, r, middleware.RequestIDFrom(r.Context()), &in) {
		return
	}
	group, err := h.svc.Assign(r.Context(), pathID(r), in, actorID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), group)
}

// History serves the audit trail of one group (GET /lhpp/groups/{id}/history).
func (h *Handler) History(w http.ResponseWriter, r *http.Request) {
	rows, err := h.svc.History(r.Context(), pathID(r))
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), rows)
}

// actorID is the authenticated user id, or 0 for an unauthenticated internal
// call. It is written to created_by/changed_by as nullable, so 0 becomes NULL
// rather than a bogus FK to user 0.
func actorID(r *http.Request) int64 {
	if u := auth.UserFrom(r.Context()); u != nil {
		return u.ID
	}
	return 0
}

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

func respondErr(w http.ResponseWriter, r *http.Request, err error) {
	rid := middleware.RequestIDFrom(r.Context())
	switch {
	case errors.Is(err, ErrNotFound), errors.Is(err, ErrGroupNotFound), errors.Is(err, finance.ErrNotFound):
		httpresponse.Error(w, http.StatusNotFound, rid, httpresponse.CodeNotFound, "LHPP tidak ditemukan", nil)
	case errors.Is(err, ErrDOTidakDitemukan), errors.Is(err, ErrInvoiceBelumTerbit),
		errors.Is(err, ErrInvoiceTidakDitemukan),
		errors.Is(err, ErrInvalidAmount), errors.Is(err, ErrAmountExceedsSisa),
		errors.Is(err, ErrMergeNeedsTwoGroups), errors.Is(err, ErrMergeTargetNotListed),
		errors.Is(err, ErrMergeDriverMismatch), errors.Is(err, ErrMergeNoMembers),
		errors.Is(err, ErrSplitNoDOs), errors.Is(err, ErrGroupDriverLocked),
		errors.Is(err, ErrGroupNotActive), errors.Is(err, ErrDriverRequired),
		errors.Is(err, finance.ErrPaymentTooLarge), errors.Is(err, finance.ErrPaymentMethodInvalid),
		errors.Is(err, finance.ErrNotPayable), err != nil && strings.Contains(err.Error(), "not payable"):
		httpresponse.Error(w, http.StatusUnprocessableEntity, rid, httpresponse.CodeBusinessViolation, err.Error(), nil)
	case errors.Is(err, ErrNoEmployee):
		httpresponse.Error(w, http.StatusUnprocessableEntity, rid, httpresponse.CodeBusinessViolation, err.Error(), nil)
	// A DO that lost the race for a membership slot, and a merge that lost a
	// serialization race three times, are both 409/CodeConflict: the caller
	// should re-read and retry, not treat it as a server fault. Never a 500.
	case errors.Is(err, ErrDOAlreadyInAnotherGroup), errors.Is(err, ErrConcurrentGrouping):
		httpresponse.Error(w, http.StatusConflict, rid, httpresponse.CodeConflict, err.Error(), nil)
	case err != nil:
		httpresponse.Internal(w, rid, logger.FromContext(r.Context()), err)
	}
}

// --- Sales LHPP (billing visits scheduled by Finance) ------------------------

func (h *Handler) ListSales(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	f := SalesListFilter{
		Page:    queryInt(q.Get("page")),
		PerPage: queryInt(q.Get("per_page")),
		Search:  q.Get("search"),
		Status:  q.Get("status"),
	}
	rows, total, err := h.svc.ListSales(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()), rows,
		normOr(f.Page, 1), perPageOrDefault(f.PerPage), total)
}

// MySales serves GET /lhpp/sales/me — the self-service worksheet a sales rep
// prints for their own penagihan visits.
func (h *Handler) MySales(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	f := SalesListFilter{
		Page:    queryInt(q.Get("page")),
		PerPage: queryInt(q.Get("per_page")),
		Search:  q.Get("search"),
		Status:  q.Get("status"),
	}
	u := auth.UserFrom(r.Context())
	if u == nil {
		httpresponse.Error(w, http.StatusUnauthorized, middleware.RequestIDFrom(r.Context()), httpresponse.CodeAuthentication, "unauthorized", nil)
		return
	}
	rows, total, err := h.svc.MySalesGroups(r.Context(), u.ID, f)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OKPaginated(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), rows,
		normOr(f.Page, 1), perPageOrDefault(f.PerPage), total)
}

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

func (h *Handler) RecordSalesPayment(w http.ResponseWriter, r *http.Request) {
	var in SalesPaymentInput
	defer r.Body.Close()
	if !httpresponse.DecodeJSON(w, r, middleware.RequestIDFrom(r.Context()), &in) {
		return
	}
	group, err := h.svc.RecordSalesPayment(r.Context(), pathID(r), in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), group)
}

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

func normOr(n, def int) int {
	if n < 1 {
		return def
	}
	return n
}

func perPageOrDefault(n int) int {
	if n < 1 || n > 100 {
		return 15
	}
	return n
}
