package server

import (
	"context"
	"fmt"
	"os"
	"time"

	"emperror.dev/errors"
	"github.com/google/uuid"

	"github.com/Tech-Gamer/nwy-wings/config"
	"github.com/Tech-Gamer/nwy-wings/environment"
)

type PowerAction string

// The power actions that can be performed for a given server. This taps into the given server
// environment and performs them in a way that prevents a race condition from occurring. For
// example, sending two "start" actions back to back will not process the second action until
// the first action has been completed.
//
// This utilizes a workerpool with a limit of one worker so that all the actions execute
// in a sync manner.
const (
	PowerActionStart     = "start"
	PowerActionStop      = "stop"
	PowerActionRestart   = "restart"
	PowerActionTerminate = "kill"
)

// IsValid checks if the power action being received is valid.
func (pa PowerAction) IsValid() bool {
	return pa == PowerActionStart ||
		pa == PowerActionStop ||
		pa == PowerActionTerminate ||
		pa == PowerActionRestart
}

func (pa PowerAction) IsStart() bool {
	return pa == PowerActionStart || pa == PowerActionRestart
}

// ExecutingPowerAction checks if there is currently a power action being
// processed for the server.
func (s *Server) ExecutingPowerAction() bool {
	return s.powerLock.IsLocked()
}

// HandlePowerAction is a helper function that can receive a power action and then process the
// actions that need to occur for it. This guards against someone calling Start() twice at the
// same time, or trying to restart while another restart process is currently running.
//
// However, the code design for the daemon does depend on the user correctly calling this
// function rather than making direct calls to the start/stop/restart functions on the
// environment struct.
func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error {
	if s.IsInstalling() || s.IsTransferring() || s.IsRestoring() {
		if s.IsRestoring() {
			return ErrServerIsRestoring
		} else if s.IsTransferring() {
			return ErrServerIsTransferring
		}
		return ErrServerIsInstalling
	}

	lockId, _ := uuid.NewUUID()
	log := s.Log().WithField("lock_id", lockId.String()).WithField("action", action)

	cleanup := func() {
		log.Info("releasing exclusive lock for power action")
		s.powerLock.Release()
	}

	var wait int
	if len(waitSeconds) > 0 && waitSeconds[0] > 0 {
		wait = waitSeconds[0]
	}

	log.WithField("wait_seconds", wait).Debug("acquiring power action lock for instance")
	// Only attempt to acquire a lock on the process if this is not a termination event. We want to
	// just allow those events to pass right through for good reason. If a server is currently trying
	// to process a power action but has gotten stuck you still should be able to pass through the
	// terminate event. The good news here is that doing that oftentimes will get the stuck process to
	// move again, and naturally continue through the process.
	if action != PowerActionTerminate {
		// Determines if we should wait for the lock or not. If a value greater than 0 is passed
		// into this function we will wait that long for a lock to be acquired.
		if wait > 0 {
			ctx, cancel := context.WithTimeout(s.ctx, time.Second*time.Duration(wait))
			defer cancel()

			// Attempt to acquire a lock on the power action lock for up to 30 seconds. If more
			// time than that passes an error will be propagated back up the chain and this
			// request will be aborted.
			if err := s.powerLock.TryAcquire(ctx); err != nil {
				return errors.Wrap(err, fmt.Sprintf("could not acquire lock on power action after %d seconds", wait))
			}
		} else {
			// If no wait duration was provided we will attempt to immediately acquire the lock
			// and bail out with a context deadline error if it is not acquired immediately.
			if err := s.powerLock.Acquire(); err != nil {
				return errors.Wrap(err, "failed to acquire exclusive lock for power actions")
			}
		}

		log.Info("acquired exclusive lock on power actions, processing event...")
		defer cleanup()
	} else {
		// Still try to acquire the lock if terminating, and it is available, just so that
		// other power actions are blocked until it has completed. However, if it cannot be
		// acquired we won't stop the entire process.
		//
		// If we did successfully acquire the lock, make sure we release it once we're done
		// executiong the power actions.
		if err := s.powerLock.Acquire(); err == nil {
			log.Info("acquired exclusive lock on power actions, processing event...")
			defer cleanup()
		} else {
			log.Warn("failed to acquire exclusive lock, ignoring failure for termination event")
		}
	}

	switch action {
	case PowerActionStart:
		if s.Environment.State() != environment.ProcessOfflineState {
			return ErrIsRunning
		}

		// Run the pre-boot logic for the server before processing the environment start.
		if err := s.onBeforeStart(); err != nil {
			return err
		}

		return s.Environment.Start(s.Context())
	case PowerActionStop:
		fallthrough
	case PowerActionRestart:
		// We're specifically waiting for the process to be stopped here, otherwise the lock is
		// released too soon, and you can rack up all sorts of issues.
		if err := s.Environment.WaitForStop(s.Context(), time.Minute*10, true); err != nil {
			// Even timeout errors should be bubbled back up the stack. If the process didn't stop
			// nicely, but the terminate argument was passed then the server is stopped without an
			// error being returned.
			//
			// However, if terminate is not passed you'll get a context deadline error. We could
			// probably handle that nicely here, but I'd rather just pass it back up the stack for now.
			// Either way, any type of error indicates we should not attempt to start the server back
			// up.
			return err
		}

		if action == PowerActionStop {
			return nil
		}

		// Now actually try to start the process by executing the normal pre-boot logic.
		if err := s.onBeforeStart(); err != nil {
			return err
		}

		return s.Environment.Start(s.Context())
	case PowerActionTerminate:
		return s.Environment.Terminate(s.Context(), os.Kill)
	}

	return errors.New("attempting to handle unknown power action")
}

// Execute a few functions before actually calling the environment start commands. This ensures
// that everything is ready to go for environment booting, and that the server can even be started.
func (s *Server) onBeforeStart() error {
	s.Log().Info("syncing server configuration with panel")
	if err := s.Sync(); err != nil {
		return errors.WithMessage(err, "unable to sync server data from Panel instance")
	}

	// Disallow start & restart if the server is suspended. Do this check after performing a sync
	// action with the Panel to ensure that we have the most up-to-date information for that server.
	if s.IsSuspended() {
		return ErrSuspended
	}

	// Ensure we sync the server information with the environment so that any new environment variables
	// and process resource limits are correctly applied.
	s.SyncWithEnvironment()

	// If a server has unlimited disk space, we don't care enough to block the startup to check remaining.
	// However, we should trigger a size anyway, as it'd be good to kick it off for other processes.
	if s.DiskSpace() <= 0 {
		s.Filesystem().HasSpaceAvailable(true)
	} else {
		s.PublishConsoleOutputFromDaemon("Checking server disk space usage, this could take a few seconds...")
		if err := s.Filesystem().HasSpaceErr(false); err != nil {
			return err
		}
	}

	// Update the configuration files defined for the server before beginning the boot process.
	// This process executes a bunch of parallel updates, so we just block until that process
	// is complete. Any errors as a result of this will just be bubbled out in the logger,
	// we don't need to actively do anything about it at this point, worse comes to worst the
	// server starts in a weird state and the user can manually adjust.
	s.PublishConsoleOutputFromDaemon("Updating process configuration files...")
	s.Log().Debug("updating server configuration files...")
	s.UpdateConfigurationFiles()
	s.Log().Debug("updated server configuration files")

	if config.Get().System.CheckPermissionsOnBoot {
		s.PublishConsoleOutputFromDaemon("Ensuring file permissions are set correctly, this could take a few seconds...")
		// Ensure all the server file permissions are set correctly before booting the process.
		s.Log().Debug("chowning server root directory...")
		if err := s.Filesystem().Chown("/"); err != nil {
			return errors.WithMessage(err, "failed to chown root server directory during pre-boot process")
		}
	}

	s.Log().Info("completed server preflight, starting boot process...")
	return nil
}
