using DeepDrftData.Data;
using DeepDrftModels.Entities;
using DeepDrftModels.Enums;
using Microsoft.EntityFrameworkCore;

namespace DeepDrftData.Repositories;

/// <summary>
/// Data access for the Phase 16 anonymous telemetry tables (all SQL — the FileDatabase vault is not
/// involved). Owns the append-only writes to <c>play_event</c> / <c>share_event</c> and the
/// incremental-on-write bump of the <c>play_counter</c> rollup (D6). Server-side release resolution
/// (§2.3 / D4) lives here: a play event carries only the track key, and this repository joins
/// track→release at write time and stamps the release id on the row.
///
/// <para>
/// Unlike <see cref="TrackRepository"/> these entities are not <c>BaseEntity</c>/<c>IEntity</c> (no
/// soft-delete lifecycle), so this is a plain context-backed repository rather than an extension of the
/// BlazorBlocks <c>Repository&lt;&gt;</c> base. It holds the same scoped <see cref="DeepDrftContext"/>
/// the rest of the SQL layer uses, never a service locator.
/// </para>
/// </summary>
public class EventRepository
{
    private readonly DeepDrftContext _context;

    public EventRepository(DeepDrftContext context)
    {
        _context = context;
    }

    /// <summary>
    /// Append one play event and bump the track's counter in a single transaction (D6). The release id
    /// is resolved here from the track key (§2.3 / D4): a live track contributes its release id (null
    /// for a loose track); an unknown key records the event with a null release and no counter bump
    /// (there is no track to roll up against). Returns true when the event was written.
    /// </summary>
    public async Task<bool> RecordPlayAsync(
        string trackEntryKey, PlayBucket bucket, string? anonId, CancellationToken ct = default)
    {
        // Resolve the track→release link server-side. Soft-deleted tracks resolve to null so a play of
        // a since-removed track still logs (with no counter bump) rather than throwing.
        var track = await _context.Tracks
            .Where(t => t.EntryKey == trackEntryKey && !t.IsDeleted)
            .Select(t => new { t.Id, t.ReleaseId })
            .FirstOrDefaultAsync(ct);

        // The append and the counter bump must commit together — wrap them in one transaction so a
        // counter that drifts from the log is impossible. Reuse an ambient transaction if the caller
        // already opened one.
        var ownsTransaction = _context.Database.CurrentTransaction is null;
        var transaction = ownsTransaction
            ? await _context.Database.BeginTransactionAsync(ct)
            : null;
        try
        {
            _context.PlayEvents.Add(new PlayEvent
            {
                TrackEntryKey = trackEntryKey,
                ReleaseId = track?.ReleaseId,
                Bucket = bucket,
                AnonId = anonId,
                CreatedAt = DateTime.UtcNow,
            });

            if (track is not null)
                await BumpCounterAsync(track.Id, bucket, ct);

            await _context.SaveChangesAsync(ct);
            if (transaction is not null)
                await transaction.CommitAsync(ct);
            return true;
        }
        catch
        {
            if (transaction is not null)
                await transaction.RollbackAsync(ct);
            throw;
        }
        finally
        {
            if (transaction is not null)
                await transaction.DisposeAsync();
        }
    }

    /// <summary>
    /// Site-wide total plays: the sum of every counter's three bucket columns across all rows (Phase 16
    /// §5). Sums the mapped columns directly rather than <see cref="PlayCounter.TotalPlays"/>, which is an
    /// EF-ignored computed property and so not translatable. An empty counter table sums to 0 (the home
    /// card's expected reading until the telemetry migration is applied).
    /// </summary>
    public Task<long> CountTotalPlaysAsync(CancellationToken ct = default)
        => _context.PlayCounters
            .SumAsync(c => c.PartialCount + c.SampledCount + c.CompleteCount, ct);

    /// <summary>
    /// Count distinct non-null anon ids across every play event (Phase 16 §3 / §4.2 — the all-time
    /// unique-listener metric, D3). Null anon ids (events where the listener sent no token, or storage
    /// was unavailable) are excluded — they are not a known listener and must not inflate the count. This
    /// is the site-wide listener reach figure; the per-track / per-release overloads scope it.
    /// </summary>
    public Task<int> CountDistinctListenersAsync(CancellationToken ct = default)
        => _context.PlayEvents
            .Where(e => e.AnonId != null)
            .Select(e => e.AnonId)
            .Distinct()
            .CountAsync(ct);

    /// <summary>
    /// Distinct listeners for one track, keyed by its vault entry key (the same key the play event
    /// stamps). Null anon ids excluded. Per-track scope of <see cref="CountDistinctListenersAsync()"/>.
    /// </summary>
    public Task<int> CountDistinctListenersForTrackAsync(string trackEntryKey, CancellationToken ct = default)
        => _context.PlayEvents
            .Where(e => e.TrackEntryKey == trackEntryKey && e.AnonId != null)
            .Select(e => e.AnonId)
            .Distinct()
            .CountAsync(ct);

    /// <summary>
    /// Distinct listeners for one release, derived across the release's tracks (D4): the play event
    /// stamps the resolved release id at write time, so a distinct count over <c>anon_id</c> filtered by
    /// <c>release_id</c> is exactly "distinct listeners who played any track in this release." Null anon
    /// ids excluded. A listener who heard two tracks of the release counts once (it is a distinct count
    /// over the union, not a sum of per-track counts).
    /// </summary>
    public Task<int> CountDistinctListenersForReleaseAsync(long releaseId, CancellationToken ct = default)
        => _context.PlayEvents
            .Where(e => e.ReleaseId == releaseId && e.AnonId != null)
            .Select(e => e.AnonId)
            .Distinct()
            .CountAsync(ct);

    /// <summary>Append one share event. No rollup table for shares in wave 16.1 — a plain insert.</summary>
    public async Task RecordShareAsync(
        ShareTargetType targetType, string targetKey, ShareChannel channel, string? anonId,
        CancellationToken ct = default)
    {
        _context.ShareEvents.Add(new ShareEvent
        {
            TargetType = targetType,
            TargetKey = targetKey,
            Channel = channel,
            AnonId = anonId,
            CreatedAt = DateTime.UtcNow,
        });
        await _context.SaveChangesAsync(ct);
    }

    // Bump the matching bucket column on the track's counter row, creating the row on first play. The
    // row is added to the change tracker but not saved here — the caller's SaveChanges/commit persists
    // it inside the same transaction as the event append.
    //
    // Race note: two concurrent first-plays of the same track can both reach this method, find no
    // counter row, and both Add a new PlayCounter. The second SaveChanges will hit the unique index on
    // (track_id) and throw, causing the outer transaction to roll back and the event to be dropped —
    // no crash, no counter corruption. At the expected play volume this is an acceptable loss; the
    // unique index is the integrity backstop.
    private async Task BumpCounterAsync(long trackId, PlayBucket bucket, CancellationToken ct)
    {
        var counter = await _context.PlayCounters.FirstOrDefaultAsync(c => c.TrackId == trackId, ct);
        if (counter is null)
        {
            counter = new PlayCounter { TrackId = trackId };
            _context.PlayCounters.Add(counter);
        }

        switch (bucket)
        {
            case PlayBucket.Partial: counter.PartialCount++; break;
            case PlayBucket.Sampled: counter.SampledCount++; break;
            case PlayBucket.Complete: counter.CompleteCount++; break;
        }
    }
}
