package webfinger

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"hash/fnv"
	"sort"

	"go.sour.is/pkg/set"
	"go.sour.is/pkg/slice"
	"gopkg.in/yaml.v3"

	"go.sour.is/ev/event"
)

func StreamID(subject string) string {
	h := fnv.New128a()
	h.Write([]byte(subject))
	return "webfinger." + base64.RawURLEncoding.EncodeToString(h.Sum(nil))
}

// JRD is a JSON Resource Descriptor, specifying properties and related links
// for a resource.
type JRD struct {
	Subject           string             `json:"subject,omitempty" yaml:"subject,omitempty"`
	Aliases           []string           `json:"aliases,omitempty" yaml:"aliases,omitempty"`
	Properties        map[string]*string `json:"properties,omitempty" yaml:"properties,omitempty"`
	Links             Links              `json:"links,omitempty" yaml:"links,omitempty"`
	deleted           bool
	event.IsAggregate `json:"-" yaml:"-"`
}

func (a *JRD) CloneValues() *JRD {
	m := make(map[string]*string, len(a.Properties))
	for k, v := range a.Properties {
		m[k] = v
	}
	return &JRD{
		Subject:    a.Subject,
		Aliases:    append([]string{}, a.Aliases...),
		Properties: m,
		Links:      append([]*Link{}, a.Links...),
	}
}

var _ event.Aggregate = (*JRD)(nil)

// Link is a link to a related resource.
type Link struct {
	Index      uint64             `json:"-" yaml:"-"`
	Rel        string             `json:"rel,omitempty"`
	Type       string             `json:"type,omitempty"`
	HRef       string             `json:"href,omitempty"`
	Titles     map[string]string  `json:"titles,omitempty"`
	Properties map[string]*string `json:"properties,omitempty"`
	Template   string             `json:"template,omitempty"`
}

type Links []*Link

// Len is the number of elements in the collection.
func (l Links) Len() int {
	if l == nil {
		return 0
	}
	return len(l)
}

// Less reports whether the element with index i
func (l Links) Less(i int, j int) bool {
	if l[i] == nil || l[j] == nil {
		return false
	}
	if l[i].Rel == l[j].Rel {
		return l[i].Type < l[j].Type
	}
	return l[i].Rel < l[j].Rel
}

// Swap swaps the elements with indexes i and j.
func (l Links) Swap(i int, j int) {
	if l == nil {
		return
	}
	l[i], l[j] = l[j], l[i]
}

// ParseJRD parses the JRD using json.Unmarshal.
func ParseJRD(blob []byte) (*JRD, error) {
	jrd := JRD{}
	err := json.Unmarshal(blob, &jrd)
	if err != nil {
		return nil, err
	}
	for i := range jrd.Links {
		jrd.Links[i].Index = uint64(i)
	}
	return &jrd, nil
}

// GetLinkByRel returns the first *Link with the specified rel value.
func (jrd *JRD) GetLinkByRel(rel string) *Link {
	for _, link := range jrd.Links {
		if link.Rel == rel {
			return link
		}
	}
	return nil
}

// GetLinksByRel returns each *Link with the specified rel value.
func (jrd *JRD) GetLinksByRel(rel ...string) []*Link {
	var lis []*Link
	rels := set.New(rel...)

	for _, link := range jrd.Links {
		if rels.Has(link.Rel) {
			lis = append(lis, link)
		}
	}
	return lis
}

// GetProperty Returns the property value as a string.
// Per spec a property value can be null, empty string is returned in this case.
func (jrd *JRD) GetProperty(uri string) string {
	if jrd.Properties[uri] == nil {
		return ""
	}
	return *jrd.Properties[uri]
}
func (a *JRD) SetProperty(name string, value *string) {
	if a.Properties == nil {
		a.Properties = make(map[string]*string)
	}
	a.Properties[name] = value
}
func (a *JRD) DeleteProperty(name string) {
	if a.Properties == nil {
		return
	}
	delete(a.Properties, name)
}
func (a *JRD) IsDeleted() bool {
	return a.deleted
}

// GetProperty Returns the property value as a string.
// Per spec a property value can be null, empty string is returned in this case.
func (link *Link) GetProperty(uri string) string {
	if link.Properties[uri] == nil {
		return ""
	}
	return *link.Properties[uri]
}
func (link *Link) SetProperty(name string, value *string) {
	if link.Properties == nil {
		link.Properties = make(map[string]*string)
	}
	link.Properties[name] = value
}
func (link *Link) DeleteProperty(name string) {
	if link.Properties == nil {
		return
	}
	delete(link.Properties, name)
}

// ApplyEvent implements event.Aggregate
func (a *JRD) ApplyEvent(events ...event.Event) {
	for _, e := range events {
		switch e := e.(type) {
		case *SubjectSet:
			a.deleted = false

			a.Subject = e.Subject
			a.Aliases = e.Aliases
			a.Properties = e.Properties

		case *SubjectDeleted:
			a.deleted = true

			a.Subject = e.Subject
			a.Aliases = a.Aliases[:0]
			a.Links = a.Links[:0]
			a.Properties = map[string]*string{}

		case *LinkSet:
			link, ok := slice.FindFn(func(l *Link) bool { return l.Index == e.Index }, a.Links...)
			if !ok {
				link = &Link{}
				link.Index = uint64(len(a.Links))
				a.Links = append(a.Links, link)
			}

			link.Rel = e.Rel
			link.HRef = e.HRef
			link.Type = e.Type
			link.Titles = e.Titles
			link.Properties = e.Properties
			link.Template = e.Template

		case *LinkDeleted:
			a.Links = slice.FilterFn(func(link *Link) bool { return link.Index != e.Index }, a.Links...)
		}
	}
}

const NSauth = "https://sour.is/ns/auth"
const NSpubkey = "https://sour.is/ns/pubkey"
const NSredirect = "https://sour.is/rel/redirect"

func (a *JRD) OnAuth(claim, auth *JRD) error {
	pubkey := claim.Properties[NSpubkey]

	if v, ok := auth.Properties[NSpubkey]; ok && v != nil && cmpPtr(v, pubkey) {
		// pubkey matches!
	} else {
		return fmt.Errorf("pubkey does not match")
	}

	if a.Version() > 0 && !a.IsDeleted() && a.Subject != claim.Subject {
		return fmt.Errorf("subject does not match")
	}

	if auth.Subject == claim.Subject {
		claim.SetProperty(NSpubkey, pubkey)
	} else {
		claim.SetProperty(NSauth, &auth.Subject)
		claim.DeleteProperty(NSpubkey)
	}

	return nil
}

func (a *JRD) OnDelete(jrd *JRD) error {
	if a.Version() == 0 || a.IsDeleted() {
		return nil
	}

	event.Raise(a, &SubjectDeleted{Subject: jrd.Subject})
	return nil
}

func (a *JRD) OnClaims(jrd *JRD) error {

	err := a.OnSubjectSet(jrd.Subject, jrd.Aliases, jrd.Properties)
	if err != nil {
		return err
	}

	for _, z := range slice.Align(
		a.Links,   // old
		jrd.Links, // new
		func(l, r *Link) bool { return l.Index < r.Index },
	) {
		// Not in new == delete
		if z.Key == nil {
			link := *z.Value
			event.Raise(a, &LinkDeleted{Index: link.Index, Rel: link.Rel})
			continue
		}

		// Not in old == create
		if z.Value == nil {
			link := *z.Key
			event.Raise(a, &LinkSet{
				Index:      link.Index,
				Rel:        link.Rel,
				Type:       link.Type,
				HRef:       link.HRef,
				Titles:     link.Titles,
				Properties: link.Properties,
				Template:   link.Template,
			})
			continue
		}

		// in both == compare
		a.OnLinkSet(*z.Key, *z.Value)
	}

	return nil
}

func (a *JRD) OnSubjectSet(subject string, aliases []string, props map[string]*string) error {
	modified := false
	e := &SubjectSet{
		Subject:    subject,
		Aliases:    aliases,
		Properties: props,
	}

	if subject != a.Subject {
		modified = true
	}

	sort.Strings(aliases)
	sort.Strings(a.Aliases)
	for _, z := range slice.Zip(aliases, a.Aliases) {
		if z.Key != z.Value {
			modified = true
			break
		}
	}

	for _, z := range slice.Zip(
		slice.Zip(slice.FromMap(props)),
		slice.Zip(slice.FromMap(a.Properties)),
	) {
		newValue := z.Key
		curValue := z.Value

		if newValue.Key != curValue.Key {
			modified = true
			break
		}

		if !cmpPtr(newValue.Value, curValue.Value) {
			modified = true
			break
		}
	}

	if modified {
		event.Raise(a, e)
	}

	return nil
}

func (a *JRD) OnLinkSet(o, n *Link) error {
	modified := false
	e := &LinkSet{
		Index:      n.Index,
		Rel:        n.Rel,
		Type:       n.Type,
		HRef:       n.HRef,
		Titles:     n.Titles,
		Properties: n.Properties,
		Template:   n.Template,
	}

	if n.Rel != o.Rel {
		modified = true
	}
	if n.Type != o.Type {
		modified = true
	}
	if n.HRef != o.HRef {
		modified = true
	}
	if n.Template != o.Template {
		fmt.Println(360, n.Template, o.Template, e.Template)

		modified = true
	}

	nKeys := slice.FromMapKeys(n.Properties)
	sort.Strings(nKeys)

	oKeys := slice.FromMapKeys(o.Properties)
	sort.Strings(oKeys)

	for _, z := range slice.Zip(
		slice.Zip(nKeys, slice.FromMapValues(n.Titles, nKeys)),
		slice.Zip(oKeys, slice.FromMapValues(o.Titles, oKeys)),
	) {
		if z.Key != z.Value {
			modified = true
			break
		}
	}

	nKeys = slice.FromMapKeys(n.Properties)
	sort.Strings(nKeys)

	oKeys = slice.FromMapKeys(o.Properties)
	sort.Strings(oKeys)

	for _, z := range slice.Zip(
		slice.Zip(nKeys, slice.FromMapValues(n.Properties, nKeys)),
		slice.Zip(oKeys, slice.FromMapValues(o.Properties, oKeys)),
	) {
		newValue := z.Key
		curValue := z.Value

		if newValue.Key != curValue.Key {
			modified = true
			break
		}

		if !cmpPtr(newValue.Value, curValue.Value) {
			modified = true
			break
		}
	}

	if modified {
		event.Raise(a, e)
	}

	return nil
}

func cmpPtr[T comparable](l, r *T) bool {
	if l == nil {
		return r == nil
	}

	if r == nil {
		return l == nil
	}

	return *l == *r
}

func (a *JRD) String() string {
	b := &bytes.Buffer{}
	y := yaml.NewEncoder(b)
	_ = y.Encode(a)

	return b.String()
}
