// Package dedicatedapi owns the session-scoped Dedicated metadata/control API
// contract. The existing host Kubernetes API remains the workload runtime.
package dedicatedapi

import (
	"fmt"
	"net"
	"path/filepath"
	"strings"

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

const (
	// StateDirName is the private session directory for Dedicated API material.
	StateDirName = "dedicated-api"
	// DefaultNodePort is fixed inside the k3d network; the host-side port is
	// allocated per session and bound to loopback only.
	DefaultNodePort  = 32443
	internalEndpoint = "https://dedicated-kube-apiserver.cp9-system.svc:6443"
)

// AllocateLoopbackPort returns a currently available loopback-only host port.
// The k3d create operation remains the authority for detecting the small race
// between allocation and bind.
func AllocateLoopbackPort() (int, error) {
	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		return 0, fmt.Errorf("allocate loopback port: %w", err)
	}
	defer listener.Close()
	address, ok := listener.Addr().(*net.TCPAddr)
	if !ok || address.Port < 1 {
		return 0, fmt.Errorf("allocate loopback port: unexpected address %s", listener.Addr())
	}
	return address.Port, nil
}

// PortReservation holds a loopback port until k3d is ready to bind it.
type PortReservation struct {
	listener net.Listener
}

// ReservePreparedPort prevents the Tilt API allocator or another local
// process from taking the already-persisted Dedicated API port before k3d
// creation.
func ReservePreparedPort(port int) (*PortReservation, error) {
	if port < 1 || port > 65535 {
		return nil, fmt.Errorf("reserve Dedicated API port: invalid port %d", port)
	}
	listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
	if err != nil {
		return nil, fmt.Errorf("reserve Dedicated API port %d: %w", port, err)
	}
	return &PortReservation{listener: listener}, nil
}

// Release frees the port exactly once.
func (r *PortReservation) Release() error {
	if r == nil || r.listener == nil {
		return nil
	}
	err := r.listener.Close()
	r.listener = nil
	return err
}

// Prepare creates or validates the durable second-API identity before k3d is
// created. Calling Prepare again is a resume and must not allocate a new port
// or identity.
func Prepare(selected *host.Session, allocatePort func() (int, error)) error {
	if selected == nil {
		return fmt.Errorf("prepare Dedicated API: selected host is required")
	}
	if strings.TrimSpace(selected.StateDir) == "" || strings.TrimSpace(selected.ClusterName) == "" {
		return fmt.Errorf("prepare Dedicated API: host state directory and cluster name are required")
	}
	if selected.DedicatedAPI != nil {
		if err := validate(selected.DedicatedAPI); err != nil {
			return fmt.Errorf(
				"legacy Dedicated API state for session %q cannot be migrated safely in place: %w; run cp9 destroy and cp9 up again",
				selected.ClusterName,
				err,
			)
		}
		return nil
	}
	if allocatePort == nil {
		return fmt.Errorf("prepare Dedicated API: host port allocator is required")
	}
	port, err := allocatePort()
	if err != nil {
		return fmt.Errorf("prepare Dedicated API host port: %w", err)
	}
	if port < 1 || port > 65535 {
		return fmt.Errorf("prepare Dedicated API: invalid host port %d", port)
	}
	root := filepath.Join(selected.StateDir, StateDirName)
	selected.DedicatedAPI = &host.DedicatedAPISession{
		HostPort:               port,
		NodePort:               DefaultNodePort,
		InternalEndpoint:       internalEndpoint,
		LocalEndpoint:          fmt.Sprintf("https://127.0.0.1:%d", port),
		AdminKubeconfigPath:    filepath.Join(root, "admin.kubeconfig"),
		InternalKubeconfigPath: filepath.Join(root, "internal.kubeconfig"),
		PKIDir:                 filepath.Join(root, "pki"),
		APIServerID:            selected.ClusterName + "-dedicated-api",
		EtcdMemberID:           selected.ClusterName + "-dedicated-etcd",
		EtcdImage:              "registry.k8s.io/etcd:3.5.17-0",
	}
	return nil
}

func validate(state *host.DedicatedAPISession) error {
	if state == nil {
		return fmt.Errorf("state is nil")
	}
	missing := make([]string, 0, 9)
	if state.HostPort < 1 || state.HostPort > 65535 {
		missing = append(missing, "host_port")
	}
	if state.NodePort != DefaultNodePort {
		missing = append(missing, "node_port")
	}
	for name, value := range map[string]string{
		"internal_endpoint":        state.InternalEndpoint,
		"local_endpoint":           state.LocalEndpoint,
		"admin_kubeconfig_path":    state.AdminKubeconfigPath,
		"internal_kubeconfig_path": state.InternalKubeconfigPath,
		"pki_dir":                  state.PKIDir,
		"api_server_id":            state.APIServerID,
		"etcd_member_id":           state.EtcdMemberID,
	} {
		if strings.TrimSpace(value) == "" {
			missing = append(missing, name)
		}
	}
	if state.APIServerID == state.EtcdMemberID {
		missing = append(missing, "independent api_server_id/etcd_member_id")
	}
	if len(missing) > 0 {
		return fmt.Errorf("missing or invalid fields: %s", strings.Join(missing, ", "))
	}
	return nil
}
