package buildtools

import (
	"celer/pkgs/cmd"
	"celer/pkgs/expr"
	"celer/pkgs/fileio"
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"strings"

	"github.com/BurntSushi/toml"
)

const msys2Packages = "msys2_packages.txt"

type msys2 struct {
	BuiltinPackages []string `toml:"builtin_packages"`

	// internal fields.
	availablePackages []string `toml:"-"`
	rootDir           string
}

// SetupMSYS2 checks and installs the required MSYS2 packages.
func SetupMSYS2(rootDir string, extraTools *[]string) error {
	// Remove none msys2:xxx from list.
	*extraTools = slices.DeleteFunc(*extraTools, func(element string) bool {
		return !strings.HasPrefix(element, "msys2:")
	})

	defer func() {
		// Remove msys2 related after setted up.
		*extraTools = slices.DeleteFunc(*extraTools, func(tool string) bool {
			return strings.HasPrefix(tool, "msys2:")
		})
	}()

	// Read toml file.
	bytes, err := static.ReadFile("static/msys2_builtin_packages.toml")
	if err != nil {
		return err
	}

	// Decode toml file.
	var msys2 = msys2{
		rootDir: rootDir,
	}
	if err := toml.Unmarshal(bytes, &msys2); err != nil {
		return err
	}

	// Merge tools for installation.
	msys2.BuiltinPackages = append(msys2.BuiltinPackages, *extraTools...)

	// Check and install packages.
	for _, item := range msys2.BuiltinPackages {
		item = strings.TrimPrefix(item, "msys2:")
		installed, err := msys2.checkIfInstalled(item)
		if err != nil {
			return fmt.Errorf("check if %s is installed: %s", item, err)
		}
		if installed {
			continue
		}

		if err := msys2.install(item); err != nil {
			return fmt.Errorf("install %s into msys2: %s", item, err)
		}
	}

	if err := msys2.removeConflictFiles(); err != nil {
		return fmt.Errorf("remove link.exe: %s", err)
	}

	return nil
}

// msys2's link.exe and perl.exe will conflict with the native environment, so remove it.
func (m msys2) removeConflictFiles() error {
	for _, file := range []string{"link.exe"} {
		if err := m.removeFile(file); err != nil {
			return err
		}
	}

	return nil
}

func (m msys2) checkIfInstalled(target string) (bool, error) {
	// Check if package is marked as installed.
	recorded, err := m.isMarkedAsInstalled(target)
	if err != nil {
		return false, fmt.Errorf("check if %s is marked as installed: %s", target, err)
	}
	if recorded {
		return true, nil
	}

	// Update msys2 packages if no package installed yet.
	if !fileio.PathExists(filepath.Join(m.rootDir, msys2Packages)) {
		executor := cmd.NewExecutor("[pacman -Syu]", "pacman --noconfirm -Syu")
		executor.MSYS2Env(true)
		if err := executor.Execute(); err != nil {
			return false, fmt.Errorf("update msys2: %s", err)
		}
	}

	// Check if group package.
	isGroup, err := m.isGroupPackage(target)
	if err != nil {
		return false, fmt.Errorf("check if %s is group package: %s", target, err)
	}

	// Check if package is installed.
	var installed bool
	if isGroup {
		installed, err = m.isGroupInstalled(target)
	} else {
		installed, err = m.isPackageInstalled(target)
	}
	if err != nil {
		return false, fmt.Errorf("check if %s is installed: %s", target, err)
	}

	// Record state if missing.
	if err := m.markAsInstalled(target); err != nil {
		return false, err
	}

	return installed, nil
}

func (m msys2) install(target string) error {
	title := fmt.Sprintf("[pacman -S %s]", target)
	executor := cmd.NewExecutor(title, "pacman --noconfirm -S "+target)
	executor.MSYS2Env(true)
	if err := executor.Execute(); err != nil {
		return fmt.Errorf("install %s into msys2: %s", target, err)
	}

	// Record installed state.
	if err := m.markAsInstalled(target); err != nil {
		return fmt.Errorf("record installed state: %s", err)
	}

	return nil
}

func (m msys2) markAsInstalled(target string) error {
	var newCreated bool
	recordFilePath := filepath.Join(m.rootDir, msys2Packages)
	if fileio.PathExists(recordFilePath) {
		newCreated = false

		// Read file.
		bytes, err := os.ReadFile(recordFilePath)
		if err != nil {
			return fmt.Errorf("read msys2_package.txt: %s", err)
		}

		// Check if the package is already installed.
		pkgNames := strings.Split(string(bytes), "\n")
		if slices.Contains(pkgNames, "group:"+target) || slices.Contains(pkgNames, "package:"+target) {
			return nil
		}
	} else {
		newCreated = true
	}

	// Open or create file for writing.
	pkgFile, err := os.OpenFile(recordFilePath, os.O_CREATE|os.O_RDWR|os.O_APPEND, os.ModePerm)
	if err != nil {
		return fmt.Errorf("open msys2_package.txt: %s", err)
	}
	defer pkgFile.Close()

	// Check if the package is group.
	isGroupd, err := m.isGroupPackage(target)
	if err != nil {
		return fmt.Errorf("check if %s is group package: %s", target, err)
	}

	// Write package name into file.
	prefix := expr.If(newCreated, "# Do not modify it, it's generated by celer!\n", "")
	pkgType := expr.If(isGroupd, "group", "package")
	content := fmt.Sprintf("%s%s:%s\n", prefix, pkgType, target)
	if _, err := pkgFile.WriteString(content); err != nil {
		return fmt.Errorf("write msys2_package.txt: %s", err)
	}

	return nil
}

func (m msys2) isMarkedAsInstalled(target string) (bool, error) {
	// Check if msys2_packages.txt exists.
	packagesFile := filepath.Join(m.rootDir, "msys2_packages.txt")
	if !fileio.PathExists(packagesFile) {
		return false, nil
	}

	// Check if the package is already installed.
	bytes, err := os.ReadFile(packagesFile)
	if err != nil {
		return false, fmt.Errorf("read msys2_package.txt: %s", err)
	}
	packages := strings.Split(string(bytes), "\n")
	if slices.Contains(packages, "group:"+target) || slices.Contains(packages, "package:"+target) {
		return true, nil
	}

	return false, nil
}

func (m msys2) isPackageInstalled(target string) (bool, error) {
	executor := cmd.NewExecutor("[pacman query install state]", "pacman -Q "+target)
	executor.MSYS2Env(true)
	if err := executor.Execute(); err != nil {
		return false, nil // msys2 return error if the package is not installed.
	}

	return true, nil
}

func (m msys2) isGroupInstalled(target string) (bool, error) {
	packages, err := m.queryGroupPackages(target)
	if err != nil {
		return false, fmt.Errorf("query group packages: %s", err)
	}

	for _, pkg := range packages {
		installed, err := m.isPackageInstalled(pkg)
		if err != nil {
			return false, fmt.Errorf("check if %s is installed: %s", pkg, err)
		}

		if !installed {
			return false, nil
		}
	}

	return true, nil
}

func (m msys2) queryGroupPackages(group string) ([]string, error) {
	executor := cmd.NewExecutor("[pacman query group packages]", "pacman -Sg "+group)
	executor.MSYS2Env(true)
	output, err := executor.ExecuteOutput()
	if err != nil {
		return nil, fmt.Errorf("query group packages: %s", err)
	}

	lines := strings.Split(output, "\n")
	var packages []string
	for _, line := range lines {
		if strings.HasPrefix(line, "#") || strings.TrimSpace(line) == "" {
			continue
		}
		packages = append(packages, strings.Split(line, " ")[1])
	}
	return packages, nil
}

func (m *msys2) isGroupPackage(target string) (bool, error) {
	// Query all available packages and cache them.
	if m.availablePackages == nil {
		executor := cmd.NewExecutor("[pacman list all groups]", "pacman -Sg")
		executor.MSYS2Env(true)
		output, err := executor.ExecuteOutput()
		if err != nil {
			return false, fmt.Errorf("query available groups: %s", err)
		}

		m.availablePackages = strings.Split(output, "\n")
	}

	// Check if the target is one of the available groups.
	if slices.Contains(m.availablePackages, target) {
		return true, nil
	}

	return false, nil
}

func (m msys2) removeFile(file string) error {
	pattern := filepath.ToSlash(filepath.Join(m.rootDir, "/*/usr/bin/"+file))
	matches, err := filepath.Glob(pattern)
	if err != nil {
		return fmt.Errorf("glob syntax\n %w", err)
	}

	for _, match := range matches {
		if err := os.Remove(match); err != nil {
			return fmt.Errorf("remove %s\n %w", file, err)
		}
	}

	return nil
}
