package rules

import (
	"fmt"
	"sync"
	"time"

	"iwarma.ru/console/correlator/es"
	"iwarma.ru/console/correlator/events"
	"iwarma.ru/console/correlator/stat"

	log "github.com/sirupsen/logrus"

	"github.com/olivere/elastic/v7"
)

// StoreItem store rule and additional info for it
type StoreItem struct {
	Enabled bool
	Rule
}

// RuleStore is a store for rules
// It's is a correlator
type RuleStore struct {
	rules      []*StoreItem
	rulesMutex sync.Mutex
	stat       *Stat
	client     *es.Elastic
	cl         *log.Entry
	bulk       *elastic.BulkService
}

// NewRuleStore Create new rule store
func NewRuleStore(client *es.Elastic) *RuleStore {
	result := new(RuleStore)
	result.rules = make([]*StoreItem, 0)
	result.client = client
	result.cl = log.WithField("part", "RuleStore")
	result.bulk = client.NewBulkRequest().Refresh("wait_for")
	result.stat = NewStat()
	return result
}

//ClearStore clear store's rules and statistics
func (store *RuleStore) ClearStore() {
	store.rulesMutex.Lock()
	defer store.rulesMutex.Unlock()

	store.rules = make([]*StoreItem, 0)
	store.stat = NewStat()
	store.cl.Debug("Store cleared")
}

//AddRules add array of rules to store
func (store *RuleStore) AddRules(rules ...Rule) {
	store.rulesMutex.Lock()
	defer store.rulesMutex.Unlock()

	var found = false

	for _, rule := range rules {
		for index, cur := range store.rules {
			if cur.Id == rule.Id {
				store.rules[index] = &StoreItem{
					Enabled: true,
					Rule:    rule,
				}
				store.cl.Debugf("Rule %v updated", rule.Id)
				found = true
				break
			}
		}

		if !found {
			store.rules = append(store.rules, &StoreItem{
				Enabled: true,
				Rule:    rule,
			})
			store.stat.AverageRuleTime[rule.Id] = new(stat.AvgTime)
			store.cl.Debugf("New rule %v added", rule.Id)
		}
	}

	store.stat.RuleCount = uint64(len(store.rules))
	store.cl.Debugf("Total rules count: %v", store.stat.RuleCount)
}

//RunRulesSync run all rules in store one by one
func (store *RuleStore) RunRulesSync() error {
	store.rulesMutex.Lock()
	defer store.rulesMutex.Unlock()

	disableRule := func(curRule *StoreItem, err error) {
		curRule.Enabled = false
		store.stat.Errors[curRule.Id] = err.Error()
		store.cl.Infof("Rule %v [%v] was disabled because of an error: %v", curRule.Name, curRule.Id, err)
	}

	iterStart := time.Now()
	for _, rule := range store.rules {
		cl := store.cl.WithFields(log.Fields{"rule_id": rule.Id, "rule_name": rule.Name})

		if !rule.Enabled {
			cl.Debug("Rule is disabled")
			continue
		}

		ruleStart := time.Now()
		resultEvents, err := rule.Do(store.client)
		if err != nil {
			cl.Debugf("Rule exec error: %v", err)

			// Need to check error. If it is 404 from aggregated index - show error and do nothing
			// If it's not 404? we need to disable rule
			if e, ok := err.(*elastic.Error); ok {
				cl.Debugf("Elastic error: %v", err)

				if e.Status == 404 {
					err = store.client.CheckAndCreateIndex(events.GetAggregatedIndexName())
					if err != nil {
						cl.Errorf("Can't create index: %v", err)
						disableRule(rule, fmt.Errorf("can't create index: %v", err))
					}
				} else {
					disableRule(rule, fmt.Errorf("elastic error: %v", err))
				}
			} else {
				disableRule(rule, fmt.Errorf("rule execute error: %v", err))
				cl.Errorf("Can't Do rule: %v", err)
				return err
			}
		}
		if resultEvents != nil {
			cl.Debugf("Got %v events match", len(*resultEvents))
			store.stat.IncidentCount++
			store.stat.EventsCount += uint64(len(*resultEvents))

			for _, event := range *resultEvents {

				// Check for index in event
				if event.GetString("index") == "" {
					cl.Errorf("Event with id %v dont't have an index", event.GetString(events.EventID))
					disableRule(rule, fmt.Errorf("go event %v without index", event.GetString(events.EventID)))

					// Don't stop update cycle, we can find
					// more events without index
					continue
				}

				// Prepare tags
				tags := event.AddTag(rule.Id)
				updateRequest := elastic.NewBulkUpdateRequest().
					Doc(struct {
						Tags []string `json:"rule_tags"`
					}{Tags: tags}).
					Id(event.GetString(events.AggregatedId)).Index(event.GetString("index"))
				store.bulk.Add(updateRequest)
			}
		}
		store.stat.AverageRuleTime[rule.Id].Add(time.Since(ruleStart))
		cl.Debug("Done executing rule")
		if store.bulk.NumberOfActions() > 0 {
			store.cl.Debug("Start bulk update")
			response, err := store.client.ExecuteBulk(store.bulk)
			store.cl.Debug("End bulk update")
			if err != nil {
				store.cl.Errorf("Got error from bulk update: %v", err)
				disableRule(rule, fmt.Errorf("cant' update rule %v with elastic error: %v", rule.Name, err))
			}

			store.cl.Debugf("Updated count: %v", len(response.Updated()))
			store.cl.Debugf("Failed count: %v", len(response.Failed()))

			failed := response.Failed()
			if len(failed) > 0 {
				disableRule(rule, fmt.Errorf("can't update rule %v with error %v (Id %v Index %v)", rule.Name, failed[0].Error.Reason, failed[0].Id, failed[0].Index))
			}
		} else {
			store.cl.Debug("No bulk actions")
		}

	}

	store.stat.AverageIterationTime.Add(time.Since(iterStart))

	return nil
}

func (store *RuleStore) GetStat() *Stat {
	store.rulesMutex.Lock()
	defer store.rulesMutex.Unlock()

	return store.stat
}
