package handler

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"os"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/warmbly/warmbly/internal/infrastructure/kafka"
	"github.com/warmbly/warmbly/internal/repository"
)

// Internal worker bootstrap + config endpoints. A worker process starts with
// a tiny envelope on disk and pulls everything else from here on boot:
//
//	GET  /api/v1/worker/config       -> WorkerConfig JSON
//	POST /api/v1/worker/heartbeat    -> 204, auto-registers a new worker on
//	                                    first contact
//
// Auth: shared bearer token (INTERNAL_API_TOKEN). Future upgrade is per-worker
// JWTs minted at registration time so tier comes from token claims rather
// than the heartbeat body.

type WorkerEgressConfig struct {
	ID       uuid.UUID `json:"id"`
	BindIP   string    `json:"bind_ip"`
	Hostname string    `json:"hostname"`
	Tier     string    `json:"tier"`
	Tags     []string  `json:"tags,omitempty"`
}

type WorkerKafkaConfig struct {
	Bootstrap   string `json:"bootstrap"`
	SASLUser    string `json:"sasl_user,omitempty"`
	SASLPass    string `json:"sasl_pass,omitempty"`
	SchemaURL   string `json:"schema_url,omitempty"`
	SchemaKey   string `json:"schema_key,omitempty"`
	SchemaSec   string `json:"schema_secret,omitempty"`
	WorkerTopic string `json:"worker_topic"`
}

type WorkerStorageConfig struct {
	EncryptedKeysProvider string `json:"encrypted_keys_provider"`
	EncryptedKeysBackend  string `json:"encrypted_keys_backend_url,omitempty"`
}

type WorkerConfig struct {
	WorkerID  uuid.UUID            `json:"worker_id"`
	BindIP    string               `json:"bind_ip,omitempty"`
	Tag       string               `json:"tag,omitempty"`
	Egresses  []WorkerEgressConfig `json:"egresses"`
	Kafka     WorkerKafkaConfig    `json:"kafka"`
	Storage   WorkerStorageConfig  `json:"storage"`
	EventBus  string               `json:"event_bus_provider"`
	BlobStore string               `json:"blob_store_provider"`
}

func (h *Handler) InternalWorkerConfig(c *gin.Context) {
	idParam := c.Query("worker_id")
	id, err := uuid.Parse(idParam)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "valid worker_id query param required"})
		return
	}
	bindIP := c.Query("bind_ip")
	tag := c.Query("tag")

	cfg := WorkerConfig{
		WorkerID: id,
		BindIP:   bindIP,
		Tag:      tag,
		Egresses: []WorkerEgressConfig{
			{
				ID:       id,
				BindIP:   bindIP,
				Hostname: tag,
				Tier:     "shared",
			},
		},
		Kafka: WorkerKafkaConfig{
			Bootstrap:   envOr("KAFKA_BOOTSTRAP_SERVERS", ""),
			SASLUser:    envOr("KAFKA_SASL_USERNAME", ""),
			SASLPass:    envOr("KAFKA_SASL_PASSWORD", ""),
			SchemaURL:   envOr("SCHEMA_REGISTRY_URL", ""),
			SchemaKey:   envOr("SCHEMA_REGISTRY_KEY", ""),
			SchemaSec:   envOr("SCHEMA_REGISTRY_SECRET", ""),
			WorkerTopic: kafka.GetWorkerTopic(id.String()),
		},
		Storage: WorkerStorageConfig{
			EncryptedKeysProvider: envOr("ENCRYPTED_KEYS_PROVIDER", "http"),
			EncryptedKeysBackend:  envOr("ENCRYPTED_KEYS_BACKEND_URL", ""),
		},
		EventBus:  envOr("EVENTBUS_PROVIDER", "kafka"),
		BlobStore: envOr("BLOB_PROVIDER", "s3"),
	}
	c.JSON(http.StatusOK, cfg)
}

// HeartbeatPayload is what a worker sends on every heartbeat. The first
// heartbeat from an unknown WorkerID triggers auto-registration in the
// workers table; subsequent heartbeats just refresh last_seen.
type HeartbeatPayload struct {
	WorkerID   string `json:"worker_id"`
	BindIP     string `json:"bind_ip"`
	Tier       string `json:"tier,omitempty"`        // shared_free | shared_premium (dedicated is rejected; allocated by the control plane)
	EgressKind string `json:"egress_kind,omitempty"` // cold_smtp | oauth_api | warmup_only
	// Stopping is set on the farewell beat a worker sends as it shuts down, so
	// the row goes inactive at once instead of staying selectable until its
	// heartbeat ages out. Placement would otherwise keep handing work to a
	// process that is already gone.
	Stopping bool `json:"stopping,omitempty"`
	// Booted is set on the first beat of a fresh process. A worker holds its
	// mailboxes in memory only, so the backend reloads every mailbox assigned
	// to it right away; until then each send to it fails with "not found".
	Booted bool `json:"booted,omitempty"`
}

func (h *Handler) InternalWorkerHeartbeat(c *gin.Context) {
	body, err := io.ReadAll(c.Request.Body)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "read body"})
		return
	}
	var p HeartbeatPayload
	if err := json.Unmarshal(body, &p); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "decode body"})
		return
	}
	id, err := uuid.Parse(p.WorkerID)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "valid worker_id required"})
		return
	}
	if p.BindIP == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "bind_ip required"})
		return
	}
	// A worker may only register as a shared tier. Dedicated capacity is
	// allocated by the control plane (by promoting a spare shared worker), so
	// a worker must never self-designate as dedicated. A blank tier is fine —
	// it maps to the shared-premium default.
	if !repository.IsClientRequestableTier(p.Tier) {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": "dedicated tier cannot be requested by a worker; dedicated capacity is allocated automatically by the control plane",
			"code":  "tier_not_allowed",
		})
		return
	}
	if h.WorkerRepo == nil {
		// Worker repository not wired (e.g. tests). Treat as 204 noop.
		c.Status(http.StatusNoContent)
		return
	}
	if p.Stopping {
		if err := h.WorkerRepo.DeactivateWorker(c.Request.Context(), id); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.Status(http.StatusNoContent)
		return
	}
	if err := h.WorkerRepo.UpsertOnHeartbeat(c.Request.Context(), id, p.BindIP, p.Tier, p.EgressKind); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	if p.Booted && h.EmailService != nil {
		// Off the request: the reload publishes one ADD_EMAIL per mailbox.
		go func() {
			ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
			defer cancel()
			h.EmailService.ReloadWorkerAccounts(ctx, id)
		}()
	}
	c.Status(http.StatusNoContent)
}

func envOr(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}
