package mcp

import (
	"context"
	"log/slog"
	"sync"

	mcpgo "github.com/mark3labs/mcp-go/mcp"
)

// poolEntry holds a shared connection and its discovered tools.
type poolEntry struct {
	state    *serverState // connection + health state
	tools    []mcpgo.Tool // discovered MCP tool definitions
	refCount int          // number of active Manager references
}

// Pool manages shared MCP server connections across agents.
// One physical connection per server name, shared by all agents
// that have grants to that server. Each agent creates its own
// BridgeTools pointing to the shared client/connected pointers.
type Pool struct {
	mu      sync.Mutex
	servers map[string]*poolEntry
}

// NewPool creates a shared MCP connection pool.
func NewPool() *Pool {
	return &Pool{
		servers: make(map[string]*poolEntry),
	}
}

// Acquire returns a shared connection for the named server.
// If no connection exists, it connects using the provided config.
// Increments the reference count.
func (p *Pool) Acquire(ctx context.Context, name, transportType, command string, args []string, env map[string]string, url string, headers map[string]string, timeoutSec int) (*poolEntry, error) {
	p.mu.Lock()

	if entry, ok := p.servers[name]; ok && entry.state.connected.Load() {
		entry.refCount++
		p.mu.Unlock()
		slog.Debug("mcp.pool.reuse", "server", name, "refCount", entry.refCount)
		return entry, nil
	}

	// If entry exists but disconnected, close old connection first
	if old, ok := p.servers[name]; ok {
		if old.state.cancel != nil {
			old.state.cancel()
		}
		if old.state.client != nil {
			_ = old.state.client.Close()
		}
		delete(p.servers, name)
	}

	p.mu.Unlock()

	// Connect outside the lock (may be slow)
	ss, mcpTools, err := connectAndDiscover(ctx, name, transportType, command, args, env, url, headers, timeoutSec)
	if err != nil {
		return nil, err
	}

	// Start health loop
	hctx, hcancel := context.WithCancel(context.Background())
	ss.cancel = hcancel
	go poolHealthLoop(hctx, ss)

	entry := &poolEntry{
		state:    ss,
		tools:    mcpTools,
		refCount: 1,
	}

	p.mu.Lock()
	// Check if another goroutine connected while we were connecting
	if existing, ok := p.servers[name]; ok && existing.state.connected.Load() {
		// Use existing, close ours
		p.mu.Unlock()
		hcancel()
		_ = ss.client.Close()
		p.mu.Lock()
		existing.refCount++
		p.mu.Unlock()
		return existing, nil
	}
	p.servers[name] = entry
	p.mu.Unlock()

	slog.Info("mcp.pool.connected", "server", name, "tools", len(mcpTools))
	return entry, nil
}

// Release decrements the reference count for a server.
// The connection is NOT closed when refCount reaches 0 — it stays
// alive for future agents. Use Stop() to close all connections.
func (p *Pool) Release(name string) {
	p.mu.Lock()
	defer p.mu.Unlock()

	if entry, ok := p.servers[name]; ok {
		entry.refCount--
		if entry.refCount < 0 {
			entry.refCount = 0
		}
		slog.Debug("mcp.pool.release", "server", name, "refCount", entry.refCount)
	}
}

// Stop closes all pooled connections. Called on gateway shutdown.
func (p *Pool) Stop() {
	p.mu.Lock()
	defer p.mu.Unlock()

	for name, entry := range p.servers {
		if entry.state.cancel != nil {
			entry.state.cancel()
		}
		if entry.state.client != nil {
			_ = entry.state.client.Close()
		}
		slog.Debug("mcp.pool.stopped", "server", name)
	}
	p.servers = make(map[string]*poolEntry)
}

// poolHealthLoop is a standalone health loop for pool-managed connections.
// Unlike Manager.healthLoop, it doesn't trigger reconnection via Manager
// since the pool owns the connection lifecycle.
func poolHealthLoop(ctx context.Context, ss *serverState) {
	ticker := newHealthTicker()
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			if err := ss.client.Ping(ctx); err != nil {
				if isMethodNotFound(err) {
					ss.connected.Store(true)
					continue
				}
				ss.connected.Store(false)
				ss.mu.Lock()
				ss.lastErr = err.Error()
				ss.mu.Unlock()
				slog.Warn("mcp.pool.health_failed", "server", ss.name, "error", err)
			} else {
				ss.connected.Store(true)
				ss.mu.Lock()
				ss.reconnAttempts = 0
				ss.lastErr = ""
				ss.mu.Unlock()
			}
		}
	}
}
