module dub.packagesuppliers.fallback;

import dub.packagesuppliers.packagesupplier;
import std.typecons : AutoImplement;

package abstract class AbstractFallbackPackageSupplier : PackageSupplier
{
	protected import core.time : minutes;
	protected import std.datetime : Clock, SysTime;

	static struct Pair { PackageSupplier ps; SysTime failTime; }
	protected Pair[] m_suppliers;

	this(PackageSupplier[] suppliers)
	{
		assert(suppliers.length);
		m_suppliers.length = suppliers.length;
		foreach (i, ps; suppliers)
			m_suppliers[i].ps = ps;
	}

	override @property string description()
	{
		import std.algorithm.iteration : map;
		import std.format : format;
		return format("%s (fallbacks %-(%s, %))", m_suppliers[0].ps.description,
			m_suppliers[1 .. $].map!(pair => pair.ps.description));
	}

	// Workaround https://issues.dlang.org/show_bug.cgi?id=2525
	abstract override Version[] getVersions(string package_id);
	abstract override void fetchPackage(NativePath path, string package_id, Dependency dep, bool pre_release);
	abstract override Json fetchPackageRecipe(string package_id, Dependency dep, bool pre_release);
	abstract override SearchResult[] searchPackages(string query);
}


/**
	Combines two package suppliers and uses the second as fallback to handle failures.

	Assumes that both registries serve the same packages (--mirror).
*/
package(dub) alias FallbackPackageSupplier = AutoImplement!(AbstractFallbackPackageSupplier, fallback);

private template fallback(T, alias func)
{
	import std.format : format;
	enum fallback = q{
		import dub.internal.logging : logDebug;

		Exception firstEx;
		try
			return m_suppliers[0].ps.%1$s(args);
		catch (Exception e)
		{
			logDebug("Package supplier %%s failed with '%%s', trying fallbacks.",
				m_suppliers[0].ps.description, e.msg);
			firstEx = e;
		}

		immutable now = Clock.currTime;
		foreach (ref pair; m_suppliers[1 .. $])
		{
			if (pair.failTime > now - 10.minutes)
				continue;
			try
			{
				scope (success) logDebug("Fallback %%s succeeded", pair.ps.description);
				return pair.ps.%1$s(args);
			}
			catch (Exception e)
			{
				pair.failTime = now;
				logDebug("Fallback package supplier %%s failed with '%%s'.",
					pair.ps.description, e.msg);
			}
		}
		throw firstEx;
	}.format(__traits(identifier, func));
}
