/*
 *
 * Copyright 2018 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

// This file is for testing only. Runs a fake grpclb balancer server.
// The name of the service to load balance for and the addresses
// of that service are provided by command line flags.
package main

import (
	"flag"
	"net"
	"strconv"
	"strings"
	"time"

	"google.golang.org/grpc"
	lbpb "google.golang.org/grpc/balancer/grpclb/grpc_lb_v1"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/alts"
	"google.golang.org/grpc/grpclog"
	"google.golang.org/grpc/status"
	"google.golang.org/grpc/testdata"
)

var (
	port         = flag.Int("port", 10000, "Port to listen on.")
	backendAddrs = flag.String("backend_addrs", "", "Comma separated list of backend IP/port addresses.")
	useALTS      = flag.Bool("use_alts", false, "Listen on ALTS credentials.")
	useTLS       = flag.Bool("use_tls", false, "Listen on TLS credentials, using a test certificate.")
	shortStream  = flag.Bool("short_stream", false, "End the balancer stream immediately after sending the first server list.")
	serviceName  = flag.String("service_name", "UNSET", "Name of the service being load balanced for.")
)

type loadBalancerServer struct {
	lbpb.UnimplementedLoadBalancerServer
	serverListResponse *lbpb.LoadBalanceResponse
}

func (l *loadBalancerServer) BalanceLoad(stream lbpb.LoadBalancer_BalanceLoadServer) error {
	grpclog.Info("Begin handling new BalancerLoad request.")
	var lbReq *lbpb.LoadBalanceRequest
	var err error
	if lbReq, err = stream.Recv(); err != nil {
		grpclog.Errorf("Error receiving LoadBalanceRequest: %v", err)
		return err
	}
	grpclog.Info("LoadBalancerRequest received.")
	initialReq := lbReq.GetInitialRequest()
	if initialReq == nil {
		grpclog.Info("Expected first request to be an InitialRequest. Got: %v", lbReq)
		return status.Error(codes.Unknown, "First request not an InitialRequest")
	}
	// gRPC clients targeting foo.bar.com:443 can sometimes include the ":443" suffix in
	// their requested names; handle this case. TODO: make 443 configurable?
	var cleanedName string
	var requestedNamePortNumber string
	if cleanedName, requestedNamePortNumber, err = net.SplitHostPort(initialReq.Name); err != nil {
		cleanedName = initialReq.Name
	} else {
		if requestedNamePortNumber != "443" {
			grpclog.Info("Bad requested service name port number: %v.", requestedNamePortNumber)
			return status.Error(codes.Unknown, "Bad requested service name port number")
		}
	}
	if cleanedName != *serviceName {
		grpclog.Info("Expected requested service name: %v. Got: %v", *serviceName, initialReq.Name)
		return status.Error(codes.NotFound, "Bad requested service name")
	}
	if err := stream.Send(&lbpb.LoadBalanceResponse{
		LoadBalanceResponseType: &lbpb.LoadBalanceResponse_InitialResponse{
			InitialResponse: &lbpb.InitialLoadBalanceResponse{},
		},
	}); err != nil {
		grpclog.Errorf("Error sending initial LB response: %v", err)
		return status.Error(codes.Unknown, "Error sending initial response")
	}
	grpclog.Info("Send LoadBalanceResponse: %v", l.serverListResponse)
	if err := stream.Send(l.serverListResponse); err != nil {
		grpclog.Errorf("Error sending LB response: %v", err)
		return status.Error(codes.Unknown, "Error sending response")
	}
	if *shortStream {
		return nil
	}
	for {
		grpclog.Info("Send LoadBalanceResponse: %v", l.serverListResponse)
		if err := stream.Send(l.serverListResponse); err != nil {
			grpclog.Errorf("Error sending LB response: %v", err)
			return status.Error(codes.Unknown, "Error sending response")
		}
		time.Sleep(10 * time.Second)
	}
}

func main() {
	flag.Parse()
	var opts []grpc.ServerOption
	if *useTLS {
		certFile := testdata.Path("server1.pem")
		keyFile := testdata.Path("server1.key")
		creds, err := credentials.NewServerTLSFromFile(certFile, keyFile)
		if err != nil {
			grpclog.Fatalf("Failed to generate credentials %v", err)
		}
		opts = append(opts, grpc.Creds(creds))
	} else if *useALTS {
		altsOpts := alts.DefaultServerOptions()
		altsTC := alts.NewServerCreds(altsOpts)
		opts = append(opts, grpc.Creds(altsTC))
	}
	var serverList []*lbpb.Server
	if len(*backendAddrs) == 0 {
		serverList = make([]*lbpb.Server, 0)
	} else {
		rawBackendAddrs := strings.Split(*backendAddrs, ",")
		serverList = make([]*lbpb.Server, len(rawBackendAddrs))
		for i := range rawBackendAddrs {
			rawIP, rawPort, err := net.SplitHostPort(rawBackendAddrs[i])
			if err != nil {
				grpclog.Fatalf("Failed to parse --backend_addrs[%d]=%v, error: %v", i, rawBackendAddrs[i], err)
			}
			ip := net.ParseIP(rawIP)
			if ip == nil {
				grpclog.Fatalf("Failed to parse ip: %v", rawIP)
			}
			numericPort, err := strconv.Atoi(rawPort)
			if err != nil {
				grpclog.Fatalf("Failed to convert port %v to int", rawPort)
			}
			grpclog.Infof("Adding backend ip: %v, port: %d", ip.String(), numericPort)
			serverList[i] = &lbpb.Server{
				IpAddress: ip,
				Port:      int32(numericPort),
			}
		}
	}
	serverListResponse := &lbpb.LoadBalanceResponse{
		LoadBalanceResponseType: &lbpb.LoadBalanceResponse_ServerList{
			ServerList: &lbpb.ServerList{
				Servers: serverList,
			},
		},
	}
	server := grpc.NewServer(opts...)
	grpclog.Infof("Begin listening on %d.", *port)
	lis, err := net.Listen("tcp", ":"+strconv.Itoa(*port))
	if err != nil {
		grpclog.Fatalf("Failed to listen on port %v: %v", *port, err)
	}
	lbpb.RegisterLoadBalancerServer(server, &loadBalancerServer{
		serverListResponse: serverListResponse,
	})
	server.Serve(lis)
}
