using System.Collections.ObjectModel;
using System.Globalization;
using System.Text.Json.Serialization;
using StellaOps.Scanner.Core.Utility;

namespace StellaOps.Scanner.Core.Contracts;

[JsonConverter(typeof(ScanJobIdJsonConverter))]
public readonly record struct ScanJobId(Guid Value)
{
    public static readonly ScanJobId Empty = new(Guid.Empty);

    public override string ToString()
        => Value.ToString("n", CultureInfo.InvariantCulture);

    public static ScanJobId From(Guid value)
        => new(value);

    public static bool TryParse(string? text, out ScanJobId id)
    {
        if (Guid.TryParse(text, out var guid))
        {
            id = new ScanJobId(guid);
            return true;
        }

        id = Empty;
        return false;
    }
}

[JsonConverter(typeof(JsonStringEnumConverter<ScanJobStatus>))]
public enum ScanJobStatus
{
    Unknown = 0,
    Pending,
    Queued,
    Running,
    Succeeded,
    Failed,
    Cancelled
}

public sealed class ScanJob
{
    private static readonly IReadOnlyDictionary<string, string> EmptyMetadata =
        new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(0, StringComparer.Ordinal));

    [JsonConstructor]
    public ScanJob(
        ScanJobId id,
        ScanJobStatus status,
        string imageReference,
        string? imageDigest,
        DateTimeOffset createdAt,
        DateTimeOffset? updatedAt,
        string correlationId,
        string? tenantId,
        IReadOnlyDictionary<string, string>? metadata = null,
        ScannerError? failure = null)
    {
        if (string.IsNullOrWhiteSpace(imageReference))
        {
            throw new ArgumentException("Image reference cannot be null or whitespace.", nameof(imageReference));
        }

        if (string.IsNullOrWhiteSpace(correlationId))
        {
            throw new ArgumentException("Correlation identifier cannot be null or whitespace.", nameof(correlationId));
        }

        Id = id;
        Status = status;
        ImageReference = imageReference.Trim();
        ImageDigest = NormalizeDigest(imageDigest);
        CreatedAt = ScannerTimestamps.Normalize(createdAt);
        UpdatedAt = updatedAt is null ? null : ScannerTimestamps.Normalize(updatedAt.Value);
        CorrelationId = correlationId;
        TenantId = string.IsNullOrWhiteSpace(tenantId) ? null : tenantId.Trim();
        Metadata = metadata is null or { Count: 0 }
            ? EmptyMetadata
            : new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(metadata, StringComparer.Ordinal));
        Failure = failure;
    }

    [JsonPropertyName("id")]
    [JsonPropertyOrder(0)]
    public ScanJobId Id { get; }

    [JsonPropertyName("status")]
    [JsonPropertyOrder(1)]
    public ScanJobStatus Status { get; init; }

    [JsonPropertyName("imageReference")]
    [JsonPropertyOrder(2)]
    public string ImageReference { get; }

    [JsonPropertyName("imageDigest")]
    [JsonPropertyOrder(3)]
    public string? ImageDigest { get; }

    [JsonPropertyName("createdAt")]
    [JsonPropertyOrder(4)]
    public DateTimeOffset CreatedAt { get; }

    [JsonPropertyName("updatedAt")]
    [JsonPropertyOrder(5)]
    public DateTimeOffset? UpdatedAt { get; init; }

    [JsonPropertyName("correlationId")]
    [JsonPropertyOrder(6)]
    public string CorrelationId { get; }

    [JsonPropertyName("tenantId")]
    [JsonPropertyOrder(7)]
    public string? TenantId { get; }

    [JsonPropertyName("metadata")]
    [JsonPropertyOrder(8)]
    public IReadOnlyDictionary<string, string> Metadata { get; }

    [JsonPropertyName("failure")]
    [JsonPropertyOrder(9)]
    public ScannerError? Failure { get; init; }

    public ScanJob WithStatus(ScanJobStatus status, DateTimeOffset? updatedAt = null)
        => new(
            Id,
            status,
            ImageReference,
            ImageDigest,
            CreatedAt,
            updatedAt ?? UpdatedAt ?? CreatedAt,
            CorrelationId,
            TenantId,
            Metadata,
            Failure);

    public ScanJob WithFailure(ScannerError failure, DateTimeOffset? updatedAt = null, TimeProvider? timeProvider = null)
        => new(
            Id,
            ScanJobStatus.Failed,
            ImageReference,
            ImageDigest,
            CreatedAt,
            updatedAt ?? ScannerTimestamps.UtcNow(timeProvider),
            CorrelationId,
            TenantId,
            Metadata,
            failure);

    private static string? NormalizeDigest(string? digest)
    {
        if (string.IsNullOrWhiteSpace(digest))
        {
            return null;
        }

        var trimmed = digest.Trim();
        if (!trimmed.StartsWith("sha", StringComparison.OrdinalIgnoreCase))
        {
            return trimmed;
        }

        var parts = trimmed.Split(':', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (parts.Length != 2)
        {
            return trimmed.ToLowerInvariant();
        }

        return $"{parts[0].ToLowerInvariant()}:{parts[1].ToLowerInvariant()}";
    }
}
