package daemon

import (
	"context"
	"errors"
	"strings"
	"sync"
)

var ErrRequestTrackerRequestExists = errors.New("daemon request tracker request already registered")

type RequestTracker[T any] struct {
	mu       sync.Locker
	ownedMu  sync.Mutex
	requests *map[string]chan T
}

func NewRequestTracker[T any]() *RequestTracker[T] {
	requests := make(map[string]chan T)
	tracker := &RequestTracker[T]{requests: &requests}
	tracker.mu = &tracker.ownedMu
	return tracker
}

func NewRequestTrackerWithStore[T any](mu sync.Locker, requests *map[string]chan T) *RequestTracker[T] {
	tracker := &RequestTracker[T]{requests: requests}
	if mu != nil {
		tracker.mu = mu
	} else {
		tracker.mu = &tracker.ownedMu
	}
	return tracker
}

func (tracker *RequestTracker[T]) Register(requestID string) (chan T, func(), error) {
	if tracker == nil {
		return nil, nil, errors.New("nil daemon request tracker")
	}
	requestID = strings.TrimSpace(requestID)
	resultCh := make(chan T, 1)
	tracker.mu.Lock()
	defer tracker.mu.Unlock()
	requests := tracker.ensureLocked()
	if _, exists := requests[requestID]; exists {
		return nil, nil, ErrRequestTrackerRequestExists
	}
	requests[requestID] = resultCh
	return resultCh, func() { tracker.Cancel(requestID) }, nil
}

func (tracker *RequestTracker[T]) Deliver(requestID string, result T) bool {
	if tracker == nil {
		return false
	}
	requestID = strings.TrimSpace(requestID)
	tracker.mu.Lock()
	requests := tracker.ensureLocked()
	resultCh := requests[requestID]
	tracker.mu.Unlock()
	if resultCh == nil {
		return false
	}
	select {
	case resultCh <- result:
	default:
	}
	return true
}

func (tracker *RequestTracker[T]) Cancel(requestID string) bool {
	if tracker == nil {
		return false
	}
	requestID = strings.TrimSpace(requestID)
	tracker.mu.Lock()
	requests := tracker.ensureLocked()
	_, exists := requests[requestID]
	delete(requests, requestID)
	tracker.mu.Unlock()
	return exists
}

func (tracker *RequestTracker[T]) FailAll(result T) int {
	if tracker == nil {
		return 0
	}
	tracker.mu.Lock()
	defer tracker.mu.Unlock()
	requests := tracker.ensureLocked()
	delivered := 0
	for _, resultCh := range requests {
		if resultCh == nil {
			continue
		}
		select {
		case resultCh <- result:
			delivered++
		default:
		}
	}
	return delivered
}

func (tracker *RequestTracker[T]) Wait(ctx context.Context, requestID string, send func() error) (T, error) {
	resultCh, cleanup, err := tracker.Register(requestID)
	if err != nil {
		var zero T
		return zero, err
	}
	defer cleanup()
	if send != nil {
		if err := send(); err != nil {
			var zero T
			return zero, err
		}
	}
	select {
	case <-ctx.Done():
		var zero T
		return zero, ctx.Err()
	case result := <-resultCh:
		return result, nil
	}
}

func (tracker *RequestTracker[T]) Len() int {
	if tracker == nil {
		return 0
	}
	tracker.mu.Lock()
	defer tracker.mu.Unlock()
	return len(tracker.ensureLocked())
}

func (tracker *RequestTracker[T]) ensureLocked() map[string]chan T {
	if tracker.requests == nil {
		requests := make(map[string]chan T)
		tracker.requests = &requests
	}
	if *tracker.requests == nil {
		*tracker.requests = make(map[string]chan T)
	}
	return *tracker.requests
}
