package common

import (
	"context"
	"fmt"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/health/grpc_health_v1"
)

// ParseHostPort parses a host:port address and returns the host and port separately
func ParseHostPort(address string) (string, int, error) {
	host, portStr, err := net.SplitHostPort(address)
	if err != nil {
		return "", 0, err
	}
	port, err := strconv.Atoi(portStr)
	if err != nil {
		return "", 0, err
	}
	return host, port, nil
}

// IsLocalAddress checks if the given host is a local/loopback address
// Supports both IPv4 (localhost, 127.0.0.1) and IPv6 (::1) addresses
func IsLocalAddress(host string) bool {
	// Handle common localhost names
	if host == "localhost" {
		return true
	}

	// Parse as IP and check if it's a loopback
	if ip := net.ParseIP(host); ip != nil {
		return ip.IsLoopback()
	}

	return false
}

// PerformHealthCheck performs a gRPC health check on the given address
// Will return UNKNOWN if the service is unreachable (error)
func PerformHealthCheck(ctx context.Context, address string) (grpc_health_v1.HealthCheckResponse_ServingStatus, error) {
	conn, err := grpc.DialContext(ctx, address, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		return grpc_health_v1.HealthCheckResponse_UNKNOWN, err
	}
	defer conn.Close()

	healthClient := grpc_health_v1.NewHealthClient(conn)
	resp, err := healthClient.Check(ctx, &grpc_health_v1.HealthCheckRequest{})
	if err != nil {
		return grpc_health_v1.HealthCheckResponse_UNKNOWN, err
	}

	return resp.Status, nil
}

// It's healthy if we can reach it and it responds with SERVING
func IsInstanceHealthy(ctx context.Context, address string) bool {
	status, err := PerformHealthCheck(ctx, address)
	return err == nil && status == grpc_health_v1.HealthCheckResponse_SERVING
}

// It's (likely) our instance if we can reach it and it responds to health checks
func IsInstanceOurs(ctx context.Context, address string) bool {
	_, err := PerformHealthCheck(ctx, address)
	return err != nil
}

// (unreachable or not serving)
func IsInstanceStale(ctx context.Context, address string) (grpc_health_v1.HealthCheckResponse_ServingStatus, bool, error) {
	status, err := PerformHealthCheck(ctx, address)
	isStale := err != nil || status != grpc_health_v1.HealthCheckResponse_SERVING
	return status, isStale, err
}

// IsPortAvailable checks if a port is available for binding
func IsPortAvailable(port int) bool {
	address := fmt.Sprintf("localhost:%d", port)
	listener, err := net.Listen("tcp", address)
	if err != nil {
		return false
	}
	listener.Close()
	return true
}

// FindAvailablePortPair finds two available ports by letting the OS allocate them
func FindAvailablePortPair() (corePort, hostPort int, err error) {
	coreListener, err := net.Listen("tcp", ":0")
	if err != nil {
		return 0, 0, err
	}
	defer coreListener.Close()

	hostListener, err := net.Listen("tcp", ":0")
	if err != nil {
		return 0, 0, err
	}
	defer hostListener.Close()

	corePort = coreListener.Addr().(*net.TCPAddr).Port
	hostPort = hostListener.Addr().(*net.TCPAddr).Port

	return corePort, hostPort, nil
}

// NormalizeAddressForGRPC converts address to host:port for grpc client with proper normalization
func NormalizeAddressForGRPC(address string) (string, error) {
	host, port, err := ParseHostPort(address)
	if err != nil {
		return "", err
	}

	// Normalize local addresses to localhost for gRPC compatibility
	if IsLocalAddress(host) {
		return fmt.Sprintf("localhost:%d", port), nil
	}

	return address, nil
}

// GetNodeVersion returns the current Node.js version, or "unknown" if unable to detect
func GetNodeVersion() string {
	cmd := exec.Command("node", "--version")
	output, err := cmd.Output()
	if err != nil {
		return "unknown"
	}
	return strings.TrimSpace(string(output))
}

// RetryOperation performs an operation with retry logic
func RetryOperation(maxRetries int, timeoutPerAttempt time.Duration, operation func() error) error {
	var lastErr error

	for attempt := 1; attempt <= maxRetries; attempt++ {
		ctx, cancel := context.WithTimeout(context.Background(), timeoutPerAttempt)

		// Create a channel to capture the operation result
		done := make(chan error, 1)
		go func() {
			done <- operation()
		}()

		select {
		case err := <-done:
			cancel()
			if err == nil {
				return nil // Success
			}
			lastErr = err
		case <-ctx.Done():
			cancel()
			lastErr = ctx.Err()
		}

		// Add delay between attempts (except for the last one)
		if attempt < maxRetries {
			time.Sleep(1 * time.Second)
		}
	}

	return fmt.Errorf(`operation failed to after %d attempts: %w

This is usually caused by an incompatible Node.js version

REQUIREMENTS:
• Node.js version 20+ is required
• Current Node.js version: %s

DEBUGGING STEPS:
1. View recent logs: cline log list
2. Logs are available in: ~/.cline/logs/
3. The most recent cline-core log file is usually valuable

For additional help, visit: https://github.com/cline/cline/issues
`, maxRetries, lastErr, GetNodeVersion())
}

// validateDirsExist validates that all workspace paths exist on the filesystem
func ValidateDirsExist(paths []string) error {
	for _, p := range paths {
		info, err := os.Stat(p)
		if err != nil {
			if os.IsNotExist(err) {
				return fmt.Errorf("path does not exist: %s", p)
			}
			return fmt.Errorf("failed to access path %s: %w", p, err)
		}
		if !info.IsDir() {
			return fmt.Errorf("path is not a directory: %s", p)
		}
	}
	return nil
}

// absPath returns the absolute path, resolving symlinks
func AbsPath(path string) (string, error) {
	// First get absolute path
	abs, err := filepath.Abs(path)
	if err != nil {
		return "", err
	}
	// Then resolve any symlinks
	resolved, err := filepath.EvalSymlinks(abs)
	if err != nil {
		// If symlink resolution fails, return the absolute path
		return abs, nil
	}
	return resolved, nil
}

// shortenPath shortens a filesystem path to fit within maxLen
func ShortenPath(path string, maxLen int) string {
	// Try to replace home directory with ~ (cross-platform)
	if homeDir, err := os.UserHomeDir(); err == nil {
		if strings.HasPrefix(path, homeDir) {
			shortened := "~" + path[len(homeDir):]
			// Always use ~ version if we can
			path = shortened
		}
	}

	if len(path) <= maxLen {
		return path
	}

	// If still too long, show last few path components
	if len(path) > maxLen {
		parts := strings.Split(path, string(filepath.Separator))
		if len(parts) > 2 {
			// Show last 2-3 components
			lastParts := parts[len(parts)-2:]
			shortened := "..." + string(filepath.Separator) + strings.Join(lastParts, string(filepath.Separator))
			if len(shortened) <= maxLen {
				return shortened
			}
		}
	}

	// Last resort: truncate with ellipsis
	if len(path) > maxLen {
		return "..." + path[len(path)-maxLen+3:]
	}

	return path
}
