// Package portforward provides kubectl port-forward management for ArgoCD access
package portforward

import (
	"bufio"
	"context"
	"fmt"
	"os"
	"os/exec"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

	cblog "github.com/charmbracelet/log"
)

const (
	// DefaultServerName is the default ArgoCD server app name used in label selector
	DefaultServerName = "argocd-server"

	// DefaultTargetPort is the ArgoCD server port to forward to
	DefaultTargetPort = 8080

	// maxReconnectAttempts is the maximum number of consecutive reconnection attempts
	maxReconnectAttempts = 5
)

// reconnectDelay is the delay between reconnection attempts.
// Overridable via ARGONAUT_PF_RECONNECT_DELAY (e.g. "200ms") for tests.
var reconnectDelay = func() time.Duration {
	if v := os.Getenv("ARGONAUT_PF_RECONNECT_DELAY"); v != "" {
		if d, err := time.ParseDuration(v); err == nil && d >= 0 {
			return d
		}
	}
	return 2 * time.Second
}()

// portRegex matches kubectl port-forward output like "Forwarding from 127.0.0.1:12345 -> 8080"
var portRegex = regexp.MustCompile(`Forwarding from 127\.0\.0\.1:(\d+)`)

// Manager handles kubectl port-forward lifecycle
type Manager struct {
	namespace  string
	serverName string
	targetPort int

	mu              sync.RWMutex
	cmd             *exec.Cmd
	localPort       int
	running         bool
	stopCh          chan struct{}
	monitorDone     chan struct{} // closed when monitor goroutine exits
	reconnectCount  int
	onReconnect     func(port int)
	onDisconnect    func(err error)
}

// Options configures the port-forward manager
type Options struct {
	// Namespace is the Kubernetes namespace where ArgoCD is installed
	Namespace string

	// ServerName is the ArgoCD server app name (default: "argocd-server")
	ServerName string

	// TargetPort is the port to forward to on the ArgoCD server (default: 8080)
	TargetPort int

	// OnReconnect is called when port-forward is re-established with the new port
	OnReconnect func(port int)

	// OnDisconnect is called when port-forward fails permanently
	OnDisconnect func(err error)
}

// NewManager creates a new port-forward manager
func NewManager(opts Options) *Manager {
	if opts.Namespace == "" {
		opts.Namespace = "argocd"
	}
	if opts.ServerName == "" {
		opts.ServerName = DefaultServerName
	}
	if opts.TargetPort == 0 {
		opts.TargetPort = DefaultTargetPort
	}

	return &Manager{
		namespace:    opts.Namespace,
		serverName:   opts.ServerName,
		targetPort:   opts.TargetPort,
		stopCh:       make(chan struct{}),
		onReconnect:  opts.OnReconnect,
		onDisconnect: opts.OnDisconnect,
	}
}

// Start initiates the port-forward connection and returns the local port
func (m *Manager) Start(ctx context.Context) (int, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	if m.running {
		return m.localPort, nil
	}

	// Recreate stopCh if it was closed by a previous Stop()
	select {
	case <-m.stopCh:
		m.stopCh = make(chan struct{})
	default:
	}

	// Find a ready pod
	podName, err := m.findReadyPod(ctx)
	if err != nil {
		return 0, fmt.Errorf("failed to find ArgoCD server pod: %w", err)
	}

	cblog.With("component", "portforward").Info("Found ArgoCD server pod", "pod", podName, "namespace", m.namespace)

	// Start port-forward
	cmd, port, err := m.startPortForward(ctx, podName)
	if err != nil {
		return 0, err
	}

	m.cmd = cmd
	m.localPort = port
	m.running = true
	m.reconnectCount = 0
	m.monitorDone = make(chan struct{})

	// Start monitoring goroutine
	go m.monitor()

	return port, nil
}

// Stop terminates the port-forward connection
func (m *Manager) Stop() {
	m.mu.Lock()

	if !m.running {
		m.mu.Unlock()
		return
	}

	close(m.stopCh)
	m.running = false

	// Kill the process to unblock monitor()'s Wait() call
	if m.cmd != nil && m.cmd.Process != nil {
		_ = m.cmd.Process.Kill()
	}

	// Capture monitorDone before releasing lock
	monitorDone := m.monitorDone
	m.mu.Unlock()

	// Wait for monitor goroutine to finish (outside the lock to avoid deadlock)
	// monitor() will call Wait() on the process
	if monitorDone != nil {
		<-monitorDone
	}

	cblog.With("component", "portforward").Info("Port-forward stopped")
}

// LocalPort returns the current local port
func (m *Manager) LocalPort() int {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.localPort
}

// IsRunning returns true if port-forward is active
func (m *Manager) IsRunning() bool {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.running
}

// ServerAddress returns the local server address (e.g., "127.0.0.1:12345")
func (m *Manager) ServerAddress() string {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return fmt.Sprintf("127.0.0.1:%d", m.localPort)
}

// findReadyPod finds a ready ArgoCD server pod using kubectl
func (m *Manager) findReadyPod(ctx context.Context) (string, error) {
	// Use label selector like ArgoCD CLI: app.kubernetes.io/name=argocd-server
	labelSelector := fmt.Sprintf("app.kubernetes.io/name=%s", m.serverName)

	cmd := exec.CommandContext(ctx, "kubectl", "get", "pods",
		"-n", m.namespace,
		"-l", labelSelector,
		"--field-selector=status.phase=Running",
		"-o", "jsonpath={.items[?(@.status.containerStatuses[0].ready==true)].metadata.name}",
	)

	output, err := cmd.Output()
	if err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok {
			return "", fmt.Errorf("kubectl error: %s", string(exitErr.Stderr))
		}
		return "", err
	}

	pods := strings.Fields(string(output))
	if len(pods) == 0 {
		return "", fmt.Errorf("no ready pods found with selector %s in namespace %s", labelSelector, m.namespace)
	}

	// Return first ready pod
	return pods[0], nil
}

// startPortForward starts kubectl port-forward and returns the command and allocated local port.
// The caller is responsible for assigning the returned cmd to m.cmd under appropriate locking.
// The ctx parameter is used only for startup timeout - the kubectl process lifecycle
// is managed explicitly via Stop() and not tied to any context.
func (m *Manager) startPortForward(ctx context.Context, podName string) (*exec.Cmd, int, error) {
	// Use :0 to let kubectl pick an available port
	portSpec := fmt.Sprintf(":%d", m.targetPort)

	// Use exec.Command (not CommandContext) - we manage the process lifecycle explicitly
	// via Stop() rather than tying it to a context that might be cancelled.
	cmd := exec.Command("kubectl", "port-forward",
		"-n", m.namespace,
		podName,
		portSpec,
	)

	// Capture stdout to parse the port
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, 0, fmt.Errorf("failed to create stdout pipe: %w", err)
	}

	stderr, err := cmd.StderrPipe()
	if err != nil {
		return nil, 0, fmt.Errorf("failed to create stderr pipe: %w", err)
	}

	if err := cmd.Start(); err != nil {
		return nil, 0, fmt.Errorf("failed to start kubectl port-forward: %w", err)
	}

	// Ensure cleanup on any error path - the process is killed unless we succeed
	started := true
	defer func() {
		if started && cmd.Process != nil {
			_ = cmd.Process.Kill()
			_ = cmd.Wait() // Reap the child process to avoid zombies
		}
	}()

	// Read stderr in background for logging
	go func() {
		scanner := bufio.NewScanner(stderr)
		for scanner.Scan() {
			cblog.With("component", "portforward").Debug("kubectl stderr", "line", scanner.Text())
		}
	}()

	// Parse stdout for port assignment
	portCh := make(chan int, 1)
	errCh := make(chan error, 1)

	go func() {
		scanner := bufio.NewScanner(stdout)
		for scanner.Scan() {
			line := scanner.Text()
			cblog.With("component", "portforward").Debug("kubectl stdout", "line", line)

			matches := portRegex.FindStringSubmatch(line)
			if len(matches) >= 2 {
				port, err := strconv.Atoi(matches[1])
				if err == nil {
					portCh <- port
					return
				}
			}
		}
		if err := scanner.Err(); err != nil {
			errCh <- err
		} else {
			errCh <- fmt.Errorf("kubectl port-forward exited without providing port")
		}
	}()

	// Wait for port or error with timeout
	// The ctx here is only used for startup cancellation, not process lifecycle
	select {
	case port := <-portCh:
		cblog.With("component", "portforward").Info("Port-forward established", "localPort", port, "targetPort", m.targetPort)
		started = false // Success - don't kill the process, let monitor manage it
		return cmd, port, nil
	case err := <-errCh:
		return nil, 0, err
	case <-time.After(10 * time.Second):
		return nil, 0, fmt.Errorf("timeout waiting for port-forward to establish")
	case <-ctx.Done():
		return nil, 0, ctx.Err()
	}
}

// monitor watches the port-forward process and handles reconnection
func (m *Manager) monitor() {
	defer close(m.monitorDone)

	for {
		select {
		case <-m.stopCh:
			// Ensure process is killed before waiting to avoid blocking indefinitely
			m.mu.RLock()
			cmd := m.cmd
			m.mu.RUnlock()
			if cmd != nil && cmd.Process != nil {
				_ = cmd.Process.Kill()
				_ = cmd.Wait()
			}
			return
		default:
		}

		// Get cmd under lock to avoid data race
		m.mu.RLock()
		cmd := m.cmd
		m.mu.RUnlock()

		// Wait for process to exit
		if cmd != nil {
			err := cmd.Wait()
			cblog.With("component", "portforward").Warn("Port-forward disconnected", "err", err)
		}

		m.mu.Lock()
		if !m.running {
			m.mu.Unlock()
			return
		}

		m.reconnectCount++
		if m.reconnectCount > maxReconnectAttempts {
			m.running = false
			m.mu.Unlock()

			cblog.With("component", "portforward").Error("Max reconnection attempts reached")
			if m.onDisconnect != nil {
				m.onDisconnect(fmt.Errorf("port-forward failed after %d reconnection attempts", maxReconnectAttempts))
			}
			return
		}
		m.mu.Unlock()

		cblog.With("component", "portforward").Info("Attempting to reconnect", "attempt", m.reconnectCount)

		// Wait before reconnecting
		select {
		case <-m.stopCh:
			return
		case <-time.After(reconnectDelay):
		}

		// Try to reconnect
		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)

		// Find a ready pod (might be different after restart)
		podName, err := m.findReadyPod(ctx)
		if err != nil {
			cblog.With("component", "portforward").Warn("Failed to find pod for reconnection", "err", err)
			cancel()
			continue
		}

		cmd, port, err := m.startPortForward(ctx, podName)
		cancel()

		if err != nil {
			cblog.With("component", "portforward").Warn("Failed to reconnect", "err", err)
			continue
		}

		m.mu.Lock()
		// If Stop() was called while we were reconnecting, clean up and exit
		if !m.running {
			m.mu.Unlock()
			if cmd != nil && cmd.Process != nil {
				_ = cmd.Process.Kill()
				_ = cmd.Wait()
			}
			return
		}
		m.cmd = cmd
		m.localPort = port
		m.reconnectCount = 0 // Reset on successful reconnection
		m.mu.Unlock()

		cblog.With("component", "portforward").Info("Reconnected successfully", "port", port)

		if m.onReconnect != nil {
			m.onReconnect(port)
		}
	}
}

// CheckKubectl verifies that kubectl is available and configured
func CheckKubectl(ctx context.Context) error {
	cmd := exec.CommandContext(ctx, "kubectl", "version", "--client", "--output=json")
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("kubectl not found or not configured: %w", err)
	}
	return nil
}
