package archive

import (
	"context"
	"sync"
	"time"

	"git.mleku.dev/mleku/nostr/encoders/event"
	"git.mleku.dev/mleku/nostr/encoders/filter"
	"git.mleku.dev/mleku/nostr/ws"
	"lol.mleku.dev/log"
)

// RelayConnection manages a single archive relay connection.
type RelayConnection struct {
	url    string
	client *ws.Client
	ctx    context.Context
	cancel context.CancelFunc

	// Connection state
	mu             sync.RWMutex
	lastConnect    time.Time
	reconnectDelay time.Duration
	connected      bool
}

const (
	// Initial delay between reconnection attempts
	initialReconnectDelay = 5 * time.Second
	// Maximum delay between reconnection attempts
	maxReconnectDelay = 5 * time.Minute
	// Connection timeout
	connectTimeout = 10 * time.Second
	// Query timeout (per query, not global)
	queryTimeout = 30 * time.Second
)

// NewRelayConnection creates a new relay connection.
func NewRelayConnection(parentCtx context.Context, url string) *RelayConnection {
	ctx, cancel := context.WithCancel(parentCtx)
	return &RelayConnection{
		url:            url,
		ctx:            ctx,
		cancel:         cancel,
		reconnectDelay: initialReconnectDelay,
	}
}

// Connect establishes a connection to the archive relay.
func (rc *RelayConnection) Connect() error {
	rc.mu.Lock()
	defer rc.mu.Unlock()

	if rc.connected && rc.client != nil {
		return nil
	}

	connectCtx, cancel := context.WithTimeout(rc.ctx, connectTimeout)
	defer cancel()

	client, err := ws.RelayConnect(connectCtx, rc.url)
	if err != nil {
		rc.reconnectDelay = min(rc.reconnectDelay*2, maxReconnectDelay)
		return err
	}

	rc.client = client
	rc.connected = true
	rc.lastConnect = time.Now()
	rc.reconnectDelay = initialReconnectDelay

	log.D.F("archive: connected to %s", rc.url)

	return nil
}

// Query executes a query against the archive relay.
// Returns a slice of events matching the filter.
func (rc *RelayConnection) Query(ctx context.Context, f *filter.F) ([]*event.E, error) {
	rc.mu.RLock()
	client := rc.client
	connected := rc.connected
	rc.mu.RUnlock()

	if !connected || client == nil {
		if err := rc.Connect(); err != nil {
			return nil, err
		}
		rc.mu.RLock()
		client = rc.client
		rc.mu.RUnlock()
	}

	// Create query context with timeout
	queryCtx, cancel := context.WithTimeout(ctx, queryTimeout)
	defer cancel()

	// Subscribe to the filter
	sub, err := client.Subscribe(queryCtx, filter.NewS(f))
	if err != nil {
		rc.handleDisconnection()
		return nil, err
	}
	defer sub.Unsub()

	// Collect events until EOSE or timeout
	var events []*event.E

	for {
		select {
		case <-queryCtx.Done():
			return events, nil
		case <-sub.EndOfStoredEvents:
			return events, nil
		case ev := <-sub.Events:
			if ev == nil {
				return events, nil
			}
			events = append(events, ev)
		}
	}
}

// handleDisconnection marks the connection as disconnected.
func (rc *RelayConnection) handleDisconnection() {
	rc.mu.Lock()
	defer rc.mu.Unlock()

	rc.connected = false
	if rc.client != nil {
		rc.client.Close()
		rc.client = nil
	}
}

// IsConnected returns whether the relay is currently connected.
func (rc *RelayConnection) IsConnected() bool {
	rc.mu.RLock()
	defer rc.mu.RUnlock()

	if !rc.connected || rc.client == nil {
		return false
	}

	// Check if client is still connected
	return rc.client.IsConnected()
}

// Close closes the relay connection.
func (rc *RelayConnection) Close() {
	rc.cancel()

	rc.mu.Lock()
	defer rc.mu.Unlock()

	rc.connected = false
	if rc.client != nil {
		rc.client.Close()
		rc.client = nil
	}
}

// URL returns the relay URL.
func (rc *RelayConnection) URL() string {
	return rc.url
}

// min returns the smaller of two durations.
func min(a, b time.Duration) time.Duration {
	if a < b {
		return a
	}
	return b
}
