package config

import (
	"strings"

	"github.com/casbin/casbin/v2"
	gormadapter "github.com/casbin/gorm-adapter/v3"
	"github.com/gofiber/fiber/v2"
)

// Config holds the configuration for the middleware
type CasbinAuthConfig struct {
	// ModelFilePath is path to model file for Casbin.
	// Optional. Default: "./model.conf".
	Enforcer *casbin.Enforcer

	// PolicyAdapter is an interface for different persistent providers.
	// Optional. Default: fileadapter.NewAdapter("./policy.csv").
	PolicyAdapter *gormadapter.Adapter

	// Lookup is a function that is used to look up current subject.
	// An empty string is considered as unauthenticated user.
	// Optional. Default: func(c *fiber.Ctx) string { return "" }
	Lookup func(*fiber.Ctx) string

	// Unauthorized defines the response body for unauthorized responses.
	// Optional. Default: func(c *fiber.Ctx) error { return c.SendStatus(401) }
	Unauthorized fiber.Handler

	// Forbidden defines the response body for forbidden responses.
	// Optional. Default: func(c *fiber.Ctx) error { return c.SendStatus(403) }
	Forbidden fiber.Handler
}

// Casbin ...
type Casbin struct {
	config   CasbinAuthConfig
	enforcer *casbin.Enforcer
}

// New creates an authorization middleware for use in Fiber
func CasbinAuth(config ...CasbinAuthConfig) *Casbin {

	var cfg CasbinAuthConfig
	if len(config) > 0 {
		cfg = config[0]
	}

	if cfg.Lookup == nil {
		cfg.Lookup = func(c *fiber.Ctx) string { return "" }
	}

	if cfg.Unauthorized == nil {
		cfg.Unauthorized = func(c *fiber.Ctx) error {
			return c.SendStatus(fiber.StatusUnauthorized)
		}
	}

	if cfg.Forbidden == nil {
		cfg.Forbidden = func(c *fiber.Ctx) error {
			return c.SendStatus(fiber.StatusForbidden)
		}
	}

	return &Casbin{
		config:   cfg,
		enforcer: cfg.Enforcer,
	}
}

type validationRule int

const (
	matchAll validationRule = iota
	atLeastOne
)

// MatchAll is an option that defines all permissions
// or roles should match the user.
var MatchAll = func(o *Options) {
	o.ValidationRule = matchAll
}

// AtLeastOne is an option that defines at least on of
// permissions or roles should match to pass.
var AtLeastOne = func(o *Options) {
	o.ValidationRule = atLeastOne
}

// PermissionParserFunc is used for parsing the permission
// to extract object and action usually
type PermissionParserFunc func(str string) []string

func permissionParserWithSeperator(sep string) PermissionParserFunc {
	return func(str string) []string {
		return strings.Split(str, sep)
	}
}

// PermissionParserWithSeperator is an option that parses permission
// with seperators
func PermissionParserWithSeperator(sep string) func(o *Options) {
	return func(o *Options) {
		o.PermissionParser = permissionParserWithSeperator(sep)
	}
}

// Options holds options of middleware
type Options struct {
	ValidationRule   validationRule
	PermissionParser PermissionParserFunc
}

// RequiresPermissions tries to find the current subject and determine if the
// subject has the required permissions according to predefined Casbin policies.
func (cm *Casbin) RequiresPermissions(permissions []string, opts ...func(o *Options)) fiber.Handler {

	options := &Options{
		ValidationRule:   matchAll,
		PermissionParser: permissionParserWithSeperator(":"),
	}

	for _, o := range opts {
		o(options)
	}

	return func(c *fiber.Ctx) error {
		if len(permissions) == 0 {
			return c.Next()
		}

		sub := cm.config.Lookup(c)
		if sub == "" {
			return cm.config.Unauthorized(c)
		}

		if options.ValidationRule == matchAll {
			for _, permission := range permissions {
				vals := append([]string{sub}, options.PermissionParser(permission)...)
				if ok, err := cm.enforcer.Enforce(convertToInterface(vals)...); err != nil {
					return c.SendStatus(fiber.StatusInternalServerError)
				} else if !ok {
					return cm.config.Forbidden(c)
				}
			}
			return c.Next()
		} else if options.ValidationRule == atLeastOne {
			for _, permission := range permissions {
				vals := append([]string{sub}, options.PermissionParser(permission)...)
				if ok, err := cm.enforcer.Enforce(convertToInterface(vals)...); err != nil {
					return c.SendStatus(fiber.StatusInternalServerError)
				} else if ok {
					return c.Next()
				}
			}
			return cm.config.Forbidden(c)
		}

		return c.Next()
	}
}

// RequiresPermissions tries to find the current subject and determine if the
// subject has the required permissions according to predefined Casbin policies.
func (cm *Casbin) Can(sub string, perm string, opts ...func(o *Options)) bool {
	permissions := []string{perm}
	options := &Options{
		ValidationRule:   matchAll,
		PermissionParser: permissionParserWithSeperator(":"),
	}

	for _, o := range opts {
		o(options)
	}
	if len(permissions) == 0 {
		return false
	}

	if sub == "" {
		return false
	}

	if options.ValidationRule == matchAll {
		for _, permission := range permissions {
			vals := append([]string{sub}, options.PermissionParser(permission)...)
			if ok, err := cm.enforcer.Enforce(convertToInterface(vals)...); err != nil {
				return false
			} else if !ok {
				return false
			}
		}
		return true
	} else if options.ValidationRule == atLeastOne {
		for _, permission := range permissions {
			vals := append([]string{sub}, options.PermissionParser(permission)...)
			if ok, err := cm.enforcer.Enforce(convertToInterface(vals)...); err != nil {
				return false
			} else if ok {
				return true
			}
		}
		return false
	}
	return false
}

// RoutePermission tries to find the current subject and determine if the
// subject has the required permissions according to predefined Casbin policies.
// This method uses http Path and Method as object and action.
func (cm *Casbin) RoutePermission() fiber.Handler {
	return func(c *fiber.Ctx) error {
		sub := cm.config.Lookup(c)
		if sub == "" {
			return cm.config.Unauthorized(c)
		}

		if ok, err := cm.enforcer.Enforce(sub, c.Path(), c.Method()); err != nil {
			return c.SendStatus(fiber.StatusInternalServerError)
		} else if !ok {
			return cm.config.Forbidden(c)
		}

		return c.Next()
	}
}

// RequiresRoles tries to find the current subject and determine if the
// subject has the required roles according to predefined Casbin policies.
func (cm *Casbin) RequiresRoles(roles []string, opts ...func(o *Options)) fiber.Handler {
	options := &Options{
		ValidationRule:   matchAll,
		PermissionParser: permissionParserWithSeperator(":"),
	}

	for _, o := range opts {
		o(options)
	}

	return func(c *fiber.Ctx) error {
		if len(roles) == 0 {
			return c.Next()
		}

		sub := cm.config.Lookup(c)
		if sub == "" {
			return cm.config.Unauthorized(c)
		}

		userRoles, err := cm.enforcer.GetRolesForUser(sub)
		if err != nil {
			return c.SendStatus(fiber.StatusInternalServerError)
		}
		if options.ValidationRule == matchAll {
			for _, role := range roles {
				if !contains(userRoles, role) {
					return cm.config.Forbidden(c)
				}
			}
			return c.Next()
		} else if options.ValidationRule == atLeastOne {
			for _, role := range roles {
				if contains(userRoles, role) {
					return c.Next()
				}
			}
			return cm.config.Forbidden(c)
		}

		return c.Next()
	}
}

func contains(s []string, v string) bool {
	for _, vv := range s {
		if vv == v {
			return true
		}
	}
	return false
}

func convertToInterface(arr []string) []interface{} {
	in := make([]interface{}, 0)
	for _, a := range arr {
		in = append(in, a)
	}
	return in
}
