/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements.  See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License.  You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package event

import (
	"context"
	"fmt"

	v1 "github.com/apache/camel-k/pkg/apis/camel/v1"
	"github.com/apache/camel-k/pkg/apis/camel/v1alpha1"
	"github.com/apache/camel-k/pkg/client"
	"github.com/apache/camel-k/pkg/util/kubernetes"
	"github.com/apache/camel-k/pkg/util/log"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/tools/record"
	runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	// ReasonIntegrationPhaseUpdated --
	ReasonIntegrationPhaseUpdated = "IntegrationPhaseUpdated"
	// ReasonIntegrationConditionChanged --
	ReasonIntegrationConditionChanged = "IntegrationConditionChanged"
	// ReasonIntegrationError --
	ReasonIntegrationError = "IntegrationError"

	// ReasonIntegrationKitPhaseUpdated --
	ReasonIntegrationKitPhaseUpdated = "IntegrationKitPhaseUpdated"
	// ReasonIntegrationKitConditionChanged --
	ReasonIntegrationKitConditionChanged = "IntegrationKitConditionChanged"
	// ReasonIntegrationKitError --
	ReasonIntegrationKitError = "IntegrationKitError"

	// ReasonIntegrationPlatformPhaseUpdated --
	ReasonIntegrationPlatformPhaseUpdated = "IntegrationPlatformPhaseUpdated"
	// ReasonIntegrationPlatformConditionChanged --
	ReasonIntegrationPlatformConditionChanged = "IntegrationPlatformConditionChanged"
	// ReasonIntegrationPlatformError --
	ReasonIntegrationPlatformError = "IntegrationPlatformError"

	// ReasonBuildPhaseUpdated --
	ReasonBuildPhaseUpdated = "BuildPhaseUpdated"
	// ReasonBuildConditionChanged --
	ReasonBuildConditionChanged = "BuildConditionChanged"
	// ReasonBuildError --
	ReasonBuildError = "BuildError"

	// ReasonKameletError --
	ReasonKameletError = "KameletError"
	// ReasonKameletConditionChanged --
	ReasonKameletConditionChanged = "KameletConditionChanged"
	// ReasonKameletPhaseUpdated --
	ReasonKameletPhaseUpdated = "KameletPhaseUpdated"

	// ReasonKameletBindingError --
	ReasonKameletBindingError = "KameletBindingError"
	// ReasonKameletBindingConditionChanged --
	ReasonKameletBindingConditionChanged = "KameletBindingConditionChanged"
	// ReasonKameletBindingPhaseUpdated --
	ReasonKameletBindingPhaseUpdated = "KameletBindingPhaseUpdated"

	// ReasonRelatedObjectChanged --
	ReasonRelatedObjectChanged = "ReasonRelatedObjectChanged"
)

// NotifyIntegrationError automatically generates error events when the integration reconcile cycle phase has an error
func NotifyIntegrationError(ctx context.Context, c client.Client, recorder record.EventRecorder, old, new *v1.Integration, err error) {
	it := old
	if new != nil {
		it = new
	}
	if it == nil {
		return
	}
	recorder.Eventf(it, corev1.EventTypeWarning, ReasonIntegrationError, "Cannot reconcile Integration %s: %v", it.Name, err)
}

// NotifyIntegrationUpdated automatically generates events when the integration changes
func NotifyIntegrationUpdated(ctx context.Context, c client.Client, recorder record.EventRecorder, old, new *v1.Integration) {
	if new == nil {
		return
	}
	oldPhase := ""
	var oldConditions []v1.ResourceCondition
	if old != nil {
		oldPhase = string(old.Status.Phase)
		oldConditions = old.Status.GetConditions()
	}
	if new.Status.Phase != v1.IntegrationPhaseNone {
		notifyIfConditionUpdated(recorder, new, oldConditions, new.Status.GetConditions(), "Integration", new.Name, ReasonIntegrationConditionChanged)
	}
	notifyIfPhaseUpdated(ctx, c, recorder, new, oldPhase, string(new.Status.Phase), "Integration", new.Name, ReasonIntegrationPhaseUpdated, "")
}

// NotifyIntegrationKitUpdated automatically generates events when an integration kit changes
func NotifyIntegrationKitUpdated(ctx context.Context, c client.Client, recorder record.EventRecorder, old, new *v1.IntegrationKit) {
	if new == nil {
		return
	}
	oldPhase := ""
	var oldConditions []v1.ResourceCondition
	if old != nil {
		oldPhase = string(old.Status.Phase)
		oldConditions = old.Status.GetConditions()
	}
	if new.Status.Phase != v1.IntegrationKitPhaseNone {
		notifyIfConditionUpdated(recorder, new, oldConditions, new.Status.GetConditions(), "Integration Kit", new.Name, ReasonIntegrationKitConditionChanged)
	}
	notifyIfPhaseUpdated(ctx, c, recorder, new, oldPhase, string(new.Status.Phase), "Integration Kit", new.Name, ReasonIntegrationKitPhaseUpdated, "")
}

// NotifyIntegrationKitError automatically generates error events when the integration kit reconcile cycle phase has an error
func NotifyIntegrationKitError(ctx context.Context, c client.Client, recorder record.EventRecorder, old, new *v1.IntegrationKit, err error) {
	kit := old
	if new != nil {
		kit = new
	}
	if kit == nil {
		return
	}
	recorder.Eventf(kit, corev1.EventTypeWarning, ReasonIntegrationKitError, "Cannot reconcile Integration Kit %s: %v", kit.Name, err)
}

// NotifyIntegrationPlatformUpdated automatically generates events when an integration platform changes
func NotifyIntegrationPlatformUpdated(ctx context.Context, c client.Client, recorder record.EventRecorder, old, new *v1.IntegrationPlatform) {
	if new == nil {
		return
	}
	oldPhase := ""
	var oldConditions []v1.ResourceCondition
	if old != nil {
		oldPhase = string(old.Status.Phase)
		oldConditions = old.Status.GetConditions()
	}
	if new.Status.Phase != v1.IntegrationPlatformPhaseNone {
		notifyIfConditionUpdated(recorder, new, oldConditions, new.Status.GetConditions(), "Integration Platform", new.Name, ReasonIntegrationPlatformConditionChanged)
	}
	notifyIfPhaseUpdated(ctx, c, recorder, new, oldPhase, string(new.Status.Phase), "Integration Platform", new.Name, ReasonIntegrationPlatformPhaseUpdated, "")
}

// NotifyIntegrationPlatformError automatically generates error events when the integration Platform reconcile cycle phase has an error
func NotifyIntegrationPlatformError(ctx context.Context, c client.Client, recorder record.EventRecorder, old, new *v1.IntegrationPlatform, err error) {
	p := old
	if new != nil {
		p = new
	}
	if p == nil {
		return
	}
	recorder.Eventf(p, corev1.EventTypeWarning, ReasonIntegrationPlatformError, "Cannot reconcile Integration Platform %s: %v", p.Name, err)
}

// NotifyKameletUpdated automatically generates events when a Kamelet changes
func NotifyKameletUpdated(ctx context.Context, c client.Client, recorder record.EventRecorder, old, new *v1alpha1.Kamelet) {
	if new == nil {
		return
	}
	oldPhase := ""
	var oldConditions []v1.ResourceCondition
	if old != nil {
		oldPhase = string(old.Status.Phase)
		oldConditions = old.Status.GetConditions()
	}
	if new.Status.Phase != v1alpha1.KameletPhaseNone {
		notifyIfConditionUpdated(recorder, new, oldConditions, new.Status.GetConditions(), "Kamelet", new.Name, ReasonKameletConditionChanged)
	}
	notifyIfPhaseUpdated(ctx, c, recorder, new, oldPhase, string(new.Status.Phase), "Kamelet", new.Name, ReasonKameletPhaseUpdated, "")
}

// NotifyKameletError automatically generates error events when the kamelet reconcile cycle phase has an error
func NotifyKameletError(ctx context.Context, c client.Client, recorder record.EventRecorder, old, new *v1alpha1.Kamelet, err error) {
	k := old
	if new != nil {
		k = new
	}
	if k == nil {
		return
	}
	recorder.Eventf(k, corev1.EventTypeWarning, ReasonKameletError, "Cannot reconcile Kamelet %s: %v", k.Name, err)
}

// NotifyKameletBindingUpdated automatically generates events when a KameletBinding changes
func NotifyKameletBindingUpdated(ctx context.Context, c client.Client, recorder record.EventRecorder, old, new *v1alpha1.KameletBinding) {
	if new == nil {
		return
	}
	oldPhase := ""
	var oldConditions []v1.ResourceCondition
	if old != nil {
		oldPhase = string(old.Status.Phase)
		oldConditions = old.Status.GetConditions()
	}
	if new.Status.Phase != v1alpha1.KameletBindingPhaseNone {
		notifyIfConditionUpdated(recorder, new, oldConditions, new.Status.GetConditions(), "KameletBinding", new.Name, ReasonKameletBindingConditionChanged)
	}
	notifyIfPhaseUpdated(ctx, c, recorder, new, oldPhase, string(new.Status.Phase), "KameletBinding", new.Name, ReasonKameletBindingPhaseUpdated, "")
}

// NotifyKameletBindingError automatically generates error events when the kameletBinding reconcile cycle phase has an error
func NotifyKameletBindingError(ctx context.Context, c client.Client, recorder record.EventRecorder, old, new *v1alpha1.KameletBinding, err error) {
	k := old
	if new != nil {
		k = new
	}
	if k == nil {
		return
	}
	recorder.Eventf(k, corev1.EventTypeWarning, ReasonKameletError, "Cannot reconcile KameletBinding %s: %v", k.Name, err)
}

// NotifyBuildUpdated automatically generates events when a build changes
func NotifyBuildUpdated(ctx context.Context, c client.Client, recorder record.EventRecorder, old, new *v1.Build) {
	if new == nil {
		return
	}
	oldPhase := ""
	var oldConditions []v1.ResourceCondition
	if old != nil {
		oldPhase = string(old.Status.Phase)
		oldConditions = old.Status.GetConditions()
	}
	if new.Status.Phase != v1.BuildPhaseNone {
		notifyIfConditionUpdated(recorder, new, oldConditions, new.Status.GetConditions(), "Build", new.Name, ReasonBuildConditionChanged)
	}
	info := ""
	if new.Status.Failure != nil {
		attempt := new.Status.Failure.Recovery.Attempt
		attemptMax := new.Status.Failure.Recovery.AttemptMax
		info = fmt.Sprintf(" (recovery %d of %d)", attempt, attemptMax)
	}
	notifyIfPhaseUpdated(ctx, c, recorder, new, oldPhase, string(new.Status.Phase), "Build", new.Name, ReasonBuildPhaseUpdated, info)
}

// NotifyBuildError automatically generates error events when the build reconcile cycle phase has an error
func NotifyBuildError(ctx context.Context, c client.Client, recorder record.EventRecorder, old, new *v1.Build, err error) {
	p := old
	if new != nil {
		p = new
	}
	if p == nil {
		return
	}
	recorder.Eventf(p, corev1.EventTypeWarning, ReasonBuildError, "Cannot reconcile Build %s: %v", p.Name, err)
}

// nolint:lll
func notifyIfPhaseUpdated(ctx context.Context, c client.Client, recorder record.EventRecorder, new runtime.Object, oldPhase, newPhase string, resourceType, name, reason, info string) {
	// Update information about phase changes
	if oldPhase != newPhase {
		phase := newPhase
		if phase == "" {
			phase = "[none]"
		}
		recorder.Eventf(new, corev1.EventTypeNormal, reason, "%s %s in phase %q%s", resourceType, name, phase, info)

		if creatorRef, creator := getCreatorObject(ctx, c, new); creatorRef != nil && creator != nil {
			recorder.Eventf(creator, corev1.EventTypeNormal, ReasonRelatedObjectChanged, "%s %s subresource %s (%s) changed phase to %q%s", creatorRef.Kind, creatorRef.Name, name, resourceType, phase, info)
		}
	}
}

func notifyIfConditionUpdated(recorder record.EventRecorder, new runtime.Object, oldConditions, newConditions []v1.ResourceCondition, resourceType, name, reason string) {
	// Update information about changes in conditions
	for _, cond := range getCommonChangedConditions(oldConditions, newConditions) {
		tail := ""
		if cond.GetMessage() != "" {
			tail = fmt.Sprintf(": %s", cond.GetMessage())
		}
		recorder.Eventf(new, corev1.EventTypeNormal, reason, "Condition %q is %q for %s %s%s", cond.GetType(), cond.GetStatus(), resourceType, name, tail)
	}
}

func getCommonChangedConditions(old, new []v1.ResourceCondition) (res []v1.ResourceCondition) {
	oldState := make(map[string]v1.ResourceCondition)
	for _, c := range old {
		oldState[c.GetType()] = c
	}

	for _, newCond := range new {
		oldCond := oldState[newCond.GetType()]
		if oldCond == nil || oldCond.GetStatus() != newCond.GetStatus() || oldCond.GetMessage() != newCond.GetMessage() {
			res = append(res, newCond)
		}
	}
	return res
}

func getCreatorObject(ctx context.Context, c client.Client, obj runtime.Object) (ref *corev1.ObjectReference, creator runtime.Object) {
	if ref := kubernetes.GetCamelCreator(obj); ref != nil {
		if ref.Kind == "Integration" {
			it := v1.NewIntegration(ref.Namespace, ref.Name)
			key := runtimeclient.ObjectKey{
				Namespace: ref.Namespace,
				Name:      ref.Name,
			}
			if err := c.Get(ctx, key, &it); err != nil {
				log.Infof("Cannot get information about the Integration creating resource %v: %v", ref, err)
				return nil, nil
			}
			return ref, &it
		}
	}
	return nil, nil
}
