package main

import (
	"context"
	"crypto/tls"
	"encoding/base64"
	"flag"
	"fmt"
	"math/rand"
	"net/http"
	"net/url"
	"time"

	_ "github.com/envoyproxy/go-control-plane/envoy/api/v2"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/grpc/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_authz/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/lua/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
	_ "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3"
	"github.com/google/uuid"
	"golang.org/x/sync/errgroup"
	"google.golang.org/grpc"

	"github.com/pomerium/pomerium/internal/log"
	"github.com/pomerium/pomerium/internal/tests/xdserr"
	"github.com/pomerium/pomerium/pkg/grpc/config"
	"github.com/pomerium/pomerium/pkg/grpc/databroker"
	"github.com/pomerium/pomerium/pkg/grpcutil"
	"github.com/pomerium/pomerium/pkg/protoutil"
)

var httpClient = &http.Client{
	Transport: &http.Transport{
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: true,
		},
	},
}

func main() {
	ctx := context.Background()

	graceful := flag.Bool("graceful", false, "gracefully grow")
	domain := flag.String("domain", "localhost.pomerium.io", "domain to create routes in")
	routes := flag.Int("routes", 100, "number of routes")
	cycles := flag.Int("cycles", 1, "number of cycles")
	change := flag.Int("change", 1, "number of change per cycle")
	addr := flag.String("db-url", "http://localhost:5443", "databroker url")
	key := flag.String("key", "", "databroker connection key")
	to := flag.String("to", "", "route To url")

	flag.Parse()

	toURL, err := url.Parse(*to)
	if err != nil {
		log.Error(ctx).Err(err).Msg(*to)
		return
	}

	eg, ctx := errgroup.WithContext(ctx)
	conn, err := grpcConn(ctx, *addr, *key)
	if err != nil {
		log.Error(ctx).Err(err).Msg("databroker grpc conn")
		return
	}
	defer conn.Close()

	if *to == "" {
		*to, err = xdserr.RunEcho(ctx)
		if err != nil {
			log.Error(ctx).Err(err).Msg("echo server")
			return
		}
	}
	log.Info(ctx).Str("url", *to).Msg("echo server")

	eg.Go(func() error {
		return run(ctx, conn, *toURL, *domain, opts{
			graceful: *graceful,
			nRoutes:  *routes,
			nIter:    *cycles,
			nMod:     *change,
		})
	})
	if err := eg.Wait(); err != nil {
		log.Error(ctx).Err(err).Msg("altering config")
	}
}

type opts struct {
	nRoutes, nIter, nMod int
	graceful             bool
}

func run(ctx context.Context, conn *grpc.ClientConn, to url.URL, domain string, o opts) error {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	dbc := databroker.NewDataBrokerServiceClient(conn)
	cfg := new(config.Config)

	for i := 0; i < o.nRoutes; i++ {
		cfg.Routes = append(cfg.Routes, makeRoute(domain, to))
	}

	rand.Seed(time.Now().Unix())

	changed := make([]int, o.nMod)
	for i := 0; i < o.nIter; i++ {
		for j := 0; j < o.nMod; j++ {
			// nolint: gosec
			idx := rand.Intn(o.nRoutes)
			changed[j] = idx
			cfg.Routes[idx] = makeRoute(domain, to)
		}
		log.Info(ctx).Ints("changed", changed).Msg("changed")
		if err := saveAndLogConfig(ctx, dbc, cfg, o.graceful); err != nil {
			return err
		}
	}

	if !o.graceful {
		return waitHealthy(ctx, httpClient, cfg.Routes)
	}

	return nil
}

func grpcConn(ctx context.Context, addr, keyTxt string) (*grpc.ClientConn, error) {
	u, err := url.Parse(addr)
	if err != nil {
		return nil, err
	}

	key, err := base64.StdEncoding.DecodeString(keyTxt)
	if err != nil {
		return nil, err
	}
	fmt.Println(keyTxt)
	return grpcutil.NewGRPCClientConn(ctx, &grpcutil.Options{
		Address:            u,
		InsecureSkipVerify: true,
		SignedJWTKey:       key,
	})
}

func makeRoute(domain string, to url.URL) *config.Route {
	id := fmt.Sprintf("r-%s", uuid.NewString())
	return &config.Route{
		Name:                             id,
		From:                             fmt.Sprintf("https://%s.%s", id, domain),
		Path:                             "/",
		PrefixRewrite:                    to.Path,
		To:                               []string{to.String()},
		AllowPublicUnauthenticatedAccess: true,
	}
}

func saveAndLogConfig(ctx context.Context, client databroker.DataBrokerServiceClient, cfg *config.Config, graceful bool) error {
	if err := saveConfig(ctx, client, cfg); err != nil {
		return err
	}

	if graceful {
		return waitHealthy(ctx, httpClient, cfg.Routes)
	}

	return nil
}

func waitHealthy(ctx context.Context, client *http.Client, routes []*config.Route) error {
	now := time.Now()
	if err := xdserr.WaitForHealthy(ctx, httpClient, routes); err != nil {
		return err
	}

	log.Info(ctx).
		Int("routes", len(routes)).
		Str("elapsed", time.Since(now).String()).
		Msg("ok")

	return nil
}
func saveConfig(ctx context.Context, client databroker.DataBrokerServiceClient, cfg *config.Config) error {
	any := protoutil.NewAny(cfg)
	r, err := client.Put(ctx, &databroker.PutRequest{
		Record: &databroker.Record{
			Type: any.GetTypeUrl(),
			Id:   "test_config",
			Data: any,
		}})
	if err != nil {
		return err
	}
	log.Info(ctx).Uint64("version", r.GetRecord().GetVersion()).Msg("set config")
	return nil
}