package webhook

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"text/template"

	"github.com/artifacthub/hub/internal/handlers/helpers"
	"github.com/artifacthub/hub/internal/httpw"
	"github.com/artifacthub/hub/internal/hub"
	"github.com/artifacthub/hub/internal/notification"
	"github.com/go-chi/chi/v5"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
)

// Handlers represents a group of http handlers in charge of handling webhooks
// operations.
type Handlers struct {
	webhookManager hub.WebhookManager
	logger         zerolog.Logger
	hc             hub.HTTPClient
}

// NewHandlers creates a new Handlers instance.
func NewHandlers(webhookManager hub.WebhookManager, hc hub.HTTPClient) *Handlers {
	return &Handlers{
		webhookManager: webhookManager,
		logger:         log.With().Str("handlers", "webhook").Logger(),
		hc:             hc,
	}
}

// Add is an http handler that adds the provided webhook to the database.
func (h *Handlers) Add(w http.ResponseWriter, r *http.Request) {
	orgName := chi.URLParam(r, "orgName")
	wh := &hub.Webhook{}
	if err := json.NewDecoder(r.Body).Decode(&wh); err != nil {
		h.logger.Error().Err(err).Str("method", "Add").Msg(hub.ErrInvalidInput.Error())
		helpers.RenderErrorJSON(w, hub.ErrInvalidInput)
		return
	}
	if err := h.webhookManager.Add(r.Context(), orgName, wh); err != nil {
		h.logger.Error().Err(err).Str("method", "Add").Send()
		helpers.RenderErrorJSON(w, err)
		return
	}
	w.WriteHeader(http.StatusCreated)
}

// Delete is an http handler that deletes the provided webhook from the database.
func (h *Handlers) Delete(w http.ResponseWriter, r *http.Request) {
	webhookID := chi.URLParam(r, "webhookID")
	if err := h.webhookManager.Delete(r.Context(), webhookID); err != nil {
		h.logger.Error().Err(err).Str("method", "Delete").Send()
		helpers.RenderErrorJSON(w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// Get is an http handler that returns the requested webhook.
func (h *Handlers) Get(w http.ResponseWriter, r *http.Request) {
	webhookID := chi.URLParam(r, "webhookID")
	dataJSON, err := h.webhookManager.GetJSON(r.Context(), webhookID)
	if err != nil {
		h.logger.Error().Err(err).Str("method", "Get").Send()
		helpers.RenderErrorJSON(w, err)
		return
	}
	helpers.RenderJSON(w, dataJSON, 0, http.StatusOK)
}

// GetOwnedByOrg is an http handler that returns the webhooks owned by the
// organization provided. The user doing the request must belong to the
// organization.
func (h *Handlers) GetOwnedByOrg(w http.ResponseWriter, r *http.Request) {
	orgName := chi.URLParam(r, "orgName")
	p, err := helpers.GetPagination(r.URL.Query(), helpers.PaginationDefaultLimit, helpers.PaginationMaxLimit)
	if err != nil {
		err = fmt.Errorf("%w: %w", hub.ErrInvalidInput, err)
		h.logger.Error().Err(err).Str("query", r.URL.RawQuery).Str("method", "GetOwnedByOrg").Send()
		helpers.RenderErrorJSON(w, err)
		return
	}
	result, err := h.webhookManager.GetOwnedByOrgJSON(r.Context(), orgName, p)
	if err != nil {
		h.logger.Error().Err(err).Str("method", "GetOwnedByOrg").Send()
		helpers.RenderErrorJSON(w, err)
		return
	}
	w.Header().Set(helpers.PaginationTotalCount, strconv.Itoa(result.TotalCount))
	helpers.RenderJSON(w, result.Data, 0, http.StatusOK)
}

// GetOwnedByUser is an http handler that returns the webhooks owned by the
// user doing the request.
func (h *Handlers) GetOwnedByUser(w http.ResponseWriter, r *http.Request) {
	p, err := helpers.GetPagination(r.URL.Query(), helpers.PaginationDefaultLimit, helpers.PaginationMaxLimit)
	if err != nil {
		err = fmt.Errorf("%w: %w", hub.ErrInvalidInput, err)
		h.logger.Error().Err(err).Str("query", r.URL.RawQuery).Str("method", "GetOwnedByUser").Send()
		helpers.RenderErrorJSON(w, err)
		return
	}
	result, err := h.webhookManager.GetOwnedByUserJSON(r.Context(), p)
	if err != nil {
		h.logger.Error().Err(err).Str("method", "GetOwnedByUser").Send()
		helpers.RenderErrorJSON(w, err)
		return
	}
	w.Header().Set(helpers.PaginationTotalCount, strconv.Itoa(result.TotalCount))
	helpers.RenderJSON(w, result.Data, 0, http.StatusOK)
}

// TriggerTest is an http handler used to test a webhook before adding or
// updating it.
func (h *Handlers) TriggerTest(w http.ResponseWriter, r *http.Request) {
	// Read webhook from request body
	wh := &hub.Webhook{}
	if err := json.NewDecoder(r.Body).Decode(&wh); err != nil {
		helpers.RenderErrorJSON(w, hub.ErrInvalidInput)
		return
	}

	// Prepare payload
	var tmpl *template.Template
	if wh.Template != "" {
		var err error
		tmpl, err = template.New("").Parse(wh.Template)
		if err != nil {
			err = fmt.Errorf("error parsing template: %w", err)
			helpers.RenderErrorWithCodeJSON(w, err, http.StatusBadRequest)
			return
		}
	} else {
		tmpl = notification.DefaultWebhookPayloadTmpl
	}
	var payload bytes.Buffer
	if err := tmpl.Execute(&payload, webhookTestTemplateData); err != nil {
		err = fmt.Errorf("error executing template: %w", err)
		helpers.RenderErrorWithCodeJSON(w, err, http.StatusBadRequest)
		return
	}

	// Call webhook endpoint
	req, err := httpw.NewRequest("POST", wh.URL, &payload)
	if err != nil {
		helpers.RenderErrorWithCodeJSON(w, err, http.StatusBadRequest)
		return
	}
	contentType := wh.ContentType
	if contentType == "" {
		contentType = notification.DefaultPayloadContentType
	}
	req.Header.Set("Content-Type", contentType)
	req.Header.Set("X-ArtifactHub-Secret", wh.Secret)
	resp, err := h.hc.Do(req)
	if err != nil {
		err = fmt.Errorf("error doing request: %w", err)
		helpers.RenderErrorWithCodeJSON(w, err, http.StatusBadRequest)
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 400 {
		err := fmt.Errorf("received unexpected status code: %d", resp.StatusCode)
		helpers.RenderErrorWithCodeJSON(w, err, http.StatusBadRequest)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// Update is an http handler that updates the provided webhook in the database.
func (h *Handlers) Update(w http.ResponseWriter, r *http.Request) {
	wh := &hub.Webhook{}
	if err := json.NewDecoder(r.Body).Decode(&wh); err != nil {
		h.logger.Error().Err(err).Str("method", "Update").Msg(hub.ErrInvalidInput.Error())
		helpers.RenderErrorJSON(w, hub.ErrInvalidInput)
		return
	}
	wh.WebhookID = chi.URLParam(r, "webhookID")
	if err := h.webhookManager.Update(r.Context(), wh); err != nil {
		h.logger.Error().Err(err).Str("method", "Update").Send()
		helpers.RenderErrorJSON(w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// webhookTestTemplateData represents the notification template data used by
// TriggerTest handler.
var webhookTestTemplateData = &hub.PackageNotificationTemplateData{
	BaseURL: "https://baseURL",
	Event: map[string]interface{}{
		"ID":   "00000000-0000-0000-0000-000000000001",
		"Kind": "package.new-release",
	},
	Package: map[string]interface{}{
		"Name":    "sample-package",
		"Version": "1.0.0",
		"URL":     "https://artifacthub.io/packages/helm/artifacthub/sample-package/1.0.0",
		"Changes": []*hub.Change{
			{
				Description: "Cool feature",
			},
			{
				Description: "Bug fixed",
			},
		},
		"ContainsSecurityUpdates": true,
		"Prerelease":              true,
		"Repository": map[string]interface{}{
			"Kind":      "helm",
			"Name":      "repo1",
			"Publisher": "org1",
		},
	},
}
