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

namespace JobTrackerApi.Controllers;

public sealed record EmailMessageDetailDto(
    string Id,
    string ThreadId,
    string Subject,
    string From,
    string To,
    DateTimeOffset? Date,
    string Snippet,
    string BodyText,
    IReadOnlyList<string> Labels,
    IReadOnlyList<EmailAttachmentRef> Attachments);

[ApiController]
[Route("api/email")]
[Authorize(AuthenticationSchemes = "local")]
public sealed class EmailController(
    IEmailProviderRegistry providers,
    JobTrackerContext db,
    EmailSendAttemptStore attempts,
    ILogger<EmailController> logger) : ControllerBase
{
    public sealed record ProviderStatus(string Provider, string DisplayName, bool Connected, string? Address, bool CanRead, bool CanSend);
    public sealed record SendRequest(int JobApplicationId, string? Provider, string? ClientRequestId, string? To, string? Subject, string? BodyText, string? ThreadId, bool Confirmed);
    public sealed record SendResult(Guid AttemptId, string Status, bool Duplicate, string? ExternalMessageId, string? ExternalThreadId, string? FailureCategory);
    [HttpGet("providers")]
    public async Task<ActionResult<IReadOnlyList<ProviderStatus>>> GetProviders(CancellationToken cancellationToken)
    {
        var ownerUserId = GetOwnerUserId();
        if (ownerUserId is null) return Unauthorized();

        var statuses = new List<ProviderStatus>(providers.All.Count);
        foreach (var provider in providers.All)
        {
            var connection = await provider.GetConnectionAsync(ownerUserId, cancellationToken);
            statuses.Add(new ProviderStatus(
                provider.ProviderKey,
                GetDisplayName(provider.ProviderKey),
                connection is not null,
                connection?.Address,
                CanRead: connection is not null,
                CanSend: connection?.CanSend ?? false));
        }

        return Ok(statuses);
    }

    [HttpGet("messages")]
    public async Task<ActionResult<IReadOnlyList<EmailMessageSummary>>> Search(
        [FromQuery] string provider,
        [FromQuery] string? q,
        [FromQuery] int limit = 25,
        CancellationToken cancellationToken = default)
    {
        var resolved = providers.Get(provider);
        if (resolved is null) return BadRequest("Unknown email provider.");

        var ownerUserId = GetOwnerUserId();
        if (ownerUserId is null) return Unauthorized();
        if (await resolved.GetConnectionAsync(ownerUserId, cancellationToken) is null)
            return Conflict(new ProblemDetails { Title = "Email provider is not connected." });

        return Ok(await resolved.SearchAsync(ownerUserId, q, Math.Clamp(limit, 1, 100), cancellationToken));
    }

    [HttpGet("thread")]
    public async Task<ActionResult<IReadOnlyList<EmailMessageSummary>>> GetThread(
        [FromQuery] string provider,
        [FromQuery] string threadId,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(threadId)) return BadRequest("threadId is required.");
        var resolved = providers.Get(provider);
        if (resolved is null) return BadRequest("Unknown email provider.");

        var ownerUserId = GetOwnerUserId();
        if (ownerUserId is null) return Unauthorized();
        if (await resolved.GetConnectionAsync(ownerUserId, cancellationToken) is null)
            return Conflict(new ProblemDetails { Title = "Email provider is not connected." });

        return Ok(await resolved.ListThreadMessagesAsync(ownerUserId, threadId.Trim(), cancellationToken));
    }

    [HttpGet("message")]
    public async Task<ActionResult<EmailMessageDetailDto>> GetMessage(
        [FromQuery] string provider,
        [FromQuery] string messageId,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(messageId)) return BadRequest("messageId is required.");
        var resolved = providers.Get(provider);
        if (resolved is null) return BadRequest("Unknown email provider.");

        var ownerUserId = GetOwnerUserId();
        if (ownerUserId is null) return Unauthorized();
        if (await resolved.GetConnectionAsync(ownerUserId, cancellationToken) is null)
            return Conflict(new ProblemDetails { Title = "Email provider is not connected." });

        var detail = await resolved.GetMessageAsync(ownerUserId, messageId.Trim(), cancellationToken);
        return Ok(new EmailMessageDetailDto(
            detail.Id,
            detail.ThreadId,
            detail.Subject,
            detail.From,
            detail.To,
            detail.Date,
            detail.Snippet,
            detail.BodyText,
            detail.Labels,
            detail.Attachments));
    }

    [HttpPost("send")]
    [EnableRateLimiting("email-send")]
    public async Task<ActionResult<SendResult>> Send([FromBody] SendRequest request, CancellationToken cancellationToken)
    {
        var ownerUserId = GetOwnerUserId();
        if (ownerUserId is null) return Unauthorized();
        if (!request.Confirmed) return BadRequest("Explicit send confirmation is required.");
        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 (!Guid.TryParse(request.ClientRequestId, out var requestId)) return BadRequest("clientRequestId must be a UUID.");
        var recipient = request.To?.Trim();
        var subject = request.Subject?.Trim();
        var bodyText = request.BodyText;
        var threadId = string.IsNullOrWhiteSpace(request.ThreadId) ? null : request.ThreadId.Trim();
        if (!MailAddress.TryCreate(recipient, out _) || recipient.Length > 320) return BadRequest("A valid recipient is required.");
        if (string.IsNullOrWhiteSpace(subject) || subject.Length > 998) return BadRequest("Subject is required and must be at most 998 characters.");
        if (string.IsNullOrWhiteSpace(bodyText) || bodyText.Length > 200_000) return BadRequest("Body is required and must be at most 200000 characters.");
        if (threadId?.Length > 512) return BadRequest("Thread ID must be at most 512 characters.");

        var job = await db.JobApplications.Include(item => item.Company)
            .FirstOrDefaultAsync(item => item.Id == request.JobApplicationId, cancellationToken);
        if (job is null) return NotFound();

        var normalizedProvider = provider.ProviderKey.ToLowerInvariant();
        var clientRequestId = requestId.ToString("D");
        var payloadHash = ComputePayloadHash(job.Id, normalizedProvider, recipient, subject, bodyText, threadId);
        EmailSendAttemptCreation reservation;
        try
        {
            reservation = await attempts.CreateAsync(new CreateEmailSendAttempt(job.Id, normalizedProvider, clientRequestId, payloadHash), cancellationToken);
        }
        catch (EmailSendConflictException ex)
        {
            return Conflict(new ProblemDetails { Title = "Idempotency conflict", Detail = ex.Message });
        }

        if (!reservation.Created)
        {
            var existing = reservation.Attempt;
            if (existing.Status == EmailSendStatuses.Sent)
                return Ok(new SendResult(existing.Id, existing.Status, true, existing.ProviderMessageId, null, existing.FailureCategory));
            return Conflict(new SendResult(existing.Id, existing.Status, true, existing.ProviderMessageId, null, existing.FailureCategory));
        }

        if (await attempts.BeginAsync(reservation.Attempt.Id, cancellationToken) != 1)
            return Conflict(new SendResult(reservation.Attempt.Id, EmailSendStatuses.Pending, true, null, null, null));

        EmailConnectionInfo? connection;
        try
        {
            connection = await provider.GetConnectionAsync(ownerUserId, cancellationToken);
        }
        catch (OperationCanceledException)
        {
            await attempts.MarkFailedAsync(reservation.Attempt.Id, "cancelled_before_delivery", CancellationToken.None);
            throw;
        }
        catch (Exception ex)
        {
            logger.LogWarning("Email connection check failed for attempt {AttemptId} ({ExceptionType})", reservation.Attempt.Id, ex.GetType().Name);
            await attempts.MarkFailedAsync(reservation.Attempt.Id, "connection_check_failed", CancellationToken.None);
            return StatusCode(StatusCodes.Status502BadGateway,
                new SendResult(reservation.Attempt.Id, EmailSendStatuses.Failed, false, null, null, "connection_check_failed"));
        }
        if (connection is null || !connection.CanSend)
        {
            await attempts.MarkFailedAsync(reservation.Attempt.Id, "reauthorization_required", CancellationToken.None);
            return Conflict(new SendResult(reservation.Attempt.Id, EmailSendStatuses.Failed, false, null, null, "reauthorization_required"));
        }

        EmailDeliveryResult delivery;
        try
        {
            delivery = await provider.SendAsync(ownerUserId, new EmailDeliveryRequest(recipient, subject, bodyText, threadId), cancellationToken);
        }
        catch (EmailProviderDeliveryException ex)
        {
            if (ex.Uncertain) await attempts.MarkUncertainAsync(reservation.Attempt.Id, ex.Category, CancellationToken.None);
            else await attempts.MarkFailedAsync(reservation.Attempt.Id, ex.Category, CancellationToken.None);
            var status = ex.Uncertain ? EmailSendStatuses.Uncertain : EmailSendStatuses.Failed;
            return StatusCode(ex.Uncertain ? StatusCodes.Status409Conflict : StatusCodes.Status502BadGateway,
                new SendResult(reservation.Attempt.Id, status, false, null, null, ex.Category));
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Unexpected email delivery failure for attempt {AttemptId}", reservation.Attempt.Id);
            await attempts.MarkUncertainAsync(reservation.Attempt.Id, "unexpected_delivery_error", CancellationToken.None);
            return StatusCode(StatusCodes.Status409Conflict,
                new SendResult(reservation.Attempt.Id, EmailSendStatuses.Uncertain, false, null, null, "unexpected_delivery_error"));
        }

        try
        {
            await using var transaction = db.Database.IsRelational() ? await db.Database.BeginTransactionAsync(CancellationToken.None) : null;
            db.Correspondences.Add(new Correspondence
            {
                JobApplicationId = job.Id,
                From = "Me",
                Direction = "outbound",
                Subject = subject,
                Channel = "Email",
                ExternalMessageId = delivery.ExternalMessageId,
                ExternalThreadId = delivery.ExternalThreadId ?? threadId,
                ExternalFrom = connection.Address,
                ExternalTo = recipient,
                Provider = normalizedProvider,
                Content = bodyText,
                Date = DateTime.UtcNow,
            });
            db.JobEvents.Add(new JobEvent
            {
                JobApplicationId = job.Id,
                Type = "EmailSent",
                NewValue = normalizedProvider,
                Note = $"attempt:{reservation.Attempt.Id}",
                At = DateTime.UtcNow,
            });
            if (await attempts.MarkSentAsync(reservation.Attempt.Id, delivery.ExternalMessageId, CancellationToken.None) != 1)
                throw new InvalidOperationException("The email attempt could not be finalized.");
            await db.SaveChangesAsync(CancellationToken.None);
            if (transaction is not null) await transaction.CommitAsync(CancellationToken.None);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Email was accepted but local persistence failed for attempt {AttemptId}", reservation.Attempt.Id);
            db.ChangeTracker.Clear();
            await attempts.MarkUncertainAsync(reservation.Attempt.Id, "local_persistence_failed", CancellationToken.None);
            return StatusCode(StatusCodes.Status409Conflict,
                new SendResult(reservation.Attempt.Id, EmailSendStatuses.Uncertain, false, delivery.ExternalMessageId, delivery.ExternalThreadId, "local_persistence_failed"));
        }

        return Ok(new SendResult(reservation.Attempt.Id, EmailSendStatuses.Sent, false, delivery.ExternalMessageId, delivery.ExternalThreadId, null));
    }

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

    private static string ComputePayloadHash(int jobApplicationId, string provider, string recipient, string subject, string bodyText, string? threadId)
    {
        var canonical = JsonSerializer.Serialize(new
        {
            JobApplicationId = jobApplicationId,
            Provider = provider,
            To = recipient.ToLowerInvariant(),
            Subject = subject,
            Body = bodyText,
            ThreadId = threadId ?? string.Empty,
        });
        return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(canonical))).ToLowerInvariant();
    }

    private static string GetDisplayName(string provider) => provider.ToLowerInvariant() switch
    {
        "gmail" => "Gmail",
        "microsoft" => "Outlook",
        "imap" => "IMAP",
        _ => provider,
    };
}
