/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package cmd

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"syscall"

	"github.com/spf13/cobra"
	"golang.org/x/term"

	"helm.sh/helm/v4/internal/plugin"
	"helm.sh/helm/v4/pkg/cmd/require"
	"helm.sh/helm/v4/pkg/provenance"
)

const pluginPackageDesc = `
This command packages a Helm plugin directory into a tarball.

By default, the command will generate a provenance file signed with a PGP key.
This ensures the plugin can be verified after installation.

Use --sign=false to skip signing (not recommended for distribution).
`

type pluginPackageOptions struct {
	sign           bool
	keyring        string
	key            string
	passphraseFile string
	pluginPath     string
	destination    string
}

func newPluginPackageCmd(out io.Writer) *cobra.Command {
	o := &pluginPackageOptions{}

	cmd := &cobra.Command{
		Use:   "package [PATH]",
		Short: "package a plugin directory into a plugin archive",
		Long:  pluginPackageDesc,
		Args:  require.ExactArgs(1),
		RunE: func(_ *cobra.Command, args []string) error {
			o.pluginPath = args[0]
			return o.run(out)
		},
	}

	f := cmd.Flags()
	f.BoolVar(&o.sign, "sign", true, "use a PGP private key to sign this plugin")
	f.StringVar(&o.key, "key", "", "name of the key to use when signing. Used if --sign is true")
	f.StringVar(&o.keyring, "keyring", defaultKeyring(), "location of a public keyring")
	f.StringVar(&o.passphraseFile, "passphrase-file", "", "location of a file which contains the passphrase for the signing key. Use \"-\" to read from stdin.")
	f.StringVarP(&o.destination, "destination", "d", ".", "location to write the plugin tarball.")

	return cmd
}

func (o *pluginPackageOptions) run(out io.Writer) error {
	// Check if the plugin path exists and is a directory
	fi, err := os.Stat(o.pluginPath)
	if err != nil {
		return err
	}
	if !fi.IsDir() {
		return errors.New("plugin package only supports directories, not tarballs")
	}

	// Load and validate plugin metadata
	pluginMeta, err := plugin.LoadDir(o.pluginPath)
	if err != nil {
		return fmt.Errorf("invalid plugin directory: %w", err)
	}

	// Create destination directory if needed
	if err := os.MkdirAll(o.destination, 0755); err != nil {
		return err
	}

	// If signing is requested, prepare the signer first
	var signer *provenance.Signatory
	if o.sign {
		// Load the signing key
		signer, err = provenance.NewFromKeyring(o.keyring, o.key)
		if err != nil {
			return fmt.Errorf("error reading from keyring: %w", err)
		}

		// Get passphrase
		passphraseFetcher := o.promptUser
		if o.passphraseFile != "" {
			passphraseFetcher, err = o.passphraseFileFetcher()
			if err != nil {
				return err
			}
		}

		// Decrypt the key
		if err := signer.DecryptKey(passphraseFetcher); err != nil {
			return err
		}
	} else {
		// User explicitly disabled signing
		fmt.Fprint(out, "WARNING: Skipping plugin signing. This is not recommended for plugins intended for distribution.\n")
	}

	// Now create the tarball (only after signing prerequisites are met)
	// Use plugin metadata for filename: PLUGIN_NAME-SEMVER.tgz
	metadata := pluginMeta.Metadata()
	filename := fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version)
	tarballPath := filepath.Join(o.destination, filename)

	tarFile, err := os.Create(tarballPath)
	if err != nil {
		return fmt.Errorf("failed to create tarball: %w", err)
	}
	defer tarFile.Close()

	if err := plugin.CreatePluginTarball(o.pluginPath, metadata.Name, tarFile); err != nil {
		os.Remove(tarballPath)
		return fmt.Errorf("failed to create plugin tarball: %w", err)
	}
	tarFile.Close() // Ensure file is closed before signing

	// If signing was requested, sign the tarball
	if o.sign {
		// Read the tarball data
		tarballData, err := os.ReadFile(tarballPath)
		if err != nil {
			os.Remove(tarballPath)
			return fmt.Errorf("failed to read tarball for signing: %w", err)
		}

		// Sign the plugin tarball data
		sig, err := plugin.SignPlugin(tarballData, filepath.Base(tarballPath), signer)
		if err != nil {
			os.Remove(tarballPath)
			return fmt.Errorf("failed to sign plugin: %w", err)
		}

		// Write the signature
		provFile := tarballPath + ".prov"
		if err := os.WriteFile(provFile, []byte(sig), 0644); err != nil {
			os.Remove(tarballPath)
			return err
		}

		fmt.Fprintf(out, "Successfully signed. Signature written to: %s\n", provFile)
	}

	fmt.Fprintf(out, "Successfully packaged plugin and saved it to: %s\n", tarballPath)

	return nil
}

func (o *pluginPackageOptions) promptUser(name string) ([]byte, error) {
	fmt.Printf("Password for key %q >  ", name)
	pw, err := term.ReadPassword(int(syscall.Stdin))
	fmt.Println()
	return pw, err
}

func (o *pluginPackageOptions) passphraseFileFetcher() (provenance.PassphraseFetcher, error) {
	file, err := openPassphraseFile(o.passphraseFile, os.Stdin)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	// Read the entire passphrase
	passphrase, err := io.ReadAll(file)
	if err != nil {
		return nil, err
	}

	// Trim any trailing newline characters (both \n and \r\n)
	passphrase = bytes.TrimRight(passphrase, "\r\n")

	return func(_ string) ([]byte, error) {
		return passphrase, nil
	}, nil
}

// copied from action.openPassphraseFile
// TODO: should we move this to pkg/action so we can reuse the func from there?
func openPassphraseFile(passphraseFile string, stdin *os.File) (*os.File, error) {
	if passphraseFile == "-" {
		stat, err := stdin.Stat()
		if err != nil {
			return nil, err
		}
		if (stat.Mode() & os.ModeNamedPipe) == 0 {
			return nil, errors.New("specified reading passphrase from stdin, without input on stdin")
		}
		return stdin, nil
	}
	return os.Open(passphraseFile)
}
