package queue

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"sync"
	"time"

	"github.com/igodwin/notifier/internal/domain"
	"github.com/google/uuid"
)

// LocalQueue is an in-memory queue implementation
type LocalQueue struct {
	queue          chan *domain.QueueMessage
	messages       map[string]*domain.QueueMessage
	mu             sync.RWMutex
	config         *domain.LocalQueueConfig
	persistToDisk  bool
	persistPath    string
	closed         bool
	closeChan      chan struct{}
}

// NewLocalQueue creates a new local queue instance
func NewLocalQueue(config *domain.LocalQueueConfig) (*LocalQueue, error) {
	if config == nil {
		config = &domain.LocalQueueConfig{
			BufferSize:    1000,
			PersistToDisk: false,
		}
	}

	lq := &LocalQueue{
		queue:         make(chan *domain.QueueMessage, config.BufferSize),
		messages:      make(map[string]*domain.QueueMessage),
		config:        config,
		persistToDisk: config.PersistToDisk,
		persistPath:   config.PersistPath,
		closeChan:     make(chan struct{}),
	}

	// Load persisted messages if enabled
	if lq.persistToDisk && lq.persistPath != "" {
		if err := lq.loadFromDisk(); err != nil {
			return nil, fmt.Errorf("failed to load persisted queue: %w", err)
		}
	}

	return lq, nil
}

// Enqueue adds a notification to the queue
func (lq *LocalQueue) Enqueue(ctx context.Context, notification *domain.Notification) error {
	lq.mu.Lock()
	defer lq.mu.Unlock()

	if lq.closed {
		return fmt.Errorf("queue is closed")
	}

	msg := &domain.QueueMessage{
		ID:           uuid.New().String(),
		Notification: notification,
		Attempt:      0,
		EnqueuedAt:   time.Now().Unix(),
	}

	select {
	case lq.queue <- msg:
		lq.messages[msg.ID] = msg
		notification.Status = domain.StatusQueued

		if lq.persistToDisk {
			return lq.persistToDiskSync()
		}
		return nil
	case <-ctx.Done():
		return ctx.Err()
	case <-lq.closeChan:
		return fmt.Errorf("queue is closed")
	}
}

// EnqueueBatch adds multiple notifications to the queue
func (lq *LocalQueue) EnqueueBatch(ctx context.Context, notifications []*domain.Notification) error {
	lq.mu.Lock()
	defer lq.mu.Unlock()

	if lq.closed {
		return fmt.Errorf("queue is closed")
	}

	for _, notification := range notifications {
		msg := &domain.QueueMessage{
			ID:           uuid.New().String(),
			Notification: notification,
			Attempt:      0,
			EnqueuedAt:   time.Now().Unix(),
		}

		select {
		case lq.queue <- msg:
			lq.messages[msg.ID] = msg
			notification.Status = domain.StatusQueued
		case <-ctx.Done():
			return ctx.Err()
		case <-lq.closeChan:
			return fmt.Errorf("queue is closed")
		}
	}

	if lq.persistToDisk {
		return lq.persistToDiskSync()
	}
	return nil
}

// Dequeue retrieves the next notification from the queue
func (lq *LocalQueue) Dequeue(ctx context.Context) (*domain.QueueMessage, error) {
	if lq.closed {
		return nil, fmt.Errorf("queue is closed")
	}

	select {
	case msg := <-lq.queue:
		lq.mu.Lock()
		msg.Attempt++
		msg.Notification.Status = domain.StatusProcessing
		lq.mu.Unlock()
		return msg, nil
	case <-ctx.Done():
		return nil, ctx.Err()
	case <-lq.closeChan:
		return nil, fmt.Errorf("queue is closed")
	}
}

// Ack acknowledges successful processing of a message
func (lq *LocalQueue) Ack(ctx context.Context, messageID string) error {
	lq.mu.Lock()
	defer lq.mu.Unlock()

	if msg, exists := lq.messages[messageID]; exists {
		msg.Notification.Status = domain.StatusSent
		delete(lq.messages, messageID)

		if lq.persistToDisk {
			return lq.persistToDiskSync()
		}
	}

	return nil
}

// Nack indicates processing failure and may requeue the message
func (lq *LocalQueue) Nack(ctx context.Context, messageID string, requeue bool) error {
	lq.mu.Lock()
	defer lq.mu.Unlock()

	msg, exists := lq.messages[messageID]
	if !exists {
		return fmt.Errorf("message not found: %s", messageID)
	}

	if requeue {
		msg.Notification.Status = domain.StatusRetrying
		select {
		case lq.queue <- msg:
			if lq.persistToDisk {
				return lq.persistToDiskSync()
			}
			return nil
		case <-ctx.Done():
			return ctx.Err()
		case <-lq.closeChan:
			return fmt.Errorf("queue is closed")
		}
	} else {
		msg.Notification.Status = domain.StatusFailed
		delete(lq.messages, messageID)

		if lq.persistToDisk {
			return lq.persistToDiskSync()
		}
	}

	return nil
}

// Size returns the current number of messages in the queue
func (lq *LocalQueue) Size(ctx context.Context) (int64, error) {
	lq.mu.RLock()
	defer lq.mu.RUnlock()
	return int64(len(lq.queue)), nil
}

// Purge removes all messages from the queue
func (lq *LocalQueue) Purge(ctx context.Context) error {
	lq.mu.Lock()
	defer lq.mu.Unlock()

	// Drain the channel
	for len(lq.queue) > 0 {
		<-lq.queue
	}

	lq.messages = make(map[string]*domain.QueueMessage)

	if lq.persistToDisk {
		return lq.persistToDiskSync()
	}

	return nil
}

// Close cleanly shuts down the queue
func (lq *LocalQueue) Close() error {
	lq.mu.Lock()
	defer lq.mu.Unlock()

	if lq.closed {
		return nil
	}

	lq.closed = true
	close(lq.closeChan)

	if lq.persistToDisk {
		if err := lq.persistToDiskSync(); err != nil {
			return err
		}
	}

	close(lq.queue)
	return nil
}

// HealthCheck verifies the queue is operational
func (lq *LocalQueue) HealthCheck(ctx context.Context) error {
	lq.mu.RLock()
	defer lq.mu.RUnlock()

	if lq.closed {
		return fmt.Errorf("queue is closed")
	}

	return nil
}

// persistToDiskSync persists the queue state to disk (must be called with lock held)
func (lq *LocalQueue) persistToDiskSync() error {
	if !lq.persistToDisk || lq.persistPath == "" {
		return nil
	}

	data, err := json.Marshal(lq.messages)
	if err != nil {
		return fmt.Errorf("failed to marshal queue state: %w", err)
	}

	if err := os.WriteFile(lq.persistPath, data, 0644); err != nil {
		return fmt.Errorf("failed to write queue state: %w", err)
	}

	return nil
}

// loadFromDisk loads the queue state from disk
func (lq *LocalQueue) loadFromDisk() error {
	if lq.persistPath == "" {
		return nil
	}

	data, err := os.ReadFile(lq.persistPath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil // No persisted state yet
		}
		return fmt.Errorf("failed to read queue state: %w", err)
	}

	var messages map[string]*domain.QueueMessage
	if err := json.Unmarshal(data, &messages); err != nil {
		return fmt.Errorf("failed to unmarshal queue state: %w", err)
	}

	// Re-enqueue persisted messages
	for _, msg := range messages {
		lq.queue <- msg
		lq.messages[msg.ID] = msg
	}

	return nil
}
