// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: BUSL-1.1

package plugincatalog

import (
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"os"
	"path"
	"runtime"
	"strings"
	"sync"

	"github.com/ProtonMail/gopenpgp/v3/crypto"
)

const (
	extractedArtifactDirFmt = "%s_%s_%s_%s" // vault-plugin-database-oracle_1.2.3+ent_linux_amd64
	metadataFile            = "metadata.json"
	metadataSig             = "metadata.json.sig"
)

var (
	once     sync.Once
	verifier crypto.PGPVerify

	errExtractedArtifactDirNotFound = errors.New("extracted artifact directory not found")
	errReadMetadata                 = errors.New("failed to read metadata")
	errReadMetadataSig              = errors.New("failed to read metadata signature")
	errReadPlugin                   = errors.New("failed to read plugin binary")
	errReadPluginMetadata           = errors.New("failed to read plugin metadata")
	errReadPluginPGPSig             = errors.New("failed to read plugin binary PGP signature")
	errVerifyMetadataSig            = errors.New("failed to verify metadata detached signature")
	errVerifyPluginSig              = errors.New("failed to verify plugin binary PGP signature")
)

func load() error {
	var errs error
	once.Do(func() {
		// hashiCorpPGPPubKey is HashiCorp's PGP public key
		// at https://www.hashicorp.com/.well-known/pgp-key.txt.
		// This key is used to verify the authenticity of HashiCorp plugins.
		const hashiCorpPGPPubKey = `
-----BEGIN PGP PUBLIC KEY BLOCK-----

[encoded data, 7605 characters]
ZF5q4h4I33PSGDdSvGXn9UMY5Isjpg==
=7pIB
-----END PGP PUBLIC KEY BLOCK-----
`

		pgp := crypto.PGP()
		key, err := crypto.NewKeyFromArmored(hashiCorpPGPPubKey)
		if err != nil {
			errs = errors.Join(errs, err)
		}

		verifier, err = pgp.Verify().VerificationKey(key).New()
		if err != nil {
			errs = errors.Join(errs, err)
		}
	})

	if verifier == nil {
		errs = errors.Join(errs, errors.New("verifier is nil after initialization"))
	}

	return errs
}

func GetExtractedArtifactDir(pluginName, pluginVersion string) string {
	if strings.HasPrefix(pluginVersion, "v") {
		pluginVersion = pluginVersion[1:]
	}

	return fmt.Sprintf(extractedArtifactDirFmt, pluginName, pluginVersion, runtime.GOOS, runtime.GOARCH)
}

type verifyFunc func(data, sig []byte) error

func verifyPGPSignatureDetached(data, sig []byte) error {
	if err := load(); err != nil {
		return fmt.Errorf("failed to load verifier: %w", err)
	}

	verifyResult, err := verifier.VerifyDetached(data, sig, crypto.Armor)
	if err != nil {
		return fmt.Errorf("failed to verify data: %w", err)
	}
	if sigErr := verifyResult.SignatureError(); sigErr != nil {
		return fmt.Errorf("unexpected signature error: %w", sigErr)
	}

	return nil
}

func VerifyPlugin(pluginDir string, pluginName string, verifyFunc verifyFunc) (*PluginMetadata, error) {
	if st, err := os.Stat(pluginDir); err != nil {
		if os.IsNotExist(err) {
			return nil, fmt.Errorf("%w: %s", errExtractedArtifactDirNotFound, pluginDir)
		}
		return nil, fmt.Errorf("unexpected err %w", err)
	} else if !st.IsDir() {
		return nil, fmt.Errorf("%w: %s is not a directory", errExtractedArtifactDirNotFound, pluginDir)
	}

	// verify metadata.json is untampered
	metadataPath := path.Join(pluginDir, "metadata.json")
	metadataBytes, err := os.ReadFile(metadataPath)
	if err != nil {
		return nil, fmt.Errorf("%w: %w", errReadMetadata, err)
	}

	metadataSigBytes, err := os.ReadFile(path.Join(pluginDir, "metadata.json.sig"))
	if err != nil {
		return nil, fmt.Errorf("%w: %w", errReadMetadataSig, err)
	}

	if err := verifyFunc(metadataBytes, metadataSigBytes); err != nil {
		return nil, fmt.Errorf("%w: %w", errVerifyMetadataSig, err)
	}

	// verify plugin binary is untampered
	// TODO: We should update this to not read in the whole file into memory to be more efficient. Reference the secure-plugin-api's HashFile function.
	pluginBytes, err := os.ReadFile(path.Join(pluginDir, pluginName))
	if err != nil {
		return nil, fmt.Errorf("%w: %w", errReadPlugin, err)
	}

	metadata, err := readPluginMetadata(metadataPath)
	if err != nil {
		return nil, fmt.Errorf("%w: %w", errReadPluginMetadata, err)
	}

	if err := verifyFunc(pluginBytes, []byte(metadata.Plugin.PGPSig)); err != nil {
		return nil, fmt.Errorf("%w: %w", errVerifyPluginSig, err)
	}

	// TODO: Once we set sha256 on the metadata file from releases.hashicorp.com,
	// we should compare the file hash with the metadata file's sha256.
	hasher := sha256.New()
	if _, err := hasher.Write(pluginBytes); err != nil {
		return nil, fmt.Errorf("failed to hash plugin binary: %w", err)
	}
	metadata.Plugin.Sha256 = hex.EncodeToString(hasher.Sum(nil))

	return metadata, nil
}
