package rule

import (
	"encoding/json"
	"errors"

	"github.com/icinga/icinga-go-library/types"
	"github.com/icinga/icinga-notifications/internal/config/baseconf"
	"github.com/icinga/icinga-notifications/internal/filter"
	"github.com/icinga/icinga-notifications/internal/recipient"
	"github.com/icinga/icinga-notifications/internal/timeperiod"
	"go.uber.org/zap/zapcore"
	"time"
)

type Rule struct {
	baseconf.IncrementalPkDbEntry[int64] `db:",inline"`

	Name             string                 `db:"name"`
	TimePeriod       *timeperiod.TimePeriod `db:"-"`
	TimePeriodID     types.Int              `db:"timeperiod_id"`
	SourceID         int64                  `db:"source_id"`
	ObjectFilter     filter.Filter          `db:"-"`
	ObjectFilterExpr types.String           `db:"object_filter"`
	Escalations      map[int64]*Escalation  `db:"-"`

	// FilterColumns is a set of all filter columns used in the rule's ObjectFilter.
	//
	// This is computed from the ObjectFilter once and can be used by sources to determine which
	// columns they need to provide for the events to be able to evaluate the rule.
	FilterColumns FilterAttrsType `db:"-"`
}

// FilterAttrsType represents a list of filter attributes for a given list of filter conditions.
type FilterAttrsType [][]string

// IncrementalInitAndValidate implements the config.IncrementalConfigurableInitAndValidatable interface.
func (r *Rule) IncrementalInitAndValidate() error {
	if r.ObjectFilterExpr.Valid {
		data := map[string]json.RawMessage{}
		if err := json.Unmarshal([]byte(r.ObjectFilterExpr.String), &data); err != nil {
			return err
		}
		filterBytes, exists := data["ast"]
		if !exists {
			return errors.New("missing 'ast' field in object filter expression")
		}

		f, err := filter.UnmarshalJSON(filterBytes)
		if err != nil {
			return err
		}

		r.ObjectFilter = f
		if f != nil {
			for _, condition := range f.ExtractConditions() {
				if attrs, ok := condition.Attributes().([]string); ok {
					r.FilterColumns = append(r.FilterColumns, attrs)
				}
			}
		}
	}
	return nil
}

// MarshalLogObject implements the zapcore.ObjectMarshaler interface.
func (r *Rule) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
	encoder.AddInt64("id", r.ID)
	encoder.AddString("name", r.Name)
	encoder.AddInt64("source_id", r.SourceID)

	if r.TimePeriodID.Valid && r.TimePeriodID.Int64 != 0 {
		encoder.AddInt64("timeperiod_id", r.TimePeriodID.Int64)
	}

	return nil
}

// Eval evaluates the configured object filter for the provided filterable.
//
// Returns always true if the current rule doesn't have a configured object filter.
func (r *Rule) Eval(filterable filter.Filterable) (bool, error) {
	if r.ObjectFilter == nil {
		return true, nil
	}
	return r.ObjectFilter.Eval(filterable)
}

// ContactChannels stores a set of channel IDs for each set of individual contacts.
type ContactChannels map[*recipient.Contact]map[int64]bool

// LoadFromEscalationRecipients loads recipients channel of the specified escalation to the current map.
// You can provide this method a callback to control whether the channel of a specific contact should
// be loaded, and it will skip those for whom the callback returns false. Pass AlwaysNotifiable for default actions.
func (ch ContactChannels) LoadFromEscalationRecipients(escalation *Escalation, t time.Time, isNotifiable func(recipient.Key) bool) {
	for _, escalationRecipient := range escalation.Recipients {
		ch.LoadRecipientChannel(escalationRecipient, t, isNotifiable)
	}
}

// LoadRecipientChannel loads recipient channel to the current map.
// You can provide this method a callback to control whether the channel of a specific contact should
// be loaded, and it will skip those for whom the callback returns false. Pass AlwaysNotifiable for default actions.
func (ch ContactChannels) LoadRecipientChannel(er *EscalationRecipient, t time.Time, isNotifiable func(recipient.Key) bool) {
	if isNotifiable(er.Key) {
		for _, c := range er.Recipient.GetContactsAt(t) {
			if ch[c] == nil {
				ch[c] = make(map[int64]bool)
			}
			if er.ChannelID.Valid {
				ch[c][er.ChannelID.Int64] = true
			} else {
				ch[c][c.DefaultChannelID] = true
			}
		}
	}
}

// AlwaysNotifiable (checks) whether the given recipient is notifiable and returns always true.
// This function is usually passed as an argument to ContactChannels.LoadFromEscalationRecipients whenever you do
// not want to perform any custom actions.
func AlwaysNotifiable(_ recipient.Key) bool {
	return true
}
