package trace

import (
	"encoding/json"
	"net"
	"time"

	"github.com/gravitational/trace/internal"
	"github.com/jonboulle/clockwork"
	log "github.com/sirupsen/logrus"
)

const (
	// UDPDefaultAddr is a default address to emit logs to
	UDPDefaultAddr = "127.0.0.1:5000"
	// UDPDefaultNet is a default network
	UDPDefaultNet = "udp"
)

// UDPOptionSetter represents functional arguments passed to ELKHook
type UDPOptionSetter func(f *UDPHook)

// NewUDPHook returns logrus-compatible hook that sends data to UDP socket
func NewUDPHook(opts ...UDPOptionSetter) (*UDPHook, error) {
	f := &UDPHook{}
	for _, o := range opts {
		o(f)
	}
	if f.Clock == nil {
		f.Clock = clockwork.NewRealClock()
	}
	if f.clientNet == "" {
		f.clientNet = UDPDefaultNet
	}
	if f.clientAddr == "" {
		f.clientAddr = UDPDefaultAddr
	}
	addr, err := net.ResolveUDPAddr(f.clientNet, f.clientAddr)
	if err != nil {
		return nil, Wrap(err)
	}
	conn, err := net.ListenPacket("udp", ":0")
	if err != nil {
		return nil, Wrap(err)
	}
	f.addr = addr
	f.conn = conn.(*net.UDPConn)
	return f, nil
}

type UDPHook struct {
	Clock      clockwork.Clock
	clientNet  string
	clientAddr string
	addr       *net.UDPAddr
	conn       *net.UDPConn
}

type Frame struct {
	Time    time.Time              `json:"time"`
	Type    string                 `json:"type"`
	Entry   map[string]interface{} `json:"entry"`
	Message string                 `json:"message"`
	Level   string                 `json:"level"`
}

// Fire fires the event to the ELK beat
func (elk *UDPHook) Fire(e *log.Entry) error {
	// Make a copy to safely modify
	entry := e.WithFields(nil)
	if cursor := findFrame(); cursor != nil {
		t := internal.GetTracesFromCursor(*cursor)
		entry.Data[FileField] = t.String()
		entry.Data[FunctionField] = t.Func()
	}
	data, err := json.Marshal(Frame{
		Time:    elk.Clock.Now().UTC(),
		Type:    "trace",
		Entry:   entry.Data,
		Message: entry.Message,
		Level:   entry.Level.String(),
	})
	if err != nil {
		return Wrap(err)
	}

	conn, err := net.ListenPacket("udp", ":0")
	if err != nil {
		return Wrap(err)
	}
	defer conn.Close()

	resolvedAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:5000")
	if err != nil {
		return Wrap(err)
	}

	_, err = (conn.(*net.UDPConn)).WriteToUDP(data, resolvedAddr)
	return Wrap(err)

}

// Levels returns logging levels supported by logrus
func (elk *UDPHook) Levels() []log.Level {
	return []log.Level{
		log.PanicLevel,
		log.FatalLevel,
		log.ErrorLevel,
		log.WarnLevel,
		log.InfoLevel,
		log.DebugLevel,
	}
}
