package msg

import (
	"errors"
	"net"
	"strconv"
	"time"

	tap "github.com/dnstap/golang-dnstap"
	"github.com/miekg/dns"
)

// Builder helps to build a Dnstap message.
type Builder struct {
	Packed      []byte
	SocketProto tap.SocketProtocol
	SocketFam   tap.SocketFamily
	Address     net.IP
	Port        uint32
	TimeSec     uint64
	TimeNsec    uint32

	err error
}

// New returns a new Builder
func New() *Builder {
	return &Builder{}
}

// Addr adds the remote address to the message.
func (b *Builder) Addr(remote net.Addr) *Builder {
	if b.err != nil {
		return b
	}

	switch addr := remote.(type) {
	case *net.TCPAddr:
		b.Address = addr.IP
		b.Port = uint32(addr.Port)
		b.SocketProto = tap.SocketProtocol_TCP
	case *net.UDPAddr:
		b.Address = addr.IP
		b.Port = uint32(addr.Port)
		b.SocketProto = tap.SocketProtocol_UDP
	default:
		b.err = errors.New("unknown remote address type")
		return b
	}

	if b.Address.To4() != nil {
		b.SocketFam = tap.SocketFamily_INET
	} else {
		b.SocketFam = tap.SocketFamily_INET6
	}
	return b
}

// Msg adds the raw DNS message to the dnstap message.
func (b *Builder) Msg(m *dns.Msg) *Builder {
	if b.err != nil {
		return b
	}

	b.Packed, b.err = m.Pack()
	return b
}

// HostPort adds the remote address as encoded by dnsutil.ParseHostPortOrFile to the message.
func (b *Builder) HostPort(addr string) *Builder {
	ip, port, err := net.SplitHostPort(addr)
	if err != nil {
		b.err = err
		return b
	}
	p, err := strconv.ParseUint(port, 10, 32)
	if err != nil {
		b.err = err
		return b
	}
	b.Port = uint32(p)

	if ip := net.ParseIP(ip); ip != nil {
		b.Address = []byte(ip)
		if ip := ip.To4(); ip != nil {
			b.SocketFam = tap.SocketFamily_INET
		} else {
			b.SocketFam = tap.SocketFamily_INET6
		}
		return b
	}
	b.err = errors.New("not an ip address")
	return b
}

// Time adds the timestamp to the message.
func (b *Builder) Time(ts time.Time) *Builder {
	b.TimeSec = uint64(ts.Unix())
	b.TimeNsec = uint32(ts.Nanosecond())
	return b
}

// ToClientResponse transforms Data into a client response message.
func (b *Builder) ToClientResponse() (*tap.Message, error) {
	t := tap.Message_CLIENT_RESPONSE
	return &tap.Message{
		Type:             &t,
		SocketFamily:     &b.SocketFam,
		SocketProtocol:   &b.SocketProto,
		ResponseTimeSec:  &b.TimeSec,
		ResponseTimeNsec: &b.TimeNsec,
		ResponseMessage:  b.Packed,
		QueryAddress:     b.Address,
		QueryPort:        &b.Port,
	}, b.err
}

// ToClientQuery transforms Data into a client query message.
func (b *Builder) ToClientQuery() (*tap.Message, error) {
	t := tap.Message_CLIENT_QUERY
	return &tap.Message{
		Type:           &t,
		SocketFamily:   &b.SocketFam,
		SocketProtocol: &b.SocketProto,
		QueryTimeSec:   &b.TimeSec,
		QueryTimeNsec:  &b.TimeNsec,
		QueryMessage:   b.Packed,
		QueryAddress:   b.Address,
		QueryPort:      &b.Port,
	}, b.err
}

// ToOutsideQuery transforms the data into a forwarder or resolver query message.
func (b *Builder) ToOutsideQuery(t tap.Message_Type) (*tap.Message, error) {
	return &tap.Message{
		Type:            &t,
		SocketFamily:    &b.SocketFam,
		SocketProtocol:  &b.SocketProto,
		QueryTimeSec:    &b.TimeSec,
		QueryTimeNsec:   &b.TimeNsec,
		QueryMessage:    b.Packed,
		ResponseAddress: b.Address,
		ResponsePort:    &b.Port,
	}, b.err
}

// ToOutsideResponse transforms the data into a forwarder or resolver response message.
func (b *Builder) ToOutsideResponse(t tap.Message_Type) (*tap.Message, error) {
	return &tap.Message{
		Type:             &t,
		SocketFamily:     &b.SocketFam,
		SocketProtocol:   &b.SocketProto,
		ResponseTimeSec:  &b.TimeSec,
		ResponseTimeNsec: &b.TimeNsec,
		ResponseMessage:  b.Packed,
		ResponseAddress:  b.Address,
		ResponsePort:     &b.Port,
	}, b.err
}
