package main

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"mime/multipart"
	"net/http"
	"os"
	"runtime/debug"
	"strconv"

	"git.sr.ht/~ashkeel/strimertul/log"

	"git.sr.ht/~ashkeel/containers/sync"
	kv "git.sr.ht/~ashkeel/kilovolt/v12"
	"github.com/nicklaw5/helix/v2"
	"github.com/urfave/cli/v2"
	"github.com/wailsapp/wails/v2/pkg/options"
	"github.com/wailsapp/wails/v2/pkg/runtime"

	"git.sr.ht/~ashkeel/strimertul/database"
	"git.sr.ht/~ashkeel/strimertul/docs"
	"git.sr.ht/~ashkeel/strimertul/loyalty"
	"git.sr.ht/~ashkeel/strimertul/migrations"
	"git.sr.ht/~ashkeel/strimertul/twitch"
	"git.sr.ht/~ashkeel/strimertul/twitch/client"
	"git.sr.ht/~ashkeel/strimertul/webserver"
)

// App struct
type App struct {
	ctx           context.Context
	cliParams     *cli.Context
	driver        database.Driver
	ready         *sync.RWSync[bool]
	isFatalError  *sync.RWSync[bool]
	backupOptions database.BackupOptions
	cancelLogs    database.CancelFunc
	logger        *slog.Logger

	db             *database.LocalDBClient
	twitchManager  *client.Manager
	httpServer     *webserver.WebServer
	loyaltyManager *loyalty.Manager
}

// NewApp creates a new App application struct
func NewApp(cliParams *cli.Context) *App {
	return &App{
		cliParams:    cliParams,
		ready:        sync.NewRWSync(false),
		isFatalError: sync.NewRWSync(false),
		logger:       slog.Default(),
	}
}

// startup is called when the app starts
func (a *App) startup(ctx context.Context) {
	defer func() {
		if r := recover(); r != nil {
			a.stop(ctx)
			switch v := r.(type) {
			case error:
				a.showFatalError(v, v.Error())
			default:
				a.showFatalError(errors.New(fmt.Sprint(v)), "Runtime error encountered")
			}
		}
	}()

	slog.Info("Started", slog.String("version", appVersion))

	a.ctx = ctx

	a.backupOptions = database.BackupOptions{
		BackupDir:      a.cliParams.String("backup-dir"),
		BackupInterval: a.cliParams.Int("backup-interval"),
		MaxBackups:     a.cliParams.Int("max-backups"),
	}

	// Initialize database
	if err := a.initializeDatabase(); err != nil {
		a.showFatalError(err, "Failed to initialize database")
		return
	}

	// Check for migrations
	if err := migrations.Run(a.driver, a.db, slog.With(slog.String("operation", "migration"))); err != nil {
		a.showFatalError(err, "Failed to migrate database to latest version")
		return
	}

	// Initialize components
	if err := a.initializeComponents(); err != nil {
		a.showFatalError(err, "Failed to initialize required component")
		return
	}

	// Set meta keys
	_ = a.db.PutKey(docs.VersionKey, appVersion)

	a.ready.Set(true)
	runtime.EventsEmit(ctx, "ready", true)
	slog.Info("Strimertul is ready")

	// Add logs I/O to UI
	a.cancelLogs, _ = a.listenForLogs()
	go a.forwardLogs()

	// Run HTTP server
	if err := a.httpServer.Listen(); err != nil {
		a.showFatalError(err, "HTTP server stopped")
		return
	}
}

func (a *App) initializeDatabase() error {
	var err error

	// Make KV hub
	a.driver, err = database.GetDatabaseDriver(a.cliParams)
	if err != nil {
		return fmt.Errorf("could not get database driver: %w", err)
	}

	// Start database backup task
	if a.backupOptions.BackupInterval > 0 {
		go BackupTask(a.driver, a.backupOptions)
	}

	hub := a.driver.Hub()
	go hub.Run()
	hub.UseInteractiveAuth(a.interactiveAuth)

	a.db, err = database.NewLocalClient(hub)
	if err != nil {
		return fmt.Errorf("could not initialize database client: %w", err)
	}

	return nil
}

func (a *App) initializeComponents() error {
	var err error

	// Create logger and endpoints
	a.httpServer, err = webserver.NewServer(
		log.WithLogger(a.ctx, a.logger.With(slog.String("strimertul.module", "webserver"))),
		a.db,
		webserver.DefaultServerFactory,
	)
	if err != nil {
		return fmt.Errorf("could not initialize http server: %w", err)
	}

	// Create twitch client
	a.twitchManager, err = client.NewManager(
		log.WithLogger(a.ctx, a.logger.With(slog.String("strimertul.module", "twitch"))),
		a.db,
		a.httpServer,
	)
	if err != nil {
		return fmt.Errorf("could not initialize twitch client: %w", err)
	}

	// Initialize loyalty system
	a.loyaltyManager, err = loyalty.NewManager(
		log.WithLogger(a.ctx, a.logger.With(slog.String("strimertul.module", "logger"))),
		a.db,
		a.twitchManager,
	)
	if err != nil {
		return fmt.Errorf("could not initialize loyalty manager: %w", err)
	}

	return nil
}

func (a *App) listenForLogs() (database.CancelFunc, error) {
	return a.db.SubscribeKey(docs.LogRPCKey, func(newValue string) {
		var entry docs.ExternalLog
		if err := json.Unmarshal([]byte(newValue), &entry); err != nil {
			return
		}

		var level slog.Level
		if err := level.UnmarshalText([]byte(entry.Level)); err != nil {
			level = slog.LevelInfo
		}

		slog.Log(a.ctx, level, entry.Message, log.ParseLogFields(entry.Data)...)
	})
}

func (a *App) forwardLogs() {
	for entry := range log.IncomingLogs {
		runtime.EventsEmit(a.ctx, "log-event", entry)
	}
}

func (a *App) stop(_ context.Context) {
	if a.cancelLogs != nil {
		a.cancelLogs()
	}
	if a.loyaltyManager != nil {
		warnOnError(a.loyaltyManager.Close(), "Could not cleanly close loyalty manager")
	}
	if a.httpServer != nil {
		warnOnError(a.httpServer.Close(), "Could not cleanly close HTTP server")
	}
	warnOnError(a.db.Close(), "Could not cleanly close database")

	warnOnError(a.driver.Close(), "Could not close driver")
}

func (a *App) AuthenticateKVClient(id string) {
	idInt, err := strconv.ParseInt(id, 10, 64)
	if err != nil {
		return
	}
	warnOnError(a.driver.Hub().SetAuthenticated(idInt, true), "Could not mark session as authenticated", slog.String("session-id", id))
}

func (a *App) IsServerReady() bool {
	return a.ready.Get()
}

func (a *App) IsFatalError() bool {
	return a.isFatalError.Get()
}

func (a *App) GetKilovoltBind() string {
	if a.httpServer == nil {
		return ""
	}
	return a.httpServer.Config.Get().Bind
}

func (a *App) GetTwitchAuthURL(state string) string {
	return twitch.GetAuthorizationURL(a.twitchManager.Client().API, state)
}

func (a *App) GetTwitchLoggedUser(key string) (helix.User, error) {
	userClient, err := twitch.GetUserClient(a.db, key, false)
	if err != nil {
		return helix.User{}, err
	}

	users, err := userClient.GetUsers(&helix.UsersParams{})
	if err != nil {
		return helix.User{}, err
	}

	if len(users.Data.Users) < 1 {
		return helix.User{}, errors.New("no users found")
	}

	return users.Data.Users[0], nil
}

func (a *App) GetLastLogs() []log.Entry {
	return log.LastLogs.Get()
}

func (a *App) GetDocumentation() map[string]docs.KeyObject {
	return docs.Keys
}

func (a *App) SendCrashReport(errorData string, info string) (string, error) {
	var b bytes.Buffer
	w := multipart.NewWriter(&b)

	// Add text fields
	if err := w.WriteField("error", errorData); err != nil {
		slog.Error("Could not encode field error for crash report", log.Error(err))
	}
	if len(info) > 0 {
		if err := w.WriteField("info", info); err != nil {
			slog.Error("Could not encode field info for crash report", log.Error(err))
		}
	}

	// Add log files
	addFile(w, "log", log.Filename)
	addFile(w, "paniclog", log.PanicFilename)

	if err := w.Close(); err != nil {
		slog.Error("Could not prepare request for crash report", log.Error(err))
		return "", err
	}

	resp, err := http.Post(crashReportURL, w.FormDataContentType(), &b)
	if err != nil {
		slog.Error("Could not send crash report", log.Error(err))
		return "", err
	}
	defer resp.Body.Close()

	// Check the response
	if resp.StatusCode != http.StatusOK {
		byt, _ := io.ReadAll(resp.Body)
		slog.Error("Crash report server returned error", slog.String("status", resp.Status), slog.String("response", string(byt)))
		return "", fmt.Errorf("crash report server returned error: %s - %s", resp.Status, string(byt))
	}

	byt, err := io.ReadAll(resp.Body)
	return string(byt), err
}

type VersionInfo struct {
	Release   string           `json:"release"`
	BuildInfo *debug.BuildInfo `json:"build"`
}

func (a *App) GetAppVersion() VersionInfo {
	info, _ := debug.ReadBuildInfo()
	return VersionInfo{
		Release:   appVersion,
		BuildInfo: info,
	}
}

func (a *App) TestTemplate(message string, data any) error {
	tpl, err := a.twitchManager.Client().GetTemplateEngine().MakeTemplate(message)
	if err != nil {
		return err
	}
	return tpl.Execute(io.Discard, data)
}

func (a *App) TestCommandTemplate(message string) error {
	return a.TestTemplate(message, twitch.TestMessageData)
}

func (a *App) interactiveAuth(client kv.Client, message map[string]any) bool {
	callbackID := fmt.Sprintf("auth-callback-%d", client.UID())
	authResult := make(chan bool)
	runtime.EventsOnce(a.ctx, callbackID, func(optional ...any) {
		if len(optional) == 0 {
			authResult <- false
			return
		}
		val, _ := optional[0].(bool)
		authResult <- val
	})
	runtime.EventsEmit(a.ctx, "interactiveAuth", client.UID(), message, callbackID)

	return <-authResult
}

func (a *App) showFatalError(err error, text string, fields ...any) {
	if err != nil {
		fields = append(fields, log.ErrorSkip(err, 2), slog.String("Z", string(debug.Stack())))
		slog.Error(text, fields...)
		runtime.EventsEmit(a.ctx, "fatalError")
		a.isFatalError.Set(true)
	}
}

func (a *App) onSecondInstanceLaunch(_ options.SecondInstanceData) {
	_, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
		Type:    runtime.ErrorDialog,
		Title:   "strimertul is already running",
		Message: "Only one copy of strimertul can run at the same time, make sure to close other instances first",
	})
}

func addFile(m *multipart.Writer, field string, filename string) {
	logfile, err := m.CreateFormFile(field, filename)
	if err != nil {
		slog.Error("Could not encode field log for crash report", log.Error(err))
		return
	}

	file, err := os.Open(filename)
	if err != nil {
		slog.Error("Could not open file for including in crash report", slog.String("file", filename), log.Error(err))
		return
	}

	if _, err = io.Copy(logfile, file); err != nil {
		slog.Error("Could not read from file for including in crash report", slog.String("file", filename), log.Error(err))
	}
}
