package tilt

import (
	"context"
	"errors"
	"fmt"
	"net"
	"os/exec"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/tidbcloud/controlplane9/internal/verbose"
)

// PortReservation holds a local TCP port until Tilt is ready to bind it. This
// prevents concurrent cp9 sessions from both observing the same port as free
// before their long-running substrate phases complete.
type PortReservation struct {
	mu       sync.Mutex
	listener net.Listener
	port     int
}

// ReserveAPIPort reserves requestedPort on localhost. A zero request asks the
// operating system for a collision-free ephemeral port.
func ReserveAPIPort(requestedPort int) (*PortReservation, error) {
	if requestedPort < 0 || requestedPort > 65535 {
		return nil, fmt.Errorf("tilt port %d is outside the valid TCP port range", requestedPort)
	}
	address := "127.0.0.1:0"
	if requestedPort > 0 {
		address = net.JoinHostPort("127.0.0.1", strconv.Itoa(requestedPort))
	}
	listener, err := net.Listen("tcp", address)
	if err != nil {
		owner := tiltPortOwner(requestedPort)
		return nil, fmt.Errorf(
			"tilt port %d is occupied by %s; change tilt.port in the public session config and rerun cp9 up before any session mutation",
			requestedPort,
			owner,
		)
	}
	port := listener.Addr().(*net.TCPAddr).Port
	return &PortReservation{listener: listener, port: port}, nil
}

// Port returns the reserved local port.
func (r *PortReservation) Port() int {
	if r == nil {
		return 0
	}
	return r.port
}

// ReleasedPortReservation represents a port already owned by the same running
// session. Release remains an idempotent no-op.
func ReleasedPortReservation(port int) *PortReservation {
	return &PortReservation{port: port}
}

// Release is idempotent.
func (r *PortReservation) Release() error {
	if r == nil {
		return nil
	}
	r.mu.Lock()
	defer r.mu.Unlock()
	if r.listener == nil {
		return nil
	}
	err := r.listener.Close()
	r.listener = nil
	if errors.Is(err, net.ErrClosed) {
		return nil
	}
	return err
}

func tiltPortOwner(port int) string {
	if port <= 0 {
		return "an unknown process"
	}
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	// #nosec G204 -- port is range-validated numeric session config, never shell-expanded.
	cmd := exec.CommandContext(ctx, "lsof", "-nP", "-iTCP:"+strconv.Itoa(port), "-sTCP:LISTEN", "-Fpct")
	output, err := verbose.RunCommand(cmd)
	if err != nil {
		return "an unknown process"
	}
	var pid, command string
	for line := range strings.SplitSeq(string(output), "\n") {
		line = strings.TrimSpace(line)
		if len(line) < 2 {
			continue
		}
		switch line[0] {
		case 'p':
			pid = line[1:]
		case 'c':
			command = line[1:]
		}
	}
	if command != "" && pid != "" {
		return fmt.Sprintf("process %s (pid %s)", command, pid)
	}
	if pid != "" {
		return "process pid " + pid
	}
	return "an unknown process"
}
