package docker

import (
	"context"
	"github.com/apex/log"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/client"
	"github.com/pkg/errors"
	"github.com/pterodactyl/wings/api"
	"github.com/pterodactyl/wings/environment"
	"os"
	"strings"
	"time"
)

// Run before the container starts and get the process configuration from the Panel.
// This is important since we use this to check configuration files as well as ensure
// we always have the latest version of an egg available for server processes.
//
// This process will also confirm that the server environment exists and is in a bootable
// state. This ensures that unexpected container deletion while Wings is running does
// not result in the server becoming unbootable.
func (e *Environment) OnBeforeStart() error {
	// Always destroy and re-create the server container to ensure that synced data from
	// the Panel is usee.
	if err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil {
		if !client.IsErrNotFound(err) {
			return errors.Wrap(err, "failed to remove server docker container during pre-boot")
		}
	}

	// The Create() function will check if the container exists in the first place, and if
	// so just silently return without an error. Otherwise, it will try to create the necessary
	// container and data storage directory.
	//
	// This won't actually run an installation process however, it is just here to ensure the
	// environment gets created properly if it is missing and the server is started. We're making
	// an assumption that all of the files will still exist at this point.
	if err := e.Create(); err != nil {
		return err
	}

	return nil
}

// Starts the server environment and begins piping output to the event listeners for the
// console. If a container does not exist, or needs to be rebuilt that will happen in the
// call to OnBeforeStart().
func (e *Environment) Start() error {
	sawError := false
	// If sawError is set to true there was an error somewhere in the pipeline that
	// got passed up, but we also want to ensure we set the server to be offline at
	// that point.
	defer func() {
		if sawError {
			// If we don't set it to stopping first, you'll trigger crash detection which
			// we don't want to do at this point since it'll just immediately try to do the
			// exact same action that lead to it crashing in the first place...
			e.setState(environment.ProcessStoppingState)
			e.setState(environment.ProcessOfflineState)
		}
	}()

	if c, err := e.client.ContainerInspect(context.Background(), e.Id); err != nil {
		// Do nothing if the container is not found, we just don't want to continue
		// to the next block of code here. This check was inlined here to guard against
		// a nil-pointer when checking c.State below.
		//
		// @see https://github.com/pterodactyl/panel/issues/2000
		if !client.IsErrNotFound(err) {
			return errors.WithStack(err)
		}
	} else {
		// If the server is running update our internal state and continue on with the attach.
		if c.State.Running {
			e.setState(environment.ProcessRunningState)

			return e.Attach()
		}

		// Truncate the log file so we don't end up outputting a bunch of useless log information
		// to the websocket and whatnot. Check first that the path and file exist before trying
		// to truncate them.
		if _, err := os.Stat(c.LogPath); err == nil {
			if err := os.Truncate(c.LogPath, 0); err != nil {
				return errors.WithStack(err)
			}
		}
	}

	e.setState(environment.ProcessStartingState)

	// Set this to true for now, we will set it to false once we reach the
	// end of this chain.
	sawError = true

	// Run the before start function and wait for it to finish. This will validate that the container
	// exists on the system, and rebuild the container if that is required for server booting to
	// occur.
	if err := e.OnBeforeStart(); err != nil {
		return errors.WithStack(err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
	defer cancel()

	if err := e.client.ContainerStart(ctx, e.Id, types.ContainerStartOptions{}); err != nil {
		return errors.WithStack(err)
	}

	// No errors, good to continue through.
	sawError = false

	return e.Attach()
}

// Stops the container that the server is running in. This will allow up to 30 seconds to pass
// before the container is forcefully terminated if we are trying to stop it without using a command
// sent into the instance.
//
// You most likely want to be using WaitForStop() rather than this function, since this will return
// as soon as the command is sent, rather than waiting for the process to be completed stopped.
func (e *Environment) Stop() error {
	e.mu.RLock()
	s := e.meta.Stop
	e.mu.RUnlock()

	if s.Type == "" || s.Type == api.ProcessStopSignal {
		if s.Type == "" {
			log.WithField("container_id", e.Id).Warn("no stop configuration detected for environment, using termination procedure")
		}

		return e.Terminate(os.Kill)
	}

	// If the process is already offline don't switch it back to stopping. Just leave it how
	// it is and continue through to the stop handling for the process.
	if e.State() != environment.ProcessOfflineState {
		e.setState(environment.ProcessStoppingState)
	}

	// Only attempt to send the stop command to the instance if we are actually attached to
	// the instance. If we are not for some reason, just send the container stop event.
	if e.IsAttached() && s.Type == api.ProcessStopCommand {
		return e.SendCommand(s.Value)
	}

	t := time.Second * 30
	err := e.client.ContainerStop(context.Background(), e.Id, &t)
	if err != nil {
		// If the container does not exist just mark the process as stopped and return without
		// an error.
		if client.IsErrNotFound(err) {
			e.SetStream(nil)
			e.setState(environment.ProcessOfflineState)

			return nil
		}

		return err
	}

	return nil
}

// Attempts to gracefully stop a server using the defined stop command. If the server
// does not stop after seconds have passed, an error will be returned, or the instance
// will be terminated forcefully depending on the value of the second argument.
func (e *Environment) WaitForStop(seconds uint, terminate bool) error {
	if err := e.Stop(); err != nil {
		return errors.WithStack(err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(seconds)*time.Second)
	defer cancel()

	// Block the return of this function until the container as been marked as no
	// longer running. If this wait does not end by the time seconds have passed,
	// attempt to terminate the container, or return an error.
	ok, errChan := e.client.ContainerWait(ctx, e.Id, container.WaitConditionNotRunning)
	select {
	case <-ctx.Done():
		if ctxErr := ctx.Err(); ctxErr != nil {
			if terminate {
				log.WithField("container_id", e.Id).Debug("server did not stop in time, executing process termination")

				return errors.WithStack(e.Terminate(os.Kill))
			}

			return errors.WithStack(ctxErr)
		}
	case err := <-errChan:
		if err != nil {
			if terminate {
				log.WithField("container_id", e.Id).WithField("error", errors.WithStack(err)).Warn("error while waiting for container stop, attempting process termination")

				return errors.WithStack(e.Terminate(os.Kill))
			}

			return errors.WithStack(err)
		}
	case <-ok:
	}

	return nil
}

// Forcefully terminates the container using the signal passed through.
func (e *Environment) Terminate(signal os.Signal) error {
	c, err := e.client.ContainerInspect(context.Background(), e.Id)
	if err != nil {
		return errors.WithStack(err)
	}

	if !c.State.Running {
		// If the container is not running but we're not already in a stopped state go ahead
		// and update things to indicate we should be completely stopped now. Set to stopping
		// first so crash detection is not triggered.
		if e.State() != environment.ProcessOfflineState {
			e.setState(environment.ProcessStoppingState)
			e.setState(environment.ProcessOfflineState)
		}

		return nil
	}

	// We set it to stopping than offline to prevent crash detection from being triggered.
	e.setState(environment.ProcessStoppingState)

	sig := strings.TrimSuffix(strings.TrimPrefix(signal.String(), "signal "), "ed")

	if err := e.client.ContainerKill(context.Background(), e.Id, sig); err != nil {
		return err
	}

	e.setState(environment.ProcessOfflineState)

	return nil
}