package nostr

import (
	"errors"
	"iter"
	"slices"
	"strings"
)

type Tag []string

// StartsWith checks if a tag contains a prefix.
// for example,
//
//	["p", "abcdef...", "wss://relay.com"]
//
// would match against
//
//	["p", "abcdef..."]
//
// or even
//
//	["p", "abcdef...", "wss://"]
func (tag Tag) StartsWith(prefix []string) bool {
	prefixLen := len(prefix)

	if prefixLen > len(tag) {
		return false
	}
	// check initial elements for equality
	for i := 0; i < prefixLen-1; i++ {
		if prefix[i] != tag[i] {
			return false
		}
	}
	// check last element just for a prefix
	return strings.HasPrefix(tag[prefixLen-1], prefix[prefixLen-1])
}

func (tag Tag) Key() string {
	if len(tag) > 0 {
		return tag[0]
	}
	return ""
}

func (tag Tag) Value() string {
	if len(tag) > 1 {
		return tag[1]
	}
	return ""
}

func (tag Tag) Relay() string {
	if len(tag) > 2 && (tag[0] == "e" || tag[0] == "p") {
		return NormalizeURL(tag[2])
	}
	return ""
}

type Tags []Tag

// GetD gets the first "d" tag (for parameterized replaceable events) value or ""
func (tags Tags) GetD() string {
	for _, v := range tags {
		if v.StartsWith([]string{"d", ""}) {
			return v[1]
		}
	}
	return ""
}

// GetFirst gets the first tag in tags that matches the prefix, see [Tag.StartsWith]
func (tags Tags) GetFirst(tagPrefix []string) *Tag {
	for _, v := range tags {
		if v.StartsWith(tagPrefix) {
			return &v
		}
	}
	return nil
}

// GetLast gets the last tag in tags that matches the prefix, see [Tag.StartsWith]
func (tags Tags) GetLast(tagPrefix []string) *Tag {
	for i := len(tags) - 1; i >= 0; i-- {
		v := tags[i]
		if v.StartsWith(tagPrefix) {
			return &v
		}
	}
	return nil
}

// GetAll gets all the tags that match the prefix, see [Tag.StartsWith]
func (tags Tags) GetAll(tagPrefix []string) Tags {
	result := make(Tags, 0, len(tags))
	for _, v := range tags {
		if v.StartsWith(tagPrefix) {
			result = append(result, v)
		}
	}
	return result
}

// All returns an iterator for all the tags that match the prefix, see [Tag.StartsWith]
func (tags Tags) All(tagPrefix []string) iter.Seq2[int, Tag] {
	return func(yield func(int, Tag) bool) {
		for i, v := range tags {
			if v.StartsWith(tagPrefix) {
				if !yield(i, v) {
					break
				}
			}
		}
	}
}

// FilterOut returns a new slice with only the elements that match the prefix, see [Tag.StartsWith]
func (tags Tags) FilterOut(tagPrefix []string) Tags {
	filtered := make(Tags, 0, len(tags))
	for _, v := range tags {
		if !v.StartsWith(tagPrefix) {
			filtered = append(filtered, v)
		}
	}
	return filtered
}

// FilterOutInPlace removes all tags that match the prefix, but potentially reorders the tags in unpredictable ways, see [Tag.StartsWith]
func (tags *Tags) FilterOutInPlace(tagPrefix []string) {
	for i := 0; i < len(*tags); i++ {
		tag := (*tags)[i]
		if tag.StartsWith(tagPrefix) {
			// remove this by swapping the last tag into this place
			last := len(*tags) - 1
			(*tags)[i] = (*tags)[last]
			*tags = (*tags)[0:last]
			i-- // this is so we can match this just swapped item in the next iteration
		}
	}
}

// AppendUnique appends a tag if it doesn't exist yet, otherwise does nothing.
// the uniqueness comparison is done based only on the first 2 elements of the tag.
func (tags Tags) AppendUnique(tag Tag) Tags {
	n := len(tag)
	if n > 2 {
		n = 2
	}

	if tags.GetFirst(tag[:n]) == nil {
		return append(tags, tag)
	}
	return tags
}

func (t *Tags) Scan(src any) error {
	var jtags []byte

	switch v := src.(type) {
	case []byte:
		jtags = v
	case string:
		jtags = []byte(v)
	default:
		return errors.New("couldn't scan tags, it's not a json string")
	}

	json.Unmarshal(jtags, &t)
	return nil
}

func (tags Tags) ContainsAny(tagName string, values []string) bool {
	for _, tag := range tags {
		if len(tag) < 2 {
			continue
		}

		if tag[0] != tagName {
			continue
		}

		if slices.Contains(values, tag[1]) {
			return true
		}
	}

	return false
}

// Marshal Tag. Used for Serialization so string escaping should be as in RFC8259.
func (tag Tag) marshalTo(dst []byte) []byte {
	dst = append(dst, '[')
	for i, s := range tag {
		if i > 0 {
			dst = append(dst, ',')
		}
		dst = escapeString(dst, s)
	}
	dst = append(dst, ']')
	return dst
}

// MarshalTo appends the JSON encoded byte of Tags as [][]string to dst.
// String escaping is as described in RFC8259.
func (tags Tags) marshalTo(dst []byte) []byte {
	dst = append(dst, '[')
	for i, tag := range tags {
		if i > 0 {
			dst = append(dst, ',')
		}
		dst = tag.marshalTo(dst)
	}
	dst = append(dst, ']')
	return dst
}
