using Baya.Application.Contracts.Analytics;
using Baya.Application.Contracts.Audit;
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Configuration;
using Baya.Application.Contracts.Holidays;
using Baya.Application.Contracts.Notifications;
using Baya.Application.Contracts.Payments;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Contracts.Search;
using Baya.Application.Contracts.SupportAlerts;
using Baya.Domain.Entities.Payments;
using Baya.Infrastructure.Persistence.Interceptors;
using Baya.Infrastructure.Persistence.Repositories.Common;
using Baya.Infrastructure.Persistence.Services.Analytics;
using Baya.Infrastructure.Persistence.Services.Audit;
using Baya.Infrastructure.Persistence.Services.Configuration;
using Baya.Infrastructure.Persistence.Services.Holidays;
using Baya.Infrastructure.Persistence.Services.Notifications;
using Baya.Infrastructure.Persistence.Services.Payments;
using Baya.Infrastructure.Persistence.Services.Scheduling;
using Baya.Infrastructure.Persistence.Services.Scheduling.Jobs;
using Baya.Infrastructure.Persistence.Services.Search;
using Baya.Infrastructure.Persistence.Services.Seeding;
using Baya.Infrastructure.Persistence.Services.SupportAlerts;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Baya.Infrastructure.Persistence.ServiceConfiguration;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddPersistenceServices(this IServiceCollection services,IConfiguration configuration)
    {
        services.AddScoped<IUnitOfWork, UnitOfWork>();

        services.AddScoped<AuditFieldInterceptor>();

        services.AddDbContext<ApplicationDbContext>((serviceProvider, options) =>
        {
            options
                .UseSqlServer(configuration.GetConnectionString("SqlServer"))
                .AddInterceptors(serviceProvider.GetRequiredService<AuditFieldInterceptor>());
        });

        // Platform-signal facades — DB-backed implementations of the Application contracts other domains
        // (b2…b15) depend on. Config/holiday lookups cache through ICacheService.
        services.AddScoped<IPlatformConfig, PlatformConfigService>();
        services.AddScoped<IHolidayCalendar, HolidayCalendarService>();
        services.AddScoped<IAnalyticsSink, AnalyticsSink>();
        services.AddScoped<IAuditLogger, AuditLogger>();
        services.AddScoped<INotificationService, NotificationService>();
        services.AddScoped<ISupportAlertService, SupportAlertService>();

        // Supersedes the b0 log/no-op stub with the real in-app notifications write.
        services.AddScoped<INotificationDispatcher, InAppNotificationDispatcher>();

        // "Has the nurse been paid?" — the refund pre-payout/clawback fork (b11). b13 now owns the authoritative
        // impl: a booking is paid iff a nurse_payout_booking_links row ties it to a `paid` nurse_payouts row. This
        // supersedes the interim NursePayoutStatusService (dispute-window derivation); the refund fork is unchanged.
        services.AddScoped<INursePayoutStatus, NursePayoutLinkStatusService>();

        // Unattended operation (refinement-phase-7). One in-process scheduler drives every IRecurringJob on its
        // own cadence — the two re-homed sweeps plus the previously admin-manual crons, each reading its seeded
        // cadence key. No new infra: SQL Server stays the only external dependency (Hangfire/Quartz + Redis are the
        // documented scale-out gate for >1 instance). Phase 8 adds the Moadian reconciliation job the same way.
        services.AddSingleton<IRecurringJob, BookingRequestExpiryJob>();
        services.AddSingleton<IRecurringJob, NotificationRetentionJob>();
        services.AddSingleton<IRecurringJob, CredentialExpiryScanJob>();
        services.AddSingleton<IRecurringJob, NoShowSweepJob>();
        services.AddSingleton<IRecurringJob, WeeklyPayoutGenerationJob>();
        // refinement-phase-8 (6.5): walk pending/submitted سامانه مودیان invoices toward their registered reference.
        services.AddSingleton<IRecurringJob, MoadianReconciliationJob>();
        // refinement-phase-9 (§9.4): two-tier retention sweep over the append-only audit_logs table.
        services.AddSingleton<IRecurringJob, AuditLogRetentionJob>();
        services.AddHostedService<RecurringJobSchedulerHostedService>();

        // Search (backend-phase-7). The index maintainer keeps nurse_search_index consistent inline inside
        // each source write's unit of work. The INurseSearch backend is config-selected — SQL is the real
        // MVP backend; a later ElasticNurseSearch drops in here with no caller change.
        services.AddScoped<ISearchIndexMaintainer, SearchIndexMaintainer>();

        var searchBackend = configuration["Search:Backend"];
        if (string.IsNullOrWhiteSpace(searchBackend) || searchBackend.Equals("sql", StringComparison.OrdinalIgnoreCase))
            services.AddScoped<INurseSearch, SqlNurseSearch>();
        else
            throw new NotSupportedException(
                $"Search backend '{searchBackend}' is not available — only 'sql' is implemented (Elasticsearch is deferred).");

        // Development-only demo marketplace seeder. Registered always (cheap), but invoked from Program.cs
        // only under IsDevelopment() — never against a production/staging DB.
        services.AddScoped<DemoWorldSeeder>();

        return services;
    }

    public static async Task ApplyMigrationsAsync(this WebApplication app)
    {
        await using var scope = app.Services.CreateAsyncScope();
        var context = scope.ServiceProvider.GetService<ApplicationDbContext>();

        if (context is null)
            throw new Exception("Database Context Not Found");

        await context.Database.MigrateAsync();
    }

    /// <summary>
    /// Boot-time schema <b>check</b> (refinement-phase-7) for deployed environments: applying migrations is a
    /// separate deploy step (the <c>migrate</c> one-shot / a CI <c>dotnet ef database update</c>), so concurrent
    /// multi-instance start-ups never race on DDL and the app login needs no permanent DDL rights. If any
    /// migration is pending, fail fast with a clear message rather than starting against a stale schema.
    /// </summary>
    public static async Task EnsureSchemaUpToDateAsync(this WebApplication app)
    {
        await using var scope = app.Services.CreateAsyncScope();
        var context = scope.ServiceProvider.GetService<ApplicationDbContext>()
                      ?? throw new Exception("Database Context Not Found");

        var pending = (await context.Database.GetPendingMigrationsAsync()).ToList();
        if (pending.Count > 0)
            throw new InvalidOperationException(
                $"Database schema is not up to date — {pending.Count} migration(s) pending: {string.Join(", ", pending)}. " +
                "Run the deploy-time migration step (`dotnet run -- migrate`, or `dotnet ef database update` in CI) before starting the API.");
    }

    /// <summary>
    /// Idempotently seeds one active <c>standard</c> payment gateway so the b10 card rail has a selectable
    /// provider out of the box. <c>config_json</c> is encrypted at rest by the EF converter on save (so it
    /// must go through the DbContext, not <c>HasData</c>). Real merchant credentials come from user-secrets /
    /// environment per deployment — this sandbox row is non-secret and only enables the local/dev flow.
    /// </summary>
    public static async Task SeedPaymentGatewaysAsync(this WebApplication app)
    {
        await using var scope = app.Services.CreateAsyncScope();
        var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();

        if (await context.Set<PaymentGateway>().AnyAsync(g => g.Type == PaymentGatewayType.Standard))
            return;

        context.Set<PaymentGateway>().Add(new PaymentGateway
        {
            ProviderCode = "zarinpal",
            Type = PaymentGatewayType.Standard,
            DisplayName = "ZarinPal (sandbox)",
            ConfigJson = "{\"merchantId\":\"00000000-0000-0000-0000-000000000000\",\"baseUrl\":\"https://sandbox.zarinpal.com\",\"sandbox\":true}",
            IsActive = true,
            Priority = 0
        });

        await context.SaveChangesAsync();
    }

    /// <summary>
    /// Seeds a coherent <b>Development-only</b> demo marketplace (verified/unverified nurses with variants +
    /// Tehran coverage, demo customers with patients + addresses) on top of the reference seeds, so every
    /// real-path discovery/search/booking screen has data. Idempotent — guarded on each persona's phone —
    /// and drives the search projection through the real maintainer. Callers must gate this on
    /// <c>IsDevelopment()</c>: seeding fake accounts into a real DB would be a trust/data-integrity disaster.
    /// </summary>
    public static async Task SeedDemoWorldAsync(this WebApplication app)
    {
        await using var scope = app.Services.CreateAsyncScope();
        var seeder = scope.ServiceProvider.GetRequiredService<DemoWorldSeeder>();
        await seeder.SeedAsync(CancellationToken.None);
    }
}