package auth

import (
	"encoding/json"
	"net/http"

	"github.com/labstack/echo/v4"
	"github.com/lib/pq"
	null "gopkg.in/volatiletech/null.v6"
)

var ErrPermDenied = echo.NewHTTPError(http.StatusForbidden, "permission denied")

// PermType indicates a generic permission type which is either get (read) or manage (write).
type PermType uint8

const (
	PermTypeGet PermType = 1 << iota
	PermTypeManage
)

const (
	// UserHTTPCtxKey is the key on which the User profile is set on echo handlers.
	UserHTTPCtxKey = "auth_user"
	SessionKey     = "auth_session"
)

const (
	// SuperAdminRoleID is the database ID of the primordial super admin role.
	SuperAdminRoleID = 1

	// User.
	UserTypeUser       = "user"
	UserTypeAPI        = "api"
	UserStatusEnabled  = "enabled"
	UserStatusDisabled = "disabled"

	// Role.
	RoleTypeUser = "user"
	RoleTypeList = "list"
)

// List of all granular permissions.
const (
	PermListGetAll            = "lists:get_all"
	PermListManageAll         = "lists:manage_all"
	PermListManage            = "list:manage"
	PermListGet               = "list:get"
	PermSubscribersGet        = "subscribers:get"
	PermSubscribersGetAll     = "subscribers:get_all"
	PermSubscribersManage     = "subscribers:manage"
	PermSubscribersImport     = "subscribers:import"
	PermSubscribersSqlQuery   = "subscribers:sql_query"
	PermTxSend                = "tx:send"
	PermCampaignsGet          = "campaigns:get"
	PermCampaignsGetAll       = "campaigns:get_all"
	PermCampaignsGetAnalytics = "campaigns:get_analytics"
	PermCampaignsManage       = "campaigns:manage"
	PermCampaignsManageAll    = "campaigns:manage_all"
	PermCampaignsSend         = "campaigns:send"
	PermBouncesGet            = "bounces:get"
	PermBouncesManage         = "bounces:manage"
	PermWebhooksPostBounce    = "webhooks:post_bounce"
	PermMediaGet              = "media:get"
	PermMediaManage           = "media:manage"
	PermTemplatesGet          = "templates:get"
	PermTemplatesManage       = "templates:manage"
	PermUsersGet              = "users:get"
	PermUsersManage           = "users:manage"
	PermRolesGet              = "roles:get"
	PermRolesManage           = "roles:manage"
	PermSettingsGet           = "settings:get"
	PermSettingsManage        = "settings:manage"
	PermSettingsMaintain      = "settings:maintain"
)

// Base holds common fields shared across models.
type Base struct {
	ID        int       `db:"id" json:"id"`
	CreatedAt null.Time `db:"created_at" json:"created_at"`
	UpdatedAt null.Time `db:"updated_at" json:"updated_at"`
}

// User represents an admin user.
type User struct {
	Base

	Username string `db:"username" json:"username"`

	// For API users, this stores the SHA-256 token hash.
	Password null.String `db:"password" json:"password,omitempty"`

	PasswordLogin bool             `db:"password_login" json:"password_login"`
	Email         null.String      `db:"email" json:"email"`
	Name          string           `db:"name" json:"name"`
	Type          string           `db:"type" json:"type"`
	Status        string           `db:"status" json:"status"`
	Avatar        null.String      `db:"avatar" json:"avatar"`
	TwofaType     string           `db:"twofa_type" json:"twofa_type"`
	TwofaKey      null.String      `db:"twofa_key" json:"-"`
	LoggedInAt    null.Time        `db:"loggedin_at" json:"loggedin_at"`
	UserRoleID    int              `db:"user_role_id" json:"user_role_id,omitempty"`
	UserRoleName  string           `db:"user_role_name" json:"-"`
	ListRoleID    *int             `db:"list_role_id" json:"list_role_id,omitempty"`
	ListRoleName  null.String      `db:"list_role_name" json:"-"`
	UserRolePerms pq.StringArray   `db:"user_role_permissions" json:"-"`
	ListsPermsRaw *json.RawMessage `db:"list_role_perms" json:"-"`

	// Non-DB fields filled post-retrieval.
	UserRole struct {
		ID          int      `db:"-" json:"id"`
		Name        string   `db:"-" json:"name"`
		Permissions []string `db:"-" json:"permissions"`
	} `db:"-" json:"user_role"`

	ListRole           *ListRolePermissions        `db:"-" json:"list_role"`
	PermissionsMap     map[string]struct{}         `db:"-" json:"-"`
	ListPermissionsMap map[int]map[string]struct{} `db:"-" json:"-"`
	GetListIDs         []int                       `db:"-" json:"-"`
	ManageListIDs      []int                       `db:"-" json:"-"`
	HasPassword        bool                        `db:"-" json:"-"`
}

type ListPermission struct {
	ID          int            `json:"id"`
	Name        string         `json:"name"`
	Permissions pq.StringArray `json:"permissions"`
}

type ListRolePermissions struct {
	ID    int              `db:"-" json:"id"`
	Name  string           `db:"-" json:"name"`
	Lists []ListPermission `db:"-" json:"lists"`
}

type Role struct {
	Base

	Type        string         `db:"type" json:"type"`
	Name        null.String    `db:"name" json:"name"`
	Permissions pq.StringArray `db:"permissions" json:"permissions"`

	ListID   null.Int         `db:"list_id" json:"-"`
	ParentID null.Int         `db:"parent_id" json:"-"`
	ListsRaw json.RawMessage  `db:"list_permissions" json:"-"`
	Lists    []ListPermission `db:"-" json:"lists"`
}

type ListRole struct {
	Base

	Name null.String `db:"name" json:"name"`

	ListID   null.Int         `db:"list_id" json:"-"`
	ParentID null.Int         `db:"parent_id" json:"-"`
	ListsRaw json.RawMessage  `db:"list_permissions" json:"-"`
	Lists    []ListPermission `db:"-" json:"lists"`
}

// HasPerm checks if the user has a specific permission.
func (u *User) HasPerm(perm string) bool {
	// Short-circuit if the user is the primordial super admin.
	if u.UserRoleID == SuperAdminRoleID {
		return true
	}

	_, ok := u.PermissionsMap[perm]
	return ok
}

// HasListPerm checks if the user has get or manage access to the given list.
// perm is either PermGet or PermManage.
func (u *User) HasListPerm(types PermType, listIDs ...int) error {
	var permAll, perm string

	if types == 0 {
		return ErrPermDenied
	}

	if types&PermTypeGet != 0 {
		permAll = PermListGetAll
		perm = PermListGet
	} else if types&PermTypeManage != 0 {
		permAll = PermListManageAll
		perm = PermListManage
	}

	// Check if the user has permissions for all lists or the specific list.
	if u.HasPerm(permAll) {
		return nil
	}

	for _, id := range listIDs {
		if id > 0 {
			if !u.hasListPerm(perm, id) {
				return ErrPermDenied
			}
		}
	}

	return nil
}

func (u *User) hasListPerm(perm string, listID int) bool {
	// Short-circuit if the user is the primordial super admin.
	if u.UserRoleID == SuperAdminRoleID {
		return true
	}

	if _, ok := u.ListPermissionsMap[listID]; !ok {
		return false
	}

	_, ok := u.ListPermissionsMap[listID][perm]
	return ok
}

// GetPermittedLists returns a list of IDs the user has access to based on
// the given get / manage permissions. If the user has the blanket "*_all"
// permission (or the user is a super admin), then the bool is set to true and
// the list is nil as all lists are permitted.
func (u *User) GetPermittedLists(types PermType) (bool, []int) {
	if types == 0 {
		return false, nil
	}

	// Short-circuit if the user is the primordial super admin.
	if u.UserRoleID == SuperAdminRoleID {
		return true, nil
	}

	var (
		get    = types&PermTypeGet != 0
		manage = types&PermTypeManage != 0
	)

	// If the user has the list:get_all or list:manage_all permission, no
	// further checks are required.
	if get {
		if _, ok := u.PermissionsMap[PermListGetAll]; ok {
			return true, nil
		}
	}
	if manage {
		if _, ok := u.PermissionsMap[PermListManageAll]; ok {
			return true, nil
		}
	}

	if get {
		// If the user has per-list permissions, return that. Otherwise, let the
		// 'manage' permission check run.
		if len(u.GetListIDs) > 0 {
			out := make([]int, len(u.GetListIDs))
			copy(out, u.GetListIDs)
			return false, out
		}
	}

	if manage {
		// User has per-list permissions.
		out := make([]int, len(u.ManageListIDs))
		copy(out, u.ManageListIDs)
		return false, out
	}

	return false, nil
}

// FilterListsByPerm returns list IDs filtered by either of the given perms.
func (u *User) FilterListsByPerm(types PermType, listIDs []int) []int {
	if types == 0 {
		return nil
	}

	var (
		get    = types&PermTypeGet != 0
		manage = types&PermTypeManage != 0
	)

	// If the user has full list management permission,
	// no further checks are required.
	if get {
		if _, ok := u.PermissionsMap[PermListGetAll]; ok {
			return listIDs
		}
	}
	if manage {
		if _, ok := u.PermissionsMap[PermListManageAll]; ok {
			return listIDs
		}
	}

	out := make([]int, 0, len(listIDs))
	for _, id := range listIDs {
		// Check if it exists in the map.
		if l, ok := u.ListPermissionsMap[id]; ok {
			// Check if any of the given permission exists for it.
			if get {
				if _, ok := l[PermListGet]; ok {
					out = append(out, id)
				}
			} else if manage {
				if _, ok := l[PermListManage]; ok {
					out = append(out, id)
				}
			}
		}
	}

	return out
}

// GetPermittedListIDs filters the given list IDs by the user's get/manage
// permissions and returns the filtered set. Unlike `FilterListsByPerm()`, if no
// IDs are present (empty input or 0 permitted lists), it falls back to the user's
// permitted list IDs if any. This is useful for endpoints which accept a few IDs
// or the lack of which implies "all".
func (u *User) GetPermittedListIDs(listIDs []int) []int {
	listIDs = u.FilterListsByPerm(PermTypeGet|PermTypeManage, listIDs)
	if len(listIDs) == 0 {
		if _, ok := u.PermissionsMap[PermSubscribersGetAll]; !ok {
			if len(u.GetListIDs) > 0 {
				return u.GetListIDs
			}
			return []int{-1}
		}
	}
	return listIDs
}
