package gobackend

import (
	"archive/zip"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/dop251/goja"
)

func compareVersions(v1, v2 string) int {
	parts1 := strings.Split(strings.TrimPrefix(v1, "v"), ".")
	parts2 := strings.Split(strings.TrimPrefix(v2, "v"), ".")

	maxLen := len(parts1)
	if len(parts2) > maxLen {
		maxLen = len(parts2)
	}

	for i := 0; i < maxLen; i++ {
		var n1, n2 int
		if i < len(parts1) {
			n1, _ = strconv.Atoi(parts1[i])
		}
		if i < len(parts2) {
			n2, _ = strconv.Atoi(parts2[i])
		}

		if n1 < n2 {
			return -1
		}
		if n1 > n2 {
			return 1
		}
	}

	return 0
}

type loadedExtension struct {
	ID          string             `json:"id"`
	Manifest    *ExtensionManifest `json:"manifest"`
	VM          *goja.Runtime      `json:"-"`
	VMMu        sync.Mutex         `json:"-"`
	runtime     *extensionRuntime
	initialized bool
	Enabled     bool   `json:"enabled"`
	Error       string `json:"error,omitempty"`
	DataDir     string `json:"data_dir"`
	SourceDir   string `json:"source_dir"`
	IconPath    string `json:"icon_path"`
}

func getExtensionInitSettings(extensionID string) map[string]interface{} {
	settings := GetExtensionSettingsStore().GetAll(extensionID)
	if len(settings) == 0 {
		return settings
	}

	filtered := make(map[string]interface{}, len(settings))
	for key, value := range settings {
		if strings.HasPrefix(key, "_") {
			continue
		}
		filtered[key] = value
	}
	return filtered
}

func ensureRuntimeReadyLocked(ext *loadedExtension, applyStoredSettings bool) error {
	if ext.VM == nil || ext.runtime == nil {
		if err := initializeVMLocked(ext); err != nil {
			ext.Error = err.Error()
			ext.Enabled = false
			return err
		}
	}

	if applyStoredSettings && !ext.initialized {
		settings := getExtensionInitSettings(ext.ID)
		if len(settings) > 0 {
			if err := initializeExtensionWithSettingsLocked(ext, settings); err != nil {
				teardownVMLocked(ext)
				ext.Error = err.Error()
				ext.Enabled = false
				return err
			}
		} else {
			ext.initialized = true
		}
	}

	ext.Error = ""
	return nil
}

func (ext *loadedExtension) ensureRuntimeReady() error {
	ext.VMMu.Lock()
	defer ext.VMMu.Unlock()

	return ensureRuntimeReadyLocked(ext, true)
}

func (ext *loadedExtension) lockReadyVM() (*goja.Runtime, error) {
	ext.VMMu.Lock()
	if err := ensureRuntimeReadyLocked(ext, true); err != nil {
		ext.VMMu.Unlock()
		return nil, err
	}
	return ext.VM, nil
}

type extensionManager struct {
	mu            sync.RWMutex
	extensions    map[string]*loadedExtension
	extensionsDir string
	dataDir       string
}

var (
	globalExtManager     *extensionManager
	globalExtManagerOnce sync.Once
)

func getExtensionManager() *extensionManager {
	globalExtManagerOnce.Do(func() {
		globalExtManager = &extensionManager{
			extensions: make(map[string]*loadedExtension),
		}
	})
	return globalExtManager
}

func (m *extensionManager) SetDirectories(extensionsDir, dataDir string) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	m.extensionsDir = extensionsDir
	m.dataDir = dataDir

	if err := os.MkdirAll(extensionsDir, 0755); err != nil {
		return fmt.Errorf("failed to create extensions directory: %w", err)
	}
	if err := os.MkdirAll(dataDir, 0755); err != nil {
		return fmt.Errorf("failed to create data directory: %w", err)
	}

	return nil
}

func (m *extensionManager) LoadExtensionFromFile(filePath string) (*loadedExtension, error) {
	if !strings.HasSuffix(strings.ToLower(filePath), ".spotiflac-ext") {
		return nil, fmt.Errorf("invalid file format: please select a .spotiflac-ext file")
	}

	zipReader, err := zip.OpenReader(filePath)
	if err != nil {
		return nil, fmt.Errorf("cannot open extension file: the file may be corrupted or not a valid extension package")
	}
	defer zipReader.Close()

	var manifestData []byte
	var hasIndexJS bool
	for _, file := range zipReader.File {
		name := filepath.Base(file.Name)
		if name == "manifest.json" {
			rc, err := file.Open()
			if err != nil {
				return nil, fmt.Errorf("failed to open manifest.json: %w", err)
			}
			manifestData, err = io.ReadAll(rc)
			rc.Close()
			if err != nil {
				return nil, fmt.Errorf("failed to read manifest.json: %w", err)
			}
		}
		if name == "index.js" {
			hasIndexJS = true
		}
	}

	if manifestData == nil {
		return nil, fmt.Errorf("invalid extension package: manifest.json not found")
	}

	if !hasIndexJS {
		return nil, fmt.Errorf("invalid extension package: index.js not found")
	}

	manifest, err := ParseManifest(manifestData)
	if err != nil {
		return nil, fmt.Errorf("invalid extension manifest: %w", err)
	}

	m.mu.RLock()
	existing, exists := m.extensions[manifest.Name]
	var existingVersion string
	var existingDisplayName string
	if exists {
		existingVersion = existing.Manifest.Version
		existingDisplayName = existing.Manifest.DisplayName
	}
	m.mu.RUnlock()

	if exists {
		versionCompare := compareVersions(manifest.Version, existingVersion)
		if versionCompare > 0 {
			return m.UpgradeExtension(filePath)
		} else if versionCompare == 0 {
			return nil, fmt.Errorf("extension '%s' v%s is already installed", existingDisplayName, existingVersion)
		} else {
			return nil, fmt.Errorf("cannot downgrade '%s' from v%s to v%s", existingDisplayName, existingVersion, manifest.Version)
		}
	}

	m.mu.Lock()
	defer m.mu.Unlock()

	if _, exists := m.extensions[manifest.Name]; exists {
		return nil, fmt.Errorf("extension '%s' was installed by another process", manifest.DisplayName)
	}

	extDir := filepath.Join(m.extensionsDir, manifest.Name)
	if err := os.MkdirAll(extDir, 0755); err != nil {
		return nil, fmt.Errorf("failed to create extension directory: %w", err)
	}

	for _, file := range zipReader.File {
		if file.FileInfo().IsDir() {
			continue
		}

		relPath := filepath.Clean(file.Name)
		if strings.HasPrefix(relPath, "..") || filepath.IsAbs(relPath) {
			GoLog("[Extension] Skipping unsafe path in archive: %s\n", file.Name)
			continue
		}
		destPath := filepath.Join(extDir, relPath)

		destDir := filepath.Dir(destPath)
		if err := os.MkdirAll(destDir, 0755); err != nil {
			return nil, fmt.Errorf("failed to create directory %s: %w", destDir, err)
		}

		destFile, err := os.Create(destPath)
		if err != nil {
			return nil, fmt.Errorf("failed to create file %s: %w", destPath, err)
		}

		srcFile, err := file.Open()
		if err != nil {
			destFile.Close()
			return nil, fmt.Errorf("failed to open file in archive: %w", err)
		}

		_, err = io.Copy(destFile, srcFile)
		srcFile.Close()
		destFile.Close()
		if err != nil {
			return nil, fmt.Errorf("failed to extract file: %w", err)
		}
	}

	extDataDir := filepath.Join(m.dataDir, manifest.Name)
	if err := os.MkdirAll(extDataDir, 0755); err != nil {
		return nil, fmt.Errorf("failed to create extension data directory: %w", err)
	}

	ext := &loadedExtension{
		ID:        manifest.Name,
		Manifest:  manifest,
		Enabled:   false, // New extensions start disabled
		DataDir:   extDataDir,
		SourceDir: extDir,
	}

	if err := validateExtensionLoad(ext); err != nil {
		ext.Error = err.Error()
		ext.Enabled = false
		GoLog("[Extension] Failed to validate extension %s: %v\n", manifest.Name, err)
	}

	m.extensions[manifest.Name] = ext
	GoLog("[Extension] Loaded extension: %s v%s\n", manifest.DisplayName, manifest.Version)

	return ext, nil
}

func initializeVMLocked(ext *loadedExtension) error {
	ext.VM = nil
	ext.runtime = nil
	ext.initialized = false
	vm := goja.New()
	ext.VM = vm

	indexPath := filepath.Join(ext.SourceDir, "index.js")
	jsCode, err := os.ReadFile(indexPath)
	if err != nil {
		return fmt.Errorf("failed to read index.js: %w", err)
	}

	runtime := newExtensionRuntime(ext)
	ext.runtime = runtime
	runtime.RegisterAPIs(vm)
	runtime.RegisterGoBackendAPIs(vm)

	console := vm.NewObject()
	console.Set("log", func(call goja.FunctionCall) goja.Value {
		args := make([]interface{}, len(call.Arguments))
		for i, arg := range call.Arguments {
			args[i] = arg.Export()
		}
		GoLog("[Extension:%s] %v\n", ext.ID, args)
		return goja.Undefined()
	})
	vm.Set("console", console)

	var registeredExtension goja.Value
	vm.Set("registerExtension", func(call goja.FunctionCall) goja.Value {
		if len(call.Arguments) > 0 {
			registeredExtension = call.Arguments[0]
			vm.Set("extension", call.Arguments[0])
		}
		return goja.Undefined()
	})

	_, err = vm.RunString(string(jsCode))
	if err != nil {
		return fmt.Errorf("failed to execute extension code: %w", err)
	}

	if registeredExtension == nil || goja.IsUndefined(registeredExtension) {
		return fmt.Errorf("extension did not call registerExtension()")
	}

	return nil
}

func newIsolatedExtensionRuntime(ext *loadedExtension) (*goja.Runtime, *extensionRuntime, error) {
	vm := goja.New()

	indexPath := filepath.Join(ext.SourceDir, "index.js")
	jsCode, err := os.ReadFile(indexPath)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to read index.js: %w", err)
	}

	runtime := &extensionRuntime{
		extensionID:       ext.ID,
		manifest:          ext.Manifest,
		settings:          make(map[string]interface{}),
		cookieJar:         nil,
		dataDir:           ext.DataDir,
		vm:                vm,
		storageFlushDelay: defaultStorageFlushDelay,
	}
	if ext.runtime != nil && ext.runtime.cookieJar != nil {
		runtime.cookieJar = ext.runtime.cookieJar
	} else {
		jar, _ := newSimpleCookieJar()
		runtime.cookieJar = jar
	}
	runtime.httpClient = newExtensionHTTPClient(ext, runtime.cookieJar, extensionHTTPTimeout(ext, 30*time.Second), true)
	runtime.downloadClient = newExtensionHTTPClient(ext, runtime.cookieJar, DownloadTimeout, false)
	runtime.RegisterAPIs(vm)
	runtime.RegisterGoBackendAPIs(vm)

	console := vm.NewObject()
	console.Set("log", func(call goja.FunctionCall) goja.Value {
		args := make([]interface{}, len(call.Arguments))
		for i, arg := range call.Arguments {
			args[i] = arg.Export()
		}
		GoLog("[Extension:%s] %v\n", ext.ID, args)
		return goja.Undefined()
	})
	vm.Set("console", console)

	var registeredExtension goja.Value
	vm.Set("registerExtension", func(call goja.FunctionCall) goja.Value {
		if len(call.Arguments) > 0 {
			registeredExtension = call.Arguments[0]
			vm.Set("extension", call.Arguments[0])
		}
		return goja.Undefined()
	})

	if _, err := vm.RunString(string(jsCode)); err != nil {
		runtime.closeStorageFlusher()
		return nil, nil, fmt.Errorf("failed to execute extension code: %w", err)
	}

	if registeredExtension == nil || goja.IsUndefined(registeredExtension) {
		runtime.closeStorageFlusher()
		return nil, nil, fmt.Errorf("extension did not call registerExtension()")
	}

	settings := getExtensionInitSettings(ext.ID)
	if len(settings) > 0 {
		if err := initializeExtensionRuntimeWithSettings(vm, ext.ID, settings); err != nil {
			runtime.closeStorageFlusher()
			return nil, nil, err
		}
	}

	return vm, runtime, nil
}

func (m *extensionManager) initializeVM(ext *loadedExtension) error {
	ext.VMMu.Lock()
	defer ext.VMMu.Unlock()
	return initializeVMLocked(ext)
}

func initializeExtensionRuntimeWithSettings(
	vm *goja.Runtime,
	extensionID string,
	settings map[string]interface{},
) error {
	settingsJSON, err := json.Marshal(settings)
	if err != nil {
		return fmt.Errorf("failed to save settings")
	}

	script := fmt.Sprintf(`
		(function() {
			var settings = %s;
			if (typeof extension !== 'undefined' && typeof extension.initialize === 'function') {
				try {
					extension.initialize(settings);
					return { success: true };
				} catch (e) {
					return { success: false, error: e.toString() };
				}
			}
			return { success: true, message: 'no initialize function' };
		})()
	`, string(settingsJSON))

	result, err := vm.RunString(script)
	if err != nil {
		GoLog("[Extension] Initialize error for %s: %v\n", extensionID, err)
		return err
	}

	if result != nil && !goja.IsUndefined(result) {
		exported := result.Export()
		if resultMap, ok := exported.(map[string]interface{}); ok {
			if success, ok := resultMap["success"].(bool); ok && !success {
				errMsg := "unknown error"
				if e, ok := resultMap["error"].(string); ok {
					errMsg = e
				}
				GoLog("[Extension] Initialize failed for %s: %s\n", extensionID, errMsg)
				return fmt.Errorf("initialize failed: %s", errMsg)
			}
		}
	}

	return nil
}

func initializeExtensionWithSettingsLocked(
	ext *loadedExtension,
	settings map[string]interface{},
) error {
	if ext.VM == nil {
		return fmt.Errorf("extension failed to load: please reinstall the extension")
	}

	if err := initializeExtensionRuntimeWithSettings(ext.VM, ext.ID, settings); err != nil {
		ext.Error = err.Error()
		ext.Enabled = false
		return err
	}

	ext.initialized = true
	GoLog("[Extension] Initialized %s\n", ext.ID)
	return nil
}

func runCleanupLocked(ext *loadedExtension) error {
	if ext.VM != nil {
		if err := runCleanupOnVM(ext.VM); err != nil {
			return err
		}
		if ext.VM.Get("extension") != nil {
			GoLog("[Extension] Cleanup called for %s\n", ext.ID)
		}
	}
	return nil
}

func runCleanupOnVM(vm *goja.Runtime) error {
	if vm == nil {
		return nil
	}

	script := `
		(function() {
			if (typeof extension !== 'undefined' && typeof extension.cleanup === 'function') {
				try {
					extension.cleanup();
					return { success: true };
				} catch (e) {
					return { success: false, error: e.toString() };
				}
			}
			return { success: true, message: 'no cleanup function' };
		})()
	`

	result, err := vm.RunString(script)
	if err != nil {
		return err
	}

	if result != nil && !goja.IsUndefined(result) {
		exported := result.Export()
		if resultMap, ok := exported.(map[string]interface{}); ok {
			if success, ok := resultMap["success"].(bool); ok && !success {
				errMsg := "unknown error"
				if e, ok := resultMap["error"].(string); ok {
					errMsg = e
				}
				return fmt.Errorf("cleanup failed: %s", errMsg)
			}
		}
	}

	return nil
}

func teardownVMLocked(ext *loadedExtension) {
	if err := runCleanupLocked(ext); err != nil {
		GoLog("[Extension] Error calling cleanup for %s: %v\n", ext.ID, err)
	}
	if ext.runtime != nil {
		if err := ext.runtime.flushStorageNow(); err != nil {
			GoLog("[Extension] Failed to flush storage for %s: %v\n", ext.ID, err)
		}
		ext.runtime.closeStorageFlusher()
	}
	ext.runtime = nil
	ext.VM = nil
	ext.initialized = false
}

func validateExtensionLoad(ext *loadedExtension) error {
	ext.VMMu.Lock()
	defer ext.VMMu.Unlock()

	if err := initializeVMLocked(ext); err != nil {
		return err
	}
	teardownVMLocked(ext)
	return nil
}

func (m *extensionManager) UnloadExtension(extensionID string) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	ext, exists := m.extensions[extensionID]
	if !exists {
		return fmt.Errorf("extension not found")
	}

	ext.VMMu.Lock()
	teardownVMLocked(ext)
	ext.VMMu.Unlock()

	delete(m.extensions, extensionID)
	GoLog("[Extension] Unloaded extension: %s\n", extensionID)

	return nil
}

func (m *extensionManager) GetExtension(extensionID string) (*loadedExtension, error) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	ext, exists := m.extensions[extensionID]
	if !exists {
		return nil, fmt.Errorf("extension not found")
	}
	return ext, nil
}

func (m *extensionManager) GetAllExtensions() []*loadedExtension {
	m.mu.RLock()
	defer m.mu.RUnlock()

	result := make([]*loadedExtension, 0, len(m.extensions))
	for _, ext := range m.extensions {
		result = append(result, ext)
	}
	return result
}

func (m *extensionManager) SetExtensionEnabled(extensionID string, enabled bool) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	ext, exists := m.extensions[extensionID]
	if !exists {
		return fmt.Errorf("extension not found")
	}

	if enabled {
		ext.Enabled = true
		if err := ext.ensureRuntimeReady(); err != nil {
			store := GetExtensionSettingsStore()
			ext.Enabled = false
			_ = store.Set(extensionID, "_enabled", false)
			return err
		}
	} else {
		ext.Enabled = false
		ext.Error = ""
		ext.VMMu.Lock()
		teardownVMLocked(ext)
		ext.VMMu.Unlock()
	}
	GoLog("[Extension] %s %s\n", extensionID, map[bool]string{true: "enabled", false: "disabled"}[enabled])

	store := GetExtensionSettingsStore()
	if err := store.Set(extensionID, "_enabled", enabled); err != nil {
		GoLog("[Extension] Failed to persist enabled state for %s: %v\n", extensionID, err)
	}

	return nil
}

func (m *extensionManager) LoadExtensionsFromDirectory(dirPath string) ([]string, []error) {
	var loaded []string
	var errors []error

	entries, err := os.ReadDir(dirPath)
	if err != nil {
		if os.IsNotExist(err) {
			return loaded, errors
		}
		return nil, []error{fmt.Errorf("failed to read extensions directory: %w", err)}
	}

	for _, entry := range entries {
		if entry.IsDir() {
			manifestPath := filepath.Join(dirPath, entry.Name(), "manifest.json")
			if _, err := os.Stat(manifestPath); err == nil {
				ext, err := m.loadExtensionFromDirectory(filepath.Join(dirPath, entry.Name()))
				if err != nil {
					GoLog("[Extension] Failed to load %s: %v\n", entry.Name(), err)
					errors = append(errors, fmt.Errorf("%s: %w", entry.Name(), err))
				} else {
					loaded = append(loaded, ext.ID)
				}
			}
		} else if strings.HasSuffix(strings.ToLower(entry.Name()), ".spotiflac-ext") {
			ext, err := m.LoadExtensionFromFile(filepath.Join(dirPath, entry.Name()))
			if err != nil {
				GoLog("[Extension] Failed to load %s: %v\n", entry.Name(), err)
				errors = append(errors, fmt.Errorf("%s: %w", entry.Name(), err))
			} else {
				loaded = append(loaded, ext.ID)
			}
		}
	}

	return loaded, errors
}

func (m *extensionManager) loadExtensionFromDirectory(dirPath string) (*loadedExtension, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	manifestPath := filepath.Join(dirPath, "manifest.json")
	manifestData, err := os.ReadFile(manifestPath)
	if err != nil {
		return nil, fmt.Errorf("failed to read manifest.json: %w", err)
	}

	manifest, err := ParseManifest(manifestData)
	if err != nil {
		return nil, fmt.Errorf("invalid extension manifest: %w", err)
	}

	indexPath := filepath.Join(dirPath, "index.js")
	if _, err := os.Stat(indexPath); os.IsNotExist(err) {
		return nil, fmt.Errorf("extension is missing index.js file")
	}

	if existing, exists := m.extensions[manifest.Name]; exists {
		GoLog("[Extension] Extension '%s' already loaded, skipping\n", manifest.DisplayName)
		return existing, nil
	}

	extDataDir := filepath.Join(m.dataDir, manifest.Name)
	if err := os.MkdirAll(extDataDir, 0755); err != nil {
		return nil, fmt.Errorf("failed to create extension data directory: %w", err)
	}

	ext := &loadedExtension{
		ID:        manifest.Name,
		Manifest:  manifest,
		Enabled:   false, // Will be restored from settings store
		DataDir:   extDataDir,
		SourceDir: dirPath,
	}

	store := GetExtensionSettingsStore()
	if enabledVal, err := store.Get(manifest.Name, "_enabled"); err == nil {
		if enabled, ok := enabledVal.(bool); ok {
			ext.Enabled = enabled
			GoLog("[Extension] Restored enabled state for %s: %v\n", manifest.Name, enabled)
		}
	}

	if err := validateExtensionLoad(ext); err != nil {
		ext.Error = err.Error()
		ext.Enabled = false
		GoLog("[Extension] Failed to validate extension %s: %v\n", manifest.Name, err)
	}

	m.extensions[manifest.Name] = ext
	GoLog("[Extension] Loaded extension: %s v%s\n", manifest.DisplayName, manifest.Version)

	return ext, nil
}

func (m *extensionManager) RemoveExtension(extensionID string) error {
	ext, err := m.GetExtension(extensionID)
	if err != nil {
		return err
	}

	if err := m.UnloadExtension(extensionID); err != nil {
		return err
	}

	if ext.SourceDir != "" {
		if err := os.RemoveAll(ext.SourceDir); err != nil {
			GoLog("[Extension] Warning: failed to remove source dir: %v\n", err)
		}
	}

	return nil
}

// Only allows upgrades (new version > current version), not downgrades
func (m *extensionManager) UpgradeExtension(filePath string) (*loadedExtension, error) {
	if !strings.HasSuffix(strings.ToLower(filePath), ".spotiflac-ext") {
		return nil, fmt.Errorf("invalid file format: please select a .spotiflac-ext file")
	}

	zipReader, err := zip.OpenReader(filePath)
	if err != nil {
		return nil, fmt.Errorf("cannot open extension file: the file may be corrupted or not a valid extension package")
	}
	defer zipReader.Close()

	var manifestData []byte
	var hasIndexJS bool
	for _, file := range zipReader.File {
		name := filepath.Base(file.Name)
		if name == "manifest.json" {
			rc, err := file.Open()
			if err != nil {
				return nil, fmt.Errorf("failed to open manifest.json: %w", err)
			}
			manifestData, err = io.ReadAll(rc)
			rc.Close()
			if err != nil {
				return nil, fmt.Errorf("failed to read manifest.json: %w", err)
			}
		}
		if name == "index.js" {
			hasIndexJS = true
		}
	}

	if manifestData == nil {
		return nil, fmt.Errorf("invalid extension package: manifest.json not found")
	}

	if !hasIndexJS {
		return nil, fmt.Errorf("invalid extension package: index.js not found")
	}

	newManifest, err := ParseManifest(manifestData)
	if err != nil {
		return nil, fmt.Errorf("invalid extension manifest: %w", err)
	}

	m.mu.RLock()
	existing, exists := m.extensions[newManifest.Name]
	m.mu.RUnlock()

	if !exists {
		return nil, fmt.Errorf("extension '%s' is not installed; use install instead of upgrade", newManifest.DisplayName)
	}

	versionCompare := compareVersions(newManifest.Version, existing.Manifest.Version)
	if versionCompare < 0 {
		return nil, fmt.Errorf("cannot downgrade extension: current version: %s, new version: %s", existing.Manifest.Version, newManifest.Version)
	}
	if versionCompare == 0 {
		return nil, fmt.Errorf("extension is already at version %s", existing.Manifest.Version)
	}

	GoLog("[Extension] Upgrading %s from v%s to v%s\n", newManifest.DisplayName, existing.Manifest.Version, newManifest.Version)

	extDataDir := existing.DataDir
	extDir := existing.SourceDir
	wasEnabled := existing.Enabled

	m.UnloadExtension(existing.ID)

	if extDir != "" {
		if err := os.RemoveAll(extDir); err != nil {
			GoLog("[Extension] Warning: failed to remove old source dir: %v\n", err)
		}
	}

	if err := os.MkdirAll(extDir, 0755); err != nil {
		return nil, fmt.Errorf("failed to create extension directory: %w", err)
	}

	for _, file := range zipReader.File {
		if file.FileInfo().IsDir() {
			continue
		}

		relPath := filepath.Clean(file.Name)
		if strings.HasPrefix(relPath, "..") || filepath.IsAbs(relPath) {
			GoLog("[Extension] Skipping unsafe path in archive: %s\n", file.Name)
			continue
		}
		destPath := filepath.Join(extDir, relPath)

		destDir := filepath.Dir(destPath)
		if err := os.MkdirAll(destDir, 0755); err != nil {
			return nil, fmt.Errorf("failed to create directory %s: %w", destDir, err)
		}

		destFile, err := os.Create(destPath)
		if err != nil {
			return nil, fmt.Errorf("failed to create file %s: %w", destPath, err)
		}

		srcFile, err := file.Open()
		if err != nil {
			destFile.Close()
			return nil, fmt.Errorf("failed to open file in archive: %w", err)
		}

		_, err = io.Copy(destFile, srcFile)
		srcFile.Close()
		destFile.Close()
		if err != nil {
			return nil, fmt.Errorf("failed to extract file: %w", err)
		}
	}

	ext := &loadedExtension{
		ID:        newManifest.Name,
		Manifest:  newManifest,
		Enabled:   wasEnabled, // Preserve enabled state from before upgrade
		DataDir:   extDataDir,
		SourceDir: extDir,
	}

	if wasEnabled {
		if err := ext.ensureRuntimeReady(); err != nil {
			GoLog("[Extension] Failed to initialize upgraded extension %s: %v\n", newManifest.Name, err)
		}
	} else if err := validateExtensionLoad(ext); err != nil {
		ext.Error = err.Error()
		ext.Enabled = false
		GoLog("[Extension] Failed to validate upgraded extension %s: %v\n", newManifest.Name, err)
	}

	m.mu.Lock()
	m.extensions[newManifest.Name] = ext
	m.mu.Unlock()

	GoLog("[Extension] Upgraded extension: %s to v%s\n", newManifest.DisplayName, newManifest.Version)

	return ext, nil
}

type ExtensionUpgradeInfo struct {
	ExtensionID    string `json:"extension_id"`
	CurrentVersion string `json:"current_version"`
	NewVersion     string `json:"new_version"`
	CanUpgrade     bool   `json:"can_upgrade"`
	IsInstalled    bool   `json:"is_installed"`
}

func (m *extensionManager) checkExtensionUpgradeInternal(filePath string) (*ExtensionUpgradeInfo, error) {
	if !strings.HasSuffix(strings.ToLower(filePath), ".spotiflac-ext") {
		return nil, fmt.Errorf("invalid file format: please select a .spotiflac-ext file")
	}

	zipReader, err := zip.OpenReader(filePath)

	if err != nil {
		return nil, fmt.Errorf("cannot open extension file")
	}
	defer zipReader.Close()

	var manifestData []byte
	for _, file := range zipReader.File {
		name := filepath.Base(file.Name)
		if name == "manifest.json" {
			rc, err := file.Open()
			if err != nil {
				return nil, fmt.Errorf("failed to open manifest.json")
			}
			manifestData, err = io.ReadAll(rc)
			rc.Close()
			if err != nil {
				return nil, fmt.Errorf("failed to read manifest.json")
			}
			break
		}
	}

	if manifestData == nil {
		return nil, fmt.Errorf("manifest.json not found")
	}

	newManifest, err := ParseManifest(manifestData)
	if err != nil {
		return nil, fmt.Errorf("invalid manifest: %w", err)
	}

	m.mu.RLock()
	existing, exists := m.extensions[newManifest.Name]
	m.mu.RUnlock()

	info := &ExtensionUpgradeInfo{
		ExtensionID: newManifest.Name,
		NewVersion:  newManifest.Version,
		IsInstalled: exists,
	}

	if !exists {
		info.CurrentVersion = ""
		info.CanUpgrade = false
	} else {
		info.CurrentVersion = existing.Manifest.Version
		info.CanUpgrade = compareVersions(newManifest.Version, existing.Manifest.Version) > 0
	}

	return info, nil
}

func (m *extensionManager) CheckExtensionUpgradeJSON(filePath string) (string, error) {
	info, err := m.checkExtensionUpgradeInternal(filePath)
	if err != nil {
		return "", err
	}

	jsonBytes, err := json.Marshal(info)
	if err != nil {
		return "", err
	}

	return string(jsonBytes), nil
}

func (m *extensionManager) GetInstalledExtensionsJSON() (string, error) {
	extensions := m.GetAllExtensions()

	type ExtensionInfo struct {
		ID                     string                 `json:"id"`
		Name                   string                 `json:"name"`
		DisplayName            string                 `json:"display_name"`
		Version                string                 `json:"version"`
		Description            string                 `json:"description"`
		Homepage               string                 `json:"homepage,omitempty"`
		IconPath               string                 `json:"icon_path,omitempty"`
		Types                  []ExtensionType        `json:"types"`
		Enabled                bool                   `json:"enabled"`
		Status                 string                 `json:"status"`
		Error                  string                 `json:"error_message,omitempty"`
		Settings               []ExtensionSetting     `json:"settings,omitempty"`
		QualityOptions         []QualityOption        `json:"quality_options,omitempty"`
		Permissions            []string               `json:"permissions"`
		HasMetadataProvider    bool                   `json:"has_metadata_provider"`
		HasDownloadProvider    bool                   `json:"has_download_provider"`
		HasLyricsProvider      bool                   `json:"has_lyrics_provider"`
		SkipMetadataEnrichment bool                   `json:"skip_metadata_enrichment"`
		SkipLyrics             bool                   `json:"skip_lyrics"`
		StopProviderFallback   bool                   `json:"stop_provider_fallback"`
		SearchBehavior         *SearchBehaviorConfig  `json:"search_behavior,omitempty"`
		TrackMatching          *TrackMatchingConfig   `json:"track_matching,omitempty"`
		PostProcessing         *PostProcessingConfig  `json:"post_processing,omitempty"`
		ServiceHealth          []ExtensionHealthCheck `json:"service_health,omitempty"`
		Capabilities           map[string]interface{} `json:"capabilities,omitempty"`
	}

	infos := make([]ExtensionInfo, len(extensions))
	for i, ext := range extensions {
		permissions := []string{}
		for _, domain := range ext.Manifest.Permissions.Network {
			permissions = append(permissions, "network:"+domain)
		}
		if ext.Manifest.Permissions.Storage {
			permissions = append(permissions, "storage:enabled")
		}

		status := "loaded"
		if ext.Error != "" {
			status = "error"
		} else if !ext.Enabled {
			status = "disabled"
		}

		iconPath := ""
		if ext.Manifest.Icon != "" && ext.SourceDir != "" {
			possibleIcon := filepath.Join(ext.SourceDir, ext.Manifest.Icon)
			if _, err := os.Stat(possibleIcon); err == nil {
				iconPath = possibleIcon
			}
		}
		if iconPath == "" && ext.SourceDir != "" {
			possibleIcon := filepath.Join(ext.SourceDir, "icon.png")
			if _, err := os.Stat(possibleIcon); err == nil {
				iconPath = possibleIcon
			}
		}

		infos[i] = ExtensionInfo{
			ID:                     ext.ID,
			Name:                   ext.Manifest.Name,
			DisplayName:            ext.Manifest.DisplayName,
			Version:                ext.Manifest.Version,
			Description:            ext.Manifest.Description,
			Homepage:               ext.Manifest.Homepage,
			IconPath:               iconPath,
			Types:                  ext.Manifest.Types,
			Enabled:                ext.Enabled,
			Status:                 status,
			Error:                  ext.Error,
			Settings:               ext.Manifest.Settings,
			QualityOptions:         ext.Manifest.QualityOptions,
			Permissions:            permissions,
			HasMetadataProvider:    ext.Manifest.IsMetadataProvider(),
			HasDownloadProvider:    ext.Manifest.IsDownloadProvider(),
			HasLyricsProvider:      ext.Manifest.IsLyricsProvider(),
			SkipMetadataEnrichment: ext.Manifest.SkipMetadataEnrichment,
			SkipLyrics:             ext.Manifest.SkipLyrics,
			StopProviderFallback:   ext.Manifest.StopsProviderFallback(),
			SearchBehavior:         ext.Manifest.SearchBehavior,
			TrackMatching:          ext.Manifest.TrackMatching,
			PostProcessing:         ext.Manifest.PostProcessing,
			ServiceHealth:          ext.Manifest.ServiceHealth,
			Capabilities:           ext.Manifest.Capabilities,
		}
	}

	jsonBytes, err := json.Marshal(infos)
	if err != nil {
		return "", err
	}

	return string(jsonBytes), nil
}

func (m *extensionManager) InitializeExtension(extensionID string, settings map[string]interface{}) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	ext, exists := m.extensions[extensionID]
	if !exists {
		return fmt.Errorf("extension not found")
	}

	ext.VMMu.Lock()
	defer ext.VMMu.Unlock()

	if err := ensureRuntimeReadyLocked(ext, false); err != nil {
		return err
	}
	return initializeExtensionWithSettingsLocked(ext, settings)
}

func (m *extensionManager) CleanupExtension(extensionID string) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	ext, exists := m.extensions[extensionID]
	if !exists {
		return fmt.Errorf("extension not found")
	}

	if ext.VM == nil {
		return nil
	}
	ext.VMMu.Lock()
	defer ext.VMMu.Unlock()
	if err := runCleanupLocked(ext); err != nil {
		GoLog("[Extension] Cleanup error for %s: %v\n", extensionID, err)
		return err
	}
	GoLog("[Extension] Cleaned up %s\n", extensionID)
	return nil
}

func (m *extensionManager) UnloadAllExtensions() {
	m.mu.Lock()
	extensionIDs := make([]string, 0, len(m.extensions))
	for id := range m.extensions {
		extensionIDs = append(extensionIDs, id)
	}
	m.mu.Unlock()

	for _, id := range extensionIDs {
		m.UnloadExtension(id)
	}

	GoLog("[Extension] All extensions unloaded\n")
}

func (m *extensionManager) InvokeAction(extensionID string, actionName string) (map[string]interface{}, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	ext, exists := m.extensions[extensionID]
	if !exists {
		return nil, fmt.Errorf("extension not found: %s", extensionID)
	}

	if !ext.Enabled {
		return nil, fmt.Errorf("extension is disabled")
	}
	vm, err := ext.lockReadyVM()
	if err != nil {
		return nil, err
	}
	defer ext.VMMu.Unlock()

	// Merge extension return values onto the top-level JSON object so Flutter can read
	// message, open_auth_url, setting_updates without unwrapping a nested "result" key.
	script := fmt.Sprintf(`
		(function() {
			if (typeof extension !== 'undefined' && typeof extension.%s === 'function') {
				try {
					var result = extension.%s();
					if (result && typeof result.then === 'function') {
						return { success: true, pending: true, message: 'Action started' };
					}
					if (result !== null && result !== undefined && typeof result === 'object') {
						var isArr = false;
						if (typeof Array !== 'undefined' && Array.isArray) {
							isArr = Array.isArray(result);
						}
						if (!isArr) {
							var out = { success: true };
							for (var k in result) {
								out[k] = result[k];
							}
							return out;
						}
					}
					return { success: true, result: result };
				} catch (e) {
					return { success: false, error: e.toString() };
				}
			}
			return { success: false, error: 'Action function not found: %s' };
		})()
	`, actionName, actionName, actionName)

	result, err := RunWithTimeoutAndRecover(vm, script, DefaultJSTimeout)
	if err != nil {
		GoLog("[Extension] InvokeAction error for %s.%s: %v\n", extensionID, actionName, err)
		return nil, fmt.Errorf("action failed: %v", err)
	}

	if result == nil || goja.IsUndefined(result) {
		return map[string]interface{}{"success": true}, nil
	}

	exported := result.Export()
	if resultMap, ok := exported.(map[string]interface{}); ok {
		GoLog("[Extension] InvokeAction %s.%s result: %v\n", extensionID, actionName, resultMap)
		return resultMap, nil
	}

	return map[string]interface{}{"success": true, "result": exported}, nil
}
