package services

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"
	"plugin"
	"runtime"
	"strings"
	"sync"
	"time"

	"github.com/Wikid82/charon/backend/internal/logger"
	"github.com/Wikid82/charon/backend/internal/models"
	"github.com/Wikid82/charon/backend/pkg/dnsprovider"
	"gorm.io/gorm"
)

// PluginLoaderService manages loading and unloading of DNS provider plugins.
type PluginLoaderService struct {
	pluginDir     string
	allowedSigs   map[string]string // plugin name -> expected signature
	loadedPlugins map[string]string // plugin type -> file path
	db            *gorm.DB
	mu            sync.RWMutex
}

// NewPluginLoaderService creates a new plugin loader.
func NewPluginLoaderService(db *gorm.DB, pluginDir string, allowedSignatures map[string]string) *PluginLoaderService {
	return &PluginLoaderService{
		pluginDir:     pluginDir,
		allowedSigs:   allowedSignatures,
		loadedPlugins: make(map[string]string),
		db:            db,
	}
}

// LoadAllPlugins loads all .so files from the plugin directory.
func (s *PluginLoaderService) LoadAllPlugins() error {
	// Check if plugins are supported on this platform
	if runtime.GOOS == "windows" {
		logger.Log().Warn("Go plugins are not supported on Windows - only built-in providers available")
		return nil
	}

	if s.pluginDir == "" {
		logger.Log().Info("Plugin directory not configured, skipping plugin loading")
		return nil
	}

	// Check if directory exists
	entries, err := os.ReadDir(s.pluginDir)
	if err != nil {
		if os.IsNotExist(err) {
			logger.Log().Info("Plugin directory does not exist, skipping plugin loading")
			return nil
		}
		return fmt.Errorf("failed to read plugin directory: %w", err)
	}

	// Verify directory permissions (security requirement)
	if err := s.verifyDirectoryPermissions(s.pluginDir); err != nil {
		return fmt.Errorf("plugin directory has insecure permissions: %w", err)
	}

	loadedCount := 0
	failedCount := 0

	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".so") {
			continue
		}

		pluginPath := filepath.Join(s.pluginDir, entry.Name())
		if err := s.LoadPlugin(pluginPath); err != nil {
			logger.Log().WithError(err).Warnf("Failed to load plugin: %s", entry.Name())
			failedCount++

			// Update plugin status in database
			s.updatePluginStatus(pluginPath, models.PluginStatusError, err.Error())
			continue
		}
		loadedCount++
	}

	logger.Log().Infof("Loaded %d external DNS provider plugins (%d failed)", loadedCount, failedCount)
	return nil
}

// LoadPlugin loads a single plugin from the specified path.
func (s *PluginLoaderService) LoadPlugin(path string) error {
	// Verify signature if allowlist is configured (nil = permissive, non-nil = strict)
	if s.allowedSigs != nil {
		pluginName := strings.TrimSuffix(filepath.Base(path), ".so")
		expectedSig, ok := s.allowedSigs[pluginName]
		if !ok {
			return fmt.Errorf("%w: %s", dnsprovider.ErrPluginNotInAllowlist, pluginName)
		}

		actualSig, err := s.computeSignature(path)
		if err != nil {
			return fmt.Errorf("failed to compute signature: %w", err)
		}

		if actualSig != expectedSig {
			return fmt.Errorf("%w: expected %s, got %s", dnsprovider.ErrSignatureMismatch, expectedSig, actualSig)
		}
	}

	// Load the plugin
	p, err := plugin.Open(path)
	if err != nil {
		return fmt.Errorf("%w: %v", dnsprovider.ErrPluginLoadFailed, err)
	}

	// Look up the Plugin symbol (support both T and *T)
	symbol, err := p.Lookup("Plugin")
	if err != nil {
		return fmt.Errorf("%w: missing 'Plugin' symbol: %v", dnsprovider.ErrInvalidPlugin, err)
	}

	// Assert the interface (handle both value and pointer)
	var provider dnsprovider.ProviderPlugin

	// Try direct interface assertion first
	provider, ok := symbol.(dnsprovider.ProviderPlugin)
	if !ok {
		// Try pointer to interface
		providerPtr, ok := symbol.(*dnsprovider.ProviderPlugin)
		if !ok {
			return fmt.Errorf("%w: 'Plugin' symbol does not implement ProviderPlugin interface", dnsprovider.ErrInvalidPlugin)
		}
		provider = *providerPtr
	}

	// Validate provider metadata
	meta := provider.Metadata()
	if meta.Type == "" || meta.Name == "" {
		return fmt.Errorf("%w: invalid metadata (missing type or name)", dnsprovider.ErrInvalidPlugin)
	}

	// Verify Go version compatibility
	if meta.GoVersion != "" && meta.GoVersion != runtime.Version() {
		logger.Log().Warnf("Plugin %s built with Go %s, host is %s - may cause compatibility issues",
			meta.Type, meta.GoVersion, runtime.Version())
	}

	// Verify interface version compatibility
	if meta.InterfaceVersion != "" && meta.InterfaceVersion != dnsprovider.InterfaceVersion {
		return fmt.Errorf("%w: plugin interface %s, host requires %s",
			dnsprovider.ErrInterfaceVersionMismatch, meta.InterfaceVersion, dnsprovider.InterfaceVersion)
	}

	// Initialize the provider
	if err := provider.Init(); err != nil {
		return fmt.Errorf("%w: %v", dnsprovider.ErrPluginInitFailed, err)
	}

	// Register with global registry
	if err := dnsprovider.Global().Register(provider); err != nil {
		// Cleanup on registration failure
		_ = provider.Cleanup()
		return fmt.Errorf("failed to register plugin: %w", err)
	}

	s.mu.Lock()
	s.loadedPlugins[provider.Type()] = path
	s.mu.Unlock()

	// Update database with success status
	now := time.Now()
	s.updatePluginRecord(path, meta, models.PluginStatusLoaded, "", &now)

	logger.Log().WithFields(map[string]interface{}{
		"type":    meta.Type,
		"name":    meta.Name,
		"version": meta.Version,
		"author":  meta.Author,
	}).Info("Loaded DNS provider plugin")

	return nil
}

// computeSignature calculates SHA-256 hash of plugin file.
func (s *PluginLoaderService) computeSignature(path string) (string, error) {
	// #nosec G304 -- path is from ReadDir iteration within pluginDir
	data, err := os.ReadFile(path)
	if err != nil {
		return "", err
	}
	hash := sha256.Sum256(data)
	return "sha256:" + hex.EncodeToString(hash[:]), nil
}

// verifyDirectoryPermissions checks that the plugin directory has secure permissions.
func (s *PluginLoaderService) verifyDirectoryPermissions(dir string) error {
	info, err := os.Stat(dir)
	if err != nil {
		return err
	}

	// On Unix-like systems, check that directory is not world-writable
	if runtime.GOOS != "windows" {
		mode := info.Mode().Perm()
		if mode&0002 != 0 { // Check if world-writable bit is set
			return fmt.Errorf("directory is world-writable (mode: %o) - this is a security risk", mode)
		}
	}

	return nil
}

// updatePluginStatus updates the status of a plugin in the database.
func (s *PluginLoaderService) updatePluginStatus(filePath, status, errorMsg string) {
	if s.db == nil {
		return
	}

	var plugin models.Plugin
	result := s.db.Where("file_path = ?", filePath).First(&plugin)
	if result.Error != nil {
		// Plugin not in database yet, skip
		return
	}

	updates := map[string]interface{}{
		"status": status,
		"error":  errorMsg,
	}

	if status == models.PluginStatusLoaded {
		now := time.Now()
		updates["loaded_at"] = &now
	}

	s.db.Model(&plugin).Updates(updates)
}

// updatePluginRecord creates or updates a plugin record in the database.
func (s *PluginLoaderService) updatePluginRecord(filePath string, meta dnsprovider.ProviderMetadata, status, errorMsg string, loadedAt *time.Time) {
	if s.db == nil {
		return
	}

	var plugin models.Plugin
	result := s.db.Where("file_path = ?", filePath).First(&plugin)

	if result.Error != nil {
		// Create new record
		plugin = models.Plugin{
			UUID:     generateUUID(),
			Name:     meta.Name,
			Type:     meta.Type,
			FilePath: filePath,
			Enabled:  true,
			Status:   status,
			Error:    errorMsg,
			Version:  meta.Version,
			Author:   meta.Author,
			LoadedAt: loadedAt,
		}
		s.db.Create(&plugin)
	} else {
		// Update existing record
		updates := map[string]interface{}{
			"name":      meta.Name,
			"type":      meta.Type,
			"status":    status,
			"error":     errorMsg,
			"version":   meta.Version,
			"author":    meta.Author,
			"loaded_at": loadedAt,
		}
		s.db.Model(&plugin).Updates(updates)
	}
}

// ListLoadedPlugins returns information about loaded external plugins.
func (s *PluginLoaderService) ListLoadedPlugins() []dnsprovider.ProviderMetadata {
	s.mu.RLock()
	defer s.mu.RUnlock()

	var plugins []dnsprovider.ProviderMetadata
	for providerType := range s.loadedPlugins {
		if provider, ok := dnsprovider.Global().Get(providerType); ok {
			plugins = append(plugins, provider.Metadata())
		}
	}
	return plugins
}

// IsPluginLoaded checks if a provider type was loaded from an external plugin.
func (s *PluginLoaderService) IsPluginLoaded(providerType string) bool {
	s.mu.RLock()
	defer s.mu.RUnlock()
	_, ok := s.loadedPlugins[providerType]
	return ok
}

// UnloadPlugin removes a plugin from the registry.
// Note: Go plugins cannot be truly unloaded from memory.
func (s *PluginLoaderService) UnloadPlugin(providerType string) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	// Get provider for cleanup
	provider, ok := dnsprovider.Global().Get(providerType)
	if ok {
		// Call cleanup hook
		if err := provider.Cleanup(); err != nil {
			logger.Log().WithError(err).Warnf("Error during plugin cleanup: %s", providerType)
		}
	}

	// Unregister from global registry
	dnsprovider.Global().Unregister(providerType)

	// Remove from loaded plugins map
	delete(s.loadedPlugins, providerType)

	logger.Log().Infof("Unloaded plugin: %s (memory not reclaimed - restart required for full unload)", providerType)
	return nil
}

// Cleanup calls Cleanup() on all loaded plugins.
func (s *PluginLoaderService) Cleanup() error {
	s.mu.Lock()
	defer s.mu.Unlock()

	for providerType := range s.loadedPlugins {
		if provider, ok := dnsprovider.Global().Get(providerType); ok {
			if err := provider.Cleanup(); err != nil {
				logger.Log().WithError(err).Warnf("Error during plugin cleanup: %s", providerType)
			}
		}
	}

	return nil
}

// generateUUID generates a simple UUID (using timestamp-based approach for simplicity).
// In production, consider using github.com/google/uuid package.
func generateUUID() string {
	return fmt.Sprintf("%d-%d", time.Now().UnixNano(), time.Now().Unix())
}
