package gobackend

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/dop251/goja"
)

func initializeVMLocked(ext *loadedExtension) error {
	ext.VM = nil
	ext.runtime = nil
	ext.indexProgram = 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)
	}
	indexProgram, err := goja.Compile(indexPath, string(jsCode), false)
	if err != nil {
		return fmt.Errorf("failed to compile extension code: %w", err)
	}
	ext.indexProgram = indexProgram

	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([]any, 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.RunProgram(indexProgram)
	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()

	indexProgram := ext.indexProgram
	if indexProgram == nil {
		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)
		}
		indexProgram, err = goja.Compile(indexPath, string(jsCode), false)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to compile extension code: %w", err)
		}
	}

	runtime := &extensionRuntime{
		extensionID: ext.ID,
		manifest:    ext.Manifest,
		settings:    make(map[string]any),
		cookieJar:   nil,
		dataDir:     ext.DataDir,
		vm:          vm,
	}
	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([]any, 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.RunProgram(indexProgram); 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
}

// A goja runtime plus an executed extension program is several MB of live
// heap; rebuilding one per download multiplies that by the number of tracks.
// Extensions already serve many calls on the persistent shared VM, so reusing
// an initialized isolated runtime for consecutive downloads is the same
// lifecycle contract.
const maxIdleIsolatedRuntimes = 1

// acquireIsolatedExtensionRuntime pops an idle pooled runtime or builds one.
func acquireIsolatedExtensionRuntime(ext *loadedExtension) (*goja.Runtime, *extensionRuntime, error) {
	if hasQuarantinedRuntime(ext) {
		return nil, nil, fmt.Errorf("extension runtime is still stopping after an unresponsive request")
	}
	ext.isolatedPoolMu.Lock()
	if n := len(ext.isolatedPool); n > 0 {
		handle := ext.isolatedPool[n-1]
		ext.isolatedPool = ext.isolatedPool[:n-1]
		ext.isolatedPoolMu.Unlock()
		return handle.vm, handle.runtime, nil
	}
	ext.isolatedPoolMu.Unlock()

	ext.VMMu.Lock()
	defer ext.VMMu.Unlock()
	if hasQuarantinedRuntime(ext) {
		return nil, nil, fmt.Errorf("extension runtime is still stopping after an unresponsive request")
	}
	return newIsolatedExtensionRuntime(ext)
}

// releaseIsolatedExtensionRuntime pools a healthy runtime for reuse or tears
// it down. Pass healthy=false after an interrupt/timeout/script error, whose
// VM state can't be trusted for reuse.
func releaseIsolatedExtensionRuntime(
	ext *loadedExtension,
	vm *goja.Runtime,
	runtime *extensionRuntime,
	healthy, cleanupSafe bool,
	unsafeDone <-chan struct{},
) {
	if !cleanupSafe {
		registerQuarantinedRuntime(ext, runtime, unsafeDone)
		return
	}

	if runtime != nil {
		if err := runtime.flushStorageNow(); err != nil {
			GoLog("[Extension:%s] isolated download storage flush failed: %v\n", ext.ID, err)
		}
	}

	if healthy && vm != nil && runtime != nil && ext.Enabled {
		ext.isolatedPoolMu.Lock()
		if len(ext.isolatedPool) < maxIdleIsolatedRuntimes {
			ext.isolatedPool = append(ext.isolatedPool, &isolatedRuntimeHandle{vm: vm, runtime: runtime})
			ext.isolatedPoolMu.Unlock()
			return
		}
		ext.isolatedPoolMu.Unlock()
	}

	if cleanupSafe {
		if cleanupErr := runCleanupOnVM(vm); cleanupErr != nil {
			GoLog("[Extension:%s] isolated download cleanup failed: %v\n", ext.ID, cleanupErr)
		}
	}
	if runtime != nil {
		runtime.closeStorageFlusher()
	}
}

func hasQuarantinedRuntime(ext *loadedExtension) bool {
	if ext == nil {
		return false
	}
	ext.quarantineMu.Lock()
	defer ext.quarantineMu.Unlock()
	return ext.quarantinedRuntimes > 0
}

// registerQuarantinedRuntime keeps the extension gated until the interrupted
// goroutine actually exits. A Go goroutine cannot be killed safely, so allowing
// a replacement VM immediately would let a broken extension accumulate an
// unbounded number of runtimes. The runtime is touched only after completion.
func registerQuarantinedRuntime(ext *loadedExtension, runtime *extensionRuntime, done <-chan struct{}) {
	if ext == nil {
		return
	}
	ext.quarantineMu.Lock()
	ext.quarantinedRuntimes++
	ext.quarantineMu.Unlock()

	if done == nil {
		GoLog("[Extension:%s] quarantined runtime has no completion signal; keeping extension gated\n", ext.ID)
		return
	}
	go func() {
		<-done
		if runtime != nil {
			runtime.closeStorageFlusher()
		}
		ext.quarantineMu.Lock()
		if ext.quarantinedRuntimes > 0 {
			ext.quarantinedRuntimes--
		}
		ext.quarantineMu.Unlock()
	}()
}

// quarantineRuntimeLocked detaches a VM that remained busy after interrupt.
// The caller holds VMMu. Touching or cleaning up that VM would race its stuck
// goroutine; replacement calls remain gated until that goroutine exits.
func quarantineRuntimeLocked(ext *loadedExtension, vm *goja.Runtime, err error) {
	if ext == nil || ext.VM != vm {
		return
	}
	runtime := ext.runtime
	ext.VM = nil
	ext.runtime = nil
	ext.initialized = false
	ext.Error = "extension runtime was quarantined after an unresponsive script"
	registerQuarantinedRuntime(ext, runtime, runtimeCompletion(err))
}

// drainIsolatedRuntimePool tears down idle isolated runtimes. Called on
// extension teardown and on app-wide memory release.
func drainIsolatedRuntimePool(ext *loadedExtension) {
	ext.isolatedPoolMu.Lock()
	pool := ext.isolatedPool
	ext.isolatedPool = nil
	ext.isolatedPoolMu.Unlock()

	for _, handle := range pool {
		if cleanupErr := runCleanupOnVM(handle.vm); cleanupErr != nil {
			GoLog("[Extension:%s] isolated pool cleanup failed: %v\n", ext.ID, cleanupErr)
		}
		if handle.runtime != nil {
			if err := handle.runtime.flushStorageNow(); err != nil {
				GoLog("[Extension:%s] isolated pool storage flush failed: %v\n", ext.ID, err)
			}
			handle.runtime.closeStorageFlusher()
		}
	}
}

// drainAllIsolatedRuntimePools releases every extension's idle isolated
// runtimes (memory-pressure hook).
func drainAllIsolatedRuntimePools() {
	m := getExtensionManager()
	m.mu.RLock()
	exts := make([]*loadedExtension, 0, len(m.extensions))
	for _, ext := range m.extensions {
		exts = append(exts, ext)
	}
	m.mu.RUnlock()

	for _, ext := range exts {
		drainIsolatedRuntimePool(ext)
	}
}

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]any,
) 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]any); 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]any,
) 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]any); 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) {
	drainIsolatedRuntimePool(ext)
	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
}

// supportedRuntimeFeatures maps every feature name the goja runtime provides
// to its current contract version (documented in SIGNED_SESSION_GUIDE.md).
