package lhash

import (
	"crypto/subtle"
	"encoding/base64"
	"encoding/hex"
	"errors"
	"fmt"

	"github.com/mr-tron/base58"

	"github.com/safing/portbase/container"
)

// LabeledHash represents a typed hash value.
type LabeledHash struct {
	alg    Algorithm
	digest []byte
}

// Digest creates a new labeled hash and digests the given data.
func Digest(alg Algorithm, data []byte) *LabeledHash {
	hasher := alg.new()
	_, _ = hasher.Write(data) // never returns an error
	defer hasher.Reset()      // internal state may leak data if kept in memory

	return &LabeledHash{
		alg:    alg,
		digest: hasher.Sum(nil),
	}
}

// Load loads a labeled hash from the given []byte slice.
func Load(labeledHash []byte) (*LabeledHash, error) {
	c := container.New(labeledHash)

	algID, err := c.GetNextN64()
	if err != nil {
		return nil, fmt.Errorf("failed to parse algorithm ID: %w", err)
	}

	digest, err := c.GetNextBlock()
	if err != nil {
		return nil, fmt.Errorf("failed to parse digest: %w", err)
	}

	if c.Length() > 0 {
		return nil, errors.New("integrity error: data left over after parsing")
	}

	alg := Algorithm(uint(algID))
	if alg.new() == nil {
		return nil, errors.New("compatibility error: invalid or unsupported algorithm")
	}

	if alg.new().Size() != len(digest) {
		return nil, errors.New("integrity error: invalid digest length")
	}

	return &LabeledHash{
		alg:    alg,
		digest: digest,
	}, nil
}

// FromHex loads a labeled hash from the given hexadecimal string.
func FromHex(hexEncoded string) (*LabeledHash, error) {
	raw, err := hex.DecodeString(hexEncoded)
	if err != nil {
		return nil, fmt.Errorf("failed to decode hex: %w", err)
	}

	return Load(raw)
}

// FromBase64 loads a labeled hash from the given Base64 string using raw url
// encoding.
func FromBase64(base64Encoded string) (*LabeledHash, error) {
	raw, err := base64.RawURLEncoding.DecodeString(base64Encoded)
	if err != nil {
		return nil, fmt.Errorf("failed to decode base64: %w", err)
	}

	return Load(raw)
}

// FromBase58 loads a labeled hash from the given Base58 string using the BTC
// alphabet.
func FromBase58(base58Encoded string) (*LabeledHash, error) {
	raw, err := base58.Decode(base58Encoded)
	if err != nil {
		return nil, fmt.Errorf("failed to decode base58: %w", err)
	}

	return Load(raw)
}

// Bytes return the []byte representation of the labeled hash.
func (lh *LabeledHash) Bytes() []byte {
	c := container.New()
	c.AppendNumber(uint64(lh.alg))
	c.AppendAsBlock(lh.digest)
	return c.CompileData()
}

// Hex returns the hexadecimal string representation of the labeled hash.
func (lh *LabeledHash) Hex() string {
	return hex.EncodeToString(lh.Bytes())
}

// Base64 returns the Base64 string representation of the labeled hash using
// raw url encoding.
func (lh *LabeledHash) Base64() string {
	return base64.RawURLEncoding.EncodeToString(lh.Bytes())
}

// Base58 returns the Base58 string representation of the labeled hash using
// the BTC alphabet.
func (lh *LabeledHash) Base58() string {
	return base58.Encode(lh.Bytes())
}

// Equal returns true if the given labeled hash is equal.
// Equality is checked by comparing both the algorithm and the digest value.
func (lh *LabeledHash) Equal(other *LabeledHash) bool {
	return lh.alg == other.alg &&
		subtle.ConstantTimeCompare(lh.digest, other.digest) == 1
}

// MatchesString returns true if the digest of the given string matches the hash.
func (lh *LabeledHash) MatchesString(s string) bool {
	return lh.MatchesData([]byte(s))
}

// MatchesData returns true if the digest of the given data matches the hash.
func (lh *LabeledHash) MatchesData(data []byte) bool {
	hasher := lh.alg.new()
	_, _ = hasher.Write(data) // never returns an error
	defer hasher.Reset()      // internal state may leak data if kept in memory

	return subtle.ConstantTimeCompare(lh.digest, hasher.Sum(nil)) == 1
}