using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace ZB.MOM.WW.GalaxyRepository;

/// <summary>
///     JSON-file implementation of <see cref="IGalaxyHierarchySnapshotStore"/>.
///     Writes the on-disk snapshot atomically (temp file + rename) so a crash
///     mid-write can never leave a torn file, and ignores files whose schema
///     version it does not recognize. When
///     <see cref="GalaxyRepositoryOptions.PersistSnapshot"/> is <see langword="false"/>
///     — or <see cref="GalaxyRepositoryOptions.SnapshotCachePath"/> is empty —
///     both operations are no-ops. The snapshot path is fully consumer-supplied;
///     this store imposes no platform-specific default, so it is cross-platform.
/// </summary>
public sealed class GalaxyHierarchySnapshotStore : IGalaxyHierarchySnapshotStore, IDisposable
{
    /// <summary>
    ///     On-disk format version. Bump this whenever the persisted shape changes
    ///     in a way an older or newer consumer cannot read; a mismatched file is
    ///     ignored rather than misparsed.
    /// </summary>
    private const int CurrentSchemaVersion = 1;

    private static readonly JsonSerializerOptions SerializerOptions = new()
    {
        WriteIndented = false,
    };

    private readonly string? _path;
    private readonly TimeSpan _writeTimeout;
    private readonly ILogger<GalaxyHierarchySnapshotStore>? _logger;
    private readonly SemaphoreSlim _ioGate = new(1, 1);

    /// <summary>Initializes a new instance of the <see cref="GalaxyHierarchySnapshotStore"/> class.</summary>
    /// <param name="options">Galaxy repository options carrying the snapshot path and enable flag.</param>
    /// <param name="logger">Optional logger for diagnostic output.</param>
    public GalaxyHierarchySnapshotStore(
        IOptions<GalaxyRepositoryOptions> options,
        ILogger<GalaxyHierarchySnapshotStore>? logger = null)
    {
        GalaxyRepositoryOptions value = options.Value;
        _path = value.PersistSnapshot && !string.IsNullOrWhiteSpace(value.SnapshotCachePath)
            ? value.SnapshotCachePath
            : null;
        _writeTimeout = TimeSpan.FromSeconds(Math.Max(1, value.CommandTimeoutSeconds));
        _logger = logger;
    }

    /// <inheritdoc />
    public async Task SaveAsync(GalaxyHierarchySnapshot snapshot, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(snapshot);
        if (_path is null)
        {
            return;
        }

        PersistedFile file = new(CurrentSchemaVersion, snapshot);

        await _ioGate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            // Bound the write so a stuck disk — e.g. a SnapshotCachePath on an
            // unresponsive network share — cannot stall the caller. On the cache
            // refresh path that would otherwise pin the whole refresh loop.
            using CancellationTokenSource writeCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            writeCts.CancelAfter(_writeTimeout);

            string? directory = Path.GetDirectoryName(_path);
            if (!string.IsNullOrEmpty(directory))
            {
                Directory.CreateDirectory(directory);
            }

            string tempPath = _path + ".tmp";
            await using (FileStream stream = new(tempPath, FileMode.Create, FileAccess.Write, FileShare.None))
            {
                await JsonSerializer.SerializeAsync(stream, file, SerializerOptions, writeCts.Token).ConfigureAwait(false);
            }

            File.Move(tempPath, _path, overwrite: true);
            _logger?.LogDebug(
                "Persisted Galaxy hierarchy snapshot to {Path} ({ObjectCount} objects, {AttributeCount} attributes).",
                _path,
                snapshot.Hierarchy.Count,
                snapshot.Attributes.Count);
        }
        finally
        {
            _ioGate.Release();
        }
    }

    /// <inheritdoc />
    public async Task<GalaxyHierarchySnapshot?> TryLoadAsync(CancellationToken cancellationToken)
    {
        if (_path is null || !File.Exists(_path))
        {
            return null;
        }

        await _ioGate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            PersistedFile? file;
            await using (FileStream stream = new(_path, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                file = await JsonSerializer.DeserializeAsync<PersistedFile>(
                    stream, SerializerOptions, cancellationToken).ConfigureAwait(false);
            }

            if (file is null || file.SchemaVersion != CurrentSchemaVersion || file.Snapshot is null)
            {
                _logger?.LogWarning(
                    "Ignoring Galaxy hierarchy snapshot at {Path}: unrecognized or empty schema version.",
                    _path);
                return null;
            }

            return file.Snapshot;
        }
        catch (Exception exception) when (exception is JsonException or IOException or UnauthorizedAccessException)
        {
            // A corrupt, truncated, locked, or access-denied snapshot file is an
            // expected failure mode for a disk cache — honor the Try contract and
            // return null rather than throwing.
            _logger?.LogWarning(
                exception,
                "Ignoring Galaxy hierarchy snapshot at {Path}: the file is unreadable or not valid JSON.",
                _path);
            return null;
        }
        finally
        {
            _ioGate.Release();
        }
    }

    /// <summary>
    ///     Disposes the I/O gate. As a DI singleton the store is disposed once at host
    ///     shutdown, by which point no save/load is in flight.
    /// </summary>
    public void Dispose()
    {
        _ioGate.Dispose();
    }

    /// <summary>On-disk envelope: a schema version plus the snapshot payload.</summary>
    private sealed record PersistedFile(int SchemaVersion, GalaxyHierarchySnapshot? Snapshot);
}
