using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Invoices;
using Baya.Application.Contracts.Payments;
using Baya.Application.Contracts.Reviews;
using Baya.Infrastructure.CrossCutting.Seams;
using Baya.Infrastructure.CrossCutting.Seams.Real;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Baya.Infrastructure.CrossCutting.ServiceConfiguration;

public static class ServiceCollectionExtension
{
    /// <summary>
    /// Registers the cross-cutting + vendor-rail seams. Each rail is <b>config-selected</b> (refinement-phase-8):
    /// the deterministic mock is the default, and setting the rail's <c>Seams:*:Provider</c> to a real provider
    /// token swaps in the real HTTP adapter behind the same Application contract — <b>callers never change</b>. An
    /// unconfigured/typo'd provider falls closed to the mock. This makes a partial rollout the normal case (real SMS
    /// + real geocoder while payments stay mocked in a pre-launch environment). Real adapters read credentials from
    /// <c>Seams:*</c> (user-secrets/environment) and get an <see cref="System.Net.Http.HttpClient"/> from the
    /// <c>IHttpClientFactory</c>. (The real in-app <c>INotificationDispatcher</c> needs the database, so it is
    /// registered in the Persistence layer.)
    /// </summary>
    public static IServiceCollection AddCrossCuttingSeams(this IServiceCollection services, IConfiguration configuration)
    {
        services.Configure<SeamOptions>(configuration.GetSection(SeamOptions.SectionName));
        var seams = configuration.GetSection(SeamOptions.SectionName).Get<SeamOptions>() ?? new SeamOptions();

        services.AddHttpClient();
        services.AddMemoryCache();

        services.AddSingleton<IDateTimeProvider, SystemDateTimeProvider>();
        services.AddSingleton<IFieldEncryptor, SymmetricFieldEncryptor>();
        services.AddSingleton<ICacheService, MemoryCacheService>();

        RegisterObjectStorage(services, seams);
        RegisterSms(services, seams);
        RegisterTrustRails(services, seams);
        RegisterGeocoder(services, seams);
        RegisterPaymentRails(services, seams);
        RegisterBnpl(services, seams);
        RegisterPayoutRail(services, seams);
        RegisterMoadian(services, seams);

        // Payment-capture trigger (backend-phase-9). refinement-phase-8 (6.4): the real card capture (b10) supersedes
        // it, so production registers the fail-closed DisabledPaymentCaptureSimulator (the dev-only bookings/convert
        // endpoint fabricates nothing in a deployed env); Development/Testing re-register the real MockPaymentCaptureSimulator
        // over this via AddDevelopmentPaymentCapture (last registration wins).
        services.AddSingleton<IPaymentCaptureSimulator, DisabledPaymentCaptureSimulator>();

        // Non-vendor mocks that stay as-is (real behaviour is out of this phase's scope / manual is the intended MVP).
        services.AddSingleton<ICredentialVerifier, MockCredentialVerifier>();              // 5.6 manual = intended MVP
        services.AddSingleton<IReviewModerationService, MockReviewModerationService>();
        services.AddSingleton<ILicenseVerificationService, MockLicenseVerificationService>(); // 5.6 manual = intended MVP

        return services;
    }

    // ---- object storage (5.5) --------------------------------------------------------------------------------

    private static void RegisterObjectStorage(IServiceCollection services, SeamOptions seams)
    {
        if (Is(seams.ObjectStorage.Provider, SeamProviders.S3))
        {
            services.AddHttpClient(HttpClients.ObjectStorage);
            services.AddSingleton<IObjectStorage>(sp => new S3ObjectStorage(
                Client(sp, HttpClients.ObjectStorage),
                sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SeamOptions>>()));
        }
        else
        {
            services.AddSingleton<IObjectStorage, LocalDiskObjectStorage>();
        }
    }

    // ---- SMS (5.1, launch-critical) --------------------------------------------------------------------------

    private static void RegisterSms(IServiceCollection services, SeamOptions seams)
    {
        var provider = seams.Sms.Provider;
        if (Is(provider, SeamProviders.Kavenegar))
        {
            services.AddHttpClient(HttpClients.Sms, c => c.BaseAddress = new Uri(BaseOrDefault(seams.Sms.BaseUrl, "https://api.kavenegar.com/")));
            services.AddSingleton<ISmsSender>(sp => new KavenegarSmsSender(
                Client(sp, HttpClients.Sms),
                sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SeamOptions>>(),
                sp.GetRequiredService<ILogger<KavenegarSmsSender>>()));
        }
        else if (Is(provider, SeamProviders.SmsIr) || Is(provider, SeamProviders.Ghasedak))
        {
            throw new NotSupportedException(
                $"SMS provider '{provider}' is not implemented — only 'kavenegar' has a real adapter (refinement-phase-8). " +
                "Add its adapter behind ISmsSender, or use 'mock'/'kavenegar'.");
        }
        else
        {
            // b2 log-only mock; the Development OTP-capture decorator is layered on in Program.cs (Development only,
            // and only while this mock is selected — a real SMS provider disables it so the OTP is never logged).
            services.AddSingleton<ISmsSender, LoggingSmsSender>();
        }
    }

    // ---- trust & identity (5.2, 5.3) — Finnotech-class KYC bridge --------------------------------------------

    private static void RegisterTrustRails(IServiceCollection services, SeamOptions seams)
    {
        var anyFinnotech =
            Is(seams.Shahkar.Provider, SeamProviders.Finnotech) ||
            Is(seams.IdentityKyc.Provider, SeamProviders.Finnotech) ||
            Is(seams.BankOwnership.Provider, SeamProviders.Finnotech);

        if (anyFinnotech)
        {
            services.AddHttpClient(HttpClients.Finnotech);
            services.AddSingleton(sp => new FinnotechClient(
                Client(sp, HttpClients.Finnotech),
                sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SeamOptions>>()));
        }

        if (Is(seams.Shahkar.Provider, SeamProviders.Finnotech))
            services.AddSingleton<IShahkarVerifier>(sp => new FinnotechShahkarVerifier(sp.GetRequiredService<FinnotechClient>()));
        else
            services.AddSingleton<IShahkarVerifier, MockShahkarVerifier>();

        if (Is(seams.IdentityKyc.Provider, SeamProviders.Finnotech))
            services.AddSingleton<IIdentityKycProvider>(sp => new FinnotechIdentityKycProvider(sp.GetRequiredService<FinnotechClient>()));
        else
            services.AddSingleton<IIdentityKycProvider, MockIdentityKycProvider>();

        if (Is(seams.BankOwnership.Provider, SeamProviders.Finnotech))
            services.AddSingleton<IBankAccountOwnershipVerifier>(sp => new FinnotechBankAccountOwnershipVerifier(sp.GetRequiredService<FinnotechClient>()));
        else
            services.AddSingleton<IBankAccountOwnershipVerifier, MockBankAccountOwnershipVerifier>();
    }

    // ---- geocoding (5.4) -------------------------------------------------------------------------------------

    private static void RegisterGeocoder(IServiceCollection services, SeamOptions seams)
    {
        if (Is(seams.Geocoding.Provider, SeamProviders.Neshan))
        {
            services.AddHttpClient(HttpClients.Geocoding);
            services.AddSingleton<IGeocoder>(sp => new NeshanGeocoder(
                Client(sp, HttpClients.Geocoding),
                sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SeamOptions>>(),
                sp.GetRequiredService<ILogger<NeshanGeocoder>>()));
        }
        else
        {
            services.AddSingleton<IGeocoder, MockGeocoder>();
        }
    }

    // ---- card PSP + webhook signature + تسهیم split (6.1) ----------------------------------------------------

    private static void RegisterPaymentRails(IServiceCollection services, SeamOptions seams)
    {
        var real = !Is(seams.Payments.Provider, SeamProviders.Mock) && !string.IsNullOrWhiteSpace(seams.Payments.Provider);

        if (real)
        {
            services.AddHttpClient(HttpClients.Psp);
            services.AddSingleton<IPaymentProvider>(sp => new ZarinPalPaymentProvider(
                Client(sp, HttpClients.Psp),
                sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SeamOptions>>(),
                sp.GetRequiredService<ILogger<ZarinPalPaymentProvider>>()));
            services.AddSingleton<ISettlementSplitProvider>(sp => new ProviderSettlementSplitProvider(
                Client(sp, HttpClients.Psp),
                sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SeamOptions>>(),
                sp.GetRequiredService<ILogger<ProviderSettlementSplitProvider>>()));
            // Per-provider HMAC over the raw callback body — never trust a callback alone (the confirm path still
            // re-verifies server-side). Shared by the PSP + BNPL + payout-reconciliation callbacks.
            services.AddSingleton<IWebhookVerifier, HmacWebhookVerifier>();
        }
        else
        {
            services.AddSingleton<IPaymentProvider, MockPaymentProvider>();
            services.AddSingleton<ISettlementSplitProvider, MockSettlementSplitProvider>();
            services.AddSingleton<IWebhookVerifier, MockWebhookVerifier>();
        }

        // Money-path mutex — in-proc today (the DB uniques/state-machine are the authoritative backstop);
        // Redis-backed for >1 instance. Unchanged by the vendor swap.
        services.AddSingleton<IDistributedLock, InProcessDistributedLock>();
    }

    // ---- BNPL (6.2) ------------------------------------------------------------------------------------------

    private static void RegisterBnpl(IServiceCollection services, SeamOptions seams)
    {
        // The in-house net-of-fee model is always available — it stands in for the `balinyaar` provider_code even
        // in real mode (no external API), and is the mock for every code in mock mode.
        services.AddSingleton<MockBnplProvider>();
        services.AddSingleton<ICurrencyNormalizer, MockCurrencyNormalizer>(); // config-driven multiplier = the real impl

        if (string.Equals(seams.Bnpl.Provider, "real", StringComparison.OrdinalIgnoreCase))
        {
            services.AddHttpClient(HttpClients.BnplSnappPay);
            services.AddHttpClient(HttpClients.BnplDigipay);

            services.AddSingleton(sp => new SnappPayBnplProvider(
                Client(sp, HttpClients.BnplSnappPay),
                sp.GetRequiredService<ICurrencyNormalizer>(),
                Connection(seams, Baya.Domain.Entities.Bnpl.BnplProviderCodes.SnappPay),
                seams.Bnpl.WireCurrency,
                sp.GetRequiredService<ILogger<SnappPayBnplProvider>>()));
            services.AddSingleton(sp => new DigipayBnplProvider(
                Client(sp, HttpClients.BnplDigipay),
                sp.GetRequiredService<ICurrencyNormalizer>(),
                Connection(seams, Baya.Domain.Entities.Bnpl.BnplProviderCodes.Digipay),
                seams.Bnpl.WireCurrency,
                sp.GetRequiredService<ILogger<DigipayBnplProvider>>()));

            services.AddSingleton<IBnplProviderResolver, ConfiguredBnplProviderResolver>();
            // The b11 refund `bnpl_revert` path injects IBnplProvider directly (not per-code); SnappPay is the
            // default revert provider. Per-code revert resolution through the resolver is a documented follow-up.
            services.AddSingleton<IBnplProvider>(sp => sp.GetRequiredService<SnappPayBnplProvider>());
        }
        else
        {
            services.AddSingleton<IBnplProvider>(sp => sp.GetRequiredService<MockBnplProvider>());
            services.AddSingleton<IBnplProviderResolver, MockBnplProviderResolver>();
        }
    }

    // ---- PAYA/SATNA payout rail (6.3) ------------------------------------------------------------------------

    private static void RegisterPayoutRail(IServiceCollection services, SeamOptions seams)
    {
        if (Is(seams.BankTransfer.Provider, SeamProviders.Jibit))
        {
            services.AddHttpClient(HttpClients.BankTransfer);
            services.AddSingleton<IBankTransferProvider>(sp => new JibitBankTransferProvider(
                Client(sp, HttpClients.BankTransfer),
                sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SeamOptions>>(),
                sp.GetRequiredService<ILogger<JibitBankTransferProvider>>()));
        }
        else
        {
            services.AddSingleton<IBankTransferProvider, MockBankTransferProvider>();
        }
    }

    // ---- Moadian e-invoicing (6.5) ---------------------------------------------------------------------------

    private static void RegisterMoadian(IServiceCollection services, SeamOptions seams)
    {
        if (Is(seams.Moadian.Provider, SeamProviders.Moadian))
        {
            services.AddHttpClient(HttpClients.Moadian);
            services.AddSingleton<IMoadianClient>(sp => new MoadianClient(
                Client(sp, HttpClients.Moadian),
                sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SeamOptions>>(),
                sp.GetRequiredService<ILogger<MoadianClient>>()));
        }
        else
        {
            services.AddSingleton<IMoadianClient, MockMoadianClient>();
        }
    }

    // ---- helpers ---------------------------------------------------------------------------------------------

    private static bool Is(string? configured, string token)
        => string.Equals(configured, token, StringComparison.OrdinalIgnoreCase);

    private static System.Net.Http.HttpClient Client(IServiceProvider sp, string name)
        => sp.GetRequiredService<System.Net.Http.IHttpClientFactory>().CreateClient(name);

    private static string BaseOrDefault(string configured, string fallback)
        => string.IsNullOrWhiteSpace(configured) ? fallback : configured;

    private static BnplProviderConnection Connection(SeamOptions seams, string code)
        => seams.Bnpl.Providers.TryGetValue(code, out var connection) ? connection : new BnplProviderConnection();

    private static class HttpClients
    {
        public const string ObjectStorage = "seam-object-storage";
        public const string Sms = "seam-sms";
        public const string Finnotech = "seam-finnotech";
        public const string Geocoding = "seam-geocoding";
        public const string Psp = "seam-psp";
        public const string BnplSnappPay = "seam-bnpl-snapppay";
        public const string BnplDigipay = "seam-bnpl-digipay";
        public const string BankTransfer = "seam-bank-transfer";
        public const string Moadian = "seam-moadian";
    }
}
