using JobTrackerApi.Models;
using JobTrackerApi.Services;
using JobTrackerApi.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;

namespace JobTrackerApi.Controllers;

[ApiController]
[Route("api/users")]
[Authorize(Roles = "Admin")]
public sealed class UsersController : ControllerBase
{
    private readonly UserManager<ApplicationUser> _users;
    private readonly RoleManager<IdentityRole> _roles;
    private readonly IAppEmailSender _email;
    private readonly ExternalOrigin _externalOrigin;
    private readonly ILogger<UsersController> _logger;
    private readonly AccountDeletionService? _deletions;
    private readonly JobTrackerContext? _db;
    public UsersController(UserManager<ApplicationUser> users, RoleManager<IdentityRole> roles, IAppEmailSender email, IConfiguration cfg, ILogger<UsersController> logger, ExternalOrigin? externalOrigin = null, AccountDeletionService? deletions = null, JobTrackerContext? db = null)
    {
        _users = users;
        _roles = roles;
        _email = email;
        _externalOrigin = externalOrigin ?? ExternalOrigin.FromConfiguration(cfg);
        _logger = logger;
        _deletions = deletions;
        _db = db;
    }

    public sealed record UserDto(
        string Id,
        string? Email,
        string? UserName,
        string? FirstName,
        string? LastName,
        string? DisplayName,
        bool EmailConfirmed,
        string? GoogleEmail,
        DateTimeOffset? GoogleLinkedAt,
        List<string> Roles,
        bool IsCurrentUser,
        bool CanRemoveAdmin);

    [HttpGet]
    public async Task<ActionResult<List<UserDto>>> List(CancellationToken cancellationToken)
    {
        if (_db is not null)
        {
            var users = await _db.Users.AsNoTracking()
                .OrderBy(user => user.Email)
                .ToListAsync(cancellationToken);
            var roleRows = await (
                from userRole in _db.UserRoles.AsNoTracking()
                join role in _db.Roles.AsNoTracking() on userRole.RoleId equals role.Id
                select new { userRole.UserId, role.Name })
                .ToListAsync(cancellationToken);
            var rolesByUser = roleRows
                .Where(row => !string.IsNullOrWhiteSpace(row.Name))
                .GroupBy(row => row.UserId)
                .ToDictionary(group => group.Key, group => group.Select(row => row.Name!).ToList());
            var relationalAdminCount = roleRows
                .Where(row => string.Equals(row.Name, "Admin", StringComparison.OrdinalIgnoreCase))
                .Select(row => row.UserId)
                .Distinct(StringComparer.Ordinal)
                .Count();
            var relationalCurrentUserId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub");

            return Ok(users.Select(user =>
            {
                var roles = rolesByUser.GetValueOrDefault(user.Id) ?? [];
                return ToDto(user, roles, relationalCurrentUserId, !roles.Contains("Admin", StringComparer.OrdinalIgnoreCase) || relationalAdminCount > 1);
            }).ToList());
        }

        // Retained only for isolated controller tests/manual construction. The application DI path
        // always supplies JobTrackerContext and uses the fixed two-query projection above.
        var items = await _users.Users
            .OrderBy(u => u.Email)
            .ToListAsync(cancellationToken);
        var currentUserId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub");
        var adminCount = (await _users.GetUsersInRoleAsync("Admin")).Count;

        var outList = new List<UserDto>(items.Count);
        foreach (var u in items)
        {
            var rs = await _users.GetRolesAsync(u);
            var roles = rs.ToList();
            outList.Add(ToDto(u, roles, currentUserId, !roles.Contains("Admin", StringComparer.OrdinalIgnoreCase) || adminCount > 1));
        }

        return Ok(outList);
    }

    public sealed record CreateUserRequest(string Email, string Password, string? UserName, string? FirstName, string? LastName, string? DisplayName, string[]? Roles);

    [HttpPost]
    public async Task<ActionResult<UserDto>> Create([FromBody] CreateUserRequest request, CancellationToken cancellationToken)
    {
        var email = (request.Email ?? "").Trim();
        var password = request.Password ?? "";

        if (email.Length == 0) return BadRequest("Email is required.");
        if (password.Length == 0) return BadRequest("Password is required.");

        var existing = await _users.FindByEmailAsync(email);
        if (existing is not null) return BadRequest("User already exists.");

        var u = new ApplicationUser
        {
            UserName = string.IsNullOrWhiteSpace(request.UserName) ? email : request.UserName.Trim(),
            Email = email,
            EmailConfirmed = true,
            FirstName = TrimOrNull(request.FirstName),
            LastName = TrimOrNull(request.LastName),
            DisplayName = TrimOrNull(request.DisplayName)
        };
        var res = await _users.CreateAsync(u, password);
        if (!res.Succeeded)
            return BadRequest(string.Join("; ", res.Errors.Select(e => e.Description)));

        var roles = (request.Roles ?? Array.Empty<string>()).Select(r => (r ?? "").Trim()).Where(r => r.Length > 0).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
        foreach (var r in roles)
        {
            if (!await _roles.RoleExistsAsync(r))
                await _roles.CreateAsync(new IdentityRole(r));
            await _users.AddToRoleAsync(u, r);
        }

        var rs = await _users.GetRolesAsync(u);
        var currentUserId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub");
        return Ok(ToDto(u, rs.ToList(), currentUserId, true));
    }

    public sealed record SetRolesRequest(string[] Roles);

    [HttpPut("{id}/roles")]
    public async Task<IActionResult> SetRoles([FromRoute] string id, [FromBody] SetRolesRequest request, CancellationToken cancellationToken)
    {
        var u = await _users.FindByIdAsync(id);
        if (u is null) return NotFound();

        var desired = (request.Roles ?? Array.Empty<string>()).Select(r => (r ?? "").Trim()).Where(r => r.Length > 0).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
        var current = await _users.GetRolesAsync(u);

        var toRemove = current.Where(r => !desired.Contains(r, StringComparer.OrdinalIgnoreCase)).ToList();
        var toAdd = desired.Where(r => !current.Contains(r, StringComparer.OrdinalIgnoreCase)).ToList();

        if (toRemove.Contains("Admin", StringComparer.OrdinalIgnoreCase)
            && (await _users.GetUsersInRoleAsync("Admin")).Count <= 1)
        {
            return Conflict(new ProblemDetails
            {
                Title = "Last administrator protected",
                Detail = "Assign the Admin role to another user before removing it from the final administrator."
            });
        }

        foreach (var r in toAdd)
        {
            if (!await _roles.RoleExistsAsync(r))
            {
                var createRole = await _roles.CreateAsync(new IdentityRole(r));
                if (!createRole.Succeeded) return IdentityFailure(createRole);
            }

            var addRole = await _users.AddToRoleAsync(u, r);
            if (!addRole.Succeeded) return IdentityFailure(addRole);
        }

        if (toRemove.Count > 0)
        {
            var removeRoles = await _users.RemoveFromRolesAsync(u, toRemove);
            if (!removeRoles.Succeeded) return IdentityFailure(removeRoles);
        }

        // Role claims are embedded in issued JWTs. Without revoking the target user's live
        // sessions, a removed Admin role would remain effective until those tokens expired.
        // The application DI path always supplies the context; the null branch only supports
        // isolated controller construction in older tests.
        if (_db is not null && (toAdd.Count > 0 || toRemove.Count > 0))
            await SessionRevocation.RevokeAllAsync(_db, u.Id, trustedDeviceHashToKeep: null, cancellationToken);

        return NoContent();
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete([FromRoute] string id, CancellationToken cancellationToken)
    {
        var u = await _users.FindByIdAsync(id);
        if (u is null) return NotFound();

        if (await _users.IsInRoleAsync(u, "Admin")
            && (await _users.GetUsersInRoleAsync("Admin")).Count <= 1)
        {
            return Conflict(new ProblemDetails
            {
                Title = "Last administrator protected",
                Detail = "Assign the Admin role to another user before deleting the final administrator."
            });
        }

        if (_deletions is null || !_deletions.CanAcceptRequests)
            return Problem(statusCode: StatusCodes.Status503ServiceUnavailable, title: "Account deletion unavailable", detail: "Account deletion remains disabled until retention and restore safeguards are approved.");
        if (!string.Equals(Request.Headers["X-Confirm-Account-Deletion"].ToString(), u.Email, StringComparison.OrdinalIgnoreCase))
            return BadRequest("Confirm the exact account email before deletion.");
        var requestedBy = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub");
        if (string.IsNullOrWhiteSpace(requestedBy)) return Unauthorized();
        var request = await _deletions.RequestAsync(u.Id, requestedBy, cancellationToken);
        return request is null
            ? Problem(statusCode: StatusCodes.Status503ServiceUnavailable, title: "Account deletion unavailable")
            : Accepted(new { requestId = request.RequestId, status = request.Status, stage = request.Stage });
    }

    [HttpPost("{id}/send-password-reset")]
    [EnableRateLimiting("auth-email")]
    public async Task<IActionResult> SendPasswordReset([FromRoute] string id, CancellationToken cancellationToken)
    {
        var u = await _users.FindByIdAsync(id);
        if (u is null) return NotFound();
        if (string.IsNullOrWhiteSpace(u.Email)) return BadRequest("User has no email.");

        var token = await _users.GeneratePasswordResetTokenAsync(u);

        var link = _externalOrigin.BuildPath($"/reset-password?email={Uri.EscapeDataString(u.Email)}&token={Uri.EscapeDataString(token)}");

        try
        {
            await _email.SendAsync(
                u.Email,
                "Password reset",
                $"An admin initiated a password reset for your Jobbjakt account.\n\nReset link:\n{link}\n",
                cancellationToken
            );
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to send admin-initiated password reset email to {Email}", u.Email);
            return Problem(statusCode: StatusCodes.Status503ServiceUnavailable, title: "Email delivery unavailable", detail: "Password reset email could not be sent right now. Please try again later.");
        }

        return NoContent();
    }

    public sealed record SendTestEmailRequest(string? ToEmail, string? Subject, string? Message);

    [HttpPost("send-test-email")]
    [EnableRateLimiting("auth-email")]
    public async Task<IActionResult> SendTestEmail([FromBody] SendTestEmailRequest? request, CancellationToken cancellationToken)
    {
        var currentUserId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue("sub");
        var currentUser = currentUserId is null ? null : await _users.FindByIdAsync(currentUserId);

        var toEmail = (request?.ToEmail ?? currentUser?.Email ?? "").Trim();
        if (string.IsNullOrWhiteSpace(toEmail)) return BadRequest("Recipient email is required.");

        var subject = string.IsNullOrWhiteSpace(request?.Subject) ? "Jobbjakt test email" : request!.Subject!.Trim();
        var message = string.IsNullOrWhiteSpace(request?.Message)
            ? "This is a test email from the Jobbjakt admin panel.\n\nIf you received this, the SMTP configuration is working."
            : request!.Message!.Trim();

        try
        {
            await _email.SendAsync(
                toEmail,
                subject,
                $"{message}\n\nSent at: {DateTimeOffset.UtcNow:u}",
                cancellationToken
            );
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to send test email to {Email}", toEmail);
            return Problem(statusCode: StatusCodes.Status503ServiceUnavailable, title: "Email delivery unavailable", detail: "Test email could not be sent right now. Please try again later.");
        }

        return NoContent();
    }

    private static UserDto ToDto(ApplicationUser user, List<string> roles, string? currentUserId, bool canRemoveAdmin)
    {
        return new UserDto(
            user.Id,
            user.Email,
            user.UserName,
            user.FirstName,
            user.LastName,
            user.DisplayName,
            user.EmailConfirmed,
            user.GoogleEmail,
            user.GoogleLinkedAt,
            roles,
            string.Equals(user.Id, currentUserId, StringComparison.Ordinal),
            canRemoveAdmin);
    }

    private BadRequestObjectResult IdentityFailure(IdentityResult result)
    {
        return BadRequest(string.Join("; ", result.Errors.Select(e => e.Description)));
    }

    private static string? TrimOrNull(string? value)
    {
        return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
    }
}
