package auth

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"

	"github.com/fiskerinc/cloud-services/pkg/httpclient"
	"github.com/fiskerinc/cloud-services/pkg/jwt"
	"github.com/fiskerinc/cloud-services/pkg/logger"
	"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
	"github.com/pkg/errors"
)

var cognitoUserConsentClientID = envtool.GetEnv("COGNITO_USER_CONSENT_CLIENT_ID", "REPLACE_ME")
var cognitoUserConsentClientSecret = envtool.GetEnv("COGNITO_USER_CONSENT_CLIENT_SECRET", "REPLACE_ME")
var cognitoUserConsentAuthURL = envtool.GetEnv("COGNITO_USER_CONSENT_AUTH_URL", "REPLACE_ME")

var CognitoUserConsentJWT CognitoJWT

type CognitoJWT struct {
	token      string
	expiration *time.Time
	mu         sync.RWMutex
}

func (c *CognitoJWT) GetUserConsentToken() (string, error) {
	c.mu.Lock()
	defer c.mu.Unlock()

	// check for an unexpired token. if it is there, return it
	if c.token != "" && time.Now().Before(*c.expiration) {
		return c.token, nil
	}

	// make a request to Cognito to get the token
	authResponse, err := RequestUserConsentToken()
	if err != nil {
		return "", err
	}

	// cache the token and expire time
	c.token = authResponse.AccessToken.JWTToken
	c.expiration = &authResponse.ExpireTime

	return c.token, nil
}

func RequestUserConsentToken() (AuthResponse, error) {
	var resp AuthTokens
	var tokens AuthResponse

	tokenReq, err := getUserConsentTokenRequest()
	if err != nil {
		return tokens, err
	}

	tokenRes, err := httpclient.Do(tokenReq)
	if err != nil {
		return tokens, err
	}
	defer tokenRes.Body.Close()

	err = json.NewDecoder(tokenRes.Body).Decode(&resp)
	if err != nil {
		logger.Error().Err(err).Send()
	}

	if len(resp.Error) > 0 {
		return tokens, errors.New(resp.Error)
	}

	return getAuthResponse(&resp), nil
}

// getUserConsentTokenRequest returns http request to exchange code for a user consent token
func getUserConsentTokenRequest() (*http.Request, error) {
	basicAuth := base64.StdEncoding.EncodeToString([]byte(strings.Join([]string{cognitoUserConsentClientID, ":", cognitoUserConsentClientSecret}, "")))

	body := url.Values{
		"client_id":  {cognitoUserConsentClientID},
		"grant_type": {"client_credentials"},
	}

	r, err := http.NewRequest(http.MethodPost, getUserConsentTokenURL(), strings.NewReader(body.Encode()))
	if err != nil {
		return nil, errors.WithStack(err)
	}

	r.Header.Add("Authorization", fmt.Sprintf("Basic %s", basicAuth))
	r.Header.Add("Content-type", "application/x-www-form-urlencoded")

	return r, nil
}

// GetAuthResponse converts AuthTokens response from Cognito to include decoded payload
func getAuthResponse(data *AuthTokens) AuthResponse {
	var result AuthResponse

	result.AccessToken.JWTToken = data.AcessToken
	result.IDToken.JWTToken = data.IDToken
	result.RefreshToken.Token = data.RefreshToken

	// calculate the expire time of the token, leaving off one minute of wiggle room
	result.ExpireTime = time.Now().Add(time.Duration(data.ExpiresIn)*time.Second - time.Minute)

	return result
}

func getUserConsentTokenURL() string {
	return cognitoUserConsentAuthURL + "/oauth2/token"
}

// AuthTokens json response for auth tokens
type AuthTokens struct {
	AcessToken   string `json:"access_token"`
	RefreshToken string `json:"refresh_token,omitempty"`
	IDToken      string `json:"id_token,omitempty"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int64  `json:"expires_in"`
	Error        string `json:"error,omitempty"`
}

// JWTResponse json response
type JWTResponse struct {
	JWTToken string `json:"jwtToken"`
}

// AuthResponse json response
type AuthResponse struct {
	AccessToken  JWTResponse   `json:"accessToken,omitempty"`
	IDToken      JWTResponse   `json:"idToken,omitempty"`
	RefreshToken jwt.AuthToken `json:"refreshToken,omitempty"`
	ExpireTime   time.Time     `json:"expire_time"`
}
