package principal

import (
	"bytes"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base32"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"github.com/fxamacker/cbor/v2"
	"hash/crc32"
	"strings"
)

// AnonymousID is used for the anonymous caller. It can be used in call and query requests without a signature.
var AnonymousID = Principal{Raw: []byte{0x04}}

var encoding = base32.StdEncoding.WithPadding(base32.NoPadding)

// Principal are generic identifiers for canisters, users and possibly other concepts in the future.
type Principal struct {
	Raw []byte
}

// Decode converts a textual representation into a principal.
func Decode(s string) (Principal, error) {
	p := strings.Split(s, "-")
	for i, c := range p {
		if len(c) > 5 {
			return Principal{}, fmt.Errorf("invalid length: %s", c)
		}
		if i != len(p)-1 && len(c) < 5 {
			return Principal{}, fmt.Errorf("invalid length: %s", c)
		}
	}
	b32, err := encoding.DecodeString(strings.ToUpper(strings.Join(p, "")))
	if err != nil {
		return Principal{}, err
	}
	if len(b32) < 4 {
		return Principal{}, fmt.Errorf("invalid length: %s", b32)
	}
	if crc32.ChecksumIEEE(b32[4:]) != binary.BigEndian.Uint32(b32[:4]) {
		return Principal{}, fmt.Errorf("invalid checksum: %s", b32)
	}
	return Principal{b32[4:]}, err
}

// MustDecode converts a textual representation into a principal. It panics if the input is invalid.
func MustDecode(s string) Principal {
	p, err := Decode(s)
	if err != nil {
		panic(err)
	}
	return p
}

// NewRandomPrincipal returns a new random principal.
func NewRandomPrincipal() Principal {
	var raw [29]byte
	_, err := rand.Read(raw[:])
	if err != nil {
		panic(err)
	}
	return Principal{Raw: raw[:]}
}

// NewSelfAuthenticating returns a self authenticating principal identifier based on the given public key.
func NewSelfAuthenticating(pub []byte) Principal {
	hash := sha256.Sum224(pub)
	return Principal{
		Raw: append(hash[:], 0x02),
	}
}

// Encode converts the principal to its textual representation.
func (p Principal) Encode() string {
	cs := make([]byte, 4)
	binary.BigEndian.PutUint32(cs, crc32.ChecksumIEEE(p.Raw))
	b32 := strings.ToLower(encoding.EncodeToString(append(cs, p.Raw...)))
	var str string
	for i, c := range b32 {
		if i != 0 && i%5 == 0 {
			str += "-"
		}
		str += string(c)
	}
	return str
}

// Equal checks if two principals are equal.
func (p Principal) Equal(other Principal) bool {
	return bytes.Equal(p.Raw, other.Raw)
}

// IsAnonymous checks if the principal is the anonymous id.
func (p Principal) IsAnonymous() bool {
	return bytes.Equal(p.Raw, AnonymousID.Raw)
}

// IsDerived checks if the principal is derived.
func (p Principal) IsDerived() bool {
	return p.Raw[len(p.Raw)-1] == 0x03
}

// IsOpaque checks if the principal is opaque, these are always generated by the IC and have no structure of interest
// outside of it.
func (p Principal) IsOpaque() bool {
	return p.Raw[len(p.Raw)-1] == 0x01
}

// IsReserved checks if the principal is reserved. These ids can be useful for applications that want to re-use the
// Textual representation of principals but want to indicate explicitly that the blob does not address any canisters or
// a user.
func (p Principal) IsReserved() bool {
	if 29 < len(p.Raw) {
		return false
	}
	return p.Raw[len(p.Raw)-1] == 0x7f
}

// IsSelfAuthenticating checks if the principal is self authenticating, an external user can use these ids as the sender
// of a request if they own the corresponding private key.
func (p Principal) IsSelfAuthenticating() bool {
	return p.Raw[len(p.Raw)-1] == 0x02
}

// MarshalCBOR converts the principal to its CBOR representation.
func (p Principal) MarshalCBOR() ([]byte, error) {
	return cbor.Marshal(p.Raw)
}

// MarshalJSON converts the principal to its JSON representation as a string.
func (p Principal) MarshalJSON() ([]byte, error) {
	return json.Marshal(p.String())
}

// String implements the Stringer interface.
func (p Principal) String() string {
	return p.Encode()
}

// UnmarshalCBOR converts a CBOR representation into a principal.
func (p *Principal) UnmarshalCBOR(bytes []byte) error {
	return cbor.Unmarshal(bytes, &p.Raw)
}

// UnmarshalJSON converts the JSON bytes into a principal from a string.
func (p *Principal) UnmarshalJSON(bytes []byte) error {
	var principal string
	if err := json.Unmarshal(bytes, &principal); err != nil {
		return err
	}
	decoded, err := Decode(principal)
	if err != nil {
		return err
	}
	*p = decoded
	return nil
}
