using System.Security.Claims;
using System.Net.Mail;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services.EmailProviders;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace JobTrackerApi.Controllers;

[ApiController]
[Route("api/email/drafts")]
[Authorize(AuthenticationSchemes = "local")]
public sealed class EmailDraftsController(
    JobTrackerContext db,
    IEmailProviderRegistry providers,
    TimeProvider timeProvider) : ControllerBase
{
    public sealed record DraftDto(
        Guid Id,
        int JobApplicationId,
        string Provider,
        string To,
        string Subject,
        string BodyText,
        string? ThreadId,
        string ClientRequestId,
        long Revision,
        DateTime CreatedAtUtc,
        DateTime UpdatedAtUtc);

    public sealed record CreateDraftRequest(
        int JobApplicationId,
        string? Provider,
        string? To,
        string? Subject,
        string? BodyText,
        string? ThreadId);

    public sealed record UpdateDraftRequest(long Revision, string? To, string? Subject, string? BodyText);
    public sealed record NewAttemptRequest(long Revision);

    [HttpGet]
    public async Task<ActionResult<IReadOnlyList<DraftDto>>> List(
        [FromQuery] int? jobApplicationId,
        CancellationToken cancellationToken)
    {
        var ownerUserId = GetOwnerUserId();
        if (ownerUserId is null) return Unauthorized();
        if (jobApplicationId is <= 0) return BadRequest("A valid job application is required.");

        var query = db.EmailDrafts.AsNoTracking().Where(draft => draft.OwnerUserId == ownerUserId);
        if (jobApplicationId.HasValue)
            query = query.Where(draft => draft.JobApplicationId == jobApplicationId.Value);
        return Ok(await query
            .OrderByDescending(draft => draft.UpdatedAtUtc)
            .Select(draft => new DraftDto(
                draft.Id,
                draft.JobApplicationId,
                draft.Provider,
                draft.To,
                draft.Subject,
                draft.BodyText,
                draft.ThreadId,
                draft.ClientRequestId,
                draft.Revision,
                draft.CreatedAtUtc,
                draft.UpdatedAtUtc))
            .ToListAsync(cancellationToken));
    }

    [HttpGet("{id:guid}")]
    public async Task<ActionResult<DraftDto>> Get(Guid id, CancellationToken cancellationToken)
    {
        var ownerUserId = GetOwnerUserId();
        if (ownerUserId is null) return Unauthorized();
        var draft = await db.EmailDrafts.AsNoTracking()
            .FirstOrDefaultAsync(item => item.Id == id && item.OwnerUserId == ownerUserId, cancellationToken);
        return draft is null ? NotFound() : Ok(ToDto(draft));
    }

    [HttpPost]
    public async Task<ActionResult<DraftDto>> Create(CreateDraftRequest request, CancellationToken cancellationToken)
    {
        var ownerUserId = GetOwnerUserId();
        if (ownerUserId is null) return Unauthorized();
        if (request.JobApplicationId <= 0) return BadRequest("A valid job application is required.");
        var provider = providers.Get(request.Provider);
        if (provider is null) return BadRequest("Unknown email provider.");
        if (!TryNormalizeContent(request.To, request.Subject, request.BodyText, out var recipient, out var subject, out var bodyText, out var error))
            return BadRequest(error);
        var threadId = string.IsNullOrWhiteSpace(request.ThreadId) ? null : request.ThreadId.Trim();
        if (threadId?.Length > 512) return BadRequest("Thread ID must be at most 512 characters.");

        var ownsJob = await db.JobApplications.AsNoTracking()
            .AnyAsync(job => job.Id == request.JobApplicationId && job.OwnerUserId == ownerUserId, cancellationToken);
        if (!ownsJob) return NotFound();

        var now = timeProvider.GetUtcNow().UtcDateTime;
        var draft = new EmailDraft
        {
            Id = Guid.NewGuid(),
            OwnerUserId = ownerUserId,
            JobApplicationId = request.JobApplicationId,
            Provider = provider.ProviderKey.ToLowerInvariant(),
            To = recipient,
            Subject = subject,
            BodyText = bodyText,
            ThreadId = threadId,
            ClientRequestId = Guid.NewGuid().ToString("D"),
            Revision = 1,
            CreatedAtUtc = now,
            UpdatedAtUtc = now,
        };
        db.EmailDrafts.Add(draft);
        await db.SaveChangesAsync(cancellationToken);
        return CreatedAtAction(nameof(Get), new { id = draft.Id }, ToDto(draft));
    }

    [HttpPut("{id:guid}")]
    public async Task<ActionResult<DraftDto>> Update(Guid id, UpdateDraftRequest request, CancellationToken cancellationToken)
    {
        var ownerUserId = GetOwnerUserId();
        if (ownerUserId is null) return Unauthorized();
        if (request.Revision <= 0) return BadRequest("A positive revision is required.");
        if (!TryNormalizeContent(request.To, request.Subject, request.BodyText, out var recipient, out var subject, out var bodyText, out var error))
            return BadRequest(error);

        var now = timeProvider.GetUtcNow().UtcDateTime;
        var affected = await db.EmailDrafts
            .Where(draft => draft.Id == id && draft.OwnerUserId == ownerUserId && draft.Revision == request.Revision)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(draft => draft.To, recipient)
                .SetProperty(draft => draft.Subject, subject)
                .SetProperty(draft => draft.BodyText, bodyText)
                .SetProperty(draft => draft.Revision, draft => draft.Revision + 1)
                .SetProperty(draft => draft.UpdatedAtUtc, now), cancellationToken);
        if (affected == 0)
        {
            var exists = await db.EmailDrafts.AsNoTracking()
                .AnyAsync(draft => draft.Id == id && draft.OwnerUserId == ownerUserId, cancellationToken);
            return exists
                ? Conflict(new ProblemDetails { Title = "Draft revision conflict", Detail = "Reload the latest draft before saving again." })
                : NotFound();
        }

        return await Get(id, cancellationToken);
    }

    [HttpDelete("{id:guid}")]
    public async Task<IActionResult> Delete(Guid id, [FromQuery] long revision, CancellationToken cancellationToken)
    {
        var ownerUserId = GetOwnerUserId();
        if (ownerUserId is null) return Unauthorized();
        if (revision <= 0) return BadRequest("A positive revision is required.");

        var affected = await db.EmailDrafts
            .Where(draft => draft.Id == id && draft.OwnerUserId == ownerUserId && draft.Revision == revision)
            .ExecuteDeleteAsync(cancellationToken);
        if (affected == 1) return NoContent();
        var exists = await db.EmailDrafts.AsNoTracking()
            .AnyAsync(draft => draft.Id == id && draft.OwnerUserId == ownerUserId, cancellationToken);
        return exists
            ? Conflict(new ProblemDetails { Title = "Draft revision conflict", Detail = "Reload the latest draft before deleting it." })
            : NotFound();
    }

    [HttpPost("{id:guid}/new-attempt")]
    public async Task<ActionResult<DraftDto>> NewAttempt(Guid id, NewAttemptRequest request, CancellationToken cancellationToken)
    {
        var ownerUserId = GetOwnerUserId();
        if (ownerUserId is null) return Unauthorized();
        if (request.Revision <= 0) return BadRequest("A positive revision is required.");

        var draft = await db.EmailDrafts.AsNoTracking()
            .FirstOrDefaultAsync(item => item.Id == id && item.OwnerUserId == ownerUserId, cancellationToken);
        if (draft is null) return NotFound();
        if (draft.Revision != request.Revision)
            return Conflict(new ProblemDetails { Title = "Draft revision conflict", Detail = "Reload the latest draft before creating a new attempt." });

        var failedAttempt = await db.EmailSendAttempts.AsNoTracking().AnyAsync(attempt =>
            attempt.OwnerUserId == ownerUserId &&
            attempt.ClientRequestId == draft.ClientRequestId &&
            attempt.Status == EmailSendStatuses.Failed,
            cancellationToken);
        if (!failedAttempt)
            return Conflict(new ProblemDetails { Title = "A new attempt is not allowed", Detail = "Only a definitively failed delivery can receive a new attempt identity." });

        var newClientRequestId = Guid.NewGuid().ToString("D");
        var now = timeProvider.GetUtcNow().UtcDateTime;
        var affected = await db.EmailDrafts
            .Where(item => item.Id == id && item.OwnerUserId == ownerUserId && item.Revision == request.Revision && item.ClientRequestId == draft.ClientRequestId)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(item => item.ClientRequestId, newClientRequestId)
                .SetProperty(item => item.Revision, item => item.Revision + 1)
                .SetProperty(item => item.UpdatedAtUtc, now), cancellationToken);
        if (affected != 1)
            return Conflict(new ProblemDetails { Title = "Draft revision conflict", Detail = "Reload the latest draft before creating a new attempt." });
        return await Get(id, cancellationToken);
    }

    private string? GetOwnerUserId() =>
        User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub");

    private static bool TryNormalizeContent(
        string? to,
        string? subject,
        string? bodyText,
        out string recipient,
        out string normalizedSubject,
        out string normalizedBody,
        out string error)
    {
        recipient = to?.Trim() ?? string.Empty;
        normalizedSubject = subject?.Trim() ?? string.Empty;
        normalizedBody = bodyText ?? string.Empty;
        error = string.Empty;
        if (recipient.Length > 320 || (recipient.Length > 0 && !MailAddress.TryCreate(recipient, out _)))
            error = "Recipient must be empty or a valid address of at most 320 characters.";
        else if (normalizedSubject.Length > 998)
            error = "Subject must be at most 998 characters.";
        else if (normalizedBody.Length > 200_000)
            error = "Body must be at most 200000 characters.";
        return error.Length == 0;
    }

    private static DraftDto ToDto(EmailDraft draft) => new(
        draft.Id,
        draft.JobApplicationId,
        draft.Provider,
        draft.To,
        draft.Subject,
        draft.BodyText,
        draft.ThreadId,
        draft.ClientRequestId,
        draft.Revision,
        draft.CreatedAtUtc,
        draft.UpdatedAtUtc);
}
