// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

// Package session provides a session manager with TTL cleanup.
package session

import (
	"context"
	"fmt"
	"log/slog"
	"time"

	"github.com/google/uuid"
)

const (
	// defaultOperationTimeout is the timeout for standard storage operations
	defaultOperationTimeout = 5 * time.Second
	// cleanupOperationTimeout is the timeout for cleanup operations which may take longer
	cleanupOperationTimeout = 30 * time.Second
)

// Session interface defines the contract for all session types
type Session interface {
	ID() string
	Type() SessionType
	CreatedAt() time.Time
	UpdatedAt() time.Time

	// Data and metadata methods
	GetData() interface{}
	SetData(data interface{})
	GetMetadata() map[string]string
	GetMetadataValue(key string) (string, bool)
	SetMetadata(key, value string)
}

// Manager holds sessions with TTL cleanup.
type Manager struct {
	storage Storage
	ttl     time.Duration
	stopCh  chan struct{}
	factory Factory
}

// Factory defines a function type for creating new sessions.
// It now returns the Session interface to support different session types.
type Factory func(id string) Session

// LegacyFactory is the old factory type for backward compatibility
type LegacyFactory func(id string) *ProxySession

// NewManager creates a session manager with TTL and starts cleanup worker.
// It accepts either the new Factory or the legacy factory for backward compatibility.
func NewManager(ttl time.Duration, factory interface{}) *Manager {
	var f Factory

	switch factoryFunc := factory.(type) {
	case Factory:
		f = factoryFunc
	case LegacyFactory:
		// Wrap legacy factory to return Session interface
		f = func(id string) Session {
			return factoryFunc(id)
		}
	case func(id string) *ProxySession:
		// Also support direct function for backward compatibility
		f = func(id string) Session {
			return factoryFunc(id)
		}
	default:
		// Default to creating basic ProxySession
		f = func(id string) Session {
			return NewProxySession(id)
		}
	}

	m := &Manager{
		storage: NewLocalStorage(),
		ttl:     ttl,
		stopCh:  make(chan struct{}),
		factory: f,
	}
	go m.cleanupRoutine()
	return m
}

// NewTypedManager creates a session manager for a specific session type.
func NewTypedManager(ttl time.Duration, sessionType SessionType) *Manager {
	factory := func(id string) Session {
		switch sessionType {
		case SessionTypeSSE:
			return NewSSESession(id)
		case SessionTypeMCP:
			return NewProxySession(id)
		case SessionTypeStreamable:
			return NewTypedProxySession(id, sessionType)
		default:
			return NewTypedProxySession(id, sessionType)
		}
	}

	return NewManager(ttl, factory)
}

// NewManagerWithStorage creates a session manager with a custom storage backend.
func NewManagerWithStorage(ttl time.Duration, factory Factory, storage Storage) *Manager {
	m := &Manager{
		storage: storage,
		ttl:     ttl,
		stopCh:  make(chan struct{}),
		factory: factory,
	}
	go m.cleanupRoutine()
	return m
}

// NewManagerWithRedis creates a session manager backed by Redis.
// ctx is used for the initial Ping during construction and should carry any
// deadline appropriate for the connection attempt (e.g. a startup timeout).
// cfg supplies the Redis connection configuration; ttl is applied as both the
// manager's cleanup interval and the Redis key TTL.
// Returns an error if the Redis client cannot be constructed (e.g. invalid config or TLS error).
// Callers that do not require Redis should use NewManager or NewTypedManager instead.
func NewManagerWithRedis(ctx context.Context, ttl time.Duration, factory Factory, cfg RedisConfig) (*Manager, error) {
	storage, err := NewRedisStorage(ctx, cfg, ttl)
	if err != nil {
		return nil, fmt.Errorf("creating redis storage: %w", err)
	}
	return NewManagerWithStorage(ttl, factory, storage), nil
}

func (m *Manager) cleanupRoutine() {
	ticker := time.NewTicker(m.ttl / 2)
	defer ticker.Stop()
	for {
		select {
		case <-ticker.C:
			cutoff := time.Now().Add(-m.ttl)
			ctx, cancel := context.WithTimeout(context.Background(), cleanupOperationTimeout)
			if err := m.storage.DeleteExpired(ctx, cutoff); err != nil {
				slog.Error("failed to delete expired sessions", "error", err)
			}
			cancel()
		case <-m.stopCh:
			return
		}
	}
}

// validateSessionID returns an error if id is empty or not a valid UUID.
// UUID format is enforced across all storage backends to keep ID semantics consistent.
func validateSessionID(id string) error {
	if id == "" {
		return fmt.Errorf("session ID cannot be empty")
	}
	if _, err := uuid.Parse(id); err != nil {
		return fmt.Errorf("invalid session ID format: %w", err)
	}
	return nil
}

// AddWithID creates (and adds) a new session with the provided ID.
// Returns error if ID is empty, not a valid UUID, or already exists.
func (m *Manager) AddWithID(id string) error {
	if err := validateSessionID(id); err != nil {
		return err
	}
	// Check if session already exists
	ctx, cancel := context.WithTimeout(context.Background(), defaultOperationTimeout)
	defer cancel()

	if _, err := m.storage.Load(ctx, id); err == nil {
		return fmt.Errorf("session ID %q already exists", id)
	}

	// Create and store new session
	session := m.factory(id)
	return m.storage.Store(ctx, session)
}

// AddSession adds an existing session to the manager.
// This is useful when you need to create a session with specific properties.
func (m *Manager) AddSession(session Session) error {
	if session == nil {
		return fmt.Errorf("session cannot be nil")
	}
	if err := validateSessionID(session.ID()); err != nil {
		return err
	}

	// Check if session already exists
	ctx, cancel := context.WithTimeout(context.Background(), defaultOperationTimeout)
	defer cancel()

	if _, err := m.storage.Load(ctx, session.ID()); err == nil {
		return fmt.Errorf("session ID %q already exists", session.ID())
	}

	return m.storage.Store(ctx, session)
}

// Get retrieves a session by ID. Returns (session, true) if found.
// For LocalStorage, the storage backend updates the session's last-access timestamp on Load.
func (m *Manager) Get(id string) (Session, bool) {
	ctx, cancel := context.WithTimeout(context.Background(), defaultOperationTimeout)
	defer cancel()

	sess, err := m.storage.Load(ctx, id)
	if err != nil {
		return nil, false
	}
	return sess, true
}

// GetWithError retrieves a session by ID and returns the underlying storage
// error when the lookup fails. Callers that need to distinguish between
// ErrSessionNotFound (session absent or expired) and other storage errors
// (e.g. Redis connectivity failures) should use this method so they can
// return an appropriate HTTP status code (400 vs 503) to the client.
func (m *Manager) GetWithError(id string) (Session, error) {
	ctx, cancel := context.WithTimeout(context.Background(), defaultOperationTimeout)
	defer cancel()
	return m.storage.Load(ctx, id)
}

// UpsertSession inserts or updates a session in storage, replacing any existing
// session with the same ID. Used by SessionManager to replace placeholder sessions
// with fully-formed MultiSession objects after phase-2 construction.
func (m *Manager) UpsertSession(session Session) error {
	if session == nil {
		return fmt.Errorf("session cannot be nil")
	}
	if err := validateSessionID(session.ID()); err != nil {
		return err
	}
	ctx, cancel := context.WithTimeout(context.Background(), defaultOperationTimeout)
	defer cancel()
	return m.storage.Store(ctx, session)
}

// Delete removes a session by ID.
// Returns an error if the ID is invalid or the deletion fails.
func (m *Manager) Delete(id string) error {
	if err := validateSessionID(id); err != nil {
		return err
	}
	ctx, cancel := context.WithTimeout(context.Background(), defaultOperationTimeout)
	defer cancel()
	return m.storage.Delete(ctx, id)
}

// Stop stops the cleanup worker and closes the storage backend.
// Returns an error if closing the storage backend fails.
func (m *Manager) Stop() error {
	close(m.stopCh)
	if m.storage != nil {
		return m.storage.Close()
	}
	return nil
}

// Count returns the number of active sessions.
//
// Note: This method only works with LocalStorage backend and returns 0 for
// other storage backends. Count is not part of the Storage interface because
// it's not feasible for distributed storage backends like Redis where counting
// all keys can be prohibitively expensive.
//
// For distributed storage, consider maintaining a counter or using approximate
// count mechanisms provided by the storage backend.
func (m *Manager) Count() int {
	if localStorage, ok := m.storage.(*LocalStorage); ok {
		return localStorage.Count()
	}
	return 0
}

func (m *Manager) cleanupExpiredOnce() error {
	cutoff := time.Now().Add(-m.ttl)
	ctx, cancel := context.WithTimeout(context.Background(), cleanupOperationTimeout)
	defer cancel()
	return m.storage.DeleteExpired(ctx, cutoff)
}
