package drivers

import (
	"context"
	"encoding/binary"
	"hash/fnv"
	"sort"

	"github.com/MontFerret/ferret/pkg/runtime/core"
	"github.com/MontFerret/ferret/pkg/runtime/values"
	"github.com/MontFerret/ferret/pkg/runtime/values/types"

	"github.com/wI2L/jettison"
)

type HTTPCookies struct {
	values map[string]HTTPCookie
}

func NewHTTPCookies() *HTTPCookies {
	return NewHTTPCookiesWith(make(map[string]HTTPCookie))
}

func NewHTTPCookiesWith(values map[string]HTTPCookie) *HTTPCookies {
	return &HTTPCookies{values}
}

func (c *HTTPCookies) MarshalJSON() ([]byte, error) {
	return jettison.MarshalOpts(c.values, jettison.NoHTMLEscaping())
}

func (c *HTTPCookies) Type() core.Type {
	return HTTPCookiesType
}

func (c *HTTPCookies) String() string {
	j, err := c.MarshalJSON()

	if err != nil {
		return "{}"
	}

	return string(j)
}

func (c *HTTPCookies) Compare(other core.Value) int64 {
	if other.Type() != HTTPCookiesType {
		return Compare(HTTPCookiesType, other.Type())
	}

	oc := other.(*HTTPCookies)

	switch {
	case len(c.values) > len(oc.values):
		return 1
	case len(c.values) < len(oc.values):
		return -1
	}

	for name := range c.values {
		cEl, cExists := c.Get(values.NewString(name))

		if !cExists {
			return -1
		}

		ocEl, ocExists := oc.Get(values.NewString(name))

		if !ocExists {
			return 1
		}

		c := cEl.Compare(ocEl)

		if c != 0 {
			return c
		}
	}

	return 0
}

func (c *HTTPCookies) Unwrap() interface{} {
	return c.values
}

func (c *HTTPCookies) Hash() uint64 {
	hash := fnv.New64a()

	hash.Write([]byte(c.Type().String()))
	hash.Write([]byte(":"))
	hash.Write([]byte("{"))

	keys := make([]string, 0, len(c.values))

	for key := range c.values {
		keys = append(keys, key)
	}

	// order does not really matter
	// but it will give us a consistent hash sum
	sort.Strings(keys)
	endIndex := len(keys) - 1

	for idx, key := range keys {
		hash.Write([]byte(key))
		hash.Write([]byte(":"))

		el := c.values[key]

		bytes := make([]byte, 8)
		binary.LittleEndian.PutUint64(bytes, el.Hash())

		hash.Write(bytes)

		if idx != endIndex {
			hash.Write([]byte(","))
		}
	}

	hash.Write([]byte("}"))

	return hash.Sum64()
}

func (c *HTTPCookies) Copy() core.Value {
	return NewHTTPCookiesWith(c.values)
}

func (c *HTTPCookies) Clone() core.Cloneable {
	clone := make(map[string]HTTPCookie)

	for _, cookie := range c.values {
		clone[cookie.Name] = cookie
	}

	return NewHTTPCookiesWith(clone)
}

func (c *HTTPCookies) Length() values.Int {
	return values.NewInt(len(c.values))
}

func (c *HTTPCookies) Keys() []values.String {
	result := make([]values.String, 0, len(c.values))

	for k := range c.values {
		result = append(result, values.NewString(k))
	}

	return result
}

func (c *HTTPCookies) Values() []HTTPCookie {
	result := make([]HTTPCookie, 0, len(c.values))

	for _, v := range c.values {
		result = append(result, v)
	}

	return result
}

func (c *HTTPCookies) Get(key values.String) (HTTPCookie, values.Boolean) {
	value, found := c.values[key.String()]

	if found {
		return value, values.True
	}

	return HTTPCookie{}, values.False
}

func (c *HTTPCookies) Set(cookie HTTPCookie) {
	c.values[cookie.Name] = cookie
}

func (c *HTTPCookies) GetIn(ctx context.Context, path []core.Value) (core.Value, core.PathError) {
	if len(path) == 0 {
		return values.None, nil
	}

	segmentIdx := 0
	segment := path[segmentIdx]

	err := core.ValidateType(segment, types.String)

	if err != nil {
		return values.None, core.NewPathError(err, segmentIdx)
	}

	cookie, found := c.values[segment.String()]

	if found {
		if len(path) == 1 {
			return cookie, nil
		}

		return values.GetIn(ctx, cookie, path[segmentIdx+1:])
	}

	return values.None, nil
}

func (c *HTTPCookies) ForEach(predicate func(value HTTPCookie, key values.String) bool) {
	for key, val := range c.values {
		if !predicate(val, values.NewString(key)) {
			break
		}
	}
}