package updates

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"
)

// Version represents a semantic version
type Version struct {
	Major      int
	Minor      int
	Patch      int
	Prerelease string
	Build      string
}

// VersionInfo contains detailed version information
type VersionInfo struct {
	Version        string `json:"version"`
	Build          string `json:"build"`
	Runtime        string `json:"runtime"`
	Channel        string `json:"channel,omitempty"`
	IsDocker       bool   `json:"isDocker"`
	IsSourceBuild  bool   `json:"isSourceBuild"`
	IsDevelopment  bool   `json:"isDevelopment"`
	DeploymentType string `json:"deploymentType"`
}

// ParseVersion parses a version string into a Version struct
func ParseVersion(versionStr string) (*Version, error) {
	// Remove 'v' prefix if present
	versionStr = strings.TrimPrefix(versionStr, "v")

	// Regular expression for semantic versioning
	re := regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)(?:-([^+]+))?(?:\+(.+))?$`)
	matches := re.FindStringSubmatch(versionStr)

	if len(matches) == 0 {
		return nil, fmt.Errorf("invalid version format: %s", versionStr)
	}

	major, _ := strconv.Atoi(matches[1])
	minor, _ := strconv.Atoi(matches[2])
	patch, _ := strconv.Atoi(matches[3])

	return &Version{
		Major:      major,
		Minor:      minor,
		Patch:      patch,
		Prerelease: matches[4],
		Build:      matches[5],
	}, nil
}

// String returns the string representation of the version
func (v *Version) String() string {
	version := fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
	if v.Prerelease != "" {
		version += "-" + v.Prerelease
	}
	if v.Build != "" {
		version += "+" + v.Build
	}
	return version
}

// Compare compares two versions
// Returns:
//
//	-1 if v < other
//	 0 if v == other
//	 1 if v > other
func (v *Version) Compare(other *Version) int {
	if v.Major != other.Major {
		return compareInts(v.Major, other.Major)
	}
	if v.Minor != other.Minor {
		return compareInts(v.Minor, other.Minor)
	}
	if v.Patch != other.Patch {
		return compareInts(v.Patch, other.Patch)
	}

	// Handle prerelease versions
	if v.Prerelease == "" && other.Prerelease != "" {
		return 1 // v is release, other is prerelease
	}
	if v.Prerelease != "" && other.Prerelease == "" {
		return -1 // v is prerelease, other is release
	}
	if v.Prerelease != other.Prerelease {
		// Try to parse RC numbers for comparison
		vRC := extractRCNumber(v.Prerelease)
		otherRC := extractRCNumber(other.Prerelease)
		if vRC >= 0 && otherRC >= 0 {
			return compareInts(vRC, otherRC)
		}
		return strings.Compare(v.Prerelease, other.Prerelease)
	}

	return 0
}

// IsNewerThan returns true if v is newer than other
func (v *Version) IsNewerThan(other *Version) bool {
	return v.Compare(other) > 0
}

// IsPrerelease returns true if this is a prerelease version
func (v *Version) IsPrerelease() bool {
	return v.Prerelease != ""
}

// GetCurrentVersion gets the current running version
func GetCurrentVersion() (*VersionInfo, error) {
	buildInfo := func(raw string, build string, isDev bool) *VersionInfo {
		normalized := normalizeVersionString(raw)
		return &VersionInfo{
			Version:        normalized,
			Build:          build,
			Runtime:        "go",
			Channel:        detectChannelFromVersion(normalized),
			IsDevelopment:  isDev,
			IsDocker:       isDockerEnvironment(),
			IsSourceBuild:  isSourceBuildEnvironment(),
			DeploymentType: GetDeploymentType(),
		}
	}

	if gitVersion, err := getGitVersion(); err == nil && gitVersion != "" {
		return buildInfo(gitVersion, "development", true), nil
	}

	// Try to read from VERSION file first (release builds)
	versionPaths := []string{
		"VERSION",
		"/opt/pulse/VERSION",
		filepath.Join(filepath.Dir(os.Args[0]), "VERSION"),
	}

	for _, path := range versionPaths {
		versionBytes, err := os.ReadFile(path)
		if err == nil {
			if raw := strings.TrimSpace(string(versionBytes)); raw != "" {
				return buildInfo(raw, "release", false), nil
			}
		}
	}

	// Fall back to git (development builds)
	gitVersion, err := getGitVersion()
	if err == nil && gitVersion != "" {
		return buildInfo(gitVersion, "development", true), nil
	}

	// Final fallback
	return buildInfo("4.26.0", "release", false), nil
}

// normalizeVersionString ensures any version string can be parsed as semantic version.
// Release builds are returned unchanged, while git describe strings and branch names are
// converted to a safe 0.0.0-based prerelease format.
func normalizeVersionString(version string) string {
	version = strings.TrimSpace(version)
	version = strings.TrimPrefix(version, "v")

	if version == "" {
		return "0.0.0-dev"
	}

	if normalized, ok := normalizeGitDescribeVersion(version); ok {
		return normalized
	}

	if _, err := ParseVersion(version); err == nil {
		return version
	}

	return fmt.Sprintf("0.0.0-%s", sanitizePrereleaseIdentifier(version))
}

var gitDescribeRegex = regexp.MustCompile(`^(\d+\.\d+\.\d+(?:-[0-9A-Za-z\.-]+)?)-(\d+)-g([0-9a-fA-F]+)(-dirty)?$`)

func normalizeGitDescribeVersion(version string) (string, bool) {
	matches := gitDescribeRegex.FindStringSubmatch(version)
	if matches == nil {
		return "", false
	}

	base := matches[1]
	if _, err := ParseVersion(base); err != nil {
		return "", false
	}

	build := fmt.Sprintf("git.%s.g%s", matches[2], strings.ToLower(matches[3]))
	if matches[4] != "" {
		build += ".dirty"
	}

	return fmt.Sprintf("%s+%s", base, build), true
}

func sanitizePrereleaseIdentifier(raw string) string {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return "dev"
	}

	var b strings.Builder
	lastSep := false

	for _, r := range raw {
		switch {
		case r >= '0' && r <= '9',
			r >= 'A' && r <= 'Z',
			r >= 'a' && r <= 'z':
			b.WriteRune(r)
			lastSep = false
		case r == '-' || r == '.':
			if lastSep {
				continue
			}
			b.WriteRune(r)
			lastSep = true
		default:
			if !lastSep {
				b.WriteRune('-')
				lastSep = true
			}
		}
	}

	clean := strings.Trim(b.String(), "-.")
	if clean == "" {
		return "dev"
	}
	return strings.ToLower(clean)
}

func detectChannelFromVersion(version string) string {
	versionLower := strings.ToLower(version)
	if strings.Contains(versionLower, "rc") {
		return "rc"
	}
	return "stable"
}

// getGitVersion gets version information from git
func getGitVersion() (string, error) {
	// Get the latest tag
	cmd := exec.Command("git", "describe", "--tags", "--always", "--dirty")
	output, err := cmd.Output()
	if err != nil {
		return "", err
	}

	version := strings.TrimSpace(string(output))
	if version == "" {
		return "", fmt.Errorf("no version found")
	}

	return version, nil
}

// isDockerEnvironment checks if running in Docker
func isDockerEnvironment() bool {
	// Check for Docker-specific files
	if fileExists("/.dockerenv") {
		return true
	}

	// Check cgroup for Docker - with timeout to prevent hanging
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel()

	cmd := exec.CommandContext(ctx, "cat", "/proc/1/cgroup")
	data, err := cmd.Output()
	if err == nil && strings.Contains(string(data), "docker") {
		return true
	}

	return false
}

// isSourceBuildEnvironment checks if running from a source build
func isSourceBuildEnvironment() bool {
	markerPaths := []string{
		"BUILD_FROM_SOURCE",
		"/opt/pulse/BUILD_FROM_SOURCE",
		filepath.Join(filepath.Dir(os.Args[0]), "BUILD_FROM_SOURCE"),
	}

	for _, path := range markerPaths {
		if fileExists(path) {
			return true
		}
	}

	return false
}

// fileExists checks if a file exists
func fileExists(path string) bool {
	// Use os.Stat instead of exec.Command for better performance and reliability
	_, err := os.Stat(path)
	return err == nil
}

// GetDeploymentType determines how Pulse was deployed
func GetDeploymentType() string {
	// Check if running in Docker
	if isDockerEnvironment() {
		return "docker"
	}

	if isSourceBuildEnvironment() {
		return "source"
	}

	// Check for ProxmoxVE LXC installation (has update command)
	if fileExists("/bin/update") {
		// Read file directly instead of using exec.Command
		data, err := os.ReadFile("/bin/update")
		if err == nil && (strings.Contains(string(data), "pulse.sh") || strings.Contains(string(data), "install.sh")) {
			return "proxmoxve"
		}
	}

	// Check for systemd service to determine installation type
	if fileExists("/etc/systemd/system/pulse-backend.service") {
		// Read file directly instead of using exec.Command
		data, err := os.ReadFile("/etc/systemd/system/pulse-backend.service")
		if err == nil {
			content := string(data)
			if strings.Contains(content, "User=pulse") && strings.Contains(content, "/opt/pulse/bin/pulse") {
				return "proxmoxve"
			}
		}
		return "systemd"
	}

	if fileExists("/etc/systemd/system/pulse.service") {
		return "systemd"
	}

	// Development or manual run
	if strings.Contains(os.Args[0], "go-build") || fileExists(".git") {
		return "development"
	}

	return "manual"
}

// compareInts compares two integers
func compareInts(a, b int) int {
	if a < b {
		return -1
	}
	if a > b {
		return 1
	}
	return 0
}

// extractRCNumber extracts the RC number from a prerelease string like "rc.9" or "rc9"
func extractRCNumber(prerelease string) int {
	re := regexp.MustCompile(`rc\.?(\d+)`)
	matches := re.FindStringSubmatch(strings.ToLower(prerelease))
	if len(matches) > 1 {
		num, err := strconv.Atoi(matches[1])
		if err == nil {
			return num
		}
	}
	return -1
}
