package serverruntime

import (
	"context"
	"database/sql"
	"errors"
	"sync"
	"time"

	"github.com/nexuscrm/backend/internal/host/adapters/apihost/middleware"
	database "github.com/nexuscrm/db"
	records "github.com/nexuscrm/records"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"github.com/rs/zerolog/log"
)

const (
	apiQueueObservationInterval = time.Hour
	apiQueueObservationTimeout  = 30 * time.Second
)

type apiQueueObservationSource struct {
	name       string
	timeout    time.Duration
	initialize func()
	refresh    func(context.Context, *sql.DB) error
}

type apiQueueObservationDBOpener func(database.PoolOptions) (*sql.DB, error)

var (
	apiQueueObservationLastSuccess = promauto.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: "nexuscrm_api_queue_observation_last_success_timestamp_seconds",
			Help: "Unix timestamp of the last successful exact API queue observation.",
		},
		[]string{"queue"},
	)
	apiQueueObservationStale = promauto.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: "nexuscrm_api_queue_observation_stale",
			Help: "Whether the cached API queue observation has never succeeded or the latest attempt failed.",
		},
		[]string{"queue"},
	)
	apiQueueObservationFailures = promauto.NewCounterVec(
		prometheus.CounterOpts{
			Name: "nexuscrm_api_queue_observation_failures_total",
			Help: "Total failed exact API queue observation attempts.",
		},
		[]string{"queue"},
	)
	apiQueueObservationDuration = promauto.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "nexuscrm_api_queue_observation_duration_seconds",
			Help:    "Duration of exact API queue observation attempts.",
			Buckets: prometheus.DefBuckets,
		},
		[]string{"queue"},
	)
)

func apiQueueObservationSources(connectorQueue *connectorQueueObservation) []apiQueueObservationSource {
	return []apiQueueObservationSource{
		{
			name:       "outbox",
			initialize: middleware.InitializeOutboxBacklogMetrics,
			refresh:    middleware.RefreshOutboxBacklogStats,
		},
		{
			name:       "rollup_delta",
			initialize: records.InitializeRollupDeltaBacklogMetrics,
			refresh:    records.RefreshRollupDeltaBacklogStats,
		},
		{
			name:       connectorQueueObservationSourceName,
			timeout:    connectorQueueObservationTimeout,
			initialize: connectorQueue.initialize,
			refresh:    connectorQueue.refresh,
		},
	}
}

// apiQueueObservationPool is a fixed L0 diagnostic budget. It is separate from
// the primary API pool so exact observations never consume a connection needed
// by business reads or writes.
func apiQueueObservationPool() database.PoolOptions {
	return database.PoolOptions{
		MaxOpenConns:    1,
		MaxIdleConns:    1,
		ConnMaxLifetime: 5 * time.Minute,
		ConnMaxIdleTime: 3 * time.Minute,
	}
}

func startAPIQueueObservationSampler(
	ctx context.Context,
	open apiQueueObservationDBOpener,
	connectorQueue *connectorQueueObservation,
) func() {
	return startAPIQueueObservationSamplerWithConfig(
		ctx,
		open,
		apiQueueObservationInterval,
		apiQueueObservationTimeout,
		apiQueueObservationSources(connectorQueue),
	)
}

func startAPIQueueObservationSamplerWithConfig(
	parent context.Context,
	open apiQueueObservationDBOpener,
	interval time.Duration,
	timeout time.Duration,
	sources []apiQueueObservationSource,
) func() {
	if parent == nil {
		parent = context.Background()
	}
	if interval <= 0 {
		interval = apiQueueObservationInterval
	}
	if timeout <= 0 {
		timeout = apiQueueObservationTimeout
	}
	initializeAPIQueueObservationSources(sources)

	ctx, cancel := context.WithCancel(parent)
	done := make(chan struct{})
	go func() {
		defer close(done)
		ticker := time.NewTicker(interval)
		defer ticker.Stop()
		runAPIQueueObservationSampler(ctx, open, timeout, sources, ticker.C)
	}()

	var stopOnce sync.Once
	return func() {
		stopOnce.Do(func() {
			cancel()
			<-done
		})
	}
}

func runAPIQueueObservationSampler(
	ctx context.Context,
	open apiQueueObservationDBOpener,
	timeout time.Duration,
	sources []apiQueueObservationSource,
	ticks <-chan time.Time,
) {
	var observationDB *sql.DB
	defer func() {
		if observationDB != nil {
			_ = observationDB.Close()
		}
	}()

	for {
		select {
		case <-ctx.Done():
			return
		case _, ok := <-ticks:
			if !ok {
				return
			}
		}

		if observationDB == nil {
			var err error
			if open == nil {
				err = errors.New("independent database opener is unavailable")
			} else {
				observationDB, err = open(apiQueueObservationPool())
				if err == nil && observationDB == nil {
					err = errors.New("independent database opener returned a nil pool")
				}
			}
			if err != nil {
				observationDB = nil
				for _, source := range sources {
					recordAPIQueueObservationFailure(source.name)
				}
				if ctx.Err() == nil {
					log.Debug().Err(err).Msg("API queue observation pool open failed")
				}
				continue
			}
		}

		for _, source := range sources {
			started := time.Now()
			sourceTimeout := timeout
			if source.timeout > 0 {
				sourceTimeout = source.timeout
			}
			sampleCtx, cancel := context.WithTimeout(ctx, sourceTimeout)
			err := source.refresh(sampleCtx, observationDB)
			cancel()
			duration := time.Since(started).Seconds()
			apiQueueObservationDuration.WithLabelValues(source.name).Observe(duration)
			if err != nil {
				recordAPIQueueObservationFailure(source.name)
				if ctx.Err() == nil {
					log.Debug().Err(err).Str("queue", source.name).Msg("API queue observation failed")
				}
			} else {
				apiQueueObservationLastSuccess.WithLabelValues(source.name).Set(float64(time.Now().Unix()))
				apiQueueObservationStale.WithLabelValues(source.name).Set(0)
			}
			if ctx.Err() != nil {
				return
			}
		}
	}
}

func initializeAPIQueueObservationSources(sources []apiQueueObservationSource) {
	for _, source := range sources {
		if source.initialize != nil {
			source.initialize()
		}
		apiQueueObservationLastSuccess.WithLabelValues(source.name).Set(0)
		apiQueueObservationStale.WithLabelValues(source.name).Set(1)
		apiQueueObservationFailures.WithLabelValues(source.name).Add(0)
	}
}

func recordAPIQueueObservationFailure(queue string) {
	apiQueueObservationFailures.WithLabelValues(queue).Inc()
	apiQueueObservationStale.WithLabelValues(queue).Set(1)
}
