package hr

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

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

	"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"
)

// Handler binds the HR 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}
}

// --- employees ---------------------------------------------------------------

func (h *Handler) ListEmployees(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	f := Filter{Page: queryInt(q.Get("page")), PerPage: queryInt(q.Get("per_page")), SortBy: q.Get("sort_by"), SortDir: q.Get("sort_dir")}
	var active *bool
	if s := q.Get("status"); s == "active" || s == "inactive" {
		b := s == "active"
		active = &b
	}
	list, total, err := h.svc.ListEmployees(r.Context(), f, q.Get("search"), active)
	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, norm(f.Page), norm(f.PerPage), total)
}

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

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

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

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

// --- attendance --------------------------------------------------------------

func (h *Handler) ListAttendance(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	f := Filter{Page: queryInt(q.Get("page")), PerPage: queryInt(q.Get("per_page")),
		Status: q.Get("status"), DateFrom: q.Get("date_from"), DateTo: q.Get("date_to")}
	list, total, err := h.svc.ListAttendance(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, norm(f.Page), norm(f.PerPage), total)
}

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

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

func (h *Handler) MyAttendance(w http.ResponseWriter, r *http.Request) {
	u := auth.UserFrom(r.Context())
	if u == nil {
		unauthorized(w, r)
		return
	}
	q := r.URL.Query()
	f := Filter{Page: queryInt(q.Get("page")), PerPage: queryInt(q.Get("per_page")),
		DateFrom: q.Get("date_from"), DateTo: q.Get("date_to")}
	list, total, err := h.svc.MyAttendance(r.Context(), u.ID, f)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OKPaginated(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), list, norm(f.Page), norm(f.PerPage), total)
}

func (h *Handler) ClockIn(w http.ResponseWriter, r *http.Request) {
	u := auth.UserFrom(r.Context())
	if u == nil {
		unauthorized(w, r)
		return
	}
	var in ClockInOutInput
	if !decode(w, r, &in) {
		return
	}
	a, err := h.svc.ClockIn(r.Context(), u.ID, in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), a)
}

func (h *Handler) ClockOut(w http.ResponseWriter, r *http.Request) {
	u := auth.UserFrom(r.Context())
	if u == nil {
		unauthorized(w, r)
		return
	}
	var in ClockInOutInput
	if !decode(w, r, &in) {
		return
	}
	a, err := h.svc.ClockOut(r.Context(), u.ID, in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), a)
}

// --- leaves ------------------------------------------------------------------

func (h *Handler) ListLeaves(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	f := Filter{Page: queryInt(q.Get("page")), PerPage: queryInt(q.Get("per_page")),
		Status: q.Get("status"), DateFrom: q.Get("date_from"), DateTo: q.Get("date_to")}
	list, total, err := h.svc.ListLeaves(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, norm(f.Page), norm(f.PerPage), total)
}

func (h *Handler) MyLeaves(w http.ResponseWriter, r *http.Request) {
	u := auth.UserFrom(r.Context())
	if u == nil {
		unauthorized(w, r)
		return
	}
	q := r.URL.Query()
	f := Filter{Page: queryInt(q.Get("page")), PerPage: queryInt(q.Get("per_page")), Status: q.Get("status")}
	list, total, err := h.svc.MyLeaves(r.Context(), u.ID, f)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OKPaginated(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), list, norm(f.Page), norm(f.PerPage), total)
}

func (h *Handler) CreateLeave(w http.ResponseWriter, r *http.Request) {
	u := auth.UserFrom(r.Context())
	if u == nil {
		unauthorized(w, r)
		return
	}
	var in LeaveInput
	if !decode(w, r, &in) {
		return
	}
	l, err := h.svc.CreateLeave(r.Context(), u.ID, in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), l)
}

func (h *Handler) ApproveLeave(w http.ResponseWriter, r *http.Request) {
	h.reviewLeave(w, r, true)
}

func (h *Handler) RejectLeave(w http.ResponseWriter, r *http.Request) {
	h.reviewLeave(w, r, false)
}

func (h *Handler) reviewLeave(w http.ResponseWriter, r *http.Request, approve bool) {
	u := auth.UserFrom(r.Context())
	if u == nil {
		unauthorized(w, r)
		return
	}
	var l *Leave
	var err error
	if approve {
		l, err = h.svc.ApproveLeave(r.Context(), pathID(r), u.ID)
	} else {
		l, err = h.svc.RejectLeave(r.Context(), pathID(r), u.ID)
	}
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), l)
}

// --- payrolls ----------------------------------------------------------------

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

func (h *Handler) MyPayrolls(w http.ResponseWriter, r *http.Request) {
	u := auth.UserFrom(r.Context())
	if u == nil {
		unauthorized(w, r)
		return
	}
	q := r.URL.Query()
	f := Filter{Page: queryInt(q.Get("page")), PerPage: queryInt(q.Get("per_page"))}
	list, total, err := h.svc.MyPayrolls(r.Context(), u.ID, f)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OKPaginated(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), list, norm(f.Page), norm(f.PerPage), total)
}

func (h *Handler) GeneratePayrolls(w http.ResponseWriter, r *http.Request) {
	var in struct {
		PeriodMonth int `json:"period_month"`
		PeriodYear  int `json:"period_year"`
	}
	if !decode(w, r, &in) {
		return
	}
	list, err := h.svc.GeneratePayrolls(r.Context(), in.PeriodMonth, in.PeriodYear)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), list)
}

func (h *Handler) GetPayroll(w http.ResponseWriter, r *http.Request) {
	p, err := h.svc.PayrollByID(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) ApprovePayroll(w http.ResponseWriter, r *http.Request) {
	p, err := h.svc.ApprovePayroll(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) PayPayroll(w http.ResponseWriter, r *http.Request) {
	var in PayPayrollInput
	if !decode(w, r, &in) {
		return
	}
	var callerID *int64
	if u := auth.UserFrom(r.Context()); u != nil {
		callerID = &u.ID
	}
	p, err := h.svc.PayPayroll(r.Context(), pathID(r), in, callerID)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), p)
}

// --- performances ------------------------------------------------------------

func (h *Handler) ListPerformances(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	f := Filter{Page: queryInt(q.Get("page")), PerPage: queryInt(q.Get("per_page"))}
	list, total, err := h.svc.ListPerformances(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, norm(f.Page), norm(f.PerPage), total)
}

func (h *Handler) CreatePerformance(w http.ResponseWriter, r *http.Request) {
	u := auth.UserFrom(r.Context())
	if u == nil {
		unauthorized(w, r)
		return
	}
	var in PerformanceInput
	if !decode(w, r, &in) {
		return
	}
	p, err := h.svc.CreatePerformance(r.Context(), u.ID, in)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), p)
}

func (h *Handler) GetPerformance(w http.ResponseWriter, r *http.Request) {
	p, err := h.svc.PerformanceByID(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) UpdatePerformance(w http.ResponseWriter, r *http.Request) {
	var in PerformanceInput
	if !decode(w, r, &in) {
		return
	}
	p, err := h.svc.UpdatePerformance(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) DeletePerformance(w http.ResponseWriter, r *http.Request) {
	if err := h.svc.DeletePerformance(r.Context(), pathID(r)); err != nil {
		respondErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// --- helpers -----------------------------------------------------------------

func unauthorized(w http.ResponseWriter, r *http.Request) {
	httpresponse.Error(w, http.StatusUnauthorized, middleware.RequestIDFrom(r.Context()),
		httpresponse.CodeAuthentication, "Unauthenticated", nil)
}

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), errors.Is(err, ErrEmployeeRequired):
		httpresponse.Error(w, http.StatusNotFound, rid, httpresponse.CodeNotFound, "Not found", nil)
	case errors.Is(err, ErrNumberAllocation):
		// The server's own number generator collided. The caller sent no
		// employee number, so 409 with "duplicate value" would blame them for
		// something they did not do, and would tell them to go and find a
		// duplicate employee that does not exist. This is our bug: 500, generic
		// message, and the constraint name goes to the log where an operator can
		// act on it. httpresponse.Internal logs at Error with the request ID,
		// which is what correlates the two.
		httpresponse.Internal(w, rid, logger.FromContext(r.Context()), err)
	case errors.Is(err, ErrDuplicate), errors.Is(err, ErrAlreadyClockedIn),
		errors.Is(err, ErrNotClockedIn), errors.Is(err, ErrNotPending):
		httpresponse.Error(w, http.StatusConflict, rid, httpresponse.CodeBusinessViolation, err.Error(), nil)
	case errors.Is(err, ErrLeaveForOther):
		// The caller is authenticated and allowed to file leave, just not for
		// somebody else. 403, not 404: the employee exists and the caller may
		// well read them, so hiding it would be misleading.
		httpresponse.Error(w, http.StatusForbidden, rid, httpresponse.CodeForbidden, err.Error(), nil)
	case errors.Is(err, ErrLeaveSelfApproval):
		// A business rule the caller cannot work around by retrying.
		httpresponse.Error(w, http.StatusConflict, rid, httpresponse.CodeBusinessViolation, err.Error(), nil)
	case errors.Is(err, ErrInvalidDates):
		httpresponse.Error(w, http.StatusBadRequest, rid, httpresponse.CodeValidation, err.Error(), 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 norm(n int) int {
	if n < 1 {
		return 20
	}
	return n
}
