using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;

namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;

/// <summary>
/// EF Core implementation of ISiteRepository for site and data connection management.
/// </summary>
public class SiteRepository : ISiteRepository
{
    private readonly ScadaBridgeDbContext _dbContext;
    private readonly ITemplateGraphWatermark? _watermark;

    /// <summary>
    /// Initializes a new instance of the SiteRepository.
    /// </summary>
    /// <param name="dbContext">The database context.</param>
    /// <param name="watermark">
    /// Graph version watermark bumped when a committed change alters a flattening
    /// input this repository owns (see <see cref="SaveChangesAsync"/>). Optional so
    /// existing single-argument construction (tests, ad-hoc tooling) keeps
    /// compiling; when absent the caches simply never get the invalidation signal
    /// from this repository, which is the pre-existing behaviour.
    /// </param>
    public SiteRepository(ScadaBridgeDbContext dbContext, ITemplateGraphWatermark? watermark = null)
    {
        _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
        _watermark = watermark;
    }

    // --- Sites ---

    /// <inheritdoc />
    public async Task<Site?> GetSiteByIdAsync(int id, CancellationToken cancellationToken = default)
    {
        return await _dbContext.Sites.FindAsync([id], cancellationToken);
    }

    /// <inheritdoc />
    public async Task<Site?> GetSiteByIdentifierAsync(string siteIdentifier, CancellationToken cancellationToken = default)
    {
        return await _dbContext.Sites
            .FirstOrDefaultAsync(s => s.SiteIdentifier == siteIdentifier, cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<Site>> GetAllSitesAsync(CancellationToken cancellationToken = default)
    {
        return await _dbContext.Sites.OrderBy(s => s.Name).ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task AddSiteAsync(Site site, CancellationToken cancellationToken = default)
    {
        await _dbContext.Sites.AddAsync(site, cancellationToken);
    }

    /// <inheritdoc />
    public Task UpdateSiteAsync(Site site, CancellationToken cancellationToken = default)
    {
        _dbContext.Sites.Update(site);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public Task DeleteSiteAsync(int id, CancellationToken cancellationToken = default)
    {
        var entity = _dbContext.Sites.Local.FirstOrDefault(s => s.Id == id);
        if (entity != null)
        {
            _dbContext.Sites.Remove(entity);
        }
        else
        {
            var stub = new Site("stub", "stub") { Id = id };
            _dbContext.Sites.Attach(stub);
            _dbContext.Sites.Remove(stub);
        }
        return Task.CompletedTask;
    }

    // --- Data Connections ---

    /// <inheritdoc />
    public async Task<DataConnection?> GetDataConnectionByIdAsync(int id, CancellationToken cancellationToken = default)
    {
        return await _dbContext.DataConnections.FindAsync([id], cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<DataConnection>> GetAllDataConnectionsAsync(CancellationToken cancellationToken = default)
    {
        return await _dbContext.DataConnections.OrderBy(c => c.Name).ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<DataConnection>> GetDataConnectionsBySiteIdAsync(int siteId, CancellationToken cancellationToken = default)
    {
        return await _dbContext.DataConnections
            .Where(c => c.SiteId == siteId)
            .OrderBy(c => c.Name)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task AddDataConnectionAsync(DataConnection connection, CancellationToken cancellationToken = default)
    {
        await _dbContext.DataConnections.AddAsync(connection, cancellationToken);
    }

    /// <inheritdoc />
    public Task UpdateDataConnectionAsync(DataConnection connection, CancellationToken cancellationToken = default)
    {
        _dbContext.DataConnections.Update(connection);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public Task DeleteDataConnectionAsync(int id, CancellationToken cancellationToken = default)
    {
        var entity = _dbContext.DataConnections.Local.FirstOrDefault(c => c.Id == id);
        if (entity != null)
        {
            _dbContext.DataConnections.Remove(entity);
        }
        else
        {
            var stub = new DataConnection("stub", "stub", 0) { Id = id };
            _dbContext.DataConnections.Attach(stub);
            _dbContext.DataConnections.Remove(stub);
        }
        return Task.CompletedTask;
    }

    // --- Instances (for deletion / move constraint checks) ---

    /// <inheritdoc />
    public async Task<IReadOnlyList<Instance>> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default)
    {
        return await _dbContext.Instances
            .Where(i => i.SiteId == siteId)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<Instance>> GetInstancesWithNativeAlarmOverridesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default)
    {
        return await _dbContext.Instances
            .Where(i => i.SiteId == siteId)
            .Include(i => i.NativeAlarmSourceOverrides)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<Instance>> GetInstancesReferencingDataConnectionAsync(int dataConnectionId, CancellationToken cancellationToken = default)
    {
        // The distinct instance ids whose bindings reference the connection.
        // Bindings are not exposed via their own DbSet on ISiteRepository, so
        // resolve them through the InstanceConnectionBindings set, then load the
        // owning instances (cheap; the blocker list is expected to be tiny).
        var instanceIds = await _dbContext.InstanceConnectionBindings
            .Where(b => b.DataConnectionId == dataConnectionId)
            .Select(b => b.InstanceId)
            .Distinct()
            .ToListAsync(cancellationToken);

        if (instanceIds.Count == 0)
            return Array.Empty<Instance>();

        return await _dbContext.Instances
            .Where(i => instanceIds.Contains(i.Id))
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    /// <remarks>
    /// Bumps the <see cref="ITemplateGraphWatermark"/> after a successful commit
    /// that touched a <see cref="DataConnection"/>.
    ///
    /// <para>
    /// A data connection's <c>Protocol</c>, primary/backup configuration and
    /// failover retry count are flattening inputs — <c>FlatteningService</c>
    /// packages them into the flattened config's <c>Connections</c> map and
    /// <c>RevisionHashService</c> folds them into the revision hash. Without a bump,
    /// the process-wide <c>StaleInstanceProbe</c> memo and the flatten-session
    /// caches keep reporting the pre-edit hash for every instance bound to the
    /// connection, so an instance whose deployed config genuinely drifted reads as
    /// up to date.
    /// </para>
    ///
    /// <para>
    /// The bump is <see cref="ITemplateGraphWatermark.BumpAll"/> rather than a
    /// per-template one because a connection has no owning template: the affected
    /// set is "every instance at the site bound to it", which is exactly what the
    /// unattributed fallback is for. Applied only AFTER a successful commit, so a
    /// failed save does not invalidate caches for a change that never landed.
    /// Sites, areas and instances saved here are left alone — only the connection
    /// rows feed the flattener.
    /// </para>
    /// </remarks>
    public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        // Inspect BEFORE committing: afterwards every entry is Unchanged and the
        // attribution is lost.
        var connectionsChanged = _watermark is not null && HasPendingDataConnectionChange();

        var written = await _dbContext.SaveChangesAsync(cancellationToken);

        if (connectionsChanged)
            _watermark!.BumpAll();

        return written;
    }

    /// <summary>
    /// True when the change tracker holds an added, modified or deleted
    /// <see cref="DataConnection"/>.
    /// </summary>
    /// <returns>Whether a data-connection mutation is about to be committed.</returns>
    private bool HasPendingDataConnectionChange()
    {
        foreach (var entry in _dbContext.ChangeTracker.Entries<DataConnection>())
        {
            if (entry.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)
                return true;
        }

        return false;
    }
}
