//
// Copyright 2021 The Sigstore 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 rpm

import (
	"bytes"
	"context"
	"crypto"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"

	"github.com/go-openapi/strfmt"
	"github.com/go-openapi/swag/conv"
	"github.com/sassoftware/go-rpmutils"

	"github.com/sigstore/rekor/pkg/generated/models"
	"github.com/sigstore/rekor/pkg/log"
	"github.com/sigstore/rekor/pkg/pki"
	"github.com/sigstore/rekor/pkg/pki/pgp"
	"github.com/sigstore/rekor/pkg/types"
	"github.com/sigstore/rekor/pkg/types/rpm"
	"github.com/sigstore/rekor/pkg/util"
)

const (
	APIVERSION = "0.0.1"
)

func init() {
	if err := rpm.VersionMap.SetEntryFactory(APIVERSION, NewEntry); err != nil {
		log.Logger.Panic(err)
	}
}

type V001Entry struct {
	RPMModel models.RpmV001Schema
}

func (v V001Entry) APIVersion() string {
	return APIVERSION
}

func NewEntry() types.EntryImpl {
	return &V001Entry{}
}

func (v V001Entry) IndexKeys() ([]string, error) {
	var result []string

	if v.RPMModel.PublicKey == nil || v.RPMModel.PublicKey.Content == nil {
		return nil, errors.New("rpm v0.0.1 entry not initialized")
	}

	keyObj, err := pgp.NewPublicKey(bytes.NewReader(*v.RPMModel.PublicKey.Content))
	if err != nil {
		return nil, err
	}
	key, err := keyObj.CanonicalValue()
	if err != nil {
		return nil, err
	}
	keyHash := sha256.Sum256(key)
	result = append(result, strings.ToLower(hex.EncodeToString(keyHash[:])))

	result = append(result, keyObj.Subjects()...)

	if v.RPMModel.Package != nil && v.RPMModel.Package.Hash != nil {
		if v.RPMModel.Package.Hash.Algorithm != nil && v.RPMModel.Package.Hash.Value != nil {
			hashKey := strings.ToLower(fmt.Sprintf("%s:%s", *v.RPMModel.Package.Hash.Algorithm, *v.RPMModel.Package.Hash.Value))
			result = append(result, hashKey)
		}
	}

	return result, nil
}

func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error {
	rpm, ok := pe.(*models.Rpm)
	if !ok {
		return errors.New("cannot unmarshal non RPM v0.0.1 type")
	}
	if rpm == nil {
		return errors.New("proposed entry cannot be nil")
	}

	if err := DecodeEntry(rpm.Spec, &v.RPMModel); err != nil {
		return err
	}

	// field validation
	if err := v.RPMModel.Validate(strfmt.Default); err != nil {
		return err
	}

	return v.validate()
}

// DecodeEntry decodes input into provided output pointer without mutating receiver on error
func DecodeEntry(input any, output *models.RpmV001Schema) error {
	if output == nil {
		return fmt.Errorf("nil output *models.RpmV001Schema")
	}
	var m models.RpmV001Schema
	// Single switch including map[string]any fast path
	switch data := input.(type) {
	case map[string]any:
		mm := data
		if pk, ok := mm["publicKey"].(map[string]any); ok {
			m.PublicKey = &models.RpmV001SchemaPublicKey{}
			if c, ok := pk["content"].(string); ok && c != "" {
				outb := make([]byte, base64.StdEncoding.DecodedLen(len(c)))
				n, err := base64.StdEncoding.Decode(outb, []byte(c))
				if err != nil {
					return fmt.Errorf("failed parsing base64 data for publicKey content: %w", err)
				}
				b := strfmt.Base64(outb[:n])
				m.PublicKey.Content = &b
			}
		}
		if pkgRaw, ok := mm["package"].(map[string]any); ok {
			m.Package = &models.RpmV001SchemaPackage{}
			if hdrs, ok := pkgRaw["headers"].(map[string]any); ok {
				m.Package.Headers = make(map[string]string, len(hdrs))
				for k, v2 := range hdrs {
					if s, ok := v2.(string); ok {
						m.Package.Headers[k] = s
					}
				}
			}
			if hRaw, ok := pkgRaw["hash"].(map[string]any); ok {
				m.Package.Hash = &models.RpmV001SchemaPackageHash{}
				if alg, ok := hRaw["algorithm"].(string); ok {
					m.Package.Hash.Algorithm = &alg
				}
				if val, ok := hRaw["value"].(string); ok {
					m.Package.Hash.Value = &val
				}
			}
			if c, ok := pkgRaw["content"].(string); ok && c != "" {
				outb := make([]byte, base64.StdEncoding.DecodedLen(len(c)))
				n, err := base64.StdEncoding.Decode(outb, []byte(c))
				if err != nil {
					return fmt.Errorf("failed parsing base64 data for package content: %w", err)
				}
				m.Package.Content = strfmt.Base64(outb[:n])
			}
		}
		*output = m
		return nil
	case *models.RpmV001Schema:
		if data == nil {
			return fmt.Errorf("nil *models.RpmV001Schema")
		}
		*output = *data
		return nil
	case models.RpmV001Schema:
		*output = data
		return nil
	default:
		return fmt.Errorf("unsupported input type %T for DecodeEntry", input)
	}
}

func (v *V001Entry) fetchExternalEntities(_ context.Context) (*pgp.PublicKey, *rpmutils.RpmHeader, error) {
	if err := v.validate(); err != nil {
		return nil, nil, &types.InputValidationError{Err: err}
	}

	oldSHA := ""
	if v.RPMModel.Package.Hash != nil && v.RPMModel.Package.Hash.Value != nil {
		oldSHA = conv.Value(v.RPMModel.Package.Hash.Value)
	}

	// Parse public key
	keyObj, err := pgp.NewPublicKey(bytes.NewReader(*v.RPMModel.PublicKey.Content))
	if err != nil {
		return nil, nil, &types.InputValidationError{Err: err}
	}

	keyring, err := keyObj.Entities()
	if err != nil {
		return nil, nil, &types.InputValidationError{Err: err}
	}

	pkgBytes := v.RPMModel.Package.Content
	computedSHA := sha256.Sum256(pkgBytes)
	computedSHAHex := hex.EncodeToString(computedSHA[:])
	if oldSHA != "" && computedSHAHex != oldSHA {
		return nil, nil, &types.InputValidationError{Err: fmt.Errorf("SHA mismatch: %s != %s", computedSHAHex, oldSHA)}
	}

	// Verify GPG signature and parse RPM headers
	rpmObj, sigs, err := rpmutils.Verify(bytes.NewReader(pkgBytes), keyring)
	if err != nil {
		return nil, nil, &types.InputValidationError{Err: err}
	}
	if len(sigs) == 0 {
		return nil, nil, &types.InputValidationError{Err: errors.New("no supported PGP signature found in RPM")}
	}

	// Set hash if not provided
	if oldSHA == "" {
		v.RPMModel.Package.Hash = &models.RpmV001SchemaPackageHash{}
		v.RPMModel.Package.Hash.Algorithm = conv.Pointer(models.RpmV001SchemaPackageHashAlgorithmSha256)
		v.RPMModel.Package.Hash.Value = conv.Pointer(computedSHAHex)
	}

	return keyObj, rpmObj, nil
}

func (v *V001Entry) Canonicalize(ctx context.Context) ([]byte, error) {
	keyObj, rpmObj, err := v.fetchExternalEntities(ctx)
	if err != nil {
		return nil, err
	}

	canonicalEntry := models.RpmV001Schema{}

	// need to canonicalize key content

	var pubKeyContent []byte
	canonicalEntry.PublicKey = &models.RpmV001SchemaPublicKey{}
	pubKeyContent, err = keyObj.CanonicalValue()
	if err != nil {
		return nil, err
	}
	canonicalEntry.PublicKey.Content = (*strfmt.Base64)(&pubKeyContent)

	canonicalEntry.Package = &models.RpmV001SchemaPackage{}
	canonicalEntry.Package.Hash = &models.RpmV001SchemaPackageHash{}
	canonicalEntry.Package.Hash.Algorithm = v.RPMModel.Package.Hash.Algorithm
	canonicalEntry.Package.Hash.Value = v.RPMModel.Package.Hash.Value
	// data content is not set deliberately

	// set NEVRA headers
	nevra, err := rpmObj.GetNEVRA()
	if err != nil {
		return nil, err
	}
	canonicalEntry.Package.Headers = make(map[string]string)
	canonicalEntry.Package.Headers["Name"] = nevra.Name
	canonicalEntry.Package.Headers["Epoch"] = nevra.Epoch
	canonicalEntry.Package.Headers["Version"] = nevra.Version
	canonicalEntry.Package.Headers["Release"] = nevra.Release
	canonicalEntry.Package.Headers["Architecture"] = nevra.Arch
	if md5sum, err := rpmObj.GetBytes(rpmutils.SIG_MD5); err == nil && len(md5sum) > 0 {
		canonicalEntry.Package.Headers["RPMSIGTAG_MD5"] = hex.EncodeToString(md5sum)
	}
	// Do not emit RPMSIGTAG_SHA1 / RPMSIGTAG_SHA256. Those digests are not part
	// of canonicalized RPM entries; adding them would change leaf hashes used
	// for inclusion proofs.
	// if sha1sum, err := rpmObj.GetStrings(rpmutils.SIG_SHA1); err == nil && len(sha1sum) > 0 {
	// 	canonicalEntry.Package.Headers["RPMSIGTAG_SHA1"] = sha1sum[0]
	// }
	// if sha256sum, err := rpmObj.GetStrings(rpmutils.SIG_SHA256); err == nil && len(sha256sum) > 0 {
	// 	canonicalEntry.Package.Headers["RPMSIGTAG_SHA256"] = sha256sum[0]
	// }

	// wrap in valid object with kind and apiVersion set
	rpm := models.Rpm{}
	rpm.APIVersion = conv.Pointer(APIVERSION)
	rpm.Spec = &canonicalEntry

	return json.Marshal(&rpm)
}

// validate performs cross-field validation for fields in object
func (v V001Entry) validate() error {
	key := v.RPMModel.PublicKey
	if key == nil {
		return errors.New("missing public key")
	}
	if key.Content == nil || len(*key.Content) == 0 {
		return errors.New("'content' must be specified for publicKey")
	}

	pkg := v.RPMModel.Package
	if pkg == nil {
		return errors.New("missing package")
	}

	hash := pkg.Hash
	if hash != nil {
		// Only sha256 is supported for rpm v0.0.1; enforce length accordingly.
		if hash.Value == nil || len(*hash.Value) != crypto.SHA256.Size()*2 {
			return errors.New("invalid value for hash")
		}
		if _, err := hex.DecodeString(*hash.Value); err != nil {
			return errors.New("invalid value for hash")
		}
	} else if len(pkg.Content) == 0 {
		return errors.New("'content' must be specified for package")
	}

	return nil
}

func (v V001Entry) CreateFromArtifactProperties(ctx context.Context, props types.ArtifactProperties) (models.ProposedEntry, error) {
	returnVal := models.Rpm{}
	re := V001Entry{}

	// we will need artifact, public-key, signature
	re.RPMModel = models.RpmV001Schema{}
	re.RPMModel.Package = &models.RpmV001SchemaPackage{}

	var err error
	artifactBytes := props.ArtifactBytes
	if artifactBytes == nil {
		var artifactReader io.ReadCloser
		if props.ArtifactPath == nil {
			return nil, errors.New("path to artifact file must be specified")
		}
		if props.ArtifactPath.IsAbs() {
			artifactReader, err = util.FileOrURLReadCloser(ctx, props.ArtifactPath.String(), nil)
			if err != nil {
				return nil, fmt.Errorf("error reading RPM file: %w", err)
			}
		} else {
			artifactReader, err = os.Open(filepath.Clean(props.ArtifactPath.Path))
			if err != nil {
				return nil, fmt.Errorf("error opening RPM file: %w", err)
			}
		}
		artifactBytes, err = io.ReadAll(artifactReader)
		if err != nil {
			return nil, fmt.Errorf("error reading RPM file: %w", err)
		}
	}
	re.RPMModel.Package.Content = strfmt.Base64(artifactBytes)

	re.RPMModel.PublicKey = &models.RpmV001SchemaPublicKey{}
	publicKeyBytes := props.PublicKeyBytes
	if len(publicKeyBytes) == 0 {
		if len(props.PublicKeyPaths) != 1 {
			return nil, errors.New("only one public key must be provided to verify RPM signature")
		}
		if props.PublicKeyPaths[0] == nil {
			return nil, errors.New("public key path must be specified")
		}
		keyBytes, err := os.ReadFile(filepath.Clean(props.PublicKeyPaths[0].Path))
		if err != nil {
			return nil, fmt.Errorf("error reading public key file: %w", err)
		}
		publicKeyBytes = append(publicKeyBytes, keyBytes)
	} else if len(publicKeyBytes) != 1 {
		return nil, errors.New("only one public key must be provided")
	}

	re.RPMModel.PublicKey.Content = (*strfmt.Base64)(&publicKeyBytes[0])

	if err := re.validate(); err != nil {
		return nil, err
	}

	if _, _, err := re.fetchExternalEntities(context.Background()); err != nil {
		return nil, fmt.Errorf("error retrieving external entities: %w", err)
	}

	returnVal.APIVersion = conv.Pointer(re.APIVersion())
	returnVal.Spec = re.RPMModel

	return &returnVal, nil
}

func (v V001Entry) Verifiers() ([]pki.PublicKey, error) {
	if v.RPMModel.PublicKey == nil || v.RPMModel.PublicKey.Content == nil {
		return nil, errors.New("rpm v0.0.1 entry not initialized")
	}
	key, err := pgp.NewPublicKey(bytes.NewReader(*v.RPMModel.PublicKey.Content))
	if err != nil {
		return nil, err
	}
	return []pki.PublicKey{key}, nil
}

func (v V001Entry) ArtifactHash() (string, error) {
	if v.RPMModel.Package == nil || v.RPMModel.Package.Hash == nil || v.RPMModel.Package.Hash.Value == nil || v.RPMModel.Package.Hash.Algorithm == nil {
		return "", errors.New("rpm v0.0.1 entry not initialized")
	}
	return strings.ToLower(fmt.Sprintf("%s:%s", *v.RPMModel.Package.Hash.Algorithm, *v.RPMModel.Package.Hash.Value)), nil
}

func (v V001Entry) Insertable() (bool, error) {
	if v.RPMModel.PublicKey == nil {
		return false, errors.New("missing publicKey property")
	}
	if v.RPMModel.PublicKey.Content == nil || len(*v.RPMModel.PublicKey.Content) == 0 {
		return false, errors.New("missing publicKey content")
	}

	if v.RPMModel.Package == nil {
		return false, errors.New("missing package property")
	}
	if len(v.RPMModel.Package.Content) == 0 {
		return false, errors.New("missing package content")
	}
	return true, nil
}
