package rbac

import (
	"database/sql"
	"slices"

	adapter "github.com/Blank-Xu/sql-adapter"
	"github.com/casbin/casbin/v2"
	"github.com/casbin/casbin/v2/model"
	"github.com/casbin/casbin/v2/util"
	"tangled.org/core/api/tangled"
)

const (
	TnglSpindle = "tngl_spindle"

	Model = `
[request_definition]
r = sub, dom, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub, r.dom) && r.obj == p.obj && r.act == p.act
`
)

type Enforcer struct {
	e *casbin.SyncedEnforcer
}

func NewEnforcer(path string) (*Enforcer, error) {
	m, err := model.NewModelFromString(Model)
	if err != nil {
		return nil, err
	}

	db, err := sql.Open("sqlite3", path+"?_foreign_keys=1&_journal_mode=WAL&_busy_timeout=5000")
	if err != nil {
		return nil, err
	}

	a, err := adapter.NewAdapter(db, "sqlite3", "acl_2")
	if err != nil {
		return nil, err
	}

	e, err := casbin.NewSyncedEnforcer(m, a)
	if err != nil {
		return nil, err
	}

	if err := seedTangledPolicies(e); err != nil {
		return nil, err
	}

	return &Enforcer{e}, nil
}

func seedTangledPolicies(e *casbin.SyncedEnforcer) error {
	// policies
	_, err := e.AddPoliciesEx([][]string{
		// sub | obj | act
		{"repo:collaborator", TnglSpindle, tangled.RepoAddSecretNSID},
		{"repo:collaborator", TnglSpindle, tangled.RepoRemoveSecretNSID},
		{"repo:collaborator", TnglSpindle, tangled.RepoListSecretsNSID},
		{"repo:collaborator", TnglSpindle, tangled.CiCancelPipelineNSID},
		{"repo:collaborator", TnglSpindle, tangled.CiTriggerPipelineNSID},
	})
	if err != nil {
		return err
	}

	// grouping policies
	e.AddNamedDomainMatchingFunc("g", "keyMatch", util.KeyMatch)
	_, err = e.AddGroupingPoliciesEx([][]string{
		// sub | role | dom
		{"repo:owner", "repo:collaborator", "*"}, // repo owner is collaborator
	})
	return err
}

func (e *Enforcer) EnableAutoSave(autoSave bool) {
	e.e.EnableAutoSave(autoSave)
}

func (e *Enforcer) hasImplicitRoleForUser(name string, role string, domain ...string) (bool, error) {
	roles, err := e.e.GetImplicitRolesForUser(name, domain...)
	if err != nil {
		return false, err
	}
	return slices.Contains(roles, role), nil
}

// setRoleForUser sets single user role for specified domain.
// All existing users with that role will be removed.
func (e *Enforcer) setRoleForUser(name string, role string, domain ...string) error {
	currentUsers, err := e.e.GetUsersForRole(role, domain...)
	if err != nil {
		return err
	}

	for _, oldUser := range currentUsers {
		_, err = e.e.DeleteRoleForUser(oldUser, role, domain...)
		if err != nil {
			return err
		}
	}

	_, err = e.e.AddRoleForUser(name, role, domain...)
	return err
}
