package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"runtime/debug"
	"strconv"
	"strings"

	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
	"golang.org/x/exp/slices"

	"github.com/osbuild/images/pkg/arch"
	"github.com/osbuild/images/pkg/bib/blueprintload"
	"github.com/osbuild/images/pkg/cloud"
	"github.com/osbuild/images/pkg/cloud/awscloud"
	"github.com/osbuild/images/pkg/container"
	"github.com/osbuild/images/pkg/dnfjson"
	"github.com/osbuild/images/pkg/experimentalflags"
	"github.com/osbuild/images/pkg/manifest"
	"github.com/osbuild/images/pkg/rpmmd"

	"github.com/particle-os/debian-bootc-image-builder/bib/internal/imagetypes"
	"github.com/particle-os/debian-bootc-image-builder/bib/internal/solver"
	debianpatch "github.com/particle-os/debian-bootc-image-builder/bib/internal/debian-patch"
	podman_container "github.com/osbuild/images/pkg/bib/container"
	"github.com/osbuild/images/pkg/bib/osinfo"

	"github.com/osbuild/image-builder-cli/pkg/progress"
	"github.com/osbuild/image-builder-cli/pkg/setup"
	"github.com/osbuild/image-builder-cli/pkg/util"
)

const (
	// As a baseline heuristic we double the size of
	// the input container to support in-place updates.
	// This is planned to be more configurable in the
	// future.
	containerSizeToDiskSizeMultiplier = 2
)

// all possible locations for the bib's distro definitions
// ./data/defs and ./bib/data/defs are for development
// /usr/share/bootc-image-builder/defs is for the production, containerized version
var distroDefPaths = []string{
	"./data/defs",
	"./bib/data/defs",
	"/usr/share/bootc-image-builder/defs",
}

var (
	osGetuid = os.Getuid
	osGetgid = os.Getgid

	osStdout = os.Stdout
	osStderr = os.Stderr
)

// canChownInPath checks if the ownership of files can be set in a given path.
func canChownInPath(path string) (bool, error) {
	info, err := os.Stat(path)
	if err != nil {
		return false, err
	}
	if !info.IsDir() {
		return false, fmt.Errorf("%s is not a directory", path)
	}

	checkFile, err := os.CreateTemp(path, ".writecheck")
	if err != nil {
		return false, err
	}
	defer func() {
		if err := os.Remove(checkFile.Name()); err != nil {
			// print the error message for info but don't error out
			fmt.Fprintf(os.Stderr, "error deleting %s: %s\n", checkFile.Name(), err.Error())
		}
	}()
	return checkFile.Chown(osGetuid(), osGetgid()) == nil, nil
}

func inContainerOrUnknown() bool {
	// no systemd-detect-virt, err on the side of container
	if _, err := exec.LookPath("systemd-detect-virt"); err != nil {
		return true
	}
	// exit code "0" means the container is detected
	err := exec.Command("systemd-detect-virt", "-c", "-q").Run()
	return err == nil
}

// getContainerSize returns the size of an already pulled container image in bytes
func getContainerSize(imgref string) (uint64, error) {
	output, err := exec.Command("podman", "image", "inspect", imgref, "--format", "{{.Size}}").Output()
	if err != nil {
		return 0, fmt.Errorf("failed inspect image: %w", util.OutputErr(err))
	}
	size, err := strconv.ParseUint(strings.TrimSpace(string(output)), 10, 64)
	if err != nil {
		return 0, fmt.Errorf("cannot parse image size: %w", err)
	}

	logrus.Debugf("container size: %v", size)
	return size, nil
}

func makeManifest(c *ManifestConfig, solver solver.Solver, cacheRoot string) (manifest.OSBuildManifest, map[string][]rpmmd.RepoConfig, error) {
	mani, err := Manifest(c)
	if err != nil {
		return nil, nil, fmt.Errorf("cannot get manifest: %w", err)
	}

	// depsolve packages using our Debian solver
	depsolvedSets := make(map[string]dnfjson.DepsolveResult)
	depsolvedRepos := make(map[string][]rpmmd.RepoConfig)
	for name, pkgSets := range mani.GetPackageSetChains() {
		// Extract packages from all PackageSets
		var allPackages []string
		for _, pkgSet := range pkgSets {
			allPackages = append(allPackages, pkgSet.Include...)
		}
		res, err := solver.Depsolve(allPackages, 0)
		if err != nil {
			return nil, nil, fmt.Errorf("cannot depsolve: %w", err)
		}
		// Convert the generic result to DNF types for compatibility
		if depsolveResult, ok := res.(*debianpatch.DebianDepsolveResult); ok {
			depsolvedSets[name] = debianpatch.ConvertDebianDepsolveResultToDNF(*depsolveResult)
			depsolvedRepos[name] = debianpatch.ConvertDebianDepsolveResultToDNF(*depsolveResult).Repos
		} else {
			return nil, nil, fmt.Errorf("unexpected depsolve result type: %T", res)
		}
	}

	// Resolve container - the normal case is that host and target
	// architecture are the same. However it is possible to build
	// cross-arch images by using qemu-user. This will run everything
	// (including the build-root) with the target arch then, it
	// is fast enough (given that it's mostly I/O and all I/O is
	// run naively via syscall translation)

	// XXX: should NewResolver() take "arch.Arch"?
	resolver := container.NewResolver(c.Architecture.String())

	containerSpecs := make(map[string][]container.Spec)
	for plName, sourceSpecs := range mani.GetContainerSourceSpecs() {
		for _, c := range sourceSpecs {
			resolver.Add(c)
		}
		specs, err := resolver.Finish()
		if err != nil {
			return nil, nil, fmt.Errorf("cannot resolve containers: %w", err)
		}
		for _, spec := range specs {
			if spec.Arch != c.Architecture {
				return nil, nil, fmt.Errorf("image found is for unexpected architecture %q (expected %q), if that is intentional, please make sure --target-arch matches", spec.Arch, c.Architecture)
			}
		}
		containerSpecs[plName] = specs
	}

	var opts manifest.SerializeOptions
	// Note: RpmDownloader is Red Hat specific, not used in Debian
	// We'll use the default downloader for Debian packages
	mf, err := mani.Serialize(depsolvedSets, containerSpecs, nil, &opts)
	if err != nil {
		return nil, nil, fmt.Errorf("[ERROR] manifest serialization failed: %s", err.Error())
	}
	return mf, depsolvedRepos, nil
}

func saveManifest(ms manifest.OSBuildManifest, fpath string) (err error) {
	b, err := json.MarshalIndent(ms, "", "  ")
	if err != nil {
		return fmt.Errorf("failed to marshal data for %q: %s", fpath, err.Error())
	}
	b = append(b, '\n') // add new line at end of file
	fp, err := os.Create(fpath)
	if err != nil {
		return fmt.Errorf("failed to create output file %q: %s", fpath, err.Error())
	}
	defer func() { err = errors.Join(err, fp.Close()) }()
	if _, err := fp.Write(b); err != nil {
		return fmt.Errorf("failed to write output file %q: %s", fpath, err.Error())
	}
	return nil
}

// manifestFromCobra generate an osbuild manifest from a cobra commandline.
//
// It takes an unstarted progres bar and will start it at the right
// point (it cannot be started yet to avoid the "podman pull" progress
// and our progress fighting). The caller is responsible for stopping
// the progress bar (this function cannot know what else needs to happen
// after manifest generation).
//
// TODO: provide a podman progress reader to integrate the podman progress
// into our progress.
func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.ProgressBar) ([]byte, *mTLSConfig, error) {
	cntArch := arch.Current()

	imgref := args[0]
	userConfigFile, _ := cmd.Flags().GetString("config")
	imgTypes, _ := cmd.Flags().GetStringArray("type")
	rpmCacheRoot, _ := cmd.Flags().GetString("rpmmd")
	targetArch, _ := cmd.Flags().GetString("target-arch")
	rootFs, _ := cmd.Flags().GetString("rootfs")
	buildImgref, _ := cmd.Flags().GetString("build-container")
	useLibrepo, _ := cmd.Flags().GetBool("use-librepo")

	// If --local was given, warn in the case of --local or --local=true (true is the default), error in the case of --local=false
	if cmd.Flags().Changed("local") {
		localStorage, _ := cmd.Flags().GetBool("local")
		if localStorage {
			fmt.Fprintf(os.Stderr, "WARNING: --local is now the default behavior, you can remove it from the command line\n")
		} else {
			return nil, nil, fmt.Errorf(`--local=false is no longer supported, remove it and make sure to pull the container before running bib:
	sudo podman pull %s`, imgref)
		}
	}

	if targetArch != "" {
		target, err := arch.FromString(targetArch)
		if err != nil {
			return nil, nil, err
		}
		if target != arch.Current() {
			// TODO: detect if binfmt_misc for target arch is
			// available, e.g. by mounting the binfmt_misc fs into
			// the container and inspects the files or by
			// including tiny statically linked target-arch
			// binaries inside our bib container
			fmt.Fprintf(os.Stderr, "WARNING: target-arch is experimental and needs an installed 'qemu-user' package\n")
			if slices.Contains(imgTypes, "iso") {
				return nil, nil, fmt.Errorf("cannot build iso for different target arches yet")
			}
			cntArch = target
		}
	}
	// TODO: add "target-variant", see https://github.com/osbuild/bootc-image-builder/pull/139/files#r1467591868

	if err := setup.ValidateHasContainerStorageMounted(); err != nil {
		return nil, nil, fmt.Errorf("could not access container storage, did you forget -v /var/lib/containers/storage:/var/lib/containers/storage? (%w)", err)
	}

	imageTypes, err := imagetypes.New(imgTypes...)
	if err != nil {
		return nil, nil, fmt.Errorf("cannot detect build types %v: %w", imgTypes, err)
	}

	config, err := blueprintload.LoadWithFallback(userConfigFile)
	if err != nil {
		return nil, nil, fmt.Errorf("cannot read config: %w", err)
	}

	pbar.SetPulseMsgf("Manifest generation step")
	pbar.Start()

	if err := setup.ValidateHasContainerTags(imgref); err != nil {
		return nil, nil, err
	}

	cntSize, err := getContainerSize(imgref)
	if err != nil {
		return nil, nil, fmt.Errorf("cannot get container size: %w", err)
	}
	container, err := podman_container.New(imgref)
	if err != nil {
		return nil, nil, err
	}
	defer func() {
		if err := container.Stop(); err != nil {
			logrus.Warnf("error stopping container: %v", err)
		}
	}()

	var rootfsType string
	if rootFs != "" {
		rootfsType = rootFs
	} else {
		rootfsType, err = container.DefaultRootfsType()
		if err != nil {
			return nil, nil, fmt.Errorf("cannot get rootfs type for container: %w", err)
		}
		if rootfsType == "" {
			return nil, nil, fmt.Errorf(`no default root filesystem type specified in container, please use "--rootfs" to set manually`)
		}
	}

	// Gather some data from the containers distro
	sourceinfo, err := osinfo.Load(container.Root())
	if err != nil {
		return nil, nil, err
	}

	buildContainer := container
	buildSourceinfo := sourceinfo
	startedBuildContainer := false
	defer func() {
		if startedBuildContainer {
			if err := buildContainer.Stop(); err != nil {
				logrus.Warnf("error stopping container: %v", err)
			}
		}
	}()

	if buildImgref != "" {
		buildContainer, err = podman_container.New(buildImgref)
		if err != nil {
			return nil, nil, err
		}
		startedBuildContainer = true

		// Gather some data from the containers distro
		buildSourceinfo, err = osinfo.Load(buildContainer.Root())
		if err != nil {
			return nil, nil, err
		}
	} else {
		buildImgref = imgref
	}

	// Create a Debian-focused solver
	debianSolver, err := solver.NewSolver(sourceinfo, rpmCacheRoot, cntArch)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to create Debian solver: %w", err)
	}

	manifestConfig := &ManifestConfig{
		Architecture:    cntArch,
		Config:          config,
		ImageTypes:      imageTypes,
		Imgref:          imgref,
		BuildImgref:     buildImgref,
		RootfsMinsize:   cntSize * containerSizeToDiskSizeMultiplier,
		DistroDefPaths:  distroDefPaths,
		SourceInfo:      sourceinfo,
		BuildSourceInfo: buildSourceinfo,
		RootFSType:      rootfsType,
		UseLibrepo:      useLibrepo,
	}

	manifest, repos, err := makeManifest(manifestConfig, debianSolver, rpmCacheRoot)
	if err != nil {
		return nil, nil, err
	}

	mTLS, err := extractTLSKeys(SimpleFileReader{}, repos)
	if err != nil {
		return nil, nil, err
	}

	return manifest, mTLS, nil
}

func cmdManifest(cmd *cobra.Command, args []string) error {
	pbar, err := progress.New("")
	if err != nil {
		// this should never happen
		return fmt.Errorf("cannot create progress bar: %w", err)
	}
	defer pbar.Stop()

	mf, _, err := manifestFromCobra(cmd, args, pbar)
	if err != nil {
		return fmt.Errorf("cannot generate manifest: %w", err)
	}
	fmt.Println(string(mf))
	return nil
}

func handleAWSFlags(cmd *cobra.Command) (cloud.Uploader, error) {
	imgTypes, _ := cmd.Flags().GetStringArray("type")
	region, _ := cmd.Flags().GetString("aws-region")
	if region == "" {
		return nil, nil
	}
	bucketName, _ := cmd.Flags().GetString("aws-bucket")
	imageName, _ := cmd.Flags().GetString("aws-ami-name")
	targetArch, _ := cmd.Flags().GetString("target-arch")

	if !slices.Contains(imgTypes, "ami") {
		return nil, fmt.Errorf("aws flags set for non-ami image type (type is set to %s)", strings.Join(imgTypes, ","))
	}

	// check as many permission prerequisites as possible before starting
	uploaderOpts := &awscloud.UploaderOptions{
		TargetArch: targetArch,
	}
	uploader, err := awscloud.NewUploader(region, bucketName, imageName, uploaderOpts)
	if err != nil {
		return nil, err
	}
	status := io.Discard
	if logrus.GetLevel() >= logrus.InfoLevel {
		status = os.Stderr
	}
	if err := uploader.Check(status); err != nil {
		return nil, err
	}
	return uploader, nil
}

// isDebianBasedImage checks if the given image is Debian-based
func isDebianBasedImage(imageName string) bool {
	// Check for common Debian-based image patterns
	debianPatterns := []string{
		"debian:", "ubuntu:", "particle-os:", "raspbian:",
		"localhost/debian", "localhost/ubuntu", "localhost/particle-os",
		"quay.io/debian", "quay.io/ubuntu",
	}
	
	for _, pattern := range debianPatterns {
		if strings.Contains(imageName, pattern) {
			return true
		}
	}
	
	// Default to true for localhost images (likely Debian-based)
	if strings.HasPrefix(imageName, "localhost/") {
		return true
	}
	
	return false
}

func cmdBuild(cmd *cobra.Command, args []string) error {
	chown, _ := cmd.Flags().GetString("chown")
	imgTypes, _ := cmd.Flags().GetStringArray("type")
	osbuildStore, _ := cmd.Flags().GetString("store")
	outputDir, _ := cmd.Flags().GetString("output")
	targetArch, _ := cmd.Flags().GetString("target-arch")
	progressType, _ := cmd.Flags().GetString("progress")
	// TODO: Implement debos and osbuild backend selection
	// useDebos, _ := cmd.Flags().GetBool("use-debos")
	// useOsbuild, _ := cmd.Flags().GetBool("use-osbuild")

	// Determine which backend to use
	// For now, always use osbuild backend since debos is not implemented
	// TODO: Implement debos backend support

	logrus.Debug("Validating environment")
	if err := setup.Validate(targetArch); err != nil {
		return fmt.Errorf("cannot validate the setup: %w", err)
	}
	logrus.Debug("Ensuring environment setup")
	switch inContainerOrUnknown() {
	case false:
		fmt.Fprintf(os.Stderr, "WARNING: running outside a container, this is an unsupported configuration\n")
	case true:
		if err := setup.EnsureEnvironment(osbuildStore); err != nil {
			return fmt.Errorf("cannot ensure the environment: %w", err)
		}
	}

	if err := os.MkdirAll(outputDir, 0o777); err != nil {
		return fmt.Errorf("cannot setup build dir: %w", err)
	}

	uploader, err := handleAWSFlags(cmd)
	if err != nil {
		return fmt.Errorf("cannot handle AWS setup: %w", err)
	}

	canChown, err := canChownInPath(outputDir)
	if err != nil {
		return fmt.Errorf("cannot ensure ownership: %w", err)
	}
	if !canChown && chown != "" {
		return fmt.Errorf("chowning is not allowed in output directory")
	}

	pbar, err := progress.New(progressType)
	if err != nil {
		return fmt.Errorf("cannto create progress bar: %w", err)
	}
	defer pbar.Stop()

	manifest_fname := fmt.Sprintf("manifest-%s.json", strings.Join(imgTypes, "-"))
	pbar.SetMessagef("Generating manifest %s", manifest_fname)
	mf, mTLS, err := manifestFromCobra(cmd, args, pbar)
	if err != nil {
		return fmt.Errorf("cannot build manifest: %w", err)
	}
	pbar.SetMessagef("Done generating manifest")

	// collect pipeline exports for each image type
	imageTypes, err := imagetypes.New(imgTypes...)
	if err != nil {
		return err
	}
	exports := imageTypes.Exports()
	manifestPath := filepath.Join(outputDir, manifest_fname)
	if err := saveManifest(mf, manifestPath); err != nil {
		return fmt.Errorf("cannot save manifest: %w", err)
	}

	pbar.SetPulseMsgf("Disk image building step")
	pbar.SetMessagef("Building %s", manifest_fname)

	var osbuildEnv []string
	if !canChown {
		// set export options for osbuild
		osbuildEnv = []string{"OSBUILD_EXPORT_FORCE_NO_PRESERVE_OWNER=1"}
	}

	if mTLS != nil {
		envVars, cleanup, err := prepareOsbuildMTLSConfig(mTLS)
		if err != nil {
			return fmt.Errorf("failed to prepare osbuild TLS keys: %w", err)
		}

		defer cleanup()

		osbuildEnv = append(osbuildEnv, envVars...)
	}

	if experimentalflags.Bool("debug-qemu-user") {
		osbuildEnv = append(osbuildEnv, "OBSBUILD_EXPERIMENAL=debug-qemu-user")
	}
	osbuildOpts := progress.OSBuildOptions{
		StoreDir:  osbuildStore,
		OutputDir: outputDir,
		ExtraEnv:  osbuildEnv,
	}
	if err = progress.RunOSBuild(pbar, mf, exports, &osbuildOpts); err != nil {
		return fmt.Errorf("cannot run osbuild: %w", err)
	}

	pbar.SetMessagef("Build complete!")
	if uploader != nil {
		// XXX: pass our own progress.ProgressBar here
		// *for now* just stop our own progress and let the uploadAMI
		// progress take over - but we really need to fix this in a
		// followup
		pbar.Stop()
		for idx, imgType := range imgTypes {
			switch imgType {
			case "ami":
				diskpath := filepath.Join(outputDir, exports[idx], "disk.raw")
				if err := upload(uploader, diskpath, cmd.Flags()); err != nil {
					return fmt.Errorf("cannot upload AMI: %w", err)
				}
			default:
				continue
			}
		}
	} else {
		pbar.SetMessagef("Results saved in %s", outputDir)
	}

	if err := chownR(outputDir, chown); err != nil {
		return fmt.Errorf("cannot setup owner for %q: %w", outputDir, err)
	}

	return nil
}

func chownR(path string, chown string) error {
	if chown == "" {
		return nil
	}
	errFmt := "cannot parse chown: %v"

	var gid int
	uidS, gidS, _ := strings.Cut(chown, ":")
	uid, err := strconv.Atoi(uidS)
	if err != nil {
		return fmt.Errorf(errFmt, err)
	}
	if gidS != "" {
		gid, err = strconv.Atoi(gidS)
		if err != nil {
			return fmt.Errorf(errFmt, err)
		}
	} else {
		gid = osGetgid()
	}

	return filepath.Walk(path, func(name string, info os.FileInfo, err error) error {
		if err == nil {
			err = os.Chown(name, uid, gid)
		}
		return err
	})
}

var rootLogLevel string

func rootPreRunE(cmd *cobra.Command, _ []string) error {
	verbose, _ := cmd.Flags().GetBool("verbose")
	progress, _ := cmd.Flags().GetString("progress")
	switch {
	case rootLogLevel != "":
		level, err := logrus.ParseLevel(rootLogLevel)
		if err != nil {
			return err
		}
		logrus.SetLevel(level)
	case verbose:
		logrus.SetLevel(logrus.InfoLevel)
	default:
		logrus.SetLevel(logrus.ErrorLevel)
	}
	if verbose && progress == "auto" {
		if err := cmd.Flags().Set("progress", "verbose"); err != nil {
			return err
		}
	}

	return nil
}

func versionFromBuildInfo() (string, error) {
	info, ok := debug.ReadBuildInfo()
	if !ok {
		return "", fmt.Errorf("cannot read build info")
	}
	var buildTainted bool
	gitRev := "unknown"
	buildTime := "unknown"
	for _, bs := range info.Settings {
		switch bs.Key {
		case "vcs.revision":
			gitRev = bs.Value[:7]
		case "vcs.time":
			buildTime = bs.Value
		case "vcs.modified":
			bT, err := strconv.ParseBool(bs.Value)
			if err != nil {
				logrus.Errorf("Error parsing 'vcs.modified': %v", err)
				bT = true
			}
			buildTainted = bT
		}
	}

	return fmt.Sprintf(`build_revision: %s
build_time: %s
build_tainted: %v
`, gitRev, buildTime, buildTainted), nil
}

func buildCobraCmdline() (*cobra.Command, error) {
	version, err := versionFromBuildInfo()
	if err != nil {
		return nil, err
	}

	rootCmd := &cobra.Command{
		Use:               "bootc-image-builder",
		Long:              "Create a bootable image from an ostree native container\n\n" +
			"Supports both debos (default for Debian) and osbuild backends.\n" +
			"Debos is automatically selected for Debian-based images.",
		PersistentPreRunE: rootPreRunE,
		SilenceErrors:     true,
		Version:           version,
	}
	rootCmd.SetVersionTemplate(version)

	rootCmd.PersistentFlags().StringVar(&rootLogLevel, "log-level", "", "logging level (debug, info, error); default error")
	rootCmd.PersistentFlags().BoolP("verbose", "v", false, `Switch to verbose mode`)

	buildCmd := &cobra.Command{
		Use:   "build IMAGE_NAME",
		Short: "Create a bootable image from a container (default command)",
		Long: "Create a bootable image from a container image.\n\n" +
			"Supports both debos (default for Debian) and osbuild backends.\n" +
			"Debos is automatically selected for Debian-based images.\n\n" +
			"(default action if no command is given)\n" +
			"IMAGE_NAME: container image to build into a bootable image",
		Args:                  cobra.ExactArgs(1),
		DisableFlagsInUseLine: true,
		RunE:                  cmdBuild,
		SilenceUsage:          true,
		Example: rootCmd.Use + " build quay.io/debian-bootc/debian-bootc:bookworm\n" +
			rootCmd.Use + " quay.io/debian-bootc/debian-bootc:bookworm\n" +
			rootCmd.Use + " build debian:trixie\n" +
			rootCmd.Use + " build --debos-suite bookworm debian:bookworm\n" +
			rootCmd.Use + " build --use-osbuild fedora:latest\n",
		Version: rootCmd.Version,
	}
	buildCmd.SetVersionTemplate(version)

	rootCmd.AddCommand(buildCmd)
	manifestCmd := &cobra.Command{
		Use:                   "manifest",
		Short:                 "Only create the manifest but don't build the image.",
		Args:                  cobra.ExactArgs(1),
		DisableFlagsInUseLine: true,
		RunE:                  cmdManifest,
		SilenceUsage:          true,
		Version:               rootCmd.Version,
	}
	manifestCmd.SetVersionTemplate(version)

	versionCmd := &cobra.Command{
		Use:          "version",
		Short:        "Show the version and quit",
		SilenceUsage: true,
		RunE: func(cmd *cobra.Command, args []string) error {
			root := cmd.Root()
			root.SetArgs([]string{"--version"})
			return root.Execute()
		},
	}

	rootCmd.AddCommand(versionCmd)

	rootCmd.AddCommand(manifestCmd)
	manifestCmd.Flags().Bool("tls-verify", false, "DEPRECATED: require HTTPS and verify certificates when contacting registries")
	if err := manifestCmd.Flags().MarkHidden("tls-verify"); err != nil {
		return nil, fmt.Errorf("cannot hide 'tls-verify' :%w", err)
	}
	manifestCmd.Flags().String("rpmmd", "/rpmmd", "rpm metadata cache directory")
	manifestCmd.Flags().String("target-arch", "", "build for the given target architecture (experimental)")
	manifestCmd.Flags().String("build-container", "", "Use a custom container for the image build")
	manifestCmd.Flags().StringArray("type", []string{"qcow2"}, fmt.Sprintf("image types to build [%s]", imagetypes.Available()))
	manifestCmd.Flags().Bool("local", true, "DEPRECATED: --local is now the default behavior, make sure to pull the container image before running bootc-image-builder")
	if err := manifestCmd.Flags().MarkHidden("local"); err != nil {
		return nil, fmt.Errorf("cannot hide 'local' :%w", err)
	}
	manifestCmd.Flags().String("rootfs", "", "Root filesystem type. If not given, the default configured in the source container image is used.")
	manifestCmd.Flags().Bool("use-librepo", true, "switch to librepo for pkg download, needs new enough osbuild")
	// --config is only useful for developers who run bib outside
	// of a container to generate a manifest. so hide it by
	// default from users.
	manifestCmd.Flags().String("config", "", "build config file; /config.json will be used if present")
	if err := manifestCmd.Flags().MarkHidden("config"); err != nil {
		return nil, fmt.Errorf("cannot hide 'config' :%w", err)
	}

	buildCmd.Flags().AddFlagSet(manifestCmd.Flags())
	buildCmd.Flags().String("aws-ami-name", "", "name for the AMI in AWS (only for type=ami)")
	buildCmd.Flags().String("aws-bucket", "", "target S3 bucket name for intermediate storage when creating AMI (only for type=ami)")
	buildCmd.Flags().String("aws-region", "", "target region for AWS uploads (only for type=ami)")
	buildCmd.Flags().String("chown", "", "chown the ouput directory to match the specified UID:GID")
	buildCmd.Flags().String("output", ".", "artifact output directory")
	buildCmd.Flags().String("store", "/store", "osbuild store for intermediate pipeline trees")
	//TODO: add json progress for higher level tools like "podman bootc"
	buildCmd.Flags().String("progress", "auto", "type of progress bar to use (e.g. verbose,term)")
	
	// Add debos-specific flags
	buildCmd.Flags().Bool("use-debos", false, "Use debos backend (default for Debian-based images)")
	buildCmd.Flags().Bool("use-osbuild", false, "Force use of osbuild backend instead of debos")
	buildCmd.Flags().String("debos-suite", "", "Override Debian suite detection (e.g., bookworm, trixie)")
	buildCmd.Flags().StringArray("debos-packages", []string{}, "Additional packages to install during debos build")
	buildCmd.Flags().Bool("debos-ostree", true, "Enable OSTree integration for bootc compatibility")
	buildCmd.Flags().String("debos-repository", "/ostree/repo", "OSTree repository path")
	buildCmd.Flags().String("debos-branch", "", "OSTree branch name (auto-generated if not specified)")
	buildCmd.Flags().Bool("debos-dry-run", false, "Perform a dry run without building (debos --dry-run)")
	
	// flag rules
	for _, dname := range []string{"output", "store", "rpmmd"} {
		if err := buildCmd.MarkFlagDirname(dname); err != nil {
			return nil, err
		}
	}
	if err := buildCmd.MarkFlagFilename("config"); err != nil {
		return nil, err
	}
	buildCmd.MarkFlagsRequiredTogether("aws-region", "aws-bucket", "aws-ami-name")

	// If no subcommand is given, assume the user wants to use the build subcommand
	// See https://github.com/spf13/cobra/issues/823#issuecomment-870027246
	// which cannot be used verbatim because the arguments for "build" like
	// "quay.io" will create an "err != nil". Ideally we could check err
	// for something like cobra.UnknownCommandError but cobra just gives
	// us an error string
	cmd, _, err := rootCmd.Find(os.Args[1:])
	injectBuildArg := func() {
		args := append([]string{buildCmd.Name()}, os.Args[1:]...)
		rootCmd.SetArgs(args)
	}
	// command not known, i.e. happens for "bib quay.io/debian/..."
	if err != nil && !slices.Contains([]string{"help", "completion"}, os.Args[1]) {
		injectBuildArg()
	}
			// command appears valid, e.g. "bib --local quay.io/debian" but this
	// is the parser just assuming "quay.io" is an argument for "--local" :(
	if err == nil && cmd.Use == rootCmd.Use && cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp {
		injectBuildArg()
	}

	return rootCmd, nil
}

func run() error {
	rootCmd, err := buildCobraCmdline()
	if err != nil {
		return err
	}

	return rootCmd.Execute()
}

func main() {
	if err := run(); err != nil {
		log.Fatalf("error: %s", err)
	}
}
