package cmd

import (
	"os"
	"strconv"

	"github.com/AdguardTeam/AdGuardHome/internal/aghos"
	"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
	"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
	"github.com/AdguardTeam/golibs/log"
	"github.com/AdguardTeam/golibs/osutil"
	"github.com/google/renameio/v2/maybe"
)

// signalHandler processes incoming signals and shuts services down.
type signalHandler struct {
	// confMgrConf contains the configuration parameters for the configuration
	// manager.
	confMgrConf *configmgr.Config

	// signal is the channel to which OS signals are sent.
	signal chan os.Signal

	// pidFile is the path to the file where to store the PID, if any.
	pidFile string

	// services are the services that are shut down before application exiting.
	services []agh.Service
}

// handle processes OS signals.
func (h *signalHandler) handle() {
	defer log.OnPanic("signalHandler.handle")

	h.writePID()

	for sig := range h.signal {
		log.Info("sighdlr: received signal %q", sig)

		if aghos.IsReconfigureSignal(sig) {
			h.reconfigure()
		} else if osutil.IsShutdownSignal(sig) {
			status := h.shutdown()
			h.removePID()

			log.Info("sighdlr: exiting with status %d", status)

			os.Exit(status)
		}
	}
}

// reconfigure rereads the configuration file and updates and restarts services.
func (h *signalHandler) reconfigure() {
	log.Info("sighdlr: reconfiguring adguard home")

	status := h.shutdown()
	if status != statusSuccess {
		log.Info("sighdlr: reconfiguring: exiting with status %d", status)

		os.Exit(status)
	}

	// TODO(a.garipov): This is a very rough way to do it.  Some services can be
	// reconfigured without the full shutdown, and the error handling is
	// currently not the best.

	confMgr, err := newConfigMgr(h.confMgrConf)
	check(err)

	web := confMgr.Web()
	err = web.Start()
	check(err)

	dns := confMgr.DNS()
	err = dns.Start()
	check(err)

	h.services = []agh.Service{
		dns,
		web,
	}

	log.Info("sighdlr: successfully reconfigured adguard home")
}

// Exit status constants.
const (
	statusSuccess       = 0
	statusError         = 1
	statusArgumentError = 2
)

// shutdown gracefully shuts down all services.
func (h *signalHandler) shutdown() (status int) {
	ctx, cancel := ctxWithDefaultTimeout()
	defer cancel()

	status = statusSuccess

	log.Info("sighdlr: shutting down services")
	for i, service := range h.services {
		err := service.Shutdown(ctx)
		if err != nil {
			log.Error("sighdlr: shutting down service at index %d: %s", i, err)
			status = statusError
		}
	}

	return status
}

// newSignalHandler returns a new signalHandler that shuts down svcs.
func newSignalHandler(
	confMgrConf *configmgr.Config,
	pidFile string,
	svcs ...agh.Service,
) (h *signalHandler) {
	h = &signalHandler{
		confMgrConf: confMgrConf,
		signal:      make(chan os.Signal, 1),
		pidFile:     pidFile,
		services:    svcs,
	}

	notifier := osutil.DefaultSignalNotifier{}
	osutil.NotifyShutdownSignal(notifier, h.signal)
	aghos.NotifyReconfigureSignal(h.signal)

	return h
}

// writePID writes the PID to the file, if needed.  Any errors are reported to
// log.
func (h *signalHandler) writePID() {
	if h.pidFile == "" {
		return
	}

	// Use 8, since most PIDs will fit.
	data := make([]byte, 0, 8)
	data = strconv.AppendInt(data, int64(os.Getpid()), 10)
	data = append(data, '\n')

	err := maybe.WriteFile(h.pidFile, data, 0o644)
	if err != nil {
		log.Error("sighdlr: writing pidfile: %s", err)

		return
	}

	log.Debug("sighdlr: wrote pid to %q", h.pidFile)
}

// removePID removes the PID file, if any.
func (h *signalHandler) removePID() {
	if h.pidFile == "" {
		return
	}

	err := os.Remove(h.pidFile)
	if err != nil {
		log.Error("sighdlr: removing pidfile: %s", err)

		return
	}

	log.Debug("sighdlr: removed pid at %q", h.pidFile)
}