package main

import (
	"archive/tar"
	"compress/gzip"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/towgo/towgo/lib/system"
	g "tgk-touch/internal/global"
)

const firefoxInstallMarkerName = ".firefox-install-ok"

var forbiddenFirefoxDebPackages = map[string]struct{}{
	"base-files":   {},
	"base-passwd":  {},
	"bash":         {},
	"coreutils":    {},
	"dash":         {},
	"debconf":      {},
	"dpkg":         {},
	"gcc-8-base":   {},
	"libblkid1":    {},
	"libc6":        {},
	"libgcc1":      {},
	"libmount1":    {},
	"libselinux1":  {},
	"libsystemd0":  {},
	"libudev1":     {},
	"login":        {},
	"mount":        {},
	"passwd":       {},
	"perl-base":    {},
	"sudo":         {},
	"systemd":      {},
	"systemd-sysv": {},
	"tar":          {},
	"udev":         {},
	"util-linux":   {},
}

var optionalFirefoxDebPackages = map[string]string{
	"lubuntu-icon-theme": "theme package may conflict with existing adwaita-icon-theme-full and is not required by Firefox",
}

type firefoxBundleInstallPlan struct {
	TotalPackageCount  int
	HasFirefoxPackage  bool
	BaseSystemPackages []string
}

func isFirefoxUsable() bool {
	cmd := exec.Command("firefox", "--version")
	out, err := cmd.CombinedOutput()
	ok := err == nil
	kioskInfof("firefox usability check ok=%v output=%q err=%v", ok, strings.TrimSpace(string(out)), err)
	return ok
}

func isFirefoxInstalled() bool {
	if path, err := exec.LookPath("firefox"); err == nil {
		kioskDebugf("firefox install path check ok=true path=%s source=PATH", path)
		return true
	}

	for _, path := range []string{
		"/usr/bin/firefox",
		"/usr/local/bin/firefox",
		"/snap/bin/firefox",
	} {
		if isExecutableFile(path) {
			kioskDebugf("firefox install path check ok=true path=%s source=fallback", path)
			return true
		}
	}

	kioskDebugf("firefox install path check ok=false")
	return false
}

func isExecutableFile(path string) bool {
	info, err := os.Stat(path)
	return err == nil && !info.IsDir() && info.Mode()&0111 != 0
}

func installFirefox() error {
	bundlePath := firefoxBundlePath()
	plan, err := validateFirefoxBundle(bundlePath)
	if err != nil {
		kioskErrorf("firefox install refused bundle=%s err=%v", bundlePath, err)
		return err
	}
	baseSystemPackages := strings.Join(plan.BaseSystemPackages, " ")
	optionalSkipPackages := strings.Join(optionalFirefoxDebPackageNames(), " ")
	kioskInfof("firefox install package filter baseSystemPackages=%q optionalSkipPackages=%q", baseSystemPackages, optionalSkipPackages)

	var p struct {
		DebPkgPath       string `json:"DebPkgPath"`
		DebPkgName       string `json:"DebPkgName"`
		InstallShellPath string `json:"InstallShellPath"`
	}
	p.DebPkgPath = g.Config().Firefox.DebPkgPath
	p.DebPkgName = g.Config().Firefox.DebPkgName
	p.InstallShellPath = g.Config().Firefox.InstallShellPath
	kioskInfof("firefox install begin programPath=%s bundle=%s debPath=%s debName=%s installShell=%s", system.GetPathOfProgram(), bundlePath, p.DebPkgPath, p.DebPkgName, p.InstallShellPath)

	command := exec.Command("sudo", "sh", "-c", fmt.Sprintf(`
echo "=== current work directory ==="
pwd
echo ""

echo "=== keep existing Firefox package ==="
echo "skip automatic dpkg purge to avoid touching base system packages"
echo ""

echo "=== clean previous offline bundle files ==="
rm -rf /opt/firefox-deb /opt/firefox-installable /opt/firefox-deb.tar.gz 2>/dev/null
echo ""

echo "=== start Firefox install ==="
echo "1. copy offline bundle to /opt..."
cp -v %s /opt/firefox-deb.tar.gz || { echo "error: copy offline bundle failed"; exit 1; }
echo ""

echo "2. extract offline bundle..."
cd /opt && tar -xzvf firefox-deb.tar.gz || { echo "error: extract offline bundle failed"; exit 1; }
echo ""

echo "3. filter Firefox deb packages..."
base_system_packages=%s
optional_skip_packages=%s
mkdir -p /opt/firefox-installable
selected_count=0
skipped_installed_count=0
skipped_optional_count=0
selected_missing_base_count=0
selected_regular_count=0
firefox_selected=0
select_deb() {
  cp -v "$deb" /opt/firefox-installable/
  selected_count=$((selected_count + 1))
}
package_name_from_deb() {
  pkg=$(dpkg-deb -f "$1" Package 2>/dev/null || true)
  if [ -n "$pkg" ]; then
    echo "$pkg"
    return 0
  fi
  file=$(basename "$1")
  echo "$file" | cut -d '_' -f 1 | sed 's/[.]deb$//'
}
for deb in /opt/firefox-deb/*.deb; do
  [ -e "$deb" ] || continue
  file=$(basename "$deb")
  pkg=$(package_name_from_deb "$deb")
  if [ -z "$pkg" ]; then
    echo "skip package with empty name: $file"
    continue
  fi
  echo "package candidate: $file package=$pkg"

  status=$(dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null || true)
  installed=0
  if [ "$status" = "install ok installed" ]; then
    installed=1
  fi

  if [ "$pkg" = "firefox" ]; then
    select_deb
    firefox_selected=1
    continue
  fi

  case " $optional_skip_packages " in
    *" $pkg "*)
      echo "skip optional package: $file package=$pkg"
      skipped_optional_count=$((skipped_optional_count + 1))
      continue
      ;;
  esac

  case " $base_system_packages " in
    *" $pkg "*)
      if [ "$installed" -eq 1 ]; then
        echo "skip installed base system package: $file"
        skipped_installed_count=$((skipped_installed_count + 1))
        continue
      fi
      echo "select missing base system package: $file"
      select_deb
      selected_missing_base_count=$((selected_missing_base_count + 1))
      continue
      ;;
  esac

  if [ "$installed" -eq 1 ]; then
    echo "skip installed package: $file"
    skipped_installed_count=$((skipped_installed_count + 1))
    continue
  fi

  select_deb
  selected_regular_count=$((selected_regular_count + 1))
done
echo "selected packages: $selected_count, selected missing base: $selected_missing_base_count, selected regular: $selected_regular_count, skipped installed: $skipped_installed_count, skipped optional: $skipped_optional_count"
if [ "$selected_count" -eq 0 ]; then
  echo "error: no packages selected for install"
  exit 1
fi
if [ "$firefox_selected" -ne 1 ]; then
  echo "error: Firefox deb package was not selected"
  exit 1
fi
echo ""

echo "4. install selected Firefox packages..."
cd /opt/firefox-installable || { echo "error: enter installable package directory failed"; exit 1; }
install_status=0
sudo dpkg -i ./*.deb || install_status=$?
if [ "$install_status" -ne 0 ]; then
  echo "warning: selected Firefox deb package install returned status $install_status, verify Firefox next"
fi
echo ""

echo "5. verify Firefox..."
firefox --version || { echo "error: Firefox verification failed"; exit 1; }
echo ""

echo "=== Firefox install complete ==="
`, shellQuote(bundlePath), shellQuote(baseSystemPackages), shellQuote(optionalSkipPackages)))
	command.Stdout = os.Stdout
	command.Stderr = os.Stderr

	err = command.Run()
	if err != nil {
		kioskErrorf("firefox install command failed err=%v", err)
		return err
	}
	kioskInfof("firefox install command finished successfully")
	return nil
}

func firefoxInstallMarkerPath() string {
	return filepath.Join(system.GetPathOfProgram(), "data", firefoxInstallMarkerName)
}

func hasFirefoxInstallMarker() bool {
	path := firefoxInstallMarkerPath()
	info, err := os.Stat(path)
	ok := err == nil && !info.IsDir()
	kioskInfof("firefox install marker check path=%s ok=%v err=%v", path, ok, err)
	return ok
}

func writeFirefoxInstallMarker(reason string) error {
	path := firefoxInstallMarkerPath()
	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
		kioskErrorf("create firefox install marker dir failed path=%s err=%v", filepath.Dir(path), err)
		return err
	}

	content := fmt.Sprintf("reason=%s\ntime=%s\n", reason, time.Now().Format(time.RFC3339))
	if err := os.WriteFile(path, []byte(content), 0644); err != nil {
		kioskErrorf("write firefox install marker failed path=%s err=%v", path, err)
		return err
	}
	kioskInfof("firefox install marker written path=%s reason=%s", path, reason)
	return nil
}

func firefoxBundlePath() string {
	return filepath.Join(system.GetPathOfProgram(), g.Config().Firefox.DebPkgPath, g.Config().Firefox.DebPkgName)
}

func validateFirefoxBundle(bundlePath string) (firefoxBundleInstallPlan, error) {
	packages, err := firefoxBundlePackageNames(bundlePath)
	if err != nil {
		return firefoxBundleInstallPlan{}, fmt.Errorf("read firefox bundle packages: %w", err)
	}
	if len(packages) == 0 {
		return firefoxBundleInstallPlan{}, fmt.Errorf("firefox bundle has no deb packages")
	}

	plan := firefoxBundleInstallPlanFromPackages(packages)
	if !plan.HasFirefoxPackage {
		return firefoxBundleInstallPlan{}, fmt.Errorf("firefox bundle has no firefox deb package")
	}

	if len(plan.BaseSystemPackages) > 0 {
		kioskWarnf("firefox bundle contains base system packages; installed ones will be skipped and missing ones may be installed packages=%s", strings.Join(plan.BaseSystemPackages, ", "))
	}
	kioskInfof("firefox bundle validation passed bundle=%s totalPackages=%d baseSystemPackages=%d hasFirefox=%v", bundlePath, plan.TotalPackageCount, len(plan.BaseSystemPackages), plan.HasFirefoxPackage)
	return plan, nil
}

func firefoxBundleInstallPlanFromPackages(packages []string) firefoxBundleInstallPlan {
	plan := firefoxBundleInstallPlan{
		TotalPackageCount:  len(packages),
		BaseSystemPackages: unsafeFirefoxDebPackages(packages),
	}
	for _, name := range packages {
		if name == "firefox" {
			plan.HasFirefoxPackage = true
		}
	}
	return plan
}

func firefoxBundlePackageNames(bundlePath string) ([]string, error) {
	file, err := os.Open(bundlePath)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	gzipReader, err := gzip.NewReader(file)
	if err != nil {
		return nil, err
	}
	defer gzipReader.Close()

	reader := tar.NewReader(gzipReader)
	var packages []string
	for {
		header, err := reader.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, err
		}
		if header == nil || header.FileInfo().IsDir() || !strings.HasSuffix(header.Name, ".deb") {
			continue
		}
		name := debPackageName(filepath.Base(header.Name))
		if name != "" {
			packages = append(packages, name)
		}
	}
	sort.Strings(packages)
	return packages, nil
}

func debPackageName(fileName string) string {
	name := strings.TrimSuffix(fileName, ".deb")
	if packageName, _, ok := strings.Cut(name, "_"); ok {
		return packageName
	}
	return name
}

func unsafeFirefoxDebPackages(packages []string) []string {
	seen := make(map[string]struct{})
	for _, name := range packages {
		if _, blocked := forbiddenFirefoxDebPackages[name]; blocked {
			seen[name] = struct{}{}
		}
	}

	blocked := make([]string, 0, len(seen))
	for name := range seen {
		blocked = append(blocked, name)
	}
	sort.Strings(blocked)
	return blocked
}

func optionalFirefoxDebPackageNames() []string {
	names := make([]string, 0, len(optionalFirefoxDebPackages))
	for name := range optionalFirefoxDebPackages {
		names = append(names, name)
	}
	sort.Strings(names)
	return names
}

func shellQuote(value string) string {
	return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'"
}
