using System.Security.Cryptography;
using System.Text;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;

namespace JobTrackerApi.Services;

// "Trust this device for 30 days" -- lets a 2FA challenge be skipped on the same browser for a
// month. Shared by AuthController (checks the cookie before gating on 2FA) and
// TwoFactorController (issues/lists/revokes the cookie's backing row). Never stores the
// plaintext token, only its SHA-256 hash.
public static class TrustedDeviceService
{
    private static readonly TimeSpan Lifetime = TimeSpan.FromDays(30);

    // Only returns true (and thus skips 2FA) when the cookie's hash matches a non-expired row
    // for THIS SPECIFIC user -- UserId is part of the DB query itself, not a check applied
    // after the fact, so a trusted-device cookie minted for user A can never skip 2FA for user
    // B even if somehow presented on their request.
    public static async Task<bool> IsDeviceTrustedAsync(JobTrackerContext db, HttpRequest request, string userId, CancellationToken cancellationToken)
    {
        var token = request.Cookies[AuthSessionOptions.TrustedDeviceCookieName];
        if (string.IsNullOrWhiteSpace(token)) return false;

        var hash = HashToken(token);
        var now = DateTimeOffset.UtcNow;
        // SQLite/Pomelo cannot translate DateTimeOffset relational comparisons (>) to SQL, so the
        // expiry check has to happen after materializing the row -- fine here since the equality
        // filters (UserId, TokenHash) already narrow this to at most one row.
        var match = await db.TrustedDevices
            .IgnoreQueryFilters()
            .FirstOrDefaultAsync(x => x.UserId == userId && x.TokenHash == hash, cancellationToken);
        if (match is null || match.ExpiresAtUtc <= now) return false;

        match.LastSeenAtUtc = now;
        await db.SaveChangesAsync(cancellationToken);
        return true;
    }

    public static async Task IssueAsync(JobTrackerContext db, HttpRequest request, HttpResponse response, string userId, bool secureCookies, CancellationToken cancellationToken)
    {
        var token = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)).ToLowerInvariant();
        var now = DateTimeOffset.UtcNow;

        db.TrustedDevices.Add(new TrustedDevice
        {
            UserId = userId,
            TokenHash = HashToken(token),
            DeviceLabel = DescribeUserAgent(request.Headers["User-Agent"].ToString()),
            CreatedAtUtc = now,
            LastSeenAtUtc = now,
            ExpiresAtUtc = now.Add(Lifetime),
        });
        await db.SaveChangesAsync(cancellationToken);

        response.Cookies.Append(AuthSessionOptions.TrustedDeviceCookieName, token, AuthSessionOptions.BuildTrustedDeviceCookie(secureCookies));
    }

    public static void ClearCookie(HttpResponse response, bool secureCookies)
    {
        response.Cookies.Delete(AuthSessionOptions.TrustedDeviceCookieName, AuthSessionOptions.BuildExpiredTrustedDeviceCookie(secureCookies));
    }

    // Used to flag "this device" in the trusted-devices list without ever sending a token or
    // hash to the client -- just a boolean per row.
    public static string? CurrentDeviceTokenHash(HttpRequest request)
    {
        var token = request.Cookies[AuthSessionOptions.TrustedDeviceCookieName];
        return string.IsNullOrWhiteSpace(token) ? null : HashToken(token);
    }

    public static string HashToken(string token)
    {
        return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token.Trim()))).ToLowerInvariant();
    }

    private static readonly (string Needle, string Label)[] BrowserMarkers =
    {
        ("Edg/", "Edge"),
        ("OPR/", "Opera"),
        ("Chrome/", "Chrome"),
        ("Firefox/", "Firefox"),
        ("Safari/", "Safari"),
    };

    private static readonly (string Needle, string Label)[] OsMarkers =
    {
        ("Windows", "Windows"),
        ("Mac OS X", "Mac"),
        ("iPhone", "iOS"),
        ("iPad", "iOS"),
        ("Android", "Android"),
        ("Linux", "Linux"),
    };

    // ponytail: substring sniffing, not a real UA parser -- this only feeds a display label in
    // a security-settings list ("Chrome on Windows"), nothing security-relevant depends on it.
    // Public: also reused by AppSessionIssuer for UserSession device labels.
    public static string? DescribeUserAgent(string? userAgent)
    {
        if (string.IsNullOrWhiteSpace(userAgent)) return null;

        var browser = BrowserMarkers.FirstOrDefault(m => userAgent.Contains(m.Needle, StringComparison.Ordinal)).Label;
        var os = OsMarkers.FirstOrDefault(m => userAgent.Contains(m.Needle, StringComparison.Ordinal)).Label;

        if (browser is null && os is null) return userAgent.Length > 80 ? userAgent[..80] : userAgent;
        if (browser is null) return os;
        if (os is null) return browser;
        return $"{browser} on {os}";
    }
}
