// tenants.go — HTTP handlers for the /v1/tenants/* routes (M13b W1).
//
// Routes (all require a valid Bearer access JWT):
//
//   GET    /v1/tenants              — list (super_admin: all, tenant_admin: own only)
//   POST   /v1/tenants              — create (super_admin only)
//   GET    /v1/tenants/{id}         — detail (super_admin any, tenant_admin own only)
//   PATCH  /v1/tenants/{id}         — update (super_admin: any field, tenant_admin: own + display_name/contact_email only)
//   POST   /v1/tenants/{id}/status  — set status (super_admin only)
//
// Errors:
//   400 — bad input (validation, JSON parse)
//   401 — handled by RequireAuth middleware (no body rewrite here)
//   403 — role not allowed, or tenant_admin trying to access another tenant
//   404 — tenant id not found
//   409 — duplicate slug on create
//   500 — unexpected DB error
package main

import (
	"encoding/json"
	"errors"
	"log/slog"
	"net/http"
	"strconv"
	"strings"

	"git3.techno-world.net/lrosales/broad-announce/internal/authd"
)

// tenantsListResponse is the wire shape for GET /v1/tenants.
// `total` is included for pagination (we cap limit at 500 for
// now; the UI shows total so operators know how many pages there
// are even if it caps the per-page count).
type tenantsListResponse struct {
	Items []authd.Tenant `json:"items"`
	Total int            `json:"total"`
	Limit int            `json:"limit"`
	Offset int           `json:"offset"`
}

// listTenantsHandler wires GET /v1/tenants.
func listTenantsHandler(ad *authd.Authd, logger *slog.Logger) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		claims := authd.ClaimsFromContext(r.Context())
		if claims == nil {
			writeErr(w, http.StatusUnauthorized, "unauthorized", "claims missing")
			return
		}
		q := strings.TrimSpace(r.URL.Query().Get("q"))
		status := strings.TrimSpace(r.URL.Query().Get("status"))
		limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
		offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
		filter := authd.TenantFilter{
			Q:              q,
			Status:         status,
			Limit:          limit,
			Offset:         offset,
			CallerRole:     claims.Role,
			CallerTenantID: claims.TenantID,
		}
		items, total, err := ad.Store().ListTenants(r.Context(), filter)
		if err != nil {
			logger.Error("list tenants", "err", err, "actor", claims.UserID)
			writeErr(w, http.StatusInternalServerError, "internal", "list failed")
			return
		}
		if filter.Limit <= 0 {
			filter.Limit = 100
		}
		if filter.Limit > 500 {
			filter.Limit = 500
		}
		writeJSON(w, http.StatusOK, tenantsListResponse{
			Items: items, Total: total, Limit: filter.Limit, Offset: filter.Offset,
		})
	}
}

// createTenantRequest is the POST /v1/tenants body. Mirrors the
// store's CreateTenantInput but with snake_case JSON tags.
type createTenantRequest struct {
	Slug            string `json:"slug"`
	DisplayName     string `json:"display_name"`
	ContactEmail    string `json:"contact_email"`
	RateLimitPerSec *int   `json:"rate_limit_per_sec"`
	FCMShared       *bool  `json:"fcm_shared"`
}

// createTenantHandler wires POST /v1/tenants. Super_admin only —
// RequireRole is applied in main()'s mux.Handle call.
func createTenantHandler(ad *authd.Authd, logger *slog.Logger) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		claims := authd.ClaimsFromContext(r.Context())
		if claims == nil {
			writeErr(w, http.StatusUnauthorized, "unauthorized", "claims missing")
			return
		}
		var req createTenantRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			writeErr(w, http.StatusBadRequest, "bad_request", "invalid JSON body")
			return
		}
		in := authd.CreateTenantInput{
			Slug:         strings.TrimSpace(req.Slug),
			DisplayName:  strings.TrimSpace(req.DisplayName),
			ContactEmail: strings.TrimSpace(req.ContactEmail),
			FCMShared:    req.FCMShared,
		}
		if req.RateLimitPerSec != nil {
			in.RateLimitPerSec = *req.RateLimitPerSec
		}
		ip := clientIP(r)
		ua := r.UserAgent()
		t, err := ad.Store().CreateTenant(r.Context(), in, claims.UserID, ip, ua)
		if err != nil {
			switch {
			case errors.Is(err, authd.ErrTenantSlugTaken):
				writeErr(w, http.StatusConflict, "slug_taken", "tenant slug already in use")
			case errors.Is(err, authd.ErrTenantInvalid):
				writeErr(w, http.StatusBadRequest, "invalid", err.Error())
			default:
				logger.Error("create tenant", "err", err, "actor", claims.UserID)
				writeErr(w, http.StatusInternalServerError, "internal", "create failed")
			}
			return
		}
		logger.Info("tenant created",
			"tenant_id", t.ID, "slug", t.Slug, "actor", claims.UserID)
		writeJSON(w, http.StatusCreated, t)
	}
}

// getTenantHandler wires GET /v1/tenants/{id}. tenant_admin may
// only fetch their own tenant.
func getTenantHandler(ad *authd.Authd, logger *slog.Logger) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		claims := authd.ClaimsFromContext(r.Context())
		if claims == nil {
			writeErr(w, http.StatusUnauthorized, "unauthorized", "claims missing")
			return
		}
		id := r.PathValue("id")
		if !isUUID(id) {
			writeErr(w, http.StatusBadRequest, "bad_request", "tenant id must be a UUID")
			return
		}
		if !canAccessTenant(claims, id) {
			writeErr(w, http.StatusForbidden, "forbidden", "not your tenant")
			return
		}
		t, err := ad.Store().GetTenant(r.Context(), id)
		if err != nil {
			if errors.Is(err, authd.ErrTenantNotFound) {
				writeErr(w, http.StatusNotFound, "not_found", "tenant not found")
				return
			}
			logger.Error("get tenant", "err", err, "tenant_id", id)
			writeErr(w, http.StatusInternalServerError, "internal", "lookup failed")
			return
		}
		writeJSON(w, http.StatusOK, t)
	}
}

// updateTenantRequest is the PATCH body. All fields optional.
type updateTenantRequest struct {
	DisplayName     *string `json:"display_name"`
	ContactEmail    *string `json:"contact_email"`
	RateLimitPerSec *int    `json:"rate_limit_per_sec"`
	FCMShared       *bool   `json:"fcm_shared"`
}

// updateTenantHandler wires PATCH /v1/tenants/{id}. tenant_admin
// may only update display_name + contact_email on their own tenant.
func updateTenantHandler(ad *authd.Authd, logger *slog.Logger) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		claims := authd.ClaimsFromContext(r.Context())
		if claims == nil {
			writeErr(w, http.StatusUnauthorized, "unauthorized", "claims missing")
			return
		}
		id := r.PathValue("id")
		if !isUUID(id) {
			writeErr(w, http.StatusBadRequest, "bad_request", "tenant id must be a UUID")
			return
		}
		if !canAccessTenant(claims, id) {
			writeErr(w, http.StatusForbidden, "forbidden", "not your tenant")
			return
		}
		var req updateTenantRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			writeErr(w, http.StatusBadRequest, "bad_request", "invalid JSON body")
			return
		}
		in := authd.UpdateTenantInput{
			DisplayName:     req.DisplayName,
			ContactEmail:    req.ContactEmail,
			RateLimitPerSec: req.RateLimitPerSec,
			FCMShared:       req.FCMShared,
		}
		// tenant_admin is restricted to display_name + contact_email
		// (enforced in the store via actorScopeAll).
		scopeAll := claims.Role == "super_admin"
		ip := clientIP(r)
		ua := r.UserAgent()
		t, err := ad.Store().UpdateTenant(r.Context(), id, in, scopeAll, claims.UserID, ip, ua)
		if err != nil {
			switch {
			case errors.Is(err, authd.ErrTenantNotFound):
				writeErr(w, http.StatusNotFound, "not_found", "tenant not found")
			case errors.Is(err, authd.ErrTenantInvalid):
				writeErr(w, http.StatusBadRequest, "invalid", err.Error())
			default:
				logger.Error("update tenant", "err", err, "tenant_id", id, "actor", claims.UserID)
				writeErr(w, http.StatusInternalServerError, "internal", "update failed")
			}
			return
		}
		logger.Info("tenant updated", "tenant_id", id, "actor", claims.UserID)
		writeJSON(w, http.StatusOK, t)
	}
}

// setTenantStatusRequest is the POST /v1/tenants/{id}/status body.
type setTenantStatusRequest struct {
	Status string `json:"status"`
}

// setTenantStatusHandler wires POST /v1/tenants/{id}/status.
// super_admin only — RequireRole is applied in main()'s mux.Handle
// call.
func setTenantStatusHandler(ad *authd.Authd, logger *slog.Logger) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		claims := authd.ClaimsFromContext(r.Context())
		if claims == nil {
			writeErr(w, http.StatusUnauthorized, "unauthorized", "claims missing")
			return
		}
		id := r.PathValue("id")
		if !isUUID(id) {
			writeErr(w, http.StatusBadRequest, "bad_request", "tenant id must be a UUID")
			return
		}
		var req setTenantStatusRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			writeErr(w, http.StatusBadRequest, "bad_request", "invalid JSON body")
			return
		}
		ip := clientIP(r)
		ua := r.UserAgent()
		t, err := ad.Store().SetTenantStatus(r.Context(), id, strings.TrimSpace(req.Status), claims.UserID, ip, ua)
		if err != nil {
			switch {
			case errors.Is(err, authd.ErrTenantNotFound):
				writeErr(w, http.StatusNotFound, "not_found", "tenant not found")
			case errors.Is(err, authd.ErrTenantInvalid):
				writeErr(w, http.StatusBadRequest, "invalid", err.Error())
			default:
				logger.Error("set tenant status", "err", err, "tenant_id", id, "actor", claims.UserID)
				writeErr(w, http.StatusInternalServerError, "internal", "update failed")
			}
			return
		}
		logger.Info("tenant status changed",
			"tenant_id", id, "to", t.Status, "actor", claims.UserID)
		writeJSON(w, http.StatusOK, t)
	}
}

// canAccessTenant returns true if the caller's role + tenant_id
// grant access to the given tenant id. super_admin may access any.
// tenant_admin / viewer may only access their own.
func canAccessTenant(claims *authd.AccessClaims, tenantID string) bool {
	if claims == nil {
		return false
	}
	if claims.Role == "super_admin" {
		return true
	}
	return claims.TenantID == tenantID
}

// isUUID is a lenient UUID format check (any 8-4-4-4-12 hex
// blob). Postgres will reject malformed values on the actual
// query; this is just to keep the 400s out of the 500s.
func isUUID(s string) bool {
	if len(s) != 36 {
		return false
	}
	for i, c := range s {
		switch i {
		case 8, 13, 18, 23:
			if c != '-' {
				return false
			}
		default:
			if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
				return false
			}
		}
	}
	return true
}
