package ui

import (
	"fmt"
	"strings"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/dustin/go-humanize"
	"github.com/wagoodman/go-partybus"
	"github.com/wagoodman/go-progress"

	"github.com/anchore/bubbly/bubbles/taskprogress"
	stereoscopeParsers "github.com/anchore/stereoscope/pkg/event/parsers"
	"github.com/anchore/stereoscope/pkg/image/docker"
	"github.com/anchore/syft/internal/log"
)

var _ interface {
	progress.Stager
	progress.Progressable
} = (*dockerPullProgressAdapter)(nil)

type dockerPullStatus interface {
	Complete() bool
	Layers() []docker.LayerID
	Current(docker.LayerID) docker.LayerState
}

type dockerPullProgressAdapter struct {
	status    dockerPullStatus
	formatter dockerPullStatusFormatter
}

type dockerPullStatusFormatter struct {
	auxInfoStyle             lipgloss.Style
	dockerPullCompletedStyle lipgloss.Style
	dockerPullDownloadStyle  lipgloss.Style
	dockerPullExtractStyle   lipgloss.Style
	dockerPullStageChars     []string
	layerCaps                []string
}

func (m *Handler) handlePullDockerImage(e partybus.Event) []tea.Model {
	_, pullStatus, err := stereoscopeParsers.ParsePullDockerImage(e)
	if err != nil {
		log.WithFields("error", err).Warn("unable to parse event")
		return nil
	}

	tsk := m.newTaskProgress(
		taskprogress.Title{
			Default: "Pull image",
			Running: "Pulling image",
			Success: "Pulled image",
		},
		taskprogress.WithStagedProgressable(
			newDockerPullProgressAdapter(pullStatus),
		),
	)

	tsk.HintStyle = lipgloss.NewStyle()
	tsk.HintEndCaps = nil

	return []tea.Model{tsk}
}

func newDockerPullProgressAdapter(status dockerPullStatus) *dockerPullProgressAdapter {
	return &dockerPullProgressAdapter{
		status:    status,
		formatter: newDockerPullStatusFormatter(),
	}
}

func newDockerPullStatusFormatter() dockerPullStatusFormatter {
	return dockerPullStatusFormatter{
		auxInfoStyle:             lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")),
		dockerPullCompletedStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#fcba03")),
		dockerPullDownloadStyle:  lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")),
		dockerPullExtractStyle:   lipgloss.NewStyle().Foreground(lipgloss.Color("#ffffff")),
		dockerPullStageChars:     strings.Split("▁▃▄▅▆▇█", ""),
		layerCaps:                strings.Split("▕▏", ""),
	}
}

func (d dockerPullProgressAdapter) Size() int64 {
	return -1
}

func (d dockerPullProgressAdapter) Current() int64 {
	return 1
}

func (d dockerPullProgressAdapter) Error() error {
	if d.status.Complete() {
		return progress.ErrCompleted
	}
	// TODO: return intermediate error indications
	return nil
}

func (d dockerPullProgressAdapter) Stage() string {
	return d.formatter.Render(d.status)
}

// Render crafts the given docker image pull status summarized into a single line.
func (f dockerPullStatusFormatter) Render(pullStatus dockerPullStatus) string {
	var size, current uint64

	layers := pullStatus.Layers()
	status := make(map[docker.LayerID]docker.LayerState)
	completed := make([]string, len(layers))

	// fetch the current state
	for idx, layer := range layers {
		completed[idx] = " "
		status[layer] = pullStatus.Current(layer)
	}

	numCompleted := 0
	for idx, layer := range layers {
		prog := status[layer].PhaseProgress
		curN := prog.Current()
		curSize := prog.Size()

		if progress.IsCompleted(prog) {
			input := f.dockerPullStageChars[len(f.dockerPullStageChars)-1]
			completed[idx] = f.formatDockerPullPhase(status[layer].Phase, input)
		} else if curN != 0 {
			var ratio float64
			switch {
			case curN == 0 || curSize < 0:
				ratio = 0
			case curN >= curSize:
				ratio = 1
			default:
				ratio = float64(curN) / float64(curSize)
			}

			i := int(ratio * float64(len(f.dockerPullStageChars)-1))
			input := f.dockerPullStageChars[i]
			completed[idx] = f.formatDockerPullPhase(status[layer].Phase, input)
		}

		if progress.IsErrCompleted(status[layer].DownloadProgress.Error()) {
			numCompleted++
		}
	}

	for _, layer := range layers {
		prog := status[layer].DownloadProgress
		size += uint64(prog.Size())
		current += uint64(prog.Current())
	}

	var progStr, auxInfo string
	if len(layers) > 0 {
		render := strings.Join(completed, "")
		prefix := f.dockerPullCompletedStyle.Render(fmt.Sprintf("%d Layers", len(layers)))
		auxInfo = f.auxInfoStyle.Render(fmt.Sprintf("[%s / %s]", humanize.Bytes(current), humanize.Bytes(size)))
		if len(layers) == numCompleted {
			auxInfo = f.auxInfoStyle.Render(fmt.Sprintf("[%s] Extracting...", humanize.Bytes(size)))
		}

		progStr = fmt.Sprintf("%s%s%s%s", prefix, f.layerCap(false), render, f.layerCap(true))
	}

	return progStr + auxInfo
}

// formatDockerPullPhase returns a single character that represents the status of a layer pull.
func (f dockerPullStatusFormatter) formatDockerPullPhase(phase docker.PullPhase, inputStr string) string {
	switch phase {
	case docker.WaitingPhase:
		// ignore any progress related to waiting
		return " "
	case docker.PullingFsPhase, docker.DownloadingPhase:
		return f.dockerPullDownloadStyle.Render(inputStr)
	case docker.DownloadCompletePhase:
		return f.dockerPullDownloadStyle.Render(f.dockerPullStageChars[len(f.dockerPullStageChars)-1])
	case docker.ExtractingPhase:
		return f.dockerPullExtractStyle.Render(inputStr)
	case docker.VerifyingChecksumPhase, docker.PullCompletePhase:
		return f.dockerPullCompletedStyle.Render(inputStr)
	case docker.AlreadyExistsPhase:
		return f.dockerPullCompletedStyle.Render(f.dockerPullStageChars[len(f.dockerPullStageChars)-1])
	default:
		return inputStr
	}
}

func (f dockerPullStatusFormatter) layerCap(end bool) string {
	l := len(f.layerCaps)
	if l == 0 {
		return ""
	}
	if end {
		return f.layerCaps[l-1]
	}
	return f.layerCaps[0]
}
