using System.Diagnostics;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;

public class PromotionService : IPromotionService
{
    private readonly AppDbContext _db;
    private readonly IIdempotencyService _idempotency;
    private readonly IMrnGenerator _mrnGenerator;
    private readonly ILogger<PromotionService> _logger;

    public PromotionService(
        AppDbContext db,
        IIdempotencyService idempotency,
        IMrnGenerator mrnGenerator,
        ILogger<PromotionService> logger)
    {
        _db = db;
        _idempotency = idempotency;
        _mrnGenerator = mrnGenerator;
        _logger = logger;
    }

    public async Task<PromotionResultResponse> ApproveAndPromoteAsync(
        Guid batchId, Guid approverUserId, bool enableRetroactiveAlerts, string? idempotencyKey)
    {
        // --- Idempotency check (before transaction) ---
        if (!string.IsNullOrWhiteSpace(idempotencyKey))
        {
            var existing = await _idempotency.GetExistingAsync(idempotencyKey, "batch_promote");
            if (existing is not null)
            {
                _logger.LogInformation(
                    "Idempotent replay for batch {BatchId} with key {Key}",
                    batchId, idempotencyKey);

                return JsonSerializer.Deserialize<PromotionResultResponse>(
                    existing.ResponseBodyJson,
                    new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
            }
        }

        // --- Start timing the promotion ---
        var stopwatch = Stopwatch.StartNew();

        try
        {
            // --- Load batch with all draft data ---
            var batch = await _db.DigitizationBatches
                .Include(b => b.DraftPatient)
                .Include(b => b.DraftEncounter)
                .Include(b => b.DraftObservations)
                .FirstOrDefaultAsync(b => b.Id == batchId);

            if (batch is null)
                throw new NotFoundException("Batch not found.", "BATCH_NOT_FOUND");

            // --- Status validation ---
            if (batch.Status != BatchStatus.Verified && batch.Status != BatchStatus.AwaitingClinicalApproval)
                throw new ConflictException(
                    $"Batch must be in 'verified' or 'awaiting_clinical_approval' status to approve. Current status: '{batch.Status.ToDbString()}'.",
                    "ILLEGAL_STATUS_TRANSITION");

            // --- Separation of duties: approver cannot be the entry clerk ---
            if (batch.EnteredByUserId == approverUserId)
                throw new ConflictException(
                    "Separation of duties: the entry clerk cannot approve their own batch.",
                    "SEPARATION_OF_DUTIES_VIOLATION");

            // --- Also cannot be the verifier ---
            if (batch.VerifiedByUserId == approverUserId)
                throw new ConflictException(
                    "Separation of duties: the verifier cannot also approve the same batch.",
                    "SEPARATION_OF_DUTIES_VIOLATION");

            // --- Validate draft data completeness ---
            ValidateDraftDataForPromotion(batch);

            // --- Begin atomic transaction ---
            await using var transaction = await _db.Database.BeginTransactionAsync();

            try
            {
                var now = DateTimeOffset.UtcNow;

                batch.ApprovedByUserId = approverUserId;

                var core = await ExecutePromotionCoreAsync(
                    batch, approverUserId, enableRetroactiveAlerts, now);

                // === Store idempotency record (within same transaction) ===
                var result = new PromotionResultResponse(
                    BatchId: batchId,
                    Status: BatchStatus.Promoted.ToDbString(),
                    PatientId: core.Patient.Id,
                    Mrn: core.Patient.Mrn,
                    EncounterId: core.Encounter.Id,
                    ObservationIds: core.ObservationIds,
                    PromotedAt: now,
                    OutboxEventsWritten: core.OutboxCount
                );

                if (!string.IsNullOrWhiteSpace(idempotencyKey))
                {
                    await _idempotency.SaveAsync(
                        idempotencyKey, "batch_promote", batchId,
                        200, result, TimeSpan.FromHours(24));
                }

                // === Commit ===
                await _db.SaveChangesAsync();
                await transaction.CommitAsync();

                stopwatch.Stop();
                DiagnosticsMetrics.PromotionDuration.Observe(stopwatch.Elapsed.TotalSeconds);

                _logger.LogInformation(
                    "Batch {BatchId} promoted in {ElapsedMs}ms: Patient {PatientId} (MRN {Mrn}), " +
                    "Encounter {EncounterId}, {ObsCount} observations, {OutboxCount} outbox events",
                    batchId, stopwatch.ElapsedMilliseconds, core.Patient.Id, core.Patient.Mrn,
                    core.Encounter.Id, core.ObservationIds.Length, core.OutboxCount);

                return result;
            }
            catch
            {
                await transaction.RollbackAsync();
                throw;
            }
        }
        catch
        {
            stopwatch.Stop();
            throw;
        }
    }

    public async Task<PromotionResultResponse> GetPromotionResultAsync(Guid batchId)
    {
        var batch = await _db.DigitizationBatches
            .FirstOrDefaultAsync(b => b.Id == batchId);

        if (batch is null)
            throw new NotFoundException("Batch not found.", "BATCH_NOT_FOUND");

        if (batch.Status != BatchStatus.Promoted)
            throw new ConflictException(
                $"Batch is not promoted. Current status: '{batch.Status.ToDbString()}'.",
                "BATCH_NOT_PROMOTED");

        // Retrieve the live patient by looking up the encounter
        var encounter = await _db.Encounters
            .Include(e => e.Patient)
            .FirstOrDefaultAsync(e => e.Id == batch.PromotionEncounterId);

        if (encounter is null)
            throw new NotFoundException(
                "Promotion encounter not found. Data may be inconsistent.",
                "PROMOTION_ENCOUNTER_NOT_FOUND");

        var observationIds = await _db.Observations
            .Where(o => o.SourceBatchId == batchId)
            .Select(o => o.Id)
            .ToArrayAsync();

        return new PromotionResultResponse(
            BatchId: batchId,
            Status: batch.Status.ToDbString(),
            PatientId: encounter.PatientId,
            Mrn: encounter.Patient.Mrn,
            EncounterId: encounter.Id,
            ObservationIds: observationIds,
            PromotedAt: batch.PromotedAt!.Value,
            OutboxEventsWritten: 0  // Historical count not stored; use event metadata
        );
    }

    // ──────────────────────────────────────────────────
    // Private helpers
    // ──────────────────────────────────────────────────

    private record PromotionCoreResult(
        Patient Patient,
        Encounter Encounter,
        Guid[] ObservationIds,
        int OutboxCount,
        SupersessionResult? Supersession);

    private static void ValidateDraftDataForPromotion(DigitizationBatch batch)
    {
        if (!batch.SupersedesBatchId.HasValue)
        {
            if (batch.DraftPatient is null)
                throw new ValidationException("Batch has no draft patient data.", "MISSING_DRAFT_PATIENT");

            if (string.IsNullOrWhiteSpace(batch.DraftPatient.FullName))
                throw new ValidationException("Draft patient must have a full name.", "INVALID_DRAFT_PATIENT");
        }
        else if (!batch.PatientId.HasValue)
        {
            throw new ValidationException(
                "Correction batch has no linked patient.",
                "MISSING_PATIENT");
        }
    }

    private async Task<PromotionCoreResult> ExecutePromotionCoreAsync(
        DigitizationBatch batch, Guid actorUserId, bool enableRetroactiveAlerts, DateTimeOffset now)
    {
        // === Step 1: Create or update Patient ===
        Patient patient;
        if (batch.SupersedesBatchId.HasValue)
        {
            patient = await _db.Patients.FirstOrDefaultAsync(p => p.Id == batch.PatientId!.Value)
                ?? throw new NotFoundException(
                    $"Patient {batch.PatientId!.Value} not found.",
                    "PATIENT_NOT_FOUND");
        }
        else
        {
            patient = await CreateOrUpdatePatientAsync(batch.DraftPatient!, now);
        }

        // === Step 2: Create or match Encounter + LiveEncounter ===
        var encounter = await ResolveClinicalEncounterAsync(batch, patient.Id, now);
        await EnsureLiveEncounterAsync(encounter, now);

        // === Step 3: Insert clinical Observations + OutboxEvents ===
        var (observationIds, outboxCount) = await PromoteObservationsAsync(
            batch, patient.Id, encounter.Id, enableRetroactiveAlerts, now);

        // === Step 4: Mirror to LiveObservations ===
        PromoteLiveObservationsAsync(batch, patient.Id, encounter.Id, now);

        // === Step 5: Handle supersession for correction batches ===
        SupersessionResult? supersessionResult = null;
        if (batch.SupersedesBatchId.HasValue)
        {
            supersessionResult = await SupersedeOriginalBatchAsync(
                batch.SupersedesBatchId.Value, batch.Id, actorUserId, now);
        }

        // === Step 6: Update batch status to Promoted ===
        batch.Status = BatchStatus.Promoted;
        batch.PatientId = patient.Id;
        batch.PromotedAt = now;
        batch.PromotionEncounterId = encounter.Id;
        batch.EnableRetroactiveAlerts = enableRetroactiveAlerts;
        batch.UpdatedAt = now;

        // === Step 7: Write DigitizationEvent ===
        var promotionMetadata = new Dictionary<string, object>
        {
            ["patientId"] = patient.Id,
            ["mrn"] = patient.Mrn,
            ["encounterId"] = encounter.Id,
            ["observationCount"] = observationIds.Length,
            ["outboxEventsWritten"] = outboxCount,
            ["enableRetroactiveAlerts"] = enableRetroactiveAlerts,
            ["track"] = batch.Track.ToDbString(),
            ["isCorrection"] = batch.SupersedesBatchId.HasValue
        };

        if (supersessionResult is not null)
        {
            promotionMetadata["supersession"] = new
            {
                originalBatchId = supersessionResult.OriginalBatchId,
                observationsSuperseded = supersessionResult.ObservationsSuperseded,
                observationsReplaced = supersessionResult.ObservationsReplaced
            };
        }

        _db.DigitizationEvents.Add(new DigitizationEvent
        {
            Id = Guid.NewGuid(),
            BatchId = batch.Id,
            EventType = batch.SupersedesBatchId.HasValue
                ? DigitizationEventType.CorrectionPromoted
                : DigitizationEventType.Promoted,
            ActorUserId = actorUserId,
            OccurredAt = now,
            MetadataJson = JsonSerializer.Serialize(promotionMetadata)
        });

        return new PromotionCoreResult(patient, encounter, observationIds, outboxCount, supersessionResult);
    }

    private static string NormalizePatientName(string name) =>
        string.Join(" ", name.Trim().Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries))
            .ToUpperInvariant();

    private async Task<Patient> CreateOrUpdatePatientAsync(DraftPatient draft, DateTimeOffset now)
    {
        var normalizedName = NormalizePatientName(draft.FullName!);

        Patient? existing = null;

        if (draft.DateOfBirth.HasValue)
        {
            existing = await _db.Patients
                .FirstOrDefaultAsync(p =>
                    EF.Functions.ILike(p.FullName, normalizedName) &&
                    p.DateOfBirth == draft.DateOfBirth);
        }

        if (existing is not null)
        {
            existing.Sex = draft.Sex ?? existing.Sex;
            existing.BloodType = draft.BloodType ?? existing.BloodType;
            existing.EmergencyContact = draft.EmergencyContact ?? existing.EmergencyContact;
            existing.AllergiesJson = draft.AllergiesJson ?? existing.AllergiesJson;
            existing.NoKnownAllergies = draft.NoKnownAllergies || existing.NoKnownAllergies;
            existing.UpdatedAt = now;

            _logger.LogInformation(
                "Matched existing patient {PatientId} (MRN {Mrn}) by normalized name + DOB",
                existing.Id, existing.Mrn);

            return existing;
        }

        // No exact normalized match — check for fuzzy near-misses on same DOB
        if (draft.DateOfBirth.HasValue)
        {
            var sameDobPatients = await _db.Patients
                .Where(p => p.DateOfBirth == draft.DateOfBirth)
                .Select(p => new { p.Id, p.Mrn, p.FullName })
                .ToListAsync();

            foreach (var candidate in sameDobPatients)
            {
                var distance = LevenshteinDistance(
                    normalizedName, NormalizePatientName(candidate.FullName));

                if (distance > 0 && distance <= 3)
                {
                    _logger.LogWarning(
                        "Fuzzy patient match: new name '{NewName}' is {Distance} edits from " +
                        "existing patient {PatientId} (MRN {Mrn}, name '{ExistingName}') with same DOB. " +
                        "Creating new patient — consider merging if these are the same person",
                        normalizedName, distance, candidate.Id, candidate.Mrn, candidate.FullName);
                }
            }
        }

        var mrn = await _mrnGenerator.GenerateNextMrnAsync();

        var patient = new Patient
        {
            Id = Guid.NewGuid(),
            Mrn = mrn,
            FullName = normalizedName,
            DateOfBirth = draft.DateOfBirth,
            Sex = draft.Sex,
            BloodType = draft.BloodType,
            EmergencyContact = draft.EmergencyContact,
            AllergiesJson = draft.AllergiesJson,
            NoKnownAllergies = draft.NoKnownAllergies,
            CreatedAt = now,
            UpdatedAt = now
        };

        _db.Patients.Add(patient);

        _logger.LogInformation(
            "Created new patient {PatientId} with MRN {Mrn}",
            patient.Id, mrn);

        return patient;
    }

    private static int LevenshteinDistance(string a, string b)
    {
        if (a.Length == 0) return b.Length;
        if (b.Length == 0) return a.Length;

        var prev = new int[b.Length + 1];
        var curr = new int[b.Length + 1];

        for (var j = 0; j <= b.Length; j++)
            prev[j] = j;

        for (var i = 1; i <= a.Length; i++)
        {
            curr[0] = i;
            for (var j = 1; j <= b.Length; j++)
            {
                var cost = a[i - 1] == b[j - 1] ? 0 : 1;
                curr[j] = Math.Min(
                    Math.Min(curr[j - 1] + 1, prev[j] + 1),
                    prev[j - 1] + cost);
            }
            (prev, curr) = (curr, prev);
        }

        return prev[b.Length];
    }

    private async Task<Encounter> ResolveClinicalEncounterAsync(
        DigitizationBatch batch, Guid patientId, DateTimeOffset now)
    {
        if (batch.SupersedesBatchId.HasValue)
        {
            var originalBatch = await _db.DigitizationBatches
                .AsNoTracking()
                .FirstOrDefaultAsync(b => b.Id == batch.SupersedesBatchId.Value);

            if (originalBatch?.PromotionEncounterId is not Guid encounterId)
                throw new ValidationException(
                    "Original batch has no promotion encounter for correction reuse.",
                    "MISSING_ORIGINAL_ENCOUNTER");

            var encounter = await _db.Encounters.FirstOrDefaultAsync(e => e.Id == encounterId);
            if (encounter is null)
                throw new NotFoundException(
                    "Original promotion encounter not found.",
                    "PROMOTION_ENCOUNTER_NOT_FOUND");

            _logger.LogInformation(
                "Correction batch {CorrectionBatchId} reusing clinical encounter {EncounterId} " +
                "from original batch {OriginalBatchId}",
                batch.Id, encounter.Id, originalBatch.Id);

            return encounter;
        }

        return await CreateOrMatchEncounterAsync(batch, patientId, now);
    }

    private async Task EnsureLiveEncounterAsync(Encounter encounter, DateTimeOffset now)
    {
        if (await _db.LiveEncounters.AnyAsync(e => e.Id == encounter.Id))
            return;

        _db.LiveEncounters.Add(new LiveEncounter
        {
            Id = encounter.Id,
            PatientId = encounter.PatientId,
            AdmissionDate = encounter.AdmissionDate ?? now,
            Department = encounter.Department,
            RoomBed = encounter.RoomBed,
            AdmissionReason = encounter.AdmissionReason,
            DischargeDiagnosis = encounter.DischargeDiagnosis,
            Status = encounter.Status,
            CreatedAt = now
        });
    }

    private void PromoteLiveObservationsAsync(
        DigitizationBatch batch, Guid patientId, Guid encounterId, DateTimeOffset now)
    {
        foreach (var draft in batch.DraftObservations)
        {
            _db.LiveObservations.Add(new LiveObservation
            {
                Id = Guid.NewGuid(),
                EncounterId = encounterId,
                PatientId = patientId,
                SourceBatchId = batch.Id,
                ObservationCode = draft.ObservationCode,
                Value = draft.Value,
                Unit = draft.Unit,
                RecordedAt = draft.RecordedAt,
                Note = draft.Note,
                CreatedAt = now,
                IsSuperseded = false,
                SupersededByBatchId = null,
                SupersededAt = null
            });
        }
    }

    private async Task<Encounter> CreateOrMatchEncounterAsync(
        DigitizationBatch batch, Guid patientId, DateTimeOffset now)
    {
        // If the batch has a draft encounter, try to match an active encounter
        // for the same patient in the same department
        if (batch.DraftEncounter is not null)
        {
            var existingEncounter = await _db.Encounters
                .FirstOrDefaultAsync(e =>
                    e.PatientId == patientId &&
                    e.Department == batch.DraftEncounter.Department &&
                    e.Status == "active" &&
                    e.SourceBatchId != batch.Id);

            if (existingEncounter is not null)
            {
                _logger.LogInformation(
                    "Matched existing active encounter {EncounterId} for patient {PatientId}",
                    existingEncounter.Id, patientId);

                return existingEncounter;
            }
        }

        // Determine encounter status from draft
        var encounterStatus = batch.DraftEncounter?.Status ?? "active";
        if (batch.DraftEncounter?.DischargeDiagnosis is not null)
        {
            encounterStatus = "discharged";
        }

        var encounter = new Encounter
        {
            Id = Guid.NewGuid(),
            PatientId = patientId,
            AdmissionDate = batch.DraftEncounter?.AdmissionDate,
            Department = batch.DraftEncounter?.Department,
            RoomBed = batch.DraftEncounter?.RoomBed,
            AdmissionReason = batch.DraftEncounter?.AdmissionReason,
            DischargeDiagnosis = batch.DraftEncounter?.DischargeDiagnosis,
            Status = encounterStatus,
            SourceBatchId = batch.Id,
            CreatedAt = now,
            UpdatedAt = now
        };

        _db.Encounters.Add(encounter);

        _logger.LogInformation(
            "Created encounter {EncounterId} for patient {PatientId} with status '{Status}'",
            encounter.Id, patientId, encounterStatus);

        return encounter;
    }

    private Task<(Guid[] observationIds, int outboxCount)> PromoteObservationsAsync(
        DigitizationBatch batch, Guid patientId, Guid encounterId,
        bool enableRetroactiveAlerts, DateTimeOffset now)
    {
        var observationIds = new List<Guid>();
        var outboxCount = 0;

        // Determine the source label based on batch track
        var source = batch.Track == BatchTrack.LiveCapture
            ? "live_capture"
            : "digitization_backfill";

        // Determine whether outbox events should be written for this batch
        var shouldAlert = batch.Track == BatchTrack.LiveCapture || enableRetroactiveAlerts;

        foreach (var draft in batch.DraftObservations)
        {
            var observationId = Guid.NewGuid();

            var observation = new Observation
            {
                Id = observationId,
                EncounterId = encounterId,
                PatientId = patientId,
                ObservationCode = draft.ObservationCode,
                Value = draft.Value,
                Unit = draft.Unit,
                RecordedAt = draft.RecordedAt,
                Note = draft.Note,
                Source = source,
                SourceDraftObservationId = draft.Id,
                SourceBatchId = batch.Id,
                CreatedAt = now
            };

            _db.Observations.Add(observation);
            observationIds.Add(observationId);

            // Write outbox event only if alerting is enabled for this batch
            if (shouldAlert)
            {
                _db.OutboxEvents.Add(new OutboxEvent
                {
                    Id = Guid.NewGuid(),
                    EventType = "observation.created",
                    AggregateType = "Observation",
                    AggregateId = observationId,
                    PayloadJson = JsonSerializer.Serialize(new
                    {
                        observationId,
                        encounterId,
                        patientId,
                        observationCode = draft.ObservationCode,
                        value = draft.Value,
                        unit = draft.Unit,
                        recordedAt = draft.RecordedAt,
                        source,
                        batchId = batch.Id,
                        track = batch.Track.ToDbString()
                    }),
                    CreatedAt = now,
                    ProcessedAt = null,
                    RetryCount = 0
                });

                outboxCount++;
            }
        }

        _logger.LogInformation(
            "Promoted {Count} observations for batch {BatchId}, " +
            "{OutboxCount} outbox events (shouldAlert={ShouldAlert}, track={Track})",
            observationIds.Count, batch.Id, outboxCount, shouldAlert, batch.Track.ToDbString());

        return Task.FromResult((observationIds.ToArray(), outboxCount));
    }

    public async Task<PromotionResult> PromoteAsync(Guid batchId, Guid actorUserId)
    {
        var stopwatch = Stopwatch.StartNew();

        try
        {
            await using var transaction = await _db.Database.BeginTransactionAsync();

            try
            {
                var batch = await _db.DigitizationBatches
                    .Include(b => b.DraftPatient)
                    .Include(b => b.DraftEncounter)
                    .Include(b => b.DraftObservations)
                    .FirstOrDefaultAsync(b => b.Id == batchId);

                if (batch is null)
                    throw new NotFoundException("Batch not found.", "BATCH_NOT_FOUND");

                if (batch.Status != BatchStatus.Approved)
                    throw new ConflictException(
                        $"Only approved batches can be promoted. Batch is in '{batch.Status.ToDbString()}' status.",
                        "ILLEGAL_STATUS_TRANSITION");

                ValidateDraftDataForPromotion(batch);

                var now = DateTimeOffset.UtcNow;
                var enableRetroactiveAlerts = batch.EnableRetroactiveAlerts;

                var core = await ExecutePromotionCoreAsync(
                    batch, actorUserId, enableRetroactiveAlerts, now);

                await _db.SaveChangesAsync();
                await transaction.CommitAsync();

                stopwatch.Stop();
                DiagnosticsMetrics.PromotionDuration.Observe(stopwatch.Elapsed.TotalSeconds);

                _logger.LogInformation(
                    "Batch {BatchId} promoted via retry in {ElapsedMs}ms: Patient {PatientId} (MRN {Mrn}), " +
                    "Encounter {EncounterId}, {ObsCount} observations, {OutboxCount} outbox events",
                    batchId, stopwatch.ElapsedMilliseconds, core.Patient.Id, core.Patient.Mrn,
                    core.Encounter.Id, core.ObservationIds.Length, core.OutboxCount);

                return new PromotionResult(
                    batch.Id,
                    core.Patient.Id,
                    core.Patient.Mrn,
                    core.Encounter.Id,
                    core.ObservationIds,
                    core.ObservationIds.Length,
                    core.OutboxCount,
                    batch.SupersedesBatchId.HasValue,
                    core.Supersession);
            }
            catch
            {
                await transaction.RollbackAsync();
                throw;
            }
        }
        catch
        {
            stopwatch.Stop();
            throw;
        }
    }

    /// <summary>
    /// Marks all live observations sourced from the original batch as superseded.
    /// Records an audit event on the original batch documenting the supersession.
    /// </summary>
    private async Task<SupersessionResult> SupersedeOriginalBatchAsync(
        Guid originalBatchId, Guid correctionBatchId, Guid actorUserId, DateTimeOffset now)
    {
        // Load all live observations that came from the original erroneous batch
        var originalObservations = await _db.LiveObservations
            .Where(o => o.SourceBatchId == originalBatchId && !o.IsSuperseded)
            .ToListAsync();

        if (originalObservations.Count == 0)
        {
            _logger.LogWarning(
                "Supersession: no active live observations found for original batch {OriginalBatchId}",
                originalBatchId);
        }

        // Mark each observation as superseded — never delete
        foreach (var obs in originalObservations)
        {
            obs.IsSuperseded = true;
            obs.SupersededByBatchId = correctionBatchId;
            obs.SupersededAt = now;
        }

        // Count the correction batch's draft observations for the replacement count
        var replacementCount = await _db.DigitizationBatches
            .Where(b => b.Id == correctionBatchId)
            .SelectMany(b => b.DraftObservations)
            .CountAsync();

        // Record supersession event on the original batch's audit trail
        _db.DigitizationEvents.Add(new DigitizationEvent
        {
            Id = Guid.NewGuid(),
            BatchId = originalBatchId,
            EventType = DigitizationEventType.Superseded,
            ActorUserId = actorUserId,
            OccurredAt = now,
            MetadataJson = JsonSerializer.Serialize(new
            {
                supersededByBatchId = correctionBatchId,
                observationsSuperseded = originalObservations.Count,
                replacementObservations = replacementCount,
                reason = "Correction batch promoted — original observations marked superseded"
            })
        });

        return new SupersessionResult(
            originalBatchId,
            originalObservations.Count,
            replacementCount,
            now);
    }

}