/*
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 api

import (
	"encoding/base64"
	"fmt"
	"net/http"
	"reflect"
	"strings"

	"github.com/apache/incubator-devlake/core/errors"
	"github.com/apache/incubator-devlake/core/plugin"
	"github.com/go-playground/validator/v10"
)

// BasicAuth implements HTTP Basic Authentication
type BasicAuth struct {
	Username string `mapstructure:"username" validate:"required" json:"username"`
	Password string `mapstructure:"password" validate:"required" json:"password" gorm:"serializer:encdec"`
}

// GetEncodedToken returns encoded bearer token for HTTP Basic Authentication
func (ba *BasicAuth) GetEncodedToken() string {
	return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", ba.Username, ba.Password)))
}

// SetupAuthentication sets up the request headers for authentication
func (ba *BasicAuth) SetupAuthentication(request *http.Request) errors.Error {
	request.Header.Set("Authorization", fmt.Sprintf("Basic %v", ba.GetEncodedToken()))
	return nil
}

// GetBasicAuthenticator returns the ApiAuthenticator for setting up the HTTP request
// it looks odd to return itself with a different type, this is necessary because Callers
// might call the method from the Outer-Struct(`connection.SetupAuthentication(...)`)
// which would lead to a Stack Overflow  error
func (ba *BasicAuth) GetBasicAuthenticator() plugin.ApiAuthenticator {
	return ba
}

// AccessToken implements HTTP Bearer Authentication with Access Token
type AccessToken struct {
	Token string `mapstructure:"token" validate:"required" json:"token" gorm:"serializer:encdec"`
}

// SetupAuthentication sets up the request headers for authentication
func (at *AccessToken) SetupAuthentication(request *http.Request) errors.Error {
	request.Header.Set("Authorization", fmt.Sprintf("Bearer %v", at.Token))
	return nil
}

// GetAccessTokenAuthenticator returns SetupAuthentication
func (at *AccessToken) GetAccessTokenAuthenticator() plugin.ApiAuthenticator {
	return at
}

// AppKey implements the API Key and Secret authentication mechanism
type AppKey struct {
	AppId     string `mapstructure:"appId" validate:"required" json:"appId"`
	SecretKey string `mapstructure:"secretKey" validate:"required" json:"secretKey" gorm:"serializer:encdec"`
}

// SetupAuthentication sets up the request headers for authentication
func (ak *AppKey) SetupAuthentication(request *http.Request) errors.Error {
	// no universal way to implement AppKey authentication, plugin should alias AppKey and
	// define its own implementation if API requires signature for each request,
	// or you should implement PrepareApiClient if API requires a Token for all requests
	return nil
}

// GetAppKeyAuthenticator returns SetupAuthentication
func (ak *AppKey) GetAppKeyAuthenticator() plugin.ApiAuthenticator {
	// no universal way to implement AppKey authentication, plugin should alias AppKey and
	// define its own implementation
	return ak
}

// MultiAuth implements the MultiAuthenticator interface
type MultiAuth struct {
	AuthMethod       string `mapstructure:"authMethod" json:"authMethod" validate:"required,oneof=BasicAuth AccessToken AppKey"`
	apiAuthenticator plugin.ApiAuthenticator
}

func (ma *MultiAuth) GetApiAuthenticator(connection plugin.ApiConnection) (plugin.ApiAuthenticator, errors.Error) {
	// cache the ApiAuthenticator for performance
	if ma.apiAuthenticator != nil {
		return ma.apiAuthenticator, nil
	}
	// cache missed
	switch ma.AuthMethod {
	case plugin.AUTH_METHOD_BASIC:
		basicAuth, ok := connection.(plugin.BasicAuthenticator)
		if !ok {
			return nil, errors.Default.New("connection doesn't support Basic Authentication")
		}
		ma.apiAuthenticator = basicAuth.GetBasicAuthenticator()
	case plugin.AUTH_METHOD_TOKEN:
		accessToken, ok := connection.(plugin.AccessTokenAuthenticator)
		if !ok {
			return nil, errors.Default.New("connection doesn't support AccessToken Authentication")
		}
		ma.apiAuthenticator = accessToken.GetAccessTokenAuthenticator()
	case plugin.AUTH_METHOD_APPKEY:
		// Note that AppKey Authentication requires complex logic like signing the request with timestamp
		// so, there is no way to solve them once and for all, each Specific Connection should implement
		// on its own.
		appKey, ok := connection.(plugin.AppKeyAuthenticator)
		if !ok {
			return nil, errors.Default.New("connection doesn't support AppKey Authentication")
		}
		// check ae/models/connection.go:AeAppKey if you needed an example
		ma.apiAuthenticator = appKey.GetAppKeyAuthenticator()
	default:
		return nil, errors.Default.New("no Authentication Method was specified")
	}
	return ma.apiAuthenticator, nil
}

// SetupAuthenticationForConnection sets up authentication for the specified `req` based on connection
// Specific Connection should implement IAuthentication and then call this method for MultiAuth to work properly,
// check jira/models/connection.go:JiraConn if you needed an example
// Note: this method would be called for each request, so it is performance-sensitive, do NOT use reflection here
func (ma *MultiAuth) SetupAuthenticationForConnection(connection plugin.ApiConnection, req *http.Request) errors.Error {
	apiAuthenticator, err := ma.GetApiAuthenticator(connection)
	if err != nil {
		return err
	}
	return apiAuthenticator.SetupAuthentication(req)
}

func (ma *MultiAuth) CustomValidate(connection interface{}, v *validator.Validate) errors.Error {
	return ma.ValidateConnection(connection, v)
}

// TODO: deprecated, rename to CustomValidate instead
func (ma *MultiAuth) ValidateConnection(connection interface{}, v *validator.Validate) errors.Error {
	// the idea is to filter out errors from unselected Authentication struct
	err := v.Struct(connection)
	if err == nil {
		return nil
	}
	validationErrors, ok := err.(validator.ValidationErrors)
	if !ok {
		return errors.Default.New("err: %s cannot be cast to validationErrors")
	}
	if validationErrors != nil {
		filteredValidationErrors := make(validator.ValidationErrors, 0)
		connType := reflect.TypeOf(connection).Elem()
		for _, e := range validationErrors {
			ns := strings.Split(e.Namespace(), ".")
			if len(ns) > 1 {
				// case 1: using the origin Authenticator directly, e.g. BasicAuth in JiraConnection.JiraConn.BasicAuth.Username
				authName := ns[len(ns)-2]
				if plugin.ALL_AUTH[authName] && authName != ma.AuthMethod {
					continue
				}
				// case 2: embed origin Authenticator, e.g. GithubAppKey from
				// https://github.com/apache/incubator-devlake/blob/16f97a1a7605d5ce7cf391bbec1270eec7c77b6e/backend/plugins/github/models/connection.go#L41
				// skip the error if the field doesn't belong to the current authMethod
				shouldInclude := true
				t := connType
				// search from outer to inner of the struct to see if any field has tag `authMethod`
				for _, n := range ns[1:] {
					if field, ok := t.FieldByName(n); ok {
						authMethod := field.Tag.Get("authMethod")
						// drop error if authMethod was marked and it doesn't equal to current AutheMethod
						if authMethod != "" && authMethod != ma.AuthMethod {
							shouldInclude = false
							break
						}
						t = field.Type
						if t.Kind() == reflect.Ptr {
							t = t.Elem()
						}
					}
				}
				if shouldInclude {
					filteredValidationErrors = append(filteredValidationErrors, e)
				}
			}
		}
		if len(filteredValidationErrors) > 0 {
			return errors.BadInput.Wrap(filteredValidationErrors, "validation failed")
		}
	}
	return nil
}
