package system

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 system service to HTTP. RegisterRoutes mounts the module
// routes with the system.* permission family (admin bypass applies upstream).
type Handler struct {
	svc *Service
}

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

// RegisterRoutes mounts /users, /roles and /activity-logs on api. The router is
// expected to have auth.Middleware wired upstream (main applies it, consistent
// with the other modules); RequirePermission gates each write with system.*.
func (h *Handler) RegisterRoutes(api chi.Router) {
	api.Route("/users", func(ur chi.Router) {
		ur.With(auth.RequirePermission("system.users.view")).Get("/", h.ListUsers)
		ur.With(auth.RequirePermission("system.users.create")).Post("/", h.CreateUser)
		ur.With(auth.RequirePermission("system.users.view")).Get("/{id}", h.GetUser)
		ur.With(auth.RequirePermission("system.users.update")).Put("/{id}", h.UpdateUser)
		ur.With(auth.RequirePermission("system.users.delete")).Delete("/{id}", h.DeleteUser)
		ur.With(auth.RequirePermission("system.users.update")).Post("/{id}/toggle", h.ToggleUser)
	})
	api.Route("/roles", func(rr chi.Router) {
		rr.With(auth.RequirePermission("system.roles.view")).Get("/", h.ListRoles)
		rr.With(auth.RequireAdmin()).Post("/", h.CreateRole)
		rr.With(auth.RequireAdmin()).Put("/{id}", h.UpdateRole)
		rr.With(auth.RequireAdmin()).Delete("/{id}", h.DeleteRole)
	})
	// Centralized role-permission matrix: admin only.
	api.Route("/role-permissions", func(rp chi.Router) {
		rp.With(auth.RequireAdmin()).Get("/", h.RolePermissionMatrix)
		rp.With(auth.RequireAdmin()).Put("/", h.SetRolePermission)
	})
	// Permission catalog: the backend-owned grouped key set the ERP editors
	// render (frontend holds zero copies). Same reader gate as the matrix.
	api.Route("/permissions", func(pr chi.Router) {
		pr.With(auth.RequirePermission("system.roles.view")).Get("/", h.ListPermissions)
	})
	api.Route("/stores", func(sr chi.Router) {
		sr.With(auth.RequirePermission("system.stores.view")).Get("/", h.ListStores)
		sr.With(auth.RequirePermission("system.stores.create")).Post("/", h.CreateStore)
		sr.With(auth.RequirePermission("system.stores.view")).Get("/{id}", h.GetStore)
		sr.With(auth.RequirePermission("system.stores.update")).Put("/{id}", h.UpdateStore)
		sr.With(auth.RequirePermission("system.stores.delete")).Delete("/{id}", h.DeleteStore)
		sr.With(auth.RequirePermission("system.users.view")).Get("/{id}/users", h.ListStoreUsers)
		sr.With(auth.RequirePermission("system.stores.update")).Post("/{id}/users", h.AddStoreUser)
		sr.With(auth.RequirePermission("system.stores.update")).Delete("/{id}/users/{userID}", h.RemoveStoreUser)
	})
	api.Route("/activity-logs", func(ar chi.Router) {
		ar.With(auth.RequirePermission("system.activity-logs.view")).Get("/", h.ListLogs)
		ar.With(auth.RequirePermission("system.activity-logs.delete")).Delete("/", h.ClearLogs)
	})
	// Live notification feed (pending approvals, overdue invoices, low
	// stock). App-axis gate: the provider computes company-wide operational
	// and financial data, so only ERP/POS identities may poll it — a B2B
	// buyer must not read cross-tenant figures (authz audit 2026-09-09).
	// POST /notifications (admin broadcast) and the /support + /admin stubs
	// were removed the same day: zero consumers across every frontend.
	api.With(auth.RequireAnyApp(auth.AppERP, auth.AppPOS)).Get("/notifications", h.ListNotifications)
}

// --- users -----------------------------------------------------------------

// userResponse adds the computed effective-permission set (role glob
// expanded, then overridden by the per-user override map — same
// u.EffectivePermissions() call GET /auth/me uses) alongside the existing
// raw `permissions` override field. UsersPage.vue's per-user checkbox editor
// otherwise only sees the override map and cannot tell "granted by role"
// from "denied", so a click that should deny a role-granted permission
// looks like a no-op grant instead.
// userResponse adds the computed effective-permission set and module categorization
type userResponse struct {
	auth.User
	Module               string          `json:"module"`
	EffectivePermissions map[string]bool `json:"effective_permissions"`
}

func ModuleForRole(role auth.Role) string {
	switch role {
	case auth.RoleCashier:
		return "pos"
	case auth.RoleBuyer:
		return "b2b"
	default:
		return "erp"
	}
}

func withEffective(u auth.User) userResponse {
	return userResponse{
		User:                 u,
		Module:               ModuleForRole(u.Role),
		EffectivePermissions: u.EffectivePermissions(),
	}
}

func (h *Handler) ListUsers(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	page, per := queryInt(q.Get("page")), queryInt(q.Get("per_page"))
	filter := UserFilter{
		Search: q.Get("search"),
		Role:   q.Get("role"),
		Module: q.Get("module"),
		Status: q.Get("status"),
	}
	list, total, counts, err := h.svc.ListUsers(r.Context(), norm(page), norm(per), filter)
	if err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	resp := make([]userResponse, len(list))
	for i, u := range list {
		resp[i] = withEffective(u)
	}
	extra := map[string]any{
		"module_counts": counts,
	}
	httpresponse.OKPaginatedWithMeta(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), resp, norm(page), norm(per), int(total), extra)
}

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

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

func (h *Handler) UpdateUser(w http.ResponseWriter, r *http.Request) {
	var in UserInput
	if !decode(w, r, &in) {
		return
	}
	u, err := h.svc.UpdateUser(r.Context(), pathID(r), auth.UserFrom(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) DeleteUser(w http.ResponseWriter, r *http.Request) {
	actor := auth.UserFrom(r.Context())
	if err := h.svc.DeleteUser(r.Context(), pathID(r), actor.ID); err != nil {
		respondErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) ToggleUser(w http.ResponseWriter, r *http.Request) {
	actor := auth.UserFrom(r.Context())
	u, err := h.svc.ToggleUserActive(r.Context(), pathID(r), actor.ID)
	if err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), u)
}

// --- roles -----------------------------------------------------------------

func (h *Handler) ListRoles(w http.ResponseWriter, r *http.Request) {
	roles, err := h.svc.ListRoles(r.Context())
	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()), roles)
}

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

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

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

// --- activity logs ---------------------------------------------------------

func (h *Handler) ListLogs(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	page, per := queryInt(q.Get("page")), queryInt(q.Get("per_page"))
	uid, _ := strconv.ParseInt(q.Get("user_id"), 10, 64)
	lq := LogQuery{
		Module:   q.Get("module"),
		UserID:   uid,
		DateFrom: q.Get("date_from"),
		DateTo:   q.Get("date_to"),
	}
	list, total, err := h.svc.ListLogs(r.Context(), lq, norm(page), norm(per))
	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(page), norm(per), int(total))
}

func (h *Handler) ClearLogs(w http.ResponseWriter, r *http.Request) {
	if err := h.svc.ClearLogs(r.Context()); err != nil {
		httpresponse.Internal(w, middleware.RequestIDFrom(r.Context()), logger.FromContext(r.Context()), err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

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

func respondErr(w http.ResponseWriter, r *http.Request, err error) {
	rid := middleware.RequestIDFrom(r.Context())
	switch {
	case errors.Is(err, auth.ErrNotFound):
		httpresponse.Error(w, http.StatusNotFound, rid, httpresponse.CodeNotFound, "Not found", nil)
	case errors.Is(err, ErrInUse), errors.Is(err, ErrRoleExists):
		httpresponse.Error(w, http.StatusConflict, rid, httpresponse.CodeConflict, err.Error(), nil)
	case errors.Is(err, ErrEmailTaken):
		httpresponse.Error(w, http.StatusConflict, rid, httpresponse.CodeConflict, "Email already registered", nil)
	case errors.Is(err, ErrSelfDelete), errors.Is(err, ErrSelfEscalation), errors.Is(err, ErrLastAdmin),
		errors.Is(err, ErrRoleNotAssignable), errors.Is(err, ErrStoreOutOfScope):
		httpresponse.Error(w, http.StatusForbidden, rid, httpresponse.CodeForbidden, err.Error(), nil)
	case errors.Is(err, ErrPermissionRequired), errors.Is(err, ErrUnknownPermission):
		httpresponse.Error(w, http.StatusBadRequest, rid, httpresponse.CodeValidation, err.Error(), nil)
	case errors.Is(err, ErrInvalidRole), errors.Is(err, ErrInvalidEmail),
		errors.Is(err, ErrShortPassword), errors.Is(err, ErrRoleNameRequired),
		errors.Is(err, ErrBuiltinRole), errors.Is(err, ErrInvalidRoleName), errors.Is(err, ErrNothingToDo):
		httpresponse.Error(w, http.StatusUnprocessableEntity, rid, httpresponse.CodeValidation, err.Error(), nil)
	case err != nil:
		httpresponse.SafeError(w, rid, logger.FromContext(r.Context()), http.StatusBadRequest, httpresponse.CodeValidation, err)
	}
}

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 queryInt(s string) int {
	n, _ := strconv.Atoi(s)
	return n
}

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


// --- stores & role-permission matrix (multi-tenant §6) ----------------------

func (h *Handler) ListStores(w http.ResponseWriter, r *http.Request) {
	stores, err := h.svc.ListStores(r.Context())
	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()), stores)
}

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

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

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

func (h *Handler) DeleteStore(w http.ResponseWriter, r *http.Request) {
	if err := h.svc.DeleteStore(r.Context(), pathID(r)); err != nil {
		if errors.Is(err, ErrInUse) {
			httpresponse.Error(w, http.StatusConflict, middleware.RequestIDFrom(r.Context()),
				httpresponse.CodeConflict, "Toko tidak dapat dihapus karena sudah memiliki riwayat transaksi (pesanan penjualan/kasir). Silakan nonaktifkan toko melalui tombol Edit.", nil)
			return
		}
		respondErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

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

func (h *Handler) AddStoreUser(w http.ResponseWriter, r *http.Request) {
	var in struct {
		UserID int64 `json:"user_id"`
	}
	if !decode(w, r, &in) {
		return
	}
	if err := h.svc.AddStoreUser(r.Context(), pathID(r), in.UserID); err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusCreated, middleware.RequestIDFrom(r.Context()), map[string]any{
		"store_id": pathID(r), "user_id": in.UserID,
	})
}

func (h *Handler) RemoveStoreUser(w http.ResponseWriter, r *http.Request) {
	userID, _ := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
	if err := h.svc.RemoveStoreUser(r.Context(), pathID(r), userID); err != nil {
		respondErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// ListPermissions serves the canonical permission catalog: grouped keys for
// the ERP user/role editors. No assignments here, just the validation set.
func (h *Handler) ListPermissions(w http.ResponseWriter, r *http.Request) {
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), auth.Catalog())
}

func (h *Handler) RolePermissionMatrix(w http.ResponseWriter, r *http.Request) {
	rows, err := h.svc.RolePermissionMatrix(r.Context())
	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()), rows)
}

func (h *Handler) SetRolePermission(w http.ResponseWriter, r *http.Request) {
	var in RolePermissionInput
	if !decode(w, r, &in) {
		return
	}
	allow := in.Allow == nil || *in.Allow
	if len(in.Permissions) > 0 {
		for _, perm := range in.Permissions {
			if err := h.svc.SetRolePermission(r.Context(), in.Role, perm, allow); err != nil {
				respondErr(w, r, err)
				return
			}
		}
		httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), map[string]any{
			"role": in.Role, "permissions": in.Permissions, "allow": allow,
		})
		return
	}

	if err := h.svc.SetRolePermission(r.Context(), in.Role, in.Permission, allow); err != nil {
		respondErr(w, r, err)
		return
	}
	httpresponse.OK(w, http.StatusOK, middleware.RequestIDFrom(r.Context()), map[string]any{
		"role": in.Role, "permission": in.Permission, "allow": in.Allow,
	})
}

// ListNotifications serves the provider-computed live feed. Errors are
// propagated (500), never masked with an empty list — a silent [] would hide
// provider/DB failures from the ERP shell.
func (h *Handler) ListNotifications(w http.ResponseWriter, r *http.Request) {
	u := auth.UserFrom(r.Context())
	items, err := h.svc.ListNotifications(r.Context(), u)
	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()), items)
}
