package mail

import (
	"errors"
	"log/slog"
	"net/http"

	"github.com/go-chi/chi/v5"
	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/ultisuite/ulti-backend/internal/api/apiresponse"
	"github.com/ultisuite/ulti-backend/internal/api/apivalidate"
	"github.com/ultisuite/ulti-backend/internal/api/mail/sendguard"
	"github.com/ultisuite/ulti-backend/internal/api/middleware"
	"github.com/ultisuite/ulti-backend/internal/api/query"
	"github.com/ultisuite/ulti-backend/internal/filescan"
	"github.com/ultisuite/ulti-backend/internal/mail/credentials"
	"github.com/ultisuite/ulti-backend/internal/mail/limits"
	mailoauth "github.com/ultisuite/ulti-backend/internal/mail/oauth"
	"github.com/ultisuite/ulti-backend/internal/mail/storage"
	"github.com/ultisuite/ulti-backend/internal/securityaudit"
)

type Handler struct {
	svc         ServiceAPI
	mailSender  MailSender
	logger      *slog.Logger
	sendLimiter *sendguard.RateLimiter
	oauth       *mailoauth.Service
	appURL      string
	accountSync AccountSyncTrigger
}

// SetAccountSync wires the IMAP sync worker for on-demand account sync.
func (h *Handler) SetAccountSync(trigger AccountSyncTrigger) {
	h.accountSync = trigger
}

// SetDriveUploader wires Nextcloud export for mail attachments.
func (h *Handler) SetDriveUploader(uploader DriveUploader) {
	if s, ok := h.svc.(*Service); ok {
		s.SetDriveUploader(uploader)
	}
}

// SetFileScanner wires VirusTotal scanning for mail attachment uploads.
func (h *Handler) SetFileScanner(scanner *filescan.Scanner) {
	if s, ok := h.svc.(*Service); ok {
		s.SetFileScanner(scanner)
	}
}

func NewHandlerWithService(svc ServiceAPI) *Handler {
	return &Handler{
		svc:    svc,
		logger: slog.Default().With("component", "mail-api"),
	}
}

func NewHandler(
	db *pgxpool.Pool,
	audit *securityaudit.Logger,
	credentialManager *credentials.Manager,
	objectStorage *storage.Client,
	attachmentsBucket string,
	sendLimiter *sendguard.RateLimiter,
	oauthSvc *mailoauth.Service,
	appURL string,
	mailSender MailSender,
) *Handler {
	h := NewHandlerWithService(NewService(db, audit, credentialManager, objectStorage, attachmentsBucket))
	h.mailSender = mailSender
	h.sendLimiter = sendLimiter
	h.oauth = oauthSvc
	h.appURL = appURL
	return h
}

func (h *Handler) Routes() chi.Router {
	r := chi.NewRouter()

	r.Get("/settings", h.GetMailSettings)
	r.Patch("/settings", h.UpdateMailSettings)

	r.Get("/unified-folders", h.ListUnifiedFolders)
	r.Post("/unified-folders/reorder", h.ReorderUnifiedFolders)
	r.Post("/unified-folders", h.CreateUnifiedFolder)
	r.Put("/unified-folders/{folderID}", h.UpdateUnifiedFolder)
	r.Delete("/unified-folders/{folderID}", h.DeleteUnifiedFolder)

	r.Get("/accounts", h.ListAccounts)
	r.Post("/accounts", h.CreateAccount)
	r.Get("/accounts/discover", h.DiscoverAccountConfig)
	r.Post("/accounts/test", h.TestAccountConnection)
	r.Post("/accounts/{accountID}/test", h.TestStoredAccountConnection)
	r.Get("/accounts/oauth/providers", h.ListOAuthProviders)
	r.Post("/accounts/oauth/start", h.StartOAuthAccount)
	r.Get("/accounts/{accountID}", h.GetAccount)
	r.Put("/accounts/{accountID}", h.UpdateAccount)
	r.Delete("/accounts/{accountID}", h.DeleteAccount)
	r.Post("/accounts/{accountID}/resanitize-bodies", h.ResanitizeAccountBodies)
	r.Post("/accounts/{accountID}/sync", h.SyncAccountNow)
	r.Get("/accounts/{accountID}/identities", h.ListIdentities)
	r.Post("/accounts/{accountID}/identities", h.CreateIdentity)

	r.Get("/identities/{identityID}", h.GetIdentity)
	r.Put("/identities/{identityID}", h.UpdateIdentity)
	r.Delete("/identities/{identityID}", h.DeleteIdentity)

	r.Get("/signatures", h.ListSignatures)
	r.Post("/signatures", h.CreateSignature)
	r.Get("/signatures/{signatureID}", h.GetSignature)
	r.Put("/signatures/{signatureID}", h.UpdateSignature)
	r.Delete("/signatures/{signatureID}", h.DeleteSignature)

	r.Mount("/", h.FolderLabelRoutes())

	r.Get("/search", h.SearchMessages)

	r.Get("/drafts", h.ListDrafts)
	r.Post("/drafts", h.CreateDraft)
	r.Get("/drafts/{draftID}", h.GetDraft)
	r.Put("/drafts/{draftID}", h.UpdateDraft)
	r.Delete("/drafts/{draftID}", h.DeleteDraft)
	r.Post("/drafts/{draftID}/attachments", h.UploadDraftAttachment)
	r.Get("/drafts/{draftID}/attachments/{attachmentID}", h.DownloadDraftAttachment)
	r.Get("/drafts/{draftID}/attachments/{attachmentID}/inline", h.DownloadDraftAttachment)

	r.Get("/messages", h.ListMessages)
	r.Get("/messages/{messageID}/attachments", h.ListMessageAttachments)
	r.Get("/messages/{messageID}/attachments/cid-map", h.MessageAttachmentCIDMap)
	r.Post("/messages/{messageID}/attachments/reindex", h.ReindexMessageAttachments)
	r.Post("/messages/{messageID}/attachments", h.UploadMessageAttachment)
	r.Post("/messages/{messageID}/attachments/save-to-drive", h.SaveMessageAttachmentsToDrive)
	r.Post("/messages/{messageID}/attachments/{attachmentID}/save-to-drive", h.SaveAttachmentToDrive)
	r.Post("/messages/{messageID}/list-unsubscribe-mailto", h.SendListUnsubscribeMailto)
	r.Get("/messages/{messageID}", h.GetMessage)
	r.Put("/messages/{messageID}/labels", h.UpdateLabels)
	r.Put("/messages/{messageID}/flags", h.UpdateFlags)
	r.Delete("/messages/{messageID}", h.DeleteMessage)

	r.Get("/attachments/{attachmentID}", h.DownloadAttachment)
	r.Get("/attachments/{attachmentID}/inline", h.DownloadAttachment)

	r.Get("/threads/{threadID}", h.GetThread)

	r.Post("/send", h.SendMessage)

	r.Post("/outbox/{outboxID}/send-now", h.SendOutboxNow)
	r.Post("/outbox/{outboxID}/reschedule", h.RescheduleOutbox)
	r.Post("/outbox/{outboxID}/cancel", h.CancelScheduledOutbox)

	r.Get("/rules", h.ListRules)
	r.Post("/rules", h.CreateRule)
	r.Put("/rules/{ruleID}", h.UpdateRule)
	r.Delete("/rules/{ruleID}", h.DeleteRule)
	r.Post("/rules/simulate", h.SimulateRule)

	r.Get("/webhooks", h.ListWebhooks)
	r.Post("/webhooks", h.CreateWebhook)
	r.Post("/webhooks/preview", h.PreviewWebhookTemplate)
	r.Put("/webhooks/{webhookID}", h.UpdateWebhook)
	r.Delete("/webhooks/{webhookID}", h.DeleteWebhook)

	r.Get("/api-tokens", h.ListApiTokens)
	r.Post("/api-tokens", h.CreateApiToken)
	r.Delete("/api-tokens/{tokenID}", h.RevokeApiToken)

	return r
}

func (h *Handler) ListAccounts(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	params, err := query.ParseListRequest(r)
	if err != nil {
		apivalidate.WriteQueryError(w, r, err)
		return
	}

	result, err := h.svc.ListAccounts(r.Context(), claims.Sub, params)
	if err != nil {
		h.logger.Error("list accounts", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, result)
}

func (h *Handler) CreateAccount(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())

	var req createAccountRequest
	if err := apivalidate.DecodeJSON(w, r, maxAccountRequestBody, &req); err != nil {
		return
	}
	if verr := validateCreateAccount(&req); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}

	id, err := h.svc.CreateAccount(r.Context(), claims.Sub, &req)
	if err != nil {
		if errors.Is(err, ErrCredentialsUnavailable) {
			apiresponse.WriteError(w, r, http.StatusInternalServerError, apiresponse.CodeInternal, "credentials encryption unavailable", nil)
			return
		}
		h.logger.Error("create account", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusCreated, map[string]string{"id": id})
}

func (h *Handler) GetAccount(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	accountID := chi.URLParam(r, "accountID")
	if d := validateAccountUUID(accountID); d != nil {
		apivalidate.WriteNotFound(w, r, "not found")
		return
	}
	account, err := h.svc.GetAccount(r.Context(), claims.Sub, accountID)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		h.logger.Error("get account", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, account)
}

func (h *Handler) UpdateAccount(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	accountID := chi.URLParam(r, "accountID")
	if d := validateAccountUUID(accountID); d != nil {
		apivalidate.WriteNotFound(w, r, "not found")
		return
	}

	var req updateAccountRequest
	if err := apivalidate.DecodeJSON(w, r, maxAccountRequestBody, &req); err != nil {
		return
	}
	if verr := validateUpdateAccount(&req); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}

	if err := h.svc.UpdateAccount(r.Context(), claims.Sub, accountID, &req); err != nil {
		if errors.Is(err, ErrNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		if errors.Is(err, ErrUserNotProvisioned) {
			apiresponse.WriteError(w, r, http.StatusUnauthorized, apiresponse.CodeAuthUnauthorized, "user not provisioned", nil)
			return
		}
		if errors.Is(err, ErrCredentialsUnavailable) {
			apiresponse.WriteError(w, r, http.StatusInternalServerError, apiresponse.CodeInternal, "credentials encryption unavailable", nil)
			return
		}
		if errors.Is(err, ErrOAuthPasswordNotAllowed) {
			apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, "oauth accounts cannot use password credentials", nil)
			return
		}
		if errors.Is(err, ErrInvalidAccountCredentials) {
			apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, "username is required for password authentication", nil)
			return
		}
		h.logger.Error("update account", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) DeleteAccount(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	accountID := chi.URLParam(r, "accountID")
	if d := validateAccountUUID(accountID); d != nil {
		apivalidate.WriteNotFound(w, r, "not found")
		return
	}
	if err := h.svc.DeleteAccount(r.Context(), claims.Sub, accountID); err != nil {
		if errors.Is(err, ErrUserNotProvisioned) {
			apiresponse.WriteError(w, r, http.StatusUnauthorized, apiresponse.CodeAuthUnauthorized, "user not provisioned", nil)
			return
		}
		if errors.Is(err, ErrNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		h.logger.Error("delete account", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) ListMessages(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	params, err := query.ParseListRequest(r)
	if err != nil {
		apivalidate.WriteQueryError(w, r, err)
		return
	}

	filter := MessageListFilter{
		Folder:    r.URL.Query().Get("folder"),
		AccountID: r.URL.Query().Get("account_id"),
	}
	h.applyMailListScope(&filter, r)

	result, err := h.svc.ListMessages(r.Context(), claims.Sub, filter, params)
	if err != nil {
		h.logger.Error("list messages", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, result)
}

func (h *Handler) GetMessage(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	messageID := chi.URLParam(r, "messageID")
	if h.denyUnlessMessageInScope(w, r, messageID) {
		return
	}
	msg, err := h.svc.GetMessage(r.Context(), claims.Sub, messageID)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		h.logger.Error("get message", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, msg)
}

func (h *Handler) UpdateLabels(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())

	var req updateLabelsRequest
	if err := apivalidate.DecodeJSON(w, r, maxFlagsLabelsBody, &req); err != nil {
		return
	}
	if verr := validateUpdateLabels(&req); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}
	messageID := chi.URLParam(r, "messageID")
	if h.denyUnlessMessageInScope(w, r, messageID) {
		return
	}

	if err := h.svc.UpdateLabels(r.Context(), claims.Sub, messageID, req.Labels); err != nil {
		if errors.Is(err, ErrNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		h.logger.Error("update labels", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) UpdateFlags(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())

	var req updateFlagsRequest
	if err := apivalidate.DecodeJSON(w, r, maxFlagsLabelsBody, &req); err != nil {
		return
	}
	if verr := validateUpdateFlags(&req); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}
	messageID := chi.URLParam(r, "messageID")
	if h.denyUnlessMessageInScope(w, r, messageID) {
		return
	}

	if err := h.svc.UpdateFlags(r.Context(), claims.Sub, messageID, req.Flags); err != nil {
		if errors.Is(err, ErrNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		h.logger.Error("update flags", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) DeleteMessage(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	messageID := chi.URLParam(r, "messageID")
	if h.denyUnlessMessageInScope(w, r, messageID) {
		return
	}

	if err := h.svc.DeleteMessage(r.Context(), claims.Sub, messageID); err != nil {
		if errors.Is(err, ErrNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		h.logger.Error("delete message", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) SendListUnsubscribeMailto(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	messageID := chi.URLParam(r, "messageID")
	if h.denyUnlessMessageInScope(w, r, messageID) {
		return
	}

	if h.mailSender == nil {
		apiresponse.WriteError(w, r, http.StatusServiceUnavailable, apiresponse.CodeInternal, "mail send unavailable", nil)
		return
	}

	target, err := h.svc.SendMailtoListUnsubscribe(r.Context(), claims.Sub, messageID, h.mailSender)
	if err != nil {
		switch {
		case errors.Is(err, ErrNotFound):
			apivalidate.WriteNotFound(w, r, "not found")
		case errors.Is(err, ErrListUnsubscribeNoMailto):
			apiresponse.WriteError(w, r, http.StatusConflict, apiresponse.CodeInvalidRequest, err.Error(), nil)
		case errors.Is(err, ErrListUnsubscribeUnavailable):
			apiresponse.WriteError(w, r, http.StatusConflict, apiresponse.CodeInvalidRequest, "no mailto list-unsubscribe", nil)
		default:
			h.logger.Error("list-unsubscribe mailto send", "message_id", messageID, "error", err)
			apivalidate.WriteInternal(w, r)
		}
		return
	}

	apiresponse.WriteJSON(w, http.StatusOK, map[string]any{
		"sent":    true,
		"mailto":  target.Address,
		"subject": target.Subject,
	})
}

func (h *Handler) GetThread(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	threadID := chi.URLParam(r, "threadID")
	if h.denyUnlessThreadInScope(w, r, threadID) {
		return
	}
	result, err := h.svc.GetThread(r.Context(), claims.Sub, threadID, middleware.MailScopeAccountIDs(r.Context()))
	if err != nil {
		h.logger.Error("get thread", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, result)
}

func (h *Handler) SendMessage(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	userID, err := h.svc.ResolveUserID(r.Context(), claims.Sub)
	if err != nil {
		h.writeUserResolveError(w, r, err)
		return
	}

	if h.sendLimiter != nil {
		if err := h.sendLimiter.Allow(userID); err != nil {
			apiresponse.WriteError(w, r, http.StatusTooManyRequests, apiresponse.CodeRateLimited, "send rate limit exceeded", nil)
			return
		}
	}

	idempotencyKey, ok := normalizeIdempotencyKey(r.Header.Get("Idempotency-Key"))
	if !ok {
		apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(apivalidate.FieldDetail{
			Field: "Idempotency-Key", Message: "invalid",
		}))
		return
	}

	var req sendMessageRequest
	if err := apivalidate.DecodeJSON(w, r, limits.MaxSendRequestBodyBytes, &req); err != nil {
		return
	}
	req.IdempotencyKey = idempotencyKey
	if verr := validateSendMessage(&req); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}
	if middleware.DenyIfMailAccountOutOfScope(w, r, req.AccountID) {
		return
	}

	id, status, err := h.svc.SendMessage(r.Context(), userID, &req)
	if err != nil {
		if errors.Is(err, ErrAccountNotFound) {
			apivalidate.WriteNotFound(w, r, "account not found")
			return
		}
		h.logger.Error("send message", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusAccepted, map[string]string{"id": id, "status": status})
}

func (h *Handler) ListRules(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	params, err := query.ParseListRequest(r)
	if err != nil {
		apivalidate.WriteQueryError(w, r, err)
		return
	}

	result, err := h.svc.ListRules(r.Context(), claims.Sub, params)
	if err != nil {
		h.logger.Error("list rules", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, result)
}

func (h *Handler) CreateRule(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	userID, err := h.svc.ResolveUserID(r.Context(), claims.Sub)
	if err != nil {
		h.writeUserResolveError(w, r, err)
		return
	}

	var req createRuleRequest
	if err := apivalidate.DecodeJSON(w, r, maxRulesRequestBody, &req); err != nil {
		return
	}
	if verr := validateCreateRule(&req); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}

	id, err := h.svc.CreateRule(r.Context(), userID, &req)
	if err != nil {
		if errors.Is(err, ErrAccountNotFound) {
			apivalidate.WriteNotFound(w, r, "account not found")
			return
		}
		h.logger.Error("create rule", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusCreated, map[string]string{"id": id})
}

func (h *Handler) UpdateRule(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())

	var req updateRuleRequest
	if err := apivalidate.DecodeJSON(w, r, maxRulesRequestBody, &req); err != nil {
		return
	}
	if verr := validateUpdateRule(&req); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}

	if err := h.svc.UpdateRule(r.Context(), claims.Sub, chi.URLParam(r, "ruleID"), &req); err != nil {
		if errors.Is(err, ErrNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		h.logger.Error("update rule", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) DeleteRule(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())

	if err := h.svc.DeleteRule(r.Context(), claims.Sub, chi.URLParam(r, "ruleID")); err != nil {
		if errors.Is(err, ErrNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		h.logger.Error("delete rule", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) SimulateRule(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())

	var req simulateRuleRequest
	if err := apivalidate.DecodeJSON(w, r, maxRulesRequestBody, &req); err != nil {
		return
	}
	if verr := validateSimulateRule(&req); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}

	result, err := h.svc.SimulateRule(r.Context(), claims.Sub, &req)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		h.logger.Error("simulate rule", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, result)
}

func (h *Handler) ListWebhooks(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())
	params, err := query.ParseListRequest(r)
	if err != nil {
		apivalidate.WriteQueryError(w, r, err)
		return
	}

	result, err := h.svc.ListWebhooks(r.Context(), claims.Sub, params)
	if err != nil {
		h.logger.Error("list webhooks", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, result)
}

func (h *Handler) CreateWebhook(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())

	var req createWebhookRequest
	if err := apivalidate.DecodeJSON(w, r, maxWebhookRequestBody, &req); err != nil {
		return
	}
	method, maxRetries, verr := validateCreateWebhook(&req)
	if verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}

	id, err := h.svc.CreateWebhook(r.Context(), claims.Sub, &req, method, maxRetries)
	if err != nil {
		h.logger.Error("create webhook", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusCreated, map[string]string{"id": id})
}

func (h *Handler) UpdateWebhook(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())

	var req updateWebhookRequest
	if err := apivalidate.DecodeJSON(w, r, maxWebhookRequestBody, &req); err != nil {
		return
	}
	method, maxRetries, verr := validateUpdateWebhook(&req)
	if verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}

	if err := h.svc.UpdateWebhook(r.Context(), claims.Sub, chi.URLParam(r, "webhookID"), &req, method, maxRetries); err != nil {
		if errors.Is(err, ErrNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		h.logger.Error("update webhook", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) PreviewWebhookTemplate(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())

	var req previewWebhookRequest
	if err := apivalidate.DecodeJSON(w, r, maxWebhookRequestBody, &req); err != nil {
		return
	}
	if verr := validatePreviewWebhook(&req); verr != nil {
		apivalidate.WriteValidationError(w, r, verr)
		return
	}

	result, err := h.svc.PreviewWebhookTemplate(r.Context(), claims.Sub, &req)
	if err != nil {
		h.logger.Error("preview webhook template", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	apiresponse.WriteJSON(w, http.StatusOK, result)
}

func (h *Handler) DeleteWebhook(w http.ResponseWriter, r *http.Request) {
	claims := middleware.ClaimsFromContext(r.Context())

	if err := h.svc.DeleteWebhook(r.Context(), claims.Sub, chi.URLParam(r, "webhookID")); err != nil {
		if errors.Is(err, ErrNotFound) {
			apivalidate.WriteNotFound(w, r, "not found")
			return
		}
		h.logger.Error("delete webhook", "error", err)
		apivalidate.WriteInternal(w, r)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) writeUserResolveError(w http.ResponseWriter, r *http.Request, err error) {
	if errors.Is(err, ErrUserNotProvisioned) {
		apiresponse.WriteError(w, r, http.StatusUnauthorized, apiresponse.CodeAuthUnauthorized, "user not provisioned", nil)
		return
	}
	h.logger.Error("resolve user id", "error", err)
	apivalidate.WriteInternal(w, r)
}
