package dalec

import (
	goerrors "errors"
	"slices"

	"github.com/moby/buildkit/frontend/dockerfile/shell"
	"github.com/pkg/errors"
)

// PackageConstraints is used to specify complex constraints for a package dependency.
type PackageConstraints struct {
	// Version is a list of version constraints for the package.
	// The format of these strings is dependent on the package manager of the target system.
	// Examples:
	//   [">=1.0.0", "<2.0.0"]
	Version []string `yaml:"version,omitempty" json:"version,omitempty"`
	// Arch is a list of architecture constraints for the package.
	// Use this to specify that a package constraint only applies to certain architectures.
	Arch []string `yaml:"arch,omitempty" json:"arch,omitempty"`
}

// PackageDependencies is a list of dependencies for a package.
// This will be included in the package metadata so that the package manager can install the dependencies.
// It also includes build-time dedendencies, which we'll install before running any build steps.
type PackageDependencies struct {
	// Build is the list of packagese required to build the package.
	Build map[string]PackageConstraints `yaml:"build,omitempty" json:"build,omitempty"`
	// Runtime is the list of packages required to install/run the package.
	Runtime map[string]PackageConstraints `yaml:"runtime,omitempty" json:"runtime,omitempty"`
	// Recommends is the list of packages recommended to install with the generated package.
	// Note: Not all package managers support this (e.g. rpm)
	Recommends map[string]PackageConstraints `yaml:"recommends,omitempty" json:"recommends,omitempty"`
	// Sysext is the list of packages to include in the generated system
	// extension. No dependency resolution is performed when generating system
	// extensions, so all required dependencies must be explicitly listed here.
	Sysext map[string]PackageConstraints `yaml:"sysext,omitempty" json:"sysext,omitempty"`

	// Test lists any extra packages required for running tests
	// These packages are only installed for tests which have steps that require
	// running a command in the built container.
	// See [TestSpec] for more information.
	Test []string `yaml:"test,omitempty" json:"test,omitempty"`

	// ExtraRepos is used to inject extra package repositories that may be used to
	// satisfy package dependencies in various stages.
	ExtraRepos []PackageRepositoryConfig `yaml:"extra_repos,omitempty" json:"extra_repos,omitempty"`
}

// PackageRepositoryConfig
type PackageRepositoryConfig struct {
	// Keys are the list of keys that need to be imported to use the configured
	// repositories
	Keys map[string]Source `yaml:"keys,omitempty" json:"keys,omitempty"`

	// Config list of repo configs to to add to the environment.  The format of
	// these configs are distro specific (e.g. apt/yum configs).
	Config map[string]Source `yaml:"config" json:"config"`

	// Data lists all the extra data that needs to be made available for the
	// provided repository config to work.
	// As an example, if the provided config is referencing a file backed repository
	// then data would include the file data, assuming its not already available
	// in the environment.
	Data []SourceMount `yaml:"data,omitempty" json:"data,omitempty"`
	// Envs specifies the list of environments to make the repositories available
	// during.
	// Acceptable values are:
	//  - "build"   - Repositories are added prior to installing build dependencies
	//  - "test"    - Repositories are added prior to installing test dependencies
	//  - "install" - Repositories are added prior to installing the output
	//                package in a container build target.
	Envs []string `yaml:"envs" json:"envs" jsonschema:"enum=build,enum=test,enum=install"`
}

func (d *PackageDependencies) processBuildArgs(lex *shell.Lex, args map[string]string, allowArg func(string) bool) error {
	if d == nil {
		return nil
	}

	for k, v := range d.Build {
		for i, ver := range v.Version {
			updated, err := expandArgs(lex, ver, args, allowArg)
			if err != nil {
				return errors.Wrapf(err, "build version %s", ver)
			}
			v.Version[i] = updated
		}
		d.Build[k] = v
	}

	for k, v := range d.Runtime {
		for i, ver := range v.Version {
			updated, err := expandArgs(lex, ver, args, allowArg)
			if err != nil {
				return errors.Wrapf(err, "runtime version %s", ver)
			}
			v.Version[i] = updated
		}
		d.Runtime[k] = v
	}

	var errs []error
	for i, repo := range d.ExtraRepos {
		if err := repo.processBuildArgs(lex, args, allowArg); err != nil {
			errs = append(errs, errors.Wrapf(err, "extra repos index %d", i))
		}
		d.ExtraRepos[i] = repo
	}
	return goerrors.Join(errs...)
}

func (r *PackageRepositoryConfig) processBuildArgs(lex *shell.Lex, args map[string]string, allowArg func(string) bool) error {
	if r == nil {
		return nil
	}

	var errs []error

	for k := range r.Config {
		src := r.Config[k]
		if err := src.processBuildArgs(lex, args, allowArg); err != nil {
			errs = append(errs, errors.Wrapf(err, "config %s", k))
			continue
		}
		r.Config[k] = src
	}

	for k := range r.Keys {
		src := r.Keys[k]
		if err := src.processBuildArgs(lex, args, allowArg); err != nil {
			errs = append(errs, errors.Wrapf(err, "key %s", k))
			continue
		}
		r.Keys[k] = src
	}

	for i := range r.Data {
		d := r.Data[i]
		if err := d.processBuildArgs(lex, args, allowArg); err != nil {
			errs = append(errs, errors.Wrapf(err, "data index %d", i))
			continue
		}
		r.Data[i] = d
	}

	return goerrors.Join(errs...)
}

func (d *PackageDependencies) fillDefaults() {
	if d == nil {
		return
	}

	for i, r := range d.ExtraRepos {
		r.fillDefaults()
		d.ExtraRepos[i] = r
	}
}

func (r *PackageRepositoryConfig) fillDefaults() {
	if len(r.Envs) == 0 {
		// default to all stages for the extra repo if unspecified
		r.Envs = []string{"build", "install", "test"}
	}

	for i, src := range r.Config {
		s := &src
		s.fillDefaults()
		r.Config[i] = *s
	}

	for i, src := range r.Keys {
		s := &src
		s.fillDefaults()

		// Default to 0644 permissions for gpg keys. This is because apt will only import
		// keys with a particular permission set.
		if s.HTTP != nil {
			s.HTTP.Permissions = 0644
		}
		r.Keys[i] = *s
	}

	for i, mount := range r.Data {
		mount.fillDefaults(nil)
		r.Data[i] = mount
	}
}

func (d *PackageDependencies) validate() error {
	if d == nil {
		return nil
	}

	var errs []error
	for i, r := range d.ExtraRepos {
		if err := r.validate(); err != nil {
			errs = append(errs, errors.Wrapf(err, "extra repo %d", i))
		}
	}

	return goerrors.Join(errs...)
}

func (r *PackageRepositoryConfig) validate() error {
	var errs []error
	for name, src := range r.Keys {
		if err := src.validate(); err != nil {
			errs = append(errs, errors.Wrapf(err, "key %s", name))
		}
	}
	for name, src := range r.Config {
		if err := src.validate(); err != nil {
			errs = append(errs, errors.Wrapf(err, "config %s", name))
		}
	}
	for _, mnt := range r.Data {
		if err := mnt.validate(); err != nil {
			errs = append(errs, errors.Wrapf(err, "data mount path %s", mnt.Dest))
		}
	}

	return goerrors.Join(errs...)
}

func (p *PackageDependencies) GetExtraRepos(env string) []PackageRepositoryConfig {
	return GetExtraRepos(p.ExtraRepos, env)
}

func GetExtraRepos(repos []PackageRepositoryConfig, env string) []PackageRepositoryConfig {
	var out []PackageRepositoryConfig
	for _, repo := range repos {
		if slices.Contains(repo.Envs, env) {
			out = append(repos, repo)
		}
	}
	return out
}

func (s *Spec) GetBuildRepos(targetKey string) []PackageRepositoryConfig {
	deps := s.GetPackageDeps(targetKey)
	if deps == nil {
		deps = s.Dependencies
		if deps == nil {
			return nil
		}
	}

	return deps.GetExtraRepos("build")
}

func (s *Spec) GetInstallRepos(targetKey string) []PackageRepositoryConfig {
	deps := s.GetPackageDeps(targetKey)
	if deps == nil {
		deps = s.Dependencies
		if deps == nil {
			return nil
		}
	}

	return deps.GetExtraRepos("install")
}

func (s *Spec) GetTestRepos(targetKey string) []PackageRepositoryConfig {
	deps := s.GetPackageDeps(targetKey)
	if deps == nil {
		deps = s.Dependencies
		if deps == nil {
			return nil
		}
	}

	return deps.GetExtraRepos("test")
}
