package ui

import (
	"io"
	"net"
	"strings"
	"time"

	"gitea.mixdep.ru/mix/gosentry/assets"
	"gitea.mixdep.ru/mix/gosentry/src/app"

	"fyne.io/fyne/v2"
	fyneapp "fyne.io/fyne/v2/app"
	fynedesktop "fyne.io/fyne/v2/driver/desktop"
)

const appID = "ru.mixdep.gosentry.desktop"
const singleInstanceAddress = "127.0.0.1:37653"
const singleInstanceShowCommand = "show"

// Run is the application entry point. It owns the process lifecycle — single
// instance arbitration, Fyne app + window construction, tray wiring, and the
// startup-timing record — and delegates all view construction to newMainView in
// mainwindow.go. Keeping lifecycle here and the view there is the run.go /
// mainwindow.go split introduced in T4.1.
func Run(startInTray bool) {
	started := time.Now()
	instanceListener, primary := acquireSingleInstance(!startInTray)
	if !primary {
		return
	}
	if instanceListener != nil {
		defer instanceListener.Close()
	}

	// A stable app ID lets Fyne persist desktop preferences consistently across
	// launches and gives tray/window integration a predictable identity.
	a := fyneapp.NewWithID(appID)
	a.SetIcon(loadAppIcon())

	w := a.NewWindow("GoSentry " + app.Version)
	configureSystemTray(a, w)
	w.Resize(fyne.NewSize(1120, 720))
	content, recordStartup := newMainView(w)
	w.SetContent(content)
	serveSingleInstance(instanceListener, w)
	if startInTray {
		// Autostart launches intentionally stay hidden, so "window shown" would be
		// a misleading metric. Record a separate startup event for the tray path
		// instead of forcing one timing definition onto two different UX flows.
		recordStartup(time.Since(started), false)
		a.Run()
		return
	}
	// Show the window before recording startup time. Measuring earlier, during
	// widget construction, looked cheaper in History than the user-perceived
	// startup really was. The current point is less abstract: it ends when the
	// window has actually been handed to the desktop for display.
	w.Show()
	recordStartup(time.Since(started), true)
	a.Run()
}

func loadAppIcon() fyne.Resource {
	return assets.Icon()
}

func configureSystemTray(a fyne.App, w fyne.Window) {
	desk, ok := a.(fynedesktop.App)
	if !ok {
		// Not every Fyne driver exposes desktop tray features. Returning silently
		// keeps the same binary usable on platforms or sessions without a tray.
		return
	}

	// IsQuit marks this as the tray's quit item. Without it Fyne's
	// addMissingQuitForMenu appends a second, localized Quit (e.g. "Выход" on a
	// Russian system) because it only recognizes an existing quit by matching the
	// localized label — which our literal "Quit" does not. Setting IsQuit makes
	// Fyne reuse this item instead of adding a duplicate, regardless of locale.
	quit := fyne.NewMenuItem("Quit", func() {
		a.Quit()
	})
	quit.IsQuit = true
	menu := fyne.NewMenu("GoSentry",
		fyne.NewMenuItem("Show", func() {
			w.Show()
			w.RequestFocus()
		}),
		fyne.NewMenuItemSeparator(),
		quit,
	)
	desk.SetSystemTrayMenu(menu)
	w.SetCloseIntercept(func() {
		// Closing hides the window instead of quitting because scheduler tools are
		// expected to keep working in the background. The explicit Quit tray item
		// remains the way to stop the process.
		w.Hide()
	})
}

func acquireSingleInstance(showExisting bool) (net.Listener, bool) {
	listener, err := net.Listen("tcp", singleInstanceAddress)
	if err == nil {
		return listener, true
	}

	connection, dialErr := net.DialTimeout("tcp", singleInstanceAddress, time.Second)
	if dialErr == nil {
		// The first instance listens only on localhost and understands one tiny
		// command: "show". That keeps the implementation dependency-free and easy
		// to inspect, which matters more here than introducing a named-pipe or
		// platform-specific IPC abstraction just to focus an existing window.
		if showExisting {
			_, _ = io.WriteString(connection, singleInstanceShowCommand)
		}
		_ = connection.Close()
		return nil, false
	}

	// If the port is unavailable but does not answer as GoSentry, continue
	// startup instead of making the application impossible to open because of an
	// unrelated local listener. In the normal duplicate-start case the dial above
	// succeeds and this process exits after waking the first instance.
	return nil, true
}

func serveSingleInstance(listener net.Listener, w fyne.Window) {
	if listener == nil {
		return
	}
	go func() {
		for {
			connection, err := listener.Accept()
			if err != nil {
				return
			}
			command, _ := io.ReadAll(io.LimitReader(connection, 32))
			_ = connection.Close()
			if strings.TrimSpace(string(command)) != singleInstanceShowCommand {
				continue
			}
			// Accept runs on its own goroutine, so focusing the window must be
			// marshaled onto the main thread like every other widget update.
			fyne.Do(func() {
				w.Show()
				w.RequestFocus()
			})
		}
	}()
}
