package application

import (
	"context"
	"embed"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"os"
	"runtime"
	"slices"
	"strconv"
	"strings"
	"sync"

	"github.com/wailsapp/wails/v3/internal/assetserver"
	"github.com/wailsapp/wails/v3/internal/assetserver/bundledassets"
	"github.com/wailsapp/wails/v3/internal/assetserver/webview"
	"github.com/wailsapp/wails/v3/internal/capabilities"
)

//go:embed assets/*
var alphaAssets embed.FS

var globalApplication *App

// AlphaAssets is the default assets for the alpha application
var AlphaAssets = AssetOptions{
	Handler: BundledAssetFileServer(alphaAssets),
}

type EventListener struct {
	callback func(app *ApplicationEvent)
}

func Get() *App {
	return globalApplication
}

func New(appOptions Options) *App {
	if globalApplication != nil {
		return globalApplication
	}

	mergeApplicationDefaults(&appOptions)

	result := newApplication(appOptions)
	globalApplication = result
	fatalHandler(result.handleFatalError)

	if result.Logger == nil {
		if result.isDebugMode {
			result.Logger = DefaultLogger(result.options.LogLevel)
		} else {
			result.Logger = slog.New(slog.NewTextHandler(io.Discard, nil))
		}
	}

	// Set up signal handling (platform-specific)
	result.setupSignalHandler(appOptions)

	result.logStartup()
	result.logPlatformInfo()

	result.customEventProcessor = NewWailsEventProcessor(result.Event.dispatch)

	messageProc := NewMessageProcessor(result.Logger)

	// Initialize transport (default to HTTP if not specified)
	transport := appOptions.Transport
	if transport == nil {
		transport = NewHTTPTransport(HTTPTransportWithLogger(result.Logger))
	}

	err := transport.Start(result.ctx, messageProc)
	if err != nil {
		result.fatal("failed to start custom transport: %w", err)
	}
	// Register shutdown task to stop transport
	result.OnShutdown(func() {
		if err := transport.Stop(); err != nil {
			result.error("failed to stop custom transport: %w", err)
		}
	})

	// Auto-wire events if transport supports event delivery
	if eventTransport, ok := transport.(WailsEventListener); ok {
		result.wailsEventListeners = append(result.wailsEventListeners, eventTransport)
	} else {
		// otherwise fallback to IPC
		result.wailsEventListeners = append(result.wailsEventListeners, &EventIPCTransport{
			app: result,
		})
	}

	middlewares := []assetserver.Middleware{
		func(next http.Handler) http.Handler {
			if m := appOptions.Assets.Middleware; m != nil {
				return m(next)
			}
			return next
		},
		func(next http.Handler) http.Handler {
			return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
				path := req.URL.Path
				switch path {
				case "/wails/runtime.js":
					err := assetserver.ServeFile(rw, path, bundledassets.RuntimeJS)
					if err != nil {
						result.fatal("unable to serve runtime.js: %w", err)
					}
				case "/wails/transport.js":
					err := assetserver.ServeFile(rw, path, transport.JSClient())
					if err != nil {
						result.fatal("unable to serve transport.js: %w", err)
					}
				default:
					next.ServeHTTP(rw, req)
				}
			})
		},
	}

	if handler, ok := transport.(TransportHTTPHandler); ok {
		middlewares = append(middlewares, handler.Handler())
	}

	opts := &assetserver.Options{
		Handler:    appOptions.Assets.Handler,
		Middleware: assetserver.ChainMiddleware(middlewares...),
		Logger:     result.Logger,
	}

	if appOptions.Assets.DisableLogging {
		opts.Logger = slog.New(slog.NewTextHandler(io.Discard, nil))
	}

	srv, err := assetserver.NewAssetServer(opts)
	if err != nil {
		result.fatal("application initialisation failed: %w", err)
	}

	result.assets = srv
	result.assets.LogDetails()

	// If transport implements AssetServerTransport, configure it to serve assets
	if assetTransport, ok := transport.(AssetServerTransport); ok {
		err := assetTransport.ServeAssets(srv)
		if err != nil {
			result.fatal("failed to configure transport for serving assets: %w", err)
		}
		result.debug("Transport configured to serve assets")
	}

	result.bindings = NewBindings(appOptions.MarshalError, appOptions.BindAliases)
	result.options.Services = slices.Clone(appOptions.Services)

	// Process keybindings
	if result.options.KeyBindings != nil {
		result.keyBindings = processKeyBindingOptions(result.options.KeyBindings)
	}

	if appOptions.OnShutdown != nil {
		result.OnShutdown(appOptions.OnShutdown)
	}

	// Initialize single instance manager if enabled
	if appOptions.SingleInstance != nil {
		manager, err := newSingleInstanceManager(result, appOptions.SingleInstance)
		if err != nil {
			if errors.Is(err, alreadyRunningError) && manager != nil {
				err = manager.notifyFirstInstance()
				if err != nil {
					globalApplication.error("failed to notify first instance: %w", err)
				}
				os.Exit(appOptions.SingleInstance.ExitCode)
			}
			result.fatal("failed to initialize single instance manager: %w", err)
		} else {
			result.singleInstanceManager = manager
		}
	}

	return result
}

func mergeApplicationDefaults(o *Options) {
	if o.Name == "" {
		o.Name = "My Wails Application"
	}
	if o.Description == "" {
		o.Description = "An application written using Wails"
	}
	if o.Windows.WndClass == "" {
		o.Windows.WndClass = "WailsWebviewWindow"
	}
}

type (
	platformApp interface {
		run() error
		destroy()
		setApplicationMenu(menu *Menu)
		name() string
		getCurrentWindowID() uint
		showAboutDialog(name string, description string, icon []byte)
		setIcon(icon []byte)
		on(id uint)
		dispatchOnMainThread(id uint)
		hide()
		show()
		getPrimaryScreen() (*Screen, error)
		getScreens() ([]*Screen, error)
		GetFlags(options Options) map[string]any
		isOnMainThread() bool
		isDarkMode() bool
		getAccentColor() string
	}

	runnable interface {
		Run()
	}
)

// Messages sent from javascript get routed here
type windowMessage struct {
	windowId   uint
	message    string
	originInfo *OriginInfo
}

type OriginInfo struct {
	Origin      string
	TopOrigin   string
	IsMainFrame bool
}

var windowMessageBuffer = make(chan *windowMessage, 5)

// DropTargetDetails contains information about the HTML element
// where files were dropped (the element with data-file-drop-target attribute).
type DropTargetDetails struct {
	X          int               `json:"x"`
	Y          int               `json:"y"`
	ElementID  string            `json:"id"`
	ClassList  []string          `json:"classList"`
	Attributes map[string]string `json:"attributes,omitempty"`
}

type dragAndDropMessage struct {
	windowId   uint
	filenames  []string
	X          int
	Y          int
	DropTarget *DropTargetDetails
}

var windowDragAndDropBuffer = make(chan *dragAndDropMessage, 5)

func addDragAndDropMessage(windowId uint, filenames []string, dropTarget *DropTargetDetails) {
	windowDragAndDropBuffer <- &dragAndDropMessage{
		windowId:   windowId,
		filenames:  filenames,
		DropTarget: dropTarget,
	}
}

var _ webview.Request = &webViewAssetRequest{}

const webViewRequestHeaderWindowId = "x-wails-window-id"
const webViewRequestHeaderWindowName = "x-wails-window-name"

type webViewAssetRequest struct {
	Request    webview.Request
	windowId   uint
	windowName string
}

var windowKeyEvents = make(chan *windowKeyEvent, 5)

type windowKeyEvent struct {
	windowId          uint
	acceleratorString string
}

func (r *webViewAssetRequest) URL() (string, error) {
	return r.Request.URL()
}

func (r *webViewAssetRequest) Method() (string, error) {
	return r.Request.Method()
}

func (r *webViewAssetRequest) Header() (http.Header, error) {
	h, err := r.Request.Header()
	if err != nil {
		return nil, err
	}

	hh := h.Clone()
	hh.Set(webViewRequestHeaderWindowId, strconv.FormatUint(uint64(r.windowId), 10))
	if r.windowName != "" {
		hh.Set(webViewRequestHeaderWindowName, r.windowName)
	}
	return hh, nil
}

func (r *webViewAssetRequest) Body() (io.ReadCloser, error) {
	return r.Request.Body()
}

func (r *webViewAssetRequest) Response() webview.ResponseWriter {
	return r.Request.Response()
}

func (r *webViewAssetRequest) Close() error {
	return r.Request.Close()
}

var webviewRequests = make(chan *webViewAssetRequest, 5)

type eventHook struct {
	callback func(event *ApplicationEvent)
}

type App struct {
	ctx                           context.Context
	cancel                        context.CancelFunc
	options                       Options
	applicationEventListeners     map[uint][]*EventListener
	applicationEventListenersLock sync.RWMutex
	applicationEventHooks         map[uint][]*eventHook
	applicationEventHooksLock     sync.RWMutex

	// Manager pattern for organized API
	Window      *WindowManager
	ContextMenu *ContextMenuManager
	KeyBinding  *KeyBindingManager
	Browser     *BrowserManager
	Env         *EnvironmentManager
	Dialog      *DialogManager
	Event       *EventManager
	Menu        *MenuManager
	Screen      *ScreenManager
	Clipboard   *ClipboardManager
	SystemTray  *SystemTrayManager

	// Windows
	windows     map[uint]Window
	windowsLock sync.RWMutex

	// System Trays
	systemTrays      map[uint]*SystemTray
	systemTraysLock  sync.Mutex
	systemTrayID     uint
	systemTrayIDLock sync.RWMutex

	// MenuItems
	menuItems     map[uint]*MenuItem
	menuItemsLock sync.Mutex

	// Starting and running
	starting   bool
	running    bool
	runLock    sync.Mutex
	pendingRun []runnable

	bindings *Bindings

	// platform app
	impl platformApp

	// The main application menu (private - use app.Menu.GetApplicationMenu/SetApplicationMenu)
	applicationMenu *Menu

	clipboard            *Clipboard
	customEventProcessor *EventProcessor
	Logger               *slog.Logger

	contextMenus     map[string]*ContextMenu
	contextMenusLock sync.RWMutex

	assets   *assetserver.AssetServer
	startURL string

	// Hooks
	windowCreatedCallbacks []func(window Window)
	pid                    int

	// Capabilities
	capabilities capabilities.Capabilities
	isDebugMode  bool

	// Keybindings
	keyBindings     map[string]func(window Window)
	keyBindingsLock sync.RWMutex

	// Shutdown
	performingShutdown  bool
	shutdownLock        sync.Mutex
	serviceShutdownLock sync.Mutex

	// Shutdown tasks are run when the application is shutting down.
	// They are run in the order they are added and run on the main thread.
	// The application option `OnShutdown` is run first.
	shutdownTasks []func()

	// Platform-specific fields (includes signal handler on desktop)
	platformSignalHandler

	// Wails ApplicationEvent Listener related
	wailsEventListenerLock sync.Mutex
	wailsEventListeners    []WailsEventListener

	// singleInstanceManager handles single instance functionality
	singleInstanceManager *singleInstanceManager
}

func (a *App) Config() Options {
	return a.options
}

// Context returns the application context that is canceled when the application shuts down.
// This context should be used for graceful shutdown of goroutines and long-running operations.
func (a *App) Context() context.Context {
	return a.ctx
}

func (a *App) handleWarning(msg string) {
	if a.options.WarningHandler != nil {
		a.options.WarningHandler(msg)
	} else {
		a.Logger.Warn(msg)
	}
}

func (a *App) handleError(err error) {
	if a.options.ErrorHandler != nil {
		a.options.ErrorHandler(err)
	} else {
		a.Logger.Error(err.Error())
	}
}

// RegisterService appends the given service to the list of bound services.
// Registered services will be bound and initialised
// in registration order upon calling [App.Run].
//
// RegisterService will log an error message
// and discard the given service
// if called after [App.Run].
func (a *App) RegisterService(service Service) {
	a.runLock.Lock()
	defer a.runLock.Unlock()

	if a.starting || a.running {
		a.error(
			"services must be registered before running the application. Service '%s' will not be registered.",
			getServiceName(service),
		)
		return
	}

	a.options.Services = append(a.options.Services, service)
}

func (a *App) handleFatalError(err error) {
	a.handleError(&FatalError{err: err})
	os.Exit(1)
}

func (a *App) init() {
	a.ctx, a.cancel = context.WithCancel(context.Background())
	a.applicationEventHooks = make(map[uint][]*eventHook)
	a.applicationEventListeners = make(map[uint][]*EventListener)
	a.windows = make(map[uint]Window)
	a.systemTrays = make(map[uint]*SystemTray)
	a.contextMenus = make(map[string]*ContextMenu)
	a.keyBindings = make(map[string]func(window Window))
	a.Logger = a.options.Logger
	a.pid = os.Getpid()
	a.wailsEventListeners = make([]WailsEventListener, 0)

	// Initialize managers
	a.Window = newWindowManager(a)
	a.ContextMenu = newContextMenuManager(a)
	a.KeyBinding = newKeyBindingManager(a)
	a.Browser = newBrowserManager(a)
	a.Env = newEnvironmentManager(a)
	a.Dialog = newDialogManager(a)
	a.Event = newEventManager(a)
	a.Menu = newMenuManager(a)
	a.Screen = newScreenManager(a)
	a.Clipboard = newClipboardManager(a)
	a.SystemTray = newSystemTrayManager(a)
}

func (a *App) Capabilities() capabilities.Capabilities {
	return a.capabilities
}

func (a *App) GetPID() int {
	return a.pid
}

func (a *App) info(message string, args ...any) {
	if a.Logger != nil {
		go func() {
			defer handlePanic()
			a.Logger.Info(message, args...)
		}()
	}
}

func (a *App) debug(message string, args ...any) {
	if a.Logger != nil {
		go func() {
			defer handlePanic()
			a.Logger.Debug(message, args...)
		}()
	}
}

func (a *App) fatal(message string, args ...any) {
	err := fmt.Errorf(message, args...)
	a.handleFatalError(err)
}
func (a *App) warning(message string, args ...any) {
	msg := fmt.Sprintf(message, args...)
	a.handleWarning(msg)
}

func (a *App) error(message string, args ...any) {
	a.handleError(fmt.Errorf(message, args...))
}

func (a *App) Run() error {
	a.runLock.Lock()
	// Prevent double invocations.
	if a.starting || a.running {
		a.runLock.Unlock()
		return errors.New("application is running or a previous run has failed")
	}
	// Block further service registrations.
	a.starting = true
	a.runLock.Unlock()

	// Ensure application context is cancelled in case of failures.
	defer a.cancel()

	// Call post-create hooks
	err := a.preRun()
	if err != nil {
		return err
	}

	a.impl = newPlatformApp(a)

	// Ensure services are shut down in case of failures.
	defer a.shutdownServices()

	// Ensure application context is canceled before service shutdown (duplicate calls don't hurt).
	defer a.cancel()

	// Startup services before dispatching any events.
	// No need to hold the lock here because a.options.Services may only change when a.running is false.
	services := a.options.Services
	a.options.Services = nil
	for i, service := range services {
		if err := a.startupService(service); err != nil {
			return fmt.Errorf("error starting service '%s': %w", getServiceName(service), err)
		}
		// Schedule started services for shutdown.
		a.options.Services = services[:i+1]
	}

	go func() {
		for {
			event := <-applicationEvents
			go a.Event.handleApplicationEvent(event)
		}
	}()
	go func() {
		for {
			event := <-windowEvents
			go a.handleWindowEvent(event)
		}
	}()
	go func() {
		for {
			request := <-webviewRequests
			go a.handleWebViewRequest(request)
		}
	}()
	go func() {
		for {
			event := <-windowMessageBuffer
			go a.handleWindowMessage(event)
		}
	}()
	go func() {
		for {
			event := <-windowKeyEvents
			go a.handleWindowKeyEvent(event)
		}
	}()
	go func() {
		for {
			dragAndDropMessage := <-windowDragAndDropBuffer
			go a.handleDragAndDropMessage(dragAndDropMessage)
		}
	}()

	go func() {
		for {
			menuItemID := <-menuItemClicked
			go a.Menu.handleMenuItemClicked(menuItemID)
		}
	}()

	a.runLock.Lock()
	a.running = true
	a.runLock.Unlock()

	// No need to hold the lock here because
	//   - a.pendingRun may only change while a.running is false.
	//   - runnables are scheduled asynchronously anyway.
	for _, pending := range a.pendingRun {
		go func() {
			defer handlePanic()
			pending.Run()
		}()
	}
	a.pendingRun = nil

	// set the application menu
	if runtime.GOOS == "darwin" {
		a.impl.setApplicationMenu(a.applicationMenu)
	}
	if a.options.Icon != nil {
		a.impl.setIcon(a.options.Icon)
	}

	return a.impl.run()
}

func (a *App) startupService(service Service) error {
	err := a.bindings.Add(service)
	if err != nil {
		return fmt.Errorf("cannot bind service methods: %w", err)
	}

	if service.options.Route != "" {
		handler, ok := service.Instance().(http.Handler)
		if !ok {
			handler = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
				http.Error(
					rw,
					fmt.Sprintf(
						"Service '%s' does not handle HTTP requests",
						getServiceName(service),
					),
					http.StatusServiceUnavailable,
				)
			})
		}
		a.assets.AttachServiceHandler(service.options.Route, handler)
	}

	if s, ok := service.instance.(ServiceStartup); ok {
		a.debug("Starting up service:", "name", getServiceName(service))
		return s.ServiceStartup(a.ctx, service.options)
	}

	return nil
}

func (a *App) shutdownServices() {
	// Acquire lock to prevent double calls (defer in Run() + OnShutdown)
	a.serviceShutdownLock.Lock()
	defer a.serviceShutdownLock.Unlock()

	// Ensure app context is cancelled first (duplicate calls don't hurt).
	a.cancel()

	for len(a.options.Services) > 0 {
		last := len(a.options.Services) - 1
		service := a.options.Services[last]
		a.options.Services = a.options.Services[:last] // Prevent double shutdowns

		if s, ok := service.instance.(ServiceShutdown); ok {
			a.debug("Shutting down service:", "name", getServiceName(service))
			if err := s.ServiceShutdown(); err != nil {
				a.error("error shutting down service '%s': %w", getServiceName(service), err)
			}
		}
	}
}

func (a *App) handleDragAndDropMessage(event *dragAndDropMessage) {
	defer handlePanic()
	a.windowsLock.Lock()
	window, ok := a.windows[event.windowId]
	a.windowsLock.Unlock()
	if !ok {
		a.warning("WebviewWindow #%d not found", event.windowId)
		return
	}
	window.handleDragAndDropMessage(event.filenames, event.DropTarget)
}

func (a *App) handleWindowMessage(event *windowMessage) {
	defer handlePanic()
	// Get window from window map
	a.windowsLock.RLock()
	window, ok := a.windows[event.windowId]
	// Debug: log all window IDs
	var ids []uint
	for id := range a.windows {
		ids = append(ids, id)
	}
	a.windowsLock.RUnlock()

	a.debug("handleWindowMessage: Looking for window", "windowId", event.windowId, "availableIDs", ids)

	if !ok {
		a.warning("WebviewWindow #%d not found", event.windowId)
		return
	}
	// Check if the message starts with "wails:"
	if strings.HasPrefix(event.message, "wails:") {
		a.debug("handleWindowMessage: Processing wails message", "message", event.message)
		window.HandleMessage(event.message)
	} else {
		if a.options.RawMessageHandler != nil {
			a.options.RawMessageHandler(window, event.message, event.originInfo)
		}
	}
}

func (a *App) handleWebViewRequest(request *webViewAssetRequest) {
	defer handlePanic()
	// Log that we're processing the request
	url, _ := request.Request.URL()
	a.debug("handleWebViewRequest: Processing request", "url", url)
	// IMPORTANT: pass the wrapper request so our injected headers (x-wails-window-id/name) are used
	a.assets.ServeWebViewRequest(request)
	a.debug("handleWebViewRequest: Request processing complete", "url", url)
}

func (a *App) handleWindowEvent(event *windowEvent) {
	defer handlePanic()
	// Get window from window map
	a.windowsLock.RLock()
	window, ok := a.windows[event.WindowID]
	a.windowsLock.RUnlock()
	if !ok {
		a.warning("Window #%d not found", event.WindowID)
		return
	}
	window.HandleWindowEvent(event.EventID)
}

// OnShutdown adds a function to be run when the application is shutting down.
func (a *App) OnShutdown(f func()) {
	if f == nil {
		return
	}

	a.shutdownLock.Lock()

	if !a.performingShutdown {
		defer a.shutdownLock.Unlock()
		a.shutdownTasks = append(a.shutdownTasks, f)
		return
	}

	a.shutdownLock.Unlock()
	InvokeAsync(f)
}

func (a *App) cleanup() {
	a.shutdownLock.Lock()
	if a.performingShutdown {
		a.shutdownLock.Unlock()
		return
	}
	a.cancel() // Cancel app context before running shutdown hooks.
	a.performingShutdown = true
	a.shutdownLock.Unlock()

	// No need to hold the lock here because a.shutdownTasks
	// may only change while a.performingShutdown is false.
	for _, shutdownTask := range a.shutdownTasks {
		InvokeSync(shutdownTask)
	}
	InvokeSync(func() {
		a.shutdownServices()
		a.windowsLock.Lock()
		for _, window := range a.windows {
			window.Close()
		}
		a.windows = nil
		a.windowsLock.Unlock()
		a.systemTraysLock.Lock()
		for _, systray := range a.systemTrays {
			systray.destroy()
		}
		a.systemTrays = nil
		a.systemTraysLock.Unlock()

		// Cleanup single instance manager
		if a.singleInstanceManager != nil {
			a.singleInstanceManager.cleanup()
		}

		a.postQuit()

		if a.options.PostShutdown != nil {
			a.options.PostShutdown()
		}
	})
}

func (a *App) Quit() {
	if a.impl != nil {
		InvokeSync(a.impl.destroy)
	}
}

func (a *App) SetIcon(icon []byte) {
	if a.impl != nil {
		a.impl.setIcon(icon)
	}
}

func (a *App) dispatchOnMainThread(fn func()) {
	// If we are on the main thread, just call the function
	if a.impl.isOnMainThread() {
		fn()
		return
	}

	mainThreadFunctionStoreLock.Lock()
	id := generateFunctionStoreID()
	mainThreadFunctionStore[id] = fn
	mainThreadFunctionStoreLock.Unlock()
	// Call platform specific dispatch function
	a.impl.dispatchOnMainThread(id)
}

func (a *App) Hide() {
	if a.impl != nil {
		a.impl.hide()
	}
}

func (a *App) Show() {
	if a.impl != nil {
		a.impl.show()
	}
}

func (a *App) runOrDeferToAppRun(r runnable) {
	a.runLock.Lock()

	if !a.running {
		defer a.runLock.Unlock() // Defer unlocking for panic tolerance.
		a.pendingRun = append(a.pendingRun, r)
		return
	}

	// Unlock immediately to prevent deadlocks.
	// No TOC/TOU risk here because a.running can never switch back to false.
	a.runLock.Unlock()
	r.Run()
}

func (a *App) handleWindowKeyEvent(event *windowKeyEvent) {
	defer handlePanic()
	// Get window from window map
	a.windowsLock.RLock()
	window, ok := a.windows[event.windowId]
	a.windowsLock.RUnlock()
	if !ok {
		a.warning("WebviewWindow #%d not found", event.windowId)
		return
	}
	// Get callback from window
	window.HandleKeyEvent(event.acceleratorString)
}

func (a *App) shouldQuit() bool {
	if a.options.ShouldQuit != nil {
		return a.options.ShouldQuit()
	}
	return true
}
