/*
 * Copyright 2021 PingCAP, Inc.
 *
 * 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,
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package main

import (
	"context"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/gorilla/mux"
	"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2"
	"github.com/pingcap/fn"
	"go.uber.org/zap"
	"golang.org/x/oauth2"
	"k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/pingcap/mimir/pkg/cert"
	"github.com/pingcap/mimir/pkg/protos"
	"github.com/pingcap/mimir/pkg/service/oauth"
	"github.com/pingcap/mimir/util"
)

const (
	EnvNameClientID     = "OAUTH_CLIENT_ID"
	EnvNameClientSecret = "OAUTH_CLIENT_SECRET"
	EnvNameCallbackURL  = "OAUTH_CALLBACK"
	EnvNamespace        = "Namespace"
	EnvCAIssuer         = "CAIssuer"
	ServerADDR          = "SERVER_ADDRESS"
)

var (
	cli client.Client

	Scheme = scheme.Scheme
)

func init() {
	runtime.Must(v1alpha2.AddToScheme(Scheme))

	kubeConfig := flag.String("kubeconfig", "", "auth -kubeconfig=/path/to/config")
	flag.Parse()

	var cfg *rest.Config
	var err error

	if *kubeConfig == "" {
		cfg, err = rest.InClusterConfig()
	} else {
		cfg, err = clientcmd.BuildConfigFromFlags("", *kubeConfig)
	}

	if err != nil {
		panic(err)
	}

	c, err := client.New(cfg, client.Options{})
	if err != nil {
		panic(err)
	}

	cli = c
}

func main() {
	if os.Getenv(EnvCAIssuer) == "" {
		panic(errors.New("Need ca issuer environment"))
	}

	ctx := context.Background()

	conf := &oauth2.Config{
		ClientID:     os.Getenv(EnvNameClientID),
		ClientSecret: os.Getenv(EnvNameClientSecret),
		Scopes:       []string{"openid", "profile", "email"},
		RedirectURL:  os.Getenv(EnvNameCallbackURL),
		Endpoint: oauth2.Endpoint{
			TokenURL: "https://tidb-soc2.us.auth0.com/oauth/token",
			AuthURL:  "https://tidb-soc2.us.auth0.com/authorize",
		},
	}

	r := mux.NewRouter()
	fn.SetErrorEncoder(util.EncodeError)
	fn.SetResponseEncoder(util.EncodeResp)

	r.Handle("/api/v1/login", fn.Wrap(func(param *protos.LoginParam) (*protos.LoginData, error) {
		return &protos.LoginData{
			URL: conf.AuthCodeURL(param.State, oauth2.AccessTypeOnline),
		}, nil
	})).Methods("POST")

	r.Handle("/api/v1/login/{code}", fn.Wrap(func(r *http.Request) (*protos.ClientAuthData, error) {
		code := mux.Vars(r)["code"]
		email, err := getUserEmail(ctx, conf, code)
		if err != nil {
			return nil, err
		}

		err = cert.CreateCertIfAbsent(cli, os.Getenv(EnvNamespace), email, os.Getenv(EnvCAIssuer))
		if err != nil {
			log.Printf("get certificate for %s failed: %v", code, err)
			return nil, err
		}

		return cert.FetchCert(cli, os.Getenv(EnvNamespace), os.Getenv(ServerADDR), email)
	})).Methods("POST")

	r.PathPrefix("/callback").Handler(oauth.CallbackHandler("/callback"))

	srv := &http.Server{Addr: ":8888", Handler: r}

	log.Fatal("failed to start auth server", zap.Error(srv.ListenAndServe()))
}

func getUserEmail(ctx context.Context, conf *oauth2.Config, code string) (string, error) {
	// Use the custom HTTP client when requesting a token.
	httpClient := &http.Client{}
	ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)

	tok, err := conf.Exchange(ctx, code)
	if err != nil {
		log.Fatal(err)
	}

	c := conf.Client(ctx, tok)

	res, err := c.Get("https://tidb-soc2.us.auth0.com/userinfo")
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	m := struct {
		Email string `json:"email"`
	}{}
	if err := json.NewDecoder(res.Body).Decode(&m); err != nil {
		return "", err
	}

	if m.Email == "" {
		return "", fmt.Errorf("no nickname found in userinfo")
	}
	return m.Email, nil
}
