package central

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"time"

	"github.com/pingcap/dbaas/pkg/auth/auth0"
	"github.com/pingcap/log"
	"go.uber.org/zap"
)

type HttpClient interface {
	Do(r *http.Request) (*http.Response, error)
}

type HttpAuthenticator interface {
	Auth(r *http.Request) error
}

type Auth0Authenticator struct {
	Auth0Domain       string
	Auth0ClientID     string
	Auth0ClientSecret string
	Auth0Audience     string
}

func (auth *Auth0Authenticator) Auth(req *http.Request) error {
	token, err := auth0.GetClientToken(auth.Auth0Domain, auth.Auth0ClientID, auth.Auth0ClientSecret, auth.Auth0Audience)
	if err != nil {
		return err
	}
	req.Header.Add("Authorization", fmt.Sprintf("bearer %s", token))
	return nil
}

// Service for management portal
type Service struct {
	ListTenantClusters func(tenantID string) (*ListTenantClustersResp, error)
	ListTenants        func() (*ListTenantResp, error)
	ListAliveCluster   func() (*ListAliveClustersResp, error)
}

func NewService(baseURL string, auth0Domain, auth0ClientID, auth0ClientSecret, auth0Audience string) *Service {
	client := client{
		baseURL: baseURL,
		transport: &HttpTransport{
			HttpClient: &http.Client{Timeout: 60 * time.Second},
			Authenticator: &Auth0Authenticator{
				Auth0Domain:       auth0Domain,
				Auth0ClientID:     auth0ClientID,
				Auth0ClientSecret: auth0ClientSecret,
				Auth0Audience:     auth0Audience,
			},
		},
	}
	return &Service{
		ListTenants:        client.ListTenants,
		ListTenantClusters: client.ListTenantClusters,
		ListAliveCluster:   client.ListAliveClusters,
	}
}

// client for management portal system
type client struct {
	baseURL string

	transport ClientTransport
}

type ClientTransport interface {
	Submit(apiPath, method string, payload io.Reader) ([]byte, error)
}

type HttpTransport struct {
	HttpClient    HttpClient
	Authenticator HttpAuthenticator
}

func (c *client) ListTenants() (*ListTenantResp, error) {
	ret := &ListTenantResp{}
	return ret, c.callGET(fmt.Sprintf("%s/orgs?brief=true&no-limit=true", c.apiPrefix()), ret)
}

func (c *client) ListTenantClusters(orgId string) (*ListTenantClustersResp, error) {
	ret := &ListTenantClustersResp{}
	return ret, c.callGET(fmt.Sprintf("%s/orgs/%s/clusters?no-limit=true", c.apiPrefix(), orgId), ret)
}

// ListAliveClusters query core svc to get all alive non-devtier dedicated clusters.
// `/api/v1/admin/clusters4configservice` is a special light-weight api for config-service use case.
// we still use TenantCluster as internal item, but following field is empty and
func (c *client) ListAliveClusters() (*ListAliveClustersResp, error) {
	ret := &ListAliveClustersResp{}
	return ret, c.callGET(fmt.Sprintf("%s/clusters4configservice", c.apiPrefix()), ret)
}

func (c *client) apiPrefix() string {
	return fmt.Sprintf("%s/api/v1/admin", c.baseURL)
}

func (c *client) callGET(apiPath string, output interface{}) error {
	return c.callWithNoPayload(apiPath, "GET", output)
}

// nolint
func (c *client) callPUT(apiPath string, payload interface{}, output interface{}) error {
	return c.callWithPayload(apiPath, "PUT", payload, output)
}

// nolint
func (c *client) callPOST(apiPath string, payload interface{}, output interface{}) error {
	return c.callWithPayload(apiPath, "POST", payload, output)
}

// nolint
func (c *client) callDELETE(apiPath string, output interface{}) error {
	return c.callWithNoPayload(apiPath, "DELETE", output)
}

// nolint
func (c *client) callWithPayload(apiPath, method string, payload interface{}, output interface{}) error {
	body, err := json.Marshal(payload)
	if err != nil {
		return err
	}
	respBody, err := c.transport.Submit(apiPath, method, bytes.NewReader(body))
	if err != nil {
		return err
	}
	return json.Unmarshal(respBody, output)
}

func (c *client) callWithNoPayload(apiPath, method string, output interface{}) error {
	body, err := c.transport.Submit(apiPath, method, nil)
	if err != nil {
		return err
	}
	return json.Unmarshal(body, output)
}

func (c *HttpTransport) Submit(apiPath, method string, payload io.Reader) ([]byte, error) {
	log.Info("call central api", zap.String("path", apiPath))
	req, err := http.NewRequest(method, apiPath, payload)
	if err != nil {
		return nil, err
	}
	if err := c.Authenticator.Auth(req); err != nil {
		return nil, err
	}
	// #nosec
	resp, err := c.HttpClient.Do(req)
	if err != nil {
		return nil, err
	}
	// #nosec
	defer deferClose(resp.Body)
	bodyContent, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("call central failed with status code %d, msg:%s", resp.StatusCode, string(bodyContent))
	}
	return bodyContent, nil
}

func deferClose(c io.Closer) {
	if err := c.Close(); err != nil {
		log.Error("failed to close", zap.Error(err))
	}
}
