package filesig

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
	"strings"

	"github.com/safing/jess"
	"github.com/safing/jess/hashtools"
	"github.com/safing/jess/lhash"
)

// SignFile signs a file and replaces the signature file with a new one.
// If the dataFilePath is "-", the file data is read from stdin.
// Existing jess signatures in the signature file are removed.
func SignFile(dataFilePath, signatureFilePath string, metaData map[string]string, envelope *jess.Envelope, trustStore jess.TrustStore) (fileData *FileData, err error) {
	// Load encryption suite.
	if err := envelope.LoadSuite(); err != nil {
		return nil, err
	}

	// Extract the used hashing algorithm from the suite.
	var hashTool *hashtools.HashTool
	for _, toolID := range envelope.Suite().Tools {
		if strings.Contains(toolID, "(") {
			hashToolID := strings.Trim(strings.Split(toolID, "(")[1], "()")
			hashTool, _ = hashtools.Get(hashToolID)
			break
		}
	}
	if hashTool == nil {
		return nil, errors.New("suite not suitable for file signing")
	}

	// Hash the data file.
	var fileHash *lhash.LabeledHash
	if dataFilePath == "-" {
		fileHash, err = hashTool.LabeledHasher().DigestFromReader(os.Stdin)
	} else {
		fileHash, err = hashTool.LabeledHasher().DigestFile(dataFilePath)
	}
	if err != nil {
		return nil, fmt.Errorf("failed to hash file: %w", err)
	}

	// Sign the file data.
	signature, fileData, err := SignFileData(fileHash, metaData, envelope, trustStore)
	if err != nil {
		return nil, fmt.Errorf("failed to sign file: %w", err)
	}

	sigFileData, err := os.ReadFile(signatureFilePath)
	var newSigFileData []byte
	switch {
	case err == nil:
		// Add signature to existing file.
		newSigFileData, err = AddToSigFile(signature, sigFileData, true)
		if err != nil {
			return nil, fmt.Errorf("failed to add signature to file: %w", err)
		}
	case errors.Is(err, fs.ErrNotExist):
		// Make signature section for saving to disk.
		newSigFileData, err = MakeSigFileSection(signature)
		if err != nil {
			return nil, fmt.Errorf("failed to format signature for file: %w", err)
		}
	default:
		return nil, fmt.Errorf("failed to open existing signature file: %w", err)
	}

	// Write the signature to file.
	if err := os.WriteFile(signatureFilePath, newSigFileData, 0o0644); err != nil { //nolint:gosec
		return nil, fmt.Errorf("failed to write signature to file: %w", err)
	}

	return fileData, nil
}

// VerifyFile verifies the given files and returns the verified file data.
// If the dataFilePath is "-", the file data is read from stdin.
// 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 VerifyFile(dataFilePath, signatureFilePath string, metaData map[string]string, trustStore jess.TrustStore) (verifiedFileData []*FileData, err error) {
	var lastErr error

	// Read signature from file.
	sigFileData, err := os.ReadFile(signatureFilePath)
	if err != nil {
		return nil, fmt.Errorf("failed to read signature file: %w", err)
	}

	// Extract all signatures.
	sigs, err := ParseSigFile(sigFileData)
	switch {
	case len(sigs) == 0 && err != nil:
		return nil, fmt.Errorf("failed to parse signature file: %w", err)
	case len(sigs) == 0:
		return nil, errors.New("no signatures found in signature file")
	case err != nil:
		lastErr = fmt.Errorf("failed to parse signature file: %w", err)
	}

	// Verify all signatures.
	goodFileData := make([]*FileData, 0, len(sigs))
	var badFileData []*FileData
	for _, sigLetter := range sigs {
		// Verify signature.
		fileData, err := VerifyFileData(sigLetter, metaData, trustStore)
		if err != nil {
			lastErr = err
			if fileData != nil {
				fileData.verificationError = err
				badFileData = append(badFileData, fileData)
			}
			continue
		}

		// Hash the file.
		var fileHash *lhash.LabeledHash
		if dataFilePath == "-" {
			fileHash, err = fileData.FileHash().Algorithm().DigestFromReader(os.Stdin)
		} else {
			fileHash, err = fileData.FileHash().Algorithm().DigestFile(dataFilePath)
		}
		if err != nil {
			lastErr = err
			fileData.verificationError = err
			badFileData = append(badFileData, fileData)
			continue
		}

		// Check if the hash matches.
		if !fileData.FileHash().Equal(fileHash) {
			lastErr = errors.New("signature invalid: file was modified")
			fileData.verificationError = lastErr
			badFileData = append(badFileData, fileData)
			continue
		}

		// Add verified file data to list for return.
		goodFileData = append(goodFileData, fileData)
	}

	return append(goodFileData, badFileData...), lastErr
}