package basicauth

import (
	"encoding/base64"
	"strconv"

	"time"

	"github.com/kataras/iris"
	"github.com/kataras/iris/config"
)

type (
	encodedUser struct {
		HeaderValue string
		Username    string
		logged      bool
		expires     time.Time
	}
	encodedUsers []encodedUser

	basicAuthMiddleware struct {
		config config.BasicAuth
		// these are filled from the config.Users map at the startup
		auth             encodedUsers
		realmHeaderValue string
		expireEnabled    bool // if the config.Expires is a valid date, default disabled
	}
)

//

// New takes one parameter, the config.BasicAuth returns a HandlerFunc
// use: iris.UseFunc(New(...)), iris.Get(...,New(...),...)
func New(c config.BasicAuth) iris.HandlerFunc {
	return NewHandler(c).Serve
}

// NewHandler takes one parameter, the config.BasicAuth returns a Handler
// use: iris.Use(NewHandler(...)), iris.Get(...,iris.HandlerFunc(NewHandler(...)),...)
func NewHandler(c config.BasicAuth) iris.Handler {
	b := &basicAuthMiddleware{config: config.DefaultBasicAuth().MergeSingle(c)}
	b.init()
	return b
}

// Default takes one parameter, the users returns a HandlerFunc
// use: iris.UseFunc(Default(...)), iris.Get(...,Default(...),...)
func Default(users map[string]string) iris.HandlerFunc {
	return DefaultHandler(users).Serve
}

// DefaultHandler takes one parameter, the users returns a Handler
// use: iris.Use(DefaultHandler(...)), iris.Get(...,iris.HandlerFunc(Default(...)),...)
func DefaultHandler(users map[string]string) iris.Handler {
	c := config.DefaultBasicAuth()
	c.Users = users
	return NewHandler(c)
}

//

func (b *basicAuthMiddleware) init() {
	// pass the encoded users from the user's config's Users value
	b.auth = make(encodedUsers, 0, len(b.config.Users))

	for k, v := range b.config.Users {
		fullUser := k + ":" + v
		header := "Basic " + base64.StdEncoding.EncodeToString([]byte(fullUser))
		b.auth = append(b.auth, encodedUser{HeaderValue: header, Username: k, logged: false, expires: config.CookieExpireNever})
	}

	// set the auth realm header's value
	b.realmHeaderValue = "Basic realm=" + strconv.Quote(b.config.Realm)

	if b.config.Expires > 0 {
		b.expireEnabled = true
	}
}

func (b *basicAuthMiddleware) findAuth(headerValue string) (auth *encodedUser, found bool) {
	if len(headerValue) == 0 {
		return
	}

	for _, user := range b.auth {
		if user.HeaderValue == headerValue {
			auth = &user
			found = true
			break
		}
	}

	return
}

func (b *basicAuthMiddleware) askForCredentials(ctx *iris.Context) {
	ctx.SetHeader("WWW-Authenticate", b.realmHeaderValue)
	ctx.SetStatusCode(iris.StatusUnauthorized)
}

// Serve the actual middleware
func (b *basicAuthMiddleware) Serve(ctx *iris.Context) {

	if auth, found := b.findAuth(ctx.RequestHeader("Authorization")); !found {
		/* I spent time for nothing
		if b.banEnabled && auth != nil { // this propably never work

			if auth.tries == b.config.MaxTries {
				auth.bannedTime = time.Now()
				auth.unbanTime = time.Now().Add(b.config.BanDuration) // set the unban time
				auth.tries++                                          // we plus them in order to check if already banned later
				// client is banned send a forbidden status and don't continue
				ctx.SetStatusCode(iris.StatusForbidden)
				return
			} else if auth.tries > b.config.MaxTries { // it's already banned, so check the ban duration with the bannedTime
				if time.Now().After(auth.unbanTime) { // here we unban the client
					auth.tries = 0
					auth.bannedTime = config.CookieExpireNever
					auth.unbanTime = config.CookieExpireNever
					// continue and askCredentials as normal
				} else {
					// client is banned send a forbidden status and don't continue
					ctx.SetStatusCode(iris.StatusForbidden)
					return
				}

			}
		}
		if auth != nil {
			auth.tries++
		}*/

		b.askForCredentials(ctx)
		// don't continue to the next handler
	} else {
		// all ok set the context's value in order to be getable from the next handler
		ctx.Set(b.config.ContextKey, auth.Username)
		if b.expireEnabled {

			if auth.logged == false {
				auth.expires = time.Now().Add(b.config.Expires)
				auth.logged = true
			}

			if time.Now().After(auth.expires) {
				b.askForCredentials(ctx) // ask for authentication again
				return
			}

		}

		//auth.tries = 0
		ctx.Next() // continue
	}

}