using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace JobTrackerApi.Controllers;

// List/revoke the server-tracked UserSession rows behind the JWTs AppSessionIssuer hands out.
// Not 2FA-specific (any local-auth user has sessions, 2FA or not), hence its own small controller
// rather than folding into TwoFactorController.
[ApiController]
[Route("api/auth/sessions")]
[Authorize(AuthenticationSchemes = "local")]
public sealed class SessionsController : ControllerBase
{
    private readonly UserManager<ApplicationUser> _users;
    private readonly JobTrackerContext _db;
    private readonly ExternalOrigin _externalOrigin;

    public SessionsController(UserManager<ApplicationUser> users, JobTrackerContext db, ExternalOrigin? externalOrigin = null)
    {
        _users = users;
        _db = db;
        _externalOrigin = externalOrigin ?? ExternalOrigin.Parse(null, production: false);
    }

    public sealed record SessionDto(string Id, string? DeviceLabel, DateTimeOffset CreatedAtUtc, DateTimeOffset LastSeenAtUtc, DateTimeOffset ExpiresAtUtc, bool IsCurrentSession);

    private string? CurrentSid => User.FindFirst("sid")?.Value;

    [HttpGet]
    public async Task<IActionResult> List(CancellationToken cancellationToken)
    {
        var user = await _users.GetUserAsync(User);
        if (user is null) return Unauthorized();

        var now = DateTimeOffset.UtcNow;
        var currentSid = CurrentSid;
        // IgnoreQueryFilters + an explicit UserId filter, same convention as
        // TrustedDeviceService/TwoFactorController's device-list queries.
        // Equality-only in the DB query, then filter/sort DateTimeOffset client-side: SQLite's EF
        // Core provider cannot translate ">" or ORDER BY over DateTimeOffset to SQL ("SQLite does
        // not support expressions of type 'DateTimeOffset' in ORDER BY clauses"), so ExpiresAtUtc
        // comparison and the LastSeenAtUtc sort have to happen after materializing the (small,
        // per-user) row set.
        var candidates = await _db.UserSessions.IgnoreQueryFilters()
            .Where(x => x.UserId == user.Id && x.RevokedAtUtc == null)
            .ToListAsync(cancellationToken);

        var sessions = candidates
            .Where(x => x.ExpiresAtUtc > now)
            .OrderByDescending(x => x.LastSeenAtUtc)
            .Select(x => new SessionDto(x.Id, x.DeviceLabel, x.CreatedAtUtc, x.LastSeenAtUtc, x.ExpiresAtUtc, x.Id == currentSid))
            .ToList();

        return Ok(sessions);
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> Revoke(string id, CancellationToken cancellationToken)
    {
        var user = await _users.GetUserAsync(User);
        if (user is null) return Unauthorized();

        var session = await _db.UserSessions.IgnoreQueryFilters().FirstOrDefaultAsync(x => x.Id == id && x.UserId == user.Id, cancellationToken);
        if (session is null) return NotFound();

        session.RevokedAtUtc = DateTimeOffset.UtcNow;
        await _db.SaveChangesAsync(cancellationToken);

        if (string.Equals(id, CurrentSid, StringComparison.Ordinal))
        {
            Response.Cookies.Delete(AuthSessionOptions.SessionCookieName, AuthSessionOptions.BuildExpiredCookie(_externalOrigin.UsesHttps));
        }

        return NoContent();
    }

    [HttpPost("revoke-others")]
    public async Task<IActionResult> RevokeOthers(CancellationToken cancellationToken)
    {
        var user = await _users.GetUserAsync(User);
        if (user is null) return Unauthorized();

        var currentSid = CurrentSid;
        var now = DateTimeOffset.UtcNow;
        var others = await _db.UserSessions.IgnoreQueryFilters()
            .Where(x => x.UserId == user.Id && x.RevokedAtUtc == null && x.Id != currentSid)
            .ToListAsync(cancellationToken);

        foreach (var session in others)
        {
            session.RevokedAtUtc = now;
        }
        if (others.Count > 0)
        {
            await _db.SaveChangesAsync(cancellationToken);
        }

        return NoContent();
    }
}
