package ui

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/safedep/pmg/analyzer"
	"github.com/safedep/pmg/internal/models"
	"golang.org/x/term"
)

// The UI is internal to PMG and opinionated for the CLI.
// It is not intended to be used outside of PMG.

type VerbosityLevel int

const (
	// PMG is hidden from the user except for errors
	// and when malicious packages are detected
	VerbosityLevelSilent VerbosityLevel = iota

	// Show minimal status updates
	VerbosityLevelNormal

	// Show verbose status updates and information including
	// information about malicious packages
	VerbosityLevelVerbose
)

type BlockConfig struct {
	// ShowReference determines whether to show detailed information for suspicious packages.
	// If false, the details are omitted to avoid repeating information already shown to the user.
	ShowReference bool

	MalwarePackages []*analyzer.PackageVersionAnalysisResult
}

func NewDefaultBlockConfig() *BlockConfig {
	return &BlockConfig{
		ShowReference: true,
	}
}

var verbosityLevel VerbosityLevel = VerbosityLevelNormal

func SetVerbosityLevel(level VerbosityLevel) {
	verbosityLevel = level
}

func ClearStatus() {
	StopSpinner()
	fmt.Fprint(os.Stderr, "\r")
}

func Block(config *BlockConfig) error {
	return blockWithExit(config, true)
}

func BlockNoExit(config *BlockConfig) error {
	return blockWithExit(config, false)
}

func blockWithExit(config *BlockConfig, exit bool) error {
	StopSpinner()

	// We show the block message only in normal mode to avoid repeating information
	// already shown to the user in verbose mode as part of the reporting.
	if verbosityLevel != VerbosityLevelVerbose {
		fmt.Println()
		fmt.Printf("%s %s\n", Colors.Red("✗"), Colors.Red("Malicious package blocked"))

		if config.ShowReference {
			printMaliciousPackagesList(config.MalwarePackages)
			fmt.Println()
		}
	}

	if exit {
		os.Exit(1)
	}

	return nil
}

func SetStatus(status string) {
	if verbosityLevel == VerbosityLevelSilent {
		return
	}

	StopSpinner()
	StartSpinnerWithColor(status, Colors.Dim)
}

// GetConfirmationOnMalware prompts the user to confirm installation of suspicious packages.
// It reads from os.Stdin. Use GetConfirmationOnMalwareWithReader for custom input sources.
func GetConfirmationOnMalware(malwarePackages []*analyzer.PackageVersionAnalysisResult) (bool, error) {
	return GetConfirmationOnMalwareWithReader(malwarePackages, os.Stdin)
}

// GetConfirmationOnMalwareWithReader prompts the user to confirm installation of suspicious packages.
// It reads from the provided reader, allowing for PTY input routing during proxy mode.
func GetConfirmationOnMalwareWithReader(malwarePackages []*analyzer.PackageVersionAnalysisResult, reader io.Reader) (bool, error) {
	StopSpinner()

	fmt.Println()
	fmt.Printf("%s %s\n", Colors.Yellow("!"), Colors.Yellow(fmt.Sprintf("Suspicious package(s) detected: %d", len(malwarePackages))))

	printMaliciousPackagesList(malwarePackages)

	fmt.Println()
	fmt.Print(Colors.Normal("Do you want to continue with the installation? (y/N) "))

	// Use Scanner on the provided reader to support PTY input routing
	scanner := bufio.NewScanner(reader)
	if scanner.Scan() {
		response := strings.ToLower(strings.TrimSpace(scanner.Text()))
		if response == "y" || response == "yes" || (len(response) > 0 && response[0] == 'y') {
			return true, nil
		}
	}

	// Check for scanner errors, but don't treat them as fatal
	if err := scanner.Err(); err != nil {
		// On EOF or interrupted read, just return false (deny)
		return false, nil
	}

	return false, nil
}

func ShowWarning(message string) {
	// Print colored warning to stderr immediately - it won't be cleared by other output
	fmt.Fprintf(os.Stderr, "PMG: %s\n", Colors.Red(message))
}

// Infof prints an informational message, suppressed in silent mode.
func Infof(msg string, args ...interface{}) {
	if verbosityLevel == VerbosityLevelSilent {
		return
	}
	fmt.Println(fmt.Sprintf(msg, args...))
}

// Successf prints a green success message, suppressed in silent mode.
func Successf(msg string, args ...interface{}) {
	if verbosityLevel == VerbosityLevelSilent {
		return
	}
	fmt.Printf("%s %s\n", Colors.Green("✓"), fmt.Sprintf(msg, args...))
}

// PromptInput prints a label and reads a line of visible input from stdin.
func PromptInput(label string) (string, error) {
	fmt.Printf("%s %s", Colors.Cyan("›"), Colors.Bold(label))
	scanner := bufio.NewScanner(os.Stdin)
	if scanner.Scan() {
		return strings.TrimSpace(scanner.Text()), nil
	}
	if err := scanner.Err(); err != nil {
		return "", err
	}
	return "", fmt.Errorf("no input received")
}

// PromptSecret prints a label and reads input from stdin with echo disabled.
// Returns an error if stdin is not a terminal (e.g. piped input).
func PromptSecret(label string) (string, error) {
	if !term.IsTerminal(int(os.Stdin.Fd())) {
		return "", fmt.Errorf("interactive terminal required for secure input")
	}

	fmt.Printf("%s %s", Colors.Cyan("▪"), Colors.Bold(label))
	raw, err := term.ReadPassword(int(os.Stdin.Fd()))
	fmt.Println() // newline after hidden input
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(string(raw)), nil
}

func Fatalf(msg string, args ...interface{}) {
	ClearStatus()

	fmt.Fprintln(os.Stderr, Colors.Red(fmt.Sprintf(msg, args...)))
	os.Exit(1)
}

func printMaliciousPackagesList(malwarePackages []*analyzer.PackageVersionAnalysisResult) {
	for _, mp := range malwarePackages {
		fmt.Println()
		fmt.Printf("  %s %s\n", Colors.Red("-"),
			Colors.Red(fmt.Sprintf("%s@%s", mp.PackageVersion.GetPackage().GetName(),
				mp.PackageVersion.GetVersion())))

		if verbosityLevel == VerbosityLevelVerbose {
			fmt.Printf("    %s\n", Colors.Dim(termWidthFormatTextIndent(mp.Summary, 76, "    ")))
		}

		if mp.ReferenceURL != "" {
			fmt.Printf("    %s\n", Colors.Dim(fmt.Sprintf("Reference: %s", mp.ReferenceURL)))
		}
	}
}

func printCooldownPackagesList(packages []models.CooldownBlock) {
	for _, pkg := range packages {
		fmt.Println()
		fmt.Printf("  %s %s\n",
			Colors.Yellow("⊘"),
			Colors.Yellow(fmt.Sprintf("%s@%s", pkg.Name, pkg.Version)))

		dateStr := ""
		if !pkg.PublishDate.IsZero() {
			dateStr = fmt.Sprintf(" (%s)", pkg.PublishDate.Format("2006-01-02"))
		}

		fmt.Printf("    %s\n", Colors.Dim(fmt.Sprintf(
			"Published %s ago%s — available in %s",
			pluralizeDays(pkg.DaysAgo), dateStr, pluralizeDays(pkg.DaysLeft),
		)))
		fmt.Printf("    %s\n", Colors.Dim(fmt.Sprintf(
			"Tip: wait %s for cooldown to expire",
			pluralizeDays(pkg.DaysLeft),
		)))
	}
}

// printAdvisoryMessage renders the org-configured advisory_message as an info
// note attached to the block output. Callers are responsible for surrounding
// blank lines. No-op when the message is empty.
func printAdvisoryMessage(message string) {
	if message == "" {
		return
	}
	fmt.Printf("  %s %s\n", Colors.Cyan("ℹ"), Colors.Cyan(termWidthFormatTextIndent(message, 76, "    ")))
}

func pluralizeDays(n int) string {
	if n == 1 {
		return "1 day"
	}
	return fmt.Sprintf("%d days", n)
}

func pluralizePackages(n int) string {
	if n == 1 {
		return "1 package"
	}
	return fmt.Sprintf("%d packages", n)
}

// termWidthFormatTextIndent wraps text at maxWidth and indents continuation
// lines so wrapped output stays aligned with the first line.
func termWidthFormatTextIndent(text string, maxWidth int, indent string) string {
	return strings.ReplaceAll(termWidthFormatText(text, maxWidth), "\n", "\n"+indent)
}

// Format the string to be maximum maxWidth. Use newlines to wrap the text.
func termWidthFormatText(text string, maxWidth int) string {
	// Replace all newlines with spaces so that we can split the text into words
	// This is to ensure that we don't split the text at the newlines
	text = strings.ReplaceAll(text, "\n", " ")

	words := strings.Split(text, " ")
	lines := []string{}
	currentLine := ""

	for i, word := range words {
		// Skip empty words that might result from multiple spaces
		if word == "" {
			continue
		}

		if i == 0 {
			// First word doesn't need a leading space
			currentLine = word
		} else if len(currentLine)+len(word)+1 > maxWidth {
			// +1 for the space we would add
			lines = append(lines, currentLine)
			currentLine = word
		} else {
			currentLine += " " + word
		}
	}

	// Don't forget to add the last line
	if currentLine != "" {
		lines = append(lines, currentLine)
	}

	return strings.Join(lines, "\n")
}
