package docker

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"strings"
	"time"

	"github.com/0xJacky/Nginx-UI/internal/version"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/image"
	"github.com/docker/docker/client"
	"github.com/pkg/errors"
	"github.com/uozi-tech/cosy"
	"github.com/uozi-tech/cosy/logger"
)

const (
	ImageName  = "uozi/nginx-ui"
	TempPrefix = "nginx-ui-temp-"
	OldSuffix  = "_old"
)

// getTimestampedTempName returns a temporary container name with timestamp
func getTimestampedTempName() string {
	return fmt.Sprintf("%s%d", TempPrefix, time.Now().Unix())
}

// removeAllTempContainers removes all containers with the TempPrefix
func removeAllTempContainers(ctx context.Context, cli *client.Client) (err error) {
	containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
	if err != nil {
		return
	}

	for _, c := range containers {
		for _, name := range c.Names {
			processedName := strings.TrimPrefix(name, "/")
			if strings.HasPrefix(processedName, TempPrefix) {
				err = cli.ContainerRemove(ctx, c.ID, container.RemoveOptions{Force: true})
				if err != nil {
					logger.Error("Failed to remove temp container:", err)
				} else {
					logger.Info("Successfully removed temp container:", processedName)
				}
				break
			}
		}
	}

	return nil
}

// UpgradeStepOne Trigger in the OTA upgrade
func UpgradeStepOne(channel string, progressChan chan<- float64) (err error) {
	ctx := context.Background()

	// 1. Get the tag of the latest release
	release, err := version.GetRelease(channel)
	if err != nil {
		return err
	}
	tag := release.TagName

	// 2. Pull the image
	cli, err := initClient()
	if err != nil {
		return cosy.WrapErrorWithParams(ErrClientNotInitialized, err.Error())
	}
	defer cli.Close()

	// Pull the image with the specified tag
	out, err := cli.ImagePull(ctx, fmt.Sprintf("%s:%s", ImageName, tag), image.PullOptions{})
	if err != nil {
		return cosy.WrapErrorWithParams(ErrFailedToPullImage, err.Error())
	}
	defer out.Close()

	// Parse JSON stream and send progress updates through channel
	decoder := json.NewDecoder(out)
	type ProgressDetail struct {
		Current int64 `json:"current"`
		Total   int64 `json:"total"`
	}
	type PullStatus struct {
		Status         string         `json:"status"`
		ProgressDetail ProgressDetail `json:"progressDetail"`
		ID             string         `json:"id"`
	}

	layers := make(map[string]float64)
	var status PullStatus
	var lastProgress float64

	for {
		if err := decoder.Decode(&status); err != nil {
			if err == io.EOF {
				break
			}
			logger.Error("Error decoding Docker pull status:", err)
			continue
		}

		// Only process layers with progress information
		if status.ProgressDetail.Total > 0 {
			progress := float64(status.ProgressDetail.Current) / float64(status.ProgressDetail.Total) * 100
			layers[status.ID] = progress

			// Calculate overall progress (average of all layers)
			var totalProgress float64
			for _, p := range layers {
				totalProgress += p
			}
			overallProgress := totalProgress / float64(len(layers))

			// Only send progress updates when there's a meaningful change
			if overallProgress > lastProgress+1 || overallProgress >= 100 {
				if progressChan != nil {
					progressChan <- overallProgress
				}
				lastProgress = overallProgress
			}
		}
	}

	// Ensure we send 100% at the end
	if progressChan != nil && lastProgress < 100 {
		progressChan <- 100
	}

	// 3. Create a temp container
	// Clean up any existing temp containers
	err = removeAllTempContainers(ctx, cli)
	if err != nil {
		logger.Error("Failed to clean up existing temp containers:", err)
		// Continue execution despite cleanup errors
	}

	// Generate timestamped temp container name
	tempContainerName := getTimestampedTempName()

	// Get current container name
	containerID, err := GetContainerID()
	if err != nil {
		return cosy.WrapErrorWithParams(ErrFailedToGetContainerID, err.Error())
	}
	containerInfo, err := cli.ContainerInspect(ctx, containerID)
	if err != nil {
		return cosy.WrapErrorWithParams(ErrFailedToInspectCurrentContainer, err.Error())
	}
	currentContainerName := strings.TrimPrefix(containerInfo.Name, "/")

	// Set up the command for the temp container to execute step 2
	upgradeCmd := []string{"nginx-ui", "upgrade-docker-step2"}

	// Add old container name as environment variable
	containerEnv := containerInfo.Config.Env
	containerEnv = append(containerEnv, fmt.Sprintf("NGINX_UI_CONTAINER_NAME=%s", currentContainerName))

	// Create temp container using new image
	_, err = cli.ContainerCreate(
		ctx,
		&container.Config{
			Image:      fmt.Sprintf("%s:%s", ImageName, tag),
			Entrypoint: upgradeCmd,
			Env:        containerEnv,
		},
		&container.HostConfig{
			Binds: containerInfo.HostConfig.Binds,
		},
		nil,
		nil,
		tempContainerName,
	)
	if err != nil {
		return cosy.WrapErrorWithParams(ErrFailedToCreateTempContainer, err.Error())
	}

	// Start the temp container to execute step 2
	err = cli.ContainerStart(ctx, tempContainerName, container.StartOptions{})
	if err != nil {
		return cosy.WrapErrorWithParams(ErrFailedToStartTempContainer, err.Error())
	}

	// Output status information
	logger.Info("Docker OTA upgrade step 1 completed. Temp container started to execute step 2.")

	return nil
}

// UpgradeStepTwo Trigger in the temp container
func UpgradeStepTwo(ctx context.Context) (err error) {
	// 1. Copy the old config
	cli, err := initClient()
	if err != nil {
		return
	}
	defer cli.Close()

	// Get old container name from environment variable, fallback to settings if not available
	currentContainerName := os.Getenv("NGINX_UI_CONTAINER_NAME")
	if currentContainerName == "" {
		return errors.New("could not find old container name")
	}
	// Get the current running temp container name
	// Since we can't directly get our own container name from inside, we'll search all temp containers
	containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
	if err != nil {
		return errors.Wrap(err, "failed to list containers")
	}

	// Find containers with the temp prefix
	var tempContainerName string
	for _, c := range containers {
		for _, name := range c.Names {
			processedName := strings.TrimPrefix(name, "/")
			if strings.HasPrefix(processedName, TempPrefix) {
				tempContainerName = processedName
				break
			}
		}
		if tempContainerName != "" {
			break
		}
	}

	if tempContainerName == "" {
		return errors.New("could not find temp container")
	}

	// Get temp container info to get the new image
	tempContainerInfo, err := cli.ContainerInspect(ctx, tempContainerName)
	if err != nil {
		return errors.Wrap(err, "failed to inspect temp container")
	}
	newImage := tempContainerInfo.Config.Image

	// Get current container info
	oldContainerInfo, err := cli.ContainerInspect(ctx, currentContainerName)
	if err != nil {
		return errors.Wrap(err, "failed to inspect current container")
	}

	// 2. Stop the old container and rename to _old
	err = cli.ContainerStop(ctx, currentContainerName, container.StopOptions{})
	if err != nil {
		return errors.Wrap(err, "failed to stop current container")
	}

	// Rename the old container with _old suffix
	err = cli.ContainerRename(ctx, currentContainerName, currentContainerName+OldSuffix)
	if err != nil {
		return errors.Wrap(err, "failed to rename old container")
	}

	// Stop the old container
	err = cli.ContainerStop(ctx, currentContainerName+OldSuffix, container.StopOptions{})
	if err != nil {
		return errors.Wrap(err, "failed to stop old container")
	}

	// 3. Use the old config to create and start a new container with the updated image
	// Create new container with original config but using the new image
	newContainerEnv := oldContainerInfo.Config.Env
	// Pass the old container name to the new container
	newContainerEnv = append(newContainerEnv, fmt.Sprintf("NGINX_UI_CONTAINER_NAME=%s", currentContainerName))

	_, err = cli.ContainerCreate(
		ctx,
		&container.Config{
			Image:        newImage,
			Cmd:          oldContainerInfo.Config.Cmd,
			Env:          newContainerEnv,
			Entrypoint:   oldContainerInfo.Config.Entrypoint,
			Labels:       oldContainerInfo.Config.Labels,
			ExposedPorts: oldContainerInfo.Config.ExposedPorts,
			Volumes:      oldContainerInfo.Config.Volumes,
			WorkingDir:   oldContainerInfo.Config.WorkingDir,
		},
		&container.HostConfig{
			Binds:         oldContainerInfo.HostConfig.Binds,
			PortBindings:  oldContainerInfo.HostConfig.PortBindings,
			RestartPolicy: oldContainerInfo.HostConfig.RestartPolicy,
			NetworkMode:   oldContainerInfo.HostConfig.NetworkMode,
			Mounts:        oldContainerInfo.HostConfig.Mounts,
			Privileged:    oldContainerInfo.HostConfig.Privileged,
		},
		nil,
		nil,
		currentContainerName,
	)
	if err != nil {
		// If creation fails, try to recover
		recoverErr := cli.ContainerRename(ctx, currentContainerName+OldSuffix, currentContainerName)
		if recoverErr == nil {
			// Start old container
			recoverErr = cli.ContainerStart(ctx, currentContainerName, container.StartOptions{})
			if recoverErr == nil {
				return errors.Wrap(err, "failed to create new container, recovered to old container")
			}
		}
		return errors.Wrap(err, "failed to create new container and failed to recover")
	}

	// Start the new container
	err = cli.ContainerStart(ctx, currentContainerName, container.StartOptions{})
	if err != nil {
		logger.Error("Failed to start new container:", err)
		// If startup fails, try to recover
		// First remove the failed new container
		removeErr := cli.ContainerRemove(ctx, currentContainerName, container.RemoveOptions{Force: true})
		if removeErr != nil {
			logger.Error("Failed to remove failed new container:", removeErr)
		}

		// Rename the old container back to original
		recoverErr := cli.ContainerRename(ctx, currentContainerName+OldSuffix, currentContainerName)
		if recoverErr == nil {
			// Start old container
			recoverErr = cli.ContainerStart(ctx, currentContainerName, container.StartOptions{})
			if recoverErr == nil {
				return errors.Wrap(err, "failed to start new container, recovered to old container")
			}
		}
		return errors.Wrap(err, "failed to start new container and failed to recover")
	}

	logger.Info("Docker OTA upgrade step 2 completed successfully. New container is running.")
	return nil
}

// UpgradeStepThree Trigger in the new container
func UpgradeStepThree() error {
	ctx := context.Background()
	// Remove the old container
	cli, err := initClient()
	if err != nil {
		return cosy.WrapErrorWithParams(ErrClientNotInitialized, err.Error())
	}
	defer cli.Close()

	// Get old container name from environment variable, fallback to settings if not available
	currentContainerName := os.Getenv("NGINX_UI_CONTAINER_NAME")
	if currentContainerName == "" {
		return nil
	}
	oldContainerName := currentContainerName + OldSuffix

	// Check if old container exists and remove it if it does
	containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
	if err != nil {
		return errors.Wrap(err, "failed to list containers")
	}

	for _, c := range containers {
		for _, name := range c.Names {
			processedName := strings.TrimPrefix(name, "/")
			// Remove old container
			if processedName == oldContainerName {
				err = cli.ContainerRemove(ctx, c.ID, container.RemoveOptions{Force: true})
				if err != nil {
					logger.Error("Failed to remove old container:", err)
					// Continue execution, don't interrupt because of failure to remove old container
				} else {
					logger.Info("Successfully removed old container:", oldContainerName)
				}
				break
			}
		}
	}

	// Clean up all temp containers
	err = removeAllTempContainers(ctx, cli)
	if err != nil {
		logger.Error("Failed to clean up temp containers:", err)
		// Continue execution despite cleanup errors
	}

	return nil
}
