package agent

import (
	"fmt"
	"math/rand/v2"
	"net"
	"sync"
)

const (
	portRangeMin = 10000
	portRangeMax = 60000
	maxRetries   = 10
)

// PortAllocator manages host port allocation for route-based deployments.
// It tracks allocated ports within the agent session to avoid double-allocation.
type PortAllocator struct {
	mu        sync.Mutex
	allocated map[int]bool
}

// NewPortAllocator creates a new PortAllocator.
func NewPortAllocator() *PortAllocator {
	return &PortAllocator{
		allocated: make(map[int]bool),
	}
}

// Allocate picks a free port in range [10000, 60000).
// It tries random ports, checks availability with net.Listen, and retries up to 10 times.
func (pa *PortAllocator) Allocate() (int, error) {
	pa.mu.Lock()
	defer pa.mu.Unlock()

	for range maxRetries {
		port := portRangeMin + rand.IntN(portRangeMax-portRangeMin) //nolint:gosec // port selection, not security
		if pa.allocated[port] {
			continue
		}

		if !isPortFree(port) {
			continue
		}

		pa.allocated[port] = true
		return port, nil
	}

	return 0, fmt.Errorf("failed to allocate port after %d attempts", maxRetries)
}

// Release marks a port as available again.
func (pa *PortAllocator) Release(port int) {
	pa.mu.Lock()
	defer pa.mu.Unlock()
	delete(pa.allocated, port)
}

// isPortFree checks if a TCP port is available by attempting to listen on it.
func isPortFree(port int) bool {
	ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
	if err != nil {
		return false
	}
	_ = ln.Close()
	return true
}
