package userinvite

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"strings"
	"time"
	"unicode"

	"github.com/labstack/echo/v4"
	log "github.com/sirupsen/logrus"

	"github.com/cloudfoundry/stratos/src/jetstream/api"
)

// RefreshToken will refresh the token for the client
func (invite *UserInvite) RefreshToken(cfGUID, clientID, clientSecret string) (*api.UAAResponse, *api.TokenRecord, error) {
	endpoint, err := invite.checkEndpoint(cfGUID)
	if err != nil {
		return nil, nil, err
	}

	uaaRecord, tokenRecord, err := invite.refreshToken(clientID, clientSecret, endpoint)
	if err != nil {
		return nil, nil, err
	}

	if err = invite.portalProxy.SaveEndpointToken(cfGUID, UserInviteUserID, *tokenRecord); err != nil {
		return nil, nil, api.NewHTTPShadowError(
			http.StatusInternalServerError,
			"Unable to save user invite token",
			"Unable to save user invite token: %+v", err,
		)
	}

	return uaaRecord, tokenRecord, nil
}

func (invite *UserInvite) refreshToken(clientID, clientSecret string, endpoint api.CNSIRecord) (*api.UAAResponse, *api.TokenRecord, error) {
	now := time.Now()
	clientSecret = strings.TrimSpace(clientSecret)
	authEndpoint := fmt.Sprintf("%s/oauth/token", endpoint.TokenEndpoint)

	form := url.Values{}
	form.Set("client_id", clientID)
	form.Set("client_secret", clientSecret)
	form.Set("grant_type", "client_credentials")
	// Need to use JWT token_format to work with CF
	form.Set("token_format", "jwt")

	req, err := http.NewRequest("POST", authEndpoint, strings.NewReader(form.Encode()))
	if err != nil {
		msg := "Failed to create request for UAA: %v"
		log.Warnf(msg, err)
		return nil, nil, fmt.Errorf(msg, err)
	}

	client := invite.portalProxy.GetHttpClientForRequest(req, endpoint.SkipSSLValidation, endpoint.CACert)
	req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)

	res, err := client.Do(req)
	if err != nil || res.StatusCode != http.StatusOK {
		log.Warnf("User Invite: Error performing http request - response: %v, error: %v", res, err)

		// Try and get the error details form the WWW-Authenticate hehader
		errMsg := "Error checking UAA Client"
		data := parseAuthHeader(res.Header.Get(wwwAuthHeader))
		if len(data["error_description"]) > 0 {
			errMsg = fmt.Sprintf("Could not check Client: %s", data["error_description"])
		}

		return nil, nil, api.NewHTTPShadowError(
			res.StatusCode,
			errMsg,
			errMsg,
		)
	}

	defer res.Body.Close()

	// Check error code
	if res.StatusCode != http.StatusOK {
		errMessage := "Error validating Client ID and Client Secret"
		authError := &api.UAAErrorResponse{}
		uaaResponse, _ := ioutil.ReadAll(res.Body)
		if err := json.Unmarshal([]byte(uaaResponse), authError); err == nil {
			errMessage = errMessage + " - " + authError.ErrorDescription
		}
		return nil, nil, api.NewHTTPShadowError(
			res.StatusCode,
			errMessage,
			errMessage+" %+v", err,
		)
	}

	var uaaResponse api.UAAResponse
	dec := json.NewDecoder(res.Body)
	if err = dec.Decode(&uaaResponse); err != nil {
		return nil, nil, api.NewHTTPShadowError(
			http.StatusBadRequest,
			"Error parsing response from UAA",
			"Error parsing response from UAA: %+v", err,
		)
	}

	duration := time.Duration(uaaResponse.ExpiresIn) * time.Second
	expiry := now.Add(duration).Unix()
	tokenRecord := &api.TokenRecord{
		RefreshToken: fmt.Sprintf("%s:%s", clientID, clientSecret),
		AuthToken:    uaaResponse.AccessToken,
		TokenExpiry:  expiry,
		AuthType:     UAAClientAuthType,
	}

	return &uaaResponse, tokenRecord, nil
}

func (invite *UserInvite) checkEndpoint(cfGUID string) (api.CNSIRecord, error) {
	// Check that there is an endpoint with the specified ID and that it is a Cloud Foundry endpoint
	endpoint, err := invite.portalProxy.GetCNSIRecord(cfGUID)
	if err != nil {
		// Could find the endpoint
		return api.CNSIRecord{}, api.NewHTTPShadowError(
			http.StatusBadRequest,
			"Can not find enpoint",
			"Can not find enpoint: %s", cfGUID,
		)
	}

	if endpoint.CNSIType != "cf" {
		return api.CNSIRecord{}, api.NewHTTPShadowError(
			http.StatusBadRequest,
			"Not a Cloud Foundry endpoint",
			"Not a Cloud Foundry endpoint: %s", cfGUID,
		)
	}

	return endpoint, nil
}

func parseAuthHeader(v string) map[string]string {
	lastQuote := rune(0)
	comma := rune(',')
	f := func(c rune) bool {
		switch {
		case c == lastQuote:
			lastQuote = rune(0)
			return false
		case lastQuote != rune(0):
			return false
		case unicode.In(c, unicode.Quotation_Mark):
			lastQuote = c
			return false
		default:
			return c == comma
		}
	}
	m := strings.FieldsFunc(v, f)
	nameValues := make(map[string]string)
	for _, nv := range m {
		p := strings.Index(nv, "=")
		if p != -1 {
			name := strings.TrimSpace(nv[:p])
			value := strings.TrimSpace(nv[p+1:])
			value = value[1 : len(value)-1]
			nameValues[name] = value
		}
	}

	return nameValues
}

func (invite *UserInvite) doUAAClientAuthFlow(cnsiRequest *api.CNSIRequest, req *http.Request) (*http.Response, error) {
	log.Debug("doUAAClientAuthFlow")
	authHandler := invite.portalProxy.OAuthHandlerFunc(cnsiRequest, req, invite.refreshUAAClientToken)
	return invite.portalProxy.DoAuthFlowRequest(cnsiRequest, req, authHandler)
}

func (invite *UserInvite) getCNSIUserFromUAAClientToken(cnsiGUID string, cfTokenRecord *api.TokenRecord) (*api.ConnectedUser, bool) {
	log.Debug("getCNSIUserFromUAAClientToken")
	return &api.ConnectedUser{}, true
}

func (invite *UserInvite) refreshUAAClientToken(skipSSLValidation bool, cnsiGUID, userGUID, clientID, clientSecret, tokenEndpoint string) (t api.TokenRecord, err error) {
	log.Debug("refreshUAAClientToken")
	refreshedToken := &api.TokenRecord{}

	// See if we can get a token for the invite user
	token, ok := invite.portalProxy.GetCNSITokenRecord(cnsiGUID, userGUID)
	if !ok {
		// Not configured
		return *refreshedToken, errors.New("Can not find existing token")
	}

	client := strings.Split(token.RefreshToken, ":")
	if len(client) != 2 {
		return *refreshedToken, errors.New("Invalid token - expecting client ID and client secret")
	}

	_, refreshedToken, err = invite.RefreshToken(cnsiGUID, client[0], client[1])
	return *refreshedToken, err
}
