using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;

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

public class TemplateEngineRepository : ITemplateEngineRepository
{
    private readonly ScadaBridgeDbContext _context;
    private readonly ITemplateGraphWatermark _watermark;

    /// <summary>
    /// Initializes a new instance of the TemplateEngineRepository class.
    /// </summary>
    /// <param name="context">The database context used to access template and instance data.</param>
    /// <param name="watermark">
    /// Process-wide template/instance version watermark. This repository is the
    /// single unit-of-work through which EVERY template-graph mutation commits —
    /// <c>TemplateService</c>, the <c>ManagementActor</c> native-alarm-source
    /// handlers, and the Transport bundle importer all funnel through the same
    /// <see cref="SaveChangesAsync"/> — so bumping here (rather than in each
    /// caller) is the only placement that cannot be bypassed.
    /// </param>
    public TemplateEngineRepository(ScadaBridgeDbContext context, ITemplateGraphWatermark watermark)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
        _watermark = watermark ?? throw new ArgumentNullException(nameof(watermark));
    }

    // Template

    /// <inheritdoc />
    public async Task<Template?> GetTemplateByIdAsync(int id, CancellationToken cancellationToken = default)
    {
        return await _context.Templates
            .Include(t => t.Attributes)
            .Include(t => t.Alarms)
            .Include(t => t.Scripts)
            .Include(t => t.Compositions)
            .Include(t => t.NativeAlarmSources)
            .AsSplitQuery()
            .FirstOrDefaultAsync(t => t.Id == id, cancellationToken);
    }

    /// <inheritdoc />
    public async Task<Template?> GetTemplateWithChildrenAsync(int id, CancellationToken cancellationToken = default)
    {
        return await GetTemplateByIdAsync(id, cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<Template>> GetTemplatesWithChildrenAsync(
        IEnumerable<string> names, CancellationToken cancellationToken = default)
    {
        // Bulk lookup replaces the per-name N+1 in
        // BundleImporter.PreviewAsync. Filter out null / empty / duplicate
        // names before the query so EF emits a clean, deduplicated IN clause.
        if (names is null) return Array.Empty<Template>();
        var distinct = names
            .Where(n => !string.IsNullOrEmpty(n))
            .Distinct(StringComparer.Ordinal)
            .ToArray();
        if (distinct.Length == 0) return Array.Empty<Template>();

        return await _context.Templates
            .Where(t => distinct.Contains(t.Name))
            .Include(t => t.Attributes)
            .Include(t => t.Alarms)
            .Include(t => t.Scripts)
            .Include(t => t.Compositions)
            .Include(t => t.NativeAlarmSources)
            .AsSplitQuery()
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<Template>> GetAllTemplatesAsync(CancellationToken cancellationToken = default)
    {
        return await _context.Templates
            .Include(t => t.Attributes)
            .Include(t => t.Alarms)
            .Include(t => t.Scripts)
            .Include(t => t.Compositions)
            .Include(t => t.NativeAlarmSources)
            .AsSplitQuery()
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    /// <remarks>
    /// Two deliberate departures from <see cref="GetAllTemplatesAsync"/>:
    /// <list type="number">
    ///   <item>
    ///     <c>AsNoTracking</c> — the analysis walks (cycle detection, collision
    ///     detection, canonical-name resolution) never write through the loaded
    ///     graph, so paying for a change-tracker snapshot of every attribute,
    ///     alarm, script and composition in the database is pure waste.
    ///   </item>
    ///   <item>
    ///     <c>TemplateScript.Code</c> is projected away (left empty). Script
    ///     bodies are by far the largest column in the graph and NONE of the
    ///     analysis consumers read them — <c>CycleDetector</c> reads only ids and
    ///     edges, <c>CollisionDetector</c> and <c>TemplateResolver</c> read only
    ///     member names and lock flags.
    ///   </item>
    /// </list>
    /// Callers that DO need bodies or tracked entities — the inheritance
    /// reconciler and the flattener — must keep using
    /// <see cref="GetAllTemplatesAsync"/> / <see cref="GetTemplateWithChildrenAsync"/>.
    /// </remarks>
    public async Task<IReadOnlyList<Template>> GetAllTemplatesForAnalysisAsync(CancellationToken cancellationToken = default)
    {
        return await _context.Templates
            .AsNoTracking()
            .Select(t => new Template(t.Name)
            {
                Id = t.Id,
                Description = t.Description,
                ParentTemplateId = t.ParentTemplateId,
                FolderId = t.FolderId,
                IsDerived = t.IsDerived,
                OwnerCompositionId = t.OwnerCompositionId,
                Attributes = t.Attributes.ToList(),
                Alarms = t.Alarms.ToList(),
                NativeAlarmSources = t.NativeAlarmSources.ToList(),
                Compositions = t.Compositions.ToList(),
                Scripts = t.Scripts
                    .Select(s => new TemplateScript(s.Name, string.Empty)
                    {
                        Id = s.Id,
                        TemplateId = s.TemplateId,
                        IsLocked = s.IsLocked,
                        TriggerType = s.TriggerType,
                        TriggerConfiguration = s.TriggerConfiguration,
                        ParameterDefinitions = s.ParameterDefinitions,
                        ReturnDefinition = s.ReturnDefinition,
                        MinTimeBetweenRuns = s.MinTimeBetweenRuns,
                        ExecutionTimeoutSeconds = s.ExecutionTimeoutSeconds,
                        IsInherited = s.IsInherited,
                        LockedInDerived = s.LockedInDerived
                    })
                    .ToList()
            })
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<TemplateSummary>> GetTemplateSummariesAsync(
        int skip,
        int? take,
        CancellationToken cancellationToken = default)
    {
        var query = _context.Templates
            .AsNoTracking()
            .OrderBy(t => t.Id)
            .Skip(Math.Max(0, skip));

        if (take is > 0)
            query = query.Take(Math.Min(take.Value, TemplateSummary.MaxPageSize));

        return await query
            .Select(t => new TemplateSummary(
                t.Id,
                t.Name,
                t.Description,
                t.ParentTemplateId,
                t.FolderId,
                t.IsDerived,
                t.OwnerCompositionId,
                t.Attributes.Count,
                t.Alarms.Count,
                t.Scripts.Count,
                t.Compositions.Count,
                t.NativeAlarmSources.Count))
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<Template>> GetTemplatesComposingAsync(int composedTemplateId, CancellationToken cancellationToken = default)
    {
        return await _context.Templates
            .Where(t => t.Compositions.Any(c => c.ComposedTemplateId == composedTemplateId))
            .Include(t => t.Attributes)
            .Include(t => t.Scripts)
            .Include(t => t.Compositions)
            .AsSplitQuery()
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task AddTemplateAsync(Template template, CancellationToken cancellationToken = default)
    {
        await _context.Templates.AddAsync(template, cancellationToken);
    }

    /// <inheritdoc />
    public Task UpdateTemplateAsync(Template template, CancellationToken cancellationToken = default)
    {
        _context.Templates.Update(template);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task DeleteTemplateAsync(int id, CancellationToken cancellationToken = default)
    {
        var template = await _context.Templates.FindAsync(new object[] { id }, cancellationToken);
        if (template != null)
        {
            _context.Templates.Remove(template);
        }
    }

    // TemplateAttribute

    /// <inheritdoc />
    public async Task<TemplateAttribute?> GetTemplateAttributeByIdAsync(int id, CancellationToken cancellationToken = default)
    {
        return await _context.TemplateAttributes.FindAsync(new object[] { id }, cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<TemplateAttribute>> GetAttributesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default)
    {
        return await _context.TemplateAttributes
            .Where(a => a.TemplateId == templateId)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task AddTemplateAttributeAsync(TemplateAttribute attribute, CancellationToken cancellationToken = default)
    {
        await _context.TemplateAttributes.AddAsync(attribute, cancellationToken);
    }

    /// <inheritdoc />
    public Task UpdateTemplateAttributeAsync(TemplateAttribute attribute, CancellationToken cancellationToken = default)
    {
        _context.TemplateAttributes.Update(attribute);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task DeleteTemplateAttributeAsync(int id, CancellationToken cancellationToken = default)
    {
        var attribute = await _context.TemplateAttributes.FindAsync(new object[] { id }, cancellationToken);
        if (attribute != null)
        {
            _context.TemplateAttributes.Remove(attribute);
        }
    }

    // TemplateAlarm

    /// <inheritdoc />
    public async Task<TemplateAlarm?> GetTemplateAlarmByIdAsync(int id, CancellationToken cancellationToken = default)
    {
        return await _context.TemplateAlarms.FindAsync(new object[] { id }, cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<TemplateAlarm>> GetAlarmsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default)
    {
        return await _context.TemplateAlarms
            .Where(a => a.TemplateId == templateId)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task AddTemplateAlarmAsync(TemplateAlarm alarm, CancellationToken cancellationToken = default)
    {
        await _context.TemplateAlarms.AddAsync(alarm, cancellationToken);
    }

    /// <inheritdoc />
    public Task UpdateTemplateAlarmAsync(TemplateAlarm alarm, CancellationToken cancellationToken = default)
    {
        _context.TemplateAlarms.Update(alarm);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task DeleteTemplateAlarmAsync(int id, CancellationToken cancellationToken = default)
    {
        var alarm = await _context.TemplateAlarms.FindAsync(new object[] { id }, cancellationToken);
        if (alarm != null)
        {
            _context.TemplateAlarms.Remove(alarm);
        }
    }

    // TemplateNativeAlarmSource

    /// <inheritdoc />
    public async Task<TemplateNativeAlarmSource?> GetTemplateNativeAlarmSourceByIdAsync(int id, CancellationToken cancellationToken = default)
    {
        return await _context.TemplateNativeAlarmSources.FindAsync(new object[] { id }, cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<TemplateNativeAlarmSource>> GetNativeAlarmSourcesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default)
    {
        return await _context.TemplateNativeAlarmSources
            .Where(s => s.TemplateId == templateId)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task AddTemplateNativeAlarmSourceAsync(TemplateNativeAlarmSource source, CancellationToken cancellationToken = default)
    {
        await _context.TemplateNativeAlarmSources.AddAsync(source, cancellationToken);
    }

    /// <inheritdoc />
    public Task UpdateTemplateNativeAlarmSourceAsync(TemplateNativeAlarmSource source, CancellationToken cancellationToken = default)
    {
        _context.TemplateNativeAlarmSources.Update(source);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task DeleteTemplateNativeAlarmSourceAsync(int id, CancellationToken cancellationToken = default)
    {
        var source = await _context.TemplateNativeAlarmSources.FindAsync(new object[] { id }, cancellationToken);
        if (source != null)
        {
            _context.TemplateNativeAlarmSources.Remove(source);
        }
    }

    // TemplateScript

    /// <inheritdoc />
    public async Task<TemplateScript?> GetTemplateScriptByIdAsync(int id, CancellationToken cancellationToken = default)
    {
        return await _context.TemplateScripts.FindAsync(new object[] { id }, cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<TemplateScript>> GetScriptsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default)
    {
        return await _context.TemplateScripts
            .Where(s => s.TemplateId == templateId)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task AddTemplateScriptAsync(TemplateScript script, CancellationToken cancellationToken = default)
    {
        await _context.TemplateScripts.AddAsync(script, cancellationToken);
    }

    /// <inheritdoc />
    public Task UpdateTemplateScriptAsync(TemplateScript script, CancellationToken cancellationToken = default)
    {
        _context.TemplateScripts.Update(script);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task DeleteTemplateScriptAsync(int id, CancellationToken cancellationToken = default)
    {
        var script = await _context.TemplateScripts.FindAsync(new object[] { id }, cancellationToken);
        if (script != null)
        {
            _context.TemplateScripts.Remove(script);
        }
    }

    // TemplateComposition

    /// <inheritdoc />
    public async Task<TemplateComposition?> GetTemplateCompositionByIdAsync(int id, CancellationToken cancellationToken = default)
    {
        return await _context.TemplateCompositions.FindAsync(new object[] { id }, cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<TemplateComposition>> GetCompositionsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default)
    {
        return await _context.TemplateCompositions
            .Where(c => c.TemplateId == templateId)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task AddTemplateCompositionAsync(TemplateComposition composition, CancellationToken cancellationToken = default)
    {
        await _context.TemplateCompositions.AddAsync(composition, cancellationToken);
    }

    /// <inheritdoc />
    public Task UpdateTemplateCompositionAsync(TemplateComposition composition, CancellationToken cancellationToken = default)
    {
        _context.TemplateCompositions.Update(composition);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task DeleteTemplateCompositionAsync(int id, CancellationToken cancellationToken = default)
    {
        var composition = await _context.TemplateCompositions.FindAsync(new object[] { id }, cancellationToken);
        if (composition != null)
        {
            _context.TemplateCompositions.Remove(composition);
        }
    }

    // Instance

    /// <inheritdoc />
    public async Task<Instance?> GetInstanceByIdAsync(int id, CancellationToken cancellationToken = default)
    {
        return await _context.Instances
            .Include(i => i.AttributeOverrides)
            .Include(i => i.AlarmOverrides)
            .Include(i => i.ConnectionBindings)
            .Include(i => i.NativeAlarmSourceOverrides)
            .AsSplitQuery()
            .FirstOrDefaultAsync(i => i.Id == id, cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<Instance>> GetAllInstancesAsync(CancellationToken cancellationToken = default)
    {
        return await _context.Instances
            .Include(i => i.AttributeOverrides)
            .Include(i => i.AlarmOverrides)
            .Include(i => i.ConnectionBindings)
            .Include(i => i.NativeAlarmSourceOverrides)
            .AsSplitQuery()
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<Instance>> GetInstancesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default)
    {
        return await _context.Instances
            .Where(i => i.TemplateId == templateId)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<Instance>> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default)
    {
        return await _context.Instances
            .Where(i => i.SiteId == siteId)
            .Include(i => i.AttributeOverrides)
            .Include(i => i.AlarmOverrides)
            .Include(i => i.ConnectionBindings)
            .Include(i => i.NativeAlarmSourceOverrides)
            .AsSplitQuery()
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<Instance?> GetInstanceByUniqueNameAsync(string uniqueName, CancellationToken cancellationToken = default)
    {
        return await _context.Instances
            .Include(i => i.AttributeOverrides)
            .Include(i => i.AlarmOverrides)
            .Include(i => i.ConnectionBindings)
            .Include(i => i.NativeAlarmSourceOverrides)
            .AsSplitQuery()
            .FirstOrDefaultAsync(i => i.UniqueName == uniqueName, cancellationToken);
    }

    /// <inheritdoc />
    public async Task AddInstanceAsync(Instance instance, CancellationToken cancellationToken = default)
    {
        await _context.Instances.AddAsync(instance, cancellationToken);
    }

    /// <inheritdoc />
    public Task UpdateInstanceAsync(Instance instance, CancellationToken cancellationToken = default)
    {
        _context.Instances.Update(instance);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task DeleteInstanceAsync(int id, CancellationToken cancellationToken = default)
    {
        var instance = await _context.Instances.FindAsync(new object[] { id }, cancellationToken);
        if (instance != null)
        {
            _context.Instances.Remove(instance);
        }
    }

    // InstanceAttributeOverride

    /// <inheritdoc />
    public async Task<IReadOnlyList<InstanceAttributeOverride>> GetOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default)
    {
        return await _context.InstanceAttributeOverrides
            .Where(o => o.InstanceId == instanceId)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task AddInstanceAttributeOverrideAsync(InstanceAttributeOverride attributeOverride, CancellationToken cancellationToken = default)
    {
        await _context.InstanceAttributeOverrides.AddAsync(attributeOverride, cancellationToken);
    }

    /// <inheritdoc />
    public Task UpdateInstanceAttributeOverrideAsync(InstanceAttributeOverride attributeOverride, CancellationToken cancellationToken = default)
    {
        _context.InstanceAttributeOverrides.Update(attributeOverride);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task DeleteInstanceAttributeOverrideAsync(int id, CancellationToken cancellationToken = default)
    {
        var attributeOverride = await _context.InstanceAttributeOverrides.FindAsync(new object[] { id }, cancellationToken);
        if (attributeOverride != null)
        {
            _context.InstanceAttributeOverrides.Remove(attributeOverride);
        }
    }

    // InstanceAlarmOverride

    /// <inheritdoc />
    public async Task<IReadOnlyList<InstanceAlarmOverride>> GetAlarmOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default)
    {
        return await _context.InstanceAlarmOverrides
            .Where(o => o.InstanceId == instanceId)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<InstanceAlarmOverride?> GetAlarmOverrideAsync(int instanceId, string alarmCanonicalName, CancellationToken cancellationToken = default)
    {
        return await _context.InstanceAlarmOverrides
            .FirstOrDefaultAsync(
                o => o.InstanceId == instanceId && o.AlarmCanonicalName == alarmCanonicalName,
                cancellationToken);
    }

    /// <inheritdoc />
    public async Task AddInstanceAlarmOverrideAsync(InstanceAlarmOverride alarmOverride, CancellationToken cancellationToken = default)
    {
        await _context.InstanceAlarmOverrides.AddAsync(alarmOverride, cancellationToken);
    }

    /// <inheritdoc />
    public Task UpdateInstanceAlarmOverrideAsync(InstanceAlarmOverride alarmOverride, CancellationToken cancellationToken = default)
    {
        _context.InstanceAlarmOverrides.Update(alarmOverride);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task DeleteInstanceAlarmOverrideAsync(int id, CancellationToken cancellationToken = default)
    {
        var alarmOverride = await _context.InstanceAlarmOverrides.FindAsync(new object[] { id }, cancellationToken);
        if (alarmOverride != null)
        {
            _context.InstanceAlarmOverrides.Remove(alarmOverride);
        }
    }

    // InstanceNativeAlarmSourceOverride

    /// <inheritdoc />
    public async Task<IReadOnlyList<InstanceNativeAlarmSourceOverride>> GetNativeAlarmSourceOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default)
    {
        return await _context.InstanceNativeAlarmSourceOverrides
            .Where(o => o.InstanceId == instanceId)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task<InstanceNativeAlarmSourceOverride?> GetNativeAlarmSourceOverrideAsync(int instanceId, string sourceCanonicalName, CancellationToken cancellationToken = default)
    {
        return await _context.InstanceNativeAlarmSourceOverrides
            .FirstOrDefaultAsync(o => o.InstanceId == instanceId && o.SourceCanonicalName == sourceCanonicalName, cancellationToken);
    }

    /// <inheritdoc />
    public async Task AddInstanceNativeAlarmSourceOverrideAsync(InstanceNativeAlarmSourceOverride ovr, CancellationToken cancellationToken = default)
    {
        await _context.InstanceNativeAlarmSourceOverrides.AddAsync(ovr, cancellationToken);
    }

    /// <inheritdoc />
    public Task UpdateInstanceNativeAlarmSourceOverrideAsync(InstanceNativeAlarmSourceOverride ovr, CancellationToken cancellationToken = default)
    {
        _context.InstanceNativeAlarmSourceOverrides.Update(ovr);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task DeleteInstanceNativeAlarmSourceOverrideAsync(int id, CancellationToken cancellationToken = default)
    {
        var ovr = await _context.InstanceNativeAlarmSourceOverrides.FindAsync(new object[] { id }, cancellationToken);
        if (ovr != null)
        {
            _context.InstanceNativeAlarmSourceOverrides.Remove(ovr);
        }
    }

    // InstanceConnectionBinding

    /// <inheritdoc />
    public async Task<IReadOnlyList<InstanceConnectionBinding>> GetBindingsByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default)
    {
        return await _context.InstanceConnectionBindings
            .Where(b => b.InstanceId == instanceId)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task AddInstanceConnectionBindingAsync(InstanceConnectionBinding binding, CancellationToken cancellationToken = default)
    {
        await _context.InstanceConnectionBindings.AddAsync(binding, cancellationToken);
    }

    /// <inheritdoc />
    public Task UpdateInstanceConnectionBindingAsync(InstanceConnectionBinding binding, CancellationToken cancellationToken = default)
    {
        _context.InstanceConnectionBindings.Update(binding);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task DeleteInstanceConnectionBindingAsync(int id, CancellationToken cancellationToken = default)
    {
        var binding = await _context.InstanceConnectionBindings.FindAsync(new object[] { id }, cancellationToken);
        if (binding != null)
        {
            _context.InstanceConnectionBindings.Remove(binding);
        }
    }

    // Area

    /// <inheritdoc />
    public async Task<Area?> GetAreaByIdAsync(int id, CancellationToken cancellationToken = default)
    {
        return await _context.Areas
            .Include(a => a.Children)
            .FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<Area>> GetAreasBySiteIdAsync(int siteId, CancellationToken cancellationToken = default)
    {
        return await _context.Areas
            .Where(a => a.SiteId == siteId)
            .Include(a => a.Children)
            .ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task AddAreaAsync(Area area, CancellationToken cancellationToken = default)
    {
        await _context.Areas.AddAsync(area, cancellationToken);
    }

    /// <inheritdoc />
    public Task UpdateAreaAsync(Area area, CancellationToken cancellationToken = default)
    {
        _context.Areas.Update(area);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task DeleteAreaAsync(int id, CancellationToken cancellationToken = default)
    {
        var area = await _context.Areas.FindAsync(new object[] { id }, cancellationToken);
        if (area != null)
        {
            _context.Areas.Remove(area);
        }
    }

    // SharedScript

    /// <inheritdoc />
    public async Task<SharedScript?> GetSharedScriptByIdAsync(int id, CancellationToken cancellationToken = default)
    {
        return await _context.SharedScripts.FindAsync(new object[] { id }, cancellationToken);
    }

    /// <inheritdoc />
    public async Task<SharedScript?> GetSharedScriptByNameAsync(string name, CancellationToken cancellationToken = default)
    {
        return await _context.SharedScripts
            .FirstOrDefaultAsync(s => s.Name == name, cancellationToken);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<SharedScript>> GetAllSharedScriptsAsync(CancellationToken cancellationToken = default)
    {
        return await _context.SharedScripts.ToListAsync(cancellationToken);
    }

    /// <inheritdoc />
    public async Task AddSharedScriptAsync(SharedScript sharedScript, CancellationToken cancellationToken = default)
    {
        await _context.SharedScripts.AddAsync(sharedScript, cancellationToken);
    }

    /// <inheritdoc />
    public Task UpdateSharedScriptAsync(SharedScript sharedScript, CancellationToken cancellationToken = default)
    {
        _context.SharedScripts.Update(sharedScript);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task DeleteSharedScriptAsync(int id, CancellationToken cancellationToken = default)
    {
        var sharedScript = await _context.SharedScripts.FindAsync(new object[] { id }, cancellationToken);
        if (sharedScript != null)
        {
            _context.SharedScripts.Remove(sharedScript);
        }
    }

    // TemplateFolder

    /// <inheritdoc />
    public async Task<TemplateFolder?> GetFolderByIdAsync(int id, CancellationToken cancellationToken = default)
        => await _context.TemplateFolders.FindAsync(new object[] { id }, cancellationToken);

    /// <inheritdoc />
    public async Task<IReadOnlyList<TemplateFolder>> GetAllFoldersAsync(CancellationToken cancellationToken = default)
        => await _context.TemplateFolders
            .OrderBy(f => f.SortOrder)
            .ThenBy(f => f.Name)
            .ToListAsync(cancellationToken);

    /// <inheritdoc />
    public async Task AddFolderAsync(TemplateFolder folder, CancellationToken cancellationToken = default)
        => await _context.TemplateFolders.AddAsync(folder, cancellationToken);

    /// <inheritdoc />
    public Task UpdateFolderAsync(TemplateFolder folder, CancellationToken cancellationToken = default)
    {
        _context.TemplateFolders.Update(folder);
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public async Task DeleteFolderAsync(int id, CancellationToken cancellationToken = default)
    {
        var folder = await _context.TemplateFolders.FindAsync(new object[] { id }, cancellationToken);
        if (folder != null)
        {
            _context.TemplateFolders.Remove(folder);
        }
    }

    /// <inheritdoc />
    /// <remarks>
    /// Inspects the change tracker BEFORE committing (afterwards every entry is
    /// <see cref="EntityState.Unchanged"/> and the attribution is lost) and bumps
    /// the <see cref="ITemplateGraphWatermark"/> for each affected template /
    /// instance. The bump is applied only AFTER a successful commit, so a failed
    /// save does not invalidate caches for a change that never landed.
    /// </remarks>
    public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        var pending = CollectWatermarkBumps();
        var written = await _context.SaveChangesAsync(cancellationToken);
        ApplyWatermarkBumps(pending);
        return written;
    }

    /// <summary>
    /// Snapshot of the watermark bumps implied by the current change-tracker
    /// contents. Captured pre-commit, applied post-commit.
    /// </summary>
    private readonly record struct WatermarkBumps(
        HashSet<int> Templates,
        HashSet<int> StructuralTemplates,
        HashSet<int> Instances,
        bool Unattributed);

    /// <summary>
    /// Walks the change tracker and maps every added/modified/deleted
    /// template-graph or instance-graph entry back to the template (or instance)
    /// whose flattened output it can affect.
    ///
    /// <para>
    /// A change is treated as STRUCTURAL — invalidating cached chain membership,
    /// not just chain contents — when it adds or removes a <see cref="Template"/>
    /// row, changes a <see cref="Template.ParentTemplateId"/>, or touches a
    /// <see cref="TemplateComposition"/> row. Those are exactly the edges the
    /// flattener walks to build a chain.
    /// </para>
    ///
    /// <para>
    /// An entry whose owning id cannot be resolved (a child row deleted before
    /// its FK was materialised, or an entity type added later that this switch
    /// does not know) sets <c>Unattributed</c>, which conservatively invalidates
    /// every cached chain rather than silently letting a stale entry survive.
    /// </para>
    /// </summary>
    private WatermarkBumps CollectWatermarkBumps()
    {
        var templates = new HashSet<int>();
        var structural = new HashSet<int>();
        var instances = new HashSet<int>();
        var unattributed = false;

        foreach (var entry in _context.ChangeTracker.Entries())
        {
            if (entry.State is not (EntityState.Added or EntityState.Modified or EntityState.Deleted))
                continue;

            switch (entry.Entity)
            {
                case Template t:
                    templates.Add(t.Id);
                    // Add/Delete changes chain membership; a re-parent changes it too.
                    if (entry.State != EntityState.Modified
                        || entry.Property(nameof(Template.ParentTemplateId)).IsModified)
                    {
                        structural.Add(t.Id);
                    }
                    break;
                case TemplateAttribute a:
                    templates.Add(a.TemplateId);
                    break;
                case TemplateAlarm al:
                    templates.Add(al.TemplateId);
                    break;
                case TemplateScript s:
                    templates.Add(s.TemplateId);
                    break;
                case TemplateNativeAlarmSource ns:
                    templates.Add(ns.TemplateId);
                    break;
                case TemplateComposition c:
                    templates.Add(c.TemplateId);
                    structural.Add(c.TemplateId);
                    break;
                case Instance i:
                    instances.Add(i.Id);
                    break;
                case InstanceAttributeOverride ao:
                    instances.Add(ao.InstanceId);
                    break;
                case InstanceAlarmOverride alo:
                    instances.Add(alo.InstanceId);
                    break;
                case InstanceNativeAlarmSourceOverride nso:
                    instances.Add(nso.InstanceId);
                    break;
                case InstanceConnectionBinding cb:
                    instances.Add(cb.InstanceId);
                    break;
                case SharedScript:
                    // Shared scripts are a session-global input to validation, not
                    // owned by any one template — invalidate everything.
                    unattributed = true;
                    break;
                default:
                    // Anything this switch does not recognise is invalidated
                    // conservatively, as the doc comment above promises. The
                    // previous "areas, folders and anything else do not feed the
                    // flattener" no-op was wrong in at least one shipped case:
                    // DataConnection rows land here, and their Protocol /
                    // Primary+Backup configuration / FailoverRetryCount ARE hash
                    // inputs (FlatteningService packages them as ConnectionConfig,
                    // RevisionHashService hashes them as HashableConnection). A
                    // silent no-op on an unrecognised entity is the one failure
                    // mode this watermark must not have: it produces STALE work,
                    // whereas an over-broad bump only ever produces extra work.
                    unattributed = true;
                    break;
            }
        }

        return new WatermarkBumps(templates, structural, instances, unattributed);
    }

    /// <summary>Applies a previously collected <see cref="WatermarkBumps"/> snapshot.</summary>
    private void ApplyWatermarkBumps(WatermarkBumps bumps)
    {
        if (bumps.Unattributed)
            _watermark.BumpAll();

        foreach (var templateId in bumps.Templates)
            _watermark.BumpTemplate(templateId, bumps.StructuralTemplates.Contains(templateId));

        foreach (var instanceId in bumps.Instances)
            _watermark.BumpInstance(instanceId);
    }
}
