// Package handlers provides HTTP handlers for the rdev API.
package handlers

import (
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"net/http"
	"strconv"

	"github.com/go-chi/chi/v5"
	"github.com/google/uuid"
	"github.com/orchard9/rdev/internal/auth"
	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/port"
	"github.com/orchard9/rdev/internal/validate"
	"github.com/orchard9/rdev/pkg/api"
)

// WebhookHandler handles webhook management endpoints.
type WebhookHandler struct {
	webhooks port.WebhookRepository
	projects port.ProjectRepository
}

// NewWebhookHandler creates a new webhook handler.
func NewWebhookHandler(webhooks port.WebhookRepository, projects port.ProjectRepository) *WebhookHandler {
	return &WebhookHandler{
		webhooks: webhooks,
		projects: projects,
	}
}

// Mount registers the webhook routes.
func (h *WebhookHandler) Mount(r api.Router) {
	r.Route("/projects/{id}/webhooks", func(r chi.Router) {
		// Write operations
		r.With(auth.RequireScope(auth.ScopeWebhookWrite, auth.ScopeAdmin)).Post("/", h.Create)
		r.With(auth.RequireScope(auth.ScopeWebhookWrite, auth.ScopeAdmin)).Put("/{webhookId}", h.Update)
		r.With(auth.RequireScope(auth.ScopeWebhookWrite, auth.ScopeAdmin)).Delete("/{webhookId}", h.Delete)

		// Read operations
		r.With(auth.RequireScope(auth.ScopeWebhookRead, auth.ScopeAdmin)).Get("/", h.List)
		r.With(auth.RequireScope(auth.ScopeWebhookRead, auth.ScopeAdmin)).Get("/{webhookId}", h.Get)
		r.With(auth.RequireScope(auth.ScopeWebhookRead, auth.ScopeAdmin)).Get("/{webhookId}/deliveries", h.GetDeliveries)
	})
}

// CreateWebhookRequest is the request body for POST /projects/{id}/webhooks.
type CreateWebhookRequest struct {
	URL    string   `json:"url"`
	Events []string `json:"events"`
	Secret string   `json:"secret,omitempty"` // If empty, one will be generated
}

// CreateWebhookResponse is the response for POST /projects/{id}/webhooks.
type CreateWebhookResponse struct {
	Webhook *WebhookDTO `json:"webhook"`
	Secret  string      `json:"secret"` // Only returned on creation
}

// WebhookDTO is the data transfer object for webhooks.
type WebhookDTO struct {
	ID        string   `json:"id"`
	ProjectID string   `json:"project_id"`
	URL       string   `json:"url"`
	Events    []string `json:"events"`
	Enabled   bool     `json:"enabled"`
	HasSecret bool     `json:"has_secret"`
	CreatedAt string   `json:"created_at"`
	UpdatedAt string   `json:"updated_at"`
}

// toDTO converts a domain.Webhook to a WebhookDTO.
func toDTO(w *domain.Webhook) *WebhookDTO {
	events := make([]string, len(w.Events))
	for i, e := range w.Events {
		events[i] = string(e)
	}
	return &WebhookDTO{
		ID:        string(w.ID),
		ProjectID: w.ProjectID,
		URL:       w.URL,
		Events:    events,
		Enabled:   w.Enabled,
		HasSecret: w.HasSecret(),
		CreatedAt: w.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
		UpdatedAt: w.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
	}
}

// Create creates a new webhook.
// POST /projects/{id}/webhooks
func (h *WebhookHandler) Create(w http.ResponseWriter, r *http.Request) {
	projectID := chi.URLParam(r, "id")

	// Check project exists
	exists, err := h.projects.Exists(r.Context(), domain.ProjectID(projectID))
	if err != nil {
		api.WriteInternalError(w, r, "failed to check project")
		return
	}
	if !exists {
		api.WriteNotFound(w, r, fmt.Sprintf("project not found: %s", projectID))
		return
	}

	// Parse request
	var req CreateWebhookRequest
	if err := api.DecodeJSON(r, &req); err != nil {
		api.WriteBadRequest(w, r, "invalid request body")
		return
	}

	// Validate required fields
	if req.URL == "" {
		api.WriteBadRequest(w, r, "url is required")
		return
	}
	if len(req.Events) == 0 {
		api.WriteBadRequest(w, r, "at least one event type is required")
		return
	}
	if err := validate.HTTPURL(req.URL, "url"); err != nil {
		api.WriteBadRequest(w, r, "url must be a valid HTTP or HTTPS URL")
		return
	}
	events := make([]domain.WebhookEventType, len(req.Events))
	for i, e := range req.Events {
		eventType := domain.WebhookEventType(e)
		if !eventType.IsValid() {
			api.WriteBadRequest(w, r, fmt.Sprintf("invalid event type: %s", e))
			return
		}
		events[i] = eventType
	}

	// Generate secret if not provided
	secret := req.Secret
	if secret == "" {
		secretBytes := make([]byte, 32)
		if _, err := rand.Read(secretBytes); err != nil {
			api.WriteInternalError(w, r, "failed to generate secret")
			return
		}
		secret = hex.EncodeToString(secretBytes)
	}

	// Create webhook
	webhook := &domain.Webhook{
		ID:        domain.WebhookID(uuid.New().String()),
		ProjectID: projectID,
		URL:       req.URL,
		Secret:    secret,
		Events:    events,
		Enabled:   true,
	}

	if err := h.webhooks.Create(r.Context(), webhook); err != nil {
		api.WriteInternalError(w, r, "failed to create webhook")
		return
	}

	api.WriteCreated(w, r, CreateWebhookResponse{
		Webhook: toDTO(webhook),
		Secret:  secret, // Only returned on creation
	})
}

// List returns all webhooks for a project.
// GET /projects/{id}/webhooks
func (h *WebhookHandler) List(w http.ResponseWriter, r *http.Request) {
	projectID := chi.URLParam(r, "id")

	// Check project exists
	exists, err := h.projects.Exists(r.Context(), domain.ProjectID(projectID))
	if err != nil {
		api.WriteInternalError(w, r, "failed to check project")
		return
	}
	if !exists {
		api.WriteNotFound(w, r, fmt.Sprintf("project not found: %s", projectID))
		return
	}

	webhooks, err := h.webhooks.ListByProject(r.Context(), projectID)
	if err != nil {
		api.WriteInternalError(w, r, "failed to list webhooks")
		return
	}

	dtos := make([]*WebhookDTO, len(webhooks))
	for i, wh := range webhooks {
		dtos[i] = toDTO(wh)
	}

	api.WriteSuccess(w, r, map[string]any{
		"webhooks": dtos,
		"total":    len(dtos),
	})
}

// Get returns a specific webhook.
// GET /projects/{id}/webhooks/{webhookId}
func (h *WebhookHandler) Get(w http.ResponseWriter, r *http.Request) {
	projectID := chi.URLParam(r, "id")
	webhookID := chi.URLParam(r, "webhookId")

	// Check project exists
	exists, err := h.projects.Exists(r.Context(), domain.ProjectID(projectID))
	if err != nil {
		api.WriteInternalError(w, r, "failed to check project")
		return
	}
	if !exists {
		api.WriteNotFound(w, r, fmt.Sprintf("project not found: %s", projectID))
		return
	}

	webhook, err := h.webhooks.GetByID(r.Context(), domain.WebhookID(webhookID))
	if err != nil {
		if errors.Is(err, domain.ErrWebhookNotFound) {
			api.WriteNotFound(w, r, fmt.Sprintf("webhook not found: %s", webhookID))
			return
		}
		api.WriteInternalError(w, r, "failed to get webhook")
		return
	}

	// Verify webhook belongs to project
	if webhook.ProjectID != projectID {
		api.WriteNotFound(w, r, fmt.Sprintf("webhook not found: %s", webhookID))
		return
	}

	api.WriteSuccess(w, r, toDTO(webhook))
}

// UpdateWebhookRequest is the request body for PUT /projects/{id}/webhooks/{webhookId}.
type UpdateWebhookRequest struct {
	URL     string   `json:"url,omitempty"`
	Events  []string `json:"events,omitempty"`
	Secret  string   `json:"secret,omitempty"`
	Enabled *bool    `json:"enabled,omitempty"`
}

// Update updates a webhook.
// PUT /projects/{id}/webhooks/{webhookId}
func (h *WebhookHandler) Update(w http.ResponseWriter, r *http.Request) {
	projectID := chi.URLParam(r, "id")
	webhookID := chi.URLParam(r, "webhookId")

	// Check project exists
	exists, err := h.projects.Exists(r.Context(), domain.ProjectID(projectID))
	if err != nil {
		api.WriteInternalError(w, r, "failed to check project")
		return
	}
	if !exists {
		api.WriteNotFound(w, r, fmt.Sprintf("project not found: %s", projectID))
		return
	}

	// Get existing webhook
	webhook, err := h.webhooks.GetByID(r.Context(), domain.WebhookID(webhookID))
	if err != nil {
		if errors.Is(err, domain.ErrWebhookNotFound) {
			api.WriteNotFound(w, r, fmt.Sprintf("webhook not found: %s", webhookID))
			return
		}
		api.WriteInternalError(w, r, "failed to get webhook")
		return
	}

	// Verify webhook belongs to project
	if webhook.ProjectID != projectID {
		api.WriteNotFound(w, r, fmt.Sprintf("webhook not found: %s", webhookID))
		return
	}

	// Parse request
	var req UpdateWebhookRequest
	if err := api.DecodeJSON(r, &req); err != nil {
		api.WriteBadRequest(w, r, "invalid request body")
		return
	}

	// Update fields
	if req.URL != "" {
		if err := validate.HTTPURL(req.URL, "url"); err != nil {
			api.WriteBadRequest(w, r, "url must be a valid HTTP or HTTPS URL")
			return
		}
		webhook.URL = req.URL
	}

	if len(req.Events) > 0 {
		events := make([]domain.WebhookEventType, len(req.Events))
		for i, e := range req.Events {
			eventType := domain.WebhookEventType(e)
			if !eventType.IsValid() {
				api.WriteBadRequest(w, r, fmt.Sprintf("invalid event type: %s", e))
				return
			}
			events[i] = eventType
		}
		webhook.Events = events
	}

	if req.Secret != "" {
		webhook.Secret = req.Secret
	}

	if req.Enabled != nil {
		webhook.Enabled = *req.Enabled
	}

	if err := h.webhooks.Update(r.Context(), webhook); err != nil {
		api.WriteInternalError(w, r, "failed to update webhook")
		return
	}

	api.WriteSuccess(w, r, toDTO(webhook))
}

// Delete deletes a webhook.
// DELETE /projects/{id}/webhooks/{webhookId}
func (h *WebhookHandler) Delete(w http.ResponseWriter, r *http.Request) {
	projectID := chi.URLParam(r, "id")
	webhookID := chi.URLParam(r, "webhookId")

	// Check project exists
	exists, err := h.projects.Exists(r.Context(), domain.ProjectID(projectID))
	if err != nil {
		api.WriteInternalError(w, r, "failed to check project")
		return
	}
	if !exists {
		api.WriteNotFound(w, r, fmt.Sprintf("project not found: %s", projectID))
		return
	}

	// Verify webhook belongs to project
	webhook, err := h.webhooks.GetByID(r.Context(), domain.WebhookID(webhookID))
	if err != nil {
		if errors.Is(err, domain.ErrWebhookNotFound) {
			api.WriteNotFound(w, r, fmt.Sprintf("webhook not found: %s", webhookID))
			return
		}
		api.WriteInternalError(w, r, "failed to get webhook")
		return
	}

	if webhook.ProjectID != projectID {
		api.WriteNotFound(w, r, fmt.Sprintf("webhook not found: %s", webhookID))
		return
	}

	if err := h.webhooks.Delete(r.Context(), domain.WebhookID(webhookID)); err != nil {
		if errors.Is(err, domain.ErrWebhookNotFound) {
			api.WriteNotFound(w, r, fmt.Sprintf("webhook not found: %s", webhookID))
			return
		}
		api.WriteInternalError(w, r, "failed to delete webhook")
		return
	}

	api.WriteSuccess(w, r, map[string]any{
		"id":      webhookID,
		"deleted": true,
	})
}

// DeliveryDTO is the data transfer object for webhook deliveries.
type DeliveryDTO struct {
	ID             string `json:"id"`
	WebhookID      string `json:"webhook_id"`
	EventType      string `json:"event_type"`
	Payload        string `json:"payload"`
	ResponseStatus int    `json:"response_status,omitempty"`
	ResponseBody   string `json:"response_body,omitempty"`
	DeliveredAt    string `json:"delivered_at"`
	Success        bool   `json:"success"`
	RetryCount     int    `json:"retry_count"`
	ErrorMessage   string `json:"error_message,omitempty"`
}

// GetDeliveries returns delivery history for a webhook.
// GET /projects/{id}/webhooks/{webhookId}/deliveries
func (h *WebhookHandler) GetDeliveries(w http.ResponseWriter, r *http.Request) {
	projectID := chi.URLParam(r, "id")
	webhookID := chi.URLParam(r, "webhookId")

	// Check project exists
	exists, err := h.projects.Exists(r.Context(), domain.ProjectID(projectID))
	if err != nil {
		api.WriteInternalError(w, r, "failed to check project")
		return
	}
	if !exists {
		api.WriteNotFound(w, r, fmt.Sprintf("project not found: %s", projectID))
		return
	}

	// Verify webhook belongs to project
	webhook, err := h.webhooks.GetByID(r.Context(), domain.WebhookID(webhookID))
	if err != nil {
		if errors.Is(err, domain.ErrWebhookNotFound) {
			api.WriteNotFound(w, r, fmt.Sprintf("webhook not found: %s", webhookID))
			return
		}
		api.WriteInternalError(w, r, "failed to get webhook")
		return
	}

	if webhook.ProjectID != projectID {
		api.WriteNotFound(w, r, fmt.Sprintf("webhook not found: %s", webhookID))
		return
	}

	// Parse query params
	filters := domain.DefaultWebhookDeliveryFilters()

	if eventType := r.URL.Query().Get("event_type"); eventType != "" {
		et := domain.WebhookEventType(eventType)
		filters.EventType = &et
	}

	if successStr := r.URL.Query().Get("success"); successStr != "" {
		success := successStr == "true"
		filters.Success = &success
	}

	if limit := r.URL.Query().Get("limit"); limit != "" {
		if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 1000 {
			filters.Limit = l
		}
	}

	if offset := r.URL.Query().Get("offset"); offset != "" {
		if o, err := strconv.Atoi(offset); err == nil && o >= 0 {
			filters.Offset = o
		}
	}

	deliveries, err := h.webhooks.GetDeliveries(r.Context(), domain.WebhookID(webhookID), filters)
	if err != nil {
		api.WriteInternalError(w, r, "failed to get deliveries")
		return
	}

	dtos := make([]*DeliveryDTO, len(deliveries))
	for i, d := range deliveries {
		dtos[i] = &DeliveryDTO{
			ID:             string(d.ID),
			WebhookID:      string(d.WebhookID),
			EventType:      string(d.EventType),
			Payload:        d.Payload,
			ResponseStatus: d.ResponseStatus,
			ResponseBody:   d.ResponseBody,
			DeliveredAt:    d.DeliveredAt.Format("2006-01-02T15:04:05Z07:00"),
			Success:        d.Success,
			RetryCount:     d.RetryCount,
			ErrorMessage:   d.ErrorMessage,
		}
	}

	api.WriteSuccess(w, r, map[string]any{
		"deliveries": dtos,
		"total":      len(dtos),
		"limit":      filters.Limit,
		"offset":     filters.Offset,
	})
}
