package authn51


import (
    "errors"
    "log"
	"os"
	"fmt"
	"time"
	"context"
	"net/http"
	"strings"

	"gopkg.in/yaml.v2"
	"github.com/dgrijalva/jwt-go"
	"github.com/google/uuid"
	"github.com/labstack/echo/v4"
	"github.com/getkin/kin-openapi/openapi3filter"	
)


type Authn51 struct {
	Environment string
	Providers []string
	AuthnFunc openapi3filter.AuthenticationFunc
	Bearer struct {
		Secret string
		DefaultUser string
	}
	Basic struct {
		DefaultUser string
		Password string
	}
	Apikey struct {
		ApiValue string
	}
}


type Configuration struct {
	Environment string `yaml:"environment"`
	Authn51 struct {
		Bearer struct {
			Secret string `yaml:"secret"`
			DefaultUser string `yaml:"default_user"`
		}  `yaml:"bearer"`
		Basic struct {
			DefaultUser string `yaml:"default_user"`
			Password string `yaml:"password"`
		} `yaml:"basic"`
		Apikey struct {
			ApiValue string `yaml:"api_value"`
		} `yaml:"api_key"`
	} `yaml:"authn51"`
} 


type AuthenticationToken struct {
	Environment string `yaml:"environment"`
	Token string `yaml:"token"`
    Claims struct {
		UserId string `yaml:"user_id"`
        Exp string `yaml:"exp"`
    } `yaml:"claims"`
}

func NewAuthn51(configPath string) (*Authn51, error) {

	var authn51 Authn51
	var config Configuration

    file, err := os.Open(configPath)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    d := yaml.NewDecoder(file)

    if err := d.Decode(&config); err != nil {
        return nil, err
    }

	authn51.Bearer.DefaultUser = config.Authn51.Bearer.DefaultUser
	authn51.Bearer.Secret = config.Authn51.Bearer.Secret
	authn51.AuthnFunc = authn51.AuthenticationFunc
	if (authn51.Bearer.Secret == "%ACCESS_SECRET%") {
		authn51.Bearer.Secret = os.Getenv("%ACCESS_SECRET%")
	}
	if (authn51.Bearer.Secret == "") {
		log.Printf("No secret supplied. Generating random value")
		authn51.Bearer.Secret = uuid.New().String()
	}

	return &authn51, nil
}


func (self *Authn51) CreateToken(userId string) (AuthenticationToken, error) {
	var err error
	var authToken AuthenticationToken

	exp := time.Now().Add(time.Minute * 20)

	atClaims := jwt.MapClaims{}
	atClaims["authorized"] = true
	atClaims["organization"] = "any"
	atClaims["user_id"] = userId
	atClaims["exp"] = exp.Unix()

	at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
	token, err := at.SignedString([]byte(self.Bearer.Secret))
	if err != nil {
	   return authToken, err
	}

	authToken.Token = token
	authToken.Claims.UserId = userId
	authToken.Claims.Exp = exp.String()

	return authToken, nil
}



func (self *Authn51) extractToken(r *http.Request) string {
	bearToken := r.Header.Get("Authorization")
	strArr := strings.Split(bearToken, " ")
	if len(strArr) == 2 {
	   return strArr[1]
	}
	return ""
}

func (self *Authn51) verifyToken(r *http.Request) (*jwt.Token, error) {
	tokenString := self.extractToken(r)
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		//Make sure that the token method conform to "SigningMethodHMAC"
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return []byte(self.Bearer.Secret), nil
	})
	if err != nil {
		return nil, err
	}
	return token, nil
}

func (self *Authn51) tokenValid(r *http.Request) error {
	token, err := self.verifyToken(r)
	if err != nil {
	   return err
	}
	if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid {
	   return err
	}
	return nil
}

type AccessDetails struct {
    Organization string
    UserId   string
}

func (self *Authn51) ExtractTokenMetadata(r *http.Request) (*AccessDetails, error) {
	token, err := self.verifyToken(r)
	if err != nil {
	   return nil, err
	}
	claims, ok := token.Claims.(jwt.MapClaims)
	if ok && token.Valid {
	   	organization, ok := claims["organization"].(string)
		if !ok {
			return nil, fmt.Errorf("Extract error with missing '%s'", "organization")
		}
	   	userId, ok := claims["user_id"].(string)
	   	if !ok {
			return nil, fmt.Errorf("Extract error with missing '%s'", "user_id")
	   	}
	   	return &AccessDetails{
		  	Organization: organization,
		  	UserId:   userId,
	   	}, nil
	}
	return nil, fmt.Errorf("Token extract error: %s", "unknown")
}


func (self *Authn51) AuthenticationFunc(ctx context.Context, ai *openapi3filter.AuthenticationInput) error {

	schemeN := ai.SecuritySchemeName
	if (schemeN == "") {
		log.Print("Scheme name is blank")
	}

	req := ai.RequestValidationInput.Request

	if (ai.SecurityScheme.Scheme == "basic") {
		userName, password, ok := req.BasicAuth()
		if (ok){
			if (userName == "joe" && password ==  "secret") {
				return nil
			}
		}
		
		err := errors.New("Basic authentication failed!")
		msg := fmt.Errorf("security requirement '%q' failed", schemeN)
		return &echo.HTTPError{
			Code:     http.StatusUnauthorized,
			Message:  msg,
			Internal: err,
		}

	}

	if (ai.SecurityScheme.Scheme == "bearer") {

		ad, err := self.ExtractTokenMetadata(req)
		if (err != nil || ad == nil) {

			log.Print("Bearer authentication failed extraction!")
			err := errors.New("Bearer authentication failed!")			
			msg := fmt.Errorf("security requirement '%q' failed", schemeN)
			return &echo.HTTPError{
				Code:     http.StatusUnauthorized,
				Message:  msg,
				Internal: err,
			}
				
		}
		
		if (ad.UserId != "") {
			return nil
		}

		log.Print("Bearer authentication failed user id!")
		err = errors.New("Bearer authentication failed!")		
		msg := fmt.Errorf("security requirement %q failed", schemeN)
		return &echo.HTTPError{
			Code:     http.StatusUnauthorized,
			Message:  msg,
			Internal: err,
		}

	}

	if (ai.SecurityScheme.Type == "apiKey") {
		auth := req.Header.Get("X-ApiKey")
		if auth == "secret" {
			return nil
		}
			
		err := errors.New("API key authentication failed!")		
		msg := fmt.Errorf("security requirement '%q' failed", schemeN)
		return &echo.HTTPError{
			Code:     http.StatusUnauthorized,
			Message:  msg,
			Internal: err,
		}

	}

	log.Printf("security requirement '%q' failed", schemeN)
	err := errors.New("Authentication failed!")
	msg := fmt.Errorf("security requirement '%q' failed", schemeN)
	return &echo.HTTPError{
		Code:     http.StatusUnauthorized,
		Message:  msg,
		Internal: err,
	}

}

