package formatter

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/charmbracelet/lipgloss"
	"github.com/charmbracelet/lipgloss/table"
	"github.com/docker/go-units"
	"golang.org/x/term"

	"coopcloud.tech/abra/pkg/config"
	"coopcloud.tech/abra/pkg/log"
	"github.com/schollz/progressbar/v3"
)

var BoldStyle = lipgloss.NewStyle().
	Bold(true)

var BoldUnderlineStyle = lipgloss.NewStyle().
	Bold(true).
	Underline(true)

func ShortenID(str string) string {
	return str[:12]
}

func SmallSHA(hash string) string {
	return hash[:8]
}

// RemoveSha remove image sha from a string that are added in some docker outputs
func RemoveSha(str string) string {
	return strings.Split(str, "@")[0]
}

// HumanDuration from docker/cli RunningFor() to be accessible outside of the class
func HumanDuration(timestamp int64) string {
	date := time.Unix(timestamp, 0)
	now := time.Now().UTC()
	return units.HumanDuration(now.Sub(date)) + " ago"
}

// CreateTable prepares a table layout for output.
func CreateTable() (*table.Table, error) {
	var (
		renderer    = lipgloss.NewRenderer(os.Stdout)
		headerStyle = renderer.NewStyle().Bold(true).Align(lipgloss.Center)
		cellStyle   = renderer.NewStyle().Padding(0, 1)
		borderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
	)

	table := table.New().
		Border(lipgloss.ThickBorder()).
		BorderStyle(borderStyle).
		StyleFunc(func(row, col int) lipgloss.Style {
			var style lipgloss.Style

			switch {
			case row == table.HeaderRow:
				return headerStyle
			default:
				style = cellStyle
			}

			return style
		})

	return table, nil
}

func PrintTable(t *table.Table) error {
	if isAbraCI, ok := os.LookupEnv("ABRA_CI"); ok && isAbraCI == "1" {
		// NOTE(d1): no width limits for CI testing since we test against outputs
		log.Debug("detected ABRA_CI=1")
		fmt.Println(t)
		return nil
	}

	tWidth, _ := lipgloss.Size(t.String())

	width, _, err := term.GetSize(0)
	if err != nil {
		return err
	}

	if tWidth > width {
		t.Width(width - 10)
	}

	fmt.Println(t)

	return nil
}

// horizontal is a JoinHorizontal helper function.
func horizontal(left, mid, right string) string {
	return lipgloss.JoinHorizontal(lipgloss.Right, left, mid, right)
}

func CreateOverview(header string, rows [][]string) string {
	var borderStyle = lipgloss.NewStyle().
		BorderStyle(lipgloss.ThickBorder()).
		Padding(0, 1, 0, 1).
		BorderForeground(lipgloss.Color("63"))

	var headerStyle = lipgloss.NewStyle().
		Underline(true).
		Bold(true).
		PaddingBottom(1)

	var leftStyle = lipgloss.NewStyle()
	var rightStyle = lipgloss.NewStyle()

	var longest int
	for _, row := range rows {
		if len(row[0]) > longest {
			longest = len(row[0])
		}
	}

	var renderedRows []string
	for _, row := range rows {
		if len(row) < 2 {
			continue
		}

		if len(row) > 2 {
			panic("CreateOverview: only accepts rows of len == 2")
		}

		lenOffset := 4
		if len(row[0]) < longest {
			lenOffset += longest - len(row[0])
		}

		offset := ""
		for range lenOffset {
			offset = offset + " "
		}

		rendered := horizontal(leftStyle.Render(row[0]), offset, rightStyle.Render(row[1]))

		if row[1] == "---" {
			rendered = horizontal(
				leftStyle.
					Bold(true).
					Underline(true).
					PaddingTop(1).
					Render(row[0]),
				offset,
				rightStyle.Render(""),
			)
		}

		renderedRows = append(renderedRows, rendered)
	}

	body := strings.Builder{}
	body.WriteString(
		borderStyle.Render(
			lipgloss.JoinVertical(
				lipgloss.Center,
				headerStyle.Render(header),
				lipgloss.JoinVertical(
					lipgloss.Left,
					renderedRows...,
				),
			),
		),
	)

	return body.String()
}

// ToJSON converts a lipgloss.Table to JSON representation. It's not a robust
// implementation and mainly caters for our current use case which is basically
// a bunch of strings. See https://github.com/charmbracelet/lipgloss/issues/335
// for the real thing (hopefully).
func ToJSON(headers []string, rows [][]string) (string, error) {
	var buff bytes.Buffer

	buff.Write([]byte("["))

	for idx, row := range rows {
		payload := make(map[string]string)

		for idx, header := range headers {
			payload[strings.ToLower(header)] = row[idx]
		}

		serialized, err := json.Marshal(payload)
		if err != nil {
			return "", err
		}

		buff.Write(serialized)

		if idx < (len(rows) - 1) {
			buff.Write([]byte(","))
		}
	}

	buff.Write([]byte("]"))

	return buff.String(), nil
}

// CreateProgressbar generates a progress bar
func CreateProgressbar(length int, title string) *progressbar.ProgressBar {
	return progressbar.NewOptions(
		length,
		progressbar.OptionClearOnFinish(),
		progressbar.OptionSetPredictTime(false),
		progressbar.OptionShowCount(),
		progressbar.OptionSetDescription(title),
	)
}

// StripTagMeta strips front-matter image tag data that we don't need for parsing.
func StripTagMeta(image string) string {
	originalImage := image

	if strings.Contains(image, "docker.io") {
		image = strings.Split(image, "/")[1]
	}

	if strings.Contains(image, "library") {
		image = strings.Split(image, "/")[1]
	}

	if originalImage != image {
		log.Debugf("stripped %s to %s for parsing", originalImage, image)
	}

	return image
}

// ByteCountSI presents a human friendly representation of a byte count. See
// https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format.
func ByteCountSI(b uint64) string {
	const unit = 1000

	if b < unit {
		return fmt.Sprintf("%d B", b)
	}

	div, exp := uint64(unit), 0
	for n := b / unit; n >= unit; n /= unit {
		div *= unit
		exp++
	}

	return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
}

// BoldDirtyDefault ensures a dirty modifier is rendered in bold.
func BoldDirtyDefault(v string) string {
	if strings.HasSuffix(v, config.DIRTY_DEFAULT) {
		vBold := BoldStyle.Render(config.DIRTY_DEFAULT)
		v = strings.Replace(v, config.DIRTY_DEFAULT, vBold, 1)
	}

	return v
}

// AddDirtyMarker adds the dirty marker to a version string.
func AddDirtyMarker(v string) string {
	return fmt.Sprintf("%s%s", v, config.DIRTY_DEFAULT)
}
