package gobackend

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"reflect"
	"time"

	"github.com/dop251/goja"
)

const (
	defaultStorageFlushDelay = 400 * time.Millisecond
	storageFlushRetryDelay   = 2 * time.Second
)

func (r *extensionRuntime) getStoragePath() string {
	return filepath.Join(r.dataDir, "storage.json")
}

func cloneInterfaceMap(src map[string]interface{}) map[string]interface{} {
	if len(src) == 0 {
		return make(map[string]interface{})
	}
	dst := make(map[string]interface{}, len(src))
	for k, v := range src {
		dst[k] = v
	}
	return dst
}

func (r *extensionRuntime) ensureStorageLoaded() error {
	r.storageMu.RLock()
	if r.storageLoaded {
		r.storageMu.RUnlock()
		return nil
	}
	r.storageMu.RUnlock()

	r.storageMu.Lock()
	defer r.storageMu.Unlock()
	if r.storageLoaded {
		return nil
	}

	storagePath := r.getStoragePath()
	data, err := os.ReadFile(storagePath)
	if err != nil {
		if os.IsNotExist(err) {
			r.storageCache = make(map[string]interface{})
			r.storageLoaded = true
			return nil
		}
		return err
	}

	var storage map[string]interface{}
	if err := json.Unmarshal(data, &storage); err != nil {
		return err
	}
	if storage == nil {
		storage = make(map[string]interface{})
	}

	r.storageCache = storage
	r.storageLoaded = true
	return nil
}

func (r *extensionRuntime) loadStorage() (map[string]interface{}, error) {
	if err := r.ensureStorageLoaded(); err != nil {
		return nil, err
	}

	r.storageMu.RLock()
	defer r.storageMu.RUnlock()
	return cloneInterfaceMap(r.storageCache), nil
}

func (r *extensionRuntime) queueStorageFlushLocked(delay time.Duration) {
	if r.storageClosed {
		return
	}
	if r.storageTimer != nil {
		return
	}
	r.storageTimer = time.AfterFunc(delay, r.flushStorageDirtyAsync)
}

func (r *extensionRuntime) persistStorageSnapshot(storage map[string]interface{}) error {
	data, err := json.Marshal(storage)
	if err != nil {
		return err
	}

	r.storageWriteMu.Lock()
	defer r.storageWriteMu.Unlock()

	return os.WriteFile(r.getStoragePath(), data, 0600)
}

func (r *extensionRuntime) flushStorageDirtyAsync() {
	if err := r.flushStorageDirty(); err != nil {
		GoLog("[Extension:%s] Storage flush error: %v\n", r.extensionID, err)
	}
}

func (r *extensionRuntime) flushStorageDirty() error {
	r.storageMu.Lock()
	if r.storageClosed {
		r.storageTimer = nil
		r.storageMu.Unlock()
		return nil
	}
	if !r.storageDirty {
		r.storageTimer = nil
		r.storageMu.Unlock()
		return nil
	}
	snapshot := cloneInterfaceMap(r.storageCache)
	r.storageDirty = false
	r.storageTimer = nil
	r.storageMu.Unlock()

	if err := r.persistStorageSnapshot(snapshot); err != nil {
		r.storageMu.Lock()
		r.storageDirty = true
		r.queueStorageFlushLocked(storageFlushRetryDelay)
		r.storageMu.Unlock()
		return err
	}

	return nil
}

func (r *extensionRuntime) flushStorageNow() error {
	r.storageMu.Lock()
	if r.storageTimer != nil {
		r.storageTimer.Stop()
		r.storageTimer = nil
	}
	if !r.storageLoaded || r.storageClosed {
		r.storageMu.Unlock()
		return nil
	}
	snapshot := cloneInterfaceMap(r.storageCache)
	r.storageDirty = false
	r.storageMu.Unlock()

	return r.persistStorageSnapshot(snapshot)
}

func (r *extensionRuntime) closeStorageFlusher() {
	r.storageMu.Lock()
	r.storageClosed = true
	r.storageDirty = false
	if r.storageTimer != nil {
		r.storageTimer.Stop()
		r.storageTimer = nil
	}
	r.storageMu.Unlock()
}

func (r *extensionRuntime) storageGet(call goja.FunctionCall) goja.Value {
	if len(call.Arguments) < 1 {
		return goja.Undefined()
	}

	key := call.Arguments[0].String()

	if err := r.ensureStorageLoaded(); err != nil {
		GoLog("[Extension:%s] Storage load error: %v\n", r.extensionID, err)
		return goja.Undefined()
	}

	r.storageMu.RLock()
	value, exists := r.storageCache[key]
	r.storageMu.RUnlock()
	if !exists {
		if len(call.Arguments) > 1 {
			return call.Arguments[1]
		}
		return goja.Undefined()
	}

	return r.vm.ToValue(value)
}

func (r *extensionRuntime) storageSet(call goja.FunctionCall) goja.Value {
	if len(call.Arguments) < 2 {
		return r.vm.ToValue(false)
	}

	key := call.Arguments[0].String()
	value := call.Arguments[1].Export()

	if err := r.ensureStorageLoaded(); err != nil {
		GoLog("[Extension:%s] Storage load error: %v\n", r.extensionID, err)
		return r.vm.ToValue(false)
	}

	r.storageMu.Lock()
	if r.storageClosed {
		r.storageMu.Unlock()
		return r.vm.ToValue(false)
	}
	if existing, exists := r.storageCache[key]; exists {
		if reflect.DeepEqual(existing, value) {
			r.storageMu.Unlock()
			return r.vm.ToValue(true)
		}
	}
	r.storageCache[key] = value
	r.storageDirty = true
	r.queueStorageFlushLocked(r.storageFlushDelay)
	r.storageMu.Unlock()

	return r.vm.ToValue(true)
}

func (r *extensionRuntime) storageRemove(call goja.FunctionCall) goja.Value {
	if len(call.Arguments) < 1 {
		return r.vm.ToValue(false)
	}

	key := call.Arguments[0].String()

	if err := r.ensureStorageLoaded(); err != nil {
		GoLog("[Extension:%s] Storage load error: %v\n", r.extensionID, err)
		return r.vm.ToValue(false)
	}

	r.storageMu.Lock()
	if r.storageClosed {
		r.storageMu.Unlock()
		return r.vm.ToValue(false)
	}
	if _, exists := r.storageCache[key]; !exists {
		r.storageMu.Unlock()
		return r.vm.ToValue(true)
	}
	delete(r.storageCache, key)
	r.storageDirty = true
	r.queueStorageFlushLocked(r.storageFlushDelay)
	r.storageMu.Unlock()

	return r.vm.ToValue(true)
}

func (r *extensionRuntime) getCredentialsPath() string {
	return filepath.Join(r.dataDir, ".credentials.enc")
}

func (r *extensionRuntime) getSaltPath() string {
	return filepath.Join(r.dataDir, ".cred_salt")
}

func (r *extensionRuntime) getOrCreateSalt() ([]byte, error) {
	saltPath := r.getSaltPath()

	salt, err := os.ReadFile(saltPath)
	if err == nil && len(salt) == 32 {
		return salt, nil
	}

	salt = make([]byte, 32)
	if _, err := io.ReadFull(rand.Reader, salt); err != nil {
		return nil, fmt.Errorf("failed to generate salt: %w", err)
	}

	if err := os.WriteFile(saltPath, salt, 0600); err != nil {
		return nil, fmt.Errorf("failed to save salt: %w", err)
	}

	return salt, nil
}

func (r *extensionRuntime) getEncryptionKey() ([]byte, error) {
	salt, err := r.getOrCreateSalt()
	if err != nil {
		return nil, err
	}

	combined := append([]byte(r.extensionID), salt...)
	hash := sha256.Sum256(combined)
	return hash[:], nil
}

func (r *extensionRuntime) ensureCredentialsLoaded() error {
	r.credentialsMu.RLock()
	if r.credentialsLoaded {
		r.credentialsMu.RUnlock()
		return nil
	}
	r.credentialsMu.RUnlock()

	r.credentialsMu.Lock()
	defer r.credentialsMu.Unlock()
	if r.credentialsLoaded {
		return nil
	}

	credPath := r.getCredentialsPath()
	data, err := os.ReadFile(credPath)
	if err != nil {
		if os.IsNotExist(err) {
			r.credentialsCache = make(map[string]interface{})
			r.credentialsLoaded = true
			return nil
		}
		return err
	}

	key, err := r.getEncryptionKey()
	if err != nil {
		return fmt.Errorf("failed to get encryption key: %w", err)
	}
	decrypted, err := decryptAES(data, key)
	if err != nil {
		return fmt.Errorf("failed to decrypt credentials: %w", err)
	}

	var creds map[string]interface{}
	if err := json.Unmarshal(decrypted, &creds); err != nil {
		return err
	}
	if creds == nil {
		creds = make(map[string]interface{})
	}

	r.credentialsCache = creds
	r.credentialsLoaded = true
	return nil
}

func (r *extensionRuntime) saveCredentials(creds map[string]interface{}) error {
	data, err := json.Marshal(creds)
	if err != nil {
		return err
	}

	key, err := r.getEncryptionKey()
	if err != nil {
		return fmt.Errorf("failed to get encryption key: %w", err)
	}
	encrypted, err := encryptAES(data, key)
	if err != nil {
		return fmt.Errorf("failed to encrypt credentials: %w", err)
	}

	credPath := r.getCredentialsPath()
	if err := os.WriteFile(credPath, encrypted, 0600); err != nil {
		return err
	}

	r.credentialsMu.Lock()
	r.credentialsCache = cloneInterfaceMap(creds)
	r.credentialsLoaded = true
	r.credentialsMu.Unlock()
	return nil
}

func (r *extensionRuntime) credentialsStore(call goja.FunctionCall) goja.Value {
	if len(call.Arguments) < 2 {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   "key and value are required",
		})
	}

	key := call.Arguments[0].String()
	value := call.Arguments[1].Export()

	if err := r.ensureCredentialsLoaded(); err != nil {
		GoLog("[Extension:%s] Credentials load error: %v\n", r.extensionID, err)
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

	r.credentialsMu.RLock()
	nextCreds := cloneInterfaceMap(r.credentialsCache)
	r.credentialsMu.RUnlock()
	nextCreds[key] = value

	if err := r.saveCredentials(nextCreds); err != nil {
		GoLog("[Extension:%s] Credentials save error: %v\n", r.extensionID, err)
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

	return r.vm.ToValue(map[string]interface{}{
		"success": true,
	})
}

func (r *extensionRuntime) credentialsGet(call goja.FunctionCall) goja.Value {
	if len(call.Arguments) < 1 {
		return goja.Undefined()
	}

	key := call.Arguments[0].String()

	if err := r.ensureCredentialsLoaded(); err != nil {
		GoLog("[Extension:%s] Credentials load error: %v\n", r.extensionID, err)
		return goja.Undefined()
	}

	r.credentialsMu.RLock()
	value, exists := r.credentialsCache[key]
	r.credentialsMu.RUnlock()
	if !exists {
		if len(call.Arguments) > 1 {
			return call.Arguments[1]
		}
		return goja.Undefined()
	}

	return r.vm.ToValue(value)
}

func (r *extensionRuntime) credentialsRemove(call goja.FunctionCall) goja.Value {
	if len(call.Arguments) < 1 {
		return r.vm.ToValue(false)
	}

	key := call.Arguments[0].String()

	if err := r.ensureCredentialsLoaded(); err != nil {
		GoLog("[Extension:%s] Credentials load error: %v\n", r.extensionID, err)
		return r.vm.ToValue(false)
	}

	r.credentialsMu.RLock()
	nextCreds := cloneInterfaceMap(r.credentialsCache)
	r.credentialsMu.RUnlock()
	delete(nextCreds, key)

	if err := r.saveCredentials(nextCreds); err != nil {
		GoLog("[Extension:%s] Credentials save error: %v\n", r.extensionID, err)
		return r.vm.ToValue(false)
	}

	return r.vm.ToValue(true)
}

func (r *extensionRuntime) credentialsHas(call goja.FunctionCall) goja.Value {
	if len(call.Arguments) < 1 {
		return r.vm.ToValue(false)
	}

	key := call.Arguments[0].String()

	if err := r.ensureCredentialsLoaded(); err != nil {
		return r.vm.ToValue(false)
	}

	r.credentialsMu.RLock()
	_, exists := r.credentialsCache[key]
	r.credentialsMu.RUnlock()
	return r.vm.ToValue(exists)
}

func encryptAES(plaintext []byte, key []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}

	nonce := make([]byte, gcm.NonceSize())
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return nil, err
	}

	ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
	return ciphertext, nil
}

func decryptAES(ciphertext []byte, key []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}

	nonceSize := gcm.NonceSize()
	if len(ciphertext) < nonceSize {
		return nil, fmt.Errorf("ciphertext too short")
	}

	nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
	plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		return nil, err
	}

	return plaintext, nil
}
