// sources.go — HTTP handlers for the /v1/tenants/{id}/sources/* routes (M13b W2).
//
// Routes (all require a valid Bearer access JWT):
//
//   GET    /v1/tenants/{id}/sources              — list
//   POST   /v1/tenants/{id}/sources              — create
//   GET    /v1/tenants/{id}/sources/{sid}        — detail
//   PATCH  /v1/tenants/{id}/sources/{sid}        — update
//   POST   /v1/tenants/{id}/sources/{sid}/status        — set status
//   POST   /v1/tenants/{id}/sources/{sid}/rotate-secrets — rotate HMAC + API key
//
// 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 or source id not found
//   409 — duplicate source id on create
//   500 — unexpected DB error
//
// Responses:
//   The Create + RotateSecrets handlers return
//   { "source": {...}, "secrets": { "hmac_secret": "...", "api_key": "..." } }
//   on success when secrets were generated. The `secrets` field
//   is OMITTED if the caller did not request secrets (so the
//   UI knows not to render the one-time modal). The shape is
//   always { source, secrets? } so the UI can destructure.

package main

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

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

// sourcesListResponse is the wire shape for GET /v1/tenants/{id}/sources.
type sourcesListResponse struct {
	Items  []authd.Source `json:"items"`
	Total  int            `json:"total"`
	Limit  int            `json:"limit"`
	Offset int            `json:"offset"`
}

// createOrRotateResponse is the shape returned by Create and Rotate.
type createOrRotateResponse struct {
	Source  *authd.Source         `json:"source"`
	Secrets *authd.SecretsPayload `json:"secrets,omitempty"`
}

// listSourcesHandler wires GET /v1/tenants/{id}/sources.
func listSourcesHandler(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
		}
		tenantID := r.PathValue("id")
		if !isUUID(tenantID) {
			writeErr(w, http.StatusBadRequest, "bad_request", "tenant id must be a UUID")
			return
		}
		if !canAccessTenant(claims, tenantID) {
			writeErr(w, http.StatusForbidden, "forbidden", "not your tenant")
			return
		}
		q := strings.TrimSpace(r.URL.Query().Get("q"))
		typeFilter := strings.TrimSpace(r.URL.Query().Get("type"))
		statusFilter := 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.SourceFilter{
			CompanyID:    tenantID,
			Q:            q,
			Type:         typeFilter,
			Status:       statusFilter,
			Limit:        limit,
			Offset:       offset,
			CallerRole:   claims.Role,
			CallerTenant: claims.TenantID,
		}
		items, total, err := ad.Store().ListSources(r.Context(), filter)
		if err != nil {
			logger.Error("list sources", "err", err, "actor", claims.UserID, "tenant_id", tenantID)
			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, sourcesListResponse{
			Items: items, Total: total, Limit: filter.Limit, Offset: filter.Offset,
		})
	}
}

// createSourceRequest is the POST body. All fields except id,
// name, type, and rate_limit_per_sec are optional. The hmac_secret
// and api_key fields, if non-empty, are stored bcrypt-hashed and
// returned ONCE in the response.
type createSourceRequest struct {
	ID              string          `json:"id"`
	Name            string          `json:"name"`
	Type            string          `json:"type"`
	RateLimitPerSec int             `json:"rate_limit_per_sec"`
	AllowedTargets  json.RawMessage `json:"allowed_targets"`
	MatchExpr       json.RawMessage `json:"match_expr"`
	Description     string          `json:"description"`
	MTLSRequired    bool            `json:"mtls_required"`
	HMACSecret      string          `json:"hmac_secret"`
	APIKey          string          `json:"api_key"`
}

// createSourceHandler wires POST /v1/tenants/{id}/sources.
func createSourceHandler(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
		}
		tenantID := r.PathValue("id")
		if !isUUID(tenantID) {
			writeErr(w, http.StatusBadRequest, "bad_request", "tenant id must be a UUID")
			return
		}
		if !canAccessTenant(claims, tenantID) {
			writeErr(w, http.StatusForbidden, "forbidden", "not your tenant")
			return
		}
		var req createSourceRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			writeErr(w, http.StatusBadRequest, "bad_request", "invalid JSON body")
			return
		}
		in := authd.CreateSourceInput{
			ID:              strings.TrimSpace(req.ID),
			Name:            strings.TrimSpace(req.Name),
			Type:            strings.TrimSpace(req.Type),
			RateLimitPerSec: req.RateLimitPerSec,
			AllowedTargets:  req.AllowedTargets,
			MatchExpr:       req.MatchExpr,
			Description:     req.Description,
			MTLSRequired:    req.MTLSRequired,
			HMACSecret:      req.HMACSecret,
			APIKey:          req.APIKey,
		}
		// Look up the auth tenant's display name so the
		// bridge INSERT into public.companies has a sensible
		// `name` value when the row is first created.
		tenant, err := ad.Store().GetTenant(r.Context(), tenantID)
		if err != nil {
			if errors.Is(err, authd.ErrTenantNotFound) {
				writeErr(w, http.StatusNotFound, "not_found", "tenant not found")
				return
			}
			logger.Error("create source: lookup tenant", "err", err, "tenant_id", tenantID)
			writeErr(w, http.StatusInternalServerError, "internal", "lookup failed")
			return
		}
		ip := clientIP(r)
		ua := r.UserAgent()
		src, secrets, err := ad.Store().CreateSource(r.Context(), tenantID, tenant.DisplayName, in, claims.UserID, ip, ua)
		if err != nil {
			switch {
			case errors.Is(err, authd.ErrSourceIDTaken):
				writeErr(w, http.StatusConflict, "id_taken", "source id already in use")
			case errors.Is(err, authd.ErrSourceInvalid):
				writeErr(w, http.StatusBadRequest, "invalid", err.Error())
			default:
				logger.Error("create source", "err", err, "tenant_id", tenantID, "actor", claims.UserID)
				writeErr(w, http.StatusInternalServerError, "internal", "create failed")
			}
			return
		}
		logger.Info("source created",
			"tenant_id", tenantID, "source_id", src.ID, "actor", claims.UserID,
			"hmac_set", src.HMACSet, "api_key_set", src.APIKeySet)
		writeJSON(w, http.StatusCreated, createOrRotateResponse{Source: src, Secrets: secrets})
	}
}

// getSourceHandler wires GET /v1/tenants/{id}/sources/{sid}.
func getSourceHandler(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
		}
		tenantID := r.PathValue("id")
		if !isUUID(tenantID) {
			writeErr(w, http.StatusBadRequest, "bad_request", "tenant id must be a UUID")
			return
		}
		if !canAccessTenant(claims, tenantID) {
			writeErr(w, http.StatusForbidden, "forbidden", "not your tenant")
			return
		}
		sourceID := r.PathValue("sid")
		if sourceID == "" {
			writeErr(w, http.StatusBadRequest, "bad_request", "source id is required")
			return
		}
		src, err := ad.Store().GetSource(r.Context(), tenantID, sourceID)
		if err != nil {
			if errors.Is(err, authd.ErrSourceNotFound) {
				writeErr(w, http.StatusNotFound, "not_found", "source not found")
				return
			}
			logger.Error("get source", "err", err, "tenant_id", tenantID, "source_id", sourceID)
			writeErr(w, http.StatusInternalServerError, "internal", "lookup failed")
			return
		}
		writeJSON(w, http.StatusOK, src)
	}
}

// updateSourceRequest is the PATCH body. All fields optional.
type updateSourceRequest struct {
	Name            *string         `json:"name"`
	Type            *string         `json:"type"`
	RateLimitPerSec *int            `json:"rate_limit_per_sec"`
	Description     *string         `json:"description"`
	MTLSRequired    *bool           `json:"mtls_required"`
	AllowedTargets  json.RawMessage `json:"allowed_targets"`
	MatchExpr       json.RawMessage `json:"match_expr"`
}

// updateSourceHandler wires PATCH /v1/tenants/{id}/sources/{sid}.
func updateSourceHandler(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
		}
		tenantID := r.PathValue("id")
		if !isUUID(tenantID) {
			writeErr(w, http.StatusBadRequest, "bad_request", "tenant id must be a UUID")
			return
		}
		if !canAccessTenant(claims, tenantID) {
			writeErr(w, http.StatusForbidden, "forbidden", "not your tenant")
			return
		}
		sourceID := r.PathValue("sid")
		if sourceID == "" {
			writeErr(w, http.StatusBadRequest, "bad_request", "source id is required")
			return
		}
		var req updateSourceRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			writeErr(w, http.StatusBadRequest, "bad_request", "invalid JSON body")
			return
		}
		in := authd.UpdateSourceInput{
			Name:            req.Name,
			Type:            req.Type,
			RateLimitPerSec: req.RateLimitPerSec,
			Description:     req.Description,
			MTLSRequired:    req.MTLSRequired,
			AllowedTargets:  req.AllowedTargets,
			MatchExpr:       req.MatchExpr,
		}
		ip := clientIP(r)
		ua := r.UserAgent()
		src, err := ad.Store().UpdateSource(r.Context(), tenantID, sourceID, in, claims.UserID, ip, ua)
		if err != nil {
			switch {
			case errors.Is(err, authd.ErrSourceNotFound):
				writeErr(w, http.StatusNotFound, "not_found", "source not found")
			case errors.Is(err, authd.ErrSourceInvalid):
				writeErr(w, http.StatusBadRequest, "invalid", err.Error())
			default:
				logger.Error("update source", "err", err, "tenant_id", tenantID, "source_id", sourceID, "actor", claims.UserID)
				writeErr(w, http.StatusInternalServerError, "internal", "update failed")
			}
			return
		}
		logger.Info("source updated",
			"tenant_id", tenantID, "source_id", sourceID, "actor", claims.UserID)
		writeJSON(w, http.StatusOK, src)
	}
}

// setSourceStatusRequest is the POST /status body.
type setSourceStatusRequest struct {
	Status string `json:"status"`
}

// setSourceStatusHandler wires POST /v1/tenants/{id}/sources/{sid}/status.
func setSourceStatusHandler(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
		}
		tenantID := r.PathValue("id")
		if !isUUID(tenantID) {
			writeErr(w, http.StatusBadRequest, "bad_request", "tenant id must be a UUID")
			return
		}
		if !canAccessTenant(claims, tenantID) {
			writeErr(w, http.StatusForbidden, "forbidden", "not your tenant")
			return
		}
		sourceID := r.PathValue("sid")
		if sourceID == "" {
			writeErr(w, http.StatusBadRequest, "bad_request", "source id is required")
			return
		}
		var req setSourceStatusRequest
		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()
		src, err := ad.Store().SetSourceStatus(r.Context(), tenantID, sourceID, strings.TrimSpace(req.Status), claims.UserID, ip, ua)
		if err != nil {
			switch {
			case errors.Is(err, authd.ErrSourceNotFound):
				writeErr(w, http.StatusNotFound, "not_found", "source not found")
			case errors.Is(err, authd.ErrSourceInvalid):
				writeErr(w, http.StatusBadRequest, "invalid", err.Error())
			default:
				logger.Error("set source status", "err", err, "tenant_id", tenantID, "source_id", sourceID, "actor", claims.UserID)
				writeErr(w, http.StatusInternalServerError, "internal", "update failed")
			}
			return
		}
		logger.Info("source status changed",
			"tenant_id", tenantID, "source_id", sourceID, "to", src.Status, "actor", claims.UserID)
		writeJSON(w, http.StatusOK, src)
	}
}

// rotateSourceSecretsHandler wires POST /v1/tenants/{id}/sources/{sid}/rotate-secrets.
func rotateSourceSecretsHandler(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
		}
		tenantID := r.PathValue("id")
		if !isUUID(tenantID) {
			writeErr(w, http.StatusBadRequest, "bad_request", "tenant id must be a UUID")
			return
		}
		if !canAccessTenant(claims, tenantID) {
			writeErr(w, http.StatusForbidden, "forbidden", "not your tenant")
			return
		}
		sourceID := r.PathValue("sid")
		if sourceID == "" {
			writeErr(w, http.StatusBadRequest, "bad_request", "source id is required")
			return
		}
		ip := clientIP(r)
		ua := r.UserAgent()
		src, secrets, err := ad.Store().RotateSecrets(r.Context(), tenantID, sourceID, claims.UserID, ip, ua)
		if err != nil {
			if errors.Is(err, authd.ErrSourceNotFound) {
				writeErr(w, http.StatusNotFound, "not_found", "source not found")
				return
			}
			logger.Error("rotate source secrets", "err", err, "tenant_id", tenantID, "source_id", sourceID, "actor", claims.UserID)
			writeErr(w, http.StatusInternalServerError, "internal", "rotate failed")
			return
		}
		logger.Info("source secrets rotated",
			"tenant_id", tenantID, "source_id", sourceID, "actor", claims.UserID)
		writeJSON(w, http.StatusOK, createOrRotateResponse{Source: src, Secrets: secrets})
	}
}
