// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package profile

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"

	"github.com/google/go-containerregistry/pkg/authn"
	"github.com/google/go-containerregistry/pkg/authn/github"
	"github.com/google/go-containerregistry/pkg/crane"
	v1 "github.com/google/go-containerregistry/pkg/v1"
	"github.com/google/go-containerregistry/pkg/v1/google"
	"github.com/google/go-containerregistry/pkg/v1/layout"
	"github.com/siderolabs/gen/value"
	"golang.org/x/sync/errgroup"

	"github.com/siderolabs/talos/internal/pkg/measure"
	"github.com/siderolabs/talos/internal/pkg/secureboot/pesign"
	"github.com/siderolabs/talos/pkg/archiver"
	"github.com/siderolabs/talos/pkg/imager/profile/internal/signer/aws"
	"github.com/siderolabs/talos/pkg/imager/profile/internal/signer/azure"
	"github.com/siderolabs/talos/pkg/imager/profile/internal/signer/file"
	"github.com/siderolabs/talos/pkg/images"
	"github.com/siderolabs/talos/pkg/machinery/constants"
	"github.com/siderolabs/talos/pkg/machinery/imager/quirks"
)

const (
	arm64 = "arm64"
	amd64 = "amd64"
)

// Input describes inputs for image generation.
type Input struct {
	// Kernel is a vmlinuz file.
	Kernel FileAsset `yaml:"kernel"`
	// Initramfs is a initramfs file (without system extensions).
	Initramfs FileAsset `yaml:"initramfs"`
	// SDStub is a sd-stub file (only for SecureBoot).
	SDStub FileAsset `yaml:"sdStub,omitempty"`
	// SDBoot is a sd-boot file (only for SecureBoot).
	SDBoot FileAsset `yaml:"sdBoot,omitempty"`
	// Base installer image to mutate.
	BaseInstaller ContainerAsset `yaml:"baseInstaller,omitempty"`
	// ImageCache is an image cache to inject into the asset.
	ImageCache ContainerAsset `yaml:"imageCache,omitempty"`
	// OverlayInstaller is an overlay image to inject into the installer.
	//
	// OverlayInstaller architecture should match the output installer architecture.
	OverlayInstaller ContainerAsset `yaml:"overlayInstaller,omitempty"`
	// SecureBoot is a section with secureboot keys, only for SecureBoot enabled builds.
	SecureBoot *SecureBootAssets `yaml:"secureboot,omitempty"`
	// SystemExtensions is a list of system extensions to install.
	SystemExtensions []ContainerAsset `yaml:"systemExtensions,omitempty"`
}

// FileAsset describes a file asset.
type FileAsset struct {
	// Path to the file.
	Path string `yaml:"path"`
}

// ContainerAsset describes a container asset.
type ContainerAsset struct {
	// ImageRef is a reference to the container image.
	ImageRef string `yaml:"imageRef"`
	// ForceInsecure forces insecure registry communication.
	ForceInsecure bool `yaml:"forceInsecure,omitempty"`
	// TarballPath is a path to the .tar format container image contents.
	//
	// If TarballPath is set, ImageRef is ignored.
	TarballPath string `yaml:"tarballPath,omitempty"`
	// OCIPath is a path to the OCI format container image contents.
	//
	// If OCIPath is set, ImageRef is ignored.
	OCIPath string `yaml:"ociPath,omitempty"`
}

// SecureBootAssets describes secureboot assets.
type SecureBootAssets struct {
	// SecureBoot signing key & cert.
	SecureBootSigner SigningKeyAndCertificate `yaml:"secureBootSigner"`
	// PCR signing key.
	PCRSigner SigningKey `yaml:"pcrSigner"`
	// Optional, auto-enrollment paths.
	PlatformKeyPath    string `yaml:"platformKeyPath,omitempty"`
	KeyExchangeKeyPath string `yaml:"keyExchangeKeyPath,omitempty"`
	SignatureKeyPath   string `yaml:"signatureKeyPath,omitempty"`
	// Optional, auto-enrollment include well-known UEFI (Microsoft) certs.
	IncludeWellKnownCerts bool `yaml:"includeWellKnownCerts,omitempty"`
}

// SigningKeyAndCertificate describes a signing key & certificate.
type SigningKeyAndCertificate struct {
	// File-based.
	//
	// Static key and certificate paths.
	KeyPath  string `yaml:"keyPath,omitempty"`
	CertPath string `yaml:"certPath,omitempty"`
	// Azure.
	//
	// Azure Vault URL and certificate ID, key will be found from the certificate.
	AzureVaultURL      string `yaml:"azureVaultURL,omitempty"`
	AzureCertificateID string `yaml:"azureCertificateID,omitempty"`
	// AWS.
	//
	// AWS KMS Key ID, ACM certificate ARN, and region.
	// Support local cert file for legacy use cases.
	AwsKMSKeyID string `yaml:"awsKMSKeyID,omitempty"`
	AwsRegion   string `yaml:"awsRegion,omitempty"`
	AwsCertPath string `yaml:"awsCertPath,omitempty"`
	AwsCertARN  string `yaml:"awsCertARN,omitempty"`
}

// SigningKey describes a signing key.
type SigningKey struct {
	// File-based.
	//
	// Static key path.
	KeyPath string `yaml:"keyPath,omitempty"`
	// Azure.
	//
	// Azure Vault URL and key ID.
	// AzureKeyVersion might be left empty to use the latest key version.
	AzureVaultURL   string `yaml:"azureVaultURL,omitempty"`
	AzureKeyID      string `yaml:"azureKeyID,omitempty"`
	AzureKeyVersion string `yaml:"azureKeyVersion,omitempty"`
	// AWS.
	//
	// AWS KMS Key ID and region.
	AwsKMSKeyID string `yaml:"awsKMSKeyID,omitempty"`
	AwsRegion   string `yaml:"awsRegion,omitempty"`
}

// GetSigner returns the signer.
func (key SigningKey) GetSigner(ctx context.Context) (measure.RSAKey, error) {
	switch {
	case key.KeyPath != "":
		return file.NewPCRSigner(key.KeyPath)
	case key.AzureVaultURL != "" && key.AzureKeyID != "":
		return azure.NewPCRSigner(ctx, key.AzureVaultURL, key.AzureKeyID, key.AzureKeyVersion)
	case key.AwsKMSKeyID != "":
		return aws.NewPCRSigner(ctx, key.AwsKMSKeyID, key.AwsRegion)
	default:
		return nil, errors.New("unsupported PCR signer")
	}
}

// GetSigner returns the signer.
func (keyAndCert SigningKeyAndCertificate) GetSigner(ctx context.Context) (pesign.CertificateSigner, error) {
	switch {
	case keyAndCert.KeyPath != "" && keyAndCert.CertPath != "":
		return file.NewSecureBootSigner(keyAndCert.CertPath, keyAndCert.KeyPath)
	case keyAndCert.AzureVaultURL != "" && keyAndCert.AzureCertificateID != "":
		return azure.NewSecureBootSigner(ctx, keyAndCert.AzureVaultURL, keyAndCert.AzureCertificateID, keyAndCert.AzureCertificateID)
	case keyAndCert.AwsKMSKeyID != "" && keyAndCert.AwsCertARN != "":
		return aws.NewSecureBootACMSigner(ctx, keyAndCert.AwsKMSKeyID, keyAndCert.AwsRegion, keyAndCert.AwsCertARN)
	case keyAndCert.AwsKMSKeyID != "" && keyAndCert.AwsCertPath != "":
		return aws.NewSecureBootSigner(ctx, keyAndCert.AwsKMSKeyID, keyAndCert.AwsRegion, keyAndCert.AwsCertPath)
	default:
		return nil, errors.New("unsupported PCR signer")
	}
}

const defaultSecureBootPrefix = "/secureboot"

// FillDefaults fills default values for the input.
//
//nolint:gocyclo,cyclop
func (i *Input) FillDefaults(arch, version string, secureboot bool) {
	var (
		zeroFileAsset      FileAsset
		zeroContainerAsset ContainerAsset
	)

	if i.Kernel == zeroFileAsset {
		i.Kernel.Path = fmt.Sprintf(constants.KernelAssetPath, arch)
	}

	if i.Initramfs == zeroFileAsset {
		i.Initramfs.Path = fmt.Sprintf(constants.InitramfsAssetPath, arch)
	}

	if i.BaseInstaller == zeroContainerAsset {
		i.BaseInstaller.ImageRef = fmt.Sprintf("%s:%s", images.DefaultInstallerImageRepository, version)

		if quirks.New(version).SupportsUnifiedInstaller() {
			i.BaseInstaller.ImageRef = fmt.Sprintf("%s-base:%s", images.DefaultInstallerImageRepository, version)
		}
	}

	if i.SDStub == zeroFileAsset {
		i.SDStub.Path = fmt.Sprintf(constants.SDStubAssetPath, arch)
	}

	if i.SDBoot == zeroFileAsset {
		i.SDBoot.Path = fmt.Sprintf(constants.SDBootAssetPath, arch)
	}

	if secureboot {
		if i.SecureBoot == nil {
			i.SecureBoot = &SecureBootAssets{}
		}

		if value.IsZero(i.SecureBoot.SecureBootSigner) {
			i.SecureBoot.SecureBootSigner.KeyPath = filepath.Join(defaultSecureBootPrefix, constants.SecureBootSigningKeyAsset)
			i.SecureBoot.SecureBootSigner.CertPath = filepath.Join(defaultSecureBootPrefix, constants.SecureBootSigningCertAsset)
		}

		if value.IsZero(i.SecureBoot.PCRSigner) {
			i.SecureBoot.PCRSigner.KeyPath = filepath.Join(defaultSecureBootPrefix, constants.PCRSigningKeyAsset)
		}

		if i.SecureBoot.PlatformKeyPath == "" {
			if platformKeyPath := filepath.Join(defaultSecureBootPrefix, constants.PlatformKeyAsset); fileExists(platformKeyPath) {
				i.SecureBoot.PlatformKeyPath = platformKeyPath
			}
		}

		if i.SecureBoot.KeyExchangeKeyPath == "" {
			if keyExchangeKeyPath := filepath.Join(defaultSecureBootPrefix, constants.KeyExchangeKeyAsset); fileExists(keyExchangeKeyPath) {
				i.SecureBoot.KeyExchangeKeyPath = keyExchangeKeyPath
			}
		}

		if i.SecureBoot.SignatureKeyPath == "" {
			if signatureKeyPath := filepath.Join(defaultSecureBootPrefix, constants.SignatureKeyAsset); fileExists(signatureKeyPath) {
				i.SecureBoot.SignatureKeyPath = signatureKeyPath
			}
		}
	}
}

func fileExists(path string) bool {
	_, err := os.Stat(path)

	return err == nil
}

// Pull the container asset to the path.
func (c *ContainerAsset) Pull(ctx context.Context, arch string, printf func(string, ...any)) (v1.Image, error) {
	if c.TarballPath != "" {
		return nil, errors.New("pulling tarball container image is not supported")
	}

	if c.OCIPath != "" {
		printf("using OCI image from %s...", c.OCIPath)

		return c.pullFromOCI(arch)
	}

	printf("pulling %s...", c.ImageRef)

	opts := []crane.Option{
		crane.WithPlatform(&v1.Platform{
			Architecture: arch,
			OS:           "linux",
		}),
		crane.WithContext(ctx),
		crane.WithAuthFromKeychain(
			authn.NewMultiKeychain(
				authn.DefaultKeychain,
				github.Keychain,
				google.Keychain,
			),
		),
	}

	if c.ForceInsecure {
		opts = append(opts, crane.Insecure)
	}

	img, err := crane.Pull(c.ImageRef, opts...)
	if err != nil {
		return nil, fmt.Errorf("error pulling image %s: %w", c.ImageRef, err)
	}

	return img, nil
}

func (c *ContainerAsset) pullFromOCI(arch string) (v1.Image, error) {
	ociLayout, err := layout.FromPath(c.OCIPath)
	if err != nil {
		return nil, fmt.Errorf("error opening OCI layout: %w", err)
	}

	ociIndex, err := ociLayout.ImageIndex()
	if err != nil {
		return nil, fmt.Errorf("error opening OCI index: %w", err)
	}

	ociManifest, err := ociIndex.IndexManifest()
	if err != nil {
		return nil, fmt.Errorf("error opening OCI manifest: %w", err)
	}

	for _, manifest := range ociManifest.Manifests {
		if manifest.Platform == nil {
			continue
		}

		if manifest.Platform.OS == "linux" && manifest.Platform.Architecture == arch {
			img, err := ociLayout.Image(manifest.Digest)
			if err != nil {
				return nil, fmt.Errorf("error opening OCI image: %w", err)
			}

			return img, nil
		}
	}

	return nil, fmt.Errorf("no OCI image found for %s", arch)
}

// Extract the container asset to the path.
func (c *ContainerAsset) Extract(
	ctx context.Context,
	destination, arch string,
	printf func(string, ...any),
	xattrsMap map[string]string,
) error {
	if c.TarballPath != "" {
		in, err := os.Open(c.TarballPath)
		if err != nil {
			return err
		}

		defer in.Close() //nolint:errcheck

		printf("extracting %s...", c.TarballPath)

		return archiver.Untar(ctx, in, destination, xattrsMap)
	}

	img, err := c.Pull(ctx, arch, printf)
	if err != nil {
		return err
	}

	r, w := io.Pipe()

	eg, ctx := errgroup.WithContext(ctx)

	eg.Go(func() error {
		if exportErr := crane.Export(img, w); exportErr != nil {
			w.CloseWithError(exportErr)

			return exportErr
		}

		w.Close() //nolint:errcheck

		return nil
	})

	eg.Go(func() error {
		if untarErr := archiver.Untar(ctx, r, destination, xattrsMap); untarErr != nil {
			r.CloseWithError(untarErr)

			return untarErr
		}

		return nil
	})

	return eg.Wait()
}
