using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using StellaOps.Feedser.Core.Jobs;

namespace StellaOps.Feedser.Storage.Mongo;

[BsonIgnoreExtraElements]
public sealed class JobRunDocument
{
    [BsonId]
    public string Id { get; set; } = string.Empty;

    [BsonElement("kind")]
    public string Kind { get; set; } = string.Empty;

    [BsonElement("status")]
    public string Status { get; set; } = JobRunStatus.Pending.ToString();

    [BsonElement("trigger")]
    public string Trigger { get; set; } = string.Empty;

    [BsonElement("parameters")]
    public BsonDocument Parameters { get; set; } = new();

    [BsonElement("parametersHash")]
    [BsonIgnoreIfNull]
    public string? ParametersHash { get; set; }

    [BsonElement("createdAt")]
    public DateTime CreatedAt { get; set; }

    [BsonElement("startedAt")]
    [BsonIgnoreIfNull]
    public DateTime? StartedAt { get; set; }

    [BsonElement("completedAt")]
    [BsonIgnoreIfNull]
    public DateTime? CompletedAt { get; set; }

    [BsonElement("error")]
    [BsonIgnoreIfNull]
    public string? Error { get; set; }

    [BsonElement("timeoutMs")]
    [BsonIgnoreIfNull]
    public long? TimeoutMs { get; set; }

    [BsonElement("leaseMs")]
    [BsonIgnoreIfNull]
    public long? LeaseMs { get; set; }
}

internal static class JobRunDocumentExtensions
{
    public static JobRunDocument FromRequest(JobRunCreateRequest request, Guid id)
    {
        return new JobRunDocument
        {
            Id = id.ToString(),
            Kind = request.Kind,
            Status = JobRunStatus.Pending.ToString(),
            Trigger = request.Trigger,
            Parameters = request.Parameters is { Count: > 0 }
                ? BsonDocument.Parse(JsonSerializer.Serialize(request.Parameters))
                : new BsonDocument(),
            ParametersHash = request.ParametersHash,
            CreatedAt = request.CreatedAt.UtcDateTime,
            TimeoutMs = request.Timeout?.MillisecondsFromTimespan(),
            LeaseMs = request.LeaseDuration?.MillisecondsFromTimespan(),
        };
    }

    public static JobRunSnapshot ToSnapshot(this JobRunDocument document)
    {
        var parameters = document.Parameters?.ToDictionary() ?? new Dictionary<string, object?>();

        return new JobRunSnapshot(
            Guid.Parse(document.Id),
            document.Kind,
            Enum.Parse<JobRunStatus>(document.Status, ignoreCase: true),
            DateTime.SpecifyKind(document.CreatedAt, DateTimeKind.Utc),
            document.StartedAt.HasValue ? DateTime.SpecifyKind(document.StartedAt.Value, DateTimeKind.Utc) : null,
            document.CompletedAt.HasValue ? DateTime.SpecifyKind(document.CompletedAt.Value, DateTimeKind.Utc) : null,
            document.Trigger,
            document.ParametersHash,
            document.Error,
            document.TimeoutMs?.MillisecondsToTimespan(),
            document.LeaseMs?.MillisecondsToTimespan(),
            parameters);
    }

    public static Dictionary<string, object?> ToDictionary(this BsonDocument document)
    {
        return document.Elements.ToDictionary(
            static element => element.Name,
            static element => element.Value switch
            {
                BsonString s => (object?)s.AsString,
                BsonBoolean b => b.AsBoolean,
                BsonInt32 i => i.AsInt32,
                BsonInt64 l => l.AsInt64,
                BsonDouble d => d.AsDouble,
                BsonNull => null,
                BsonArray array => array.Select(v => v.IsBsonDocument ? ToDictionary(v.AsBsonDocument) : (object?)v.ToString()).ToArray(),
                BsonDocument doc => ToDictionary(doc),
                _ => element.Value.ToString(),
            });
    }

    private static long MillisecondsFromTimespan(this TimeSpan timeSpan)
        => (long)timeSpan.TotalMilliseconds;

    private static TimeSpan MillisecondsToTimespan(this long milliseconds)
        => TimeSpan.FromMilliseconds(milliseconds);
}
