//go:build windows || darwin

package main

// #include "menu.h"
import "C"

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"time"
	"unsafe"

	"github.com/ollama/ollama/app/dialog"
	"github.com/ollama/ollama/app/store"
	"github.com/ollama/ollama/app/webview"
)

type Webview struct {
	port    int
	token   string
	webview webview.WebView
	mutex   sync.Mutex

	Store *store.Store
}

// Run initializes the webview and starts its event loop.
// Note: this must be called from the primary app thread
// This returns the OS native window handle to the caller
func (w *Webview) Run(path string) unsafe.Pointer {
	var url string
	if devMode {
		// In development mode, use the local dev server
		url = fmt.Sprintf("http://localhost:5173%s", path)
	} else {
		url = fmt.Sprintf("http://127.0.0.1:%d%s", w.port, path)
	}
	w.mutex.Lock()
	defer w.mutex.Unlock()

	if w.webview == nil {
		// Note: turning on debug on macos throws errors but is marginally functional for debugging
		// TODO (jmorganca): we should pre-create the window and then provide it here to
		// webview so we can hide it from the start and make other modifications
		wv := webview.New(debug)
		// start the window hidden
		hideWindow(wv.Window())
		wv.SetTitle("Ollama")

		// TODO (jmorganca): this isn't working yet since it needs to be set
		// on the first page load, ideally in an interstitial page like `/token`
		// that exists only to set the cookie and redirect to /
		// wv.Init(fmt.Sprintf(`document.cookie = "token=%s; path=/"`, w.token))
		init := `
		// Disable reload
		document.addEventListener('keydown', function(e) {
			if ((e.ctrlKey || e.metaKey) && e.key === 'r') {
				e.preventDefault();
				return false;
			}
		});

		// Prevent back/forward navigation
		window.addEventListener('popstate', function(e) {
			e.preventDefault();
			history.pushState(null, '', window.location.pathname);
			return false;
		});

		// Clear history on load
		window.addEventListener('load', function() {
			history.pushState(null, '', window.location.pathname);
			window.history.replaceState(null, '', window.location.pathname);
		});

		// Set token cookie
		document.cookie = "token=` + w.token + `; path=/";
	`
		// Windows-specific scrollbar styling
		if runtime.GOOS == "windows" {
			init += `
				// Fix scrollbar styling for Edge WebView2 on Windows only
				function updateScrollbarStyles() {
					const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
					const existingStyle = document.getElementById('scrollbar-style');
					if (existingStyle) existingStyle.remove();
					
					const style = document.createElement('style');
					style.id = 'scrollbar-style';
					
					if (isDark) {
						style.textContent = ` + "`" + `
							::-webkit-scrollbar { width: 6px !important; height: 6px !important; }
							::-webkit-scrollbar-track { background: #1a1a1a !important; }
							::-webkit-scrollbar-thumb { background: #404040 !important; border-radius: 6px !important; }
							::-webkit-scrollbar-thumb:hover { background: #505050 !important; }
							::-webkit-scrollbar-corner { background: #1a1a1a !important; }
							::-webkit-scrollbar-button { 
								background: transparent !important;
								border: none !important;
								width: 0px !important;
								height: 0px !important;
								margin: 0 !important;
								padding: 0 !important;
							}
							::-webkit-scrollbar-button:vertical:start:decrement {
								background: transparent !important;
								height: 0px !important;
							}
							::-webkit-scrollbar-button:vertical:end:increment {
								background: transparent !important;
								height: 0px !important;
							}
							::-webkit-scrollbar-button:horizontal:start:decrement {
								background: transparent !important;
								width: 0px !important;
							}
							::-webkit-scrollbar-button:horizontal:end:increment {
								background: transparent !important;
								width: 0px !important;
							}
						` + "`" + `;
					} else {
						style.textContent = ` + "`" + `
							::-webkit-scrollbar { width: 6px !important; height: 6px !important; }
							::-webkit-scrollbar-track { background: #f0f0f0 !important; }
							::-webkit-scrollbar-thumb { background: #c0c0c0 !important; border-radius: 6px !important; }
							::-webkit-scrollbar-thumb:hover { background: #a0a0a0 !important; }
							::-webkit-scrollbar-corner { background: #f0f0f0 !important; }
							::-webkit-scrollbar-button { 
								background: transparent !important;
								border: none !important;
								width: 0px !important;
								height: 0px !important;
								margin: 0 !important;
								padding: 0 !important;
							}
							::-webkit-scrollbar-button:vertical:start:decrement {
								background: transparent !important;
								height: 0px !important;
							}
							::-webkit-scrollbar-button:vertical:end:increment {
								background: transparent !important;
								height: 0px !important;
							}
							::-webkit-scrollbar-button:horizontal:start:decrement {
								background: transparent !important;
								width: 0px !important;
							}
							::-webkit-scrollbar-button:horizontal:end:increment {
								background: transparent !important;
								width: 0px !important;
							}
						` + "`" + `;
					}
					document.head.appendChild(style);
				}
				
				window.addEventListener('load', updateScrollbarStyles);
				window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateScrollbarStyles);
			`
		}
		// on windows make ctrl+n open new chat
		// TODO (jmorganca): later we should use proper accelerators
		// once we introduce a native menu for the window
		// this is only used on windows since macOS uses the proper accelerators
		if runtime.GOOS == "windows" {
			init += `
				document.addEventListener('keydown', function(e) {
					if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
						e.preventDefault();
						// Use the existing navigation method
						history.pushState({}, '', '/c/new');
						window.dispatchEvent(new PopStateEvent('popstate'));
						return false;
					}
				});
			`
		}

		init += `
			window.OLLAMA_WEBSEARCH = true;
		`

		wv.Init(init)

		// Add keyboard handler for zoom
		wv.Init(`
			window.addEventListener('keydown', function(e) {
				// CMD/Ctrl + Plus/Equals (zoom in)
				if ((e.metaKey || e.ctrlKey) && (e.key === '+' || e.key === '=')) {
					e.preventDefault();
					window.zoomIn && window.zoomIn();
					return false;
				}

				// CMD/Ctrl + Minus (zoom out)
				if ((e.metaKey || e.ctrlKey) && e.key === '-') {
					e.preventDefault();
					window.zoomOut && window.zoomOut();
					return false;
				}

				// CMD/Ctrl + 0 (reset zoom)
				if ((e.metaKey || e.ctrlKey) && e.key === '0') {
					e.preventDefault();
					window.zoomReset && window.zoomReset();
					return false;
				}
			}, true);
		`)

		wv.Bind("zoomIn", func() {
			current := wv.GetZoom()
			wv.SetZoom(current + 0.1)
		})

		wv.Bind("zoomOut", func() {
			current := wv.GetZoom()
			wv.SetZoom(current - 0.1)
		})

		wv.Bind("zoomReset", func() {
			wv.SetZoom(1.0)
		})

		wv.Bind("ready", func() {
			showWindow(wv.Window())
		})

		wv.Bind("close", func() {
			hideWindow(wv.Window())
		})

		// Webviews do not allow access to the file system by default, so we need to
		// bind file system operations here
		wv.Bind("selectModelsDirectory", func() {
			go func() {
				// Helper function to call the JavaScript callback with data or null
				callCallback := func(data interface{}) {
					dataJSON, _ := json.Marshal(data)
					wv.Dispatch(func() {
						wv.Eval(fmt.Sprintf("window.__selectModelsDirectoryCallback && window.__selectModelsDirectoryCallback(%s)", dataJSON))
					})
				}

				directory, err := dialog.Directory().Title("Select Model Directory").ShowHidden(true).Browse()
				if err != nil {
					slog.Debug("Directory selection cancelled or failed", "error", err)
					callCallback(nil)
					return
				}
				slog.Debug("Directory selected", "path", directory)
				callCallback(directory)
			}()
		})

		// Bind selectFiles function for selecting multiple files at once
		wv.Bind("selectFiles", func() {
			go func() {
				// Helper function to call the JavaScript callback with data or null
				callCallback := func(data interface{}) {
					dataJSON, _ := json.Marshal(data)
					wv.Dispatch(func() {
						wv.Eval(fmt.Sprintf("window.__selectFilesCallback && window.__selectFilesCallback(%s)", dataJSON))
					})
				}

				// Define allowed extensions for native dialog filtering
				textExts := []string{
					"pdf", "docx", "txt", "md", "csv", "json", "xml", "html", "htm",
					"js", "jsx", "ts", "tsx", "py", "java", "cpp", "c", "cc", "h", "cs", "php", "rb",
					"go", "rs", "swift", "kt", "scala", "sh", "bat", "yaml", "yml", "toml", "ini",
					"cfg", "conf", "log", "rtf",
				}
				imageExts := []string{"png", "jpg", "jpeg"}
				allowedExts := append(textExts, imageExts...)

				// Use native multiple file selection with extension filtering
				filenames, err := dialog.File().
					Filter("Supported Files", allowedExts...).
					Title("Select Files").
					LoadMultiple()
				if err != nil {
					slog.Debug("Multiple file selection cancelled or failed", "error", err)
					callCallback(nil)
					return
				}

				if len(filenames) == 0 {
					callCallback(nil)
					return
				}

				var files []map[string]string
				maxFileSize := int64(10 * 1024 * 1024) // 10MB

				for _, filename := range filenames {
					// Check file extension (double-check after native dialog filtering)
					ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
					validExt := false
					for _, allowedExt := range allowedExts {
						if ext == allowedExt {
							validExt = true
							break
						}
					}
					if !validExt {
						slog.Warn("file extension not allowed, skipping", "filename", filepath.Base(filename), "extension", ext)
						continue
					}

					// Check file size before reading (pre-filter large files)
					fileStat, err := os.Stat(filename)
					if err != nil {
						slog.Error("failed to get file info", "error", err, "filename", filename)
						continue
					}

					if fileStat.Size() > maxFileSize {
						slog.Warn("file too large, skipping", "filename", filepath.Base(filename), "size", fileStat.Size())
						continue
					}

					fileBytes, err := os.ReadFile(filename)
					if err != nil {
						slog.Error("failed to read file", "error", err, "filename", filename)
						continue
					}

					mimeType := http.DetectContentType(fileBytes)
					dataURL := fmt.Sprintf("data:%s;base64,%s", mimeType, base64.StdEncoding.EncodeToString(fileBytes))

					fileResult := map[string]string{
						"filename": filepath.Base(filename),
						"path":     filename,
						"dataURL":  dataURL,
					}

					files = append(files, fileResult)
				}

				if len(files) == 0 {
					callCallback(nil)
				} else {
					callCallback(files)
				}
			}()
		})

		wv.Bind("drag", func() {
			wv.Dispatch(func() {
				drag(wv.Window())
			})
		})

		wv.Bind("doubleClick", func() {
			wv.Dispatch(func() {
				doubleClick(wv.Window())
			})
		})

		// Add binding for working directory selection
		wv.Bind("selectWorkingDirectory", func() {
			go func() {
				// Helper function to call the JavaScript callback with data or null
				callCallback := func(data interface{}) {
					dataJSON, _ := json.Marshal(data)
					wv.Dispatch(func() {
						wv.Eval(fmt.Sprintf("window.__selectWorkingDirectoryCallback && window.__selectWorkingDirectoryCallback(%s)", dataJSON))
					})
				}

				directory, err := dialog.Directory().Title("Select Working Directory").ShowHidden(true).Browse()
				if err != nil {
					slog.Debug("Directory selection cancelled or failed", "error", err)
					callCallback(nil)
					return
				}
				slog.Debug("Directory selected", "path", directory)
				callCallback(directory)
			}()
		})

		wv.Bind("setContextMenuItems", func(items []map[string]interface{}) error {
			menuMutex.Lock()
			defer menuMutex.Unlock()

			if len(menuItems) > 0 {
				pinner.Unpin()
			}

			menuItems = nil
			for _, item := range items {
				menuItem := C.menuItem{
					label:     C.CString(item["label"].(string)),
					enabled:   0,
					separator: 0,
				}

				if item["enabled"] != nil {
					menuItem.enabled = 1
				}

				if item["separator"] != nil {
					menuItem.separator = 1
				}
				menuItems = append(menuItems, menuItem)
			}
			return nil
		})

		// Debounce resize events
		var resizeTimer *time.Timer
		var resizeMutex sync.Mutex

		wv.Bind("resize", func(width, height int) {
			if w.Store != nil {
				resizeMutex.Lock()
				if resizeTimer != nil {
					resizeTimer.Stop()
				}
				resizeTimer = time.AfterFunc(100*time.Millisecond, func() {
					err := w.Store.SetWindowSize(width, height)
					if err != nil {
						slog.Error("failed to set window size", "error", err)
					}
				})
				resizeMutex.Unlock()
			}
		})

		// On Darwin, we can't have 2 threads both running global event loops
		// but on Windows, the event loops are tied to the window, so we're
		// able to run in both the tray and webview
		if runtime.GOOS != "darwin" {
			slog.Debug("starting webview event loop")
			go func() {
				wv.Run()
				slog.Debug("webview event loop exited")
			}()
		}

		if w.Store != nil {
			width, height, err := w.Store.WindowSize()
			if err != nil {
				slog.Error("failed to get window size", "error", err)
			}
			if width > 0 && height > 0 {
				wv.SetSize(width, height, webview.HintNone)
			} else {
				wv.SetSize(800, 600, webview.HintNone)
			}
		}
		wv.SetSize(800, 600, webview.HintMin)

		w.webview = wv
		w.webview.Navigate(url)
	} else {
		w.webview.Eval(fmt.Sprintf(`
			history.pushState({}, '', '%s');
		`, path))
		showWindow(w.webview.Window())
	}

	return w.webview.Window()
}

func (w *Webview) Terminate() {
	w.mutex.Lock()
	if w.webview == nil {
		w.mutex.Unlock()
		return
	}

	wv := w.webview
	w.webview = nil
	w.mutex.Unlock()
	wv.Terminate()
	wv.Destroy()
}

func (w *Webview) IsRunning() bool {
	w.mutex.Lock()
	defer w.mutex.Unlock()
	return w.webview != nil
}

var (
	menuItems []C.menuItem
	menuMutex sync.RWMutex
	pinner    runtime.Pinner
)

//export menu_get_item_count
func menu_get_item_count() C.int {
	menuMutex.RLock()
	defer menuMutex.RUnlock()
	return C.int(len(menuItems))
}

//export menu_get_items
func menu_get_items() unsafe.Pointer {
	menuMutex.RLock()
	defer menuMutex.RUnlock()

	if len(menuItems) == 0 {
		return nil
	}

	// Return pointer to the slice data
	pinner.Pin(&menuItems[0])
	return unsafe.Pointer(&menuItems[0])
}

//export menu_handle_selection
func menu_handle_selection(item *C.char) {
	wv.webview.Eval(fmt.Sprintf("window.handleContextMenuResult('%s')", C.GoString(item)))
}
