package event

import (
	"context"
	"errors"
	"fmt"
	"slices"
	"strings"

	"git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis"
	"git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis/alarmstatus"
	"git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis/alarmtag"
	"git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis/config"
	"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"
	"git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis/entitycounters"
	"git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis/entitycounters/calculator"
	"git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis/metrics"
	"git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/canopsis/pbehavior"
	"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"
	"git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/mongo"
	"git.canopsis.net/canopsis/canopsis-community/community/go-engines-community/lib/utils"
	"github.com/rs/zerolog"
	"go.mongodb.org/mongo-driver/v2/bson"
	mongodriver "go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
)

func NewNoEventsProcessor(
	client mongo.DbClient,
	alarmConfigProvider config.AlarmConfigProvider,
	alarmStatusService alarmstatus.Service,
	pbhTypeResolver pbehavior.EntityTypeResolver,
	autoInstructionMatcher AutoInstructionMatcher,
	entityServiceCountersCalculator calculator.EntityServiceCountersCalculator,
	componentCountersCalculator calculator.ComponentCountersCalculator,
	eventsSender entitycounters.EventsSender,
	metaAlarmPostProcessor MetaAlarmPostProcessor,
	metricsSender metrics.Sender,
	remediationRpcClient engine.RPCClient,
	internalTagAlarmMatcher alarmtag.InternalTagAlarmMatcher,
	encoder encoding.Encoder,
	logger zerolog.Logger,
) Processor {
	return &noEventsProcessor{
		client:                          client,
		alarmCollection:                 client.Collection(mongo.AlarmMongoCollection),
		entityCollection:                client.Collection(mongo.EntityMongoCollection),
		pbehaviorCollection:             client.Collection(mongo.PbehaviorMongoCollection),
		alarmConfigProvider:             alarmConfigProvider,
		alarmStatusService:              alarmStatusService,
		pbhTypeResolver:                 pbhTypeResolver,
		autoInstructionMatcher:          autoInstructionMatcher,
		entityServiceCountersCalculator: entityServiceCountersCalculator,
		componentCountersCalculator:     componentCountersCalculator,
		eventsSender:                    eventsSender,
		metaAlarmPostProcessor:          metaAlarmPostProcessor,
		metricsSender:                   metricsSender,
		remediationRpcClient:            remediationRpcClient,
		internalTagAlarmMatcher:         internalTagAlarmMatcher,
		encoder:                         encoder,
		logger:                          logger,
	}
}

type noEventsProcessor struct {
	client                          mongo.DbClient
	alarmCollection                 mongo.DbCollection
	entityCollection                mongo.DbCollection
	pbehaviorCollection             mongo.DbCollection
	alarmConfigProvider             config.AlarmConfigProvider
	alarmStatusService              alarmstatus.Service
	pbhTypeResolver                 pbehavior.EntityTypeResolver
	autoInstructionMatcher          AutoInstructionMatcher
	entityServiceCountersCalculator calculator.EntityServiceCountersCalculator
	componentCountersCalculator     calculator.ComponentCountersCalculator
	eventsSender                    entitycounters.EventsSender
	metaAlarmPostProcessor          MetaAlarmPostProcessor
	metricsSender                   metrics.Sender
	remediationRpcClient            engine.RPCClient
	internalTagAlarmMatcher         alarmtag.InternalTagAlarmMatcher
	encoder                         encoding.Encoder
	logger                          zerolog.Logger
}

func (p *noEventsProcessor) Process(ctx context.Context, event rpc.AxeEvent) (Result, error) {
	result := Result{}
	if event.Entity == nil || !event.Entity.Enabled || event.Parameters.State == nil {
		return result, nil
	}

	entity := *event.Entity
	var updatedServiceStates map[string]entitycounters.UpdatedServicesInfo

	var componentStateChanged bool
	var newComponentState int

	err := p.client.WithTransaction(ctx, func(ctx context.Context) error {
		result = Result{}
		entity = *event.Entity
		updatedServiceStates = nil

		alarm := types.Alarm{}
		err := p.alarmCollection.FindOne(ctx, bson.M{
			"d":          entity.ID,
			"v.resolved": nil,
		}).Decode(&alarm)
		if err != nil && !errors.Is(err, mongodriver.ErrNoDocuments) {
			return err
		}

		if alarm.ID == "" {
			var v types.Entity
			err = p.entityCollection.FindOne(ctx, bson.M{"_id": entity.ID}).Decode(&v)
			if err != nil {
				return err
			}

			entity = v
			result, err = p.createAlarm(ctx, entity, event.Parameters)
		} else {
			result, err = p.updateAlarm(ctx, alarm, entity, event.Parameters)
		}

		if err != nil {
			return err
		}

		if result.Entity.ID != "" {
			entity = result.Entity
		}

		result.IsCountersUpdated, updatedServiceStates, componentStateChanged, newComponentState, err = processComponentAndServiceCounters(
			ctx,
			p.entityServiceCountersCalculator,
			p.componentCountersCalculator,
			&result.Alarm,
			&entity,
			result.AlarmChange,
		)

		return err
	})

	if err != nil || result.Alarm.ID == "" {
		return result, err
	}

	go p.postProcess(context.WithoutCancel(ctx), event, result, updatedServiceStates, componentStateChanged, newComponentState)

	return result, nil
}

func (p *noEventsProcessor) createAlarm(ctx context.Context, entity types.Entity, params rpc.AxeParameters) (Result, error) {
	now := datetime.NewCpsTime()
	result := Result{}
	if *params.State == types.AlarmStateOK {
		return result, nil
	}

	alarmChange := types.NewAlarmChange()
	var pbehaviorInfo types.PbehaviorInfo
	updateEntityPbhInfo := false
	var err error
	if entity.PbehaviorInfo.IsDefaultActive() {
		updateEntityPbhInfo = true
		pbehaviorInfo, err = resolvePbehaviorInfo(ctx, entity, now, p.pbhTypeResolver)
		if err != nil {
			return result, err
		}
	} else {
		pbehaviorInfo = entity.PbehaviorInfo
		pbehaviorInfo.Timestamp = &now
		alarmChange.PreviousPbehaviorTypeID = entity.PbehaviorInfo.TypeID
		alarmChange.PreviousPbehaviorCannonicalType = entity.PbehaviorInfo.CanonicalType
		alarmChange.PreviousEntityPbehaviorTime = entity.PbehaviorInfo.Timestamp
	}

	alarmConfig := p.alarmConfigProvider.Get()
	alarm, err := p.newAlarm(params, entity, now, alarmConfig)
	if err != nil {
		return result, err
	}

	stateStep := NewAlarmStep(types.AlarmStepStateIncrease, params, false)
	stateStep.Value = *params.State
	statusStep := NewAlarmStep(types.AlarmStepStatusIncrease, params, false)
	statusStep.Value = types.AlarmStatusNoEvents
	alarm.Value.State = &stateStep
	alarm.Value.MaxState = stateStep.Value
	alarm.Value.InitialState = stateStep.Value
	err = alarm.Value.Steps.Add(stateStep)
	if err != nil {
		return result, fmt.Errorf("cannot add alarm steps: %w", err)
	}
	alarm.Value.Status = &statusStep
	err = alarm.Value.Steps.Add(statusStep)
	if err != nil {
		return result, fmt.Errorf("cannot add alarm steps: %w", err)
	}

	alarm.Value.TotalStateChanges++

	if pbehaviorInfo.IsDefaultActive() {
		alarmChange.Type = types.AlarmChangeTypeCreate
		alarm.NotAckedSince = &alarm.Time
	} else {
		if pbehaviorInfo.IsActive() {
			alarm.NotAckedSince = &alarm.Time
		} else {
			alarm.Value.InactiveStart = &now
		}

		newStep := types.NewPbhAlarmStep(types.AlarmStepPbhEnter, *pbehaviorInfo.Timestamp, pbehaviorInfo.Author,
			pbehaviorInfo.GetStepMessage(), "", "", types.InitiatorSystem, pbehaviorInfo.CanonicalType,
			pbehaviorInfo.IconName, pbehaviorInfo.Color)
		alarm.Value.PbehaviorInfo = pbehaviorInfo
		err := alarm.Value.Steps.Add(newStep)
		if err != nil {
			return result, fmt.Errorf("cannot add alarm steps: %w", err)
		}

		alarmChange.Type = types.AlarmChangeTypeCreateAndPbhEnter
	}

	result.IsInstructionMatched, err = p.autoInstructionMatcher.Match(alarmChange.GetTriggers(), types.AlarmWithEntity{Alarm: alarm, Entity: entity})
	if err != nil {
		return result, err
	}

	if p.alarmConfigProvider.Get().ActivateAlarmAfterAutoRemediation {
		alarm.InactiveAutoInstructionInProgress = result.IsInstructionMatched
	}

	alarm.InternalTags = p.internalTagAlarmMatcher.Match(entity, alarm)
	alarm.InternalTagsUpdated = datetime.NewMicroTime()
	alarm.Tags = slices.Clone(alarm.InternalTags)

	_, err = p.alarmCollection.InsertOne(ctx, types.AlarmWithEntityField{
		Alarm:  alarm,
		Entity: entity,
	})
	if err != nil {
		return result, fmt.Errorf("cannot create alarm: %w", err)
	}

	entityUpdate := bson.M{"$set": bson.M{
		"idle_since":           params.Timestamp,
		"last_idle_rule_apply": params.IdleRuleApply,
	}}
	if alarmChange.Type == types.AlarmChangeTypeCreateAndPbhEnter && updateEntityPbhInfo {
		entityUpdate["pbehavior_info"] = alarm.Value.PbehaviorInfo
		entityUpdate["last_pbehavior_date"] = alarm.Value.PbehaviorInfo.Timestamp
	}

	result.Entity, err = updateEntityByID(ctx, entity.ID, entityUpdate, p.entityCollection)
	if err != nil {
		return result, err
	}

	result.Forward = true
	result.Alarm = alarm
	result.AlarmChange = alarmChange

	return result, nil
}

func (p *noEventsProcessor) updateAlarm(ctx context.Context, alarm types.Alarm, entity types.Entity, params rpc.AxeParameters) (Result, error) {
	result := Result{}
	alarmChange := p.newAlarmChange(alarm)
	previousState := alarm.Value.State.Value
	previousStatus := alarm.Value.Status.Value
	newState := *params.State
	match := bson.M{
		"_id":        alarm.ID,
		"v.resolved": nil,
		"$expr":      bson.M{"$lt": bson.A{bson.M{"$size": "$v.steps"}, types.AlarmStepsHardLimit}},
	}
	set := bson.M{
		"v.output":      params.Output,
		"v.long_output": params.LongOutput,
	}
	push := bson.M{}
	inc := bson.M{}
	unset := bson.M{}

	var stateStep types.AlarmStep
	if newState != previousState {
		stateStep = NewAlarmStep(types.AlarmStepStateIncrease, params, !alarm.Value.PbehaviorInfo.IsDefaultActive())
		stateStep.Value = newState
		alarmChange.Type = types.AlarmChangeTypeStateIncrease
		if newState < previousState {
			alarmChange.Type = types.AlarmChangeTypeStateDecrease
			stateStep.Type = types.AlarmStepStateDecrease
		} else if alarm.Value.MaxState < newState {
			alarm.Value.MaxState = newState
			set["v.max_state"] = newState
		}

		alarm.Value.State = &stateStep
		err := alarm.Value.Steps.Add(stateStep)
		if err != nil {
			return result, fmt.Errorf("cannot add alarm steps: %w", err)
		}

		set["v.state"] = stateStep
		set["v.last_update_date"] = params.Timestamp
		set["v.last_st_upd_dt"] = params.Timestamp

		if alarm.IsStateLocked() {
			alarm.Value.ChangeState = nil
			unset["v.change_state"] = ""
		}
	}

	newStatus := types.CpsNumber(types.AlarmStatusNoEvents)
	statusStepMessage := params.Output
	if newState == types.AlarmStateOK {
		var statusRuleName string
		newStatus, statusRuleName = p.alarmStatusService.ComputeStatus(alarm, entity)
		statusStepMessage = ConcatOutputAndRuleName(params.Output, statusRuleName)
	}

	if newStatus == previousStatus && newState == previousState {
		return result, nil
	}

	if newStatus == previousStatus {
		if stateStep.Type != "" {
			push["v.steps"] = stateStep
			inc["v.total_state_changes"] = 1
			inc["v.state_changes_since_status_update"] = 1
		}
	} else {
		statusStep := NewAlarmStep(types.AlarmStepStatusIncrease, params, !alarm.Value.PbehaviorInfo.IsDefaultActive())
		statusStep.Value = newStatus
		statusStep.Message = statusStepMessage
		if newStatus < previousStatus {
			statusStep.Type = types.AlarmStepStatusDecrease
		}

		set["v.status"] = statusStep
		set["v.state_changes_since_status_update"] = 0
		if stateStep.Type != "" {
			push["v.steps"] = bson.M{"$each": bson.A{stateStep, statusStep}}
		} else {
			push["v.steps"] = statusStep
		}
	}

	newAlarm := types.Alarm{}
	err := p.alarmCollection.FindOneAndUpdate(ctx, match, bson.M{
		"$set":   set,
		"$push":  push,
		"$inc":   inc,
		"$unset": unset,
	}, options.FindOneAndUpdate().SetReturnDocument(options.After)).Decode(&newAlarm)
	if err != nil {
		if errors.Is(err, mongodriver.ErrNoDocuments) {
			return result, nil
		}
		return result, fmt.Errorf("cannot update alarm: %w", err)
	}

	var entityUpdate bson.M
	if newAlarm.Value.Status.Value == types.AlarmStatusNoEvents {
		entityUpdate = bson.M{"$set": bson.M{
			"idle_since":           params.Timestamp,
			"last_idle_rule_apply": params.IdleRuleApply,
		}}
	} else {
		entityUpdate = bson.M{"$unset": bson.M{
			"idle_since":           "",
			"last_idle_rule_apply": "",
		}}
	}

	result.Entity, err = updateEntityByID(ctx, entity.ID, entityUpdate, p.entityCollection)
	if err != nil {
		return result, err
	}

	result.Forward = true
	result.Alarm = newAlarm
	result.AlarmChange = alarmChange
	result.IsInstructionMatched, err = p.autoInstructionMatcher.Match(alarmChange.GetTriggers(), types.AlarmWithEntity{Alarm: newAlarm, Entity: entity})

	return result, err
}

func (p *noEventsProcessor) newAlarmChange(alarm types.Alarm) types.AlarmChange {
	alarmChange := types.NewAlarmChange()
	alarmChange.PreviousState = alarm.Value.State.Value
	alarmChange.PreviousStateChange = alarm.Value.State.Timestamp
	alarmChange.PreviousStatus = alarm.Value.Status.Value
	return alarmChange
}

func (p *noEventsProcessor) newAlarm(
	params rpc.AxeParameters,
	entity types.Entity,
	timestamp datetime.CpsTime,
	alarmConfig config.AlarmConfig,
) (types.Alarm, error) {
	alarm := types.Alarm{
		EntityID: entity.ID,
		ID:       utils.NewID(),
		Time:     timestamp,
		Value: types.AlarmValue{
			CreationDate:                timestamp,
			DisplayName:                 types.GenDisplayName(alarmConfig.DisplayNameScheme),
			InitialOutput:               params.Output,
			Output:                      params.Output,
			InitialLongOutput:           params.LongOutput,
			LongOutput:                  params.LongOutput,
			LongOutputHistory:           []string{params.LongOutput},
			LastUpdateDate:              params.Timestamp,
			LastStateOrStatusUpdateDate: params.Timestamp,
			LastEventDate:               timestamp,
			Parents:                     []string{},
			Children:                    []string{},
			UnlinkedParents:             []string{},
			Infos:                       map[string]map[string]interface{}{},
		},
	}

	if params.Initiator != types.InitiatorSystem {
		return types.Alarm{}, fmt.Errorf("unknown initiator %q", params.Initiator)
	}

	connector := ""
	connectorName := ""
	if entity.Connector == "" {
		connector = canopsis.DefaultSystemAlarmConnector
		connectorName = canopsis.DefaultSystemAlarmConnector
	} else {
		connector, connectorName, _ = strings.Cut(entity.Connector, "/")
	}

	switch entity.Type {
	case types.EntityTypeResource:
		alarm.Value.Resource = entity.Name
		alarm.Value.Component = entity.Component
		alarm.Value.Connector = connector
		alarm.Value.ConnectorName = connectorName
	case types.EntityTypeComponent, types.EntityTypeService:
		alarm.Value.Component = entity.Name
		alarm.Value.Connector = connector
		alarm.Value.ConnectorName = connectorName
	case types.EntityTypeConnector:
		alarm.Value.Connector, alarm.Value.ConnectorName, _ = strings.Cut(entity.ID, "/")
	default:
		return types.Alarm{}, fmt.Errorf("unknown entity type %q", entity.Type)
	}

	return alarm, nil
}

func (p *noEventsProcessor) postProcess(
	ctx context.Context,
	event rpc.AxeEvent,
	result Result,
	updatedServiceStates map[string]entitycounters.UpdatedServicesInfo,
	componentStateChanged bool,
	newComponentState int,
) {
	entity := *event.Entity
	if result.Entity.ID != "" {
		entity = result.Entity
	}

	p.metricsSender.SendEventMetrics(
		result.Alarm,
		entity,
		result.AlarmChange,
		event.Parameters.Timestamp.Time,
		event.Parameters.Initiator,
		event.Parameters.User,
		event.Parameters.Instruction,
		"",
	)

	for servID, servInfo := range updatedServiceStates {
		err := p.eventsSender.UpdateServiceState(ctx, servID, servInfo)
		if err != nil {
			p.logger.Err(err).Msg("failed to update service state")
		}
	}

	if componentStateChanged {
		err := p.eventsSender.UpdateComponentState(ctx, event.Entity.Component, newComponentState)
		if err != nil {
			p.logger.Err(err).Msg("failed to update component state")
		}
	}

	err := p.metaAlarmPostProcessor.Process(ctx, event, rpc.AxeResultEvent{
		Alarm:           &result.Alarm,
		AlarmChangeType: result.AlarmChange.Type,
	})
	if err != nil {
		p.logger.Err(err).Msg("cannot process meta alarm")
	}

	err = sendRemediationEvent(ctx, event, result, p.remediationRpcClient, p.encoder)
	if err != nil {
		p.logger.Err(err).Msg("cannot send event to engine-remediation")
	}

	if result.AlarmChange.Type == types.AlarmChangeTypeCreateAndPbhEnter {
		err = updatePbehaviorLastAlarmDate(ctx, p.pbehaviorCollection, result.Alarm.Value.PbehaviorInfo.ID, result.Alarm.Value.PbehaviorInfo.Timestamp)
		if err != nil {
			p.logger.Err(err).Msg("cannot update pbehavior")
		}

		if !result.Alarm.Value.PbehaviorInfo.IsDefaultActive() {
			err = updatePbehaviorAlarmCount(ctx, p.pbehaviorCollection, result.Alarm.Value.PbehaviorInfo.ID, "")
			if err != nil {
				p.logger.Err(err).Msg("cannot update pbehavior")
			}
		}
	}
}
