// Package state provides state management for AutoSSO.
// This file implements the file-based storage backend for the Store interface.
// It serves as a backup option when MySQL is not available.
//
// Requirements: 4.5, 4.6
package state

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"syscall"
	"time"
)

// File names for data storage
const (
	approvalInstancesFile  = "approval_instances.json"
	pendingRemovalsFile    = "pending_removals.json"
	processedInstancesFile = "processed_instances.json"
	lockFileExtension      = ".lock"
	defaultLockTimeout     = 10 * time.Second
	defaultLockRetryDelay  = 50 * time.Millisecond
)

// FileStore implements the Store interface using JSON files as the backend.
// It provides file locking for concurrent access safety.
//
// Requirements: 4.5, 4.6
type FileStore struct {
	dataDir     string
	lockTimeout time.Duration
	mu          sync.RWMutex
}

// FileStoreConfig contains configuration for the file store.
type FileStoreConfig struct {
	// DataDir is the directory where data files are stored
	DataDir string
	// LockTimeout is the maximum time to wait for acquiring a file lock
	LockTimeout time.Duration
}

// fileData represents the structure of the approval instances JSON file
type fileData struct {
	Instances []*ApprovalInstance `json:"instances"`
	UpdatedAt time.Time           `json:"updated_at"`
}

// pendingRemovalsData represents the structure of the pending removals JSON file
type pendingRemovalsData struct {
	Removals  []*PendingRemoval `json:"removals"`
	NextID    int64             `json:"next_id"`
	UpdatedAt time.Time         `json:"updated_at"`
}

// processedInstancesData represents the structure of the processed instances JSON file
type processedInstancesData struct {
	InstanceIDs map[string]time.Time `json:"instance_ids"`
	UpdatedAt   time.Time            `json:"updated_at"`
}

// NewFileStore creates a new file store with the given configuration.
// It initializes the data directory and creates empty data files if they don't exist.
//
// Parameters:
//   - cfg: File store configuration containing data directory and lock timeout
//
// Returns:
//   - *FileStore: The initialized file store
//   - error: Any error that occurred during initialization
//
// Requirements: 4.5
func NewFileStore(cfg *FileStoreConfig) (*FileStore, error) {
	if cfg == nil {
		return nil, errors.New("file store configuration is required")
	}

	if cfg.DataDir == "" {
		return nil, errors.New("data directory is required")
	}

	// Create data directory if it doesn't exist
	if err := os.MkdirAll(cfg.DataDir, 0755); err != nil {
		return nil, fmt.Errorf("failed to create data directory: %w", err)
	}

	lockTimeout := cfg.LockTimeout
	if lockTimeout == 0 {
		lockTimeout = defaultLockTimeout
	}

	store := &FileStore{
		dataDir:     cfg.DataDir,
		lockTimeout: lockTimeout,
	}

	// Initialize data files if they don't exist
	if err := store.initializeDataFiles(); err != nil {
		return nil, fmt.Errorf("failed to initialize data files: %w", err)
	}

	return store, nil
}

// initializeDataFiles creates empty data files if they don't exist
func (s *FileStore) initializeDataFiles() error {
	// Initialize approval instances file
	instancesPath := filepath.Join(s.dataDir, approvalInstancesFile)
	if _, err := os.Stat(instancesPath); os.IsNotExist(err) {
		data := &fileData{
			Instances: []*ApprovalInstance{},
			UpdatedAt: time.Now(),
		}
		if err := s.writeJSONFile(instancesPath, data); err != nil {
			return fmt.Errorf("failed to initialize instances file: %w", err)
		}
	}

	// Initialize pending removals file
	removalsPath := filepath.Join(s.dataDir, pendingRemovalsFile)
	if _, err := os.Stat(removalsPath); os.IsNotExist(err) {
		data := &pendingRemovalsData{
			Removals:  []*PendingRemoval{},
			NextID:    1,
			UpdatedAt: time.Now(),
		}
		if err := s.writeJSONFile(removalsPath, data); err != nil {
			return fmt.Errorf("failed to initialize removals file: %w", err)
		}
	}

	// Initialize processed instances file
	processedPath := filepath.Join(s.dataDir, processedInstancesFile)
	if _, err := os.Stat(processedPath); os.IsNotExist(err) {
		data := &processedInstancesData{
			InstanceIDs: make(map[string]time.Time),
			UpdatedAt:   time.Now(),
		}
		if err := s.writeJSONFile(processedPath, data); err != nil {
			return fmt.Errorf("failed to initialize processed file: %w", err)
		}
	}

	return nil
}

// fileLock represents a file lock
type fileLock struct {
	file *os.File
	path string
}

// acquireLock acquires an exclusive lock on the specified file.
// It uses syscall.Flock for file locking with timeout support.
//
// Requirements: 4.6
func (s *FileStore) acquireLock(ctx context.Context, filePath string) (*fileLock, error) {
	lockPath := filePath + lockFileExtension

	// Create lock file if it doesn't exist
	file, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0644)
	if err != nil {
		return nil, fmt.Errorf("failed to open lock file: %w", err)
	}

	// Try to acquire lock with timeout
	deadline := time.Now().Add(s.lockTimeout)
	for {
		// Check context cancellation
		select {
		case <-ctx.Done():
			file.Close()
			return nil, ctx.Err()
		default:
		}

		// Try non-blocking lock
		err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
		if err == nil {
			return &fileLock{file: file, path: lockPath}, nil
		}

		// Check if we've exceeded the timeout
		if time.Now().After(deadline) {
			file.Close()
			return nil, fmt.Errorf("failed to acquire lock within timeout: %w", err)
		}

		// Wait before retrying
		select {
		case <-ctx.Done():
			file.Close()
			return nil, ctx.Err()
		case <-time.After(defaultLockRetryDelay):
		}
	}
}

// releaseLock releases the file lock
func (s *FileStore) releaseLock(lock *fileLock) error {
	if lock == nil || lock.file == nil {
		return nil
	}

	// Release the lock
	if err := syscall.Flock(int(lock.file.Fd()), syscall.LOCK_UN); err != nil {
		lock.file.Close()
		return fmt.Errorf("failed to release lock: %w", err)
	}

	return lock.file.Close()
}

// readJSONFile reads and unmarshals a JSON file
func (s *FileStore) readJSONFile(filePath string, v interface{}) error {
	data, err := os.ReadFile(filePath)
	if err != nil {
		return fmt.Errorf("failed to read file: %w", err)
	}

	if err := json.Unmarshal(data, v); err != nil {
		return fmt.Errorf("failed to unmarshal JSON: %w", err)
	}

	return nil
}

// writeJSONFile marshals and writes data to a JSON file
func (s *FileStore) writeJSONFile(filePath string, v interface{}) error {
	data, err := json.MarshalIndent(v, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to marshal JSON: %w", err)
	}

	// Write to a temporary file first, then rename for atomicity
	tmpPath := filePath + ".tmp"
	if err := os.WriteFile(tmpPath, data, 0644); err != nil {
		return fmt.Errorf("failed to write temporary file: %w", err)
	}

	if err := os.Rename(tmpPath, filePath); err != nil {
		os.Remove(tmpPath) // Clean up temp file on error
		return fmt.Errorf("failed to rename temporary file: %w", err)
	}

	return nil
}

// SaveInstance saves or updates an approval instance.
// If an instance with the same ID exists, it will be updated.
//
// Requirements: 4.5, 4.6
func (s *FileStore) SaveInstance(ctx context.Context, instance *ApprovalInstance) error {
	if instance == nil {
		return errors.New("instance is required")
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	filePath := filepath.Join(s.dataDir, approvalInstancesFile)

	// Acquire file lock
	lock, err := s.acquireLock(ctx, filePath)
	if err != nil {
		return fmt.Errorf("failed to acquire lock: %w", err)
	}
	defer s.releaseLock(lock)

	// Read existing data
	var data fileData
	if err := s.readJSONFile(filePath, &data); err != nil {
		return fmt.Errorf("failed to read instances file: %w", err)
	}

	// Update timestamps
	now := time.Now()
	if instance.CreatedAt.IsZero() {
		instance.CreatedAt = now
	}
	instance.UpdatedAt = now

	// Find and update existing instance or add new one
	found := false
	for i, existing := range data.Instances {
		if existing.ID == instance.ID {
			data.Instances[i] = instance
			found = true
			break
		}
	}

	if !found {
		data.Instances = append(data.Instances, instance)
	}

	data.UpdatedAt = now

	// Write updated data
	if err := s.writeJSONFile(filePath, &data); err != nil {
		return fmt.Errorf("failed to write instances file: %w", err)
	}

	return nil
}

// GetInstance retrieves an approval instance by its ID.
//
// Requirements: 4.5
func (s *FileStore) GetInstance(ctx context.Context, instanceID string) (*ApprovalInstance, error) {
	if instanceID == "" {
		return nil, errors.New("instance ID is required")
	}

	s.mu.RLock()
	defer s.mu.RUnlock()

	filePath := filepath.Join(s.dataDir, approvalInstancesFile)

	// Acquire file lock
	lock, err := s.acquireLock(ctx, filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to acquire lock: %w", err)
	}
	defer s.releaseLock(lock)

	// Read data
	var data fileData
	if err := s.readJSONFile(filePath, &data); err != nil {
		return nil, fmt.Errorf("failed to read instances file: %w", err)
	}

	// Find instance
	for _, instance := range data.Instances {
		if instance.ID == instanceID {
			// Return a copy to prevent external modification
			result := *instance
			return &result, nil
		}
	}

	return nil, nil // Not found
}

// ListInstances retrieves approval instances matching the filter criteria.
// If filter is nil, all instances are returned.
//
// Requirements: 4.5
func (s *FileStore) ListInstances(ctx context.Context, filter *InstanceFilter) ([]*ApprovalInstance, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	filePath := filepath.Join(s.dataDir, approvalInstancesFile)

	// Acquire file lock
	lock, err := s.acquireLock(ctx, filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to acquire lock: %w", err)
	}
	defer s.releaseLock(lock)

	// Read data
	var data fileData
	if err := s.readJSONFile(filePath, &data); err != nil {
		return nil, fmt.Errorf("failed to read instances file: %w", err)
	}

	// Filter instances
	var results []*ApprovalInstance
	for _, instance := range data.Instances {
		if s.matchesFilter(instance, filter) {
			// Return a copy to prevent external modification
			result := *instance
			results = append(results, &result)
		}
	}

	// Apply pagination
	if filter != nil {
		if filter.Offset > 0 && filter.Offset < len(results) {
			results = results[filter.Offset:]
		} else if filter.Offset >= len(results) {
			results = nil
		}

		if filter.Limit > 0 && filter.Limit < len(results) {
			results = results[:filter.Limit]
		}
	}

	return results, nil
}

// matchesFilter checks if an instance matches the filter criteria
func (s *FileStore) matchesFilter(instance *ApprovalInstance, filter *InstanceFilter) bool {
	if filter == nil {
		return true
	}

	if filter.Status != "" && instance.Status != filter.Status {
		return false
	}

	if filter.CloudType != "" && instance.CloudType != filter.CloudType {
		return false
	}

	if filter.Email != "" && instance.Email != filter.Email {
		return false
	}

	if filter.AccountID != "" && instance.AccountID != filter.AccountID {
		return false
	}

	if !filter.StartTime.IsZero() && instance.StartTime.Before(filter.StartTime) {
		return false
	}

	if !filter.EndTime.IsZero() && instance.EndTime.After(filter.EndTime) {
		return false
	}

	return true
}

// DeleteInstance removes an approval instance by its ID.
//
// Requirements: 4.5, 4.6
func (s *FileStore) DeleteInstance(ctx context.Context, instanceID string) error {
	if instanceID == "" {
		return errors.New("instance ID is required")
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	filePath := filepath.Join(s.dataDir, approvalInstancesFile)

	// Acquire file lock
	lock, err := s.acquireLock(ctx, filePath)
	if err != nil {
		return fmt.Errorf("failed to acquire lock: %w", err)
	}
	defer s.releaseLock(lock)

	// Read existing data
	var data fileData
	if err := s.readJSONFile(filePath, &data); err != nil {
		return fmt.Errorf("failed to read instances file: %w", err)
	}

	// Find and remove instance
	for i, instance := range data.Instances {
		if instance.ID == instanceID {
			data.Instances = append(data.Instances[:i], data.Instances[i+1:]...)
			break
		}
	}

	data.UpdatedAt = time.Now()

	// Write updated data
	if err := s.writeJSONFile(filePath, &data); err != nil {
		return fmt.Errorf("failed to write instances file: %w", err)
	}

	return nil
}

// SavePendingRemoval saves a pending permission removal record.
//
// Requirements: 4.5, 4.6
func (s *FileStore) SavePendingRemoval(ctx context.Context, removal *PendingRemoval) error {
	if removal == nil {
		return errors.New("removal is required")
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	filePath := filepath.Join(s.dataDir, pendingRemovalsFile)

	// Acquire file lock
	lock, err := s.acquireLock(ctx, filePath)
	if err != nil {
		return fmt.Errorf("failed to acquire lock: %w", err)
	}
	defer s.releaseLock(lock)

	// Read existing data
	var data pendingRemovalsData
	if err := s.readJSONFile(filePath, &data); err != nil {
		return fmt.Errorf("failed to read removals file: %w", err)
	}

	now := time.Now()
	if removal.CreatedAt.IsZero() {
		removal.CreatedAt = now
	}

	// Check for existing removal with same key (account_id, email, permission_set)
	found := false
	for i, existing := range data.Removals {
		if existing.AccountID == removal.AccountID &&
			existing.Email == removal.Email &&
			existing.PermissionSet == removal.PermissionSet {
			// Update existing removal
			removal.ID = existing.ID
			data.Removals[i] = removal
			found = true
			break
		}
	}

	if !found {
		// Assign new ID
		removal.ID = data.NextID
		data.NextID++
		data.Removals = append(data.Removals, removal)
	}

	data.UpdatedAt = now

	// Write updated data
	if err := s.writeJSONFile(filePath, &data); err != nil {
		return fmt.Errorf("failed to write removals file: %w", err)
	}

	return nil
}

// GetPendingRemovals retrieves all pending permission removal records.
//
// Requirements: 4.5
func (s *FileStore) GetPendingRemovals(ctx context.Context) ([]*PendingRemoval, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	filePath := filepath.Join(s.dataDir, pendingRemovalsFile)

	// Acquire file lock
	lock, err := s.acquireLock(ctx, filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to acquire lock: %w", err)
	}
	defer s.releaseLock(lock)

	// Read data
	var data pendingRemovalsData
	if err := s.readJSONFile(filePath, &data); err != nil {
		return nil, fmt.Errorf("failed to read removals file: %w", err)
	}

	// Return copies to prevent external modification
	results := make([]*PendingRemoval, len(data.Removals))
	for i, removal := range data.Removals {
		r := *removal
		results[i] = &r
	}

	return results, nil
}

// DeletePendingRemoval removes a pending removal record by its ID.
//
// Requirements: 4.5, 4.6
func (s *FileStore) DeletePendingRemoval(ctx context.Context, id int64) error {
	if id <= 0 {
		return errors.New("valid ID is required")
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	filePath := filepath.Join(s.dataDir, pendingRemovalsFile)

	// Acquire file lock
	lock, err := s.acquireLock(ctx, filePath)
	if err != nil {
		return fmt.Errorf("failed to acquire lock: %w", err)
	}
	defer s.releaseLock(lock)

	// Read existing data
	var data pendingRemovalsData
	if err := s.readJSONFile(filePath, &data); err != nil {
		return fmt.Errorf("failed to read removals file: %w", err)
	}

	// Find and remove
	for i, removal := range data.Removals {
		if removal.ID == id {
			data.Removals = append(data.Removals[:i], data.Removals[i+1:]...)
			break
		}
	}

	data.UpdatedAt = time.Now()

	// Write updated data
	if err := s.writeJSONFile(filePath, &data); err != nil {
		return fmt.Errorf("failed to write removals file: %w", err)
	}

	return nil
}

// SaveDeletedInstance marks an instance as processed/deleted.
//
// Requirements: 4.5, 4.6
func (s *FileStore) SaveDeletedInstance(ctx context.Context, instanceID string) error {
	if instanceID == "" {
		return errors.New("instance ID is required")
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	filePath := filepath.Join(s.dataDir, processedInstancesFile)

	// Acquire file lock
	lock, err := s.acquireLock(ctx, filePath)
	if err != nil {
		return fmt.Errorf("failed to acquire lock: %w", err)
	}
	defer s.releaseLock(lock)

	// Read existing data
	var data processedInstancesData
	if err := s.readJSONFile(filePath, &data); err != nil {
		return fmt.Errorf("failed to read processed file: %w", err)
	}

	if data.InstanceIDs == nil {
		data.InstanceIDs = make(map[string]time.Time)
	}

	now := time.Now()
	data.InstanceIDs[instanceID] = now
	data.UpdatedAt = now

	// Write updated data
	if err := s.writeJSONFile(filePath, &data); err != nil {
		return fmt.Errorf("failed to write processed file: %w", err)
	}

	return nil
}

// IsInstanceDeleted checks if an instance has been processed/deleted.
//
// Requirements: 4.5
func (s *FileStore) IsInstanceDeleted(ctx context.Context, instanceID string) (bool, error) {
	if instanceID == "" {
		return false, errors.New("instance ID is required")
	}

	s.mu.RLock()
	defer s.mu.RUnlock()

	filePath := filepath.Join(s.dataDir, processedInstancesFile)

	// Acquire file lock
	lock, err := s.acquireLock(ctx, filePath)
	if err != nil {
		return false, fmt.Errorf("failed to acquire lock: %w", err)
	}
	defer s.releaseLock(lock)

	// Read data
	var data processedInstancesData
	if err := s.readJSONFile(filePath, &data); err != nil {
		return false, fmt.Errorf("failed to read processed file: %w", err)
	}

	_, exists := data.InstanceIDs[instanceID]
	return exists, nil
}

// Close releases any resources held by the store.
//
// Requirements: 4.5
func (s *FileStore) Close() error {
	// File store doesn't hold persistent connections,
	// so there's nothing to close
	return nil
}

// GetReadyRemovals retrieves pending removals that are ready to be processed.
// A removal is ready if the current time is after ScheduledAt.
func (s *FileStore) GetReadyRemovals(ctx context.Context) ([]*PendingRemoval, error) {
	removals, err := s.GetPendingRemovals(ctx)
	if err != nil {
		return nil, err
	}

	now := time.Now()
	var ready []*PendingRemoval
	for _, removal := range removals {
		if now.After(removal.ScheduledAt) {
			ready = append(ready, removal)
		}
	}

	return ready, nil
}

// Ensure FileStore implements Store interface
var _ Store = (*FileStore)(nil)
