// Package connpool implements the connection-management techniques from
// Netflix's "Curbing Connection Churn in Zuul": keep-alive pooling,
// per-event-loop pools, and low-discrepancy (Van der Corput) subsetting.
// It is stdlib-only and the pool is abstracted over a Resource so the
// churn behavior can be measured deterministically in tests.
package connpool

import (
	"sync"
	"sync/atomic"
	"time"
)

// Resource is anything poolable (a *net.Conn in production). Close()
// releases it.
type Resource interface{ Close() error }

// Dialer opens a NEW resource to an origin. In production this is the
// expensive TCP+TLS handshake we want to avoid repeating.
type Dialer func(origin string) (Resource, error)

// PooledConn wraps a checked-out resource with its origin and idle time.
type PooledConn struct {
	Resource
	origin string
	idleAt time.Time
}

// Origin returns the origin this connection is bound to.
func (c *PooledConn) Origin() string { return c.origin }

// Pool is a keep-alive connection pool with a bounded idle set per origin
// and an idle TTL. Created is the churn metric (connections.created); a
// healthy steady state drives it to ~0.
type Pool struct {
	dial    Dialer
	maxIdle int
	idleTTL time.Duration

	mu   sync.Mutex
	idle map[string][]*PooledConn

	Created atomic.Int64 // THE churn counter: new dials
	Reused  atomic.Int64 // checkouts served from the idle set
	Closed  atomic.Int64 // connections closed (eviction / pool full)
	nowFn   func() time.Time
}

// NewPool builds a pool. maxIdle is the max idle connections kept per
// origin; idleTTL is how long an idle connection may live before it's
// evicted (0 = never).
func NewPool(dial Dialer, maxIdle int, idleTTL time.Duration) *Pool {
	return &Pool{
		dial:    dial,
		maxIdle: maxIdle,
		idleTTL: idleTTL,
		idle:    map[string][]*PooledConn{},
		nowFn:   time.Now,
	}
}

func (p *Pool) now() time.Time { return p.nowFn() }

// Get returns a warm connection if one is available and unexpired, else
// dials a new one (counted as churn).
func (p *Pool) Get(origin string) (*PooledConn, error) {
	p.mu.Lock()
	q := p.idle[origin]
	for len(q) > 0 {
		c := q[len(q)-1]
		q = q[:len(q)-1]
		if p.idleTTL > 0 && p.now().Sub(c.idleAt) > p.idleTTL {
			// Expired: close it and keep looking. Over-eager eviction
			// (tiny idleTTL) re-creates the very churn we want to avoid.
			c.Resource.Close()
			p.Closed.Add(1)
			continue
		}
		p.idle[origin] = q
		p.mu.Unlock()
		p.Reused.Add(1)
		return c, nil // REUSE: no handshake
	}
	p.idle[origin] = q
	p.mu.Unlock()

	res, err := p.dial(origin) // CHURN: a fresh TCP+TLS handshake
	if err != nil {
		return nil, err
	}
	p.Created.Add(1)
	return &PooledConn{Resource: res, origin: origin}, nil
}

// Put returns a connection for reuse, or closes it if the idle set is
// full.
func (p *Pool) Put(c *PooledConn) {
	c.idleAt = p.now()
	p.mu.Lock()
	q := p.idle[c.origin]
	if p.maxIdle <= 0 || len(q) >= p.maxIdle {
		p.mu.Unlock()
		c.Resource.Close() // becomes churn on the next Get
		p.Closed.Add(1)
		return
	}
	p.idle[c.origin] = append(q, c)
	p.mu.Unlock()
}

// IdleCount returns the number of idle connections held (for tests/obs).
func (p *Pool) IdleCount() int {
	p.mu.Lock()
	defer p.mu.Unlock()
	n := 0
	for _, q := range p.idle {
		n += len(q)
	}
	return n
}
