/*
Copyright 2021 The Dapr 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.
*/

package commercetools

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"reflect"

	"github.com/dapr/components-contrib/bindings"
	contribMetadata "github.com/dapr/components-contrib/metadata"
	"github.com/dapr/kit/logger"

	"github.com/labd/commercetools-go-sdk/platform"
	"golang.org/x/oauth2/clientcredentials"
)

type Binding struct {
	client     *platform.Client
	logger     logger.Logger
	projectKey string
}

type Data struct {
	CommercetoolsAPI string
	Query            string
}

type commercetoolsMetadata struct {
	Region       string
	Provider     string
	ProjectKey   string
	ClientID     string
	ClientSecret string
	Scopes       string
}

func NewCommercetools(logger logger.Logger) bindings.OutputBinding {
	return &Binding{logger: logger}
}

// Init does metadata parsing and connection establishment.
func (ct *Binding) Init(_ context.Context, metadata bindings.Metadata) error {
	commercetoolsM, err := ct.getCommercetoolsMetadata(metadata)
	if err != nil {
		return err
	}
	ct.projectKey = commercetoolsM.ProjectKey

	// The helper method NewClientEndpoint no longer exists, so URLs need to be manually constructed.
	// Reference: https://github.com/labd/commercetools-go-sdk/blob/15f4e7e85260cf206301504dced00a8bbf4d8682/commercetools/client.go#L115

	baseURLdomain := fmt.Sprintf("%s.%s.commercetools.com", commercetoolsM.Region, commercetoolsM.Provider)
	authURL := fmt.Sprintf("https://auth.%s/oauth/token", baseURLdomain)
	apiURL := fmt.Sprintf("https://api.%s", baseURLdomain)

	// Create the new client. When an empty value is passed it will use the CTP_*
	// environment variables to get the value. The HTTPClient arg is optional,
	// and when empty will automatically be created using the env values.
	client, err := platform.NewClient(&platform.ClientConfig{
		URL: apiURL,
		Credentials: &clientcredentials.Config{
			TokenURL:     authURL,
			ClientID:     commercetoolsM.ClientID,
			ClientSecret: commercetoolsM.ClientSecret,
			Scopes:       []string{commercetoolsM.Scopes},
		},
	})
	if err != nil {
		ct.logger.Errorf("error creating commercetools client: %s", err)

		return err
	}

	ct.client = client

	return nil
}

func (ct *Binding) Operations() []bindings.OperationKind {
	return []bindings.OperationKind{bindings.CreateOperation}
}

// Invoke is triggered from Dapr.
func (ct *Binding) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
	var reqData Data
	err := json.Unmarshal(req.Data, &reqData)
	if err != nil {
		return nil, err
	}
	query := reqData.Query

	res := &bindings.InvokeResponse{Data: nil, Metadata: nil}

	if len(reqData.CommercetoolsAPI) > 0 {
		ct.logger.Infof("commercetoolsAPI: %s", reqData.CommercetoolsAPI)
		if reqData.CommercetoolsAPI == "GraphQLQuery" {
			res, err = handleGraphQLQuery(ctx, ct, query)
			if err != nil {
				ct.logger.Errorf("error GraphQLQuery")

				return nil, err
			}
		}
	} else {
		return nil, errors.New("commercetools error: No commercetools API provided")
	}

	return res, nil
}

// handleGraphQLQuery executes the provided query against the commercetools backend.
func handleGraphQLQuery(ctx context.Context, ct *Binding, query string) (*bindings.InvokeResponse, error) {
	ct.logger.Infof("handleGraphQLQuery")

	res := &bindings.InvokeResponse{Data: nil, Metadata: nil}

	if len(query) > 0 {
		gql := ct.client.WithProjectKey(ct.projectKey).Graphql().Post(platform.GraphQLRequest{
			Query: query,
		})
		gqlResp, errGQL := gql.Execute(ctx)
		if errGQL != nil {
			return nil, errors.New("commercetools error: Error executing the provided GraphQL query")
		}

		bQuery, errM := json.Marshal(gqlResp.Data)
		if errM != nil {
			return nil, errors.New("commercetools error: Error marshalling GraphQL query result")
		}

		res = &bindings.InvokeResponse{Data: bQuery, Metadata: nil}
	} else {
		return res, errors.New("commercetools error: No GraphQL query is provided")
	}

	return res, nil
}

// getCommercetoolsMetadata returns new commercetools metadata object.
func (ct *Binding) getCommercetoolsMetadata(metadata bindings.Metadata) (*commercetoolsMetadata, error) {
	meta := commercetoolsMetadata{}

	if val, ok := metadata.Properties["region"]; ok && val != "" {
		meta.Region = val
	} else {
		return nil, errors.New("commercetools error: missing `region` configuration")
	}

	if val, ok := metadata.Properties["provider"]; ok && val != "" {
		meta.Provider = val
	} else {
		return nil, errors.New("commercetools error: missing `provider` configuration")
	}

	if val, ok := metadata.Properties["projectKey"]; ok && val != "" {
		meta.ProjectKey = val
	} else {
		return nil, errors.New("commercetools error: missing `projectKey` configuration")
	}

	if val, ok := metadata.Properties["clientID"]; ok && val != "" {
		meta.ClientID = val
	} else {
		return nil, errors.New("commercetools error: missing `clientID` configuration")
	}

	if val, ok := metadata.Properties["clientSecret"]; ok && val != "" {
		meta.ClientSecret = val
	} else {
		return nil, errors.New("commercetools error: missing `clientSecret` configuration")
	}

	if val, ok := metadata.Properties["scopes"]; ok && val != "" {
		meta.Scopes = val
	} else {
		return nil, errors.New("commercetools error: missing `scopes` configuration")
	}

	return &meta, nil
}

// Close releases the client.
func (ct *Binding) Close() error {
	ct.client = nil

	return nil
}

// GetComponentMetadata returns the metadata of the component.
func (ct Binding) GetComponentMetadata() (metadataInfo contribMetadata.MetadataMap) {
	metadataStruct := commercetoolsMetadata{}
	contribMetadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, contribMetadata.BindingType)
	return
}
