package queryprogress

import (
	"fmt"
	"sync"

	"github.com/ClickHouse/clickhouse-go/v2"
	"github.com/google/uuid"
	"go.signoz.io/signoz/pkg/query-service/model"
	v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
	"go.uber.org/zap"
	"golang.org/x/exp/maps"
)

// tracks progress and manages subscriptions for all queries
type inMemoryQueryProgressTracker struct {
	queries map[string]*queryTracker
	lock    sync.RWMutex
}

func (tracker *inMemoryQueryProgressTracker) ReportQueryStarted(
	queryId string,
) (postQueryCleanup func(), err *model.ApiError) {
	tracker.lock.Lock()
	defer tracker.lock.Unlock()

	_, exists := tracker.queries[queryId]
	if exists {
		return nil, model.BadRequest(fmt.Errorf(
			"query %s already started", queryId,
		))
	}

	tracker.queries[queryId] = newQueryTracker(queryId)

	return func() {
		tracker.onQueryFinished(queryId)
	}, nil
}

func (tracker *inMemoryQueryProgressTracker) ReportQueryProgress(
	queryId string, chProgress *clickhouse.Progress,
) *model.ApiError {
	queryTracker, err := tracker.getQueryTracker(queryId)
	if err != nil {
		return err
	}

	queryTracker.handleProgressUpdate(chProgress)
	return nil
}

func (tracker *inMemoryQueryProgressTracker) SubscribeToQueryProgress(
	queryId string,
) (<-chan v3.QueryProgress, func(), *model.ApiError) {
	queryTracker, err := tracker.getQueryTracker(queryId)
	if err != nil {
		return nil, nil, err
	}

	return queryTracker.subscribe()
}

func (tracker *inMemoryQueryProgressTracker) onQueryFinished(
	queryId string,
) {
	tracker.lock.Lock()
	queryTracker := tracker.queries[queryId]
	if queryTracker != nil {
		delete(tracker.queries, queryId)
	}
	tracker.lock.Unlock()

	if queryTracker != nil {
		queryTracker.onFinished()
	}
}

func (tracker *inMemoryQueryProgressTracker) getQueryTracker(
	queryId string,
) (*queryTracker, *model.ApiError) {
	tracker.lock.RLock()
	defer tracker.lock.RUnlock()

	queryTracker := tracker.queries[queryId]
	if queryTracker == nil {
		return nil, model.NotFoundError(fmt.Errorf(
			"query %s doesn't exist", queryId,
		))
	}

	return queryTracker, nil
}

// Tracks progress and manages subscriptions for a single query
type queryTracker struct {
	queryId    string
	isFinished bool

	progress      *v3.QueryProgress
	subscriptions map[string]*queryProgressSubscription

	lock sync.Mutex
}

func newQueryTracker(queryId string) *queryTracker {
	return &queryTracker{
		queryId:       queryId,
		subscriptions: map[string]*queryProgressSubscription{},
	}
}

func (qt *queryTracker) handleProgressUpdate(p *clickhouse.Progress) {
	qt.lock.Lock()
	defer qt.lock.Unlock()

	if qt.isFinished {
		zap.L().Warn(
			"received clickhouse progress update for finished query",
			zap.String("queryId", qt.queryId), zap.Any("progress", p),
		)
		return
	}

	if qt.progress == nil {
		// This is the first update
		qt.progress = &v3.QueryProgress{}
	}
	updateQueryProgress(qt.progress, p)

	// broadcast latest state to all subscribers.
	for _, sub := range maps.Values(qt.subscriptions) {
		sub.send(*qt.progress)
	}
}

func (qt *queryTracker) subscribe() (
	<-chan v3.QueryProgress, func(), *model.ApiError,
) {
	qt.lock.Lock()
	defer qt.lock.Unlock()

	if qt.isFinished {
		return nil, nil, model.NotFoundError(fmt.Errorf(
			"query %s already finished", qt.queryId,
		))
	}

	subscriberId := uuid.NewString()
	subscription := newQueryProgressSubscription()
	qt.subscriptions[subscriberId] = subscription

	if qt.progress != nil {
		subscription.send(*qt.progress)
	}

	return subscription.ch, func() {
		qt.unsubscribe(subscriberId)
	}, nil
}

func (qt *queryTracker) unsubscribe(subscriberId string) {
	qt.lock.Lock()
	defer qt.lock.Unlock()

	if qt.isFinished {
		zap.L().Debug(
			"received unsubscribe request after query finished",
			zap.String("subscriber", subscriberId),
			zap.String("queryId", qt.queryId),
		)
		return
	}

	subscription := qt.subscriptions[subscriberId]
	if subscription != nil {
		subscription.close()
		delete(qt.subscriptions, subscriberId)
	}
}

func (qt *queryTracker) onFinished() {
	qt.lock.Lock()
	defer qt.lock.Unlock()

	if qt.isFinished {
		zap.L().Warn(
			"receiver query finish report after query finished",
			zap.String("queryId", qt.queryId),
		)
		return
	}

	for subId, sub := range qt.subscriptions {
		sub.close()
		delete(qt.subscriptions, subId)
	}

	qt.isFinished = true
}

type queryProgressSubscription struct {
	ch       chan v3.QueryProgress
	isClosed bool
	lock     sync.Mutex
}

func newQueryProgressSubscription() *queryProgressSubscription {
	ch := make(chan v3.QueryProgress, 1000)
	return &queryProgressSubscription{
		ch: ch,
	}
}

// Must not block or panic in any scenario
func (ch *queryProgressSubscription) send(progress v3.QueryProgress) {
	ch.lock.Lock()
	defer ch.lock.Unlock()

	if ch.isClosed {
		zap.L().Error(
			"can't send query progress: channel already closed.",
			zap.Any("progress", progress),
		)
		return
	}

	// subscription channels are expected to have big enough buffers to ensure
	// blocking while sending doesn't happen in the happy path
	select {
	case ch.ch <- progress:
		zap.L().Debug("published query progress", zap.Any("progress", progress))
	default:
		zap.L().Error(
			"couldn't publish query progress. dropping update.",
			zap.Any("progress", progress),
		)
	}
}

func (ch *queryProgressSubscription) close() {
	ch.lock.Lock()
	defer ch.lock.Unlock()

	if !ch.isClosed {
		close(ch.ch)
		ch.isClosed = true
	}
}

func updateQueryProgress(qp *v3.QueryProgress, chProgress *clickhouse.Progress) {
	qp.ReadRows += chProgress.Rows
	qp.ReadBytes += chProgress.Bytes
	qp.ElapsedMs += uint64(chProgress.Elapsed.Milliseconds())
}
