// Package ssh gère un pool de connexions SSH réutilisables vers le host Proxmox et les LXC.
// Les connexions inactives depuis plus de 5 minutes sont automatiquement fermées.
package ssh

import (
	"fmt"
	"io"
	"strings"
	"sync"
	"time"

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

const (
	idleTimeout = 5 * time.Minute
	connTimeout = 15 * time.Second
)

// poolEntry représente une connexion SSH dans le pool avec sa date de dernier usage.
type poolEntry struct {
	client    *gossh.Client
	lastUsed  time.Time
	mu        sync.Mutex
}

// Pool est un pool thread-safe de connexions SSH.
type Pool struct {
	mu      sync.Mutex
	entries map[string]*poolEntry
	ticker  *time.Ticker
	done    chan struct{}
}

// NewPool crée un pool SSH et démarre le nettoyage automatique des connexions inactives.
func NewPool() *Pool {
	p := &Pool{
		entries: make(map[string]*poolEntry),
		ticker:  time.NewTicker(1 * time.Minute),
		done:    make(chan struct{}),
	}
	go p.cleanup()
	return p
}

// Close arrête le pool et ferme toutes les connexions.
func (p *Pool) Close() {
	close(p.done)
	p.ticker.Stop()
	p.mu.Lock()
	defer p.mu.Unlock()
	for _, entry := range p.entries {
		entry.client.Close()
	}
	p.entries = make(map[string]*poolEntry)
}

// getOrCreate retourne une connexion existante ou en crée une nouvelle.
func (p *Pool) getOrCreate(key, host, user, password string) (*poolEntry, error) {
	p.mu.Lock()
	entry, exists := p.entries[key]
	p.mu.Unlock()

	if exists {
		// Vérifier que la connexion est toujours active
		entry.mu.Lock()
		_, _, err := entry.client.SendRequest("keepalive@openssh.com", true, nil)
		if err == nil {
			entry.lastUsed = time.Now()
			entry.mu.Unlock()
			return entry, nil
		}
		entry.mu.Unlock()
		// Connexion morte — on la supprime et en crée une nouvelle
		p.mu.Lock()
		delete(p.entries, key)
		p.mu.Unlock()
	}

	// Créer une nouvelle connexion
	config := &gossh.ClientConfig{
		User: user,
		Auth: []gossh.AuthMethod{
			gossh.Password(password),
		},
		Timeout:         connTimeout,
		HostKeyCallback: gossh.InsecureIgnoreHostKey(),
	}

	client, err := gossh.Dial("tcp", host, config)
	if err != nil {
		return nil, fmt.Errorf("connexion SSH vers %s : %w", host, err)
	}

	newEntry := &poolEntry{
		client:   client,
		lastUsed: time.Now(),
	}

	p.mu.Lock()
	p.entries[key] = newEntry
	p.mu.Unlock()

	return newEntry, nil
}

// RunCommand exécute une commande sur l'hôte distant et retourne la sortie combinée.
func (p *Pool) RunCommand(host, user, password, command string) (string, error) {
	key := fmt.Sprintf("%s@%s", user, host)
	entry, err := p.getOrCreate(key, host, user, password)
	if err != nil {
		return "", err
	}

	entry.mu.Lock()
	defer entry.mu.Unlock()

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

	output, err := session.CombinedOutput(command)
	entry.lastUsed = time.Now()
	return strings.TrimSpace(string(output)), err
}

// StreamCommand exécute une commande et envoie sa sortie ligne par ligne dans le channel.
// Le channel est fermé à la fin de la commande.
func (p *Pool) StreamCommand(host, user, password, command string, output chan<- string) error {
	key := fmt.Sprintf("%s@%s", user, host)
	entry, err := p.getOrCreate(key, host, user, password)
	if err != nil {
		return err
	}

	entry.mu.Lock()
	session, err := entry.client.NewSession()
	entry.mu.Unlock()
	if err != nil {
		return fmt.Errorf("ouverture session : %w", err)
	}

	// Utiliser un pipe pour lire la sortie en streaming
	stdout, err := session.StdoutPipe()
	if err != nil {
		session.Close()
		return fmt.Errorf("pipe stdout : %w", err)
	}
	stderr, err := session.StderrPipe()
	if err != nil {
		session.Close()
		return fmt.Errorf("pipe stderr : %w", err)
	}

	if err := session.Start(command); err != nil {
		session.Close()
		return fmt.Errorf("démarrage commande : %w", err)
	}

	// Lire stdout et stderr en goroutines et envoyer dans le channel
	var wg sync.WaitGroup
	readStream := func(r io.Reader) {
		defer wg.Done()
		buf := make([]byte, 4096)
		for {
			n, err := r.Read(buf)
			if n > 0 {
				output <- string(buf[:n])
			}
			if err != nil {
				break
			}
		}
	}

	wg.Add(2)
	go readStream(stdout)
	go readStream(stderr)

	go func() {
		wg.Wait()
		session.Wait()
		session.Close()
		close(output)
		p.mu.Lock()
		if e, ok := p.entries[key]; ok {
			e.lastUsed = time.Now()
		}
		p.mu.Unlock()
	}()

	return nil
}

// cleanup supprime périodiquement les connexions inactives depuis plus de idleTimeout.
func (p *Pool) cleanup() {
	for {
		select {
		case <-p.done:
			return
		case <-p.ticker.C:
			p.mu.Lock()
			for key, entry := range p.entries {
				entry.mu.Lock()
				if time.Since(entry.lastUsed) > idleTimeout {
					entry.client.Close()
					delete(p.entries, key)
				}
				entry.mu.Unlock()
			}
			p.mu.Unlock()
		}
	}
}
