package config

import (
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"sync"
	"time"

	"github.com/fsnotify/fsnotify"
	"github.com/google/uuid"
	"github.com/joho/godotenv"
	"github.com/rcourtman/pulse-go-rewrite/internal/auth"
	"github.com/rs/zerolog/log"
)

// ConfigWatcher monitors the .env file for changes and updates runtime config
type ConfigWatcher struct {
	config               *Config
	envPath              string
	mockEnvPath          string
	apiTokensPath        string
	watcher              *fsnotify.Watcher
	stopChan             chan struct{}
	stopOnce             sync.Once // Ensures Stop() can only close channel once
	lastModTime          time.Time
	mockLastModTime      time.Time
	apiTokensLastModTime time.Time
	mu                   sync.RWMutex
	onMockReload         func() // Callback to trigger backend restart
}

// NewConfigWatcher creates a new config watcher
func NewConfigWatcher(config *Config) (*ConfigWatcher, error) {
	// CRITICAL FIX: Config watcher must ALWAYS watch the persistent production config,
	// NOT the mock data directory. Mock mode should only affect Proxmox data, not auth.
	//
	// Strategy:
	// 1. Check PULSE_AUTH_CONFIG_DIR (dedicated env var for auth config)
	// 2. If PULSE_DATA_DIR looks like production (/etc/pulse or /data), use it
	// 3. Otherwise, always prefer /etc/pulse if it exists (production auth)
	// 4. Fall back to /data for Docker environments
	// 5. Last resort: use PULSE_DATA_DIR (may be mock/dev)

	persistentDataDir := ""
	dataDir := os.Getenv("PULSE_DATA_DIR")

	// Option 1: Explicit auth config directory override
	if authDir := os.Getenv("PULSE_AUTH_CONFIG_DIR"); authDir != "" {
		persistentDataDir = authDir
		log.Info().Str("authConfigDir", authDir).Msg("Using PULSE_AUTH_CONFIG_DIR for auth config")
	} else if dataDir == "/etc/pulse" || dataDir == "/data" {
		// Option 2: PULSE_DATA_DIR is already production, use it
		persistentDataDir = dataDir
	} else if _, err := os.Stat("/etc/pulse/.env"); err == nil {
		// Option 3: /etc/pulse exists, use it (production)
		persistentDataDir = "/etc/pulse"
		if dataDir != "" && dataDir != persistentDataDir {
			log.Warn().
				Str("dataDir", dataDir).
				Str("authConfigDir", persistentDataDir).
				Msg("PULSE_DATA_DIR points to non-production directory - using /etc/pulse for auth config instead")
		}
	} else if _, err := os.Stat("/data/.env"); err == nil {
		// Option 4: Docker environment
		persistentDataDir = "/data"
	} else if dataDir != "" {
		// Option 5: Use PULSE_DATA_DIR as fallback
		persistentDataDir = dataDir
		if strings.Contains(persistentDataDir, "/mock-data") || strings.Contains(persistentDataDir, "/tmp/") {
			log.Warn().
				Str("authConfigDir", persistentDataDir).
				Msg("WARNING: Auth config watcher is using temporary/mock directory - auth may be unstable")
		}
	} else {
		// Option 6: Last resort default
		persistentDataDir = "/etc/pulse"
	}

	envPath := filepath.Join(persistentDataDir, ".env")

	// Log what we're watching for debugging
	log.Info().
		Str("watchingPath", envPath).
		Str("authConfigDir", persistentDataDir).
		Str("pulseDataDir", dataDir).
		Str("configPathFromConfig", config.ConfigPath).
		Msg("Config watcher initialized - watching production auth config")

	// Determine mock.env path - skip in Docker or if directory doesn't exist
	mockEnvPath := ""
	isDocker := os.Getenv("PULSE_DOCKER") == "true"
	mockDir := "/opt/pulse"
	if !isDocker {
		if stat, err := os.Stat(mockDir); err == nil && stat.IsDir() {
			mockEnvPath = filepath.Join(mockDir, "mock.env")
		}
	}

	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		return nil, err
	}

	apiTokensPath := filepath.Join(filepath.Dir(envPath), "api_tokens.json")

	cw := &ConfigWatcher{
		config:        config,
		envPath:       envPath,
		mockEnvPath:   mockEnvPath,
		apiTokensPath: apiTokensPath,
		watcher:       watcher,
		stopChan:      make(chan struct{}),
	}

	// Get initial mod times
	if stat, err := os.Stat(envPath); err == nil {
		cw.lastModTime = stat.ModTime()
	}
	if mockEnvPath != "" {
		if stat, err := os.Stat(mockEnvPath); err == nil {
			cw.mockLastModTime = stat.ModTime()
		}
	}
	if stat, err := os.Stat(apiTokensPath); err == nil {
		cw.apiTokensLastModTime = stat.ModTime()
	}

	return cw, nil
}

// SetMockReloadCallback sets the callback function to trigger when mock.env changes
func (cw *ConfigWatcher) SetMockReloadCallback(callback func()) {
	cw.mu.Lock()
	defer cw.mu.Unlock()
	cw.onMockReload = callback
}

// Start begins watching the config file
func (cw *ConfigWatcher) Start() error {
	// Watch the directory for .env
	dir := filepath.Dir(cw.envPath)
	err := cw.watcher.Add(dir)
	if err != nil {
		log.Warn().Err(err).Str("path", dir).Msg("Failed to watch config directory")
	}

	// Also watch the mock.env directory if it's configured (not in Docker)
	if cw.mockEnvPath != "" {
		mockDir := filepath.Dir(cw.mockEnvPath)
		if err := cw.watcher.Add(mockDir); err != nil {
			log.Warn().Err(err).Str("path", mockDir).Msg("Failed to watch mock.env directory")
		}
	}

	if err != nil {
		log.Warn().Msg("Falling back to polling for config changes")
		go cw.pollForChanges()
		return nil
	}

	go cw.watchForChanges()
	logEvent := log.Info().Str("env_path", cw.envPath).Str("api_tokens_path", cw.apiTokensPath)
	if cw.mockEnvPath != "" {
		logEvent = logEvent.Str("mock_env_path", cw.mockEnvPath)
	}
	logEvent.Msg("Started watching config files for changes")
	return nil
}

// Stop stops the config watcher
func (cw *ConfigWatcher) Stop() {
	// Use sync.Once to prevent double-close panic
	cw.stopOnce.Do(func() {
		close(cw.stopChan)
		cw.watcher.Close()
	})
}

// ReloadConfig manually triggers a config reload (e.g., from SIGHUP)
func (cw *ConfigWatcher) ReloadConfig() {
	cw.reloadConfig()
}

// watchForChanges handles fsnotify events
func (cw *ConfigWatcher) watchForChanges() {
	for {
		select {
		case event, ok := <-cw.watcher.Events:
			if !ok {
				return
			}

			// Check if the event is for our .env file
			if filepath.Base(event.Name) == ".env" || event.Name == cw.envPath {
				// Debounce - wait a bit for write to complete
				time.Sleep(100 * time.Millisecond)

				if event.Op&(fsnotify.Write|fsnotify.Create) != 0 {
					log.Info().Str("event", event.Op.String()).Msg("Detected .env file change")
					cw.reloadConfig()
				}
			}

			if cw.apiTokensPath != "" && (filepath.Base(event.Name) == filepath.Base(cw.apiTokensPath) || event.Name == cw.apiTokensPath) {
				// Debounce - wait longer for atomic file operations to complete
				// (write to .tmp, rename to final file)
				time.Sleep(250 * time.Millisecond)

				if event.Op&(fsnotify.Write|fsnotify.Create) != 0 {
					log.Info().Str("event", event.Op.String()).Msg("Detected API token file change")
					cw.reloadAPITokens()
				}
			}

			// Check if the event is for mock.env (only if mock.env watching is enabled)
			if cw.mockEnvPath != "" && (filepath.Base(event.Name) == "mock.env" || event.Name == cw.mockEnvPath) {
				// Debounce - wait a bit for write to complete
				time.Sleep(100 * time.Millisecond)

				if event.Op&(fsnotify.Write|fsnotify.Create) != 0 {
					log.Info().Str("event", event.Op.String()).Msg("Detected mock.env file change")
					cw.reloadMockConfig()
				}
			}

		case err, ok := <-cw.watcher.Errors:
			if !ok {
				return
			}
			log.Error().Err(err).Msg("Config watcher error")

		case <-cw.stopChan:
			return
		}
	}
}

// pollForChanges is a fallback that polls for changes
func (cw *ConfigWatcher) pollForChanges() {
	ticker := time.NewTicker(5 * time.Second)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			// Check .env
			if stat, err := os.Stat(cw.envPath); err == nil {
				if stat.ModTime().After(cw.lastModTime) {
					log.Info().Msg("Detected .env file change via polling")
					cw.lastModTime = stat.ModTime()
					cw.reloadConfig()
				}
			}

			// Check mock.env (only if mock.env watching is enabled)
			if cw.mockEnvPath != "" {
				if stat, err := os.Stat(cw.mockEnvPath); err == nil {
					if stat.ModTime().After(cw.mockLastModTime) {
						log.Info().Msg("Detected mock.env file change via polling")
						cw.mockLastModTime = stat.ModTime()
						cw.reloadMockConfig()
					}
				}
			}

			if cw.apiTokensPath != "" {
				if stat, err := os.Stat(cw.apiTokensPath); err == nil {
					if stat.ModTime().After(cw.apiTokensLastModTime) {
						log.Info().Msg("Detected API token file change via polling")
						cw.apiTokensLastModTime = stat.ModTime()
						cw.reloadAPITokens()
					}
				}
			}

		case <-cw.stopChan:
			return
		}
	}
}

// reloadConfig reloads the config from the .env file
func (cw *ConfigWatcher) reloadConfig() {
	cw.mu.Lock()
	defer cw.mu.Unlock()

	// Load the .env file
	envMap, err := godotenv.Read(cw.envPath)
	if err != nil {
		// File might not exist, which is fine (no auth)
		if !os.IsNotExist(err) {
			log.Error().Err(err).Msg("Failed to read .env file")
			return
		}
		envMap = make(map[string]string)
	}

	// Track what changed
	var changes []string

	// Update auth settings
	oldAuthUser := cw.config.AuthUser
	oldAuthPass := cw.config.AuthPass
	oldTokenHashes := cw.config.ActiveAPITokenHashes()
	existingByHash := make(map[string]APITokenRecord, len(cw.config.APITokens))
	for _, record := range cw.config.APITokens {
		existingByHash[record.Hash] = record.Clone()
	}

	// Apply auth user
	newUser := strings.Trim(envMap["PULSE_AUTH_USER"], "'\"")
	if newUser != oldAuthUser {
		cw.config.AuthUser = newUser
		if newUser == "" {
			changes = append(changes, "auth user removed")
		} else if oldAuthUser == "" {
			changes = append(changes, "auth user added")
		} else {
			changes = append(changes, "auth user updated")
		}
	}

	// Apply auth password
	newPass := strings.Trim(envMap["PULSE_AUTH_PASS"], "'\"")
	if newPass != oldAuthPass {
		cw.config.AuthPass = newPass
		if newPass == "" {
			changes = append(changes, "auth password removed")
		} else if oldAuthPass == "" {
			changes = append(changes, "auth password added")
		} else {
			changes = append(changes, "auth password updated")
		}
	}

	// Apply API tokens if present in .env (legacy support)
	rawTokens := make([]string, 0, 4)
	if raw, ok := envMap["API_TOKENS"]; ok {
		raw = strings.Trim(raw, "'\"")
		if raw != "" {
			parts := strings.Split(raw, ",")
			for _, part := range parts {
				token := strings.TrimSpace(part)
				if token != "" {
					rawTokens = append(rawTokens, token)
				}
			}
		} else {
			// Explicit empty list clears tokens
			rawTokens = []string{}
		}
	}
	if raw, ok := envMap["API_TOKEN"]; ok {
		raw = strings.Trim(raw, "'\"")
		rawTokens = append(rawTokens, raw)
	}

	if len(rawTokens) > 0 {
		seen := make(map[string]struct{}, len(rawTokens))
		newRecords := make([]APITokenRecord, 0, len(rawTokens))
		for _, tokenValue := range rawTokens {
			tokenValue = strings.TrimSpace(tokenValue)
			if tokenValue == "" {
				continue
			}

			hashed := tokenValue
			prefix := tokenPrefix(tokenValue)
			suffix := tokenSuffix(tokenValue)
			if !auth.IsAPITokenHashed(tokenValue) {
				hashed = auth.HashAPIToken(tokenValue)
				prefix = tokenPrefix(tokenValue)
				suffix = tokenSuffix(tokenValue)
			}

			if _, exists := seen[hashed]; exists {
				continue
			}
			seen[hashed] = struct{}{}

			if existing, ok := existingByHash[hashed]; ok {
				newRecords = append(newRecords, existing)
			} else {
				newRecords = append(newRecords, APITokenRecord{
					ID:        uuid.NewString(),
					Name:      "Environment token",
					Hash:      hashed,
					Prefix:    prefix,
					Suffix:    suffix,
					CreatedAt: time.Now().UTC(),
					Scopes:    []string{ScopeWildcard},
				})
			}
		}

		cw.config.APITokens = newRecords
		cw.config.SortAPITokens()
		cw.config.APITokenEnabled = len(newRecords) > 0

		newHashes := cw.config.ActiveAPITokenHashes()
		if !reflect.DeepEqual(oldTokenHashes, newHashes) {
			switch {
			case len(newHashes) == 0:
				changes = append(changes, "API tokens removed")
			case len(oldTokenHashes) == 0:
				changes = append(changes, "API tokens added")
			default:
				changes = append(changes, "API tokens updated")
			}

			if globalPersistence != nil {
				if err := globalPersistence.SaveAPITokens(cw.config.APITokens); err != nil {
					log.Error().Err(err).Msg("Failed to persist API tokens from .env reload")
				}
			}
		}
	}

	// REMOVED: POLLING_INTERVAL from .env - now ONLY in system.json
	// This prevents confusion and ensures single source of truth

	// Log changes
	if len(changes) > 0 {
		log.Info().
			Strs("changes", changes).
			Bool("has_auth", cw.config.AuthUser != "" && cw.config.AuthPass != "").
			Bool("has_token", cw.config.HasAPITokens()).
			Msg("Applied .env file changes to runtime config")
	} else {
		log.Debug().Msg("No relevant changes detected in .env file")
	}
}

func (cw *ConfigWatcher) reloadAPITokens() {
	cw.mu.Lock()
	defer cw.mu.Unlock()

	if globalPersistence == nil {
		log.Warn().Msg("Config persistence unavailable; cannot reload API tokens")
		return
	}

	// Preserve existing tokens in case reload fails
	existingTokens := cw.config.APITokens
	existingCount := len(existingTokens)

	// Retry logic to handle temporary file system issues
	var tokens []APITokenRecord
	var err error
	maxRetries := 3
	retryDelay := 50 * time.Millisecond

	for attempt := 1; attempt <= maxRetries; attempt++ {
		tokens, err = globalPersistence.LoadAPITokens()
		if err == nil {
			break
		}

		if attempt < maxRetries {
			log.Warn().
				Err(err).
				Int("attempt", attempt).
				Int("maxRetries", maxRetries).
				Dur("retryDelay", retryDelay).
				Msg("Failed to reload API tokens, retrying...")
			time.Sleep(retryDelay)
			retryDelay *= 2 // Exponential backoff
		}
	}

	if err != nil {
		log.Error().
			Err(err).
			Int("existingTokens", existingCount).
			Msg("Failed to reload API tokens after retries - preserving existing tokens")
		// CRITICAL: Keep existing tokens rather than clearing them
		return
	}

	// Only update if we successfully loaded tokens
	cw.config.APITokens = tokens
	cw.config.SortAPITokens()
	cw.config.APITokenEnabled = len(tokens) > 0

	if cw.apiTokensPath != "" {
		if stat, err := os.Stat(cw.apiTokensPath); err == nil {
			cw.apiTokensLastModTime = stat.ModTime()
		}
	}

	log.Info().
		Int("count", len(tokens)).
		Int("previousCount", existingCount).
		Msg("Reloaded API tokens from disk")
}

// reloadMockConfig handles mock.env file changes
func (cw *ConfigWatcher) reloadMockConfig() {
	// Skip if mock.env watching is disabled (Docker environment)
	if cw.mockEnvPath == "" {
		return
	}

	cw.mu.Lock()
	callback := cw.onMockReload
	cw.mu.Unlock()

	// Load the mock.env file to update environment variables
	envMap, err := godotenv.Read(cw.mockEnvPath)
	if err != nil {
		if !os.IsNotExist(err) {
			log.Error().Err(err).Msg("Failed to read mock.env file")
			return
		}
		log.Warn().Msg("mock.env file not found")
		return
	}

	// Load local overrides if they exist
	mockEnvLocalPath := cw.mockEnvPath + ".local"
	if localEnv, err := godotenv.Read(mockEnvLocalPath); err == nil {
		// Merge local overrides into envMap
		for key, value := range localEnv {
			envMap[key] = value
		}
		log.Debug().Str("path", mockEnvLocalPath).Msg("Loaded mock.env.local overrides")
	}

	// Update environment variables for the mock package to read
	for key, value := range envMap {
		if strings.HasPrefix(key, "PULSE_MOCK_") {
			os.Setenv(key, value)
		}
	}

	log.Info().
		Str("path", cw.mockEnvPath).
		Interface("config", envMap).
		Msg("Reloaded mock.env configuration")

	// Trigger callback to restart backend if set
	if callback != nil {
		log.Info().Msg("Triggering backend restart due to mock.env change")
		go func() {
			defer func() {
				if r := recover(); r != nil {
					log.Error().
						Interface("panic", r).
						Stack().
						Msg("Recovered from panic in mock.env callback")
				}
			}()
			callback()
		}()
	}
}
