using JobTrackerApi.Data;
using JobTrackerApi.Models;
using Microsoft.EntityFrameworkCore;

namespace JobTrackerApi.Services;

public sealed record CreateEmailSendAttempt(int JobApplicationId, string Provider, string ClientRequestId, string PayloadHash);
public sealed record EmailSendAttemptCreation(EmailSendAttempt Attempt, bool Created);
public sealed record EmailSendAttemptRecoveryResult(int FailedPending, int UncertainSending);

public sealed class EmailSendConflictException(string message) : InvalidOperationException(message);

public sealed class EmailSendAttemptStore(JobTrackerContext db, TimeProvider timeProvider)
{
    public static readonly TimeSpan AbandonedAge = TimeSpan.FromMinutes(15);
    private DateTime UtcNow => timeProvider.GetUtcNow().UtcDateTime;

    public Task<EmailSendAttempt?> GetAsync(Guid id, CancellationToken cancellationToken)
    {
        EnsureOwnerScope();
        return db.EmailSendAttempts.AsNoTracking().FirstOrDefaultAsync(item => item.Id == id, cancellationToken);
    }

    public async Task<EmailSendAttemptCreation> CreateAsync(CreateEmailSendAttempt request, CancellationToken cancellationToken)
    {
        var ownerUserId = db.CurrentUserId ?? throw new InvalidOperationException("Email send creation requires an authenticated owner scope.");
        Validate(request);
        if (!await db.JobApplications.AnyAsync(job => job.Id == request.JobApplicationId, cancellationToken))
            throw new InvalidOperationException("The job application does not exist in the current owner scope.");

        var existing = await db.EmailSendAttempts.AsNoTracking().FirstOrDefaultAsync(item => item.ClientRequestId == request.ClientRequestId, cancellationToken);
        if (existing is not null) return Existing(existing, request.PayloadHash);

        var attempt = new EmailSendAttempt
        {
            Id = Guid.NewGuid(),
            OwnerUserId = ownerUserId,
            JobApplicationId = request.JobApplicationId,
            Provider = request.Provider.Trim().ToLowerInvariant(),
            ClientRequestId = request.ClientRequestId.Trim(),
            PayloadHash = request.PayloadHash.Trim().ToLowerInvariant(),
            CreatedAtUtc = UtcNow,
        };
        db.EmailSendAttempts.Add(attempt);
        try
        {
            await db.SaveChangesAsync(cancellationToken);
            return new EmailSendAttemptCreation(attempt, true);
        }
        catch (DbUpdateException)
        {
            db.Entry(attempt).State = EntityState.Detached;
            existing = await db.EmailSendAttempts.AsNoTracking().FirstOrDefaultAsync(item => item.ClientRequestId == request.ClientRequestId, cancellationToken);
            if (existing is not null) return Existing(existing, request.PayloadHash);
            throw;
        }
    }

    public Task<int> BeginAsync(Guid id, CancellationToken cancellationToken)
    {
        EnsureOwnerScope();
        var now = UtcNow;
        return db.EmailSendAttempts
            .Where(item => item.Id == id && item.Status == EmailSendStatuses.Pending)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(item => item.Status, EmailSendStatuses.Sending)
                .SetProperty(item => item.StartedAtUtc, now), cancellationToken);
    }

    public Task<int> MarkSentAsync(Guid id, string? providerMessageId, CancellationToken cancellationToken) =>
        CompleteAsync(id, EmailSendStatuses.Sent, providerMessageId, null, cancellationToken);

    public Task<int> MarkFailedAsync(Guid id, string failureCategory, CancellationToken cancellationToken) =>
        CompleteAsync(id, EmailSendStatuses.Failed, null, failureCategory, cancellationToken);

    public Task<int> MarkUncertainAsync(Guid id, string failureCategory, CancellationToken cancellationToken) =>
        CompleteAsync(id, EmailSendStatuses.Uncertain, null, failureCategory, cancellationToken);

    public async Task<EmailSendAttemptRecoveryResult> ReconcileAbandonedAsync(CancellationToken cancellationToken)
    {
        var now = UtcNow;
        var cutoff = now - AbandonedAge;
        var candidates = await db.EmailSendAttempts.IgnoreQueryFilters().AsNoTracking()
            .Where(item =>
                (item.Status == EmailSendStatuses.Pending && item.CreatedAtUtc <= cutoff) ||
                (item.Status == EmailSendStatuses.Sending && (item.StartedAtUtc ?? item.CreatedAtUtc) <= cutoff))
            .Select(item => new { item.Id, item.OwnerUserId, item.Status })
            .ToListAsync(cancellationToken);
        if (candidates.Count == 0) return new EmailSendAttemptRecoveryResult(0, 0);

        await using var transaction = db.Database.IsRelational()
            ? await db.Database.BeginTransactionAsync(cancellationToken)
            : null;
        var failedPending = 0;
        var uncertainSending = 0;
        foreach (var candidate in candidates)
        {
            var failed = candidate.Status == EmailSendStatuses.Pending;
            var terminalStatus = failed ? EmailSendStatuses.Failed : EmailSendStatuses.Uncertain;
            var failureCategory = failed ? "process_stopped_before_delivery" : "process_interrupted";
            var affected = await db.EmailSendAttempts.IgnoreQueryFilters()
                .Where(item => item.Id == candidate.Id && item.Status == candidate.Status &&
                    (failed ? item.CreatedAtUtc <= cutoff : (item.StartedAtUtc ?? item.CreatedAtUtc) <= cutoff))
                .ExecuteUpdateAsync(setters => setters
                    .SetProperty(item => item.Status, terminalStatus)
                    .SetProperty(item => item.FailureCategory, failureCategory)
                    .SetProperty(item => item.CompletedAtUtc, now), cancellationToken);
            if (affected != 1) continue;

            if (failed) failedPending++;
            else uncertainSending++;
            db.UserNotifications.Add(new UserNotification
            {
                Id = Guid.NewGuid(),
                OwnerUserId = candidate.OwnerUserId,
                Kind = failed ? "email.send.stopped" : "email.send.uncertain",
                Title = failed ? "Email send stopped" : "Check email delivery",
                Message = failed
                    ? "An email attempt stopped before provider delivery. Review the draft before trying again."
                    : "Email delivery could not be confirmed after an interruption. Check the provider Sent folder and do not resend automatically.",
                LinkPath = "/correspondence",
                CreatedAtUtc = now,
            });
        }

        await db.SaveChangesAsync(cancellationToken);
        if (transaction is not null) await transaction.CommitAsync(cancellationToken);
        return new EmailSendAttemptRecoveryResult(failedPending, uncertainSending);
    }

    private Task<int> CompleteAsync(Guid id, string status, string? providerMessageId, string? failureCategory, CancellationToken cancellationToken)
    {
        EnsureOwnerScope();
        ValidateOptional(providerMessageId, 256, nameof(providerMessageId));
        ValidateOptional(failureCategory, 64, nameof(failureCategory));
        var now = UtcNow;
        return db.EmailSendAttempts
            .Where(item => item.Id == id && item.Status == EmailSendStatuses.Sending)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(item => item.Status, status)
                .SetProperty(item => item.ProviderMessageId, providerMessageId)
                .SetProperty(item => item.FailureCategory, failureCategory)
                .SetProperty(item => item.CompletedAtUtc, now), cancellationToken);
    }

    private static EmailSendAttemptCreation Existing(EmailSendAttempt existing, string payloadHash)
    {
        if (!string.Equals(existing.PayloadHash, payloadHash.Trim(), StringComparison.OrdinalIgnoreCase))
            throw new EmailSendConflictException("The client request ID was already used for different email content.");
        return new EmailSendAttemptCreation(existing, false);
    }

    private static void Validate(CreateEmailSendAttempt request)
    {
        if (request.JobApplicationId <= 0) throw new ArgumentOutOfRangeException(nameof(request.JobApplicationId));
        ValidateRequired(request.Provider, 32, nameof(request.Provider));
        ValidateRequired(request.ClientRequestId, 128, nameof(request.ClientRequestId));
        ValidateRequired(request.PayloadHash, 64, nameof(request.PayloadHash));
        if (!Guid.TryParse(request.ClientRequestId, out _)) throw new ArgumentException("Client request ID must be a UUID.", nameof(request.ClientRequestId));
        if (request.PayloadHash.Length != 64 || request.PayloadHash.Any(value => !Uri.IsHexDigit(value)))
            throw new ArgumentException("Payload hash must be a SHA-256 hex digest.", nameof(request.PayloadHash));
    }

    private static void ValidateRequired(string value, int maxLength, string name)
    {
        if (string.IsNullOrWhiteSpace(value) || value.Trim().Length > maxLength) throw new ArgumentException($"{name} is required and must be at most {maxLength} characters.", name);
    }

    private static void ValidateOptional(string? value, int maxLength, string name)
    {
        if (value?.Length > maxLength) throw new ArgumentException($"{name} must be at most {maxLength} characters.", name);
    }

    private void EnsureOwnerScope()
    {
        if (string.IsNullOrWhiteSpace(db.CurrentUserId)) throw new InvalidOperationException("Email send access requires an authenticated owner scope.");
    }
}
