// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package constraint

import (
	"fmt"
	"maps"
	"path"
	"slices"

	"github.com/expr-lang/expr"
	"gopkg.in/yaml.v3"

	"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/metadata"
	yaml_base_types "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/types/base"
	"go.woodpecker-ci.org/woodpecker/v3/shared/optional"
)

const (
	statusFailure = "failure"
	statusSuccess = "success"
)

type (
	// When defines a set of runtime constraints.
	When struct {
		// If true then read from a list of constraint
		Constraints []Constraint
	}

	Constraint struct {
		Ref      List                          `yaml:"ref,omitempty"`
		Repo     List                          `yaml:"repo,omitempty"`
		Instance List                          `yaml:"instance,omitempty"`
		Platform List                          `yaml:"platform,omitempty"`
		Branch   List                          `yaml:"branch,omitempty"`
		Cron     List                          `yaml:"cron,omitempty"`
		Status   yaml_base_types.StringOrSlice `yaml:"status,omitempty"`
		Matrix   Map                           `yaml:"matrix,omitempty"`
		Local    optional.Option[bool]         `yaml:"local,omitempty"`
		Path     Path                          `yaml:"path,omitempty"`
		Evaluate string                        `yaml:"evaluate,omitempty"`
		Event    yaml_base_types.StringOrSlice `yaml:"event,omitempty"`
	}
)

func (when *When) IsEmpty() bool {
	return len(when.Constraints) == 0
}

// Returns true if at least one of the internal constraints is true.
func (when *When) Match(metadata metadata.Metadata, global bool, env map[string]string) (bool, error) {
	for _, c := range when.Constraints {
		match, err := c.Match(metadata, global, env)
		if err != nil {
			return false, err
		}
		if match {
			return true, nil
		}
	}

	if when.IsEmpty() {
		// test against default Constraints
		empty := &Constraint{}
		return empty.Match(metadata, global, env)
	}
	return false, nil
}

func (when *When) IncludesStatusFailure(metadata metadata.Metadata, global bool, env map[string]string) bool {
	if when.IsEmpty() {
		return false
	}
	for _, c := range when.Constraints {
		if matches, err := c.Match(metadata, global, env); err == nil && matches {
			if slices.Contains(c.Status, statusFailure) {
				return true
			}
		}
	}

	return false
}

func (when *When) IncludesStatusSuccess(metadata metadata.Metadata, global bool, env map[string]string) bool {
	// "success" acts differently than "failure" in that it's
	// presumed to be included unless it's specifically not part
	// of the list
	if when.IsEmpty() {
		return true
	}
	for _, c := range when.Constraints {
		if matches, err := c.Match(metadata, global, env); err == nil && matches {
			if len(c.Status) == 0 || slices.Contains(c.Status, statusSuccess) {
				return true
			}
		}
	}
	return false
}

// False if (any) non local.
func (when *When) IsLocal() bool {
	for _, c := range when.Constraints {
		if !c.Local.ValueOrDefault(true) {
			return false
		}
	}
	return true
}

func (when *When) UnmarshalYAML(value *yaml.Node) error {
	switch value.Kind {
	case yaml.SequenceNode:
		if err := value.Decode(&when.Constraints); err != nil {
			return err
		}

	case yaml.MappingNode:
		c := Constraint{}
		if err := value.Decode(&c); err != nil {
			return err
		}
		when.Constraints = append(when.Constraints, c)

	default:
		return fmt.Errorf("not supported yaml kind: %v", value.Kind)
	}

	return nil
}

// MarshalYAML implements custom Yaml marshaling.
func (when When) MarshalYAML() (any, error) {
	// clean up local if true make it none as we will default to true
	for i := range when.Constraints {
		if when.Constraints[i].Local.ValueOrDefault(true) {
			when.Constraints[i].Local = optional.None[bool]()
		}
	}

	switch len(when.Constraints) {
	case 0:
		return nil, nil
	case 1:
		return when.Constraints[0], nil
	default:
		return when.Constraints, nil
	}
}

// Match returns true if all constraints match the given input. If a single
// constraint fails a false value is returned.
func (c *Constraint) Match(m metadata.Metadata, global bool, env map[string]string) (bool, error) {
	match := true
	if !global {
		// apply step only filters
		match = c.Matrix.Match(m.Workflow.Matrix)
	}

	match = match && c.Platform.Match(m.Sys.Platform) &&
		(len(c.Event) == 0 || slices.Contains(c.Event, string(m.Curr.Event))) &&
		c.Repo.Match(path.Join(m.Repo.Owner, m.Repo.Name)) &&
		c.Ref.Match(m.Curr.Commit.Ref) &&
		c.Instance.Match(m.Sys.Host)

	// changed files filter apply only for pull-request and push events
	if m.Curr.Event.IsPull() || m.Curr.Event == metadata.EventPush {
		match = match && c.Path.Match(m.Curr.Commit.ChangedFiles, m.Curr.Commit.Message)
	}

	if m.Curr.Event != metadata.EventTag {
		match = match && c.Branch.Match(m.Curr.Commit.Branch)
	}

	if m.Curr.Event == metadata.EventCron {
		match = match && c.Cron.Match(m.Curr.Cron)
	}

	if c.Evaluate != "" {
		if env == nil {
			env = m.Environ()
		} else {
			maps.Copy(env, m.Environ())
		}
		out, err := expr.Compile(c.Evaluate, expr.Env(env), expr.AllowUndefinedVariables(), expr.AsBool())
		if err != nil {
			return false, err
		}
		result, err := expr.Run(out, env)
		if err != nil {
			return false, err
		}
		bResult, ok := result.(bool)
		if !ok {
			return false, fmt.Errorf("could not parse result: %v", result)
		}
		match = match && bResult
	}

	return match, nil
}
