/*
Package idlealarm implements alarm modification on idle alarm.
*/
package idlealarm

//go:generate go tool go.uber.org/mock/mockgen -destination=../../../mocks/lib/canopsis/idlealarm/service.go git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis/idlealarm Service

import (
	"context"
	"fmt"
	"strings"

	"git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis"
	libalarm "git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis/alarm"
	"git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis/datetime"
	"git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis/encoding"
	"git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis/engine"
	libentity "git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis/entity"
	libevent "git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis/event"
	"git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis/idlerule"
	"git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis/rpc"
	"git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis/types"
	"github.com/rs/zerolog"
)

// Service interface is used to implement alarms modification by idle rules.
type Service interface {
	Process(ctx context.Context) ([]types.Event, error)
}

// NewService creates new service.
func NewService(
	ruleAdapter idlerule.RuleAdapter,
	alarmAdapter libalarm.Adapter,
	entityAdapter libentity.Adapter,
	pbhRpcClient engine.RPCClient,
	eventGenerator libevent.Generator,
	encoder encoding.Encoder,
	logger zerolog.Logger,
) Service {
	return &baseService{
		ruleAdapter:    ruleAdapter,
		alarmAdapter:   alarmAdapter,
		entityAdapter:  entityAdapter,
		pbhRpcClient:   pbhRpcClient,
		eventGenerator: eventGenerator,
		encoder:        encoder,
		logger:         logger,
	}
}

type baseService struct {
	ruleAdapter    idlerule.RuleAdapter
	alarmAdapter   libalarm.Adapter
	entityAdapter  libentity.Adapter
	pbhRpcClient   engine.RPCClient
	eventGenerator libevent.Generator
	encoder        encoding.Encoder
	logger         zerolog.Logger
}

func (s *baseService) Process(ctx context.Context) (res []types.Event, resErr error) {
	now := datetime.NewCpsTime()
	rules, err := s.ruleAdapter.GetEnabled(ctx)
	if err != nil {
		return nil, fmt.Errorf("cannot fetch idle rules: %w", err)
	}
	ids := make([]string, len(rules))
	for i, rule := range rules {
		ids[i] = rule.ID
	}
	s.logger.Debug().Strs("rules", ids).Msg("load idle rules")

	var allMinDuration, entityMinDuration datetime.DurationWithUnit
	for _, rule := range rules {
		if allMinDuration.Value == 0 || allMinDuration.AddTo(now).After(rule.Duration.AddTo(now)) {
			allMinDuration = rule.Duration
		}

		switch rule.Type {
		case idlerule.RuleTypeAlarm:
			/*do nothing*/
		case idlerule.RuleTypeEntity:
			if entityMinDuration.Value == 0 || entityMinDuration.AddTo(now).After(rule.Duration.AddTo(now)) {
				entityMinDuration = rule.Duration
			}
		default:
			return nil, fmt.Errorf("unknown type of idle rule: %q for id=%s", rule.Type, rule.ID)
		}
	}

	events := make([]types.Event, 0)
	checkedEntities := make([]string, 0)

	if allMinDuration.Value > 0 {
		before := allMinDuration.SubFrom(now)
		cursor, err := s.alarmAdapter.GetOpenedAlarmsWithLastDatesBefore(ctx, before)
		if err != nil {
			return events, fmt.Errorf("cannot fetch alarms: %w", err)
		}

		defer cursor.Close(ctx)

		for cursor.Next(ctx) {
			alarm := types.AlarmWithEntity{}
			err := cursor.Decode(&alarm)
			if err != nil {
				return events, fmt.Errorf("cannot decode alarm %w", err)
			}

			event, err := s.applyRules(ctx, rules, alarm.Entity, &alarm.Alarm, now)
			if err != nil {
				return events, err
			}

			if event != nil {
				events = append(events, *event)
			}

			checkedEntities = append(checkedEntities, alarm.Entity.ID)
		}
	}

	if entityMinDuration.Value > 0 {
		before := entityMinDuration.SubFrom(now)
		cursor, err := s.entityAdapter.GetAllWithLastUpdateDateBefore(ctx, before, checkedEntities)
		if err != nil {
			return events, fmt.Errorf("cannot fetch entities: %w", err)
		}

		defer cursor.Close(ctx)

		for cursor.Next(ctx) {
			entity := types.Entity{}
			err := cursor.Decode(&entity)
			if err != nil {
				return events, fmt.Errorf("cannot decode entity : %w", err)
			}

			event, err := s.applyRules(ctx, rules, entity, nil, now)
			if err != nil {
				return events, err
			}

			if event != nil {
				events = append(events, *event)
			}
		}
	}

	closeEvents, err := s.closeConnectorAlarms(ctx)
	if err != nil {
		return events, err
	}

	events = append(events, closeEvents...)

	return events, nil
}

// applyRules applies rules on entity.
// Alarm rules are skipped if alarm arg is nil.
// Last entity alarm is fetched for entity rules if alarm arg is nil.
func (s *baseService) applyRules(
	ctx context.Context,
	rules []idlerule.Rule,
	entity types.Entity,
	alarm *types.Alarm,
	now datetime.CpsTime,
) (*types.Event, error) {
	lastAlarm := alarm

	for _, rule := range rules {
		switch rule.Type {
		case idlerule.RuleTypeAlarm:
			if alarm == nil {
				continue
			}

			matched, err := rule.Matches(types.AlarmWithEntity{
				Alarm:  *alarm,
				Entity: entity,
			}, now)
			if err != nil {
				s.logger.Error().Err(err).Str("idle_rule", rule.ID).Msg("match idle rule returned error, skip")
				continue
			}

			if matched && !alarm.Value.PbehaviorInfo.OneOf(rule.DisableDuringPeriods) {
				return s.applyAlarmRule(ctx, rule, *alarm, entity, now)
			}
		case idlerule.RuleTypeEntity:
			matched, err := rule.Matches(types.AlarmWithEntity{Entity: entity}, now)
			if err != nil {
				s.logger.Error().Err(err).Str("idle_rule", rule.ID).Msg("match idle rule returned error, skip")
				continue
			}

			if !matched || entity.PbehaviorInfo.OneOf(rule.DisableDuringPeriods) {
				continue
			}

			if lastAlarm == nil {
				var err error
				lastAlarm, err = s.alarmAdapter.GetLastAlarmByEntityID(ctx, entity.ID)
				if err != nil {
					return nil, fmt.Errorf("cannot fetch alarm: %w", err)
				}

				// If some rule has already been applied on entity.
				if lastAlarm != nil && !lastAlarm.IsResolved() &&
					lastAlarm.Value.Status.Value == types.AlarmStatusNoEvents {
					return nil, nil
				}
			}

			return s.applyEntityRule(rule, entity)
		}
	}

	return nil, nil
}

func (s *baseService) applyAlarmRule(
	ctx context.Context,
	rule idlerule.Rule,
	alarm types.Alarm,
	entity types.Entity,
	now datetime.CpsTime,
) (*types.Event, error) {
	idleRuleApply := fmt.Sprintf("%s_%s", rule.Type, rule.AlarmCondition)
	event, err := s.eventGenerator.Generate(entity)
	if err != nil {
		return nil, err
	}

	event.Timestamp = now
	event.IdleRuleApply = idleRuleApply
	event.Output = s.getEventOutput(rule)
	if rule.Operation.Parameters.State != nil {
		event.State = *rule.Operation.Parameters.State
	}

	switch rule.Operation.Type {
	case types.ActionTypeAck:
		event.EventType = types.EventTypeAck
	case types.ActionTypeAckRemove:
		event.EventType = types.EventTypeAckremove
	case types.ActionTypeCancel:
		event.EventType = types.EventTypeCancel
	case types.ActionTypeAssocTicket:
		event.EventType = types.EventTypeAssocTicket
		event.TicketInfo = types.TicketInfo{
			Ticket:           rule.Operation.Parameters.Ticket,
			TicketRuleID:     rule.ID,
			TicketRuleName:   types.RuleNameIdleRulePrefix + rule.Name,
			TicketURL:        rule.Operation.Parameters.TicketURL,
			TicketURLTitle:   rule.Operation.Parameters.TicketURLTitle,
			TicketSystemName: rule.Operation.Parameters.TicketSystemName,
			TicketData:       rule.Operation.Parameters.TicketData,
			TicketComment:    rule.Comment,
		}
		event.Output = event.TicketInfo.GetStepMessage()
	case types.ActionTypeChangeState:
		event.EventType = types.EventTypeChangestate
	case types.ActionTypePbehavior:
		rpcEvent := rpc.PbehaviorEvent{
			Alarm:  &alarm,
			Entity: &entity,
			Type:   rpc.PbehaviorEventTypeCreate,
			Params: rpc.PbehaviorParameters{
				Author:         canopsis.DefaultEventAuthor,
				Name:           rule.Operation.Parameters.Name,
				Reason:         rule.Operation.Parameters.Reason,
				Type:           rule.Operation.Parameters.Type,
				RRule:          rule.Operation.Parameters.RRule,
				Tstart:         rule.Operation.Parameters.Tstart,
				Tstop:          rule.Operation.Parameters.Tstop,
				StartOnTrigger: rule.Operation.Parameters.StartOnTrigger,
				Duration:       rule.Operation.Parameters.Duration,
				RuleName:       types.RuleNameIdleRulePrefix + rule.Name,
				Color:          rule.Operation.Parameters.Color,
				Origin:         idlerule.PbehaviorOrigin,
				Comment:        rule.Comment,
			},
		}
		b, err := s.encoder.Encode(rpcEvent)
		if err != nil {
			return nil, fmt.Errorf("cannot encode rpc event: %w", err)
		}

		err = s.pbhRpcClient.Call(ctx, engine.RPCMessage{
			CorrelationID: fmt.Sprintf("%s&&%s", alarm.ID, rule.ID),
			Body:          b,
		})
		if err != nil {
			return nil, fmt.Errorf("cannot sent rpc event: %w", err)
		}

		err = s.entityAdapter.UpdateIdleFields(ctx, entity.ID, entity.IdleSince, idleRuleApply)
		if err != nil {
			return nil, fmt.Errorf("cannot update entity: %w", err)
		}

		return nil, nil
	case types.ActionTypeSnooze:
		event.EventType = types.EventTypeSnooze
		event.Duration = types.CpsNumber(rule.Operation.Parameters.Duration.AddTo(now).Sub(now.Time).Seconds())
	default:
		return nil, fmt.Errorf("unknown idle rule id=%q operation type=%q", rule.ID, rule.Operation.Type)
	}

	if event.EventType == "" {
		return nil, fmt.Errorf("invalid idle rule id=%q operation params %v", rule.ID, rule.Operation.Parameters)
	}

	return &event, nil
}

// applyEntityRule generates no events event.
// It uses alarm arg to fill event field.
// It uses entity type and connector arg to fill event field if alarm arg is nil.
func (s *baseService) applyEntityRule(
	rule idlerule.Rule,
	entity types.Entity,
) (*types.Event, error) {
	event, err := s.eventGenerator.Generate(entity)
	if err != nil {
		return nil, err
	}

	event.EventType = types.EventTypeNoEvents
	event.Timestamp = datetime.NewCpsTime()
	event.State = types.AlarmStateCritical
	event.Output = s.getEventOutput(rule)
	event.IdleRuleApply = rule.Type

	return &event, nil
}

func (s *baseService) closeConnectorAlarms(ctx context.Context) ([]types.Event, error) {
	alarms, err := s.alarmAdapter.GetOpenedAlarmsByConnectorIdleRules(ctx)
	if err != nil {
		return nil, fmt.Errorf("cannot fetch opened alarms: %w", err)
	}

	events := make([]types.Event, len(alarms))
	for i, alarm := range alarms {
		events[i] = types.Event{
			EventType:     types.EventTypeNoEvents,
			State:         types.AlarmStateOK,
			Connector:     alarm.Value.Connector,
			ConnectorName: alarm.Value.ConnectorName,
			SourceType:    types.SourceTypeConnector,
			Timestamp:     datetime.NewCpsTime(),
			Initiator:     types.InitiatorSystem,
			Output:        alarm.Value.State.Message,
			Author:        alarm.Value.State.Author,
		}
	}

	return events, nil
}

func (s *baseService) getEventOutput(rule idlerule.Rule) string {
	outputBuilder := strings.Builder{}
	outputBuilder.WriteString(types.RuleNameIdleRulePrefix)
	outputBuilder.WriteString(rule.Name)
	if rule.Operation != nil && rule.Operation.Parameters.Output != "" {
		outputBuilder.WriteString(". ")
		outputBuilder.WriteString(types.OutputCommentPrefix)
		outputBuilder.WriteString(rule.Operation.Parameters.Output)
		outputBuilder.WriteRune('.')
	}

	return outputBuilder.String()
}
