package loader

import (
	"context"
	"errors"
	"fmt"
	"sync"

	"galaxy/client/updater"
	"galaxy/connector"
	"galaxy/storage"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/theme"
	"fyne.io/fyne/v2/widget"
)

const (
	loaderLogViewportColumns = 80
	loaderLogViewportRows    = 12
)

type loader struct {
	app         fyne.App
	storage     storage.Storage
	connector   connector.Connector
	updater     *updater.Manager
	runner      uiRunner
	debugWindow fyne.Window
	textGrid    *widget.TextGrid
	btn         *widget.Button

	ctx context.Context

	resultMu sync.Mutex
	result   error

	closeMu    sync.Mutex
	closeQuits bool
}

// loaderLogViewportMinSize derives a stable monospace TextGrid viewport size
// from the active Fyne text metrics.
func loaderLogViewportMinSize(app fyne.App) fyne.Size {
	if app == nil || app.Driver() == nil {
		return fyne.NewSize(0, 0)
	}

	cellSize, _ := app.Driver().RenderedTextSize(
		"M",
		theme.TextSize(),
		fyne.TextStyle{Monospace: true},
		nil,
	)

	return fyne.NewSize(
		cellSize.Width*loaderLogViewportColumns,
		cellSize.Height*loaderLogViewportRows,
	)
}

func NewLoader(s storage.Storage, conn connector.Connector, app fyne.App) (*loader, error) {
	l := &loader{
		app:         app,
		connector:   conn,
		storage:     s,
		updater:     updater.NewManager(s, conn),
		runner:      execRunner{},
		textGrid:    widget.NewTextGrid(),
		debugWindow: app.NewWindow("Loader"),
	}
	l.btn = widget.NewButton("Retry", l.onButtonAction)
	l.btn.Disable()
	l.textGrid.Scroll = fyne.ScrollNone
	l.debugWindow.SetCloseIntercept(l.onWindowClose)

	logScroll := container.NewScroll(l.textGrid)
	logScroll.Direction = container.ScrollBoth
	logScroll.SetMinSize(loaderLogViewportMinSize(app))

	actionBar := container.NewCenter(container.NewHBox(l.btn))

	content := container.NewBorder(nil, actionBar, nil, nil, logScroll)
	l.debugWindow.SetContent(content)
	l.debugWindow.Resize(content.MinSize())
	l.debugWindow.SetFixedSize(true)
	l.debugWindow.CenterOnScreen()

	return l, nil
}

func (l *loader) runOnce(ctx context.Context) error {
	target, err := l.updater.EnsureLaunchTarget()
	if err != nil {
		return err
	}

	l.logText(fmt.Sprintf("Starting UI client v%s", target.Version))
	l.logText(fmt.Sprintf("Executable: %s", target.Path))

	exitCode, runErr := l.runner.Run(ctx, target.Path)
	markErr := l.updater.MarkLaunchResult(target.Version, exitCode, runErr)

	switch {
	case runErr != nil:
		return errors.Join(fmt.Errorf("launch UI client v%s: %w", target.Version, runErr), markErr)
	case exitCode != 0:
		return errors.Join(fmt.Errorf("UI client v%s exited with code %d", target.Version, exitCode), markErr)
	default:
		return markErr
	}
}

// init prepares and launches the standalone UI client, or shows a retry button on failure.
func (l *loader) init(ctx context.Context) {
	l.setCloseQuits(false)
	fyne.Do(func() {
		l.textGrid.SetText("")
		l.btn.Hide()
		l.btn.Disable()
		// show debugWindow can be done with future debug mode, e.g. with -debug flag
		l.debugWindow.Hide()
	})

	err := l.runOnce(ctx)
	if err == nil || errors.Is(err, context.Canceled) {
		l.setResult(nil)
		fyne.Do(func() {
			l.debugWindow.Hide()
			l.app.Quit()
		})
		return
	}

	l.setCloseQuits(true)
	l.setResult(err)
	l.logError(err)
	fyne.Do(func() {
		l.btn.SetText("Retry")
		l.btn.Enable()
		l.btn.Show()
		l.debugWindow.Show()
	})
}

func (l *loader) onButtonAction() {
	if l.ctx == nil {
		return
	}
	go l.init(l.ctx)
}

func (l *loader) onWindowClose() {
	if l.getCloseQuits() {
		l.app.Quit()
		return
	}

	l.debugWindow.Hide()
}

func (l *loader) logText(v string) {
	if l.textGrid == nil {
		return
	}
	fyne.Do(func() { l.textGrid.Append(v) })
}

func (l *loader) logError(err error) {
	l.logText(fmt.Sprintf("ERROR: %s", err))
}

func (l *loader) setResult(err error) {
	l.resultMu.Lock()
	defer l.resultMu.Unlock()
	l.result = err
}

func (l *loader) getResult() error {
	l.resultMu.Lock()
	defer l.resultMu.Unlock()
	return l.result
}

func (l *loader) setCloseQuits(v bool) {
	l.closeMu.Lock()
	defer l.closeMu.Unlock()
	l.closeQuits = v
}

func (l *loader) getCloseQuits() bool {
	l.closeMu.Lock()
	defer l.closeMu.Unlock()
	return l.closeQuits
}

// Run starts the loader window, launches the standalone UI process, and returns
// the final launch result once the loader application exits.
func (l *loader) Run(ctx context.Context) error {
	l.ctx = ctx

	go l.init(ctx)
	go func() {
		<-ctx.Done()
		fyne.Do(l.app.Quit)
	}()

	l.app.Run()
	if errors.Is(ctx.Err(), context.Canceled) {
		return nil
	}
	return l.getResult()
}
