<?php

declare(strict_types=1);

namespace Australcode\Rewards;

defined( 'ABSPATH' ) || exit;

use League\Container\Container as LeagueContainer;
use League\Container\ReflectionContainer;
use Australcode\Rewards\Domain\Campaign\CampaignRepository;
use Australcode\Rewards\Domain\Earning\EarningOverrideResolver;
use Australcode\Rewards\Domain\EarningRule\EarningRuleRepository;
use Australcode\Rewards\Domain\RedemptionRule\RedemptionRuleRepository;
use Australcode\Rewards\Domain\Referral\ReferralRepository;
use Australcode\Rewards\Domain\Settings\SettingsRepository;
use Australcode\Rewards\Domain\Tier\TierRepository;
use Australcode\Rewards\Domain\Wallet\BalanceDirectory;
use Australcode\Rewards\Domain\Wallet\BalanceRepository;
use Australcode\Rewards\Domain\Wallet\CustomerBlock;
use Australcode\Rewards\Domain\Wallet\ExpiryGateway;
use Australcode\Rewards\Domain\Wallet\LedgerAnalytics;
use Australcode\Rewards\Domain\Wallet\TransactionRepository;
use Australcode\Rewards\Hooks\RedemptionCartHook;
use Australcode\Rewards\Infrastructure\Campaign\CustomTableCampaignRepository;
use Australcode\Rewards\Infrastructure\Database\Migrations\M0001CreateCoreTables;
use Australcode\Rewards\Infrastructure\Database\Migrations\M0002CreateTiersTable;
use Australcode\Rewards\Infrastructure\Database\Migrations\M0003CreateCampaignsTable;
use Australcode\Rewards\Infrastructure\Database\Migrations\M0004CreateReferralsTable;
use Australcode\Rewards\Infrastructure\Database\Migrations\M0005RenameLegacyData;
use Australcode\Rewards\Infrastructure\Database\Migrations\M0006AddTierBadge;
use Australcode\Rewards\Infrastructure\Database\Migrations\M0007AddBalanceBlocked;
use Australcode\Rewards\Infrastructure\Database\Migrations\M0008CreateEarningRulesTable;
use Australcode\Rewards\Infrastructure\Database\Migrations\M0009CreateRedemptionRulesTable;
use Australcode\Rewards\Infrastructure\Database\Migrator;
use Australcode\Rewards\Infrastructure\Earning\MetaEarningOverrideResolver;
use Australcode\Rewards\Infrastructure\EarningRule\CustomTableEarningRuleRepository;
use Australcode\Rewards\Infrastructure\Licensing\FreemiusSdkGateway;
use Australcode\Rewards\Infrastructure\RedemptionRule\CustomTableRedemptionRuleRepository;
use Australcode\Rewards\Infrastructure\Logging\FileLogger;
use Australcode\Rewards\Infrastructure\Referral\CustomTableReferralRepository;
use Australcode\Rewards\Infrastructure\Settings\OptionsSettingsRepository;
use Australcode\Rewards\Infrastructure\Tier\CustomTableTierRepository;
use Australcode\Rewards\Infrastructure\Wallet\CustomTableBalanceDirectory;
use Australcode\Rewards\Infrastructure\Wallet\CustomTableBalanceRepository;
use Australcode\Rewards\Infrastructure\Wallet\CustomTableCustomerBlock;
use Australcode\Rewards\Infrastructure\Wallet\CustomTableExpiryGateway;
use Australcode\Rewards\Infrastructure\Wallet\CustomTableLedgerAnalytics;
use Australcode\Rewards\Infrastructure\Wallet\CustomTableTransactionRepository;
use Australcode\Rewards\Cli\ImportCommand;
use Australcode\Rewards\Service\CampaignService;
use Australcode\Rewards\Service\EarningCalculator;
use Australcode\Rewards\Service\EarningRuleEngine;
use Australcode\Rewards\Service\Import\YithImporter;
use Australcode\Rewards\Service\RedemptionRuleService;
use Australcode\Rewards\Service\Licensing\FeatureMap;
use Australcode\Rewards\Service\Licensing\FreemiusGateway;
use Australcode\Rewards\Service\Licensing\LicenseGate;
use Australcode\Rewards\Service\RedemptionService;
use Australcode\Rewards\Service\ReferralService;
use Australcode\Rewards\Service\TierService;
use Australcode\Rewards\Service\WalletService;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;

/**
 * Thin wrapper over League\Container.
 *
 * Strategy (mirrors the Perfect Image Resize canon):
 *  - Explicit factory closures for every shared binding. NOTE: calling
 *    `add($id)->setShared(true)` WITHOUT a factory makes League ignore the
 *    delegated ReflectionContainer and construct with zero args → ArgumentCountError.
 *    Always provide a closure for bindings with dependencies.
 *  - A delegated ReflectionContainer (cached) auto-wires everything else
 *    (e.g. hook classes, controllers) by constructor type-hints.
 *  - No singletons of services — only this container is long-lived per request.
 */
final class Container implements ContainerInterface {

	private LeagueContainer $container;

	public function __construct() {
		$this->container = new LeagueContainer();
		// Auto-wire fallback for unregistered classes (cached reflection).
		$this->container->delegate( new ReflectionContainer( true ) );
		$this->register_bindings();
	}

	private function register_bindings(): void {
		$this->container->add(
			LoggerInterface::class,
			static fn (): LoggerInterface => new FileLogger()
		)->setShared( true );

		$this->container->add(
			SettingsRepository::class,
			fn (): SettingsRepository => new OptionsSettingsRepository(
				$this->container->get( FeatureMap::class ),
				$this->container->get( LicenseGate::class )
			)
		)->setShared( true );

		$this->container->add(
			Migrator::class,
			static function (): Migrator {
				// M0002/M0003/M0004 create the Pro tier/campaign/referral tables and
				// are @fs_premium_only — absent from the free WP.org build. Guard each
				// so the free Migrator only registers the migrations whose class shipped
				// (order preserved: M0005 always runs last). class_exists on the literal
				// ::class never loads a missing file.
				$migrations = array( new M0001CreateCoreTables() );
				if ( class_exists( M0002CreateTiersTable::class ) ) {
					$migrations[] = new M0002CreateTiersTable();
				}
				if ( class_exists( M0003CreateCampaignsTable::class ) ) {
					$migrations[] = new M0003CreateCampaignsTable();
				}
				if ( class_exists( M0004CreateReferralsTable::class ) ) {
					$migrations[] = new M0004CreateReferralsTable();
				}
				$migrations[] = new M0005RenameLegacyData();
				if ( class_exists( M0006AddTierBadge::class ) ) {
					$migrations[] = new M0006AddTierBadge();
				}
				// M0007 (is_blocked on balances) is CORE — the earn/redeem guards read
				// the column in both builds — so it is registered unconditionally.
				$migrations[] = new M0007AddBalanceBlocked();
				// M0008 (earning rules table) is Pro/@fs_premium_only — guard like M0002-4/6.
				if ( class_exists( M0008CreateEarningRulesTable::class ) ) {
					$migrations[] = new M0008CreateEarningRulesTable();
				}
				// M0009 (redemption rules table) is Pro/@fs_premium_only.
				if ( class_exists( M0009CreateRedemptionRulesTable::class ) ) {
					$migrations[] = new M0009CreateRedemptionRulesTable();
				}
				return new Migrator( $migrations );
			}
		)->setShared( true );

		$this->container->add(
			BalanceRepository::class,
			static fn (): BalanceRepository => new CustomTableBalanceRepository()
		)->setShared( true );

		$this->container->add(
			BalanceDirectory::class,
			static fn (): BalanceDirectory => new CustomTableBalanceDirectory()
		)->setShared( true );

		// Customer block flag (B6) — CORE: written by the Pro tools, read by the
		// earn/redeem guards in both builds. Backed by the is_blocked column (M0007).
		$this->container->add(
			CustomerBlock::class,
			static fn (): CustomerBlock => new CustomTableCustomerBlock()
		)->setShared( true );

		$this->container->add(
			EarningOverrideResolver::class,
			static fn (): EarningOverrideResolver => new MetaEarningOverrideResolver()
		)->setShared( true );

		$this->container->add(
			TransactionRepository::class,
			static fn (): TransactionRepository => new CustomTableTransactionRepository()
		)->setShared( true );

		$this->container->add(
			ExpiryGateway::class,
			static fn (): ExpiryGateway => new CustomTableExpiryGateway()
		)->setShared( true );

		$this->container->add(
			LedgerAnalytics::class,
			static fn (): LedgerAnalytics => new CustomTableLedgerAnalytics()
		)->setShared( true );

		$this->container->add(
			WalletService::class,
			fn (): WalletService => new WalletService(
				$this->container->get( BalanceRepository::class ),
				$this->container->get( TransactionRepository::class )
			)
		)->setShared( true );

		$this->container->add(
			RedemptionService::class,
			fn (): RedemptionService => new RedemptionService(
				$this->container->get( WalletService::class ),
				$this->container->get( SettingsRepository::class ),
				$this->container->get( CustomerBlock::class )
			)
		)->setShared( true );

		$this->container->add(
			RedemptionCartHook::class,
			fn (): RedemptionCartHook => new RedemptionCartHook(
				$this->container->get( RedemptionService::class )
			)
		)->setShared( true );

		// Migration tooling (WP-CLI: `wp acrwd import yith`).
		$this->container->add(
			YithImporter::class,
			fn (): YithImporter => new YithImporter(
				$this->container->get( BalanceRepository::class ),
				$this->container->get( TransactionRepository::class )
			)
		)->setShared( true );

		$this->container->add(
			ImportCommand::class,
			fn (): ImportCommand => new ImportCommand(
				$this->container->get( YithImporter::class )
			)
		)->setShared( true );

		// Licensing — Freemius SDK behind the FreemiusGateway port (hub canon
		// licensing.md). The SDK owns activation/validation/crypto/grace-period;
		// the plugin keeps only the provider-agnostic gating layer.
		$this->container->add(
			FreemiusGateway::class,
			static fn (): FreemiusGateway => new FreemiusSdkGateway()
		)->setShared( true );

		$this->container->add(
			FeatureMap::class,
			static fn (): FeatureMap => new FeatureMap(
				array(
					'signup_points'            => 0,
					'daily_login_points'       => 0,
					'daily_login_streak_bonus' => 0,
					'first_order_points'       => 0,
					'review_points'            => 0,
					'points_expiration_months' => 0,
					'birthday_points'          => 0,
					'referrer_points'          => 0,
					'referee_points'           => 0,
					'profile_points'           => 0,
					'orders_milestone_points'  => 0,
					'cart_threshold_points'    => 0,
					'lifetime_spend_points'    => 0,
					'expiry_reminder_days'     => 0,
					'notify_points_updated'    => false,
				)
			)
		)->setShared( true );

		$this->container->add(
			LicenseGate::class,
			fn (): LicenseGate => new LicenseGate(
				$this->container->get( FreemiusGateway::class )
			)
		)->setShared( true );

		// Pro features (Tiers / Campaigns / Referrals) are @fs_premium_only: the
		// repo, service and Domain/Infrastructure trees are stripped from the free
		// WP.org build. Each binding is guarded by class_exists on the concrete
		// repository so the free container never references a missing class (the
		// ::class literals are compile-time strings — they never autoload). The
		// services self-register their earning hooks in Plugin::boot(), also guarded.

		// Tiers / VIP (Phase 5) — Pro.
		if ( class_exists( CustomTableTierRepository::class ) ) {
			$this->container->add(
				TierRepository::class,
				static fn (): TierRepository => new CustomTableTierRepository()
			)->setShared( true );

			$this->container->add(
				TierService::class,
				fn (): TierService => new TierService(
					$this->container->get( TierRepository::class ),
					$this->container->get( BalanceRepository::class ),
					$this->container->get( LicenseGate::class )
				)
			)->setShared( true );
		}

		// Campaigns — Pro.
		if ( class_exists( CustomTableCampaignRepository::class ) ) {
			$this->container->add(
				CampaignRepository::class,
				static fn (): CampaignRepository => new CustomTableCampaignRepository()
			)->setShared( true );

			$this->container->add(
				CampaignService::class,
				fn (): CampaignService => new CampaignService(
					$this->container->get( CampaignRepository::class ),
					$this->container->get( LicenseGate::class )
				)
			)->setShared( true );
		}

		// Referrals — Pro.
		if ( class_exists( CustomTableReferralRepository::class ) ) {
			$this->container->add(
				ReferralRepository::class,
				static fn (): ReferralRepository => new CustomTableReferralRepository()
			)->setShared( true );

			$this->container->add(
				ReferralService::class,
				fn (): ReferralService => new ReferralService(
					$this->container->get( ReferralRepository::class ),
					$this->container->get( WalletService::class ),
					$this->container->get( SettingsRepository::class )
				)
			)->setShared( true );
		}

		// Earning rules engine (B1) — Pro. The engine wires onto the
		// acrwd_earning_multiplier + acrwd_earning_bonus_points filters; the free build
		// drops it (no listener). EarningCalculator (core, auto-wired) powers the
		// dry-run preview.
		if ( class_exists( CustomTableEarningRuleRepository::class ) ) {
			$this->container->add(
				EarningRuleRepository::class,
				static fn (): EarningRuleRepository => new CustomTableEarningRuleRepository()
			)->setShared( true );

			$this->container->add(
				EarningRuleEngine::class,
				fn (): EarningRuleEngine => new EarningRuleEngine(
					$this->container->get( EarningRuleRepository::class ),
					$this->container->get( LicenseGate::class ),
					$this->container->get( EarningCalculator::class )
				)
			)->setShared( true );
		}

		// Advanced redemption (B7) — Pro. Rules mint WC coupons; the wallet-direct
		// redemption stays free. The free build drops these (the frontend renderer +
		// cron are stripped).
		if ( class_exists( CustomTableRedemptionRuleRepository::class ) ) {
			$this->container->add(
				RedemptionRuleRepository::class,
				static fn (): RedemptionRuleRepository => new CustomTableRedemptionRuleRepository()
			)->setShared( true );

			$this->container->add(
				RedemptionRuleService::class,
				fn (): RedemptionRuleService => new RedemptionRuleService(
					$this->container->get( WalletService::class ),
					$this->container->get( LicenseGate::class )
				)
			)->setShared( true );
		}
	}

	/**
	 * @template T of object
	 * @param class-string<T> $id
	 * @return T
	 */
	public function get( string $id ): mixed {
		return $this->container->get( $id );
	}

	public function has( string $id ): bool {
		return $this->container->has( $id );
	}
}
