/*
Copyright (c) 2019-2023 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Authors:
Petr Krutov, <petr.krutov@digitalenergy.online>
Stanislav Solovev, <spsolovev@digitalenergy.online>
Kasim Baybikov, <kmbaybikov@basistech.ru>

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 controller

import (
	"crypto/tls"
	"fmt"
	"io"

	"net/http"
	"net/url"
	"strconv"
	"strings"

	log "github.com/sirupsen/logrus"
	decort "repository.basistech.ru/BASIS/decort-golang-sdk"
	"repository.basistech.ru/BASIS/decort-golang-sdk/config"
	"repository.basistech.ru/BASIS/decort-golang-sdk/interfaces"
	"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudapi"
	"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudbroker"
	"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/sdn"

	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

// enumerated constants that define authentication modes
const (
	MODE_UNDEF = iota // this is the invalid mode - it should never be seen
	MODE_LEGACY
	MODE_DECS3O
	MODE_JWT
	MODE_BVS
)

type ControllerCfg struct {
	controller_url  string       // always required
	auth_mode_code  int          // always required
	auth_mode_txt   string       // always required, it is a text representation of auth mode
	bvs_user        string       // required for bvs mode
	bvs_password    string       // required for bvs mode
	domain          string       // required for bvs mode
	token           config.Token // obtained from BVS provider on successful login in bvs mode
	path_cfg        string       // the path of the configuration file entry
	path_token      string       // the path of the token file entry
	time_to_refresh int64        // the number of minutes before the expiration of the token, a refresh will be made
	legacy_user     string       // required for legacy mode
	legacy_password string       // required for legacy mode
	legacy_sid      string       // obtained from DECORT controller on successful login in legacy mode
	jwt             string       // obtained from Outh2 provider on successful login in decs3o mode, required in jwt mode
	app_id          string       // required for decs3o and bvs mode
	app_secret      string       // required for decs3o and bvs mode
	oauth2_url      string       // required for decs3o and bvs mode
	decort_username string       // assigned to either legacy_user (legacy mode) or Oauth2 user (decs3o mode) upon successful verification
	cc_client       *http.Client // assigned when all initial checks successfully passed
	caller          interfaces.Caller
}

func ControllerConfigure(d *schema.ResourceData) (*ControllerCfg, error) {
	// This function first will check that all required provider parameters for the
	// selected authenticator mode are set correctly and initialize ControllerCfg structure
	// based on the provided parameters.
	//
	// Next, it will check for validity of supplied credentials by initiating connection to the specified
	// DECORT controller URL and, if succeeded, completes ControllerCfg structure with the rest of computed
	// parameters (e.g. JWT, session ID and Oauth2 user name).
	//
	// The structure created by this function should be used with subsequent calls to decortAPICall() method,
	// which is a DECORT authentication mode aware wrapper around standard HTTP requests.

	ret_config := &ControllerCfg{
		controller_url:  d.Get("controller_url").(string),
		auth_mode_code:  MODE_UNDEF,
		legacy_user:     d.Get("user").(string),
		legacy_password: d.Get("password").(string),
		legacy_sid:      "",
		bvs_user:        d.Get("bvs_user").(string),
		bvs_password:    d.Get("bvs_password").(string),
		domain:          d.Get("domain").(string),
		jwt:             d.Get("jwt").(string),
		app_id:          d.Get("app_id").(string),
		app_secret:      d.Get("app_secret").(string),
		oauth2_url:      d.Get("oauth2_url").(string),
		decort_username: "",
		token:           config.Token{},
		path_cfg:        d.Get("path_cfg").(string),
		path_token:      d.Get("path_token").(string),
		time_to_refresh: int64(d.Get("time_to_refresh").(int)),
	}

	allow_unverified_ssl := d.Get("allow_unverified_ssl").(bool)

	if ret_config.controller_url == "" {
		return nil, fmt.Errorf("empty DECORT cloud controller URL provided")
	}

	// this should have already been done by StateFunc defined in Schema, but we want to be sure
	ret_config.auth_mode_txt = strings.ToLower(d.Get("authenticator").(string))

	switch ret_config.auth_mode_txt {
	case "jwt":
		if ret_config.jwt == "" {
			return nil, fmt.Errorf("authenticator mode 'jwt' specified but no JWT provided")
		}
		ret_config.auth_mode_code = MODE_JWT
	case "decs3o":
		if ret_config.oauth2_url == "" {
			return nil, fmt.Errorf("authenticator mode 'decs3o' specified but no OAuth2 URL provided")
		}
		if ret_config.app_id == "" {
			return nil, fmt.Errorf("authenticator mode 'decs3o' specified but no Application ID provided")
		}
		if ret_config.app_secret == "" {
			return nil, fmt.Errorf("authenticator mode 'decs3o' specified but no Secret ID provided")
		}
		ret_config.auth_mode_code = MODE_DECS3O
	case "legacy":
		//
		ret_config.legacy_user = d.Get("user").(string)
		if ret_config.legacy_user == "" {
			return nil, fmt.Errorf("authenticator mode 'legacy' specified but no user provided")
		}
		ret_config.legacy_password = d.Get("password").(string)
		if ret_config.legacy_password == "" {
			return nil, fmt.Errorf("authenticator mode 'legacy' specified but no password provided")
		}
		ret_config.auth_mode_code = MODE_LEGACY
	case "bvs":
		if ret_config.bvs_user == "" {
			return nil, fmt.Errorf("authenticator mode 'bvs' specified but no user provided")
		}
		if ret_config.bvs_password == "" {
			return nil, fmt.Errorf("authenticator mode 'bvs' specified but no password provided")
		}
		if ret_config.oauth2_url == "" {
			return nil, fmt.Errorf("authenticator mode 'bvs' specified but no bvs URL provided")
		}
		if ret_config.app_id == "" {
			return nil, fmt.Errorf("authenticator mode 'bvs' specified but no Application ID provided")
		}
		if ret_config.app_secret == "" {
			return nil, fmt.Errorf("authenticator mode 'bvs' specified but no Secret ID provided")
		}
		if ret_config.domain == "" {
			return nil, fmt.Errorf("authenticator mode 'bvs' specified but no Domain provided")
		}
		ret_config.auth_mode_code = MODE_BVS
	default:
		return nil, fmt.Errorf("unknown authenticator mode %q provided", ret_config.auth_mode_txt)
	}

	if allow_unverified_ssl {
		log.Warn("ControllerConfigure: allow_unverified_ssl is set - will not check certificates!")
		transCfg := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} //nolint:gosec
		ret_config.cc_client = &http.Client{
			Transport: transCfg,
		}
	} else {
		ret_config.cc_client = &http.Client{}
	}

	switch ret_config.auth_mode_code {
	case MODE_LEGACY:
		ok, err := ret_config.validateLegacyUser()
		if !ok {
			return nil, err
		}
		ret_config.decort_username = ret_config.legacy_user

		sdkConf := config.LegacyConfig{
			Username:      ret_config.legacy_user,
			Password:      ret_config.legacy_password,
			DecortURL:     ret_config.controller_url,
			SSLSkipVerify: allow_unverified_ssl,
		}

		ret_config.caller = decort.NewLegacy(sdkConf)

	case MODE_JWT:
		//
		ok, err := ret_config.validateJWT("")
		if !ok {
			return nil, err
		}
	case MODE_DECS3O:
		// on success getDECS3OJWT will set config.jwt to the obtained JWT, so there is no
		// need to set it once again here
		// _, err := ret_config.getDECS3OJWT()
		// if err != nil {
		// 	return nil, err
		// }
		// we are not verifying the JWT when parsing because actual verification is done on the
		// OVC controller side. Here we do parsing solely to extract Oauth2 user name (claim "user")
		// and JWT issuer name (claim "iss")
		// parser := jwt.Parser{}
		// token, _, err := parser.ParseUnverified(ret_config.jwt, jwt.MapClaims{})
		// if err != nil {
		// 	return nil, err
		// }
		// if claims, ok := token.Claims.(jwt.MapClaims); ok {
		// 	var tbuf bytes.Buffer
		// 	tbuf.WriteString(claims["username"].(string))
		// 	tbuf.WriteString("@")
		// 	tbuf.WriteString(claims["iss"].(string))
		// 	ret_config.decort_username = tbuf.String()
		// } else {
		// 	return nil, fmt.Errorf("failed to extract user and iss fields from JWT token in oauth2 mode")
		// }

		sdkConf := config.Config{
			AppID:         ret_config.app_id,
			AppSecret:     ret_config.app_secret,
			SSOURL:        ret_config.oauth2_url,
			DecortURL:     ret_config.controller_url,
			SSLSkipVerify: allow_unverified_ssl,
		}

		ret_config.caller = decort.New(sdkConf)
	case MODE_BVS:

		sdkConf := config.BVSConfig{
			AppID:         ret_config.app_id,
			AppSecret:     ret_config.app_secret,
			SSOURL:        ret_config.oauth2_url,
			DecortURL:     ret_config.controller_url,
			SSLSkipVerify: allow_unverified_ssl,
			Username:      ret_config.bvs_user,
			Password:      ret_config.bvs_password,
			Domain:        ret_config.domain,
			Token:         ret_config.token,
			PathCfg:       ret_config.path_cfg,
			PathToken:     ret_config.path_token,
			TimeToRefresh: ret_config.time_to_refresh,
		}

		ret_config.caller = decort.NewBVS(sdkConf)
	default:
		// FYI, this should never happen due to all above checks, but we want to be fool proof
		return nil, fmt.Errorf("unknown authenticator mode code %d provided", ret_config.auth_mode_code)
	}

	// All checks passed successfully, credentials corresponding to the selected authenticator mode
	// obtained and validated.
	return ret_config, nil
}

// func (config *ControllerCfg) GetDecortUsername() string {
// 	return config.decort_username
// }

// func (config *ControllerCfg) getDECS3OJWT() (string, error) {
// 	// 	Obtain JWT from the Oauth2 provider using application ID and application secret provided in config.
// 	if config.auth_mode_code == MODE_UNDEF {
// 		return "", fmt.Errorf("getOAuth2JWT method called for undefined authorization mode")
// 	}
// 	if config.auth_mode_code != MODE_DECS3O {
// 		return "", fmt.Errorf("getOAuth2JWT method called for incompatible authorization mode %q", config.auth_mode_txt)
// 	}

// 	params := url.Values{}
// 	params.Add("grant_type", "client_credentials")
// 	params.Add("client_id", config.app_id)
// 	params.Add("client_secret", config.app_secret)
// 	params.Add("response_type", "id_token")
// 	params.Add("validity", "3600")
// 	params_str := params.Encode()

// 	req, err := http.NewRequest("POST", config.oauth2_url+"/v1/oauth/access_token", strings.NewReader(params_str))
// 	if err != nil {
// 		return "", err
// 	}
// 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// 	req.Header.Set("Content-Length", strconv.Itoa(len(params_str)))

// 	resp, err := config.cc_client.Do(req)
// 	if err != nil {
// 		return "", err
// 	}
// 	if resp.StatusCode != http.StatusOK {
// 		// fmt.Println("response Status:", resp.Status)
// 		// fmt.Println("response Headers:", resp.Header)
// 		// fmt.Println("response Headers:", req.URL)
// 		return "", fmt.Errorf("getOauth2JWT: unexpected status code %d when obtaining JWT from %q for APP_ID %q, request Body %q",
// 			resp.StatusCode, req.URL, config.app_id, params_str)
// 	}
// 	defer resp.Body.Close()

// 	responseData, err := io.ReadAll(resp.Body)
// 	if err != nil {
// 		return "", err
// 	}

// 	// validation successful - store JWT in the corresponding field of the ControllerCfg structure
// 	config.jwt = strings.TrimSpace(string(responseData))

// 	return config.jwt, nil
// }

func (config *ControllerCfg) validateJWT(jwt string) (bool, error) {
	/*
			Validate JWT against DECORT controller. JWT can be supplied as argument to this method. If empty string supplied as
			argument, JWT will be taken from config attribute.
			DECORT controller URL will always be taken from the config attribute assigned at instantiation.
		    Validation is accomplished by attempting API call that lists account for the invoking user.
	*/
	if jwt == "" {
		if config.jwt == "" {
			return false, fmt.Errorf("validateJWT method called, but no meaningful JWT provided")
		}
		jwt = config.jwt
	}

	if config.oauth2_url == "" {
		return false, fmt.Errorf("validateJWT method called, but no OAuth2 URL provided")
	}

	req, err := http.NewRequest("POST", config.controller_url+"/restmachine/cloudapi/account/list", nil)
	if err != nil {
		return false, err
	}

	req.Header.Set("Authorization", fmt.Sprintf("bearer %s", jwt))
	// req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	// req.Header.Set("Content-Length", strconv.Itoa(0))

	resp, err := config.cc_client.Do(req)
	if err != nil {
		return false, err
	}
	if resp.StatusCode != http.StatusOK {
		return false, fmt.Errorf("validateJWT: unexpected status code %d when validating JWT against %q",
			resp.StatusCode, req.URL)
	}
	defer resp.Body.Close()

	return true, nil
}

func (config *ControllerCfg) validateLegacyUser() (bool, error) {
	/*
			Validate legacy user by obtaining a session key, which will be used for authenticating subsequent API calls
		    to DECORT controller.
		    If successful, the session key is stored in config.legacy_sid and true is returned. If unsuccessful for any
		    reason, the method will return false and error.
	*/
	if config.auth_mode_code == MODE_UNDEF {
		return false, fmt.Errorf("validateLegacyUser method called for undefined authorization mode")
	}
	if config.auth_mode_code != MODE_LEGACY {
		return false, fmt.Errorf("validateLegacyUser method called for incompatible authorization mode %q", config.auth_mode_txt)
	}

	params := url.Values{}
	params.Add("username", config.legacy_user)
	params.Add("password", config.legacy_password)
	params_str := params.Encode()

	req, err := http.NewRequest("POST", config.controller_url+"/restmachine/cloudapi/user/authenticate", strings.NewReader(params_str))
	if err != nil {
		return false, err
	}

	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("Content-Length", strconv.Itoa(len(params_str)))

	resp, err := config.cc_client.Do(req)
	if err != nil {
		return false, err
	}
	if resp.StatusCode != http.StatusOK {
		return false, fmt.Errorf("validateLegacyUser: unexpected status code %d when validating legacy user %q against %q",
			resp.StatusCode, config.legacy_user, config.controller_url)
	}
	defer resp.Body.Close()

	responseData, err := io.ReadAll(req.Body)
	if err != nil {
		log.Fatal(err)
	}

	// validation successful - keep session ID for future use
	config.legacy_sid = string(responseData)

	return true, nil
}

func (config *ControllerCfg) CloudAPI() *cloudapi.CloudAPI {
	switch config.auth_mode_code {
	case MODE_LEGACY:
		client, _ := config.caller.(*decort.LegacyDecortClient)
		return client.CloudAPI()
	case MODE_DECS3O:
		client, _ := config.caller.(*decort.DecortClient)
		return client.CloudAPI()
	case MODE_BVS:
		client, _ := config.caller.(*decort.BVSDecortClient)
		return client.CloudAPI()
	default:
		return &cloudapi.CloudAPI{}
	}
}
func (config *ControllerCfg) CloudBroker() *cloudbroker.CloudBroker {
	switch config.auth_mode_code {
	case MODE_LEGACY:
		client, _ := config.caller.(*decort.LegacyDecortClient)
		return client.CloudBroker()
	case MODE_DECS3O:
		client, _ := config.caller.(*decort.DecortClient)
		return client.CloudBroker()
	case MODE_BVS:
		client, _ := config.caller.(*decort.BVSDecortClient)
		return client.CloudBroker()
	default:
		return &cloudbroker.CloudBroker{}
	}
}

func (config *ControllerCfg) SDN() *sdn.SDN {
	switch config.auth_mode_code {
	case MODE_LEGACY:
		client, _ := config.caller.(*decort.LegacyDecortClient)
		return client.SDN()
	case MODE_DECS3O:
		client, _ := config.caller.(*decort.DecortClient)
		return client.SDN()
	case MODE_BVS:
		client, _ := config.caller.(*decort.BVSDecortClient)
		return client.SDN()
	default:
		return &sdn.SDN{}
	}
}
