package overlay

import (
	"encoding/binary"
	"fmt"
	"io"
	"net"
	"strings"

	"github.com/rcrowley/go-metrics"
	"github.com/sirupsen/logrus"
	"github.com/slackhq/nebula/iputil"
)

type disabledTun struct {
	read chan []byte
	cidr *net.IPNet

	// Track these metrics since we don't have the tun device to do it for us
	tx metrics.Counter
	rx metrics.Counter
	l  *logrus.Logger
}

func newDisabledTun(cidr *net.IPNet, queueLen int, metricsEnabled bool, l *logrus.Logger) *disabledTun {
	tun := &disabledTun{
		cidr: cidr,
		read: make(chan []byte, queueLen),
		l:    l,
	}

	if metricsEnabled {
		tun.tx = metrics.GetOrRegisterCounter("messages.tx.message", nil)
		tun.rx = metrics.GetOrRegisterCounter("messages.rx.message", nil)
	} else {
		tun.tx = &metrics.NilCounter{}
		tun.rx = &metrics.NilCounter{}
	}

	return tun
}

func (*disabledTun) Activate() error {
	return nil
}

func (*disabledTun) RouteFor(iputil.VpnIp) iputil.VpnIp {
	return 0
}

func (t *disabledTun) Cidr() *net.IPNet {
	return t.cidr
}

func (*disabledTun) Name() string {
	return "disabled"
}

func (t *disabledTun) Read(b []byte) (int, error) {
	r, ok := <-t.read
	if !ok {
		return 0, io.EOF
	}

	if len(r) > len(b) {
		return 0, fmt.Errorf("packet larger than mtu: %d > %d bytes", len(r), len(b))
	}

	t.tx.Inc(1)
	if t.l.Level >= logrus.DebugLevel {
		t.l.WithField("raw", prettyPacket(r)).Debugf("Write payload")
	}

	return copy(b, r), nil
}

func (t *disabledTun) handleICMPEchoRequest(b []byte) bool {
	// Return early if this is not a simple ICMP Echo Request
	//TODO: make constants out of these
	if !(len(b) >= 28 && len(b) <= 9001 && b[0] == 0x45 && b[9] == 0x01 && b[20] == 0x08) {
		return false
	}

	// We don't support fragmented packets
	if b[7] != 0 || (b[6]&0x2F != 0) {
		return false
	}

	buf := make([]byte, len(b))
	copy(buf, b)

	// Swap dest / src IPs and recalculate checksum
	ipv4 := buf[0:20]
	copy(ipv4[12:16], b[16:20])
	copy(ipv4[16:20], b[12:16])
	ipv4[10] = 0
	ipv4[11] = 0
	binary.BigEndian.PutUint16(ipv4[10:], ipChecksum(ipv4))

	// Change type to ICMP Echo Reply and recalculate checksum
	icmp := buf[20:]
	icmp[0] = 0
	icmp[2] = 0
	icmp[3] = 0
	binary.BigEndian.PutUint16(icmp[2:], ipChecksum(icmp))

	// attempt to write it, but don't block
	select {
	case t.read <- buf:
	default:
		t.l.Debugf("tun_disabled: dropped ICMP Echo Reply response")
	}

	return true
}

func (t *disabledTun) Write(b []byte) (int, error) {
	t.rx.Inc(1)

	// Check for ICMP Echo Request before spending time doing the full parsing
	if t.handleICMPEchoRequest(b) {
		if t.l.Level >= logrus.DebugLevel {
			t.l.WithField("raw", prettyPacket(b)).Debugf("Disabled tun responded to ICMP Echo Request")
		}
	} else if t.l.Level >= logrus.DebugLevel {
		t.l.WithField("raw", prettyPacket(b)).Debugf("Disabled tun received unexpected payload")
	}
	return len(b), nil
}

func (t *disabledTun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
	return t, nil
}

func (t *disabledTun) Close() error {
	if t.read != nil {
		close(t.read)
		t.read = nil
	}
	return nil
}

type prettyPacket []byte

func (p prettyPacket) String() string {
	var s strings.Builder

	for i, b := range p {
		if i > 0 && i%8 == 0 {
			s.WriteString(" ")
		}
		s.WriteString(fmt.Sprintf("%02x ", b))
	}

	return s.String()
}

func ipChecksum(b []byte) uint16 {
	var c uint32
	sz := len(b) - 1

	for i := 0; i < sz; i += 2 {
		c += uint32(b[i]) << 8
		c += uint32(b[i+1])
	}
	if sz%2 == 0 {
		c += uint32(b[sz]) << 8
	}

	for (c >> 16) > 0 {
		c = (c & 0xffff) + (c >> 16)
	}

	return ^uint16(c)
}
