// Copyright 2021 Molecula Corp. All rights reserved.
package pg

import (
	"context"
	"encoding/binary"
	"io"
	"sync"

	"github.com/pkg/errors"
)

// ErrCancelledMissingConnection is an error triggered by cancelling a connection that does not exist.
var ErrCancelledMissingConnection = errors.New("cancelled connection does not exist")

// CancellationToken is a value used to identify a backend for cancellation.
type CancellationToken struct {
	PID, Key int32
}

// CancellationManager manages postgres connection cancellation.
type CancellationManager interface {
	// Token acquires a new cancellation token.
	// The returned channel is sent to every time the connection is cancelled.
	// The connection may be cancelled an unlimited number of times.
	Token() (<-chan struct{}, context.CancelFunc, CancellationToken, error)

	// Cancel sends a cancellation notification to the connection with the associated token.
	// If the token is not associated with a connection, this returns ErrCancelledMissingConnection.
	Cancel(CancellationToken) error
}

// NewLocalCancellationManager creates an in-memory CancellationManager using randomly generated tokens.
// The provided reader is expected to be secure (e.g. crypto/rand.Reader).
func NewLocalCancellationManager(rand io.Reader) CancellationManager {
	return &localCancellationManager{
		rand:        rand,
		connections: make(map[CancellationToken]chan<- struct{}),
	}
}

type localCancellationManager struct {
	mu          sync.RWMutex
	rand        io.Reader
	connections map[CancellationToken]chan<- struct{}
}

func (c *localCancellationManager) Token() (<-chan struct{}, context.CancelFunc, CancellationToken, error) {
	notify := make(chan struct{}, 1)

gen:
	token, err := c.generateToken()
	if err != nil {
		return nil, nil, CancellationToken{}, err
	}
	cancel := c.registerToken(token, notify)
	if cancel == nil {
		goto gen
	}

	return notify, cancel, token, nil
}

func (c *localCancellationManager) generateToken() (CancellationToken, error) {
	var data [8]byte
	for {
		var n int
		for n < 8 {
			nn, err := c.rand.Read(data[n:])
			if err != nil {
				return CancellationToken{}, errors.Wrap(err, "generating a cancellation token")
			}
			n += nn
		}

		pid := int32(binary.LittleEndian.Uint32(data[:4]))
		if pid < 0 {
			continue
		}
		key := int32(binary.LittleEndian.Uint32(data[4:]))
		if key < 0 {
			continue
		}

		return CancellationToken{PID: pid, Key: key}, nil
	}
}

func (c *localCancellationManager) registerToken(token CancellationToken, notify chan<- struct{}) context.CancelFunc {
	c.mu.Lock()
	defer c.mu.Unlock()

	if _, collision := c.connections[token]; collision {
		return nil
	}

	c.connections[token] = notify

	return func() {
		c.mu.Lock()
		defer c.mu.Unlock()

		delete(c.connections, token)

		close(notify)
	}
}

func (c *localCancellationManager) Cancel(token CancellationToken) error {
	c.mu.RLock()
	defer c.mu.RUnlock()

	ch := c.connections[token]
	if ch == nil {
		return ErrCancelledMissingConnection
	}

	select {
	case ch <- struct{}{}:
	default:
	}

	return nil
}
