using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Driver;
using System.Linq;
using System.Globalization;
using StellaOps.Authority.Storage.Mongo.Documents;

namespace StellaOps.Authority.Storage.Mongo.Stores;

internal sealed class AuthorityTokenStore : IAuthorityTokenStore
{
    private readonly IMongoCollection<AuthorityTokenDocument> collection;
    private readonly ILogger<AuthorityTokenStore> logger;

    public AuthorityTokenStore(
        IMongoCollection<AuthorityTokenDocument> collection,
        ILogger<AuthorityTokenStore> logger)
    {
        this.collection = collection ?? throw new ArgumentNullException(nameof(collection));
        this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async ValueTask InsertAsync(AuthorityTokenDocument document, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(document);

        await collection.InsertOneAsync(document, cancellationToken: cancellationToken).ConfigureAwait(false);
        logger.LogDebug("Inserted Authority token {TokenId}.", document.TokenId);
    }

    public async ValueTask<AuthorityTokenDocument?> FindByTokenIdAsync(string tokenId, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(tokenId))
        {
            return null;
        }

        var id = tokenId.Trim();
        return await collection.Find(t => t.TokenId == id)
            .FirstOrDefaultAsync(cancellationToken)
            .ConfigureAwait(false);
    }

    public async ValueTask<AuthorityTokenDocument?> FindByReferenceIdAsync(string referenceId, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(referenceId))
        {
            return null;
        }

        var id = referenceId.Trim();
        return await collection.Find(t => t.ReferenceId == id)
            .FirstOrDefaultAsync(cancellationToken)
            .ConfigureAwait(false);
    }

    public async ValueTask UpdateStatusAsync(
        string tokenId,
        string status,
        DateTimeOffset? revokedAt,
        string? reason,
        string? reasonDescription,
        IReadOnlyDictionary<string, string?>? metadata,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(tokenId))
        {
            throw new ArgumentException("Token id cannot be empty.", nameof(tokenId));
        }

        if (string.IsNullOrWhiteSpace(status))
        {
            throw new ArgumentException("Status cannot be empty.", nameof(status));
        }

        var update = Builders<AuthorityTokenDocument>.Update
            .Set(t => t.Status, status)
            .Set(t => t.RevokedAt, revokedAt)
            .Set(t => t.RevokedReason, reason)
            .Set(t => t.RevokedReasonDescription, reasonDescription)
            .Set(t => t.RevokedMetadata, metadata is null ? null : new Dictionary<string, string?>(metadata, StringComparer.OrdinalIgnoreCase));

        var result = await collection.UpdateOneAsync(
            Builders<AuthorityTokenDocument>.Filter.Eq(t => t.TokenId, tokenId.Trim()),
            update,
            cancellationToken: cancellationToken).ConfigureAwait(false);

        logger.LogDebug("Updated token {TokenId} status to {Status} (matched {Matched}).", tokenId, status, result.MatchedCount);
    }


    public async ValueTask<TokenUsageUpdateResult> RecordUsageAsync(string tokenId, string? remoteAddress, string? userAgent, DateTimeOffset observedAt, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(tokenId))
        {
            return new TokenUsageUpdateResult(TokenUsageUpdateStatus.NotFound, null, null);
        }

        if (string.IsNullOrWhiteSpace(remoteAddress) && string.IsNullOrWhiteSpace(userAgent))
        {
            return new TokenUsageUpdateResult(TokenUsageUpdateStatus.MissingMetadata, remoteAddress, userAgent);
        }

        var id = tokenId.Trim();
        var token = await collection
            .Find(t => t.TokenId == id)
            .FirstOrDefaultAsync(cancellationToken)
            .ConfigureAwait(false);

        if (token is null)
        {
            return new TokenUsageUpdateResult(TokenUsageUpdateStatus.NotFound, remoteAddress, userAgent);
        }

        token.Devices ??= new List<BsonDocument>();

        string? normalizedAddress = string.IsNullOrWhiteSpace(remoteAddress) ? null : remoteAddress.Trim();
        string? normalizedAgent = string.IsNullOrWhiteSpace(userAgent) ? null : userAgent.Trim();

        var device = token.Devices.FirstOrDefault(d =>
            string.Equals(GetString(d, "remoteAddress"), normalizedAddress, StringComparison.OrdinalIgnoreCase) &&
            string.Equals(GetString(d, "userAgent"), normalizedAgent, StringComparison.Ordinal));
        var suspicious = false;

        if (device is null)
        {
            suspicious = token.Devices.Count > 0;
            var document = new BsonDocument
            {
                { "remoteAddress", normalizedAddress },
                { "userAgent", normalizedAgent },
                { "firstSeen", BsonDateTime.Create(observedAt.UtcDateTime) },
                { "lastSeen", BsonDateTime.Create(observedAt.UtcDateTime) },
                { "useCount", 1 }
            };

            token.Devices.Add(document);
        }
        else
        {
            device["lastSeen"] = BsonDateTime.Create(observedAt.UtcDateTime);
            device["useCount"] = device.TryGetValue("useCount", out var existingCount) && existingCount.IsInt32
                ? existingCount.AsInt32 + 1
                : 1;
        }

        var update = Builders<AuthorityTokenDocument>.Update.Set(t => t.Devices, token.Devices);
        await collection.UpdateOneAsync(
            Builders<AuthorityTokenDocument>.Filter.Eq(t => t.TokenId, id),
            update,
            cancellationToken: cancellationToken).ConfigureAwait(false);

        return new TokenUsageUpdateResult(suspicious ? TokenUsageUpdateStatus.SuspectedReplay : TokenUsageUpdateStatus.Recorded, normalizedAddress, normalizedAgent);
    }

    private static string? GetString(BsonDocument document, string name)
    {
        if (!document.TryGetValue(name, out var value))
        {
            return null;
        }

        return value switch
        {
            { IsString: true } => value.AsString,
            { IsBsonNull: true } => null,
            _ => value.ToString()
        };
    }

    public async ValueTask<long> DeleteExpiredAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
    {
        var filter = Builders<AuthorityTokenDocument>.Filter.And(
            Builders<AuthorityTokenDocument>.Filter.Not(
                Builders<AuthorityTokenDocument>.Filter.Eq(t => t.Status, "revoked")),
            Builders<AuthorityTokenDocument>.Filter.Lt(t => t.ExpiresAt, threshold));

        var result = await collection.DeleteManyAsync(filter, cancellationToken).ConfigureAwait(false);
        if (result.DeletedCount > 0)
        {
            logger.LogInformation("Deleted {Count} expired Authority tokens.", result.DeletedCount);
        }

        return result.DeletedCount;
    }

    public async ValueTask<IReadOnlyList<AuthorityTokenDocument>> ListRevokedAsync(DateTimeOffset? issuedAfter, CancellationToken cancellationToken)
    {
        var filter = Builders<AuthorityTokenDocument>.Filter.Eq(t => t.Status, "revoked");

        if (issuedAfter is DateTimeOffset threshold)
        {
            filter = Builders<AuthorityTokenDocument>.Filter.And(
                filter,
                Builders<AuthorityTokenDocument>.Filter.Gt(t => t.RevokedAt, threshold));
        }

        var documents = await collection
            .Find(filter)
            .Sort(Builders<AuthorityTokenDocument>.Sort.Ascending(t => t.RevokedAt).Ascending(t => t.TokenId))
            .ToListAsync(cancellationToken)
            .ConfigureAwait(false);

        return documents;
    }
}
