// Package cached provides caching wrappers for repositories.
package cached

import (
	"context"
	"sync"
	"time"

	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/port"
)

// ProjectRepository wraps another ProjectRepository with caching.
// The cache TTL determines how long the project list is cached before
// a refresh is needed. Individual project lookups are also cached.
type ProjectRepository struct {
	inner port.ProjectRepository
	ttl   time.Duration

	mu            sync.RWMutex
	projectsCache []domain.Project
	projectMap    map[domain.ProjectID]*domain.Project
	lastFetch     time.Time
}

// NewProjectRepository creates a caching wrapper around a ProjectRepository.
func NewProjectRepository(inner port.ProjectRepository, ttl time.Duration) *ProjectRepository {
	if ttl <= 0 {
		ttl = 30 * time.Second // Default cache TTL
	}
	return &ProjectRepository{
		inner:      inner,
		ttl:        ttl,
		projectMap: make(map[domain.ProjectID]*domain.Project),
	}
}

// List returns all projects, using cache if fresh.
func (r *ProjectRepository) List(ctx context.Context) ([]domain.Project, error) {
	// Check cache first
	r.mu.RLock()
	if r.isCacheFresh() {
		projects := make([]domain.Project, len(r.projectsCache))
		copy(projects, r.projectsCache)
		r.mu.RUnlock()
		return projects, nil
	}
	r.mu.RUnlock()

	// Cache miss - acquire write lock and refresh
	r.mu.Lock()
	defer r.mu.Unlock()

	// Double-check after acquiring write lock
	if r.isCacheFresh() {
		projects := make([]domain.Project, len(r.projectsCache))
		copy(projects, r.projectsCache)
		return projects, nil
	}

	// Fetch from inner repository
	projects, err := r.inner.List(ctx)
	if err != nil {
		return nil, err
	}

	// Update cache
	r.projectsCache = projects
	r.projectMap = make(map[domain.ProjectID]*domain.Project, len(projects))
	for i := range projects {
		r.projectMap[projects[i].ID] = &projects[i]
	}
	r.lastFetch = time.Now()

	// Return a copy to prevent mutation
	result := make([]domain.Project, len(projects))
	copy(result, projects)
	return result, nil
}

// Get returns a single project by ID, using cache if available.
func (r *ProjectRepository) Get(ctx context.Context, id domain.ProjectID) (*domain.Project, error) {
	// Check cache first
	r.mu.RLock()
	if r.isCacheFresh() {
		if p, ok := r.projectMap[id]; ok {
			// Return a copy
			copied := *p
			r.mu.RUnlock()
			return &copied, nil
		}
		r.mu.RUnlock()
		return nil, domain.ErrProjectNotFound
	}
	r.mu.RUnlock()

	// Cache stale - refresh and try again
	_, err := r.List(ctx)
	if err != nil {
		return nil, err
	}

	r.mu.RLock()
	defer r.mu.RUnlock()
	if p, ok := r.projectMap[id]; ok {
		copied := *p
		return &copied, nil
	}
	return nil, domain.ErrProjectNotFound
}

// Exists checks if a project exists by ID.
func (r *ProjectRepository) Exists(ctx context.Context, id domain.ProjectID) (bool, error) {
	r.mu.RLock()
	if r.isCacheFresh() {
		_, exists := r.projectMap[id]
		r.mu.RUnlock()
		return exists, nil
	}
	r.mu.RUnlock()

	// Cache stale - refresh
	_, err := r.List(ctx)
	if err != nil {
		return false, err
	}

	r.mu.RLock()
	defer r.mu.RUnlock()
	_, exists := r.projectMap[id]
	return exists, nil
}

// RefreshStatus refreshes project status from the underlying repository.
// This bypasses the cache and forces a refresh.
func (r *ProjectRepository) RefreshStatus(ctx context.Context) error {
	err := r.inner.RefreshStatus(ctx)
	if err != nil {
		return err
	}

	// Invalidate cache so next List() fetches fresh data
	r.mu.Lock()
	r.lastFetch = time.Time{} // Zero time = stale
	r.mu.Unlock()

	return nil
}

// Register is a pass-through that invalidates cache after registration.
func (r *ProjectRepository) Register(ctx context.Context, p *domain.Project) error {
	err := r.inner.Register(ctx, p)
	if err != nil {
		return err
	}

	r.mu.Lock()
	r.lastFetch = time.Time{} // Invalidate cache
	r.mu.Unlock()

	return nil
}

// Unregister is a pass-through that invalidates cache after unregistration.
func (r *ProjectRepository) Unregister(ctx context.Context, id domain.ProjectID) error {
	err := r.inner.Unregister(ctx, id)
	if err != nil {
		return err
	}

	r.mu.Lock()
	r.lastFetch = time.Time{} // Invalidate cache
	r.mu.Unlock()

	return nil
}

// isCacheFresh checks if the cache is still within TTL.
// Must be called with at least a read lock held.
func (r *ProjectRepository) isCacheFresh() bool {
	if r.lastFetch.IsZero() {
		return false
	}
	return time.Since(r.lastFetch) < r.ttl
}

// Invalidate forces a cache refresh on next access.
func (r *ProjectRepository) Invalidate() {
	r.mu.Lock()
	r.lastFetch = time.Time{}
	r.mu.Unlock()
}

// CacheStats returns statistics about the cache.
func (r *ProjectRepository) CacheStats() CacheStats {
	r.mu.RLock()
	defer r.mu.RUnlock()

	return CacheStats{
		Size:      len(r.projectsCache),
		LastFetch: r.lastFetch,
		IsFresh:   r.isCacheFresh(),
		TTL:       r.ttl,
	}
}

// CacheStats contains cache statistics.
type CacheStats struct {
	Size      int
	LastFetch time.Time
	IsFresh   bool
	TTL       time.Duration
}
