/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 whisk

import (
	"errors"
	"fmt"
	"github.com/apache/openwhisk-client-go/wski18n"
	"net/http"
	"strings"
)

type ApiService struct {
	client *Client
}

// wsk api create : Request, Response
type ApiCreateRequest struct {
	ApiDoc *Api `json:"apidoc,omitempty"`
}
type ApiCreateRequestOptions ApiOptions
type ApiCreateResponse RetApi

// wsk api list : Request, Response
type ApiListRequest struct {
}
type ApiListRequestOptions struct {
	ApiOptions
	Limit int  `url:"limit"`
	Skip  int  `url:"skip"`
	Docs  bool `url:"docs,omitempty"`
}
type ApiListResponse RetApiArray

// wsk api get : Request, Response
type ApiGetRequest struct {
	Api
}
type ApiGetRequestOptions ApiOptions
type ApiGetResponse RetApiArray

// wsk api delete : Request, Response
type ApiDeleteRequest struct {
	Api
}
type ApiDeleteRequestOptions ApiOptions
type ApiDeleteResponse struct{}

type Api struct {
	Namespace       string         `json:"namespace,omitempty"`
	ApiName         string         `json:"apiName,omitempty"`
	GatewayBasePath string         `json:"gatewayBasePath,omitempty"`
	GatewayRelPath  string         `json:"gatewayPath,omitempty"`
	GatewayMethod   string         `json:"gatewayMethod,omitempty"`
	Id              string         `json:"id,omitempty"`
	GatewayFullPath string         `json:"gatewayFullPath,omitempty"`
	Swagger         string         `json:"swagger,omitempty"`
	Action          *ApiAction     `json:"action,omitempty"`
	PathParameters  []ApiParameter `json:"pathParameters,omitempty"`
}

type ApiParameter struct {
	Name             string                 `json:"name"`
	In               string                 `json:"in"`
	Description      string                 `json:"description,omitempty"`
	Required         bool                   `json:"required,omitempty"`
	Type             string                 `json:"type,omitempty"`
	Format           string                 `json:"format,omitempty"`
	AllowEmptyValue  bool                   `json:"allowEmptyValue,omitempty"`
	Items            map[string]interface{} `json:"items,omitempty"`
	CollectionFormat string                 `json:"collectionFormat,omitempty"`
	Default          interface{}            `json:"default,omitempty"`
	Maximum          int                    `json:"maximum,omitempty"`
	ExclusiveMaximum bool                   `json:"exclusiveMaximum,omitempty"`
	Minimum          int                    `json:"minimum,omitempty"`
	ExclusiveMinimum bool                   `json:"exclusiveMinimum,omitempty"`
	MaxLength        int                    `json:"maxLength,omitempty"`
	MinLength        int                    `json:"minLength,omitempty"`
	Pattern          string                 `json:"pattern,omitempty"`
	MaxItems         int                    `json:"maxItems,omitempty"`
	MinItems         int                    `json:"minItems,omitempty"`
	UniqueItems      bool                   `json:"uniqueItems,omitempty"`
	MultipleOf       int                    `json:"multipleOf,omitempty"`
	Enum             interface{}            `json:"enum,omitempty"`
	Ref              string                 `json:"$ref,omitempty"`
}

type ApiAction struct {
	Name          string      `json:"name,omitempty"`
	Namespace     string      `json:"namespace,omitempty"`
	BackendMethod string      `json:"backendMethod,omitempty"`
	BackendUrl    string      `json:"backendUrl,omitempty"`
	Auth          string      `json:"authkey,omitempty"`
	SecureKey     interface{} `json:"secureKey,omitempty"`
}

type ApiOptions struct {
	ActionName   string `url:"action,omitempty"`
	ApiBasePath  string `url:"basepath,omitempty"`
	ApiRelPath   string `url:"relpath,omitempty"`
	ApiVerb      string `url:"operation,omitempty"`
	ApiName      string `url:"apiname,omitempty"`
	SpaceGuid    string `url:"spaceguid,omitempty"`
	AccessToken  string `url:"accesstoken,omitempty"`
	ResponseType string `url:"responsetype,omitempty"`
}

type ApiUserAuth struct {
	SpaceGuid   string `json:"spaceguid,omitempty"`
	AccessToken string `json:"accesstoken,omitempty"`
}

type RetApiArray struct {
	Apis []ApiItem `json:"apis,omitempty"`
}

type ApiItem struct {
	ApiId    string  `json:"id,omitempty"`
	QueryKey string  `json:"key,omitempty"`
	ApiValue *RetApi `json:"value,omitempty"`
}

type RetApi struct {
	Namespace string      `json:"namespace"`
	BaseUrl   string      `json:"gwApiUrl"`
	Activated bool        `json:"gwApiActivated"`
	TenantId  string      `json:"tenantId"`
	Swagger   *ApiSwagger `json:"apidoc,omitempty"`
}

type ApiSwagger struct {
	SwaggerName string                     `json:"swagger,omitempty"`
	BasePath    string                     `json:"basePath,omitempty"`
	Info        *ApiSwaggerInfo            `json:"info,omitempty"`
	Paths       map[string]*ApiSwaggerPath `json:"paths,omitempty"`
	SecurityDef interface{}                `json:"securityDefinitions,omitempty"`
	Security    interface{}                `json:"security,omitempty"`
	XConfig     interface{}                `json:"x-ibm-configuration,omitempty"`
	XRateLimit  interface{}                `json:"x-ibm-rate-limit,omitempty"`
}

type ApiSwaggerPath struct {
	Get        *ApiSwaggerOperation `json:"get,omitempty"`
	Put        *ApiSwaggerOperation `json:"put,omitempty"`
	Post       *ApiSwaggerOperation `json:"post,omitempty"`
	Delete     *ApiSwaggerOperation `json:"delete,omitempty"`
	Options    *ApiSwaggerOperation `json:"options,omitempty"`
	Head       *ApiSwaggerOperation `json:"head,omitempty"`
	Patch      *ApiSwaggerOperation `json:"patch,omitempty"`
	Parameters []ApiParameter       `json:"parameters,omitempty"`
}

func (asp *ApiSwaggerPath) MakeOperationMap() map[string]*ApiSwaggerOperation {
	var opMap map[string]*ApiSwaggerOperation = make(map[string]*ApiSwaggerOperation)
	if asp.Get != nil {
		opMap["get"] = asp.Get
	}
	if asp.Put != nil {
		opMap["put"] = asp.Put
	}
	if asp.Post != nil {
		opMap["post"] = asp.Post
	}
	if asp.Delete != nil {
		opMap["delete"] = asp.Delete
	}
	if asp.Options != nil {
		opMap["options"] = asp.Options
	}
	if asp.Head != nil {
		opMap["head"] = asp.Head
	}
	if asp.Patch != nil {
		opMap["patch"] = asp.Patch
	}
	return opMap
}

type ApiSwaggerInfo struct {
	Title   string `json:"title,omitempty"`
	Version string `json:"version,omitempty"`
}

type ApiSwaggerOperation struct {
	OperationId string                  `json:"operationId"`
	Parameters  []ApiParameter          `json:"parameters,omitempty"`
	Responses   interface{}             `json:"responses"`
	XOpenWhisk  *ApiSwaggerOpXOpenWhisk `json:"x-openwhisk,omitempty"`
}

type ApiSwaggerOpXOpenWhisk struct {
	ActionName string `json:"action"`
	Namespace  string `json:"namespace"`
	Package    string `json:"package"`
	ApiUrl     string `json:"url"`
}

// Used for printing individual APIs in non-truncated form
type ApiFilteredList struct {
	ActionName string
	ApiName    string
	BasePath   string
	RelPath    string
	Verb       string
	Url        string
}

// Used for printing individual APIs in truncated form
type ApiFilteredRow struct {
	ActionName string
	ApiName    string
	BasePath   string
	RelPath    string
	Verb       string
	Url        string
	FmtString  string
}

var ApiVerbs map[string]bool = map[string]bool{
	"GET":     true,
	"PUT":     true,
	"POST":    true,
	"DELETE":  true,
	"PATCH":   true,
	"HEAD":    true,
	"OPTIONS": true,
}

const (
	Overwrite      = true
	DoNotOverwrite = false
)

/////////////////
// Api Methods //
/////////////////

// Compare(sortable) compares api to sortable for the purpose of sorting.
// REQUIRED: sortable must also be of type ApiFilteredList.
// ***Method of type Sortable***
func (api ApiFilteredList) Compare(sortable Sortable) bool {
	// Sorts alphabetically by [BASE_PATH | API_NAME] -> REL_PATH -> API_VERB
	apiToCompare := sortable.(ApiFilteredList)
	var apiString string
	var compareString string

	apiString = strings.ToLower(fmt.Sprintf("%s%s%s", api.BasePath, api.RelPath,
		api.Verb))
	compareString = strings.ToLower(fmt.Sprintf("%s%s%s", apiToCompare.BasePath,
		apiToCompare.RelPath, apiToCompare.Verb))

	return apiString < compareString
}

// ToHeaderString() returns the header for a list of apis
func (api ApiFilteredList) ToHeaderString() string {
	return ""
}

// ToSummaryRowString() returns a compound string of required parameters for printing
//   from CLI command `wsk api list` or `wsk api-experimental list`.
// ***Method of type Sortable***
func (api ApiFilteredList) ToSummaryRowString() string {
	return fmt.Sprintf("%s %s %s %s %s %s",
		fmt.Sprintf("%s: %s\n", wski18n.T("Action"), api.ActionName),
		fmt.Sprintf("  %s: %s\n", wski18n.T("API Name"), api.ApiName),
		fmt.Sprintf("  %s: %s\n", wski18n.T("Base path"), api.BasePath),
		fmt.Sprintf("  %s: %s\n", wski18n.T("Path"), api.RelPath),
		fmt.Sprintf("  %s: %s\n", wski18n.T("Verb"), api.Verb),
		fmt.Sprintf("  %s: %s\n", wski18n.T("URL"), api.Url))
}

// Compare(sortable) compares api to sortable for the purpose of sorting.
// REQUIRED: sortable must also be of type ApiFilteredRow.
// ***Method of type Sortable***
func (api ApiFilteredRow) Compare(sortable Sortable) bool {
	// Sorts alphabetically by [BASE_PATH | API_NAME] -> REL_PATH -> API_VERB
	var apiString string
	var compareString string
	apiToCompare := sortable.(ApiFilteredRow)

	apiString = strings.ToLower(fmt.Sprintf("%s%s%s", api.BasePath, api.RelPath,
		api.Verb))
	compareString = strings.ToLower(fmt.Sprintf("%s%s%s", apiToCompare.BasePath,
		apiToCompare.RelPath, apiToCompare.Verb))

	return apiString < compareString
}

// ToHeaderString() returns the header for a list of apis
func (api ApiFilteredRow) ToHeaderString() string {
	return fmt.Sprintf("%s", fmt.Sprintf(api.FmtString, "Action", "Verb", "API Name", "URL"))
}

// ToSummaryRowString() returns a compound string of required parameters for printing
//   from CLI command `wsk api list -f` or `wsk api-experimental list -f`.
// ***Method of type Sortable***
func (api ApiFilteredRow) ToSummaryRowString() string {
	return fmt.Sprintf(api.FmtString, api.ActionName, api.Verb, api.ApiName, api.Url)
}

func (s *ApiService) List(apiListOptions *ApiListRequestOptions) (*ApiListResponse, *http.Response, error) {
	route := "web/whisk.system/apimgmt/getApi.http"

	routeUrl, err := addRouteOptions(route, apiListOptions)
	if err != nil {
		Debug(DbgError, "addRouteOptions(%s, %#v) error: '%s'\n", route, apiListOptions, err)
		errMsg := wski18n.T("Unable to add route options '{{.options}}'",
			map[string]interface{}{"options": apiListOptions})
		whiskErr := MakeWskErrorFromWskError(errors.New(errMsg), err, EXIT_CODE_ERR_GENERAL, DISPLAY_MSG,
			NO_DISPLAY_USAGE)
		return nil, nil, whiskErr
	}
	Debug(DbgInfo, "Api GET/list route with api options: %s\n", routeUrl)

	req, err := s.client.NewRequestUrl("GET", routeUrl, nil, DoNotIncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson, AuthRequired)
	if err != nil {
		Debug(DbgError, "http.NewRequestUrl(GET, %s, nil, DoNotIncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson) error: '%s'\n", routeUrl, err)
		errMsg := wski18n.T("Unable to create HTTP request for GET '{{.route}}': {{.err}}",
			map[string]interface{}{"route": routeUrl, "err": err})
		whiskErr := MakeWskErrorFromWskError(errors.New(errMsg), err, EXIT_CODE_ERR_NETWORK, DISPLAY_MSG,
			NO_DISPLAY_USAGE)
		return nil, nil, whiskErr
	}

	apiArray := new(ApiListResponse)
	resp, err := s.client.Do(req, &apiArray, ExitWithErrorOnTimeout)
	if err != nil {
		Debug(DbgError, "s.client.Do() error - HTTP req %s; error '%s'\n", req.URL.String(), err)
		return nil, resp, err
	}

	err = validateApiListResponse(apiArray)
	if err != nil {
		Debug(DbgError, "Not a valid ApiListResponse object\n")
		return nil, resp, err
	}

	return apiArray, resp, err
}

func (s *ApiService) Insert(api *ApiCreateRequest, options *ApiCreateRequestOptions, overwrite bool) (*ApiCreateResponse, *http.Response, error) {
	route := "web/whisk.system/apimgmt/createApi.http"
	Debug(DbgInfo, "Api PUT route: %s\n", route)

	routeUrl, err := addRouteOptions(route, options)
	if err != nil {
		Debug(DbgError, "addRouteOptions(%s, %#v) error: '%s'\n", route, options, err)
		errMsg := wski18n.T("Unable to add route options '{{.options}}'",
			map[string]interface{}{"options": options})
		whiskErr := MakeWskErrorFromWskError(errors.New(errMsg), err, EXIT_CODE_ERR_GENERAL, DISPLAY_MSG,
			NO_DISPLAY_USAGE)
		return nil, nil, whiskErr
	}
	Debug(DbgError, "Api create route with options: %s\n", routeUrl)

	req, err := s.client.NewRequestUrl("POST", routeUrl, api, DoNotIncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson, AuthRequired)
	if err != nil {
		Debug(DbgError, "http.NewRequestUrl(POST, %s, nil, DoNotIncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson) error: '%s'\n", route, err)
		errMsg := wski18n.T("Unable to create HTTP request for POST '{{.route}}': {{.err}}",
			map[string]interface{}{"route": route, "err": err})
		whiskErr := MakeWskErrorFromWskError(errors.New(errMsg), err, EXIT_CODE_ERR_NETWORK, DISPLAY_MSG,
			NO_DISPLAY_USAGE)
		return nil, nil, whiskErr
	}

	retApi := new(ApiCreateResponse)
	resp, err := s.client.Do(req, &retApi, ExitWithErrorOnTimeout)
	if err != nil {
		Debug(DbgError, "s.client.Do() error - HTTP req %s; error '%s'\n", req.URL.String(), err)
		return nil, resp, err
	}

	err = validateApiSwaggerResponse(retApi.Swagger)
	if err != nil {
		Debug(DbgError, "Not a valid API creation response\n")
		return nil, resp, err
	}

	return retApi, resp, nil
}

func (s *ApiService) Get(api *ApiGetRequest, options *ApiGetRequestOptions) (*ApiGetResponse, *http.Response, error) {
	route := "web/whisk.system/apimgmt/getApi.http"
	Debug(DbgInfo, "Api GET route: %s\n", route)

	routeUrl, err := addRouteOptions(route, options)
	if err != nil {
		Debug(DbgError, "addRouteOptions(%s, %#v) error: '%s'\n", route, options, err)
		errMsg := wski18n.T("Unable to add route options '{{.options}}'",
			map[string]interface{}{"options": options})
		whiskErr := MakeWskErrorFromWskError(errors.New(errMsg), err, EXIT_CODE_ERR_GENERAL, DISPLAY_MSG,
			NO_DISPLAY_USAGE)
		return nil, nil, whiskErr
	}
	Debug(DbgError, "Api get route with options: %s\n", routeUrl)

	req, err := s.client.NewRequestUrl("GET", routeUrl, nil, DoNotIncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson, AuthRequired)
	if err != nil {
		Debug(DbgError, "http.NewRequestUrl(GET, %s, nil, DoNotIncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson) error: '%s'\n", route, err)
		errMsg := wski18n.T("Unable to create HTTP request for GET '{{.route}}': {{.err}}",
			map[string]interface{}{"route": route, "err": err})
		whiskErr := MakeWskErrorFromWskError(errors.New(errMsg), err, EXIT_CODE_ERR_NETWORK, DISPLAY_MSG,
			NO_DISPLAY_USAGE)
		return nil, nil, whiskErr
	}

	retApi := new(ApiGetResponse)
	resp, err := s.client.Do(req, &retApi, ExitWithErrorOnTimeout)
	if err != nil {
		Debug(DbgError, "s.client.Do() error - HTTP req %s; error '%s'\n", req.URL.String(), err)
		return nil, resp, err
	}

	return retApi, resp, nil
}

func (s *ApiService) Delete(api *ApiDeleteRequest, options *ApiDeleteRequestOptions) (*http.Response, error) {
	route := "web/whisk.system/apimgmt/deleteApi.http"
	Debug(DbgInfo, "Api DELETE route: %s\n", route)

	routeUrl, err := addRouteOptions(route, options)
	if err != nil {
		Debug(DbgError, "addRouteOptions(%s, %#v) error: '%s'\n", route, options, err)
		errMsg := wski18n.T("Unable to add route options '{{.options}}'",
			map[string]interface{}{"options": options})
		whiskErr := MakeWskErrorFromWskError(errors.New(errMsg), err, EXIT_CODE_ERR_GENERAL, DISPLAY_MSG,
			NO_DISPLAY_USAGE)
		return nil, whiskErr
	}
	Debug(DbgError, "Api DELETE route with options: %s\n", routeUrl)

	req, err := s.client.NewRequestUrl("DELETE", routeUrl, nil, DoNotIncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson, AuthRequired)
	if err != nil {
		Debug(DbgError, "http.NewRequestUrl(DELETE, %s, nil, DoNotIncludeNamespaceInUrl, AppendOpenWhiskPathPrefix, EncodeBodyAsJson) error: '%s'\n", route, err)
		errMsg := wski18n.T("Unable to create HTTP request for DELETE '{{.route}}': {{.err}}",
			map[string]interface{}{"route": route, "err": err})
		whiskErr := MakeWskErrorFromWskError(errors.New(errMsg), err, EXIT_CODE_ERR_NETWORK, DISPLAY_MSG,
			NO_DISPLAY_USAGE)
		return nil, whiskErr
	}

	retApi := new(ApiDeleteResponse)
	resp, err := s.client.Do(req, &retApi, ExitWithErrorOnTimeout)
	if err != nil {
		Debug(DbgError, "s.client.Do() error - HTTP req %s; error '%s'\n", req.URL.String(), err)
		return resp, err
	}

	return nil, nil
}

func validateApiListResponse(apiList *ApiListResponse) error {
	for i := 0; i < len(apiList.Apis); i++ {
		if apiList.Apis[i].ApiValue == nil {
			Debug(DbgError, "validateApiResponse: No value stanza in api %v\n", apiList.Apis[i])
			errMsg := wski18n.T("Internal error. Missing value stanza in API configuration response")
			whiskErr := MakeWskError(errors.New(errMsg), EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE)
			return whiskErr
		}
		err := validateApiSwaggerResponse(apiList.Apis[i].ApiValue.Swagger)
		if err != nil {
			Debug(DbgError, "validateApiListResponse: Invalid Api: %v\n", apiList.Apis[i])
			return err
		}
	}
	return nil
}

func validateApiSwaggerResponse(swagger *ApiSwagger) error {
	if swagger == nil {
		Debug(DbgError, "validateApiSwaggerResponse: No apidoc stanza in api\n")
		errMsg := wski18n.T("Internal error. Missing apidoc stanza in API configuration")
		whiskErr := MakeWskError(errors.New(errMsg), EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE)
		return whiskErr
	}
	for path := range swagger.Paths {
		err := validateApiPath(swagger.Paths[path])
		if err != nil {
			Debug(DbgError, "validateApiResponse: Invalid Api Path object: %v\n", swagger.Paths[path])
			return err
		}
	}

	return nil
}

func validateApiPath(path *ApiSwaggerPath) error {
	for op, opv := range path.MakeOperationMap() {
		err := validateApiOperation(op, opv)
		if err != nil {
			Debug(DbgError, "validateApiPath: Invalid Api operation object: %v\n", opv)
			return err
		}
	}
	return nil
}

func validateApiOperation(opName string, op *ApiSwaggerOperation) error {
	if op.XOpenWhisk != nil && len(op.OperationId) == 0 {
		Debug(DbgError, "validateApiOperation: No operationId field in operation %v\n", op)
		errMsg := wski18n.T("Missing operationId field in API configuration for operation {{.op}}",
			map[string]interface{}{"op": opName})
		whiskErr := MakeWskError(errors.New(errMsg), EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE)
		return whiskErr
	}

	if op.XOpenWhisk != nil && len(op.XOpenWhisk.Namespace) == 0 {
		Debug(DbgError, "validateApiOperation: no x-openwhisk.namespace stanza in operation %v\n", op)
		errMsg := wski18n.T("Missing x-openwhisk.namespace field in API configuration for operation {{.op}}",
			map[string]interface{}{"op": opName})
		whiskErr := MakeWskError(errors.New(errMsg), EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE)
		return whiskErr
	}

	// Note: The op.XOpenWhisk.Package field can have a value of "", so don't enforce a value

	if op.XOpenWhisk != nil && len(op.XOpenWhisk.ActionName) == 0 {
		Debug(DbgError, "validateApiOperation: no x-openwhisk.action stanza in operation %v\n", op)
		errMsg := wski18n.T("Missing x-openwhisk.action field in API configuration for operation {{.op}}",
			map[string]interface{}{"op": opName})
		whiskErr := MakeWskError(errors.New(errMsg), EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE)
		return whiskErr
	}
	if op.XOpenWhisk != nil && len(op.XOpenWhisk.ApiUrl) == 0 {
		Debug(DbgError, "validateApiOperation: no x-openwhisk.url stanza in operation %v\n", op)
		errMsg := wski18n.T("Missing x-openwhisk.url field in API configuration for operation {{.op}}",
			map[string]interface{}{"op": opName})
		whiskErr := MakeWskError(errors.New(errMsg), EXIT_CODE_ERR_NETWORK, DISPLAY_MSG, NO_DISPLAY_USAGE)
		return whiskErr
	}
	return nil
}
