package core

import (
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"runtime"
	"time"

	iwt "github.com/Arceliar/ironwood/types"
	"github.com/Arceliar/phony"
	"github.com/yggdrasil-network/yggdrasil-go/src/version"
)

type nodeinfo struct {
	phony.Inbox
	proto      *protoHandler
	myNodeInfo json.RawMessage
	callbacks  map[keyArray]nodeinfoCallback
}

type nodeinfoCallback struct {
	call    func(nodeinfo json.RawMessage)
	created time.Time
}

// Initialises the nodeinfo cache/callback maps, and starts a goroutine to keep
// the cache/callback maps clean of stale entries
func (m *nodeinfo) init(proto *protoHandler) {
	m.Act(nil, func() {
		m._init(proto)
	})
}

func (m *nodeinfo) _init(proto *protoHandler) {
	m.proto = proto
	m.callbacks = make(map[keyArray]nodeinfoCallback)
	m._cleanup()
}

func (m *nodeinfo) _cleanup() {
	for boxPubKey, callback := range m.callbacks {
		if time.Since(callback.created) > time.Minute {
			delete(m.callbacks, boxPubKey)
		}
	}
	time.AfterFunc(time.Second*30, func() {
		m.Act(nil, m._cleanup)
	})
}

func (m *nodeinfo) _addCallback(sender keyArray, call func(nodeinfo json.RawMessage)) {
	m.callbacks[sender] = nodeinfoCallback{
		created: time.Now(),
		call:    call,
	}
}

// Handles the callback, if there is one
func (m *nodeinfo) _callback(sender keyArray, nodeinfo json.RawMessage) {
	if callback, ok := m.callbacks[sender]; ok {
		callback.call(nodeinfo)
		delete(m.callbacks, sender)
	}
}

func (m *nodeinfo) _getNodeInfo() json.RawMessage {
	return m.myNodeInfo
}

// Set the current node's nodeinfo
func (m *nodeinfo) setNodeInfo(given map[string]interface{}, privacy bool) (err error) {
	phony.Block(m, func() {
		err = m._setNodeInfo(given, privacy)
	})
	return
}

func (m *nodeinfo) _setNodeInfo(given map[string]interface{}, privacy bool) error {
	newnodeinfo := make(map[string]interface{}, len(given))
	for k, v := range given {
		newnodeinfo[k] = v
	}
	if !privacy {
		newnodeinfo["buildname"] = version.BuildName()
		newnodeinfo["buildversion"] = version.BuildVersion()
		newnodeinfo["buildplatform"] = runtime.GOOS
		newnodeinfo["buildarch"] = runtime.GOARCH
	}
	newjson, err := json.Marshal(newnodeinfo)
	switch {
	case err != nil:
		return fmt.Errorf("NodeInfo marshalling failed: %w", err)
	case len(newjson) > 16384:
		return fmt.Errorf("NodeInfo exceeds max length of 16384 bytes")
	default:
		m.myNodeInfo = newjson
		return nil
	}
}

func (m *nodeinfo) sendReq(from phony.Actor, key keyArray, callback func(nodeinfo json.RawMessage)) {
	m.Act(from, func() {
		m._sendReq(key, callback)
	})
}

func (m *nodeinfo) _sendReq(key keyArray, callback func(nodeinfo json.RawMessage)) {
	if callback != nil {
		m._addCallback(key, callback)
	}
	_, _ = m.proto.core.PacketConn.WriteTo([]byte{typeSessionProto, typeProtoNodeInfoRequest}, iwt.Addr(key[:]))
}

func (m *nodeinfo) handleReq(from phony.Actor, key keyArray) {
	m.Act(from, func() {
		m._sendRes(key)
	})
}

func (m *nodeinfo) handleRes(from phony.Actor, key keyArray, info json.RawMessage) {
	m.Act(from, func() {
		m._callback(key, info)
	})
}

func (m *nodeinfo) _sendRes(key keyArray) {
	bs := append([]byte{typeSessionProto, typeProtoNodeInfoResponse}, m._getNodeInfo()...)
	_, _ = m.proto.core.PacketConn.WriteTo(bs, iwt.Addr(key[:]))
}

// Admin socket stuff

type GetNodeInfoRequest struct {
	Key string `json:"key"`
}
type GetNodeInfoResponse map[string]json.RawMessage

func (m *nodeinfo) nodeInfoAdminHandler(in json.RawMessage) (interface{}, error) {
	var req GetNodeInfoRequest
	if err := json.Unmarshal(in, &req); err != nil {
		return nil, err
	}
	if req.Key == "" {
		return nil, fmt.Errorf("no remote public key supplied")
	}
	var key keyArray
	var kbs []byte
	var err error
	if kbs, err = hex.DecodeString(req.Key); err != nil {
		return nil, fmt.Errorf("failed to decode public key: %w", err)
	}
	copy(key[:], kbs)
	ch := make(chan []byte, 1)
	m.sendReq(nil, key, func(info json.RawMessage) {
		ch <- info
	})
	timer := time.NewTimer(6 * time.Second)
	defer timer.Stop()
	select {
	case <-timer.C:
		return nil, errors.New("timed out waiting for response")
	case info := <-ch:
		var msg json.RawMessage
		if err := msg.UnmarshalJSON(info); err != nil {
			return nil, err
		}
		key := hex.EncodeToString(kbs[:])
		res := GetNodeInfoResponse{key: msg}
		return res, nil
	}
}
