package worker

import (
	"context"
	"encoding/json"
	"errors"
	"log/slog"
	"net/http"
	"time"

	"wucher/internal/config"
	"wucher/internal/shared/pkg/metrics"
	"wucher/internal/queue"
)

type Runner struct {
	worker            *queue.Worker
	consumer          queue.QueueConsumer
	dispatcher        *OutboxDispatcher
	logger            *slog.Logger
	monitorAddr       string
	depthPollInterval time.Duration
	shutdownTimeout   time.Duration
	backend           string
	monitorServer     *http.Server
}

func NewRunner(
	cfg config.QueueConfig,
	consumer queue.QueueConsumer,
	serializer queue.MessageSerializer,
	handler queue.EmailJobHandler,
	idempotency queue.IdempotencyStore,
	dispatcher *OutboxDispatcher,
	logger *slog.Logger,
) *Runner {
	if logger == nil {
		logger = slog.Default()
	}
	metrics := queue.NewMetrics()
	health := queue.NewHealthReporter(cfg.WorkerHealthErrorThreshold)
	worker := queue.NewWorker(queue.WorkerConfig{
		Concurrency:      cfg.WorkerConcurrency,
		MaxBatchSize:     cfg.WorkerMaxBatchSize,
		ProcessTimeout:   cfg.WorkerProcessTimeout,
		ProcessingTTL:    cfg.ProcessingTTL,
		IdempotencyTTL:   cfg.IdempotencyTTL,
		PollErrorDelay:   cfg.WorkerBackoffMin,
		MetricsComponent: "email_worker",
	}, consumer, serializer, handler, queue.NewExponentialBackoffPolicy(cfg.WorkerBackoffMin, cfg.WorkerBackoffMax, cfg.WorkerPermanentFailureDelay), idempotency, metrics, health, logger)

	backend := ""
	if consumer != nil {
		backend = consumer.Backend()
	}
	return &Runner{
		worker:            worker,
		consumer:          consumer,
		dispatcher:        dispatcher,
		logger:            logger,
		monitorAddr:       cfg.WorkerMonitorAddr,
		depthPollInterval: cfg.WorkerDepthPollInterval,
		shutdownTimeout:   cfg.WorkerShutdownTimeout,
		backend:           backend,
	}
}

func (r *Runner) Start(ctx context.Context) error {
	if r.worker == nil {
		return errors.New("worker is required")
	}
	runCtx, cancelRun := context.WithCancel(ctx)
	defer cancelRun()
	if err := r.startMonitor(); err != nil {
		return err
	}
	pollerDone := make(chan struct{})
	go func() {
		defer close(pollerDone)
		r.runDepthPoller(runCtx)
	}()

	workerErrCh := make(chan error, 1)
	go func() {
		workerErrCh <- r.worker.Run(runCtx)
	}()

	var dispatcherErrCh chan error
	if r.dispatcher != nil {
		dispatcherErrCh = make(chan error, 1)
		go func() {
			dispatcherErrCh <- r.dispatcher.Run(runCtx)
		}()
	}

	var err error
	select {
	case err = <-workerErrCh:
		cancelRun()
		if dispatcherErrCh != nil {
			if dispatcherErr := <-dispatcherErrCh; err == nil {
				err = dispatcherErr
			}
		}
	case err = <-dispatcherErrCh:
		cancelRun()
		if workerErr := <-workerErrCh; err == nil {
			err = workerErr
		}
	}

	shutdownCtx, cancel := context.WithTimeout(context.Background(), r.shutdownTimeout)
	defer cancel()
	if r.monitorServer != nil {
		_ = r.monitorServer.Shutdown(shutdownCtx)
	}
	if r.consumer != nil {
		_ = r.consumer.Close()
	}
	<-pollerDone
	return err
}

func (r *Runner) startMonitor() error {
	if r.monitorAddr == "" {
		return nil
	}
	mux := http.NewServeMux()
	mux.HandleFunc("/health", r.handleHealth)
	mux.Handle("/metrics", metrics.PromHTTPHandler())
	r.monitorServer = &http.Server{
		Addr:              r.monitorAddr,
		Handler:           mux,
		ReadHeaderTimeout: 5 * time.Second,
	}
	go func() {
		r.logger.Info("worker monitor listening",
			slog.String("addr", r.monitorAddr),
			slog.String("backend", r.backend),
		)
		if err := r.monitorServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			r.logger.Error("worker monitor failed", slog.Any("error", err))
		}
	}()
	return nil
}

func (r *Runner) runDepthPoller(ctx context.Context) {
	if r.depthPollInterval <= 0 {
		return
	}
	ticker := time.NewTicker(r.depthPollInterval)
	defer ticker.Stop()
	for {
		refreshCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
		_ = r.worker.RefreshQueueDepth(refreshCtx)
		cancel()

		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
		}
	}
}

func (r *Runner) handleHealth(w http.ResponseWriter, _ *http.Request) {
	snapshot := struct {
		Backend string                 `json:"backend"`
		Health  queue.HealthSnapshot   `json:"health"`
		Metrics queue.MetricsSnapshot  `json:"metrics"`
		Outbox  *OutboxMetricsSnapshot `json:"outbox,omitempty"`
	}{
		Backend: r.backend,
		Health:  r.worker.Health().Snapshot(),
		Metrics: r.worker.Metrics().Snapshot(),
	}
	if r.dispatcher != nil {
		snapshot.Outbox = ptr(r.dispatcher.Metrics())
	}
	statusCode := http.StatusOK
	if snapshot.Health.Status == "degraded" {
		statusCode = http.StatusServiceUnavailable
	}
	writeJSON(w, statusCode, snapshot)
}

func writeJSON(w http.ResponseWriter, statusCode int, payload any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(statusCode)
	_ = json.NewEncoder(w).Encode(payload)
}

func ptr[T any](v T) *T {
	return &v
}
