package control

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"strings"
	"time"

	"github.com/golang/protobuf/proto"
	"google.golang.org/grpc"

	logService "v2ray.com/core/app/log/command"
	statsService "v2ray.com/core/app/stats/command"
	"v2ray.com/core/common"
)

type APICommand struct{}

func (c *APICommand) Name() string {
	return "api"
}

func (c *APICommand) Description() Description {
	return Description{
		Short: "Call V2Ray API",
		Usage: []string{
			"v2ctl api [--server=127.0.0.1:8080] Service.Method Request",
			"Call an API in an V2Ray process.",
			"The following methods are currently supported:",
			"\tLoggerService.RestartLogger",
			"\tStatsService.GetStats",
			"\tStatsService.QueryStats",
			"API calls in this command have a timeout to the server of 3 seconds.",
			"Examples:",
			"v2ctl api --server=127.0.0.1:8080 LoggerService.RestartLogger '' ",
			"v2ctl api --server=127.0.0.1:8080 StatsService.QueryStats 'pattern: \"\" reset: false'",
			"v2ctl api --server=127.0.0.1:8080 StatsService.GetStats 'name: \"inbound>>>statin>>>traffic>>>downlink\" reset: false'",
			"v2ctl api --server=127.0.0.1:8080 StatsService.GetSysStats ''",
		},
	}
}

func (c *APICommand) Execute(args []string) error {
	fs := flag.NewFlagSet(c.Name(), flag.ContinueOnError)

	serverAddrPtr := fs.String("server", "127.0.0.1:8080", "Server address")

	if err := fs.Parse(args); err != nil {
		return err
	}

	unnamedArgs := fs.Args()
	if len(unnamedArgs) < 2 {
		return newError("service name or request not specified.")
	}

	service, method := getServiceMethod(unnamedArgs[0])
	handler, found := serivceHandlerMap[strings.ToLower(service)]
	if !found {
		return newError("unknown service: ", service)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	conn, err := grpc.DialContext(ctx, *serverAddrPtr, grpc.WithInsecure(), grpc.WithBlock())
	if err != nil {
		return newError("failed to dial ", *serverAddrPtr).Base(err)
	}
	defer conn.Close()

	response, err := handler(ctx, conn, method, unnamedArgs[1])
	if err != nil {
		return newError("failed to call service ", unnamedArgs[0]).Base(err)
	}

	fmt.Println(response)
	return nil
}

func getServiceMethod(s string) (string, string) {
	ss := strings.Split(s, ".")
	service := ss[0]
	var method string
	if len(ss) > 1 {
		method = ss[1]
	}
	return service, method
}

type serviceHandler func(ctx context.Context, conn *grpc.ClientConn, method string, request string) (string, error)

var serivceHandlerMap = map[string]serviceHandler{
	"statsservice":  callStatsService,
	"loggerservice": callLogService,
}

func callLogService(ctx context.Context, conn *grpc.ClientConn, method string, request string) (string, error) {
	client := logService.NewLoggerServiceClient(conn)

	switch strings.ToLower(method) {
	case "restartlogger":
		r := &logService.RestartLoggerRequest{}
		if err := proto.UnmarshalText(request, r); err != nil {
			return "", err
		}
		resp, err := client.RestartLogger(ctx, r)
		if err != nil {
			return "", err
		}
		return proto.MarshalTextString(resp), nil
	default:
		return "", errors.New("Unknown method: " + method)
	}
}

func callStatsService(ctx context.Context, conn *grpc.ClientConn, method string, request string) (string, error) {
	client := statsService.NewStatsServiceClient(conn)

	switch strings.ToLower(method) {
	case "getstats":
		r := &statsService.GetStatsRequest{}
		if err := proto.UnmarshalText(request, r); err != nil {
			return "", err
		}
		resp, err := client.GetStats(ctx, r)
		if err != nil {
			return "", err
		}
		return proto.MarshalTextString(resp), nil
	case "querystats":
		r := &statsService.QueryStatsRequest{}
		if err := proto.UnmarshalText(request, r); err != nil {
			return "", err
		}
		resp, err := client.QueryStats(ctx, r)
		if err != nil {
			return "", err
		}
		return proto.MarshalTextString(resp), nil
	case "getsysstats":
		// SysStatsRequest is an empty message
		r := &statsService.SysStatsRequest{}
		resp, err := client.GetSysStats(ctx, r)
		if err != nil {
			return "", err
		}
		return proto.MarshalTextString(resp), nil
	default:
		return "", errors.New("Unknown method: " + method)
	}
}

func init() {
	common.Must(RegisterCommand(&APICommand{}))
}
