package handlers

import (
	"encoding/json"
	"errors"
	"net/http"

	"github.com/google/uuid"

	"mgit.msbls.de/m/paliad/internal/branding"
	"mgit.msbls.de/m/paliad/internal/services"
)

// admin_users.go — backing endpoints for the /admin/team page (t-paliad-050).
// All four routes are registered behind RequireAdminFunc in handlers.go, so
// the in-handler logic can assume the caller already passed the admin gate
// and only the operation itself needs validation.

// GET /api/admin/users — full unredacted list of every paliad.users row.
func handleAdminListUsers(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	users, err := dbSvc.users.List(r.Context())
	if err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"})
		return
	}
	writeJSON(w, http.StatusOK, users)
}

// GET /api/admin/users/unonboarded — auth.users entries without a paliad.users
// row. Feeds the "direct add" dropdown so an admin can onboard a colleague
// who logged in but never finished the form.
func handleAdminListUnonboarded(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	rows, err := dbSvc.users.ListUnonboardedAuthUsers(r.Context())
	if err != nil {
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"})
		return
	}
	writeJSON(w, http.StatusOK, rows)
}

// POST /api/admin/users/full — create BOTH an auth.users row (via Supabase
// Admin API) and a paliad.users row in one operation. t-paliad-223 Slice B
// (#49). Lets a global_admin onboard a colleague without forcing them
// through the email-invitation round-trip; the new user is visible in
// dropdowns immediately and can log in via the emailed magic-link.
//
// Requires SUPABASE_SERVICE_ROLE_KEY at the server. Returns 503 when
// unset so a deploy that hasn't provisioned the credential yet gets a
// clear diagnostic instead of a cryptic 500.
//
// Error mapping:
//   - ErrSupabaseAdminUnavailable    → 503
//   - ErrSupabaseEmailExists          → 409 (hint to use "Onboard existing")
//   - ErrUserAlreadyOnboarded          → 409 (paliad.users dup; should be unreachable)
//   - ErrInvalidInput                  → 400 (bad shape)
//   - email domain not on whitelist    → 403 (mirrors handleAdminCreateUser)
//   - other                            → 500
func handleAdminCreateFullUser(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	var input services.AdminCreateFullInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	if !isAllowedEmailDomain(input.Email) {
		writeJSON(w, http.StatusForbidden, map[string]string{
			"error": "email domain not on the " + branding.Name + " allow-list",
		})
		return
	}

	// Look up the inviter (the calling admin) so the welcome email and
	// audit row carry their identity. Failures here shouldn't block the
	// create; we just degrade to empty fields.
	inviter, err := dbSvc.users.GetByID(r.Context(), uid)
	if err == nil && inviter != nil {
		input.InviterID = inviter.ID
		input.InviterName = inviter.DisplayName
		input.InviterEmail = inviter.Email
	}

	u, err := dbSvc.users.AdminCreateUserFull(r.Context(), input)
	if err != nil {
		switch {
		case errors.Is(err, services.ErrSupabaseAdminUnavailable):
			writeJSON(w, http.StatusServiceUnavailable, map[string]string{
				"error": "add-user flow requires SUPABASE_SERVICE_ROLE_KEY on the server",
			})
		case errors.Is(err, services.ErrSupabaseEmailExists):
			writeJSON(w, http.StatusConflict, map[string]string{
				"error": "auth account already exists — please use 'Onboard existing' instead",
			})
		case errors.Is(err, services.ErrUserAlreadyOnboarded):
			writeJSON(w, http.StatusConflict, map[string]string{
				"error": "user already onboarded",
			})
		case errors.Is(err, services.ErrInvalidInput):
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		default:
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"})
		}
		return
	}
	writeJSON(w, http.StatusCreated, u)
}

// POST /api/admin/users — direct-create a paliad.users row for an existing
// auth.users entry. The recipient email's domain must already match the
// allowed-email policy (Supabase wouldn't have let them sign up otherwise),
// but we re-check here so a stale auth.users row from before the policy
// existed can't sneak through.
func handleAdminCreateUser(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	var input services.AdminCreateInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	if !isAllowedEmailDomain(input.Email) {
		writeJSON(w, http.StatusForbidden, map[string]string{
			"error": "email domain not on the " + branding.Name + " allow-list",
		})
		return
	}
	u, err := dbSvc.users.AdminCreateUser(r.Context(), input)
	if err != nil {
		switch {
		case errors.Is(err, services.ErrUserAlreadyOnboarded):
			writeJSON(w, http.StatusConflict, map[string]string{
				"error": "user already onboarded",
			})
		case errors.Is(err, services.ErrInvalidInput):
			// AdminCreateUser uses ErrInvalidInput for both bad-shape inputs
			// and the "no auth.users row for this email" case. Surfacing the
			// raw message keeps the form's error display useful without a
			// separate error type for each.
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		default:
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"})
		}
		return
	}
	writeJSON(w, http.StatusCreated, u)
}

// PATCH /api/admin/users/{id} — mutate any paliad.users row.
func handleAdminUpdateUser(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	id, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
		return
	}
	var input services.AdminUpdateInput
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
		return
	}
	u, err := dbSvc.users.AdminUpdateUser(r.Context(), id, input)
	if err != nil {
		switch {
		case errors.Is(err, services.ErrUserNotOnboarded):
			writeJSON(w, http.StatusNotFound, map[string]string{"error": "user not found"})
		case errors.Is(err, services.ErrLastGlobalAdmin):
			writeJSON(w, http.StatusConflict, map[string]string{
				"error": "cannot demote the last remaining global admin",
			})
		case errors.Is(err, services.ErrInvalidInput):
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		default:
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"})
		}
		return
	}
	writeJSON(w, http.StatusOK, u)
}

// DELETE /api/admin/users/{id} — remove a paliad.users row + cascade clean-up
// of project_teams / partner_unit_members. auth.users is left intact so the
// user can re-onboard later if needed.
func handleAdminDeleteUser(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	id, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
		return
	}
	if err := dbSvc.users.AdminDeleteUser(r.Context(), id); err != nil {
		switch {
		case errors.Is(err, services.ErrUserNotOnboarded):
			writeJSON(w, http.StatusNotFound, map[string]string{"error": "user not found"})
		case errors.Is(err, services.ErrLastGlobalAdmin):
			writeJSON(w, http.StatusConflict, map[string]string{
				"error": "cannot delete the last remaining global admin",
			})
		case errors.Is(err, services.ErrInvalidInput):
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		default:
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"})
		}
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// handleAdminTeamPage serves the SPA shell for /admin/team.
func handleAdminTeamPage(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "dist/admin-team.html")
}

// handleAdminIndexPage serves the SPA shell for /admin — the admin landing
// page that lists current and planned admin sub-pages so the area is
// browseable. Like /admin/team, the route is gated through RequireAdminFunc
// at registration in handlers.go; non-admins get the standard 302 to
// /dashboard?forbidden=admin from rejectAdmin.
func handleAdminIndexPage(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "dist/admin.html")
}

// handleAdminPartnerUnitsPage serves the SPA shell for /admin/partner-units.
// Same gate pattern as the other /admin/* pages.
func handleAdminPartnerUnitsPage(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "dist/admin-partner-units.html")
}
