package auth

import (
	"crypto/rsa"
	"fmt"
	"io/ioutil"
	"time"

	"github.com/pquerna/ffjson/ffjson"

	"strings"

	"bitbucket.org/nemt/nemt-portal-api/application/viewmodel"
	"bitbucket.org/nemt/nemt-portal-api/infra/config"
	"bitbucket.org/nemt/nemt-portal-api/infra/errors"
	jwt "github.com/dgrijalva/jwt-go"
	"github.com/labstack/echo"
)

const (
	// AppKeyHeaderName is the header name for application keys
	AppKeyHeaderName = "App"
	// AppTokenHeaderName is the header name for application token
	AppTokenHeaderName = "Token"
	// TokenExpiration is the auth token default expiration time
	TokenExpiration = 24 * time.Hour
)

var (
	// TokenSigningMethod is the auth token signing algorithm
	TokenSigningMethod = jwt.SigningMethodRS256
)

// GetAppKeyFromContext returns the application key from request header
func GetAppKeyFromContext(ctx echo.Context) (appKey string, err error) {
	appKey = ctx.Request().Header.Get(AppKeyHeaderName)

	if strings.TrimSpace(appKey) == "" {
		return "", errors.NewNotAuthorizedError("Application key not found")
	}

	return appKey, nil
}

// ValidateAppKey validates the presence and validity of App key
func ValidateAppKey(ctx echo.Context, cfg *config.Config) (err error) {
	appKey, err := GetAppKeyFromContext(ctx)
	if err != nil {
		return errors.Wrap(err)
	}

	if appKey != cfg.HTTP.Auth.AppKey {
		return errors.NewNotAuthorizedError("Invalid API Key")
	}

	return nil
}

// GetCertPrivateKey returns the private key for the token authentication certificate
func GetCertPrivateKey(path string) (pk *rsa.PrivateKey, err error) {
	data, err := ioutil.ReadFile(path)
	if err != nil {
		return nil, errors.Wrap(err)
	}

	pk, err = jwt.ParseRSAPrivateKeyFromPEM(data)
	if err != nil {
		return nil, errors.Wrap(err)
	}

	return pk, nil
}

// GetCertPublicKey returns the public key for the token authentication certificate
func GetCertPublicKey(path string) (pk *rsa.PublicKey, err error) {
	privateKey, err := GetCertPrivateKey(path)
	if err != nil {
		return nil, errors.Wrap(err)
	}

	return &privateKey.PublicKey, nil
}

// GenerateToken creates a token based on certificate
func GenerateToken(cfg *config.Config, data interface{}) (tokenString string, err error) {
	key, _ := GetCertPrivateKey(cfg.HTTP.Auth.CertificatePath)

	claims := jwt.MapClaims{
		"iat":  time.Now().Unix(),
		"exp":  time.Now().Add(TokenExpiration).Unix(),
		"iss":  "BDC",
		"sub":  "NEMT",
		"data": data,
	}

	token := jwt.NewWithClaims(TokenSigningMethod, claims)
	tokenString, err = token.SignedString(key)
	if err != nil {
		return tokenString, errors.Wrap(err)
	}

	return tokenString, nil
}

func GetTokenDetail(ctx echo.Context, cfg *config.Config) (interface{}, error) {
	key, _ := GetCertPublicKey(cfg.HTTP.Auth.CertificatePath)

	tokenString := ctx.Request().Header.Get("Token")
	tokenString = strings.Replace(tokenString, "Bearer ", "", -1)

	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}
		return key, nil
	})

	if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
		return claims["data"], nil
	} else {
		return nil, err
	}
}

func GetUserDetail(ctx echo.Context, cfg *config.Config) (viewmodel.User, error) {
	key, _ := GetCertPublicKey(cfg.HTTP.Auth.CertificatePath)

	tokenString := ctx.Request().Header.Get("Token")
	tokenString = strings.Replace(tokenString, "Bearer ", "", -1)

	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}
		return key, nil
	})
	if err != nil {
		return viewmodel.User{}, err
	}

	if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
		byteData, _ := ffjson.Marshal(claims["data"])
		user := viewmodel.User{}
		err = ffjson.Unmarshal(byteData, &user)
		if err != nil {
			return viewmodel.User{}, err
		} else {
			return user, nil
		}
	} else {
		return viewmodel.User{}, err
	}
}
