package csprofiles

import (
	"fmt"

	"github.com/expr-lang/expr"
	"github.com/expr-lang/expr/vm"
	log "github.com/sirupsen/logrus"

	"github.com/crowdsecurity/go-cs-lib/cstime"
	"github.com/crowdsecurity/go-cs-lib/ptr"

	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
	"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
	"github.com/crowdsecurity/crowdsec/pkg/models"
	"github.com/crowdsecurity/crowdsec/pkg/types"
)

type Runtime struct {
	RuntimeFilters      []*vm.Program        `json:"-" yaml:"-"`
	RuntimeDurationExpr *vm.Program          `json:"-" yaml:"-"`
	Cfg                 *csconfig.ProfileCfg `json:"-" yaml:"-"`
	Logger              *log.Entry           `json:"-" yaml:"-"`
}

const defaultDuration = "4h"

func NewProfile(profilesCfg []*csconfig.ProfileCfg) ([]*Runtime, error) {
	var err error

	profilesRuntime := make([]*Runtime, 0)

	for _, profile := range profilesCfg {
		var runtimeFilter, runtimeDurationExpr *vm.Program

		runtime := &Runtime{}

		xlog := log.New()
		if err := types.ConfigureLogger(xlog, ptr.Of(log.InfoLevel)); err != nil {
			return nil, fmt.Errorf("while configuring profiles-specific logger: %w", err)
		}

		runtime.Logger = xlog.WithFields(log.Fields{
			"type": "profile",
			"name": profile.Name,
		})

		runtime.RuntimeFilters = make([]*vm.Program, len(profile.Filters))
		runtime.Cfg = profile

		if runtime.Cfg.OnSuccess != "" && runtime.Cfg.OnSuccess != "continue" && runtime.Cfg.OnSuccess != "break" {
			return nil, fmt.Errorf("invalid 'on_success' for '%s': %s", profile.Name, runtime.Cfg.OnSuccess)
		}

		if runtime.Cfg.OnFailure != "" && runtime.Cfg.OnFailure != "continue" && runtime.Cfg.OnFailure != "break" && runtime.Cfg.OnFailure != "apply" {
			return nil, fmt.Errorf("invalid 'on_failure' for '%s' : %s", profile.Name, runtime.Cfg.OnFailure)
		}

		for fIdx, filter := range profile.Filters {
			if runtimeFilter, err = expr.Compile(filter, exprhelpers.GetExprOptions(map[string]interface{}{"Alert": &models.Alert{}})...); err != nil {
				return nil, fmt.Errorf("error compiling filter of '%s': %w", profile.Name, err)
			}

			runtime.RuntimeFilters[fIdx] = runtimeFilter
			if profile.Debug != nil && *profile.Debug {
				runtime.Logger.Logger.SetLevel(log.DebugLevel)
			}
		}

		if profile.DurationExpr != "" {
			if runtimeDurationExpr, err = expr.Compile(profile.DurationExpr, exprhelpers.GetExprOptions(map[string]interface{}{"Alert": &models.Alert{}})...); err != nil {
				return nil, fmt.Errorf("error compiling duration_expr of %s: %w", profile.Name, err)
			}

			runtime.RuntimeDurationExpr = runtimeDurationExpr
		}

		for _, decision := range profile.Decisions {
			if runtime.RuntimeDurationExpr == nil {
				var duration string
				if decision.Duration != nil {
					duration = *decision.Duration
				} else {
					runtime.Logger.Warningf("No duration specified for %s, using default duration %s", profile.Name, defaultDuration)
					duration = defaultDuration
				}

				if _, err := cstime.ParseDurationWithDays(duration); err != nil {
					return nil, fmt.Errorf("error parsing duration '%s' of %s: %w", duration, profile.Name, err)
				}
			}
		}

		profilesRuntime = append(profilesRuntime, runtime)
	}

	return profilesRuntime, nil
}

func (profile *Runtime) GenerateDecisionFromProfile(alert *models.Alert) ([]*models.Decision, error) {
	var decisions []*models.Decision

	for _, refDecision := range profile.Cfg.Decisions {
		decision := models.Decision{}
		/*the reference decision from profile is in simulated mode */
		if refDecision.Simulated != nil && *refDecision.Simulated {
			decision.Simulated = new(bool)
			*decision.Simulated = true
			/*the event is already in simulation mode */
		} else if alert.Simulated != nil && *alert.Simulated {
			decision.Simulated = new(bool)
			*decision.Simulated = true
		}
		/*If the profile specifies a scope, this will prevail.
		If not, we're going to get the scope from the source itself*/
		decision.Scope = new(string)
		if refDecision.Scope != nil && *refDecision.Scope != "" {
			*decision.Scope = *refDecision.Scope
		} else {
			*decision.Scope = *alert.Source.Scope
		}
		/*some fields are populated from the reference object : duration, scope, type*/

		decision.Duration = new(string)
		if refDecision.Duration != nil {
			*decision.Duration = *refDecision.Duration
		}

		if profile.Cfg.DurationExpr != "" && profile.RuntimeDurationExpr != nil {
			profileDebug := false
			if profile.Cfg.Debug != nil && *profile.Cfg.Debug {
				profileDebug = true
			}

			duration, err := exprhelpers.Run(profile.RuntimeDurationExpr, map[string]interface{}{"Alert": alert}, profile.Logger, profileDebug)
			if err != nil {
				profile.Logger.Warningf("Failed to run duration_expr : %v", err)
			} else {
				durationStr := fmt.Sprint(duration)
				if _, err := cstime.ParseDurationWithDays(durationStr); err != nil {
					profile.Logger.Warningf("Failed to parse expr duration result '%s'", duration)
				} else {
					*decision.Duration = durationStr
				}
			}
		}

		decision.Type = new(string)
		*decision.Type = *refDecision.Type

		/*for the others, let's populate it from the alert and its source*/
		decision.Value = new(string)
		*decision.Value = *alert.Source.Value
		decision.Origin = new(string)
		*decision.Origin = types.CrowdSecOrigin

		if refDecision.Origin != nil {
			*decision.Origin = fmt.Sprintf("%s/%s", *decision.Origin, *refDecision.Origin)
		}

		decision.Scenario = new(string)
		*decision.Scenario = *alert.Scenario
		decisions = append(decisions, &decision)
	}

	return decisions, nil
}

// EvaluateProfile is going to evaluate an Alert against a profile to generate Decisions
func (profile *Runtime) EvaluateProfile(alert *models.Alert) ([]*models.Decision, bool, error) {
	var decisions []*models.Decision

	matched := false

	for eIdx, expression := range profile.RuntimeFilters {
		debugProfile := false
		if profile.Cfg.Debug != nil && *profile.Cfg.Debug {
			debugProfile = true
		}

		output, err := exprhelpers.Run(expression, map[string]interface{}{"Alert": alert}, profile.Logger, debugProfile)
		if err != nil {
			profile.Logger.Warningf("failed to run profile expr for %s: %v", profile.Cfg.Name, err)
			return nil, matched, fmt.Errorf("while running expression %s: %w", profile.Cfg.Filters[eIdx], err)
		}

		switch out := output.(type) {
		case bool:
			if out {
				matched = true
				/*the expression matched, create the associated decision*/
				subdecisions, err := profile.GenerateDecisionFromProfile(alert)
				if err != nil {
					return nil, matched, fmt.Errorf("while generating decision from profile %s: %w", profile.Cfg.Name, err)
				}

				decisions = append(decisions, subdecisions...)
			} else {
				profile.Logger.Debugf("Profile %s filter is unsuccessful", profile.Cfg.Name)

				if profile.Cfg.OnFailure == "break" {
					break
				}
			}

		default:
			return nil, matched, fmt.Errorf("unexpected type %t (%v) while running '%s'", output, output, profile.Cfg.Filters[eIdx])
		}
	}

	return decisions, matched, nil
}
