package actuator

import (
	"context"
	"fmt"
	"sync"
	"time"

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

type poolEntry struct {
	client    *ssh.Client
	createdAt time.Time
}

type DialPool struct {
	mu      sync.RWMutex
	entries map[string]*poolEntry
	ttl     time.Duration
	done    chan struct{}
	stopped bool
}

func NewDialPool(ttl time.Duration) *DialPool {
	p := &DialPool{
		entries: make(map[string]*poolEntry),
		ttl:     ttl,
		done:    make(chan struct{}),
	}
	if ttl > 0 {
		go p.evictLoop()
	}
	return p
}

func (p *DialPool) key(opts DialOptions) string {
	port := opts.Port
	if port <= 0 {
		port = 22
	}
	return fmt.Sprintf("%s:%d", opts.Host, port)
}

func (p *DialPool) Get(ctx context.Context, opts DialOptions) (*ssh.Client, error) {
	k := p.key(opts)
	p.mu.RLock()
	entry, ok := p.entries[k]
	p.mu.RUnlock()
	if ok {
		// Quick health check: a session can be created without running a
		// command — if it fails, the connection is dead and we evict it.
		testSession, err := entry.client.NewSession()
		if err == nil {
			testSession.Close()
			return entry.client, nil
		}
		p.mu.Lock()
		if p.entries[k] == entry {
			entry.client.Close()
			delete(p.entries, k)
		}
		p.mu.Unlock()
		// Fall through to dial below
	}
	client, err := Dial(ctx, opts)
	if err != nil {
		return nil, err
	}
	p.mu.Lock()
	if p.stopped {
		p.mu.Unlock()
		client.Close()
		return nil, fmt.Errorf("ssh dial pool: closed")
	}
	if existing, ok2 := p.entries[k]; ok2 {
		p.mu.Unlock()
		client.Close()
		return existing.client, nil
	}
	p.entries[k] = &poolEntry{client: client, createdAt: time.Now()}
	p.mu.Unlock()
	return client, nil
}

func (p *DialPool) Close() {
	p.mu.Lock()
	p.stopped = true
	for k, entry := range p.entries {
		entry.client.Close()
		delete(p.entries, k)
	}
	p.mu.Unlock()
	if p.ttl > 0 {
		close(p.done)
	}
}

func (p *DialPool) evictLoop() {
	ticker := time.NewTicker(p.ttl / 2)
	defer ticker.Stop()
	for {
		select {
		case <-p.done:
			return
		case <-ticker.C:
			p.evict()
		}
	}
}

func (p *DialPool) evict() {
	deadline := time.Now().Add(-p.ttl)
	p.mu.Lock()
	defer p.mu.Unlock()
	for k, entry := range p.entries {
		if entry.createdAt.Before(deadline) {
			entry.client.Close()
			delete(p.entries, k)
		}
	}
}