package main

import (
	"flag"
	"fmt"
	"log"
	"log/slog"
	"os"
	"os/signal"
	"slices"
	"sync"
	"time"

	"searchix/internal/config"
	"searchix/internal/importer"
	"searchix/internal/index"
	"searchix/internal/server"

	"github.com/getsentry/sentry-go"
	"github.com/pelletier/go-toml/v2"
)

var buildVersion string

var (
	configFile         = flag.String("config", "config.toml", "config `file` to use")
	printDefaultConfig = flag.Bool(
		"print-default-config",
		false,
		"print default configuration and exit",
	)
	liveReload = flag.Bool("live", false, "whether to enable live reloading (development)")
	replace    = flag.Bool("replace", false, "replace existing index and exit")
	update     = flag.Bool("update", false, "update index and exit")
	version    = flag.Bool("version", false, "print version information")
)

func nextOccurrenceOfLocalTime(t toml.LocalTime) time.Time {
	now := time.Now()
	dayTime := t
	nextRun := time.Date(
		now.Year(),
		now.Month(),
		now.Day(),
		dayTime.Hour,
		dayTime.Minute,
		dayTime.Second,
		0,
		time.Local,
	)
	if nextRun.Before(now) {
		return nextRun.AddDate(0, 0, 1)
	}

	return nextRun
}

func main() {
	flag.Parse()
	if *version {
		fmt.Fprintf(os.Stderr, "searchix %s", buildVersion)
		if buildVersion != config.CommitSHA && buildVersion != config.ShortSHA {
			fmt.Fprintf(os.Stderr, " %s", config.CommitSHA)
		}
		_, err := fmt.Fprint(os.Stderr, "\n")
		if err != nil {
			panic("can't write to standard error?!")
		}
		os.Exit(0)
	}
	if *printDefaultConfig {
		_, err := fmt.Print(config.GetDefaultConfig())
		if err != nil {
			panic("can't write to standard output?!")
		}
		os.Exit(0)
	}

	cfg, err := config.GetConfig(*configFile)
	if err != nil {
		// only use log functions after the config file has been read successfully
		fmt.Fprintf(os.Stderr, "error parsing configuration file: %v", err)
		os.Exit(1)
	}
	slog.SetLogLoggerLevel(cfg.LogLevel)
	if cfg.Web.Environment == "production" {
		log.SetFlags(0)
	} else {
		log.SetFlags(log.LstdFlags)
	}

	err = sentry.Init(sentry.ClientOptions{
		EnableTracing:    true,
		TracesSampleRate: 1.0,
		Dsn:              cfg.Web.SentryDSN,
		Environment:      cfg.Web.Environment,
	})
	if err != nil {
		slog.Warn("could not initialise sentry", "error", err)
	}

	var i uint
	cfgEnabledSources := make([]string, len(cfg.Importer.Sources))
	for key := range cfg.Importer.Sources {
		cfgEnabledSources[i] = key
		i++
	}
	slices.Sort(cfgEnabledSources)

	read, write, exists, err := index.OpenOrCreate(cfg.DataPath, *replace)
	if err != nil {
		log.Fatalf("Failed to open or create index: %v", err)
	}

	if !exists || *replace || *update {
		slog.Info(
			"Starting build job",
			"new",
			!exists,
			"replace",
			*replace,
			"update",
			*update,
		)
		err = importer.Start(cfg, write, *replace, nil)
		if err != nil {
			log.Fatalf("Failed to build index: %v", err)
		}
		if *replace || *update {
			return
		}
	} else {
		indexedSources, err := read.GetEnabledSources()
		if err != nil {
			log.Fatalln("failed to get enabled sources from index")
		}
		slices.Sort(indexedSources)
		if !slices.Equal(cfgEnabledSources, indexedSources) {
			newSources := slices.DeleteFunc(slices.Clone(cfgEnabledSources), func(s string) bool {
				return slices.Contains(indexedSources, s)
			})
			retiredSources := slices.DeleteFunc(slices.Clone(indexedSources), func(s string) bool {
				return slices.Contains(cfgEnabledSources, s)
			})
			if len(newSources) > 0 {
				slog.Info("adding new sources", "sources", newSources)
				err := importer.Start(cfg, write, false, &newSources)
				if err != nil {
					log.Fatalf("failed to update index with new sources: %v", err)
				}
			}
			if len(retiredSources) > 0 {
				slog.Info("removing retired sources", "sources", retiredSources)
				for _, s := range retiredSources {
					err := write.DeleteBySource(s)
					if err != nil {
						log.Fatalf("failed to remove retired source %s from index: %v", s, err)
					}
				}
			}
		}
	}

	c := make(chan os.Signal, 2)
	signal.Notify(c, os.Interrupt)
	sv, err := server.New(cfg, read, *liveReload)
	if err != nil {
		log.Fatalf("error setting up server: %v", err)
	}
	wg := &sync.WaitGroup{}
	wg.Add(1)
	go func() {
		defer wg.Done()
		sig := <-c
		log.Printf("signal captured: %v", sig)
		<-sv.Stop()
		slog.Debug("server stopped")
	}()

	go func(localHub *sentry.Hub) {
		const monitorSlug = "import"
		localHub.WithScope(func(scope *sentry.Scope) {
			scope.SetContext("monitor", sentry.Context{"slug": monitorSlug})
			monitorConfig := &sentry.MonitorConfig{
				Schedule: sentry.IntervalSchedule(1, sentry.MonitorScheduleUnitDay),
				// minutes
				MaxRuntime:    10,
				CheckInMargin: 5,
				Timezone:      time.Local.String(),
			}

			nextRun := nextOccurrenceOfLocalTime(cfg.Importer.UpdateAt.LocalTime)
			for {
				slog.Debug("scheduling next run", "next-run", nextRun)
				<-time.After(time.Until(nextRun))
				wg.Add(1)
				slog.Info("updating index")

				eventID := localHub.CaptureCheckIn(&sentry.CheckIn{
					MonitorSlug: monitorSlug,
					Status:      sentry.CheckInStatusInProgress,
				}, monitorConfig)

				err = importer.Start(cfg, write, false, nil)
				wg.Done()
				if err != nil {
					slog.Warn("error updating index", "error", err)

					localHub.CaptureException(err)
					localHub.CaptureCheckIn(&sentry.CheckIn{
						ID:          *eventID,
						MonitorSlug: monitorSlug,
						Status:      sentry.CheckInStatusError,
					}, monitorConfig)
				} else {
					slog.Info("update complete")

					localHub.CaptureCheckIn(&sentry.CheckIn{
						ID:          *eventID,
						MonitorSlug: monitorSlug,
						Status:      sentry.CheckInStatusOK,
					}, monitorConfig)
				}
				nextRun = nextRun.AddDate(0, 0, 1)
			}
		})
	}(sentry.CurrentHub().Clone())

	sErr := make(chan error)
	wg.Add(1)
	go func() {
		defer wg.Done()
		sErr <- sv.Start()
	}()

	if cfg.Web.Environment == "development" {
		log.Printf("server listening on %s", cfg.Web.BaseURL.String())
	}

	err = <-sErr
	if err != nil {
		// Error starting or closing listener:
		log.Fatalf("error: %v", err)
	}
	sentry.Flush(2 * time.Second)
	wg.Wait()
}