package server

import (
	"fmt"
	"log"
	"strings"
	"sync"

	"golang.org/x/crypto/bcrypt"
	"golang.org/x/crypto/ssh"
)

// LabelManager manages Traefik routing labels on its own Swarm service
// by SSHing into the Swarm manager and running docker service update.
type LabelManager struct {
	mu           sync.Mutex
	remoteHost   string // Swarm manager, e.g. "ingress.nixc.us"
	remoteUser   string // SSH user
	signer       ssh.Signer
	serviceName  string // Swarm service name, e.g. "better-argo-tunnels_tunnel-server"
	entrypoint   string // e.g. "websecure"
	certResolver string // e.g. "letsencryptresolver"
	labels       map[string]bool // track which tunnel keys we've added
	authLabels   map[string]bool // track which tunnel keys have auth middleware
}

// NewLabelManager creates a label manager that updates Swarm service labels via SSH.
func NewLabelManager(
	remoteHost, remoteUser string,
	signer ssh.Signer,
	serviceName, entrypoint, certResolver string,
) (*LabelManager, error) {

	lm := &LabelManager{
		remoteHost:   remoteHost,
		remoteUser:   remoteUser,
		signer:       signer,
		serviceName:  serviceName,
		entrypoint:   entrypoint,
		certResolver: certResolver,
		labels:       make(map[string]bool),
		authLabels:   make(map[string]bool),
	}

	// Verify we can reach the Swarm manager and the service exists.
	cmd := fmt.Sprintf("docker service inspect --format '{{.Spec.Name}}' %s", serviceName)
	if err := lm.runRemote(cmd); err != nil {
		log.Printf("WARN: could not verify service %s (may not exist yet): %v", serviceName, err)
	} else {
		log.Printf("Verified Swarm service: %s", serviceName)
	}

	log.Printf("Label manager ready (host=%s, service=%s, ep=%s, resolver=%s)",
		remoteHost, serviceName, entrypoint, certResolver)

	return lm, nil
}

// Add adds Traefik routing labels to the Swarm service for a tunnel.
// If authUser and authPass are non-empty, a basicauth middleware is also added.
func (lm *LabelManager) Add(tunKey, domain string, port int, authUser, authPass string) error {
	lm.mu.Lock()
	defer lm.mu.Unlock()

	routerName := fmt.Sprintf("tunnel-%s-router", tunKey)
	serviceName := fmt.Sprintf("tunnel-%s-service", tunKey)
	middlewareName := fmt.Sprintf("tunnel-%s-auth", tunKey)

	// Build the label-add flags for docker service update.
	labelArgs := []string{
		labelFlag(fmt.Sprintf("traefik.http.routers.%s.rule", routerName),
			fmt.Sprintf("Host(`%s`)", domain)),
		labelFlag(fmt.Sprintf("traefik.http.routers.%s.entrypoints", routerName),
			lm.entrypoint),
		labelFlag(fmt.Sprintf("traefik.http.routers.%s.tls", routerName),
			"true"),
		labelFlag(fmt.Sprintf("traefik.http.routers.%s.tls.certresolver", routerName),
			lm.certResolver),
		labelFlag(fmt.Sprintf("traefik.http.routers.%s.service", routerName),
			serviceName),
		labelFlag(fmt.Sprintf("traefik.http.services.%s.loadbalancer.server.port", serviceName),
			fmt.Sprintf("%d", port)),
	}

	// If auth credentials are provided, add basicauth middleware labels.
	if authUser != "" && authPass != "" {
		htpasswd, err := generateHTPasswd(authUser, authPass)
		if err != nil {
			return fmt.Errorf("generate htpasswd for %s: %w", domain, err)
		}
		labelArgs = append(labelArgs,
			labelFlag(
				fmt.Sprintf("traefik.http.middlewares.%s.basicauth.users", middlewareName),
				htpasswd,
			),
			labelFlag(
				fmt.Sprintf("traefik.http.routers.%s.middlewares", routerName),
				middlewareName,
			),
		)
		lm.authLabels[tunKey] = true
		log.Printf("BasicAuth middleware %s added for %s", middlewareName, domain)
	}

	cmd := fmt.Sprintf("docker service update --label-add %s %s",
		strings.Join(labelArgs, " --label-add "), lm.serviceName)

	if err := lm.runRemote(cmd); err != nil {
		return fmt.Errorf("add labels for %s: %w", domain, err)
	}

	lm.labels[tunKey] = true
	log.Printf("Added Swarm labels: %s -> %s:%d", domain, lm.serviceName, port)
	return nil
}

// Remove removes Traefik routing labels from the Swarm service for a tunnel.
func (lm *LabelManager) Remove(tunKey string) error {
	lm.mu.Lock()
	defer lm.mu.Unlock()

	if !lm.labels[tunKey] {
		return nil // nothing to remove
	}

	routerName := fmt.Sprintf("tunnel-%s-router", tunKey)
	serviceName := fmt.Sprintf("tunnel-%s-service", tunKey)

	middlewareName := fmt.Sprintf("tunnel-%s-auth", tunKey)

	// Build the label-rm flags.
	rmLabels := []string{
		fmt.Sprintf("traefik.http.routers.%s.rule", routerName),
		fmt.Sprintf("traefik.http.routers.%s.entrypoints", routerName),
		fmt.Sprintf("traefik.http.routers.%s.tls", routerName),
		fmt.Sprintf("traefik.http.routers.%s.tls.certresolver", routerName),
		fmt.Sprintf("traefik.http.routers.%s.service", routerName),
		fmt.Sprintf("traefik.http.services.%s.loadbalancer.server.port", serviceName),
	}

	// Remove auth middleware labels if they were added.
	if lm.authLabels[tunKey] {
		rmLabels = append(rmLabels,
			fmt.Sprintf("traefik.http.middlewares.%s.basicauth.users", middlewareName),
			fmt.Sprintf("traefik.http.routers.%s.middlewares", routerName),
		)
		delete(lm.authLabels, tunKey)
		log.Printf("Removing BasicAuth middleware %s", middlewareName)
	}

	cmd := fmt.Sprintf("docker service update --label-rm %s %s",
		strings.Join(rmLabels, " --label-rm "), lm.serviceName)

	if err := lm.runRemote(cmd); err != nil {
		return fmt.Errorf("remove labels for %s: %w", tunKey, err)
	}

	delete(lm.labels, tunKey)
	log.Printf("Removed Swarm labels for tunnel: %s", tunKey)
	return nil
}

// generateHTPasswd creates a bcrypt-hashed htpasswd entry for Traefik basicauth.
// The output format is user:$hash. Dollar signs are NOT doubled here because
// we pass labels via docker service update with single-quoted values, which
// preserves them literally. Doubling is only needed in compose files.
func generateHTPasswd(user, pass string) (string, error) {
	hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
	if err != nil {
		return "", fmt.Errorf("bcrypt hash: %w", err)
	}
	return fmt.Sprintf("%s:%s", user, string(hash)), nil
}

// labelFlag formats a --label-add value, quoting properly for shell.
func labelFlag(key, value string) string {
	return fmt.Sprintf("'%s=%s'", key, value)
}

// runRemote executes a command on the Swarm manager via SSH.
func (lm *LabelManager) runRemote(cmd string) error {
	addr := lm.remoteHost
	if !strings.Contains(addr, ":") {
		addr = addr + ":22"
	}

	config := &ssh.ClientConfig{
		User: lm.remoteUser,
		Auth: []ssh.AuthMethod{
			ssh.PublicKeys(lm.signer),
		},
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
	}

	client, err := ssh.Dial("tcp", addr, config)
	if err != nil {
		return fmt.Errorf("SSH dial %s: %w", addr, err)
	}
	defer client.Close()

	session, err := client.NewSession()
	if err != nil {
		return fmt.Errorf("SSH session: %w", err)
	}
	defer session.Close()

	output, err := session.CombinedOutput(cmd)
	if err != nil {
		return fmt.Errorf("remote cmd failed: %w (output: %s)", err, string(output))
	}

	return nil
}

// Close is a no-op — SSH connections are opened/closed per operation.
func (lm *LabelManager) Close() error {
	return nil
}
