package tap

import (
	"context"
	"fmt"
	"io"
	"net"
	"strings"
	"time"

	common "github.com/runconduit/conduit/controller/gen/common"
	pb "github.com/runconduit/conduit/controller/gen/controller/tap"
	proxy "github.com/runconduit/conduit/controller/gen/proxy/tap"
	public "github.com/runconduit/conduit/controller/gen/public"
	"github.com/runconduit/conduit/controller/k8s"
	"github.com/runconduit/conduit/controller/util"
	log "github.com/sirupsen/logrus"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"k8s.io/api/core/v1"
)

var tapInterval = 10 * time.Second

type (
	server struct {
		tapPort uint
		// We use the Kubernetes API to find the IP addresses of pods to tap
		replicaSets *k8s.ReplicaSetStore
		pods        *k8s.PodIndex
	}
)

func (s *server) Tap(req *public.TapRequest, stream pb.Tap_TapServer) error {

	// TODO: Allow a configurable aperture A.
	//       If the target contains more than A pods, select A of them at random.
	var pods []*v1.Pod
	var targetName string
	switch target := req.Target.(type) {
	case *public.TapRequest_Pod:
		targetName = target.Pod
		pod, err := s.pods.GetPod(target.Pod)
		if err != nil {
			return status.Errorf(codes.NotFound, err.Error())
		}
		pods = []*v1.Pod{pod}
	case *public.TapRequest_Deployment:
		targetName = target.Deployment
		var err error
		pods, err = (*s.pods).GetPodsByIndex(target.Deployment)
		if err != nil {
			return err
		}
	}

	log.Printf("Tapping %d pods for target %s", len(pods), targetName)

	events := make(chan *common.TapEvent)

	go func() { // Stop sending back events if the request is cancelled
		<-stream.Context().Done()
		close(events)
	}()

	// divide the rps evenly between all pods to tap
	rpsPerPod := req.MaxRps / float32(len(pods))
	if rpsPerPod < 1 {
		rpsPerPod = 1
	}

	for _, pod := range pods {
		// initiate a tap on the pod
		match, err := makeMatch(req)
		if err != nil {
			return nil
		}
		go s.tapProxy(stream.Context(), rpsPerPod, match, pod.Status.PodIP, events)
	}

	// read events from the taps and send them back
	for event := range events {
		err := stream.Send(event)
		if err != nil {
			return err
		}
	}
	return nil
}

func validatePort(port uint32) error {
	if port > 65535 {
		return fmt.Errorf("Port number of range: %d", port)
	}
	return nil
}

func makeMatch(req *public.TapRequest) (*proxy.ObserveRequest_Match, error) {
	matches := make([]*proxy.ObserveRequest_Match, 0)
	if req.FromIP != "" {
		ip, err := util.ParseIPV4(req.FromIP)
		if err != nil {
			return nil, err
		}
		matches = append(matches, &proxy.ObserveRequest_Match{
			Match: &proxy.ObserveRequest_Match_Source{
				Source: &proxy.ObserveRequest_Match_Tcp{
					Match: &proxy.ObserveRequest_Match_Tcp_Netmask_{
						Netmask: &proxy.ObserveRequest_Match_Tcp_Netmask{
							Ip:   ip,
							Mask: 32,
						},
					},
				},
			},
		})
	}

	if req.FromPort != 0 {
		if err := validatePort(req.FromPort); err != nil {
			return nil, err
		}
		matches = append(matches, &proxy.ObserveRequest_Match{
			Match: &proxy.ObserveRequest_Match_Source{
				Source: &proxy.ObserveRequest_Match_Tcp{
					Match: &proxy.ObserveRequest_Match_Tcp_Ports{
						Ports: &proxy.ObserveRequest_Match_Tcp_PortRange{
							Min: req.FromPort,
						},
					},
				},
			},
		})
	}

	if req.ToIP != "" {
		ip, err := util.ParseIPV4(req.ToIP)
		if err != nil {
			return nil, err
		}
		matches = append(matches, &proxy.ObserveRequest_Match{
			Match: &proxy.ObserveRequest_Match_Destination{
				Destination: &proxy.ObserveRequest_Match_Tcp{
					Match: &proxy.ObserveRequest_Match_Tcp_Netmask_{
						Netmask: &proxy.ObserveRequest_Match_Tcp_Netmask{
							Ip:   ip,
							Mask: 32,
						},
					},
				},
			},
		})
	}

	if req.ToPort != 0 {
		if err := validatePort(req.ToPort); err != nil {
			return nil, err
		}
		matches = append(matches, &proxy.ObserveRequest_Match{
			Match: &proxy.ObserveRequest_Match_Destination{
				Destination: &proxy.ObserveRequest_Match_Tcp{
					Match: &proxy.ObserveRequest_Match_Tcp_Ports{
						Ports: &proxy.ObserveRequest_Match_Tcp_PortRange{
							Min: req.ToPort,
						},
					},
				},
			},
		})
	}

	if req.Scheme != "" {
		matches = append(matches, &proxy.ObserveRequest_Match{
			Match: &proxy.ObserveRequest_Match_Http_{
				Http: &proxy.ObserveRequest_Match_Http{
					Match: &proxy.ObserveRequest_Match_Http_Scheme{
						Scheme: parseScheme(req.Scheme),
					},
				},
			},
		})
	}

	if req.Method != "" {
		matches = append(matches, &proxy.ObserveRequest_Match{
			Match: &proxy.ObserveRequest_Match_Http_{
				Http: &proxy.ObserveRequest_Match_Http{
					Match: &proxy.ObserveRequest_Match_Http_Method{
						Method: parseMethod(req.Method),
					},
				},
			},
		})
	}

	// exact match
	if req.Authority != "" {
		matches = append(matches, &proxy.ObserveRequest_Match{
			Match: &proxy.ObserveRequest_Match_Http_{
				Http: &proxy.ObserveRequest_Match_Http{
					Match: &proxy.ObserveRequest_Match_Http_Authority{
						Authority: &proxy.ObserveRequest_Match_Http_StringMatch{
							Match: &proxy.ObserveRequest_Match_Http_StringMatch_Exact{
								Exact: req.Authority,
							},
						},
					},
				},
			},
		})
	}

	// prefix match
	if req.Path != "" {
		matches = append(matches, &proxy.ObserveRequest_Match{
			Match: &proxy.ObserveRequest_Match_Http_{
				Http: &proxy.ObserveRequest_Match_Http{
					Match: &proxy.ObserveRequest_Match_Http_Path{
						Path: &proxy.ObserveRequest_Match_Http_StringMatch{
							Match: &proxy.ObserveRequest_Match_Http_StringMatch_Prefix{
								Prefix: req.Path,
							},
						},
					},
				},
			},
		})
	}

	return &proxy.ObserveRequest_Match{
		Match: &proxy.ObserveRequest_Match_All{
			All: &proxy.ObserveRequest_Match_Seq{
				Matches: matches,
			},
		},
	}, nil
}

// TODO: validate scheme
func parseScheme(scheme string) *common.Scheme {
	value, ok := common.Scheme_Registered_value[strings.ToUpper(scheme)]
	if ok {
		return &common.Scheme{
			Type: &common.Scheme_Registered_{
				Registered: common.Scheme_Registered(value),
			},
		}
	}
	return &common.Scheme{
		Type: &common.Scheme_Unregistered{
			Unregistered: strings.ToUpper(scheme),
		},
	}
}

// TODO: validate method
func parseMethod(method string) *common.HttpMethod {
	value, ok := common.HttpMethod_Registered_value[strings.ToUpper(method)]
	if ok {
		return &common.HttpMethod{
			Type: &common.HttpMethod_Registered_{
				Registered: common.HttpMethod_Registered(value),
			},
		}
	}
	return &common.HttpMethod{
		Type: &common.HttpMethod_Unregistered{
			Unregistered: strings.ToUpper(method),
		},
	}
}

// Tap a pod.
// This method will run continuously until an error is encountered or the
// request is cancelled via the context.  Thus it should be called as a
// go-routine.
// To limit the rps to maxRps, this method calls Observe on the pod with a limit
// of maxRps * 10s at most once per 10s window.  If this limit is reached in
// less than 10s, we sleep until the end of the window before calling Observe
// again.
func (s *server) tapProxy(ctx context.Context, maxRps float32, match *proxy.ObserveRequest_Match, addr string, events chan *common.TapEvent) {
	tapAddr := fmt.Sprintf("%s:%d", addr, s.tapPort)
	log.Printf("Establishing tap on %s", tapAddr)
	conn, err := grpc.DialContext(ctx, tapAddr, grpc.WithInsecure())
	if err != nil {
		log.Println(err)
		return
	}
	client := proxy.NewTapClient(conn)

	req := &proxy.ObserveRequest{
		Limit: uint32(maxRps * float32(tapInterval.Seconds())),
		Match: match,
	}

	for { // Request loop
		windowStart := time.Now()
		windowEnd := windowStart.Add(tapInterval)
		rsp, err := client.Observe(ctx, req)
		if err != nil {
			log.Println(err)
			return
		}
		for { // Stream loop
			event, err := rsp.Recv()
			if err == io.EOF {
				break
			}
			if err != nil {
				log.Println(err)
				return
			}
			events <- event
		}
		if time.Now().Before(windowEnd) {
			time.Sleep(time.Until(windowEnd))
		}
	}
}

func NewServer(addr string, tapPort uint, kubeconfig string) (*grpc.Server, net.Listener, error) {

	clientSet, err := k8s.NewClientSet(kubeconfig)
	if err != nil {
		return nil, nil, err
	}

	replicaSets, err := k8s.NewReplicaSetStore(clientSet)
	if err != nil {
		return nil, nil, err
	}
	err = replicaSets.Run()
	if err != nil {
		return nil, nil, err
	}

	// index pods by deployment
	deploymentIndex := func(obj interface{}) ([]string, error) {
		pod, ok := obj.(*v1.Pod)
		if !ok {
			return nil, fmt.Errorf("object is not a Pod")
		}
		deployment, err := replicaSets.GetDeploymentForPod(pod)
		if err != nil {
			log.Debugf("Cannot get deployment for pod %s: %s", pod.Name, err)
			return []string{}, nil
		}
		return []string{deployment}, nil
	}

	pods, err := k8s.NewPodIndex(clientSet, deploymentIndex)
	if err != nil {
		return nil, nil, err
	}
	err = pods.Run()
	if err != nil {
		return nil, nil, err
	}

	lis, err := net.Listen("tcp", addr)
	if err != nil {
		return nil, nil, err
	}

	s := util.NewGrpcServer()
	srv := server{
		tapPort:     tapPort,
		replicaSets: replicaSets,
		pods:        pods,
	}
	pb.RegisterTapServer(s, &srv)

	// TODO: register shutdown hook to call pods.Stop() and replicatSets.Stop()

	return s, lis, nil
}
