package main

import (
	"errors"
	"fmt"
	"io"
	"slices"
	"strings"

	"gopkg.in/yaml.v3"

	"github.com/osbuild/blueprint/pkg/blueprint"
	"github.com/osbuild/images/pkg/disk"
	"github.com/osbuild/images/pkg/distro"
	"github.com/osbuild/images/pkg/distro/defs"
	"github.com/osbuild/images/pkg/imagefilter"
	"github.com/osbuild/images/pkg/manifest"
	"github.com/osbuild/images/pkg/ostree"
)

// Use yaml output by default because it is both nicely human and
// machine readable and parts of our image defintions will be written
// in yaml too.  This means this should be a possible input a
// "flattended" image definiton.
type describeImgYAML struct {
	Distro string `yaml:"distro"`
	Type   string `yaml:"type"`
	Arch   string `yaml:"arch"`

	// XXX: think about ordering (as this is what the user will see)
	OsVersion string `yaml:"os_version"`

	Bootmode        string `yaml:"bootmode"`
	PartitionType   string `yaml:"partition_type"`
	DefaultFilename string `yaml:"default_filename"`

	BuildPipelines   []string                 `yaml:"build_pipelines"`
	PayloadPipelines []string                 `yaml:"payload_pipelines"`
	Packages         map[string]*packagesYAML `yaml:"packages"`

	PartitionTable *disk.PartitionTable `yaml:"partition_table,omitempty"`
}

type packagesYAML struct {
	Include []string `yaml:"include"`
	Exclude []string `yaml:"exclude"`
}

func dummyManifestFor(imgType distro.ImageType) (*manifest.Manifest, error) {
	var bp blueprint.Blueprint
	// XXX: '*-simplified-installer' images require the installation device to be specified as a BP customization.
	// Workaround this for now by setting a dummy device. We should ideally have a way to get image type pkg sets
	// without doing this.
	if strings.HasSuffix(imgType.Name(), "-simplified-installer") {
		bp.Customizations = &blueprint.Customizations{
			InstallationDevice: "/dev/dummy",
		}
	}

	var imgOpts distro.ImageOptions
	// Mock ostree options for ostree-based images to make describe work
	if imgType.OSTreeRef() != "" {
		imgOpts.OSTree = &ostree.ImageOptions{
			URL: "http://example.com/repo",
		}
	}

	manifest, _, err := imgType.Manifest(&bp, imgOpts, nil, nil)
	if err != nil {
		return nil, err
	}
	return manifest, nil
}

func packageSetsFor(imgType distro.ImageType) (map[string]*packagesYAML, error) {
	manifest, err := dummyManifestFor(imgType)
	if err != nil {
		return nil, err
	}

	res := make(map[string]*packagesYAML)

	for pipelineName, pkgSets := range manifest.GetPackageSetChains() {
		incM := map[string]bool{}
		excM := map[string]bool{}
		for _, pkgSet := range pkgSets {
			for _, s := range pkgSet.Include {
				incM[s] = true
			}
			for _, s := range pkgSet.Exclude {
				excM[s] = true
			}
		}
		inc := make([]string, 0, len(incM))
		exc := make([]string, 0, len(excM))
		for name := range incM {
			inc = append(inc, name)
		}
		for name := range excM {
			exc = append(exc, name)
		}
		slices.Sort(inc)
		slices.Sort(exc)

		res[pipelineName] = &packagesYAML{
			Include: inc,
			Exclude: exc,
		}
	}
	return res, nil
}

// XXX: should this live in images instead?
func describeImage(img *imagefilter.Result, out io.Writer) error {
	// see
	// https://github.com/osbuild/images/pull/1019#discussion_r1832376568
	// for what is available on an image (without depsolve or partitioning)
	pkgSets, err := packageSetsFor(img.ImgType)
	if err != nil {
		return err
	}
	partTable, err := img.ImgType.BasePartitionTable()
	if err != nil && !errors.Is(err, defs.ErrNoPartitionTableForImgType) {
		return err
	}
	m, err := dummyManifestFor(img.ImgType)
	if err != nil {
		return err
	}

	outYaml := &describeImgYAML{
		Distro:           img.Distro.Name(),
		OsVersion:        img.Distro.OsVersion(),
		Arch:             img.Arch.Name(),
		Type:             img.ImgType.Name(),
		Bootmode:         img.ImgType.BootMode().String(),
		PartitionType:    img.ImgType.PartitionType().String(),
		DefaultFilename:  img.ImgType.Filename(),
		BuildPipelines:   m.BuildPipelines(),
		PayloadPipelines: m.PayloadPipelines(),
		Packages:         pkgSets,
		PartitionTable:   partTable,
	}
	// deliberately break the yaml until the feature is stable
	fmt.Fprint(out, "@WARNING - the output format is not stable yet and may change\n")
	enc := yaml.NewEncoder(out)
	enc.SetIndent(2)
	return enc.Encode(outYaml)
}
