package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"os/signal"
	"sync"
	"sync/atomic"
	"syscall"
	"time"

	_ "time/tzdata"

	activitylog "github.com/abhinavxd/libredesk/internal/activity_log"
	"github.com/abhinavxd/libredesk/internal/ai"
	auth_ "github.com/abhinavxd/libredesk/internal/auth"
	"github.com/abhinavxd/libredesk/internal/authz"
	businesshours "github.com/abhinavxd/libredesk/internal/business_hours"
	"github.com/abhinavxd/libredesk/internal/colorlog"
	"github.com/abhinavxd/libredesk/internal/csat"
	customAttribute "github.com/abhinavxd/libredesk/internal/custom_attribute"
	"github.com/abhinavxd/libredesk/internal/macro"
	notifier "github.com/abhinavxd/libredesk/internal/notification"
	"github.com/abhinavxd/libredesk/internal/report"
	"github.com/abhinavxd/libredesk/internal/search"
	"github.com/abhinavxd/libredesk/internal/sla"
	"github.com/abhinavxd/libredesk/internal/view"

	"github.com/abhinavxd/libredesk/internal/automation"
	"github.com/abhinavxd/libredesk/internal/conversation"
	"github.com/abhinavxd/libredesk/internal/conversation/priority"
	"github.com/abhinavxd/libredesk/internal/conversation/status"
	"github.com/abhinavxd/libredesk/internal/inbox"
	"github.com/abhinavxd/libredesk/internal/media"
	"github.com/abhinavxd/libredesk/internal/oidc"
	"github.com/abhinavxd/libredesk/internal/role"
	"github.com/abhinavxd/libredesk/internal/setting"
	"github.com/abhinavxd/libredesk/internal/tag"
	"github.com/abhinavxd/libredesk/internal/team"
	"github.com/abhinavxd/libredesk/internal/template"
	"github.com/abhinavxd/libredesk/internal/user"
	"github.com/abhinavxd/libredesk/internal/webhook"
	"github.com/knadh/go-i18n"
	"github.com/knadh/koanf/v2"
	"github.com/knadh/stuffbin"
	"github.com/valyala/fasthttp"
	"github.com/zerodha/fastglue"
	"github.com/zerodha/logf"
)

var (
	ko          = koanf.New(".")
	ctx         = context.Background()
	appName     = "libredesk"
	frontendDir = "frontend/dist"

	// Injected at build time.
	buildString   string
	versionString string
)

// App is the global app context which is passed and injected in the http handlers.
type App struct {
	fs              stuffbin.FileSystem
	consts          atomic.Value
	auth            *auth_.Auth
	authz           *authz.Enforcer
	i18n            *i18n.I18n
	lo              *logf.Logger
	oidc            *oidc.Manager
	media           *media.Manager
	setting         *setting.Manager
	role            *role.Manager
	user            *user.Manager
	team            *team.Manager
	status          *status.Manager
	priority        *priority.Manager
	tag             *tag.Manager
	inbox           *inbox.Manager
	tmpl            *template.Manager
	macro           *macro.Manager
	conversation    *conversation.Manager
	automation      *automation.Engine
	businessHours   *businesshours.Manager
	sla             *sla.Manager
	csat            *csat.Manager
	view            *view.Manager
	ai              *ai.Manager
	search          *search.Manager
	activityLog     *activitylog.Manager
	notifier        *notifier.Service
	customAttribute *customAttribute.Manager
	report          *report.Manager
	webhook         *webhook.Manager

	// Global state that stores data on an available app update.
	update *AppUpdate
	// Flag to indicate if app restart is required for settings to take effect.
	restartRequired bool
	sync.Mutex
}

func main() {
	// Set up signal handler.
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
	defer stop()

	// Load command line flags into Koanf.
	initFlags()

	// Version flag.
	if ko.Bool("version") {
		fmt.Println(buildString)
		os.Exit(0)
	}

	// Build string injected at build time.
	colorlog.Green("Build: %s", buildString)

	// Load the config files into Koanf.
	initConfig(ko)

	// Init stuffbin fs.
	fs := initFS()

	// Init DB.
	db := initDB()

	// Installer.
	if ko.Bool("install") {
		install(ctx, db, fs, ko.Bool("idempotent-install"), !ko.Bool("yes"))
		os.Exit(0)
	}

	// Set system user password.
	if ko.Bool("set-system-user-password") {
		setSystemUserPass(ctx, db)
		os.Exit(0)
	}

	// Check if schema is installed.
	installed, err := checkSchema(db)
	if err != nil {
		log.Fatalf("error checking db schema: %v", err)
	}
	if !installed {
		log.Println("database tables are missing. Use the `--install` flag to set up the database schema.")
		os.Exit(0)
	}

	// Upgrade.
	if ko.Bool("upgrade") {
		upgrade(db, fs, !ko.Bool("yes"))
		os.Exit(0)
	}

	// Check for pending upgrade.
	checkPendingUpgrade(db)

	// Load app settings from DB into the Koanf instance.
	settings := initSettings(db)
	loadSettings(settings)

	// Fallback for config typo. Logs a warning but continues to work with the incorrect key.
	// Uses 'message.message_outgoing_scan_interval' (correct key) as default key, falls back to the common typo.
	msgOutgoingScanIntervalKey := "message.message_outgoing_scan_interval"
	if ko.String(msgOutgoingScanIntervalKey) == "" {
		if ko.String("message.message_outoing_scan_interval") != "" {
			colorlog.Red("WARNING: typo in config key 'message.message_outoing_scan_interval' detected. Use 'message.message_outgoing_scan_interval' instead in your config.toml file. Support for this incorrect key will be removed in a future release.")
			msgOutgoingScanIntervalKey = "message.message_outoing_scan_interval"
		}
	}

	var (
		autoAssignInterval          = ko.MustDuration("autoassigner.autoassign_interval")
		unsnoozeInterval            = ko.MustDuration("conversation.unsnooze_interval")
		automationWorkers           = ko.MustInt("automation.worker_count")
		messageOutgoingQWorkers     = ko.MustDuration("message.outgoing_queue_workers")
		messageIncomingQWorkers     = ko.MustDuration("message.incoming_queue_workers")
		messageOutgoingScanInterval = ko.MustDuration(msgOutgoingScanIntervalKey)
		slaEvaluationInterval       = ko.MustDuration("sla.evaluation_interval")
		lo                          = initLogger(appName)
		rdb                         = initRedis()
		constants                   = initConstants()
		i18n                        = initI18n(fs)
		csat                        = initCSAT(db, i18n)
		oidc                        = initOIDC(db, settings, i18n)
		status                      = initStatus(db, i18n)
		priority                    = initPriority(db, i18n)
		auth                        = initAuth(oidc, rdb, i18n)
		template                    = initTemplate(db, fs, constants, i18n)
		media                       = initMedia(db, i18n)
		inbox                       = initInbox(db, i18n)
		team                        = initTeam(db, i18n)
		businessHours               = initBusinessHours(db, i18n)
		webhook                     = initWebhook(db, i18n)
		user                        = initUser(i18n, db)
		wsHub                       = initWS(user)
		notifier                    = initNotifier()
		automation                  = initAutomationEngine(db, i18n)
		sla                         = initSLA(db, team, settings, businessHours, notifier, template, user, i18n)
		conversation                = initConversations(i18n, sla, status, priority, wsHub, notifier, db, inbox, user, team, media, settings, csat, automation, template, webhook)
		autoassigner                = initAutoAssigner(team, user, conversation)
	)
	automation.SetConversationStore(conversation)

	startInboxes(ctx, inbox, conversation, user)
	go automation.Run(ctx, automationWorkers)
	go autoassigner.Run(ctx, autoAssignInterval)
	go conversation.Run(ctx, messageIncomingQWorkers, messageOutgoingQWorkers, messageOutgoingScanInterval)
	go conversation.RunUnsnoozer(ctx, unsnoozeInterval)
	go webhook.Run(ctx)
	go notifier.Run(ctx)
	go sla.Run(ctx, slaEvaluationInterval)
	go sla.SendNotifications(ctx)
	go media.DeleteUnlinkedMedia(ctx)
	go user.MonitorAgentAvailability(ctx)

	var app = &App{
		lo:              lo,
		fs:              fs,
		sla:             sla,
		oidc:            oidc,
		i18n:            i18n,
		auth:            auth,
		media:           media,
		setting:         settings,
		inbox:           inbox,
		user:            user,
		team:            team,
		status:          status,
		priority:        priority,
		tmpl:            template,
		notifier:        notifier,
		consts:          atomic.Value{},
		conversation:    conversation,
		automation:      automation,
		businessHours:   businessHours,
		activityLog:     initActivityLog(db, i18n),
		customAttribute: initCustomAttribute(db, i18n),
		authz:           initAuthz(i18n),
		view:            initView(db, i18n),
		report:          initReport(db, i18n),
		csat:            initCSAT(db, i18n),
		search:          initSearch(db, i18n),
		role:            initRole(db, i18n),
		tag:             initTag(db, i18n),
		macro:           initMacro(db, i18n),
		ai:              initAI(db, i18n),
		webhook:         webhook,
	}
	app.consts.Store(constants)

	g := fastglue.NewGlue()
	g.SetContext(app)
	initHandlers(g, wsHub)

	s := &fasthttp.Server{
		Name:                 appName,
		ReadTimeout:          ko.MustDuration("app.server.read_timeout"),
		WriteTimeout:         ko.MustDuration("app.server.write_timeout"),
		MaxRequestBodySize:   ko.MustInt("app.server.max_body_size"),
		MaxKeepaliveDuration: ko.MustDuration("app.server.keepalive_timeout"),
		ReadBufferSize:       ko.Int("app.server.read_buffer_size"),
	}

	go func() {
		colorlog.Green("Server started at %s", ko.String("app.server.address"))
		if ko.String("server.socket") != "" {
			colorlog.Green("Unix socket created at %s", ko.String("server.socket"))
		}
		if err := g.ListenAndServe(ko.String("app.server.address"), ko.String("server.socket"), s); err != nil {
			log.Fatalf("error starting server: %v", err)
		}
	}()

	// Start the app update checker.
	if ko.Bool("app.check_updates") {
		go checkUpdates(versionString, time.Hour*1, app)
	}

	// Wait for shutdown signal.
	<-ctx.Done()
	colorlog.Red("Shutting down HTTP server...")
	s.Shutdown()
	colorlog.Red("Shutting down inboxes...")
	inbox.Close()
	colorlog.Red("Shutting down automation...")
	automation.Close()
	colorlog.Red("Shutting down autoassigner...")
	autoassigner.Close()
	colorlog.Red("Shutting down notifier...")
	notifier.Close()
	colorlog.Red("Shutting down webhook...")
	webhook.Close()
	colorlog.Red("Shutting down conversation...")
	conversation.Close()
	colorlog.Red("Shutting down SLA...")
	sla.Close()
	colorlog.Red("Shutting down database...")
	db.Close()
	colorlog.Red("Shutting down redis...")
	rdb.Close()
	colorlog.Green("Shutdown complete.")
}
