package serverconfig

import (
	"fmt"

	"bitbucket.org/nemt/nemt-portal-api/application/applicationservice"
	"bitbucket.org/nemt/nemt-portal-api/application/viewmodel"
	"bitbucket.org/nemt/nemt-portal-api/infra/auth"
	"bitbucket.org/nemt/nemt-portal-api/infra/config"
	"bitbucket.org/nemt/nemt-portal-api/infra/logger"
	"bitbucket.org/nemt/nemt-portal-api/server/router/routeutils"
	"github.com/casbin/casbin"
	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"

	"strings"
)

type (
	// Config defines the config for CasbinAuth middleware.
	Config struct {
		// Skipper defines a function to skip middleware.
		Skipper middleware.Skipper

		// Enforcer CasbinAuth main rule.
		// Required.
		Enforcer *casbin.Enforcer

		Application *config.Config

		Svc    *applicationservice.Service
		Logger *logger.Logger
	}
)

var (
	// DefaultConfig is the default CasbinAuth middleware config.
	DefaultConfig = Config{
		Skipper: middleware.DefaultSkipper,
	}
)

// MiddlewareWithConfig returns a CasbinAuth middleware with config.
// See `Middleware()`.
func MiddlewareWithConfig(cfg *config.Config, svc *applicationservice.Service, log *logger.Logger) echo.MiddlewareFunc {

	config := &DefaultConfig

	config.Enforcer = casbin.NewEnforcer("authorization_model.conf", "authorization_policy.csv")

	config.Svc = svc
	config.Logger = log
	config.Application = cfg

	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			if config.Skipper(c) || config.CheckPermission(c) {
				return next(c)
			}

			return routeutils.HandleHTMLError(c, echo.ErrForbidden)
		}
	}
}

func setAuthorizationMiddleware(e *echo.Echo, log *logger.Logger, cfg *config.Config, svc *applicationservice.Service) {
	e.Use(MiddlewareWithConfig(cfg, svc, log))
}

// CheckPermission checks the user/method/path combination from the request.
// Returns true (permission granted) or false (permission forbidden)
func (a *Config) CheckPermission(c echo.Context) bool {
	user, err := auth.GetUserDetail(c, a.Application)
	if err != nil {
		a.Logger.Warningf("Cannot get user details. %v\n", err)
		user = viewmodel.User{}
	}
	method := c.Request().Method
	path := c.Request().URL.Path

	objectsRole, objectsOrganization, objectsOrganizationType, object := a.policyObjectAttributes(c, user)

	currentUsersOrganization := viewmodel.Organization{}
	if len(user.Organizations) > 0 {
		currentUsersOrganization = user.Organizations[0]
	}

	currentUsersRole := viewmodel.Profile{}
	if len(user.Profiles) > 0 {
		currentUsersRole = user.Profiles[0]
	}

	currentUsersOrganizationType := ""
	if len(user.Profiles) > 0 {
		currentUsersOrganizationType = user.Profiles[0].Organization.Type.Key
	}

	orgRelation := organizationsRelation(currentUsersOrganization, objectsOrganization)
	objRelation := a.objectRelation(object, user)

	// parameters to Enforce must match the request section of the authorization_model.conf
	return a.Enforcer.Enforce(currentUsersRole.Key,
		objectsRole.Key,
		objectsOrganizationType,
		currentUsersOrganizationType,
		orgRelation,
		objRelation,
		path,
		method)

}

// policyObjectAttributes returns all information about the object being accessed for the policy
// in case object exists and returns users information if it is a new object
func (a *Config) policyObjectAttributes(c echo.Context, userDetails viewmodel.User) (viewmodel.Profile, viewmodel.Organization, string, interface{}) {

	var object interface{}

	const userIDParamName = "user_uuid"
	existingUser := strings.Contains(c.Request().URL.Path, "/users/") && c.Param(userIDParamName) != ""
	newUser := strings.Contains(c.Request().URL.Path, "/users/") && c.Param(userIDParamName) == ""

	const organizationIDParamName = "org_uuid"
	existingOrganization := strings.Contains(c.Request().URL.Path, "/organization") && c.Param(organizationIDParamName) != ""
	newOrganization := strings.Contains(c.Request().URL.Path, "/organization") && c.Param(organizationIDParamName) == ""

	fmt.Println("**********")
	fmt.Printf("url %v\n", c.Param(userIDParamName))
	fmt.Printf("user %v\n", userDetails.ID)
	fmt.Printf("existing %v\n", existingUser)
	fmt.Printf("new %v\n", newUser)
	fmt.Println("**********")

	switch {
	case existingUser:
		object, _ = a.Svc.Users.GetByUUID(c.Param(userIDParamName), "")
	case newUser && len(userDetails.Organizations) > 0:
		object = userDetails
	case existingOrganization:
		object, _ = a.Svc.Organization.GetByUUID(c.Param(organizationIDParamName), userDetails)
	case newOrganization:
		object = viewmodel.Organization{}
	}

	objectsRole := viewmodel.Profile{}
	switch obj := object.(type) {
	case viewmodel.User:
		if len(obj.Profiles) > 0 {
			objectsRole = obj.Profiles[0]
		}
	}

	objectsOrganization := viewmodel.Organization{}
	switch obj := object.(type) {
	case viewmodel.User:
		if len(obj.Profiles) > 0 {
			objectsOrganization = obj.Profiles[0].Organization
		}
	case viewmodel.Organization:
		objectsOrganization = obj
	}

	objectsOrganizationType := objectsOrganization.Type.Key

	return objectsRole, objectsOrganization, objectsOrganizationType, object
}

func organizationsRelation(requestOrganization, currentUsersOrganization viewmodel.Organization) string {
	if requestOrganization.UUID == currentUsersOrganization.UUID {
		return "[equal]"
	}

	for _, childOrg := range currentUsersOrganization.ChildOrgs {
		if childOrg.UUID == requestOrganization.UUID {
			return "[equal-or-child]"
		}
	}

	for _, childOrg := range requestOrganization.ChildOrgs {
		if childOrg.UUID == currentUsersOrganization.UUID {
			return "[parent]"
		}
	}

	return "[unrelated]"
}

// organizationGoverningObject returns the role that is the owner of the object that is being accessed
// in case object exists and returns users role if it is a new object
func (a *Config) objectRelation(object interface{}, currentUser viewmodel.User) string {

	switch obj := object.(type) {
	case viewmodel.User:
		if obj.ID == currentUser.ID {
			return "[self]"
		}
	}
	return "[other]"
}
