package proxy

import (
	"context"
	"errors"
	"strings"
	"sync"
	"sync/atomic"

	"golang.org/x/sync/semaphore"
)

var (
	// ErrProxyExists is returned when trying to create a proxy with an existing ID.
	ErrProxyExists = errors.New("proxy already exists")
	// ErrProxyNotFound is returned when a proxy ID is not found.
	ErrProxyNotFound = errors.New("proxy not found")
	// ErrProxyAmbiguous is returned when a fuzzy lookup matches multiple proxies.
	ErrProxyAmbiguous = errors.New("proxy ID is ambiguous - multiple matches")
)

// ProxyManager manages multiple reverse proxy servers with lock-free access.
type ProxyManager struct {
	proxies      sync.Map // map[string]*ProxyServer
	activeCount  atomic.Int64
	totalStarted atomic.Int64

	shutdownOnce sync.Once
	shuttingDown atomic.Bool
}

// NewProxyManager creates a new proxy manager.
func NewProxyManager() *ProxyManager {
	return &ProxyManager{}
}

// Create creates and starts a new proxy server.
func (pm *ProxyManager) Create(ctx context.Context, config ProxyConfig) (*ProxyServer, error) {
	if pm.shuttingDown.Load() {
		return nil, errors.New("proxy manager is shutting down")
	}

	// Check if proxy already exists
	if _, exists := pm.proxies.Load(config.ID); exists {
		return nil, ErrProxyExists
	}

	// Create proxy server
	proxy, err := NewProxyServer(config)
	if err != nil {
		return nil, err
	}

	// Start proxy
	if err := proxy.Start(ctx); err != nil {
		// Release resources owned by the constructed-but-never-started server
		// (page-tracker actor goroutine, reorder queue).
		proxy.pageTracker.Stop()
		proxy.chaosEngine.Stop()
		return nil, err
	}

	// Store in registry
	pm.proxies.Store(config.ID, proxy)

	// Re-check after the store: if Shutdown set the flag between our entry
	// check and the Store, its registry sweep may already have run and missed
	// this proxy — tear it down ourselves. If the flag is still false here, the
	// store happened before the sweep and Shutdown will stop it normally.
	if pm.shuttingDown.Load() {
		pm.proxies.Delete(config.ID)
		_ = proxy.Stop(ctx)
		return nil, errors.New("proxy manager is shutting down")
	}

	pm.activeCount.Add(1)
	pm.totalStarted.Add(1)

	return proxy, nil
}

// Get retrieves a proxy by ID with fuzzy matching support.
// First tries exact match, then looks for proxies where the ID contains
// the search string as a component (for compound IDs like "project:name:host-port").
func (pm *ProxyManager) Get(id string) (*ProxyServer, error) {
	return pm.GetWithPathFilter(id, "")
}

// GetWithPathFilter retrieves a proxy by ID with fuzzy matching, filtered by path.
// If pathFilter is non-empty, only proxies with matching Path are considered for fuzzy lookup.
// Exact matches are always returned regardless of path filter.
func (pm *ProxyManager) GetWithPathFilter(id, pathFilter string) (*ProxyServer, error) {
	// First try exact match (lock-free read) - always works regardless of path
	if val, ok := pm.proxies.Load(id); ok {
		return val.(*ProxyServer), nil
	}

	// Normalize path filter for comparison
	normalizedFilter := normalizePath(pathFilter)

	// Fuzzy match: look for proxy where the ID contains the search string as a component
	// Compound ID format: {project-hash}:{proxy-name}:{host-port}
	var matches []*ProxyServer
	pm.proxies.Range(func(key, value any) bool {
		proxyID := key.(string)
		proxy := value.(*ProxyServer)

		// If path filter is specified, only consider proxies in that path
		if normalizedFilter != "" && normalizedFilter != "." {
			proxyPath := normalizePath(proxy.Path)
			if proxyPath != normalizedFilter {
				return true // Skip this proxy, continue iteration
			}
		}

		// Check if search string matches a component of the compound ID
		// Split by ":" and check each part
		parts := strings.Split(proxyID, ":")
		for _, part := range parts {
			if part == id {
				matches = append(matches, proxy)
				break
			}
		}
		return true
	})

	if len(matches) == 0 {
		return nil, ErrProxyNotFound
	}
	if len(matches) > 1 {
		return nil, ErrProxyAmbiguous
	}
	return matches[0], nil
}

// normalizePath normalizes a path for comparison.
func normalizePath(p string) string {
	if p == "" {
		return ""
	}
	// Remove trailing slashes and normalize
	for len(p) > 1 && p[len(p)-1] == '/' {
		p = p[:len(p)-1]
	}
	return p
}

// Stop stops a proxy server and removes it from the registry.
func (pm *ProxyManager) Stop(ctx context.Context, id string) error {
	proxy, err := pm.Get(id)
	if err != nil {
		return err
	}

	if err := proxy.Stop(ctx); err != nil {
		return err
	}

	pm.proxies.Delete(id)
	pm.activeCount.Add(-1)

	return nil
}

// List returns all managed proxy servers.
func (pm *ProxyManager) List() []*ProxyServer {
	var result []*ProxyServer
	pm.proxies.Range(func(key, value any) bool {
		result = append(result, value.(*ProxyServer))
		return true
	})
	return result
}

// ActiveCount returns the number of running proxies.
func (pm *ProxyManager) ActiveCount() int64 {
	return pm.activeCount.Load()
}

// TotalStarted returns the total number of proxies ever started.
func (pm *ProxyManager) TotalStarted() int64 {
	return pm.totalStarted.Load()
}

// maxParallelStops bounds concurrent proxy stop operations to prevent goroutine explosion.
const maxParallelStops int64 = 20

// StopAll stops all running proxies and removes them from the registry.
// Unlike Shutdown, this does NOT set shuttingDown flag, allowing new proxies
// to be started afterward. This is used for cleanup when the last client disconnects.
// Returns the list of stopped proxy IDs for state persistence cleanup.
func (pm *ProxyManager) StopAll(ctx context.Context) ([]string, error) {
	var stopWg sync.WaitGroup
	var errMu sync.Mutex
	var errs []error
	var stoppedIDs []string
	var stoppedMu sync.Mutex
	sem := semaphore.NewWeighted(maxParallelStops)

	// Collect all proxy IDs to stop
	var toStop []string
	pm.proxies.Range(func(key, value any) bool {
		toStop = append(toStop, key.(string))
		return true
	})

	// Stop proxies with bounded concurrency
	for _, id := range toStop {
		if err := sem.Acquire(ctx, 1); err != nil {
			break // context cancelled
		}
		stopWg.Add(1)
		go func(proxyID string) {
			defer sem.Release(1)
			defer stopWg.Done()
			if err := pm.Stop(ctx, proxyID); err != nil {
				errMu.Lock()
				errs = append(errs, err)
				errMu.Unlock()
			} else {
				stoppedMu.Lock()
				stoppedIDs = append(stoppedIDs, proxyID)
				stoppedMu.Unlock()
			}
		}(id)
	}

	// Wait for all stops to complete with timeout
	done := make(chan struct{})
	go func() {
		stopWg.Wait()
		close(done)
	}()

	select {
	case <-done:
		// All proxies stopped
	case <-ctx.Done():
		// Context cancelled, return what we have
		if len(errs) > 0 {
			errs = append(errs, ctx.Err())
		} else {
			return stoppedIDs, ctx.Err()
		}
	}

	if len(errs) > 0 {
		return stoppedIDs, errors.Join(errs...)
	}
	return stoppedIDs, nil
}

// StopByProjectPath stops all running proxies for a specific project path and removes them.
// This is used for session-scoped cleanup when a client disconnects.
// Returns the list of stopped proxy IDs.
func (pm *ProxyManager) StopByProjectPath(ctx context.Context, projectPath string) ([]string, error) {
	var toStop []*ProxyServer
	pm.proxies.Range(func(key, value any) bool {
		proxy := value.(*ProxyServer)
		if proxy.Path == projectPath {
			toStop = append(toStop, proxy)
		}
		return true
	})

	if len(toStop) == 0 {
		return nil, nil
	}

	var stopWg sync.WaitGroup
	var errMu sync.Mutex
	var errs []error
	var stoppedIDs []string
	var stoppedMu sync.Mutex
	sem := semaphore.NewWeighted(maxParallelStops)

	for _, proxy := range toStop {
		if err := sem.Acquire(ctx, 1); err != nil {
			break // context cancelled
		}
		stopWg.Add(1)
		go func(p *ProxyServer) {
			defer sem.Release(1)
			defer stopWg.Done()
			if err := pm.Stop(ctx, p.ID); err != nil {
				errMu.Lock()
				errs = append(errs, err)
				errMu.Unlock()
			} else {
				stoppedMu.Lock()
				stoppedIDs = append(stoppedIDs, p.ID)
				stoppedMu.Unlock()
			}
		}(proxy)
	}

	done := make(chan struct{})
	go func() {
		stopWg.Wait()
		close(done)
	}()

	select {
	case <-done:
	case <-ctx.Done():
		if len(errs) > 0 {
			errs = append(errs, ctx.Err())
		} else {
			return stoppedIDs, ctx.Err()
		}
	}

	if len(errs) > 0 {
		return stoppedIDs, errors.Join(errs...)
	}
	return stoppedIDs, nil
}

// Shutdown stops all managed proxies.
func (pm *ProxyManager) Shutdown(ctx context.Context) error {
	var shutdownErr error

	pm.shutdownOnce.Do(func() {
		pm.shuttingDown.Store(true)

		var stopWg sync.WaitGroup
		var errMu sync.Mutex
		var errs []error
		sem := semaphore.NewWeighted(maxParallelStops)

		// Collect IDs to stop so we don't hold Range while stopping.
		var toStop []string
		pm.proxies.Range(func(key, value any) bool {
			proxy := value.(*ProxyServer)
			if proxy.IsRunning() {
				toStop = append(toStop, key.(string))
			} else {
				// Crashed or never-started proxies skip the Stop path but still
				// own a page-tracker actor goroutine — release it here.
				proxy.pageTracker.Stop()
			}
			return true
		})

		for _, id := range toStop {
			if err := sem.Acquire(ctx, 1); err != nil {
				break // context cancelled
			}
			stopWg.Add(1)
			go func(proxyID string) {
				defer sem.Release(1)
				defer stopWg.Done()
				if err := pm.Stop(ctx, proxyID); err != nil {
					errMu.Lock()
					errs = append(errs, err)
					errMu.Unlock()
				}
			}(id)
		}

		done := make(chan struct{})
		go func() {
			stopWg.Wait()
			close(done)
		}()

		select {
		case <-done:
			// All proxies stopped
		case <-ctx.Done():
			shutdownErr = ctx.Err()
		}

		if len(errs) > 0 {
			shutdownErr = errors.Join(errs...)
		}
	})

	return shutdownErr
}
