package distro

import (
	"encoding/json"
	"fmt"

	"github.com/osbuild/osbuild-composer/internal/blueprint"
	"github.com/osbuild/osbuild-composer/internal/disk"
	"github.com/osbuild/osbuild-composer/internal/ostree"
	"github.com/osbuild/osbuild-composer/internal/rpmmd"
)

const (
	// architecture names

	X86_64ArchName  = "x86_64"
	Aarch64ArchName = "aarch64"
	Ppc64leArchName = "ppc64le"
	S390xArchName   = "s390x"
)

type BootType string

const (
	UnsetBootType  BootType = ""
	LegacyBootType BootType = "legacy"
	UEFIBootType   BootType = "uefi"
	HybridBootType BootType = "hybrid"
)

// A Distro represents composer's notion of what a given distribution is.
type Distro interface {
	// Returns the name of the distro.
	Name() string

	// Returns the release version of the distro. This is used in repo
	// files on the host system and required for the subscription support.
	Releasever() string

	// Returns the module platform id of the distro. This is used by DNF
	// for modularity support.
	ModulePlatformID() string

	// Returns the ostree reference template
	OSTreeRef() string

	// Returns a sorted list of the names of the architectures this distro
	// supports.
	ListArches() []string

	// Returns an object representing the given architecture as support
	// by this distro.
	GetArch(arch string) (Arch, error)
}

// An Arch represents a given distribution's support for a given architecture.
type Arch interface {
	// Returns the name of the architecture.
	Name() string

	// Returns a sorted list of the names of the image types this architecture
	// supports.
	ListImageTypes() []string

	// Returns an object representing a given image format for this architecture,
	// on this distro.
	GetImageType(imageType string) (ImageType, error)

	// Returns the parent distro
	Distro() Distro
}

// An ImageType represents a given distribution's support for a given Image Type
// for a given architecture.
type ImageType interface {
	// Returns the name of the image type.
	Name() string

	// Returns the parent architecture
	Arch() Arch

	// Returns the canonical filename for the image type.
	Filename() string

	// Retrns the MIME-type for the image type.
	MIMEType() string

	// Returns the default OSTree ref for the image type.
	OSTreeRef() string

	// Returns the proper image size for a given output format. If the input size
	// is 0 the default value for the format will be returned.
	Size(size uint64) uint64

	// Returns the corresponding partion type ("gpt", "dos") or "" the image type
	// has no partition table. Only support for RHEL 8.5+
	PartitionType() string

	// Returns the sets of packages to include and exclude when building the image.
	// Indexed by a string label. How each set is labeled and used depends on the
	// image type.
	PackageSets(bp blueprint.Blueprint, repos []rpmmd.RepoConfig) map[string][]rpmmd.PackageSet

	// Returns the names of the pipelines that set up the build environment (buildroot).
	BuildPipelines() []string

	// Returns the names of the pipelines that create the image.
	PayloadPipelines() []string

	// Returns the package set names safe to install custom packages via custom repositories.
	PayloadPackageSets() []string

	// Returns named arrays of package set names which should be depsolved in a chain.
	PackageSetsChains() map[string][]string

	// Returns the names of the stages that will produce the build output.
	Exports() []string

	// Returns an osbuild manifest, containing the sources and pipeline necessary
	// to build an image, given output format with all packages and customizations
	// specified in the given blueprint. The packageSpecSets must be labelled in
	// the same way as the originating PackageSets.
	Manifest(b *blueprint.Customizations, options ImageOptions, repos []rpmmd.RepoConfig, packageSpecSets map[string][]rpmmd.PackageSpec, seed int64) (Manifest, error)
}

// The ImageOptions specify options for a specific image build
type ImageOptions struct {
	OSTree       ostree.RequestParams
	Size         uint64
	Subscription *SubscriptionImageOptions
}

// The SubscriptionImageOptions specify subscription-specific image options
// ServerUrl denotes the host to register the system with
// BaseUrl specifies the repository URL for DNF
type SubscriptionImageOptions struct {
	Organization  string
	ActivationKey string
	ServerUrl     string
	BaseUrl       string
	Insights      bool
}

type BasePartitionTableMap map[string]disk.PartitionTable

// A Manifest is an opaque JSON object, which is a valid input to osbuild
type Manifest []byte

func (m Manifest) MarshalJSON() ([]byte, error) {
	return json.RawMessage(m).MarshalJSON()
}

func (m *Manifest) UnmarshalJSON(payload []byte) error {
	var raw json.RawMessage
	err := (&raw).UnmarshalJSON(payload)
	if err != nil {
		return err
	}
	*m = Manifest(raw)
	return nil
}

type manifestVersion struct {
	Version string `json:"version"`
}

func (m Manifest) Version() (string, error) {
	mver := new(manifestVersion)
	if err := json.Unmarshal(m, mver); err != nil {
		return "", err
	}

	switch mver.Version {
	case "":
		return "1", nil
	case "2":
		return "2", nil
	default:
		return "", fmt.Errorf("Unsupported Manifest version %s", mver.Version)
	}
}

// Fallbacks: When a new method is added to an interface to provide to provide
// information that isn't available for older implementations, the older
// methods should return a fallback/default value by calling the appropriate
// function from below.
// Example: Exports() simply returns "assembler" for older image type
// implementations that didn't produce v1 manifests that have named pipelines.
func BuildPipelinesFallback() []string {
	return []string{"build"}
}

func PayloadPipelinesFallback() []string {
	return []string{"os", "assembler"}
}

func ExportsFallback() []string {
	return []string{"assembler"}
}

func PayloadPackageSets() []string {
	return []string{}
}

func MakePackageSetChains(t ImageType, packageSets map[string]rpmmd.PackageSet, repos []rpmmd.RepoConfig) map[string][]rpmmd.PackageSet {
	allSetNames := make([]string, len(packageSets))
	idx := 0
	for setName := range packageSets {
		allSetNames[idx] = setName
		idx++
	}

	// map repository PackageSets to the list of repo configs
	packageSetsRepos := make(map[string][]rpmmd.RepoConfig)
	for idx := range repos {
		repo := repos[idx]
		if len(repo.PackageSets) == 0 {
			// repos that don't specify package sets get used everywhere
			repo.PackageSets = allSetNames
		}
		for _, name := range repo.PackageSets {
			psRepos := packageSetsRepos[name]
			psRepos = append(psRepos, repo)
			packageSetsRepos[name] = psRepos
		}
	}

	chainedSets := make(map[string][]rpmmd.PackageSet)
	addedSets := make(map[string]bool)
	// first collect package sets that are part of a chain
	for specName, setNames := range t.PackageSetsChains() {
		pkgSets := make([]rpmmd.PackageSet, len(setNames))

		// add package-set-specific repositories to each set if one is defined
		for idx, pkgSetName := range setNames {
			pkgSet, ok := packageSets[pkgSetName]
			if !ok {
				panic(fmt.Sprintf("image type %q specifies chained package set %q but no package set with that name exists", t.Name(), pkgSetName))
			}
			pkgSet.Repositories = packageSetsRepos[pkgSetName]
			pkgSets[idx] = pkgSet
			addedSets[pkgSetName] = true
		}
		chainedSets[specName] = pkgSets
	}

	// add the rest of the package sets
	for name, pkgSet := range packageSets {
		if addedSets[name] {
			// already added
			continue
		}
		pkgSet.Repositories = packageSetsRepos[name]
		chainedSets[name] = []rpmmd.PackageSet{pkgSet}
		addedSets[name] = true // NOTE: not really necessary but good book-keeping in case this function gets expanded
	}

	return chainedSets
}
