package print

import (
	"bufio"
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"syscall"

	"github.com/goccy/go-yaml"

	"log/slog"
	"os"
	"os/exec"
	"strings"

	"github.com/fatih/color"
	"github.com/lmittmann/tint"
	"github.com/mattn/go-colorable"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"

	"golang.org/x/term"
)

type Level string

const (
	DebugLevel   Level = "debug"
	InfoLevel    Level = "info"
	WarningLevel Level = "warning"
	ErrorLevel   Level = "error"

	// Needed to avoid import cycle
	// Originally defined in "internal/pkg/config/config.go"
	outputFormatKey = "output-format"

	JSONOutputFormat   = "json"
	PrettyOutputFormat = "pretty"
	NoneOutputFormat   = "none"
	YAMLOutputFormat   = "yaml"
)

var (
	errAborted = errors.New("operation aborted")

	WhiteBold  = color.New(color.FgHiWhite, color.Bold).SprintFunc()
	RedBold    = color.New(color.FgHiRed, color.Bold).SprintFunc()
	YellowBold = color.New(color.FgHiYellow, color.Bold).SprintFunc()
)

type Printer struct {
	Cmd       *cobra.Command
	AssumeYes bool
	Verbosity Level
}

// NewPrinter creates a new printer, including setting up the default logger.
func NewPrinter() *Printer {
	w := os.Stderr
	logger := slog.New(
		tint.NewHandler(colorable.NewColorable(w), &tint.Options{AddSource: false, Level: slog.LevelDebug}),
	)
	slog.SetDefault(logger)

	return &Printer{}
}

// Print an output using Printf to the defined output (falling back to Stderr if not set).
// If output format is set to none, it does nothing
func (p *Printer) Outputf(msg string, args ...any) {
	outputFormat := viper.GetString(outputFormatKey)
	if outputFormat == NoneOutputFormat {
		return
	}
	p.Cmd.Printf(msg, args...)
}

// Print an output using Println to the defined output (falling back to Stderr if not set).
// If output format is set to none, it does nothing
func (p *Printer) Outputln(msg string) {
	outputFormat := viper.GetString(outputFormatKey)
	if outputFormat == NoneOutputFormat {
		return
	}
	p.Cmd.Println(msg)
}

// Print a Debug level log through the "slog" package.
// If the verbosity level is not Debug, it does nothing
func (p *Printer) Debug(level Level, msg string, args ...any) {
	if !p.IsVerbosityDebug() {
		return
	}
	msg = fmt.Sprintf(msg, args...)
	switch level {
	case DebugLevel:
		slog.Debug(msg)
	case InfoLevel:
		slog.Info(msg)
	case WarningLevel:
		slog.Warn(msg)
	case ErrorLevel:
		slog.Error(msg)
	}
}

// Print an Info level output to the defined Err output (falling back to Stderr if not set).
// If the verbosity level is not Debug or Info, it does nothing.
func (p *Printer) Info(msg string, args ...any) {
	if !p.IsVerbosityDebug() && !p.IsVerbosityInfo() {
		return
	}
	p.Cmd.PrintErrf(msg, args...)
}

// Print a Warn level output to the defined Err output (falling back to Stderr if not set).
// If the verbosity level is not Debug, Info, or Warn, it does nothing.
func (p *Printer) Warn(msg string, args ...any) {
	if !p.IsVerbosityDebug() && !p.IsVerbosityInfo() && !p.IsVerbosityWarning() {
		return
	}
	warning := fmt.Sprintf(msg, args...)
	p.Cmd.PrintErrf("%s %s", YellowBold("Warning:"), warning)
}

// Print an Error level output to the defined Err output (falling back to Stderr if not set).
func (p *Printer) Error(msg string, args ...any) {
	err := fmt.Sprintf(msg, args...)
	p.Cmd.PrintErrln(RedBold(p.Cmd.ErrPrefix()), err)
}

// Prompts the user for confirmation.
//
// Returns nil only if the user (explicitly) answers positive.
// Returns ErrAborted if the user answers negative.
func (p *Printer) PromptForConfirmation(prompt string) error {
	if p.AssumeYes {
		p.Warn("Auto-confirming prompt: %q\n", prompt)
		return nil
	}
	question := fmt.Sprintf("%s [y/N] ", prompt)
	reader := bufio.NewReader(p.Cmd.InOrStdin())
	for i := 0; i < 3; i++ {
		p.Cmd.PrintErr(question)
		answer, err := reader.ReadString('\n')
		if err != nil {
			continue
		}
		answer = strings.ToLower(strings.TrimSpace(answer))
		if answer == "y" || answer == "yes" {
			return nil
		}
		if answer == "" || answer == "n" || answer == "no" {
			return errAborted
		}
	}
	return fmt.Errorf("max number of wrong inputs")
}

// Prompts the user for confirmation by pressing Enter.
//
// Returns nil if the user presses Enter.
func (p *Printer) PromptForEnter(prompt string) error {
	if p.AssumeYes {
		p.Warn("Auto-confirming prompt: %q", prompt)
		return nil
	}
	reader := bufio.NewReader(p.Cmd.InOrStdin())
	p.Cmd.PrintErr(prompt)
	_, err := reader.ReadString('\n')
	if err != nil {
		return fmt.Errorf("read user response: %w", err)
	}
	return nil
}

// Prompts the user for a password.
//
// Returns the password that was given, otherwise returns error
func (p *Printer) PromptForPassword(prompt string) (string, error) {
	p.Cmd.PrintErr(prompt)
	defer p.Outputln("")
	if term.IsTerminal(syscall.Stdin) {
		bytePassword, err := term.ReadPassword(syscall.Stdin)
		if err != nil {
			return "", fmt.Errorf("read password: %w", err)
		}
		return string(bytePassword), nil
	}
	// Fallback for non-terminal environments
	reader := bufio.NewReader(p.Cmd.InOrStdin())
	pw, err := reader.ReadString('\n')
	if err != nil {
		return "", fmt.Errorf("read password from non-terminal: %w", err)
	}
	return pw[:len(pw)-1], nil // remove trailing newline
}

// Shows the content in the command's stdout using the "less" command
// If output format is set to none, it does nothing
func (p *Printer) PagerDisplay(content string) error {
	outputFormat := viper.GetString(outputFormatKey)
	if outputFormat == NoneOutputFormat {
		return nil
	}

	// less arguments
	// -F: exits if the entire file fits on the first screen
	// -S: disables line wrapping
	// -w: highlight the first line after moving one full page down
	// -R: interprets ANSI color and style sequences
	// -K: exits if an interrupt character is typed
	pagerCmd := exec.Command("less", "-F", "-S", "-w", "-R", "-K")

	pager, pagerExists := os.LookupEnv("PAGER")
	if pagerExists && pager != "nil" && pager != "" {
		pagerCmd = exec.Command(pager) // #nosec G204
	}

	pagerCmd.Stdin = strings.NewReader(content)
	pagerCmd.Stdout = p.Cmd.OutOrStdout()

	p.Debug(DebugLevel, "using pager: %s", pagerCmd.Args[0])
	err := pagerCmd.Run()
	if err != nil {
		p.Debug(ErrorLevel, "run pager command: %v", err)
		p.Outputln(content)
	}
	return nil
}

// Returns True if the verbosity level is set to Debug, False otherwise.
func (p *Printer) IsVerbosityDebug() bool {
	return p.Verbosity == DebugLevel
}

// Returns True if the verbosity level is set to Info, False otherwise.
func (p *Printer) IsVerbosityInfo() bool {
	return p.Verbosity == InfoLevel
}

// Returns True if the verbosity level is set to Warning, False otherwise.
func (p *Printer) IsVerbosityWarning() bool {
	return p.Verbosity == WarningLevel
}

// Returns True if the verbosity level is set to Error, False otherwise.
func (p *Printer) IsVerbosityError() bool {
	return p.Verbosity == ErrorLevel
}

// DebugInputModel prints the given input model in case verbosity level is set to Debug, does nothing otherwise
func (p *Printer) DebugInputModel(model any) {
	if p.IsVerbosityDebug() {
		modelStr, err := buildDebugStrFromInputModel(model)
		if err != nil {
			p.Debug(ErrorLevel, "convert model to string for debugging: %v", err)
		} else {
			p.Debug(DebugLevel, "parsed input values: %s", modelStr)
		}
	}
}

func (p *Printer) OutputResult(outputFormat string, output any, prettyOutputFunc func() error) error {
	switch outputFormat {
	case JSONOutputFormat:
		buffer := &bytes.Buffer{}
		encoder := json.NewEncoder(buffer)
		encoder.SetEscapeHTML(false)
		encoder.SetIndent("", "  ")
		err := encoder.Encode(output)
		if err != nil {
			return fmt.Errorf("marshal json: %w", err)
		}
		details := buffer.Bytes()
		p.Outputln(string(details))

		return nil
	case YAMLOutputFormat:
		details, err := yaml.MarshalWithOptions(output, yaml.IndentSequence(true), yaml.UseJSONMarshaler())
		if err != nil {
			return fmt.Errorf("marshal yaml: %w", err)
		}
		p.Outputln(string(details))

		return nil
	default:
		return prettyOutputFunc()
	}
}
