package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"os/signal"
	"path/filepath"

	"strings"
	"syscall"

	"github.com/spf13/cobra"

	"github.com/osbuild/image-builder-cli/pkg/progress"
	"github.com/osbuild/images/pkg/arch"
	"github.com/osbuild/images/pkg/customizations/subscription"
	"github.com/osbuild/images/pkg/imagefilter"
	"github.com/osbuild/images/pkg/osbuild"
	"github.com/osbuild/images/pkg/ostree"

	"github.com/osbuild/image-builder-cli/internal/blueprintload"
	"github.com/osbuild/image-builder-cli/internal/olog"
)

var (
	osStdout io.Writer = os.Stdout
	osStderr io.Writer = os.Stderr
)

// basenameFor returns the basename for directory and filenames
// for the given imageType. This can be user overriden via userBasename.
func basenameFor(img *imagefilter.Result, userBasename string) string {
	if userBasename != "" {
		// If the user provided a basename that already has the
		// image extension just strip that off. I.e. when
		// we get "foo.qcow2" for a qcow out basename is just
		// "foo". This is mostly for convenience for our users.
		//
		// This code assumes that all our ImgType filesnames have
		// $name.$ext.$extraExt (e.g. disk.qcow2 or disk.raw.xz)
		l := strings.SplitN(img.ImgType.Filename(), ".", 2)
		if len(l) > 1 && l[1] != "" {
			imgExt := fmt.Sprintf(".%s", l[1])
			userBasename = strings.TrimSuffix(userBasename, imgExt)
		}
		return userBasename
	}
	return fmt.Sprintf("%s-%s-%s", img.Distro.Name(), img.ImgType.Name(), img.Arch.Name())
}

func cmdListImages(cmd *cobra.Command, args []string) error {
	filter, err := cmd.Flags().GetStringArray("filter")
	if err != nil {
		return err
	}
	format, err := cmd.Flags().GetString("format")
	if err != nil {
		return err
	}
	dataDir, err := cmd.Flags().GetString("data-dir")
	if err != nil {
		return err
	}
	extraRepos, err := cmd.Flags().GetStringArray("extra-repo")
	if err != nil {
		return err
	}

	return listImages(dataDir, extraRepos, format, filter)
}

func ostreeImageOptions(cmd *cobra.Command) (*ostree.ImageOptions, error) {
	imageRef, err := cmd.Flags().GetString("ostree-ref")
	if err != nil {
		return nil, err
	}
	parentRef, err := cmd.Flags().GetString("ostree-parent")
	if err != nil {
		return nil, err
	}
	url, err := cmd.Flags().GetString("ostree-url")
	if err != nil {
		return nil, err
	}
	if imageRef == "" && parentRef == "" && url == "" {
		return nil, nil
	}

	// XXX: how to add RHSM?
	return &ostree.ImageOptions{
		ImageRef:  imageRef,
		ParentRef: parentRef,
		URL:       url,
	}, nil
}

type registrations struct {
	Redhat struct {
		Subscription *subscription.ImageOptions `json:"subscription,omitempty"`
	} `json:"redhat,omitempty"`
}

func subscriptionImageOptions(cmd *cobra.Command) (*subscription.ImageOptions, error) {
	regFilePath, err := cmd.Flags().GetString("registrations")
	if err != nil {
		return nil, err
	}
	if regFilePath == "" {
		return nil, nil
	}

	f, err := os.Open(regFilePath)
	if err != nil {
		return nil, fmt.Errorf("cannot open registrations file: %w", err)
	}
	defer f.Close()

	// XXX: support yaml eventually
	var regs registrations
	dec := json.NewDecoder(f)
	dec.DisallowUnknownFields()
	if err := dec.Decode(&regs); err != nil {
		return nil, fmt.Errorf("cannot parse registrations file: %w", err)
	}
	return regs.Redhat.Subscription, nil
}

type cmdManifestWrapperOptions struct {
	useBootstrapIfNeeded bool
}

func cmdManifestWrapper(pbar progress.ProgressBar, cmd *cobra.Command, args []string, w io.Writer, wd io.Writer, wrapperOpts *cmdManifestWrapperOptions) (*imagefilter.Result, error) {
	if wrapperOpts == nil {
		wrapperOpts = &cmdManifestWrapperOptions{}
	}
	dataDir, err := cmd.Flags().GetString("data-dir")
	if err != nil {
		return nil, err
	}
	extraRepos, err := cmd.Flags().GetStringArray("extra-repo")
	if err != nil {
		return nil, err
	}
	forceRepos, err := cmd.Flags().GetStringArray("force-repo")
	if err != nil {
		return nil, err
	}
	archStr, err := cmd.Flags().GetString("arch")
	if err != nil {
		return nil, err
	}
	if archStr == "" {
		archStr = arch.Current().String()
	}
	distroStr, err := cmd.Flags().GetString("distro")
	if err != nil {
		return nil, err
	}
	withSBOM, err := cmd.Flags().GetBool("with-sbom")
	if err != nil {
		return nil, err
	}
	ignoreWarnings, err := cmd.Flags().GetBool("ignore-warnings")
	if err != nil {
		return nil, err
	}
	outputDir, err := cmd.Flags().GetString("output-dir")
	if err != nil {
		return nil, err
	}
	ostreeImgOpts, err := ostreeImageOptions(cmd)
	if err != nil {
		return nil, err
	}
	useLibrepo, err := cmd.Flags().GetBool("use-librepo")
	if err != nil {
		return nil, err
	}
	var rpmDownloader osbuild.RpmDownloader
	if useLibrepo {
		rpmDownloader = osbuild.RpmDownloaderLibrepo
	}
	blueprintPath, err := cmd.Flags().GetString("blueprint")
	if err != nil {
		return nil, err
	}
	var customSeed *int64
	if cmd.Flags().Changed("seed") {
		seedFlagVal, err := cmd.Flags().GetInt64("seed")
		if err != nil {
			return nil, err
		}
		customSeed = &seedFlagVal
	}
	subscription, err := subscriptionImageOptions(cmd)
	if err != nil {
		return nil, err
	}
	// no error check here as this is (deliberately) not defined on
	// "manifest" (if "images" learn to set the output filename in
	// manifests we would change this
	outputFilename, _ := cmd.Flags().GetString("output-name")

	bp, err := blueprintload.Load(blueprintPath)
	if err != nil {
		return nil, err
	}

	distroStr, err = findDistro(distroStr, bp.Distro)
	if err != nil {
		return nil, err
	}
	imgTypeStr := args[0]
	pbar.SetPulseMsgf("Manifest generation step")
	pbar.SetMessagef("Building manifest for %s-%s", distroStr, imgTypeStr)

	repoOpts := &repoOptions{
		DataDir:    dataDir,
		ExtraRepos: extraRepos,
		ForceRepos: forceRepos,
	}
	img, err := getOneImage(distroStr, imgTypeStr, archStr, repoOpts)
	if err != nil {
		return nil, err
	}
	if len(img.ImgType.Exports()) > 1 {
		return nil, fmt.Errorf("image %q has multiple exports: this is current unsupport: please report this as a bug", basenameFor(img, ""))
	}

	opts := &manifestOptions{
		OutputDir:      outputDir,
		OutputFilename: outputFilename,
		BlueprintPath:  blueprintPath,
		Ostree:         ostreeImgOpts,
		RpmDownloader:  rpmDownloader,
		WithSBOM:       withSBOM,
		IgnoreWarnings: ignoreWarnings,
		CustomSeed:     customSeed,
		Subscription:   subscription,

		ForceRepos: forceRepos,
	}
	opts.UseBootstrapContainer = wrapperOpts.useBootstrapIfNeeded && (img.Arch.Name() != arch.Current().String())
	if opts.UseBootstrapContainer {
		fmt.Fprintf(os.Stderr, "WARNING: using experimental cross-architecture building to build %q\n", img.Arch.Name())
	}

	err = generateManifest(dataDir, extraRepos, img, w, wd, opts)
	return img, err
}

func cmdManifest(cmd *cobra.Command, args []string) error {
	pbar, err := progress.New("")
	if err != nil {
		return err
	}
	_, err = cmdManifestWrapper(pbar, cmd, args, osStdout, io.Discard, nil)
	return err
}

func progressFromCmd(cmd *cobra.Command) (progress.ProgressBar, error) {
	progressType, err := cmd.Flags().GetString("progress")
	if err != nil {
		return nil, err
	}
	verbose, err := cmd.Flags().GetBool("verbose")
	if err != nil {
		return nil, err
	}
	if progressType == "auto" && verbose {
		progressType = "verbose"
	}

	return progress.New(progressType)
}

// listOutputdir will return a string with the output dir content.
// Any errors will also just appear as part of the string (as it is
// purely informational)
func listOutputdir(path string) string {
	ents, err := filepath.Glob(filepath.Join(path, "/*"))
	if err != nil {
		return fmt.Sprintf("error: %v", err)
	}
	return strings.Join(ents, ",")
}

func cmdBuild(cmd *cobra.Command, args []string) error {
	cacheDir, err := cmd.Flags().GetString("cache")
	if err != nil {
		return err
	}
	outputDir, err := cmd.Flags().GetString("output-dir")
	if err != nil {
		return err
	}
	outputBasename, err := cmd.Flags().GetString("output-name")
	if err != nil {
		return err
	}
	withManifest, err := cmd.Flags().GetBool("with-manifest")
	if err != nil {
		return err
	}
	withBuildlog, err := cmd.Flags().GetBool("with-buildlog")
	if err != nil {
		return err
	}
	// XXX: check env here, i.e. if user is root and osbuild is installed

	pbar, err := progressFromCmd(cmd)
	if err != nil {
		return err
	}
	pbar.Start()
	defer pbar.Stop()
	go func() {
		c := make(chan os.Signal, 1)
		signal.Notify(c, os.Interrupt, syscall.SIGTERM)
		<-c
		pbar.Stop()
	}()

	var mf bytes.Buffer
	opts := &cmdManifestWrapperOptions{
		useBootstrapIfNeeded: true,
	}

	// We discard any warnings from the depsolver until we figure out a better
	// idea (likely in manifestgen)
	res, err := cmdManifestWrapper(pbar, cmd, args, &mf, io.Discard, opts)
	if err != nil {
		return err
	}

	bootMode := res.ImgType.BootMode()
	uploader, err := uploaderFor(cmd, res.ImgType.Name(), res.ImgType.Arch().Name(), &bootMode)
	if errors.Is(err, ErrUploadTypeUnsupported) || errors.Is(err, ErrUploadConfigNotProvided) {
		err = nil
	}

	if err != nil {
		return err
	}

	if uploader != nil {
		pbar.SetPulseMsgf("Checking cloud access")
		if err := uploaderCheckWithProgress(pbar, uploader); err != nil {
			return err
		}
	}
	outputDir = basenameFor(res, outputDir)

	buildOpts := &buildOptions{
		OutputDir:      outputDir,
		OutputBasename: outputBasename,
		StoreDir:       cacheDir,
		WriteManifest:  withManifest,
		WriteBuildlog:  withBuildlog,
	}
	pbar.SetPulseMsgf("Image building step")
	imagePath, err := buildImage(pbar, res, mf.Bytes(), buildOpts)
	if err != nil {
		return err
	}
	pbar.Stop()

	fmt.Fprintf(osStdout, "Image build successful, results:\n%s\n", listOutputdir(outputDir))

	if uploader != nil {
		// XXX: integrate better into the progress, see bib
		if err := uploadImageWithProgress(uploader, imagePath); err != nil {
			return err
		}
	}

	return nil
}

func cmdDescribeImg(cmd *cobra.Command, args []string) error {
	// XXX: boilderplate identical to cmdManifest() above
	dataDir, err := cmd.Flags().GetString("data-dir")
	if err != nil {
		return err
	}
	distroStr, err := cmd.Flags().GetString("distro")
	if err != nil {
		return err
	}
	archStr, err := cmd.Flags().GetString("arch")
	if err != nil {
		return err
	}
	if archStr == "" {
		archStr = arch.Current().String()
	}

	distroStr, err = findDistro(distroStr, "")
	if err != nil {
		return err
	}

	imgTypeStr := args[0]
	res, err := getOneImage(distroStr, imgTypeStr, archStr, &repoOptions{DataDir: dataDir})
	if err != nil {
		return err
	}

	return describeImage(res, osStdout)
}

func run() error {
	// Initialize console logger (stderr, no prefix)
	log.SetFlags(0)

	rootCmd := &cobra.Command{
		Use:   "image-builder",
		Short: "Build operating system images from a given distro/image-type/blueprint",
		Long: `Build operating system images from a given distribution,
image-type and blueprint.

Image-builder builds operating system images for a range of predefined
operating systems like Fedora, CentOS and RHEL with easy customizations support.`,
		SilenceErrors: true,
		Version:       prettyVersion(),
		CompletionOptions: cobra.CompletionOptions{
			HiddenDefaultCmd: true,
		},
	}
	rootCmd.SetVersionTemplate(prettyVersion())

	rootCmd.PersistentFlags().String("data-dir", "", `Override the default data directory for e.g. custom repositories/*.json data`)
	rootCmd.PersistentFlags().StringArray("extra-repo", nil, `Add an extra repository during build (will *not* be gpg checked and not be part of the final image)`)
	rootCmd.PersistentFlags().StringArray("force-repo", nil, `Override the base repositories during build (these will not be part of the final image)`)
	rootCmd.PersistentFlags().String("output-dir", "", `Put output into the specified directory`)
	rootCmd.PersistentFlags().BoolP("verbose", "v", false, `Switch to verbose mode (more logging on stderr and verbose progress)`)
	rootCmd.SetOut(osStdout)
	rootCmd.SetErr(osStderr)

	listCmd := &cobra.Command{
		Use:          "list",
		Short:        "List buildable images, use --filter to limit further",
		RunE:         cmdListImages,
		SilenceUsage: true,
		Args:         cobra.NoArgs,
		Aliases:      []string{"list-images"},
	}
	listCmd.Flags().StringArray("filter", nil, `Filter distributions by a specific criteria (e.g. "type:iot*")`)
	listCmd.Flags().String("format", "", "Output in a specific format (text, json)")
	rootCmd.AddCommand(listCmd)

	manifestCmd := &cobra.Command{
		Use:          "manifest <image-type>",
		Short:        "Build manifest for the given image-type, e.g. qcow2 (tip: combine with --distro, --arch)",
		RunE:         cmdManifest,
		SilenceUsage: true,
		Args:         cobra.ExactArgs(1),
		Hidden:       true,
	}
	manifestCmd.Flags().String("blueprint", "", `filename of a blueprint to customize an image`)
	manifestCmd.Flags().Int64("seed", 0, `rng seed, some values are derived randomly, pinning the seed allows more reproducibility if you need it. must be an integer. only used when changed.`)
	manifestCmd.Flags().String("arch", "", `build manifest for a different architecture`)
	manifestCmd.Flags().String("distro", "", `build manifest for a different distroname (e.g. centos-9)`)
	manifestCmd.Flags().String("ostree-ref", "", `OSTREE reference`)
	manifestCmd.Flags().String("ostree-parent", "", `OSTREE parent`)
	manifestCmd.Flags().String("ostree-url", "", `OSTREE url`)
	manifestCmd.Flags().Bool("use-librepo", true, `use librepo to download packages (disable if you use old versions of osbuild)`)
	manifestCmd.Flags().Bool("with-sbom", false, `export SPDX SBOM document`)
	manifestCmd.Flags().Bool("ignore-warnings", false, `ignore warnings during manifest generation`)
	manifestCmd.Flags().String("registrations", "", `filename of a registrations file with e.g. subscription details`)
	rootCmd.AddCommand(manifestCmd)

	uploadCmd := &cobra.Command{
		Use:          "upload <image-path>",
		Short:        "Upload the given image from <image-path>",
		RunE:         cmdUpload,
		SilenceUsage: true,
		Args:         cobra.ExactArgs(1),
	}
	uploadCmd.Flags().String("aws-ami-name", "", "name for the AMI in AWS (only for type=ami)")
	uploadCmd.Flags().String("aws-bucket", "", "target S3 bucket name for intermediate storage when creating AMI (only for type=ami)")
	uploadCmd.Flags().String("aws-region", "", "target region for AWS uploads (only for type=ami)")
	uploadCmd.Flags().String("arch", "", "upload for the given architecture")
	rootCmd.AddCommand(uploadCmd)

	buildCmd := &cobra.Command{
		Use:          "build <image-type>",
		Short:        "Build the given image-type, e.g. qcow2 (tip: combine with --distro, --arch)",
		RunE:         cmdBuild,
		SilenceUsage: true,
		Args:         cobra.ExactArgs(1),
	}
	buildCmd.Flags().AddFlagSet(manifestCmd.Flags())
	buildCmd.Flags().Bool("with-manifest", false, `export osbuild manifest`)
	buildCmd.Flags().Bool("with-buildlog", false, `export osbuild buildlog`)
	// XXX: add --rpmmd cache too and put under /var/cache/image-builder/dnf
	buildCmd.Flags().String("cache", "/var/cache/image-builder/store", `osbuild directory to cache intermediate build artifacts"`)
	// XXX: add "--verbose" here, similar to how bib is doing this
	// (see https://github.com/osbuild/bootc-image-builder/pull/790/commits/5cec7ffd8a526e2ca1e8ada0ea18f927695dfe43)
	buildCmd.Flags().String("progress", "auto", "type of progress bar to use (e.g. verbose,term)")
	buildCmd.Flags().String("output-name", "", "set specific output basename")
	rootCmd.AddCommand(buildCmd)
	buildCmd.Flags().AddFlagSet(uploadCmd.Flags())

	// Enhanced build command with multi-format support
	enhancedBuildCmd := &cobra.Command{
		Use:          "enhanced-build <image-type>",
		Short:        "Enhanced build with multi-format support and blueprint management",
		RunE:         cmdEnhancedBuild,
		SilenceUsage: true,
		Args:         cobra.ExactArgs(1),
	}
	enhancedBuildCmd.Flags().AddFlagSet(manifestCmd.Flags())
	enhancedBuildCmd.Flags().StringArray("formats", nil, "output formats (qcow2, raw, vmdk, iso, tar, container)")
	enhancedBuildCmd.Flags().Bool("validate-only", false, "only validate blueprint, don't build")
	enhancedBuildCmd.Flags().Bool("with-manifest", false, `export osbuild manifest`)
	enhancedBuildCmd.Flags().Bool("with-buildlog", false, `export osbuild buildlog`)
	enhancedBuildCmd.Flags().String("cache", "/var/cache/image-builder/store", `osbuild directory to cache intermediate build artifacts"`)
	enhancedBuildCmd.Flags().String("output-name", "", "set specific output basename")
	rootCmd.AddCommand(enhancedBuildCmd)

	// Blueprint management commands
	blueprintCmd := &cobra.Command{
		Use:          "blueprint",
		Short:        "Manage Debian blueprints",
		SilenceUsage: true,
		Args:         cobra.MinimumNArgs(1),
	}
	
	blueprintCreateCmd := &cobra.Command{
		Use:          "create <name> <variant> <architecture>",
		Short:        "Create a new Debian blueprint",
		RunE:         cmdBlueprintCreate,
		SilenceUsage: true,
		Args:         cobra.ExactArgs(3),
	}
	blueprintCreateCmd.Flags().String("output", "", "output file path")
	blueprintCreateCmd.Flags().String("format", "yaml", "output format (yaml, json)")
	blueprintCmd.AddCommand(blueprintCreateCmd)
	
	blueprintValidateCmd := &cobra.Command{
		Use:          "validate <blueprint-file>",
		Short:        "Validate a Debian blueprint",
		RunE:         cmdBlueprintValidate,
		SilenceUsage: true,
		Args:         cobra.ExactArgs(1),
	}
	blueprintCmd.AddCommand(blueprintValidateCmd)
	
	blueprintListCmd := &cobra.Command{
		Use:          "list [directory]",
		Short:        "List available blueprints",
		RunE:         cmdBlueprintList,
		SilenceUsage: true,
		Args:         cobra.MaximumNArgs(1),
	}
	blueprintCmd.AddCommand(blueprintListCmd)
	
	blueprintShowCmd := &cobra.Command{
		Use:          "show <blueprint-file>",
		Short:        "Show blueprint details",
		RunE:         cmdBlueprintShow,
		SilenceUsage: true,
		Args:         cobra.ExactArgs(1),
	}
	blueprintCmd.AddCommand(blueprintShowCmd)
	
	rootCmd.AddCommand(blueprintCmd)
	// add after the rest of the uploadCmd flag set is added to avoid
	// that build gets a "--to" parameter
	uploadCmd.Flags().String("to", "", "upload to the given cloud")

	// XXX: add --format=json too?
	describeImgCmd := &cobra.Command{
		Use:          "describe <image-type>",
		Short:        "Describe the given image-type, e.g. qcow2 (tip: combine with --distro,--arch)",
		RunE:         cmdDescribeImg,
		SilenceUsage: true,
		Args:         cobra.ExactArgs(1),
		Hidden:       false,
		Aliases:      []string{"describe-image"},
	}
	describeImgCmd.Flags().String("arch", "", `use the different architecture`)
	describeImgCmd.Flags().String("distro", "", `build manifest for a different distroname (e.g. centos-9)`)

	rootCmd.AddCommand(describeImgCmd)

	verbose, err := rootCmd.PersistentFlags().GetBool("verbose")
	if err != nil {
		return err
	}
	if verbose {
		olog.SetDefault(log.New(os.Stderr, "", 0))
		// XXX: add once images has olog support
		//images_log.SetDefault(log.New(os.Stderr, "", 0))
	}

	return rootCmd.Execute()
}

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