package perms

import (
	"encoding/json"
	"errors"
	"fmt"
	"sort"
	"strings"

	"github.com/hashicorp/boundary/internal/types/action"
	"github.com/hashicorp/boundary/internal/types/resource"
	"github.com/hashicorp/boundary/internal/types/scope"
)

// GrantPair is simply a struct that can be reference from other code to return
// a set of scopes and grants to parse
type GrantPair struct {
	ScopeId string
	Grant   string
}

// Scope provides an in-memory representation of iam.Scope without the
// underlying storage references or capabilities.
type Scope struct {
	// Id is the public id of the iam.Scope
	Id string

	// Type is the scope's type (org or project)
	Type scope.Type
}

// Grant is a Go representation of a parsed grant
type Grant struct {
	// The scope ID, which will be a project ID or an org ID
	scope Scope

	// The ID in the grant, if provided.
	id string

	// The type, if provided
	typ resource.Type

	// The set of actions being granted
	actions map[action.Type]bool

	// This is used as a temporary staging area before validating permissions to
	// allow the same validation code across grant string formats
	actionsBeingParsed []string
}

func (g Grant) Id() string {
	return g.id
}

func (g Grant) Type() resource.Type {
	return g.typ
}

func (g Grant) Actions() (typs []action.Type, strs []string) {
	typs = make([]action.Type, 0, len(g.actions))
	strs = make([]string, 0, len(g.actions))
	for k, v := range g.actions {
		// Nothing should be in there if not true, but doesn't hurt to validate
		if !v {
			continue
		}
		typs = append(typs, k)
		strs = append(strs, k.String())
	}
	return
}

func (g Grant) clone() *Grant {
	ret := &Grant{
		scope: g.scope,
		id:    g.id,
		typ:   g.typ,
	}
	if g.actionsBeingParsed != nil {
		ret.actionsBeingParsed = append(ret.actionsBeingParsed, g.actionsBeingParsed...)
	}
	if g.actions != nil {
		ret.actions = make(map[action.Type]bool, len(g.actions))
		for action := range g.actions {
			ret.actions[action] = true
		}
	}
	return ret
}

// CanonicalString returns the canonical representation of the grant
func (g Grant) CanonicalString() string {
	var builder []string

	if g.id != "" {
		builder = append(builder, fmt.Sprintf("id=%s", g.id))
	}

	if g.typ != resource.Unknown {
		builder = append(builder, fmt.Sprintf("type=%s", g.typ.String()))
	}

	if len(g.actions) > 0 {
		actions := make([]string, 0, len(g.actions))
		for action := range g.actions {
			actions = append(actions, action.String())
		}
		sort.Strings(actions)
		builder = append(builder, fmt.Sprintf("actions=%s", strings.Join(actions, ",")))
	}

	return strings.Join(builder, ";")
}

// MarshalJSON provides a custom marshaller for grants
func (g Grant) MarshalJSON() ([]byte, error) {
	res := make(map[string]interface{}, 4)
	if g.id != "" {
		res["id"] = g.id
	}
	if g.typ != resource.Unknown {
		res["type"] = g.typ.String()
	}
	if len(g.actions) > 0 {
		actions := make([]string, 0, len(g.actions))
		for action := range g.actions {
			actions = append(actions, action.String())
		}
		sort.Strings(actions)
		res["actions"] = actions
	}
	return json.Marshal(res)
}

// This is purposefully unexported since the values being set here are not being
// checked for validity. This should only be called by the main parsing function
// when JSON is detected.
func (g *Grant) unmarshalJSON(data []byte) error {
	raw := make(map[string]interface{}, 4)
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}
	if rawId, ok := raw["id"]; ok {
		id, ok := rawId.(string)
		if !ok {
			return fmt.Errorf("unable to interpret %q as string", "id")
		}
		g.id = strings.ToLower(id)
	}
	if rawType, ok := raw["type"]; ok {
		typ, ok := rawType.(string)
		if !ok {
			return fmt.Errorf("unable to interpret %q as string", "type")
		}
		g.typ = resource.Map[typ]
		if g.typ == resource.Unknown {
			return fmt.Errorf("unknown type specifier %q", typ)
		}
	}
	if rawActions, ok := raw["actions"]; ok {
		interfaceActions, ok := rawActions.([]interface{})
		if !ok {
			return fmt.Errorf("unable to interpret %q as array", "actions")
		}
		if len(interfaceActions) > 0 {
			g.actionsBeingParsed = make([]string, 0, len(interfaceActions))
			for _, v := range interfaceActions {
				actionStr, ok := v.(string)
				switch {
				case !ok:
					return fmt.Errorf("unable to interpret %v in actions array as string", v)
				case actionStr == "":
					return errors.New("empty action found")
				default:
					g.actionsBeingParsed = append(g.actionsBeingParsed, strings.ToLower(actionStr))
				}
			}
		}
	}
	return nil
}

func (g *Grant) unmarshalText(grantString string) error {
	segments := strings.Split(grantString, ";")
	for _, segment := range segments {
		kv := strings.Split(segment, "=")

		// Ensure we don't accept "foo=bar=baz", "=foo", or "foo="
		switch {
		case len(kv) != 2:
			return fmt.Errorf("segment %q not formatted correctly, wrong number of equal signs", segment)
		case len(kv[0]) == 0:
			return fmt.Errorf("segment %q not formatted correctly, missing key", segment)
		case len(kv[1]) == 0:
			return fmt.Errorf("segment %q not formatted correctly, missing value", segment)
		}

		switch kv[0] {
		case "id":
			g.id = strings.ToLower(kv[1])

		case "type":
			typeString := strings.ToLower(kv[1])
			g.typ = resource.Map[typeString]
			if g.typ == resource.Unknown {
				return fmt.Errorf("unknown type specifier %q", typeString)
			}

		case "actions":
			actions := strings.Split(kv[1], ",")
			if len(actions) > 0 {
				g.actionsBeingParsed = make([]string, 0, len(actions))
				for _, action := range actions {
					if action == "" {
						return errors.New("empty action found")
					}
					g.actionsBeingParsed = append(g.actionsBeingParsed, strings.ToLower(action))
				}
			}
		}
	}

	return nil
}

// Parse parses a grant string. Note that this does not do checking
// of the validity of IDs and such; that's left for other parts of the system.
// We may not check at all (e.g. let it be an authz-time failure) or could check
// after submission to catch errors.
//
// The scope must be the org and project where this grant originated, not the
// request.
func Parse(scopeId, userId, grantString string, skipFinalValidation bool) (Grant, error) {
	if len(grantString) == 0 {
		return Grant{}, errors.New("grant string is empty")
	}

	if scopeId == "" {
		return Grant{}, errors.New("no scope ID provided")
	}

	grant := Grant{
		scope: Scope{Id: scopeId},
	}

	switch {
	case scopeId == "global":
		grant.scope.Type = scope.Global
	case strings.HasPrefix(scopeId, scope.Org.Prefix()):
		grant.scope.Type = scope.Org
	case strings.HasPrefix(scopeId, scope.Project.Prefix()):
		grant.scope.Type = scope.Project
	default:
		return Grant{}, errors.New("invalid scope type")
	}

	switch {
	case grantString[0] == '{':
		if err := grant.unmarshalJSON([]byte(grantString)); err != nil {
			return Grant{}, fmt.Errorf("unable to parse JSON grant string: %w", err)
		}

	default:
		if err := grant.unmarshalText(grantString); err != nil {
			return Grant{}, fmt.Errorf("unable to parse grant string: %w", err)
		}
	}

	// Check for templated user ID, and subtitute in with the authenticated user
	// if so
	if grant.id != "" && userId != "" && strings.HasPrefix(grant.id, "{{") {
		id := strings.TrimSuffix(strings.TrimPrefix(grant.id, "{{"), "}}")
		id = strings.ToLower(strings.TrimSpace(id))
		switch id {
		case "user.id":
			grant.id = userId
		default:
			return Grant{}, fmt.Errorf("unknown template %q in grant %q value", grant.id, "id")
		}
	}

	if err := grant.validateType(); err != nil {
		return Grant{}, err
	}

	if err := grant.parseAndValidateActions(); err != nil {
		return Grant{}, err
	}

	if !skipFinalValidation {
		// Validate the grant. Create a dummy resource and pass it through
		// Allowed and ensure that we get allowed.
		acl := NewACL(grant)
		r := Resource{
			ScopeId: scopeId,
			Id:      grant.id,
			Type:    grant.typ,
		}
		if !topLevelType(grant.typ) {
			r.Pin = grant.id
		}
		var allowed bool
		for k := range grant.actions {
			results := acl.Allowed(r, k)
			if results.Allowed {
				allowed = true
			}
		}
		if !allowed {
			return Grant{}, errors.New("parsed grant string would not result in any action being authorized")
		}
	}

	return grant, nil
}

func (g Grant) validateType() error {
	switch g.typ {
	case resource.Unknown,
		resource.All,
		resource.Scope,
		resource.User,
		resource.Group,
		resource.Role,
		resource.AuthMethod,
		resource.Account,
		resource.HostCatalog,
		resource.HostSet,
		resource.Host,
		resource.Target,
		resource.Session:
		return nil
	}
	return fmt.Errorf("unknown type specifier %q", g.typ)
}

func (g *Grant) parseAndValidateActions() error {
	if len(g.actionsBeingParsed) == 0 {
		return errors.New("no actions specified")
	}

	for _, a := range g.actionsBeingParsed {
		if a == "" {
			return errors.New("empty action found")
		}
		if g.actions == nil {
			g.actions = make(map[action.Type]bool, len(g.actionsBeingParsed))
		}
		if am := action.Map[a]; am == action.Unknown {
			return fmt.Errorf("unknown action %q", a)
		} else {
			g.actions[am] = true
		}
	}

	if len(g.actions) > 1 && g.actions[action.All] {
		return fmt.Errorf("%q cannot be specified with other actions", action.All.String())
	}

	g.actionsBeingParsed = nil

	return nil
}
