package filesig

import (
	"fmt"
	"time"

	"github.com/safing/jess"
	"github.com/safing/jess/lhash"
	"github.com/safing/structures/dsd"
)

// Extension holds the default file extension to be used for signature files.
const Extension = ".sig"

var fileSigRequirements = jess.NewRequirements().
	Remove(jess.RecipientAuthentication).
	Remove(jess.Confidentiality)

// FileData describes a file that is signed.
type FileData struct {
	LabeledHash []byte
	fileHash    *lhash.LabeledHash

	SignedAt time.Time
	MetaData map[string]string

	signature         *jess.Letter
	verificationError error
}

// FileHash returns the labeled hash of the file that was signed.
func (fd *FileData) FileHash() *lhash.LabeledHash {
	return fd.fileHash
}

// Signature returns the signature, if present.
func (fd *FileData) Signature() *jess.Letter {
	return fd.signature
}

// VerificationError returns the error encountered during verification.
func (fd *FileData) VerificationError() error {
	return fd.verificationError
}

// SignFileData signs the given file checksum and metadata.
func SignFileData(fileHash *lhash.LabeledHash, metaData map[string]string, envelope *jess.Envelope, trustStore jess.TrustStore) (letter *jess.Letter, fd *FileData, err error) {
	// Create session.
	session, err := envelope.Correspondence(trustStore)
	if err != nil {
		return nil, nil, err
	}

	// Check if the envelope is suitable for signing.
	if err := envelope.Suite().Provides.CheckComplianceTo(fileSigRequirements); err != nil {
		return nil, nil, fmt.Errorf("envelope not suitable for signing: %w", err)
	}

	// Create struct and transform data into serializable format to be signed.
	fd = &FileData{
		SignedAt: time.Now().Truncate(time.Second),
		fileHash: fileHash,
		MetaData: metaData,
	}
	fd.LabeledHash = fd.fileHash.Bytes()

	// Serialize file signature.
	fileData, err := dsd.Dump(fd, dsd.MsgPack)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to serialize file signature data: %w", err)
	}

	// Sign data.
	letter, err = session.Close(fileData)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to sign: %w", err)
	}

	return letter, fd, nil
}

// VerifyFileData verifies the given signed file data and returns the file data.
// If an error is returned, there was an error in at least some part of the process.
// Any returned file data struct must be checked for an verification error.
func VerifyFileData(letter *jess.Letter, requiredMetaData map[string]string, trustStore jess.TrustStore) (fd *FileData, err error) {
	// Parse data.
	fd = &FileData{
		signature: letter,
	}
	_, err = dsd.Load(letter.Data, fd)
	if err != nil {
		return nil, fmt.Errorf("failed to parse file signature data: %w", err)
	}

	// Verify signature and get data.
	_, err = letter.Open(fileSigRequirements, trustStore)
	if err != nil {
		fd.verificationError = fmt.Errorf("failed to verify file signature: %w", err)
		return fd, fd.verificationError
	}

	// Check if the required metadata matches.
	for reqKey, reqValue := range requiredMetaData {
		sigMetaValue, ok := fd.MetaData[reqKey]
		if !ok {
			fd.verificationError = fmt.Errorf("missing required metadata key %q", reqKey)
			return fd, fd.verificationError
		}
		if sigMetaValue != reqValue {
			fd.verificationError = fmt.Errorf("required metadata %q=%q does not match the file's value %q", reqKey, reqValue, sigMetaValue)
			return fd, fd.verificationError
		}
	}

	// Parse labeled hash.
	fd.fileHash, err = lhash.Load(fd.LabeledHash)
	if err != nil {
		fd.verificationError = fmt.Errorf("failed to parse file checksum: %w", err)
		return fd, fd.verificationError
	}

	return fd, nil
}