package ui

import (
	"fmt"
	"sort"
	"strings"
	"time"

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

	"github.com/anchore/bubbly/bubbles/taskprogress"
	"github.com/anchore/grype/grype/event/monitor"
	"github.com/anchore/grype/grype/event/parsers"
	"github.com/anchore/grype/grype/vulnerability"
	"github.com/anchore/grype/internal/log"
)

const (
	branch = "├──"
	end    = "└──"
)

var _ progress.StagedProgressable = (*vulnerabilityScanningAdapter)(nil)

type vulnerabilityProgressTree struct {
	mon        *monitor.Matching
	windowSize tea.WindowSizeMsg

	countBySeverity map[vulnerability.Severity]int64
	unknownCount    int64
	fixedCount      int64
	ignoredCount    int64
	droppedCount    int64
	totalCount      int64
	severities      []vulnerability.Severity

	id       uint32
	sequence int

	updateDuration time.Duration
	textStyle      lipgloss.Style
}

func newVulnerabilityProgressTree(monitor *monitor.Matching, textStyle lipgloss.Style) vulnerabilityProgressTree {
	allSeverities := vulnerability.AllSeverities()
	sort.Sort(sort.Reverse(vulnerability.Severities(allSeverities)))

	return vulnerabilityProgressTree{
		mon:             monitor,
		countBySeverity: make(map[vulnerability.Severity]int64),
		severities:      allSeverities,
		textStyle:       textStyle,
	}
}

// vulnerabilityProgressTreeTickMsg indicates that the timer has ticked and we should render a frame.
type vulnerabilityProgressTreeTickMsg struct {
	Time     time.Time
	Sequence int
	ID       uint32
}

type vulnerabilityScanningAdapter struct {
	mon *monitor.Matching
}

func (p vulnerabilityScanningAdapter) Current() int64 {
	return p.mon.PackagesProcessed.Current()
}

func (p vulnerabilityScanningAdapter) Error() error {
	return p.mon.MatchesDiscovered.Error()
}

func (p vulnerabilityScanningAdapter) Size() int64 {
	return p.mon.PackagesProcessed.Size()
}

func (p vulnerabilityScanningAdapter) Stage() string {
	return fmt.Sprintf("%d vulnerability matches", p.mon.MatchesDiscovered.Current()-p.mon.Ignored.Current())
}

func (m *Handler) handleVulnerabilityScanningStarted(e partybus.Event) ([]tea.Model, tea.Cmd) {
	mon, err := parsers.ParseVulnerabilityScanningStarted(e)
	if err != nil {
		log.WithFields("error", err).Warn("unable to parse event")
		return nil, nil
	}

	tsk := m.newTaskProgress(
		taskprogress.Title{
			Default: "Scan for vulnerabilities",
			Running: "Scanning for vulnerabilities",
			Success: "Scanned for vulnerabilities",
		},
		taskprogress.WithStagedProgressable(vulnerabilityScanningAdapter{mon: mon}),
	)

	tsk.HideStageOnSuccess = false

	textStyle := tsk.HintStyle

	return []tea.Model{
		tsk,
		newVulnerabilityProgressTree(mon, textStyle),
	}, nil
}

func (l vulnerabilityProgressTree) Init() tea.Cmd {
	// this is the periodic update of state information
	return func() tea.Msg {
		return vulnerabilityProgressTreeTickMsg{
			// The time at which the tick occurred.
			Time: time.Now(),

			// The ID of the log frame that this message belongs to. This can be
			// helpful when routing messages, however bear in mind that log frames
			// will ignore messages that don't contain ID by default.
			ID: l.id,

			Sequence: l.sequence,
		}
	}
}

func (l vulnerabilityProgressTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		l.windowSize = msg
		return l, nil

	case vulnerabilityProgressTreeTickMsg:
		// update the model
		l.totalCount = l.mon.MatchesDiscovered.Current()
		l.fixedCount = l.mon.Fixed.Current()
		l.ignoredCount = l.mon.Ignored.Current()
		l.droppedCount = l.mon.Dropped.Current()
		l.unknownCount = l.mon.BySeverity[vulnerability.UnknownSeverity].Current()
		for _, sev := range l.severities {
			l.countBySeverity[sev] = l.mon.BySeverity[sev].Current()
		}

		// kick off the next tick
		tickCmd := l.handleTick(msg)

		return l, tickCmd
	}

	return l, nil
}

func (l vulnerabilityProgressTree) View() string {
	sb := strings.Builder{}

	for idx, sev := range l.severities {
		count := l.countBySeverity[sev]
		sb.WriteString(fmt.Sprintf("%d %s", count, sev))
		if idx < len(l.severities)-1 {
			sb.WriteString(", ")
		}
	}
	if l.unknownCount > 0 {
		unknownStr := fmt.Sprintf(" (%d unknown)", l.unknownCount)
		sb.WriteString(unknownStr)
	}

	status := sb.String()
	sb.Reset()

	sevStr := l.textStyle.Render(fmt.Sprintf("   %s by severity: %s", branch, status))

	sb.WriteString(sevStr)

	dropped := ""
	if l.droppedCount > 0 {
		dropped = fmt.Sprintf("(%d dropped)", l.droppedCount)
	}

	fixedStr := l.textStyle.Render(
		fmt.Sprintf("   %s by status:   %d fixed, %d not-fixed, %d ignored %s",
			end, l.fixedCount, l.totalCount-l.fixedCount, l.ignoredCount, dropped,
		),
	)
	sb.WriteString("\n" + fixedStr)

	return sb.String()
}

func (l vulnerabilityProgressTree) queueNextTick() tea.Cmd {
	return tea.Tick(l.updateDuration, func(t time.Time) tea.Msg {
		return vulnerabilityProgressTreeTickMsg{
			Time:     t,
			ID:       l.id,
			Sequence: l.sequence,
		}
	})
}

func (l *vulnerabilityProgressTree) handleTick(msg vulnerabilityProgressTreeTickMsg) tea.Cmd {
	// If an ID is set, and the ID doesn't belong to this log frame, reject the message.
	if msg.ID > 0 && msg.ID != l.id {
		return nil
	}

	// If a sequence is set, and it's not the one we expect, reject the message.
	// This prevents the log frame from receiving too many messages and
	// thus updating too frequently.
	if msg.Sequence > 0 && msg.Sequence != l.sequence {
		return nil
	}

	l.sequence++

	// note: even if the log is completed we should still respond to stage changes and window size events
	return l.queueNextTick()
}
