// SPDX-FileCopyrightText: 2025 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-2.0-or-later

#include "icinga/dependency.hpp"
#include "icinga/host.hpp"
#include "icinga/service.hpp"

using namespace icinga;

/**
 * Construct a helper for evaluating the state of dependencies.
 *
 * @param dt Dependency type to check for within the individual methods.
 */
DependencyStateChecker::DependencyStateChecker(DependencyType dt)
	: m_DependencyType(dt)
{
}

/**
 * Checks whether a given checkable is currently reachable.
 *
 * @param checkable The checkable to check reachability for.
 * @param rstack The recursion stack level to prevent infinite recursion, defaults to 0.
 * @return Whether the given checkable is reachable.
 */
bool DependencyStateChecker::IsReachable(Checkable::ConstPtr checkable, int rstack)
{
	// If the reachability of this checkable was already computed, return it directly. Otherwise, already create a
	// temporary map entry that says that this checkable is unreachable so that the different cases returning false
	// don't have to deal with updating the cache, but only the final return true does. Cyclic dependencies are invalid,
	// hence recursive calls won't access the potentially not yet correct cached value.
	if (auto [it, inserted] = m_Cache.insert({checkable, false}); !inserted) {
		return it->second;
	}

	if (rstack > Dependency::MaxDependencyRecursionLevel) {
		Log(LogWarning, "Checkable")
			<< "Too many nested dependencies (>" << Dependency::MaxDependencyRecursionLevel << ") for checkable '"
			<< checkable->GetName() << "': Dependency failed.";

		return false;
	}

	/* implicit dependency on host if this is a service */
	const auto* service = dynamic_cast<const Service*>(checkable.get());
	if (service && (m_DependencyType == DependencyState || m_DependencyType == DependencyNotification)) {
		Host::Ptr host = service->GetHost();

		if (host && host->GetState() != HostUp && host->GetStateType() == StateTypeHard) {
			return false;
		}
	}

	for (auto& dependencyGroup : checkable->GetDependencyGroups()) {
		if (auto state(GetState(dependencyGroup, checkable.get(), rstack + 1)); state != DependencyGroup::State::Ok) {
			Log(LogDebug, "Checkable")
				<< "Dependency group '" << dependencyGroup->GetRedundancyGroupName() << "' have failed for checkable '"
				<< checkable->GetName() << "': Marking as unreachable.";

			return false;
		}
	}

	// Note: This must do the map lookup again. The iterator from above must not be used as a m_Cache.insert() inside a
	// recursive may have invalidated it.
	m_Cache[checkable] = true;
	return true;
}

/**
 * Retrieve the state of the given dependency group.
 *
 * The state of the dependency group is determined based on the state of the parent Checkables and dependency objects
 * of the group. A dependency group is considered unreachable when none of the parent Checkables is reachable. However,
 * a dependency group may still be marked as failed even when it has reachable parent Checkables, but an unreachable
 * group has always a failed state.
 *
 * @param group
 * @param child The child Checkable to evaluate the state for.
 * @param rstack The recursion stack level to prevent infinite recursion, defaults to 0.
 *
 * @return - Returns the state of the current dependency group.
 */
DependencyGroup::State DependencyStateChecker::GetState(const DependencyGroup::ConstPtr& group, const Checkable* child, int rstack)
{
	using State = DependencyGroup::State;

	auto dependencies(group->GetDependenciesForChild(child));
	size_t reachable = 0, available = 0;

	for (const auto& dependency : dependencies) {
		if (IsReachable(dependency->GetParent(), rstack)) {
			reachable++;

			// Only reachable parents are considered for availability. If they are unreachable and checks are
			// disabled, they could be incorrectly treated as available otherwise.
			if (dependency->IsAvailable(m_DependencyType)) {
				available++;
			}
		}
	}

	if (group->IsRedundancyGroup()) {
		// The state of a redundancy group is determined by the best state of any parent. If any parent ist reachable,
		// the redundancy group is reachable, analogously for availability.
		if (reachable == 0) {
			return State::Unreachable;
		} else if (available == 0) {
			return State::Failed;
		} else {
			return State::Ok;
		}
	} else {
		// For dependencies without a redundancy group, dependencies.size() will be 1 in almost all cases. It will only
		// contain more elements if there are duplicate dependency config objects between two checkables. In this case,
		// all of them have to be reachable/available as they don't provide redundancy.
		if (reachable < dependencies.size()) {
			return State::Unreachable;
		} else if (available < dependencies.size()) {
			return State::Failed;
		} else {
			return State::Ok;
		}
	}
}
