package main

import (
	"bufio"
	"bytes"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"

	"tailscale.com/cmd/mirage-qt/internal/debpack"
)

type buildConfig struct {
	repoRoot   string
	version    string
	arch       string
	stageDir   string
	outDir     string
	qtBuildDir string
	outputPath string
}

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func run() error {
	cwd, err := os.Getwd()
	if err != nil {
		return err
	}
	repoRootDefault, err := findRepoRoot(cwd)
	if err != nil {
		return err
	}

	repoRoot := flag.String("repo-root", repoRootDefault, "repo root containing VERSION.txt and cmd/mirage-qt")
	version := flag.String("version", "", "package version (default: VERSION.txt)")
	arch := flag.String("arch", "", "debian package architecture (default: dpkg --print-architecture)")
	stageDir := flag.String("stage-dir", "", "staging root for package contents")
	outDir := flag.String("out-dir", "", "output directory for .deb")
	qtBuildDir := flag.String("qt-build-dir", "", "Qt build directory")
	flag.Parse()

	cfg, err := resolveBuildConfig(*repoRoot, *version, *arch, *stageDir, *outDir, *qtBuildDir)
	if err != nil {
		return err
	}
	if err := prepareDirs(cfg); err != nil {
		return err
	}

	layout := debpack.DefaultLayout()
	if err := buildCLI(cfg, layout); err != nil {
		return err
	}
	if err := buildDaemon(cfg, layout); err != nil {
		return err
	}
	if err := buildLauncher(cfg, layout); err != nil {
		return err
	}
	if err := buildQtUI(cfg, layout); err != nil {
		return err
	}
	if err := stageDesktopEntry(cfg, layout); err != nil {
		return err
	}
	if err := stageAppIcon(cfg, layout); err != nil {
		return err
	}
	if err := stageServiceUnit(cfg, layout); err != nil {
		return err
	}
	if err := stageDefaultsFile(cfg, layout); err != nil {
		return err
	}
	if err := stageControlFiles(cfg); err != nil {
		return err
	}
	if err := buildDeb(cfg); err != nil {
		return err
	}

	fmt.Println(cfg.outputPath)
	return nil
}

func resolveBuildConfig(repoRoot, version, arch, stageDir, outDir, qtBuildDir string) (*buildConfig, error) {
	root := strings.TrimSpace(repoRoot)
	if root == "" {
		return nil, errors.New("repo root is required")
	}
	root, err := filepath.Abs(root)
	if err != nil {
		return nil, err
	}
	if version == "" {
		version, err = readVersion(root)
		if err != nil {
			return nil, err
		}
	}
	if arch == "" {
		arch, err = detectDebArch()
		if err != nil {
			return nil, err
		}
	}
	if stageDir == "" {
		stageDir = filepath.Join(root, "cmd", "mirage-qt", "build", "deb-stage")
	}
	if outDir == "" {
		outDir = filepath.Join(root, "cmd", "mirage-qt", "build", "deb")
	}
	if qtBuildDir == "" {
		qtBuildDir = filepath.Join(root, "cmd", "mirage-qt", "build", "qt-package")
	}
	outputPath := filepath.Join(outDir, fmt.Sprintf("%s_%s_%s.deb", debpack.DefaultPackageName, version, arch))

	return &buildConfig{
		repoRoot:   root,
		version:    version,
		arch:       arch,
		stageDir:   stageDir,
		outDir:     outDir,
		qtBuildDir: qtBuildDir,
		outputPath: outputPath,
	}, nil
}

func findRepoRoot(start string) (string, error) {
	dir := start
	for {
		if fileExists(filepath.Join(dir, "VERSION.txt")) && dirExists(filepath.Join(dir, "cmd", "mirage-qt")) {
			return dir, nil
		}
		parent := filepath.Dir(dir)
		if parent == dir {
			return "", errors.New("could not locate repo root from current directory")
		}
		dir = parent
	}
}

func readVersion(repoRoot string) (string, error) {
	data, err := os.ReadFile(filepath.Join(repoRoot, "VERSION.txt"))
	if err != nil {
		return "", fmt.Errorf("read VERSION.txt: %w", err)
	}
	version := strings.TrimSpace(string(data))
	if version == "" {
		return "", errors.New("VERSION.txt is empty")
	}
	return version, nil
}

func detectDebArch() (string, error) {
	if out, err := runCommand("", nil, "dpkg", "--print-architecture"); err == nil {
		return strings.TrimSpace(string(out)), nil
	}
	switch runtime.GOARCH {
	case "amd64":
		return "amd64", nil
	case "arm64":
		return "arm64", nil
	default:
		return "", fmt.Errorf("unsupported architecture %q; please pass -arch", runtime.GOARCH)
	}
}

func prepareDirs(cfg *buildConfig) error {
	if err := os.RemoveAll(cfg.stageDir); err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Join(cfg.stageDir, "DEBIAN"), 0o755); err != nil {
		return err
	}
	if err := os.MkdirAll(cfg.outDir, 0o755); err != nil {
		return err
	}
	return nil
}

func buildCLI(cfg *buildConfig, layout debpack.Layout) error {
	target := stageTarget(cfg.stageDir, layout.CLIPath)
	if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
		return err
	}
	_, err := runCommand(cfg.repoRoot, nil, "go", "build", "-o", target, "./cmd/mirage")
	if err != nil {
		return fmt.Errorf("build mirage CLI: %w", err)
	}
	return nil
}

func buildDaemon(cfg *buildConfig, layout debpack.Layout) error {
	target := stageTarget(cfg.stageDir, layout.DaemonPath)
	if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
		return err
	}
	_, err := runCommand(cfg.repoRoot, nil, "go", "build", "-o", target, "./cmd/miraged")
	if err != nil {
		return fmt.Errorf("build miraged daemon: %w", err)
	}
	return nil
}

func buildLauncher(cfg *buildConfig, layout debpack.Layout) error {
	target := stageTarget(cfg.stageDir, layout.LauncherPath)
	if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
		return err
	}
	_, err := runCommand(cfg.repoRoot, nil, "go", "build", "-o", target, "./cmd/mirage-qt")
	if err != nil {
		return fmt.Errorf("build launcher: %w", err)
	}
	return nil
}

func buildQtUI(cfg *buildConfig, layout debpack.Layout) error {
	qtSrcDir := filepath.Join(cfg.repoRoot, "cmd", "mirage-qt", "qt")
	if err := os.MkdirAll(cfg.qtBuildDir, 0o755); err != nil {
		return err
	}
	if _, err := runCommand(cfg.repoRoot, nil, "cmake", "-S", qtSrcDir, "-B", cfg.qtBuildDir); err != nil {
		return fmt.Errorf("configure Qt UI: %w", err)
	}
	if _, err := runCommand(cfg.repoRoot, nil, "cmake", "--build", cfg.qtBuildDir, "--target", "mirage-qt-ui", "-j4"); err != nil {
		return fmt.Errorf("build Qt UI: %w", err)
	}

	builtUI := filepath.Join(cfg.qtBuildDir, "mirage-qt-ui")
	target := stageTarget(cfg.stageDir, layout.UIPath)
	if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
		return err
	}
	return copyFile(builtUI, target, 0o755)
}

func stageDesktopEntry(cfg *buildConfig, layout debpack.Layout) error {
	source := filepath.Join(cfg.repoRoot, "cmd", "mirage-qt", "qt", "mirage-qt-ui.desktop")
	target := stageTarget(cfg.stageDir, layout.DesktopPath)
	if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
		return err
	}
	return copyFile(source, target, 0o644)
}

func stageAppIcon(cfg *buildConfig, layout debpack.Layout) error {
	source := filepath.Join(cfg.repoRoot, "cmd", "mirage", "logos", "logo.png")
	target := stageTarget(cfg.stageDir, layout.AppIconPath)
	if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
		return err
	}
	return copyFile(source, target, 0o644)
}

func stageServiceUnit(cfg *buildConfig, layout debpack.Layout) error {
	target := stageTarget(cfg.stageDir, layout.ServicePath)
	if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
		return err
	}
	return os.WriteFile(target, []byte(debpack.ServiceFile()), 0o644)
}

func stageDefaultsFile(cfg *buildConfig, layout debpack.Layout) error {
	target := stageTarget(cfg.stageDir, layout.DefaultsPath)
	if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
		return err
	}
	return os.WriteFile(target, []byte(debpack.DefaultsFile()), 0o644)
}

func stageControlFiles(cfg *buildConfig) error {
	depends, err := computeDepends(cfg)
	if err != nil {
		return err
	}
	control := debpack.ControlFile(debpack.ControlMetadata{
		Package:      debpack.DefaultPackageName,
		Version:      cfg.version,
		Architecture: cfg.arch,
		Depends:      append(depends, "policykit-1", "xdg-utils"),
		Maintainer:   "Mirage",
	})
	controlPath := filepath.Join(cfg.stageDir, "DEBIAN", "control")
	if err := os.WriteFile(controlPath, []byte(control), 0o644); err != nil {
		return err
	}
	if err := os.WriteFile(filepath.Join(cfg.stageDir, "DEBIAN", "postinst"), []byte(debpack.PostinstScript()), 0o755); err != nil {
		return err
	}
	if err := os.WriteFile(filepath.Join(cfg.stageDir, "DEBIAN", "postrm"), []byte(debpack.PostrmScript()), 0o755); err != nil {
		return err
	}
	return nil
}

func computeDepends(cfg *buildConfig) ([]string, error) {
	layout := debpack.DefaultLayout()
	cli := stageTarget(cfg.stageDir, layout.CLIPath)
	daemon := stageTarget(cfg.stageDir, layout.DaemonPath)
	ui := stageTarget(cfg.stageDir, layout.UIPath)
	launcher := stageTarget(cfg.stageDir, layout.LauncherPath)
	tmpDir, err := os.MkdirTemp("", "mirage-qt-shlibdeps-")
	if err != nil {
		return nil, err
	}
	defer os.RemoveAll(tmpDir)
	if err := os.MkdirAll(filepath.Join(tmpDir, "debian"), 0o755); err != nil {
		return nil, err
	}
	controlStub := "Source: mirage-qt\nSection: net\nPriority: optional\nMaintainer: Mirage\nStandards-Version: 4.6.0\nPackage: mirage-qt\nArchitecture: any\nDescription: Mirage Linux tray client\n"
	if err := os.WriteFile(filepath.Join(tmpDir, "debian", "control"), []byte(controlStub), 0o644); err != nil {
		return nil, err
	}
	out, err := runCommand(tmpDir, nil, "dpkg-shlibdeps", "-O", "-e"+ui, "-e"+launcher, "-e"+cli, "-e"+daemon)
	if err != nil {
		return nil, fmt.Errorf("dpkg-shlibdeps: %w", err)
	}
	line := strings.TrimSpace(string(out))
	const prefix = "shlibs:Depends="
	if !strings.HasPrefix(line, prefix) {
		return nil, fmt.Errorf("unexpected dpkg-shlibdeps output: %q", line)
	}
	parts := strings.Split(strings.TrimSpace(strings.TrimPrefix(line, prefix)), ",")
	var deps []string
	for _, part := range parts {
		part = strings.TrimSpace(part)
		if part != "" {
			deps = append(deps, part)
		}
	}
	return deps, nil
}

func buildDeb(cfg *buildConfig) error {
	if _, err := runCommand(cfg.repoRoot, nil, "fakeroot", "dpkg-deb", "--build", "--root-owner-group", cfg.stageDir, cfg.outputPath); err != nil {
		return fmt.Errorf("build .deb: %w", err)
	}
	return nil
}

func stageTarget(stageRoot, installPath string) string {
	return filepath.Join(stageRoot, strings.TrimPrefix(installPath, "/"))
}

func runCommand(dir string, env []string, name string, args ...string) ([]byte, error) {
	cmd := exec.Command(name, args...)
	cmd.Dir = dir
	if len(env) > 0 {
		cmd.Env = append(os.Environ(), env...)
	}
	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	if err := cmd.Run(); err != nil {
		msg := strings.TrimSpace(stderr.String())
		if msg == "" {
			msg = strings.TrimSpace(stdout.String())
		}
		if msg != "" {
			return nil, fmt.Errorf("%s %s: %w: %s", name, strings.Join(args, " "), err, msg)
		}
		return nil, fmt.Errorf("%s %s: %w", name, strings.Join(args, " "), err)
	}
	return stdout.Bytes(), nil
}

func copyFile(src, dst string, mode fs.FileMode) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()

	out, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode)
	if err != nil {
		return err
	}
	defer out.Close()

	if _, err := io.Copy(out, bufio.NewReader(in)); err != nil {
		return err
	}
	return out.Close()
}

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

func dirExists(path string) bool {
	info, err := os.Stat(path)
	return err == nil && info.IsDir()
}
