package accessctl

import (
	"dehub/typeobj"
	"errors"
	"fmt"
)

// ErrFilterNoMatch is returned from a FilterInterface's Match method when the
// given request was not matched to the filter due to the request itself (as
// opposed to some error in the filter's definition).
type ErrFilterNoMatch struct {
	Err error
}

func (err ErrFilterNoMatch) Error() string {
	return fmt.Sprintf("matching with filter: %s", err.Err.Error())
}

// FilterInterface describes the methods that all Filters must implement.
type FilterInterface interface {
	// MatchCommit returns nil if the CommitRequest is matched by the filter,
	// otherwise it returns an error (ErrFilterNoMatch if the error is due to
	// the CommitRequest).
	MatchCommit(CommitRequest) error
}

// Filter represents an access control filter being defined in the Config. Only
// one of its fields may be filled at a time.
type Filter struct {
	Signature    *FilterSignature    `type:"signature"`
	Branch       *FilterBranch       `type:"branch"`
	FilesChanged *FilterFilesChanged `type:"files_changed"`
	CommitType   *FilterCommitType   `type:"commit_type"`
	Not          *FilterNot          `type:"not"`
}

// MarshalYAML implements the yaml.Marshaler interface.
func (f Filter) MarshalYAML() (interface{}, error) {
	return typeobj.MarshalYAML(f)
}

// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (f *Filter) UnmarshalYAML(unmarshal func(interface{}) error) error {
	return typeobj.UnmarshalYAML(f, unmarshal)
}

// Interface returns the FilterInterface encapsulated by this Filter.
func (f Filter) Interface() (FilterInterface, error) {
	el, _, err := typeobj.Element(f)
	if err != nil {
		return nil, err
	}
	return el.(FilterInterface), nil
}

// Type returns a string describing what type of Filter this object
// encapsulates, based on which of its fields are filled in.
func (f Filter) Type() (string, error) {
	_, typeStr, err := typeobj.Element(f)
	if err != nil {
		return "", err
	}
	return typeStr, nil
}

// FilterCommitType filters by what type of commit is being requested. Exactly
// one of its fields should be filled.
type FilterCommitType struct {
	Type  string   `yaml:"commit_type"`
	Types []string `yaml:"commit_types"`
}

var _ FilterInterface = FilterCommitType{}

// MatchCommit implements the method for FilterInterface.
func (f FilterCommitType) MatchCommit(req CommitRequest) error {
	switch {
	case f.Type != "":
		if f.Type != req.Type {
			return ErrFilterNoMatch{
				Err: fmt.Errorf("commit type %q does not match filter's type %q",
					req.Type, f.Type),
			}
		}
		return nil

	case len(f.Types) > 0:
		for _, typ := range f.Types {
			if typ == req.Type {
				return nil
			}
		}
		return ErrFilterNoMatch{
			Err: fmt.Errorf("commit type %q does not match any of filter's types %+v",
				req.Type, f.Types),
		}

	default:
		return errors.New(`one of the following fields must be set: "commit_type", "commit_types"`)
	}
}