package metrics

import (
	"context"
	"fmt"
	"net"
	"time"

	"github.com/lucas-clemente/quic-go/internal/utils"

	"github.com/lucas-clemente/quic-go/internal/protocol"
	"github.com/lucas-clemente/quic-go/logging"

	"go.opencensus.io/stats"
	"go.opencensus.io/stats/view"
	"go.opencensus.io/tag"
)

// Measures
var (
	connections = stats.Int64("quic-go/connections", "number of QUIC connections", stats.UnitDimensionless)
	lostPackets = stats.Int64("quic-go/lost-packets", "number of packets declared lost", stats.UnitDimensionless)
	sentPackets = stats.Int64("quic-go/sent-packets", "number of packets sent", stats.UnitDimensionless)
	ptos        = stats.Int64("quic-go/ptos", "number of times the PTO timer fired", stats.UnitDimensionless)
	closes      = stats.Int64("quic-go/close", "number of connections closed", stats.UnitDimensionless)
)

// Tags
var (
	keyPerspective, _      = tag.NewKey("perspective")
	keyIPVersion, _        = tag.NewKey("ip_version")
	keyEncryptionLevel, _  = tag.NewKey("encryption_level")
	keyPacketLossReason, _ = tag.NewKey("packet_loss_reason")
	keyPacketType, _       = tag.NewKey("packet_type")
	keyCloseReason, _      = tag.NewKey("close_reason")
	keyCloseRemote, _      = tag.NewKey("close_remote")
	keyErrorCode, _        = tag.NewKey("error_code")
	keyHandshakePhase, _   = tag.NewKey("handshake_phase")
)

// Views
var (
	ConnectionsView = &view.View{
		Measure:     connections,
		TagKeys:     []tag.Key{keyPerspective, keyIPVersion},
		Aggregation: view.Count(),
	}
	LostPacketsView = &view.View{
		Measure:     lostPackets,
		TagKeys:     []tag.Key{keyEncryptionLevel, keyPacketLossReason},
		Aggregation: view.Count(),
	}
	SentPacketsView = &view.View{
		Measure:     sentPackets,
		TagKeys:     []tag.Key{keyPacketType},
		Aggregation: view.Count(),
	}
	PTOView = &view.View{
		Measure:     ptos,
		TagKeys:     []tag.Key{keyHandshakePhase},
		Aggregation: view.Count(),
	}
	CloseView = &view.View{
		Measure:     closes,
		TagKeys:     []tag.Key{keyCloseReason, keyErrorCode},
		Aggregation: view.Count(),
	}
)

// DefaultViews collects all OpenCensus views for metric gathering purposes
var DefaultViews = []*view.View{
	ConnectionsView,
	LostPacketsView,
	SentPacketsView,
	CloseView,
}

type tracer struct{}

var _ logging.Tracer = &tracer{}

// NewTracer creates a new metrics tracer.
func NewTracer() logging.Tracer { return &tracer{} }

func (t *tracer) TracerForConnection(p logging.Perspective, _ logging.ConnectionID) logging.ConnectionTracer {
	return newConnTracer(t, p)
}

func (t *tracer) SentPacket(_ net.Addr, hdr *logging.Header, _ protocol.ByteCount, _ []logging.Frame) {
	stats.RecordWithTags(
		context.Background(),
		[]tag.Mutator{
			tag.Upsert(keyPacketType, packetType(logging.PacketTypeFromHeader(hdr)).String()),
		},
		sentPackets.M(1),
	)
}

func (t *tracer) DroppedPacket(net.Addr, logging.PacketType, logging.ByteCount, logging.PacketDropReason) {
}

type connTracer struct {
	perspective logging.Perspective
	tracer      logging.Tracer

	handshakeComplete bool
}

func newConnTracer(tracer logging.Tracer, perspective logging.Perspective) logging.ConnectionTracer {
	return &connTracer{
		perspective: perspective,
		tracer:      tracer,
	}
}

var _ logging.ConnectionTracer = &connTracer{}

func (t *connTracer) StartedConnection(local, _ net.Addr, _ logging.VersionNumber, _, _ logging.ConnectionID) {
	perspectiveTag := tag.Upsert(keyPerspective, perspective(t.perspective).String())

	var ipVersionTag tag.Mutator
	if udpAddr, ok := local.(*net.UDPAddr); ok {
		if utils.IsIPv4(udpAddr.IP) {
			ipVersionTag = tag.Upsert(keyIPVersion, "IPv4")
		} else {
			ipVersionTag = tag.Upsert(keyIPVersion, "IPv6")
		}
	} else {
		ipVersionTag = tag.Upsert(keyIPVersion, "unknown")
	}

	stats.RecordWithTags(
		context.Background(),
		[]tag.Mutator{perspectiveTag, ipVersionTag},
		connections.M(1),
	)
}

func (t *connTracer) ClosedConnection(r logging.CloseReason) {
	var tags []tag.Mutator
	if timeout, ok := r.Timeout(); ok {
		tags = []tag.Mutator{
			tag.Upsert(keyCloseReason, timeoutReason(timeout).String()),
			tag.Upsert(keyCloseRemote, "false"),
		}
	} else if _, ok := r.StatelessReset(); ok {
		tags = []tag.Mutator{
			tag.Upsert(keyCloseReason, "stateless_reset"),
			tag.Upsert(keyCloseRemote, "true"),
		}
	} else if errorCode, remote, ok := r.ApplicationError(); ok {
		tags = []tag.Mutator{
			tag.Upsert(keyCloseReason, "application_error"),
			tag.Upsert(keyErrorCode, errorCode.String()),
			tag.Upsert(keyCloseRemote, fmt.Sprintf("%t", remote)),
		}
	} else if errorCode, remote, ok := r.TransportError(); ok {
		tags = []tag.Mutator{
			tag.Upsert(keyCloseReason, "transport_error"),
			tag.Upsert(keyErrorCode, errorCode.String()),
			tag.Upsert(keyCloseRemote, fmt.Sprintf("%t", remote)),
		}
	}
	stats.RecordWithTags(context.Background(), tags, closes.M(1))
}
func (t *connTracer) SentTransportParameters(*logging.TransportParameters)     {}
func (t *connTracer) ReceivedTransportParameters(*logging.TransportParameters) {}
func (t *connTracer) SentPacket(hdr *logging.ExtendedHeader, _ logging.ByteCount, _ *logging.AckFrame, _ []logging.Frame) {
	typ := logging.PacketTypeFromHeader(&hdr.Header)
	if typ == logging.PacketType1RTT {
		t.handshakeComplete = true
	}
	stats.RecordWithTags(
		context.Background(),
		[]tag.Mutator{
			tag.Upsert(keyPacketType, packetType(typ).String()),
		},
		sentPackets.M(1),
	)
}
func (t *connTracer) ReceivedVersionNegotiationPacket(*logging.Header, []logging.VersionNumber) {}
func (t *connTracer) ReceivedRetry(*logging.Header)                                             {}
func (t *connTracer) ReceivedPacket(*logging.ExtendedHeader, logging.ByteCount, []logging.Frame) {
}
func (t *connTracer) BufferedPacket(logging.PacketType)                                             {}
func (t *connTracer) DroppedPacket(logging.PacketType, logging.ByteCount, logging.PacketDropReason) {}
func (t *connTracer) UpdatedCongestionState(logging.CongestionState)                                {}
func (t *connTracer) UpdatedMetrics(*logging.RTTStats, logging.ByteCount, logging.ByteCount, int)   {}
func (t *connTracer) LostPacket(encLevel logging.EncryptionLevel, _ logging.PacketNumber, reason logging.PacketLossReason) {
	stats.RecordWithTags(
		context.Background(),
		[]tag.Mutator{
			tag.Upsert(keyEncryptionLevel, encryptionLevel(encLevel).String()),
			tag.Upsert(keyPacketLossReason, packetLossReason(reason).String()),
		},
		lostPackets.M(1),
	)
}

func (t *connTracer) UpdatedPTOCount(value uint32) {
	if value == 0 {
		return
	}
	phase := "during_handshake"
	if t.handshakeComplete {
		phase = "after_handshake"
	}
	stats.RecordWithTags(
		context.Background(),
		[]tag.Mutator{tag.Upsert(keyHandshakePhase, phase)},
		ptos.M(1),
	)
}
func (t *connTracer) UpdatedKeyFromTLS(logging.EncryptionLevel, logging.Perspective)     {}
func (t *connTracer) UpdatedKey(logging.KeyPhase, bool)                                  {}
func (t *connTracer) DroppedEncryptionLevel(logging.EncryptionLevel)                     {}
func (t *connTracer) DroppedKey(logging.KeyPhase)                                        {}
func (t *connTracer) SetLossTimer(logging.TimerType, logging.EncryptionLevel, time.Time) {}
func (t *connTracer) LossTimerExpired(logging.TimerType, logging.EncryptionLevel)        {}
func (t *connTracer) LossTimerCanceled()                                                 {}
func (t *connTracer) Close()                                                             {}
