package auth

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/spf13/viper"
	"net/http"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"opendev.org/airship/armada-go/pkg/log"
)

var Log = func(format string, a ...interface{}) {
	log.Printf(format, a...)
}

// Cache provides the interface for cache implementations.
type Cache interface {
	//Set stores a value with the given ttl
	Set(key string, value interface{}, ttl time.Duration)
	//Get retrieves a value previously stored in the cache.
	//value has to be a pointer to a data structure that matches the type previously given to Set
	//The return value indicates if a value was found
	Get(key string, value interface{}) bool
}

// Auth is the entrypoint for creating the middleware
type Auth struct {
	//Keystone v3 endpoint url for validating tokens ( e.g https://some.where:5000/v3)
	Endpoint string
	//User-Agent used for all http request by the middleware. Defaults to go-keystone-middleware/1.0
	UserAgent string
	//A cache implementation the middleware should use for caching tokens. By default, no caching is performed.
	TokenCache Cache
	//How long to cache tokens. Defaults to 5 minutes.
	CacheTime time.Duration

	//http client to use for requests, default to  &http.Client{ Timeout: 5 * time.Second }
	Client *http.Client
}

// New returns a new Auth object initialized with default values
func New(endpoint string) *Auth {
	auth := &Auth{Endpoint: endpoint}
	auth.ensureDefaults()
	return auth
}

// Handler returns a http handler for use in a middleware chain.
func (a *Auth) Handler(h http.Handler) gin.HandlerFunc {
	a.ensureDefaults()
	return gin.WrapH(&handler{Auth: a, handler: h})
}

// Validate a token.
// This is useful if you don't want to use the http middleware
func (a *Auth) Validate(authToken string) (*Token, error) {
	if a.TokenCache != nil {
		var cachedToken Token
		if ok := a.TokenCache.Get(authToken, &cachedToken); ok && cachedToken.Valid() {
			Log("Found valid token in cache")
			return &cachedToken, nil
		}
	}

	req, err := http.NewRequest("GET", a.Endpoint+"/auth/tokens?nocatalog", nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("X-Auth-Token", authToken)
	req.Header.Set("X-Subject-Token", authToken)
	req.Header.Set("User-Agent", a.UserAgent)

	r, err := a.Client.Do(req)
	if err != nil {
		return nil, err
	}
	defer r.Body.Close()

	if r.StatusCode >= 400 {
		return nil, errors.New(r.Status)
	}

	var resp authResponse
	if err = json.NewDecoder(r.Body).Decode(&resp); err != nil {
		return nil, err
	}

	if e := resp.Error; e != nil {
		return nil, fmt.Errorf("%s : %s", r.Status, e.Message)
	}
	if r.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("%s", r.Status)
	}
	if resp.Token == nil {
		return nil, errors.New("response didn't contain token context")
	}
	if !resp.Token.Valid() {
		return nil, errors.New("returned token is not valid")
	}

	if a.TokenCache != nil {
		ttl := a.CacheTime
		//The expiry date of the token provides an upper bound on the cache time
		if expiresIn := resp.Token.ExpiresAt.Sub(time.Now()); expiresIn < a.CacheTime {
			ttl = expiresIn
		}
		a.TokenCache.Set(authToken, *resp.Token, ttl)
	}

	return resp.Token, nil
}

func (a *Auth) ensureDefaults() {

	if a.UserAgent == "" {
		a.UserAgent = "go-keystone-middleware/1.0"
	}

	if a.CacheTime == 0 {
		a.CacheTime = 5 * time.Minute
	}

	if a.Client == nil {
		a.Client = &http.Client{
			Timeout: 5 * time.Second,
		}
	}

}

type handler struct {
	*Auth
	handler http.Handler
}

func (h *handler) ServeHTTP(_ http.ResponseWriter, req *http.Request) {
	filterIncomingHeaders(req)
	req.Header.Set("X-Identity-Status", "Invalid")
	authToken := req.Header.Get("X-Auth-Token")
	if authToken == "" {
		return
	}

	context, err := h.Auth.Validate(authToken)
	if err != nil {
		Log("Failed to validate token: %v", err)
		return
	}

	req.Header.Set("X-Identity-Status", "Confirmed")
	for k, v := range context.headers() {
		req.Header.Set(k, v)
	}
	Log("Auth OK Request validated")
}

// Domain holds information about the scope of a token
type Domain struct {
	ID      string
	Name    string
	Enabled bool
}

// Project contains information about the scope of a token
type Project struct {
	ID      string
	Name    string
	Enabled bool
	Domain  Domain
}

// Token describes the scope of a validated token
type Token struct {
	ExpiresAt time.Time `json:"expires_at"`
	IssuedAt  time.Time `json:"issued_at"`
	User      struct {
		ID      string
		Name    string
		Email   string
		Enabled bool
		Domain  struct {
			ID   string
			Name string
		}
	}
	Project *Project
	Domain  *Domain
	Roles   []struct {
		ID   string
		Name string
	}
}

// Valid returns if the token is valid based on the expiration and issue date
func (t Token) Valid() bool {
	now := time.Now().Unix()
	return t.IssuedAt.Unix() <= now && now < t.ExpiresAt.Unix()
}

type authResponse struct {
	Error *struct {
		Code    int
		Message string
		Title   string
	}
	Token *Token
}

func (t Token) headers() map[string]string {
	headers := make(map[string]string)
	headers["X-User-Id"] = t.User.ID
	headers["X-User-Name"] = t.User.Name
	headers["X-User-Domain-Id"] = t.User.Domain.ID
	headers["X-User-Domain-Name"] = t.User.Domain.Name

	if project := t.Project; project != nil {
		headers["X-Project-Name"] = project.Name
		headers["X-Project-Id"] = project.ID
		headers["X-Project-Domain-Name"] = project.Domain.Name
		headers["X-Project-Domain-Id"] = project.Domain.ID

	}

	if domain := t.Domain; domain != nil {
		headers["X-Domain-Id"] = domain.ID
		headers["X-Domain-Name"] = domain.Name
	}

	if roles := t.Roles; roles != nil {
		roleNames := []string{}
		for _, role := range t.Roles {
			roleNames = append(roleNames, role.Name)
		}
		headers["X-Roles"] = strings.Join(roleNames, ",")

	}

	return headers
}

func filterIncomingHeaders(req *http.Request) {
	req.Header.Del("X-Identity-Status")
	req.Header.Del("X-Service-Identity-Status")

	req.Header.Del("X-Domain-Id")
	req.Header.Del("X-Service-Domain-Id")

	req.Header.Del("X-Domain-Name")
	req.Header.Del("X-Service-Domain-Name")

	req.Header.Del("X-Project-Id")
	req.Header.Del("X-Service-Project-Id")

	req.Header.Del("X-Project-Name")
	req.Header.Del("X-Service-Project-Name")

	req.Header.Del("X-Project-Domain-Id")
	req.Header.Del("X-Service-Project-Domain-Id")

	req.Header.Del("X-Project-Domain-Name")
	req.Header.Del("X-Service-Project-Domain-Name")

	req.Header.Del("X-User-Id")
	req.Header.Del("X-Service-User-Id")

	req.Header.Del("X-User-Name")
	req.Header.Del("X-Service-User-Name")

	req.Header.Del("X-User-Domain-Id")
	req.Header.Del("X-Service-User-Domain-Id")

	req.Header.Del("X-User-Domain-Name")
	req.Header.Del("X-Service-User-Domain-Name")

	req.Header.Del("X-Roles")
	req.Header.Del("X-Service-Roles")

	req.Header.Del("X-Service-Catalog")

	//deprecated Headers
	req.Header.Del("X-Tenant-Id")
	req.Header.Del("X-Tenant")
	req.Header.Del("X-User")
	req.Header.Del("X-Role")
}

func Authenticate() (string, error) {
	authUrl := viper.Sub("keystone_authtoken").GetString("auth_url")
	username := viper.Sub("keystone_authtoken").GetString("username")
	password := viper.Sub("keystone_authtoken").GetString("password")
	projectDomainName := viper.Sub("keystone_authtoken").GetString("project_domain_name")
	projectName := viper.Sub("keystone_authtoken").GetString("project_name")
	userDomainName := viper.Sub("keystone_authtoken").GetString("user_domain_name")

	jsonData := []byte(fmt.Sprintf(`{
		"auth": {
			"identity": {
			  "methods": ["password"],
			  "password": {
				"user": {
				  "name": "%s",
				  "domain": { "id": "%s" },
				  "password": "%s"
				}
			  }
			},
			"scope": {
			  "project": {
				"name": "%s",
				"domain": { "id": "%s" }
			  }
			}
		  }
	}`, username, userDomainName, password, projectName, projectDomainName))

	req, err := http.NewRequest("POST", authUrl+"/auth/tokens", bytes.NewBuffer(jsonData))
	if err != nil {
		return "", err
	}
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode != 201 {
		return "", errors.New("http: not authorized")
	}

	token := resp.Header.Get("X-Subject-Token")
	if token == "" {
		return "", errors.New("http: keystone token is empty")
	}

	return token, nil
}