package gobackend

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/dop251/goja"
)

var (
	allowedDownloadDirs   []string
	allowedDownloadDirsMu sync.RWMutex
)

func SetAllowedDownloadDirs(dirs []string) {
	allowedDownloadDirsMu.Lock()
	defer allowedDownloadDirsMu.Unlock()
	allowedDownloadDirs = dirs
	GoLog("[Extension] Allowed download directories set: %v\n", dirs)
}

func AddAllowedDownloadDir(dir string) {
	allowedDownloadDirsMu.Lock()
	defer allowedDownloadDirsMu.Unlock()
	absDir, err := filepath.Abs(dir)
	if err == nil {
		allowedDownloadDirs = append(allowedDownloadDirs, absDir)
	}
}

func isPathInAllowedDirs(absPath string) bool {
	allowedDownloadDirsMu.RLock()
	defer allowedDownloadDirsMu.RUnlock()

	for _, allowedDir := range allowedDownloadDirs {
		if isPathWithinBase(allowedDir, absPath) {
			return true
		}
	}
	return false
}

func isPathWithinBase(baseDir, targetPath string) bool {
	baseAbs, err := filepath.Abs(baseDir)
	if err != nil {
		return false
	}
	targetAbs, err := filepath.Abs(targetPath)
	if err != nil {
		return false
	}

	rel, err := filepath.Rel(baseAbs, targetAbs)
	if err != nil {
		return false
	}
	rel = filepath.Clean(rel)
	if rel == "." {
		return true
	}

	prefix := ".." + string(filepath.Separator)
	if rel == ".." || strings.HasPrefix(rel, prefix) {
		return false
	}
	return true
}

func (r *extensionRuntime) validatePath(path string) (string, error) {
	if !r.manifest.Permissions.File {
		return "", fmt.Errorf("file access denied: extension does not have 'file' permission")
	}

	cleanPath := filepath.Clean(path)

	if filepath.IsAbs(cleanPath) {
		absPath, err := filepath.Abs(cleanPath)
		if err != nil {
			return "", fmt.Errorf("invalid path: %w", err)
		}

		if isPathInAllowedDirs(absPath) {
			return absPath, nil
		}

		return "", fmt.Errorf("file access denied: absolute paths are not allowed. Use relative paths within extension sandbox")
	}

	fullPath := filepath.Join(r.dataDir, cleanPath)

	absPath, err := filepath.Abs(fullPath)
	if err != nil {
		return "", fmt.Errorf("invalid path: %w", err)
	}

	absDataDir, _ := filepath.Abs(r.dataDir)
	if !isPathWithinBase(absDataDir, absPath) {
		return "", fmt.Errorf("file access denied: path '%s' is outside sandbox", path)
	}

	return absPath, nil
}

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

	urlStr := call.Arguments[0].String()
	outputPath := call.Arguments[1].String()

	if err := r.validateDomain(urlStr); err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

	fullPath, err := r.validatePath(outputPath)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

	var onProgress goja.Callable
	var headers map[string]string
	var chunkedDownload bool
	trackItemBytes := true
	var chunkSize int64
	if len(call.Arguments) > 2 && !goja.IsUndefined(call.Arguments[2]) && !goja.IsNull(call.Arguments[2]) {
		optionsObj := call.Arguments[2].Export()
		if opts, ok := optionsObj.(map[string]interface{}); ok {
			if h, ok := opts["headers"].(map[string]interface{}); ok {
				headers = make(map[string]string)
				for k, v := range h {
					headers[k] = fmt.Sprintf("%v", v)
				}
			}
			if progressVal, ok := opts["onProgress"]; ok {
				if callable, ok := goja.AssertFunction(r.vm.ToValue(progressVal)); ok {
					onProgress = callable
				}
			}
			if trackBytes, ok := opts["trackItemBytes"]; ok {
				if v, ok := trackBytes.(bool); ok {
					trackItemBytes = v
				}
			} else if trackBytes, ok := opts["track_item_bytes"]; ok {
				if v, ok := trackBytes.(bool); ok {
					trackItemBytes = v
				}
			}
			if chunked, ok := opts["chunked"]; ok {
				switch v := chunked.(type) {
				case bool:
					chunkedDownload = v
				case int64:
					if v > 0 {
						chunkedDownload = true
						chunkSize = v
					}
				case float64:
					if v > 0 {
						chunkedDownload = true
						chunkSize = int64(v)
					}
				}
			}
		}
	}

	// Default chunk size: 1MB (YouTube CDN max without poToken)
	if chunkedDownload && chunkSize <= 0 {
		chunkSize = 1024 * 1024
	}

	dir := filepath.Dir(fullPath)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   fmt.Sprintf("failed to create directory: %v", err),
		})
	}

	client := r.downloadClient
	if client == nil {
		client = r.httpClient
	}

	ua := appUserAgent()
	if h, ok := headers["User-Agent"]; ok && h != "" {
		ua = h
	}

	if chunkedDownload {
		return r.fileDownloadChunked(client, urlStr, fullPath, headers, ua, chunkSize, onProgress, trackItemBytes)
	}

	req, err := http.NewRequest("GET", urlStr, nil)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}
	req = r.bindDownloadCancelContext(req)

	for k, v := range headers {
		req.Header.Set(k, v)
	}
	if req.Header.Get("User-Agent") == "" {
		req.Header.Set("User-Agent", appUserAgent())
	}

	resp, err := client.Do(req)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}
	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   fmt.Sprintf("HTTP error: %d", resp.StatusCode),
		})
	}

	out, err := os.Create(fullPath)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   fmt.Sprintf("failed to create file: %v", err),
		})
	}
	defer out.Close()

	activeItemID := r.getActiveDownloadItemID()
	if activeItemID != "" {
		SetItemDownloading(activeItemID)
	}

	contentLength := resp.ContentLength
	shouldTrackItemBytes := activeItemID != "" && trackItemBytes
	if shouldTrackItemBytes && contentLength > 0 {
		SetItemBytesTotal(activeItemID, contentLength)
	}

	var progressWriter interface{ Write([]byte) (int, error) } = out
	if shouldTrackItemBytes {
		progressWriter = NewItemProgressWriter(out, activeItemID)
	}

	var written int64
	buf := make([]byte, 32*1024)
	for {
		nr, er := resp.Body.Read(buf)
		if nr > 0 {
			nw, ew := progressWriter.Write(buf[0:nr])
			if nw < 0 || nr < nw {
				nw = 0
				if ew == nil {
					ew = fmt.Errorf("invalid write result")
				}
			}
			written += int64(nw)
			if ew != nil {
				if ew == ErrDownloadCancelled {
					return r.vm.ToValue(map[string]interface{}{
						"success": false,
						"error":   "download cancelled",
					})
				}
				return r.vm.ToValue(map[string]interface{}{
					"success": false,
					"error":   fmt.Sprintf("failed to write file: %v", ew),
				})
			}
			if nr != nw {
				return r.vm.ToValue(map[string]interface{}{
					"success": false,
					"error":   "short write",
				})
			}

			if onProgress != nil && contentLength > 0 {
				_, _ = onProgress(goja.Undefined(), r.vm.ToValue(written), r.vm.ToValue(contentLength))
			}
		}
		if er != nil {
			if er != io.EOF {
				return r.vm.ToValue(map[string]interface{}{
					"success": false,
					"error":   fmt.Sprintf("failed to read response: %v", er),
				})
			}
			break
		}
	}

	if shouldTrackItemBytes {
		if contentLength > 0 {
			SetItemProgress(activeItemID, float64(written)/float64(contentLength), written, contentLength)
		} else if written > 0 {
			SetItemBytesReceived(activeItemID, written)
		}
	}

	GoLog("[Extension:%s] Downloaded %d bytes to %s\n", r.extensionID, written, fullPath)

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

// fileDownloadChunked downloads a URL using sequential Range requests.
// This is needed for servers (like YouTube's googlevideo CDN) that reject
// non-ranged or large-range requests with 403 and require small chunk downloads.
func (r *extensionRuntime) fileDownloadChunked(client *http.Client, urlStr, fullPath string, headers map[string]string, ua string, chunkSize int64, onProgress goja.Callable, trackItemBytes bool) goja.Value {
	// First, get the total content length with a small probe request
	probeReq, err := http.NewRequest("GET", urlStr, nil)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   fmt.Sprintf("chunked: probe request error: %v", err),
		})
	}
	probeReq = r.bindDownloadCancelContext(probeReq)
	probeReq.Header.Set("User-Agent", ua)
	for k, v := range headers {
		if k != "Range" { // Don't copy any existing Range header
			probeReq.Header.Set(k, v)
		}
	}
	probeReq.Header.Set("Range", "bytes=0-1")

	probeResp, err := client.Do(probeReq)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   fmt.Sprintf("chunked: probe error: %v", err),
		})
	}
	io.Copy(io.Discard, probeResp.Body)
	probeResp.Body.Close()

	if probeResp.StatusCode != 206 && probeResp.StatusCode != 200 {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   fmt.Sprintf("chunked: probe HTTP %d", probeResp.StatusCode),
		})
	}

	// Parse Content-Range to get total size: "bytes 0-1/TOTAL"
	var totalSize int64
	contentRange := probeResp.Header.Get("Content-Range")
	if contentRange != "" {
		// Format: "bytes 0-1/12345"
		if idx := strings.LastIndex(contentRange, "/"); idx >= 0 {
			sizeStr := contentRange[idx+1:]
			if sizeStr != "*" {
				fmt.Sscanf(sizeStr, "%d", &totalSize)
			}
		}
	}

	if totalSize <= 0 {
		// Fallback: try Content-Length from a HEAD-like approach
		// If we can't determine size, download with unknown size
		GoLog("[Extension:%s] Chunked download: unknown total size, will download until server says done\n", r.extensionID)
	} else {
		GoLog("[Extension:%s] Chunked download: total size %d bytes, chunk size %d\n", r.extensionID, totalSize, chunkSize)
	}

	out, err := os.Create(fullPath)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   fmt.Sprintf("failed to create file: %v", err),
		})
	}
	defer out.Close()

	activeItemID := r.getActiveDownloadItemID()
	if activeItemID != "" {
		SetItemDownloading(activeItemID)
	}

	shouldTrackItemBytes := activeItemID != "" && trackItemBytes
	if shouldTrackItemBytes && totalSize > 0 {
		SetItemBytesTotal(activeItemID, totalSize)
	}

	var progressWriter interface{ Write([]byte) (int, error) } = out
	if shouldTrackItemBytes {
		progressWriter = NewItemProgressWriter(out, activeItemID)
	}

	var totalWritten int64
	buf := make([]byte, 32*1024)
	maxRetries := 3

	for offset := int64(0); totalSize <= 0 || offset < totalSize; {
		end := offset + chunkSize - 1
		if totalSize > 0 && end >= totalSize {
			end = totalSize - 1
		}

		var chunkResp *http.Response
		var chunkErr error

		for retry := 0; retry < maxRetries; retry++ {
			chunkReq, err := http.NewRequest("GET", urlStr, nil)
			if err != nil {
				return r.vm.ToValue(map[string]interface{}{
					"success": false,
					"error":   fmt.Sprintf("chunked: request error at offset %d: %v", offset, err),
				})
			}
			chunkReq = r.bindDownloadCancelContext(chunkReq)
			chunkReq.Header.Set("User-Agent", ua)
			for k, v := range headers {
				if k != "Range" {
					chunkReq.Header.Set(k, v)
				}
			}
			chunkReq.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, end))

			chunkResp, chunkErr = client.Do(chunkReq)
			if chunkErr != nil {
				if retry < maxRetries-1 {
					time.Sleep(time.Duration(retry+1) * time.Second)
					continue
				}
				return r.vm.ToValue(map[string]interface{}{
					"success": false,
					"error":   fmt.Sprintf("chunked: error at offset %d after %d retries: %v", offset, maxRetries, chunkErr),
				})
			}

			if chunkResp.StatusCode == 206 || chunkResp.StatusCode == 200 {
				break // Success
			}

			// Non-success status
			io.Copy(io.Discard, chunkResp.Body)
			chunkResp.Body.Close()

			if chunkResp.StatusCode == 403 || chunkResp.StatusCode == 429 {
				if retry < maxRetries-1 {
					time.Sleep(time.Duration(retry+1) * 2 * time.Second)
					continue
				}
			}

			return r.vm.ToValue(map[string]interface{}{
				"success": false,
				"error":   fmt.Sprintf("chunked: HTTP %d at offset %d", chunkResp.StatusCode, offset),
			})
		}

		// Read chunk body and write to file
		chunkWritten := int64(0)
		for {
			nr, er := chunkResp.Body.Read(buf)
			if nr > 0 {
				nw, ew := progressWriter.Write(buf[0:nr])
				if nw < 0 || nr < nw {
					nw = 0
					if ew == nil {
						ew = fmt.Errorf("invalid write result")
					}
				}
				chunkWritten += int64(nw)
				totalWritten += int64(nw)
				if ew != nil {
					chunkResp.Body.Close()
					if ew == ErrDownloadCancelled {
						return r.vm.ToValue(map[string]interface{}{
							"success": false,
							"error":   "download cancelled",
						})
					}
					return r.vm.ToValue(map[string]interface{}{
						"success": false,
						"error":   fmt.Sprintf("failed to write file: %v", ew),
					})
				}
				if nr != nw {
					chunkResp.Body.Close()
					return r.vm.ToValue(map[string]interface{}{
						"success": false,
						"error":   "short write",
					})
				}

				if onProgress != nil && totalSize > 0 {
					_, _ = onProgress(goja.Undefined(), r.vm.ToValue(totalWritten), r.vm.ToValue(totalSize))
				}
			}
			if er != nil {
				if er != io.EOF {
					chunkResp.Body.Close()
					return r.vm.ToValue(map[string]interface{}{
						"success": false,
						"error":   fmt.Sprintf("failed to read chunk at offset %d: %v", offset, er),
					})
				}
				break
			}
		}
		chunkResp.Body.Close()

		offset += chunkWritten

		// If server returned 200 (full content) instead of 206, we're done
		if chunkResp.StatusCode == 200 {
			break
		}

		// If we got less data than expected and we know total size, check if done
		if totalSize > 0 && offset >= totalSize {
			break
		}

		// Unknown size: if we got less than chunk size, assume done
		if totalSize <= 0 && chunkWritten < chunkSize {
			break
		}
	}

	if shouldTrackItemBytes {
		if totalSize > 0 {
			SetItemProgress(activeItemID, float64(totalWritten)/float64(totalSize), totalWritten, totalSize)
		} else if totalWritten > 0 {
			SetItemBytesReceived(activeItemID, totalWritten)
		}
	}

	GoLog("[Extension:%s] Chunked download complete: %d bytes to %s\n", r.extensionID, totalWritten, fullPath)

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

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

	path := call.Arguments[0].String()
	fullPath, err := r.validatePath(path)
	if err != nil {
		return r.vm.ToValue(false)
	}

	_, err = os.Stat(fullPath)
	return r.vm.ToValue(err == nil)
}

func (r *extensionRuntime) fileDelete(call goja.FunctionCall) goja.Value {
	if len(call.Arguments) < 1 {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   "path is required",
		})
	}

	path := call.Arguments[0].String()
	fullPath, err := r.validatePath(path)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

	if err := os.Remove(fullPath); err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

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

func (r *extensionRuntime) fileRead(call goja.FunctionCall) goja.Value {
	if len(call.Arguments) < 1 {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   "path is required",
		})
	}

	path := call.Arguments[0].String()
	fullPath, err := r.validatePath(path)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

	data, err := os.ReadFile(fullPath)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

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

func (r *extensionRuntime) fileReadBytes(call goja.FunctionCall) goja.Value {
	if len(call.Arguments) < 1 {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   "path is required",
		})
	}

	path := call.Arguments[0].String()
	fullPath, err := r.validatePath(path)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

	options := parseRuntimeOptionsArgument(call, 1)
	offset := runtimeOptionInt64(options, "offset", 0)
	length := runtimeOptionInt64(options, "length", -1)
	encoding := runtimeOptionString(options, "encoding", "base64")
	if offset < 0 {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   "offset must be >= 0",
		})
	}

	file, err := os.Open(fullPath)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}
	defer file.Close()

	info, err := file.Stat()
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

	size := info.Size()
	if offset > size {
		offset = size
	}
	if _, err := file.Seek(offset, io.SeekStart); err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   fmt.Sprintf("failed to seek file: %v", err),
		})
	}

	var data []byte
	switch {
	case length == 0:
		data = []byte{}
	case length > 0:
		buf := make([]byte, int(length))
		n, readErr := file.Read(buf)
		if readErr != nil && readErr != io.EOF {
			return r.vm.ToValue(map[string]interface{}{
				"success": false,
				"error":   fmt.Sprintf("failed to read file: %v", readErr),
			})
		}
		data = buf[:n]
	default:
		data, err = io.ReadAll(file)
		if err != nil {
			return r.vm.ToValue(map[string]interface{}{
				"success": false,
				"error":   fmt.Sprintf("failed to read file: %v", err),
			})
		}
	}

	encoded, err := encodeRuntimeBytes(data, encoding)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

	return r.vm.ToValue(map[string]interface{}{
		"success":    true,
		"data":       encoded,
		"bytes_read": len(data),
		"offset":     offset,
		"size":       size,
		"eof":        offset+int64(len(data)) >= size,
	})
}

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

	path := call.Arguments[0].String()
	data := call.Arguments[1].String()

	fullPath, err := r.validatePath(path)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

	dir := filepath.Dir(fullPath)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   fmt.Sprintf("failed to create directory: %v", err),
		})
	}

	if err := os.WriteFile(fullPath, []byte(data), 0644); err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

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

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

	path := call.Arguments[0].String()
	fullPath, err := r.validatePath(path)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

	options := parseRuntimeOptionsArgument(call, 2)
	appendMode := runtimeOptionBool(options, "append", false)
	truncate := runtimeOptionBool(options, "truncate", false)
	hasOffset := runtimeOptionHasKey(options, "offset")
	offset := runtimeOptionInt64(options, "offset", 0)
	encoding := runtimeOptionString(options, "encoding", "base64")

	if appendMode && hasOffset {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   "append and offset cannot be used together",
		})
	}
	if offset < 0 {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   "offset must be >= 0",
		})
	}

	data, err := decodeRuntimeBytesValue(call.Arguments[1].Export(), encoding)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

	dir := filepath.Dir(fullPath)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   fmt.Sprintf("failed to create directory: %v", err),
		})
	}

	flags := os.O_CREATE | os.O_WRONLY
	if appendMode {
		flags |= os.O_APPEND
	}
	if truncate {
		flags |= os.O_TRUNC
	}

	file, err := os.OpenFile(fullPath, flags, 0644)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}
	defer file.Close()

	if hasOffset && !appendMode {
		if _, err := file.Seek(offset, io.SeekStart); err != nil {
			return r.vm.ToValue(map[string]interface{}{
				"success": false,
				"error":   fmt.Sprintf("failed to seek file: %v", err),
			})
		}
	}

	written, err := file.Write(data)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

	info, statErr := file.Stat()
	size := int64(0)
	if statErr == nil {
		size = info.Size()
	}

	return r.vm.ToValue(map[string]interface{}{
		"success":       true,
		"path":          fullPath,
		"bytes_written": written,
		"size":          size,
	})
}

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

	srcPath := call.Arguments[0].String()
	dstPath := call.Arguments[1].String()

	fullSrc, err := r.validatePath(srcPath)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

	fullDst, err := r.validatePath(dstPath)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

	srcFile, err := os.Open(fullSrc)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   fmt.Sprintf("failed to read source: %v", err),
		})
	}
	defer srcFile.Close()

	dir := filepath.Dir(fullDst)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   fmt.Sprintf("failed to create directory: %v", err),
		})
	}

	dstFile, err := os.OpenFile(fullDst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   fmt.Sprintf("failed to open destination: %v", err),
		})
	}

	if _, err := io.Copy(dstFile, srcFile); err != nil {
		_ = dstFile.Close()
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   fmt.Sprintf("failed to copy file: %v", err),
		})
	}

	if err := dstFile.Close(); err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   fmt.Sprintf("failed to finalize destination: %v", err),
		})
	}

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

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

	srcPath := call.Arguments[0].String()
	dstPath := call.Arguments[1].String()

	fullSrc, err := r.validatePath(srcPath)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

	fullDst, err := r.validatePath(dstPath)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

	dir := filepath.Dir(fullDst)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   fmt.Sprintf("failed to create directory: %v", err),
		})
	}

	if err := os.Rename(fullSrc, fullDst); err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   fmt.Sprintf("failed to move file: %v", err),
		})
	}

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

func (r *extensionRuntime) fileGetSize(call goja.FunctionCall) goja.Value {
	if len(call.Arguments) < 1 {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   "path is required",
		})
	}

	path := call.Arguments[0].String()
	fullPath, err := r.validatePath(path)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

	info, err := os.Stat(fullPath)
	if err != nil {
		return r.vm.ToValue(map[string]interface{}{
			"success": false,
			"error":   err.Error(),
		})
	}

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