using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Infrastructure.Data;
using PowderCoating.Shared.Constants;

namespace PowderCoating.Web.BackgroundServices;

/// <summary>
/// Runs once per day and:
/// 1. Advances Active → GracePeriod and GracePeriod → Expired based on SubscriptionEndDate.
/// 2. Sends reminder emails at 14, 7, 3, and 1 day(s) before expiry.
///
/// Skips companies that are IsComped or managed by Stripe (StripeSubscriptionId set).
/// </summary>
public class SubscriptionExpiryBackgroundService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<SubscriptionExpiryBackgroundService> _logger;

    // Run at 8:00 AM server local time every day.
    private static readonly TimeOnly RunTime = new(8, 0);

    // Days-before-expiry at which reminder emails are sent.
    private static readonly int[] ReminderDays = [14, 7, 3, 1];

    /// <summary>
    /// Initializes the service with a scope factory and logger.
    /// <see cref="IServiceScopeFactory"/> is required because <see cref="BackgroundService"/> is a singleton
    /// while the services it consumes (DbContext, IEmailService) are scoped.
    /// </summary>
    public SubscriptionExpiryBackgroundService(
        IServiceScopeFactory scopeFactory,
        ILogger<SubscriptionExpiryBackgroundService> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    /// <summary>
    /// Long-running loop that wakes once per day at <see cref="RunTime"/> (8:00 AM local) and runs
    /// the expiry check. Scheduled 1 hour before the payment reminder service (9 AM) so that
    /// subscription state transitions are committed before reminder emails reference subscription status.
    /// </summary>
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("SubscriptionExpiryBackgroundService started.");

        while (!stoppingToken.IsCancellationRequested)
        {
            var delay = GetDelayUntilNextRun();
            _logger.LogDebug("Subscription expiry service sleeping {Delay} until next run.", delay);

            try { await Task.Delay(delay, stoppingToken); }
            catch (OperationCanceledException) { break; }

            if (stoppingToken.IsCancellationRequested) break;

            await RunAsync(stoppingToken);
        }

        _logger.LogInformation("SubscriptionExpiryBackgroundService stopped.");
    }

    /// <summary>
    /// Opens a DI scope, queries non-Stripe-managed (trial) companies with active or grace-period
    /// subscriptions, and calls <see cref="ProcessCompanyAsync"/> for each. Each company is saved
    /// individually so a single failure does not prevent other companies from being updated.
    /// </summary>
    private async Task RunAsync(CancellationToken ct)
    {
        _logger.LogInformation("Running daily subscription expiry check.");
        try
        {
            using var scope = _scopeFactory.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
            var emailService = scope.ServiceProvider.GetRequiredService<IEmailService>();
            var adminNotification = scope.ServiceProvider.GetRequiredService<IAdminNotificationService>();
            var platformSettings = scope.ServiceProvider.GetRequiredService<IPlatformSettingsService>();
            var gracePeriodDays = await platformSettings.GetIntAsync(
                PlatformSettingKeys.GracePeriodDays,
                AppConstants.SubscriptionConstants.GracePeriodDays);
            var gracePeriodAppliesToTrials = await platformSettings.GetBoolAsync(
                PlatformSettingKeys.GracePeriodAppliesToTrials, defaultValue: false);

            var today = DateTime.UtcNow.Date;

            // Load all non-comped, non-Stripe-managed companies that might need attention.
            var companies = await db.Companies
                .IgnoreQueryFilters()
                .Where(c =>
                    !c.IsDeleted &&
                    !c.IsComped &&
                    string.IsNullOrEmpty(c.StripeSubscriptionId) &&
                    c.SubscriptionEndDate.HasValue &&
                    (c.SubscriptionStatus == SubscriptionStatus.Active ||
                     c.SubscriptionStatus == SubscriptionStatus.GracePeriod))
                .ToListAsync(ct);

            _logger.LogDebug("Found {Count} companies to evaluate.", companies.Count);

            // All companies reaching this point have no StripeSubscriptionId — they are trials.
            // Paid subscribers are managed by Stripe and filtered out above.
            var effectiveGraceDays = gracePeriodAppliesToTrials ? gracePeriodDays : 0;

            foreach (var company in companies)
            {
                if (ct.IsCancellationRequested) break;
                try
                {
                    await ProcessCompanyAsync(db, emailService, adminNotification, company, today, effectiveGraceDays, ct);
                    await db.SaveChangesAsync(ct);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex,
                        "Failed to process subscription expiry for company {Id} ({Name}). Status change was not persisted.",
                        company.Id, company.CompanyName);
                    // Clear EF tracked changes so bad state does not bleed into the next company.
                    db.ChangeTracker.Clear();
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Subscription expiry run failed.");
        }
    }

    /// <summary>
    /// Evaluates a single company and performs any required status transitions or reminder sends.
    /// Transition logic:
    /// <list type="bullet">
    ///   <item>Active past end date, grace days = 0 → Expired + deactivated immediately (trials).</item>
    ///   <item>Active past end date, grace days &gt; 0 → GracePeriod + grace-period email.</item>
    ///   <item>GracePeriod past grace deadline → Expired + deactivated.</item>
    /// </list>
    /// Reminder emails at <see cref="ReminderDays"/> offsets are sent only while the company is still Active.
    /// Platform admin is notified asynchronously (fire-and-forget) so that operator action can be taken
    /// without delaying the main processing loop.
    /// </summary>
    private async Task ProcessCompanyAsync(
        ApplicationDbContext db,
        IEmailService emailService,
        IAdminNotificationService adminNotification,
        PowderCoating.Core.Entities.Company company,
        DateTime today,
        int gracePeriodDays,
        CancellationToken ct)
    {
        var endDate = company.SubscriptionEndDate!.Value.Date;
        var expiredDate = endDate.AddDays(gracePeriodDays);

        // ── Status transitions ────────────────────────────────────────────
        if (company.SubscriptionStatus == SubscriptionStatus.GracePeriod && today >= expiredDate)
        {
            _logger.LogInformation(
                "Company {Id} ({Name}) grace period ended. Marking Expired and deactivating.",
                company.Id, company.CompanyName);

            company.SubscriptionStatus = SubscriptionStatus.Expired;
            company.IsActive = false;
            company.UpdatedAt = DateTime.UtcNow;
            company.UpdatedBy = "System";

            await WriteAuditLogAsync(db, company,
                $"Auto-expired: grace period ended {gracePeriodDays} days after subscription end {endDate:d}.");

            _ = adminNotification.NotifyCompanyExpiredAsync(
                company.Id, company.CompanyName,
                company.PrimaryContactEmail ?? string.Empty, expiredDate);
        }
        else if (company.SubscriptionStatus == SubscriptionStatus.Active && today > endDate)
        {
            if (gracePeriodDays == 0)
            {
                // No grace period configured — expire immediately without going through GracePeriod.
                // Trials always land here since gracePeriodAppliesToTrials defaults to false.
                _logger.LogInformation(
                    "Company {Id} ({Name}) subscription ended with no grace period. Marking Expired and deactivating.",
                    company.Id, company.CompanyName);

                company.SubscriptionStatus = SubscriptionStatus.Expired;
                company.IsActive = false;
                company.UpdatedAt = DateTime.UtcNow;
                company.UpdatedBy = "System";

                await WriteAuditLogAsync(db, company,
                    $"Auto-expired: subscription ended {endDate:d} with no grace period.");

                _ = adminNotification.NotifyCompanyExpiredAsync(
                    company.Id, company.CompanyName,
                    company.PrimaryContactEmail ?? string.Empty, endDate);
            }
            else
            {
                _logger.LogInformation(
                    "Company {Id} ({Name}) subscription ended. Entering {Days}-day grace period.",
                    company.Id, company.CompanyName, gracePeriodDays);

                company.SubscriptionStatus = SubscriptionStatus.GracePeriod;
                company.UpdatedAt = DateTime.UtcNow;
                company.UpdatedBy = "System";

                await WriteAuditLogAsync(db, company,
                    $"Auto-moved to GracePeriod: subscription ended {endDate:d}. Grace period expires {expiredDate:d}.");

                await SendEmailIfNotSentAsync(db, emailService, company, today,
                    NotificationType.SubscriptionExpiryReminder,
                    daysBeforeExpiry: 0,
                    gracePeriodDays,
                    ct);

                _ = adminNotification.NotifyCompanyGracePeriodAsync(
                    company.Id, company.CompanyName,
                    company.PrimaryContactEmail ?? string.Empty, expiredDate);
            }
        }

        // ── Reminder emails (only while still Active) ────────────────────
        if (company.SubscriptionStatus == SubscriptionStatus.Active)
        {
            var daysUntilExpiry = (endDate - today).Days;
            if (ReminderDays.Contains(daysUntilExpiry))
            {
                await SendEmailIfNotSentAsync(db, emailService, company, today,
                    NotificationType.SubscriptionExpiryReminder,
                    daysBeforeExpiry: daysUntilExpiry,
                    gracePeriodDays,
                    ct);
            }
        }
    }

    /// <summary>
    /// Sends a subscription expiry reminder or grace-period email to the company's primary contact,
    /// then records the send in <c>NotificationLogs</c> (both on success and failure).
    /// Deduplication is achieved by checking the log for the same type, company, and day+subject-tag
    /// before sending, which prevents duplicate emails if the service restarts mid-run.
    /// <paramref name="daysBeforeExpiry"/> = 0 signals a grace-period notice rather than a pre-expiry reminder.
    /// </summary>
    private static async Task SendEmailIfNotSentAsync(
        ApplicationDbContext db,
        IEmailService emailService,
        PowderCoating.Core.Entities.Company company,
        DateTime today,
        NotificationType notificationType,
        int daysBeforeExpiry,
        int gracePeriodDays,
        CancellationToken ct)
    {
        if (string.IsNullOrEmpty(company.PrimaryContactEmail)) return;

        // Prevent duplicate sends — check if we already sent this type today for this company
        var alreadySent = await db.NotificationLogs
            .IgnoreQueryFilters()
            .AsNoTracking()
            .AnyAsync(l =>
                l.CompanyId == company.Id &&
                !l.IsDeleted &&
                l.NotificationType == notificationType &&
                l.SentAt >= today &&
                l.SentAt < today.AddDays(1) &&
                l.Status == NotificationStatus.Sent &&
                l.Subject != null && l.Subject.Contains($"[{daysBeforeExpiry}d]"),
            ct);

        if (alreadySent) return;

        var (subject, plain, html) = BuildEmail(company, daysBeforeExpiry, gracePeriodDays);

        var (success, error) = await emailService.SendEmailAsync(
            company.PrimaryContactEmail,
            company.CompanyName,
            subject,
            plain,
            html);

        db.NotificationLogs.Add(new NotificationLog
        {
            CompanyId = company.Id,
            Channel = NotificationChannel.Email,
            NotificationType = notificationType,
            Status = success ? NotificationStatus.Sent : NotificationStatus.Failed,
            RecipientName = company.CompanyName,
            Recipient = company.PrimaryContactEmail,
            Subject = subject,
            Message = plain,
            ErrorMessage = error,
            SentAt = DateTime.UtcNow
        });
    }

    /// <summary>
    /// Constructs the subject line and both plain-text and HTML email bodies for the reminder or
    /// grace-period notice. The subject embeds a "[Nd]" tag (e.g., "[7d]", "[0d]") which the
    /// deduplication query in <see cref="SendEmailIfNotSentAsync"/> uses as a secondary key to
    /// distinguish emails sent on different days within the same calendar day.
    /// </summary>
    private static (string subject, string plain, string html) BuildEmail(
        PowderCoating.Core.Entities.Company company,
        int daysBeforeExpiry,
        int gracePeriodDays)
    {
        var endDate = company.SubscriptionEndDate!.Value.Date;

        if (daysBeforeExpiry == 0)
        {
            // Grace period notice
            var graceDays = gracePeriodDays;
            var expiredOn = endDate.AddDays(graceDays).ToString("MMMM d, yyyy");
            var subject = $"[0d] Your Powder Coating Logix subscription has expired";
            var plain = $"Hi {company.CompanyName},\n\n" +
                        $"Your Powder Coating Logix subscription expired on {endDate:MMMM d, yyyy}. " +
                        $"You have a {graceDays}-day grace period — your account will be fully suspended on {expiredOn} if not renewed.\n\n" +
                        $"Log in to renew your subscription and keep your shop running.\n\nThank you,\nThe Powder Coating Logix Team";
            var html = $"<p>Hi <strong>{company.CompanyName}</strong>,</p>" +
                       $"<p>Your Powder Coating Logix subscription expired on <strong>{endDate:MMMM d, yyyy}</strong>.</p>" +
                       $"<p>You have a <strong>{graceDays}-day grace period</strong>. Your account will be fully suspended on <strong>{expiredOn}</strong> if not renewed.</p>" +
                       $"<p>Log in to renew your subscription and keep your shop running.</p>" +
                       $"<p>Thank you,<br>The Powder Coating Logix Team</p>";
            return (subject, plain, html);
        }
        else
        {
            var expiresOn = endDate.ToString("MMMM d, yyyy");
            var subject = $"[{daysBeforeExpiry}d] Your Powder Coating Logix subscription expires in {daysBeforeExpiry} day{(daysBeforeExpiry == 1 ? "" : "s")}";
            var plain = $"Hi {company.CompanyName},\n\n" +
                        $"This is a reminder that your Powder Coating Logix subscription expires on {expiresOn} " +
                        $"({daysBeforeExpiry} day{(daysBeforeExpiry == 1 ? "" : "s")} from today).\n\n" +
                        $"Log in to renew your subscription before it expires.\n\nThank you,\nThe Powder Coating Logix Team";
            var html = $"<p>Hi <strong>{company.CompanyName}</strong>,</p>" +
                       $"<p>Your Powder Coating Logix subscription expires on <strong>{expiresOn}</strong> " +
                       $"({daysBeforeExpiry} day{(daysBeforeExpiry == 1 ? "" : "s")} from today).</p>" +
                       $"<p>Log in to renew your subscription before it expires.</p>" +
                       $"<p>Thank you,<br>The Powder Coating Logix Team</p>";
            return (subject, plain, html);
        }
    }

    /// <summary>
    /// Writes a system-generated audit log entry recording the subscription status transition.
    /// Uses raw SQL rather than the EF change tracker because <c>AuditLog</c> does not inherit
    /// <c>BaseEntity</c> and its <c>Timestamp</c> column is non-nullable without a default;
    /// inserting via raw SQL also avoids accidentally triggering any EF interceptors that assume
    /// a user context is present.
    /// </summary>
    private static async Task WriteAuditLogAsync(
        ApplicationDbContext db,
        PowderCoating.Core.Entities.Company company,
        string message)
    {
        await db.Database.ExecuteSqlRawAsync("""
            INSERT INTO AuditLogs (UserId, UserName, CompanyId, CompanyName, Action, EntityType, EntityId,
                EntityDescription, OldValues, NewValues, IpAddress, Timestamp)
            VALUES ({0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11})
            """,
            (object?)null,
            "System",
            (object?)null,
            (object?)null,
            "Updated",
            "Company",
            company.Id.ToString(),
            company.CompanyName,
            (object?)null,
            message,
            (object?)null,
            DateTime.UtcNow);
    }

    /// <summary>
    /// Calculates the delay until the next scheduled run at <see cref="RunTime"/> (8:00 AM local).
    /// Uses local time so the check fires during business hours on the server's clock.
    /// If 8 AM has already passed today, returns the delay to 8 AM tomorrow.
    /// </summary>
    private static TimeSpan GetDelayUntilNextRun()
    {
        var now = DateTime.Now;
        var nextRun = now.Date.Add(RunTime.ToTimeSpan());
        if (nextRun <= now) nextRun = nextRun.AddDays(1);
        return nextRun - now;
    }
}
