package api

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"strconv"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/api/types/network"
	"nhooyr.io/websocket"
	"nhooyr.io/websocket/wsjson"

	"codefirst.iut.uca.fr/git/thomas.bellembois/codefirst-dockerrunner-common/v2/callbacks"
	"codefirst.iut.uca.fr/git/thomas.bellembois/codefirst-dockerrunner-common/v2/errors"
	"codefirst.iut.uca.fr/git/thomas.bellembois/codefirst-dockerrunner-common/v2/messages"
	"codefirst.iut.uca.fr/git/thomas.bellembois/codefirst-dockerrunner-common/v2/models"
	"codefirst.iut.uca.fr/git/thomas.bellembois/codefirst-dockerrunner/v2/globals"
)

func getContainers(filter map[string]string) (containers []types.Container, err error) {
	dockerCtx := context.Background()

	f := filters.NewArgs()

	for k, v := range filter {
		f.Add(k, v)
	}

	if containers, err = globals.DockerClient.ContainerList(dockerCtx, types.ContainerListOptions{All: true, Filters: f}); err != nil {
		return
	}

	return
}

func GetContainerLog(wsCtx context.Context, wsConnection *websocket.Conn, codeFirstContainer models.CodeFirstContainer) {
	dockerCtx := context.Background()

	options := types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true}

	var (
		err error
		out io.ReadCloser
	)

	wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.BeforeGetContainerLogCallback})

	if out, err = globals.DockerClient.ContainerLogs(dockerCtx, codeFirstContainer.ID, options); err != nil {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.GetContainerLog, SourceErrorMessage: err.Error()})
		return
	}

	wsmessageResult := messages.WSMessage{Action: callbacks.AfterGetContainerLogCallback, Container: codeFirstContainer}

	var (
		s string
		b bytes.Buffer
	)

	if _, err = io.Copy(bufio.NewWriter(&b), out); err != nil {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.GetContainerLog, SourceErrorMessage: err.Error()})
		return
	}

	// FIXME: improve me
	// 32768 is the default max size for wsjson.Read
	// Remove some byte for the other messages.WSMessage fields.
	resultSize := 32768 / 2

	if len(s) > resultSize {
		wsmessageResult.Message = b.String()[len(s)-(32768/2):]
	} else {
		wsmessageResult.Message = b.String()
	}

	wsjson.Write(wsCtx, wsConnection, wsmessageResult)
}

func GetContainers(wsCtx context.Context, wsConnection *websocket.Conn, codeFirstContainer models.CodeFirstContainer, user string) {
	var (
		err                 error
		containers          []types.Container
		codefirstContainers []models.CodeFirstContainer
	)

	wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.BeforeGetContainersCallback})

	if containers, err = getContainers(map[string]string{"label": "codefirst-user=" + user}); err != nil {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.GetContainers, SourceErrorMessage: err.Error()})
		return
	}

	for i := range containers {
		c := containers[i]

		var private bool
		if c.Labels["traefik.enable"] == "false" {
			private = true
		}

		codefirstContainers = append(codefirstContainers, models.CodeFirstContainer{
			ID:          c.ID,
			Name:        c.Names[0],
			ImageURL:    c.Image,
			EndpointURL: fmt.Sprintf("%s://%s/%s%s", globals.Scheme, globals.HostName, globals.DockerPathPrefix, c.Names[0]),
			Private:     private,
		})
	}

	wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.AfterGetContainersCallback, Containers: codefirstContainers})
}

// TODO: check owner
func RemoveContainer(wsCtx context.Context, wsConnection *websocket.Conn, codeFirstContainer models.CodeFirstContainer, user string) {
	dockerCtx := context.Background()

	wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.BeforeStopContainerCallback, Container: codeFirstContainer})

	if err := globals.DockerClient.ContainerStop(dockerCtx, codeFirstContainer.ID, nil); err != nil {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.StopContainer, SourceErrorMessage: err.Error()})
		return
	}

	wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.AfterStopContainerCallback, Container: codeFirstContainer})
	wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.BeforeRemoveContainerCallback, Container: codeFirstContainer})

	if err := globals.DockerClient.ContainerRemove(dockerCtx, codeFirstContainer.ID, types.ContainerRemoveOptions{
		RemoveVolumes: true,
		Force:         true,
	}); err != nil {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.RemoveContainer, SourceErrorMessage: err.Error()})
		return
	}

	wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.AfterRemoveContainerCallback, Container: codeFirstContainer})
}

// TODO: check owner
func ExecContainer(wsCtx context.Context, wsConnection *websocket.Conn, codeFirstContainer models.CodeFirstContainer, exec types.ExecConfig, user string) {
	dockerCtx := context.Background()

	wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.BeforeExecContainerCallback, Container: codeFirstContainer})

	var (
		containers []types.Container
		err        error
	)

	if containers, err = getContainers(map[string]string{"label": "codefirst-name=" + codeFirstContainer.Name}); err != nil {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.ExecContainer, SourceErrorMessage: err.Error()})
		return
	}

	if len(containers) != 1 {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.NonExistingContainer})
		return
	}

	codeFirstContainer.ID = containers[0].ID
	exec.AttachStderr = true
	exec.AttachStdout = true

	var (
		responseID types.IDResponse
		response   types.HijackedResponse
	)

	for i := range exec.Cmd {
		fmt.Printf("cmd %d: %s\n", i, exec.Cmd[i])
	}

	if responseID, err = globals.DockerClient.ContainerExecCreate(dockerCtx, codeFirstContainer.ID, exec); err != nil {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.ExecContainer, SourceErrorMessage: err.Error()})
		return
	}

	if response, err = globals.DockerClient.ContainerExecAttach(context.Background(), responseID.ID, types.ExecStartCheck{}); err != nil {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.ExecContainer, SourceErrorMessage: err.Error()})
		return
	}
	defer response.Close()

	var data []byte

	if data, err = ioutil.ReadAll(response.Reader); err != nil {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.ExecContainer, SourceErrorMessage: err.Error()})
		return
	}

	wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.AfterExecContainerCallback, Container: codeFirstContainer, Message: string(data)})
}

func StartContainer(wsCtx context.Context, wsConnection *websocket.Conn, codeFirstContainer models.CodeFirstContainer, user string) {
	dockerCtx := context.Background()

	var (
		err        error
		containers []types.Container
	)

	// Get number of containers for user.
	if containers, err = getContainers(map[string]string{"label": "codefirst-user=" + user}); err != nil {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.GetContainers, SourceErrorMessage: err.Error()})
		return
	}

	if len(containers) >= globals.MaxAllowedContainers {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.MaxContainersReached})
		return
	}

	// Validate container.
	if codeFirstContainer.ImageURL == "" {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.ImageURLValidation})
		return
	}

	if codeFirstContainer.Name == "" {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.ImageNameValidation})
		return
	}

	// Rename container - prepend the logged user.
	codeFirstContainer.Name = fmt.Sprintf("%s-%s", user, codeFirstContainer.Name)

	// Check if container with same name already exist.
	var (
		conts []types.Container
	)

	wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.BeforeGetContainerWithSameNameCallback, Container: codeFirstContainer})

	if conts, err = getContainers(map[string]string{"label": "codefirst-name=" + codeFirstContainer.Name}); err != nil {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.GetContainers, SourceErrorMessage: err.Error()})
		return
	}

	wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.AfterGetContainerWithSameNameCallback, Container: codeFirstContainer})

	// Removing it.
	if len(conts) > 0 && conts[0].ID != "" {
		if codeFirstContainer.Overwrite {
			wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.Log, Message: (fmt.Sprintf("Removing existing container: %s", conts[0].ID))})
			RemoveContainer(wsCtx, wsConnection, models.CodeFirstContainer{ID: conts[0].ID}, user)
		} else {
			wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.AfterStartContainerCallback, Message: fmt.Sprintf("Can not overwrite existing container: %s", conts[0].ID)})
			return
		}
	}

	// Pull the Docker image.
	var (
		reader io.ReadCloser
	)

	imagePullOptions := types.ImagePullOptions{
		RegistryAuth: codeFirstContainer.Base64Credentials,
	}

	wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.BeforePullImageCallback, Container: codeFirstContainer})

	if reader, err = globals.DockerClient.ImagePull(dockerCtx, codeFirstContainer.ImageURL, imagePullOptions); err != nil {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.DockerPull, SourceErrorMessage: err.Error()})
		return
	}

	if _, err = io.Copy(os.Stdout, reader); err != nil {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.Server, SourceErrorMessage: err.Error()})
		return
	}

	wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.AfterPullImageCallback, Container: codeFirstContainer})
	wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.BeforeCreateContainerCallback, Container: codeFirstContainer})

	var traefikEnable bool
	if !codeFirstContainer.Private {
		traefikEnable = true
	}

	// Configure the container.
	containerLabels := map[string]string{
		"codefirst-user":          user,
		"codefirst-name":          codeFirstContainer.Name,
		"codefirst-usercontainer": "true",
		"traefik.enable":          strconv.FormatBool(traefikEnable),
		fmt.Sprintf("traefik.http.routers.%s.rule", codeFirstContainer.Name):                           fmt.Sprintf("Host(`%s`) && PathPrefix(`/%s/%s`)", globals.HostName, globals.DockerPathPrefix, codeFirstContainer.Name),
		fmt.Sprintf("traefik.http.routers.%s.entrypoints", codeFirstContainer.Name):                    "websecure",
		fmt.Sprintf("traefik.http.routers.%s.tls.certresolver", codeFirstContainer.Name):               "letsEncrypt",
		fmt.Sprintf("traefik.http.middlewares.strip-%s.stripprefix.prefixes", codeFirstContainer.Name): fmt.Sprintf("/%s/%s", globals.DockerPathPrefix, codeFirstContainer.Name),
		fmt.Sprintf("traefik.http.routers.%s.middlewares", codeFirstContainer.Name):                    fmt.Sprintf("strip-%s@docker", codeFirstContainer.Name),
		"traefik.docker.network": globals.DockerNetworkName,
	}
	containerConfig := &container.Config{
		Image:  codeFirstContainer.ImageURL,
		Labels: containerLabels,
		Env:    codeFirstContainer.Env,
	}
	hostConfig := &container.HostConfig{
		NetworkMode: container.NetworkMode(globals.DockerNetworkName),
	}
	networkingConfig := &network.NetworkingConfig{}

	// Create the container.
	var containerCreateResponse container.ContainerCreateCreatedBody

	if containerCreateResponse, err = globals.DockerClient.ContainerCreate(
		dockerCtx,
		containerConfig,
		hostConfig,
		networkingConfig,
		nil,
		codeFirstContainer.Name); err != nil {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.CreateContainer, SourceErrorMessage: err.Error()})
		return
	}

	codeFirstContainer.ID = containerCreateResponse.ID

	wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.AfterCreateContainerCallback, Container: codeFirstContainer})
	wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.BeforeStartContainerCallback, Container: codeFirstContainer})

	// Start the container.
	if err := globals.DockerClient.ContainerStart(dockerCtx, containerCreateResponse.ID, types.ContainerStartOptions{}); err != nil {
		sendError(wsCtx, wsConnection, errors.AppError{Type: errors.StartContainer, SourceErrorMessage: err.Error()})
		return
	}

	wsjson.Write(wsCtx, wsConnection, messages.WSMessage{Action: callbacks.AfterStartContainerCallback, Message: containerCreateResponse.ID})
}
