using ScadaLink.Commons.Entities.Templates;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Commons.Types;

namespace ScadaLink.TemplateEngine;

/// <summary>
/// Core service for Template Engine operations.
/// Covers CRUD for templates and their members (attributes, alarms, scripts, compositions),
/// inheritance and composition rules, override/locking validation, collision detection, and acyclicity enforcement.
/// </summary>
public class TemplateService
{
    private readonly ITemplateEngineRepository _repository;
    private readonly IAuditService _auditService;

    public TemplateService(ITemplateEngineRepository repository, IAuditService auditService)
    {
        _repository = repository ?? throw new ArgumentNullException(nameof(repository));
        _auditService = auditService ?? throw new ArgumentNullException(nameof(auditService));
    }

    // ========================================================================
    // WP-1: Template CRUD with Inheritance
    // ========================================================================

    public async Task<Result<Template>> CreateTemplateAsync(
        string name,
        string? description,
        int? parentTemplateId,
        string user,
        int? folderId = null,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(name))
            return Result<Template>.Failure("Template name is required.");

        // Validate parent exists if specified
        if (parentTemplateId.HasValue)
        {
            var parent = await _repository.GetTemplateByIdAsync(parentTemplateId.Value, cancellationToken);
            if (parent == null)
                return Result<Template>.Failure($"Parent template with ID {parentTemplateId.Value} not found.");
        }

        var template = new Template(name)
        {
            Description = description,
            ParentTemplateId = parentTemplateId,
            FolderId = folderId
        };

        // No collision or acyclicity check is needed here: a freshly created
        // template has no members of its own, the parent (validated above to
        // exist) was already collision-checked when its members were added,
        // and a brand-new child cannot be an ancestor of its parent. Naming
        // collisions are enforced on every member-mutating call (AddAttribute,
        // AddAlarm, AddScript, AddComposition) and on rename in UpdateTemplate.

        await _repository.AddTemplateAsync(template, cancellationToken);
        await _auditService.LogAsync(user, "Create", "Template", "0", name, template, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        return Result<Template>.Success(template);
    }

    public async Task<Result<Template>> UpdateTemplateAsync(
        int templateId,
        string name,
        string? description,
        int? parentTemplateId,
        string user,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(name))
            return Result<Template>.Failure("Template name is required.");

        var template = await _repository.GetTemplateByIdAsync(templateId, cancellationToken);
        if (template == null)
            return Result<Template>.Failure($"Template with ID {templateId} not found.");

        // ParentTemplateId is immutable after creation — set once at create time.
        // Reject any attempt to change it (null→value, value→null, or value→other).
        if (parentTemplateId != template.ParentTemplateId)
        {
            return Result<Template>.Failure(
                "Parent template cannot be changed after creation.");
        }

        template.Name = name;
        template.Description = description;

        // Check for naming collisions after the change
        var collisionResult = await ValidateCollisionsAsync(template, cancellationToken);
        if (collisionResult != null)
            return Result<Template>.Failure(collisionResult);

        await _repository.UpdateTemplateAsync(template, cancellationToken);
        await _auditService.LogAsync(user, "Update", "Template", templateId.ToString(), name, template, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        return Result<Template>.Success(template);
    }

    public async Task<Result<bool>> DeleteTemplateAsync(
        int templateId,
        string user,
        CancellationToken cancellationToken = default)
    {
        var template = await _repository.GetTemplateByIdAsync(templateId, cancellationToken);
        if (template == null)
            return Result<bool>.Failure($"Template with ID {templateId} not found.");

        // Derived templates are owned by their composition row and must be removed
        // by deleting the composition (which cascades) — block direct deletion.
        if (template.IsDerived)
            return Result<bool>.Failure(
                $"Cannot delete template '{template.Name}': it is a derived template. " +
                "Remove the owning composition on its parent template instead.");

        // Check for instances referencing this template
        var instances = await _repository.GetInstancesByTemplateIdAsync(templateId, cancellationToken);
        if (instances.Count > 0)
            return Result<bool>.Failure(
                $"Cannot delete template '{template.Name}': it is referenced by {instances.Count} instance(s).");

        // Check for child templates inheriting from this template.
        // Split derived vs. regular children — the message and remediation differ.
        var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
        var inheritors = allTemplates.Where(t => t.ParentTemplateId == templateId).ToList();
        var derivatives = inheritors.Where(t => t.IsDerived).ToList();
        var regularChildren = inheritors.Where(t => !t.IsDerived).ToList();

        if (regularChildren.Count > 0)
            return Result<bool>.Failure(
                $"Cannot delete template '{template.Name}': it is inherited by {regularChildren.Count} child template(s): " +
                string.Join(", ", regularChildren.Select(c => $"'{c.Name}'")));

        if (derivatives.Count > 0)
        {
            // Name each derivative by its owning parent template + composition slot.
            var ownerCompIds = derivatives.Select(d => d.OwnerCompositionId).Where(id => id.HasValue).Select(id => id!.Value).ToHashSet();
            var ownerLookup = allTemplates
                .SelectMany(t => t.Compositions.Select(c => new { Owner = t, Composition = c }))
                .Where(x => ownerCompIds.Contains(x.Composition.Id))
                .ToDictionary(x => x.Composition.Id, x => $"'{x.Owner.Name}' (as '{x.Composition.InstanceName}')");

            var details = derivatives
                .Select(d => d.OwnerCompositionId.HasValue && ownerLookup.TryGetValue(d.OwnerCompositionId.Value, out var label)
                    ? label
                    : $"'{d.Name}'");
            return Result<bool>.Failure(
                $"Cannot delete template '{template.Name}': it is the base of {derivatives.Count} derived template(s) used in: " +
                string.Join(", ", details) + ". Remove those compositions first.");
        }

        // Check for templates composing this template
        var composedBy = allTemplates
            .Where(t => t.Compositions.Any(c => c.ComposedTemplateId == templateId))
            .ToList();
        if (composedBy.Count > 0)
            return Result<bool>.Failure(
                $"Cannot delete template '{template.Name}': it is composed by {composedBy.Count} template(s): " +
                string.Join(", ", composedBy.Select(c => $"'{c.Name}'")));

        await _repository.DeleteTemplateAsync(templateId, cancellationToken);
        await _auditService.LogAsync(user, "Delete", "Template", templateId.ToString(), template.Name, null, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        return Result<bool>.Success(true);
    }

    public async Task<Result<Template>> MoveTemplateAsync(
        int templateId,
        int? newFolderId,
        string user,
        CancellationToken cancellationToken = default)
    {
        var template = await _repository.GetTemplateByIdAsync(templateId, cancellationToken);
        if (template == null)
            return Result<Template>.Failure($"Template with ID {templateId} not found.");

        if (newFolderId.HasValue)
        {
            var folder = await _repository.GetFolderByIdAsync(newFolderId.Value, cancellationToken);
            if (folder == null)
                return Result<Template>.Failure($"Target folder with ID {newFolderId.Value} not found.");
        }

        template.FolderId = newFolderId;
        await _repository.UpdateTemplateAsync(template, cancellationToken);
        await _auditService.LogAsync(user, "Move", "Template", template.Id.ToString(), template.Name, template, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        return Result<Template>.Success(template);
    }

    public async Task<Template?> GetTemplateByIdAsync(int templateId, CancellationToken cancellationToken = default)
    {
        return await _repository.GetTemplateByIdAsync(templateId, cancellationToken);
    }

    public async Task<IReadOnlyList<Template>> GetAllTemplatesAsync(CancellationToken cancellationToken = default)
    {
        return await _repository.GetAllTemplatesAsync(cancellationToken);
    }

    // ========================================================================
    // WP-2: Attribute Definitions with Lock Flags
    // ========================================================================

    public async Task<Result<TemplateAttribute>> AddAttributeAsync(
        int templateId,
        TemplateAttribute attribute,
        string user,
        CancellationToken cancellationToken = default)
    {
        var template = await _repository.GetTemplateByIdAsync(templateId, cancellationToken);
        if (template == null)
            return Result<TemplateAttribute>.Failure($"Template with ID {templateId} not found.");

        // Check for duplicate name at this template level
        if (template.Attributes.Any(a => a.Name == attribute.Name))
            return Result<TemplateAttribute>.Failure(
                $"Attribute '{attribute.Name}' already exists on template '{template.Name}'.");

        attribute.TemplateId = templateId;

        // If inheriting, validate not trying to add a member that would collide
        var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
        var testTemplate = CloneTemplateWithNewAttribute(template, attribute);
        var collisions = CollisionDetector.DetectCollisions(testTemplate, allTemplates);
        if (collisions.Count > 0)
            return Result<TemplateAttribute>.Failure(string.Join(" ", collisions));

        await _repository.AddTemplateAttributeAsync(attribute, cancellationToken);
        await _auditService.LogAsync(user, "Create", "TemplateAttribute", "0", attribute.Name, attribute, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        return Result<TemplateAttribute>.Success(attribute);
    }

    public async Task<Result<TemplateAttribute>> UpdateAttributeAsync(
        int attributeId,
        TemplateAttribute proposed,
        string user,
        CancellationToken cancellationToken = default)
    {
        var existing = await _repository.GetTemplateAttributeByIdAsync(attributeId, cancellationToken);
        if (existing == null)
            return Result<TemplateAttribute>.Failure($"Attribute with ID {attributeId} not found.");

        // Validate override rules if this is an override (parent has same-named attribute)
        var template = await _repository.GetTemplateByIdAsync(existing.TemplateId, cancellationToken);
        if (template?.ParentTemplateId != null)
        {
            var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
            var parentMember = TemplateResolver.FindMemberByCanonicalName(
                existing.Name, template.ParentTemplateId.Value, allTemplates);
            if (parentMember != null && parentMember.IsLocked)
                return Result<TemplateAttribute>.Failure(
                    $"Attribute '{existing.Name}' is locked in parent and cannot be overridden.");

            // Derived templates may not override fields the base marked LockedInDerived.
            if (template.IsDerived)
            {
                var baseTemplate = await _repository.GetTemplateByIdAsync(template.ParentTemplateId.Value, cancellationToken);
                var baseAttr = baseTemplate?.Attributes.FirstOrDefault(a => a.Name == existing.Name);
                if (baseAttr != null && baseAttr.LockedInDerived)
                    return Result<TemplateAttribute>.Failure(
                        $"Attribute '{existing.Name}' is locked by base template '{baseTemplate!.Name}' and cannot be overridden.");
            }
        }

        // Validate lock change rules
        var lockError = LockEnforcer.ValidateLockChange(existing.IsLocked, proposed.IsLocked, existing.Name);
        if (lockError != null)
            return Result<TemplateAttribute>.Failure(lockError);

        // Validate fixed-field granularity. DataType and DataSourceReference are
        // fixed by the defining level for every attribute — locked or not — so
        // the error is always honoured (a locked attribute is already rejected
        // earlier inside the helper).
        var granularityError = LockEnforcer.ValidateAttributeOverride(existing, proposed);
        if (granularityError != null)
            return Result<TemplateAttribute>.Failure(granularityError);

        // Apply overridable fields. DataType / DataSourceReference are fixed and
        // are deliberately not copied from the proposed attribute.
        existing.Value = proposed.Value;
        existing.Description = proposed.Description;
        existing.IsLocked = proposed.IsLocked;
        if (template?.IsDerived == true)
            existing.IsInherited = proposed.IsInherited;
        else
            existing.LockedInDerived = proposed.LockedInDerived;

        await _repository.UpdateTemplateAttributeAsync(existing, cancellationToken);
        await _auditService.LogAsync(user, "Update", "TemplateAttribute", attributeId.ToString(), existing.Name, existing, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        return Result<TemplateAttribute>.Success(existing);
    }

    public async Task<Result<bool>> DeleteAttributeAsync(
        int attributeId,
        string user,
        CancellationToken cancellationToken = default)
    {
        var attribute = await _repository.GetTemplateAttributeByIdAsync(attributeId, cancellationToken);
        if (attribute == null)
            return Result<bool>.Failure($"Attribute with ID {attributeId} not found.");

        // Cannot remove inherited parent members — only direct members
        var template = await _repository.GetTemplateByIdAsync(attribute.TemplateId, cancellationToken);
        if (template?.ParentTemplateId != null)
        {
            var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
            var parentMember = TemplateResolver.FindMemberByCanonicalName(
                attribute.Name, template.ParentTemplateId.Value, allTemplates);
            if (parentMember != null)
                return Result<bool>.Failure(
                    $"Cannot remove attribute '{attribute.Name}': it is inherited from parent template.");
        }

        await _repository.DeleteTemplateAttributeAsync(attributeId, cancellationToken);
        await _auditService.LogAsync(user, "Delete", "TemplateAttribute", attributeId.ToString(), attribute.Name, null, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        return Result<bool>.Success(true);
    }

    // ========================================================================
    // WP-3: Alarm Definitions
    // ========================================================================

    public async Task<Result<TemplateAlarm>> AddAlarmAsync(
        int templateId,
        TemplateAlarm alarm,
        string user,
        CancellationToken cancellationToken = default)
    {
        var template = await _repository.GetTemplateByIdAsync(templateId, cancellationToken);
        if (template == null)
            return Result<TemplateAlarm>.Failure($"Template with ID {templateId} not found.");

        if (template.Alarms.Any(a => a.Name == alarm.Name))
            return Result<TemplateAlarm>.Failure(
                $"Alarm '{alarm.Name}' already exists on template '{template.Name}'.");

        // Validate priority range
        if (alarm.PriorityLevel < 0 || alarm.PriorityLevel > 1000)
            return Result<TemplateAlarm>.Failure("Alarm priority must be between 0 and 1000.");

        alarm.TemplateId = templateId;

        // Check collisions
        var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
        var testTemplate = CloneTemplateWithNewAlarm(template, alarm);
        var collisions = CollisionDetector.DetectCollisions(testTemplate, allTemplates);
        if (collisions.Count > 0)
            return Result<TemplateAlarm>.Failure(string.Join(" ", collisions));

        await _repository.AddTemplateAlarmAsync(alarm, cancellationToken);
        await _auditService.LogAsync(user, "Create", "TemplateAlarm", "0", alarm.Name, alarm, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        return Result<TemplateAlarm>.Success(alarm);
    }

    public async Task<Result<TemplateAlarm>> UpdateAlarmAsync(
        int alarmId,
        TemplateAlarm proposed,
        string user,
        CancellationToken cancellationToken = default)
    {
        var existing = await _repository.GetTemplateAlarmByIdAsync(alarmId, cancellationToken);
        if (existing == null)
            return Result<TemplateAlarm>.Failure($"Alarm with ID {alarmId} not found.");

        // Validate priority range
        if (proposed.PriorityLevel < 0 || proposed.PriorityLevel > 1000)
            return Result<TemplateAlarm>.Failure("Alarm priority must be between 0 and 1000.");

        // Validate lock change
        var lockError = LockEnforcer.ValidateLockChange(existing.IsLocked, proposed.IsLocked, existing.Name);
        if (lockError != null)
            return Result<TemplateAlarm>.Failure(lockError);

        // Check parent lock
        var template = await _repository.GetTemplateByIdAsync(existing.TemplateId, cancellationToken);
        if (template?.ParentTemplateId != null)
        {
            var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
            var parentMember = TemplateResolver.FindMemberByCanonicalName(
                existing.Name, template.ParentTemplateId.Value, allTemplates);
            if (parentMember != null && parentMember.IsLocked)
                return Result<TemplateAlarm>.Failure(
                    $"Alarm '{existing.Name}' is locked in parent and cannot be overridden.");

            // Derived templates may not override alarms the base marked LockedInDerived.
            if (template.IsDerived)
            {
                var baseTemplate = await _repository.GetTemplateByIdAsync(template.ParentTemplateId.Value, cancellationToken);
                var baseAlarm = baseTemplate?.Alarms.FirstOrDefault(a => a.Name == existing.Name);
                if (baseAlarm != null && baseAlarm.LockedInDerived)
                    return Result<TemplateAlarm>.Failure(
                        $"Alarm '{existing.Name}' is locked by base template '{baseTemplate!.Name}' and cannot be overridden.");
            }
        }

        // Validate fixed fields
        var overrideError = LockEnforcer.ValidateAlarmOverride(existing, proposed);
        if (overrideError != null)
            return Result<TemplateAlarm>.Failure(overrideError);

        // Apply overridable fields
        existing.PriorityLevel = proposed.PriorityLevel;
        existing.TriggerConfiguration = proposed.TriggerConfiguration;
        existing.Description = proposed.Description;
        existing.OnTriggerScriptId = proposed.OnTriggerScriptId;
        existing.IsLocked = proposed.IsLocked;
        if (template?.IsDerived == true)
            existing.IsInherited = proposed.IsInherited;
        else
            existing.LockedInDerived = proposed.LockedInDerived;
        // Name and TriggerType are NOT updated (fixed)

        await _repository.UpdateTemplateAlarmAsync(existing, cancellationToken);
        await _auditService.LogAsync(user, "Update", "TemplateAlarm", alarmId.ToString(), existing.Name, existing, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        return Result<TemplateAlarm>.Success(existing);
    }

    public async Task<Result<bool>> DeleteAlarmAsync(
        int alarmId,
        string user,
        CancellationToken cancellationToken = default)
    {
        var alarm = await _repository.GetTemplateAlarmByIdAsync(alarmId, cancellationToken);
        if (alarm == null)
            return Result<bool>.Failure($"Alarm with ID {alarmId} not found.");

        var template = await _repository.GetTemplateByIdAsync(alarm.TemplateId, cancellationToken);
        if (template?.ParentTemplateId != null)
        {
            var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
            var parentMember = TemplateResolver.FindMemberByCanonicalName(
                alarm.Name, template.ParentTemplateId.Value, allTemplates);
            if (parentMember != null)
                return Result<bool>.Failure(
                    $"Cannot remove alarm '{alarm.Name}': it is inherited from parent template.");
        }

        await _repository.DeleteTemplateAlarmAsync(alarmId, cancellationToken);
        await _auditService.LogAsync(user, "Delete", "TemplateAlarm", alarmId.ToString(), alarm.Name, null, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        return Result<bool>.Success(true);
    }

    // ========================================================================
    // WP-4: Script Definitions
    // ========================================================================

    public async Task<Result<TemplateScript>> AddScriptAsync(
        int templateId,
        TemplateScript script,
        string user,
        CancellationToken cancellationToken = default)
    {
        var template = await _repository.GetTemplateByIdAsync(templateId, cancellationToken);
        if (template == null)
            return Result<TemplateScript>.Failure($"Template with ID {templateId} not found.");

        if (template.Scripts.Any(s => s.Name == script.Name))
            return Result<TemplateScript>.Failure(
                $"Script '{script.Name}' already exists on template '{template.Name}'.");

        script.TemplateId = templateId;

        // Check collisions
        var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
        var testTemplate = CloneTemplateWithNewScript(template, script);
        var collisions = CollisionDetector.DetectCollisions(testTemplate, allTemplates);
        if (collisions.Count > 0)
            return Result<TemplateScript>.Failure(string.Join(" ", collisions));

        await _repository.AddTemplateScriptAsync(script, cancellationToken);
        await _auditService.LogAsync(user, "Create", "TemplateScript", "0", script.Name, script, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        return Result<TemplateScript>.Success(script);
    }

    public async Task<Result<TemplateScript>> UpdateScriptAsync(
        int scriptId,
        TemplateScript proposed,
        string user,
        CancellationToken cancellationToken = default)
    {
        var existing = await _repository.GetTemplateScriptByIdAsync(scriptId, cancellationToken);
        if (existing == null)
            return Result<TemplateScript>.Failure($"Script with ID {scriptId} not found.");

        // Validate lock change
        var lockError = LockEnforcer.ValidateLockChange(existing.IsLocked, proposed.IsLocked, existing.Name);
        if (lockError != null)
            return Result<TemplateScript>.Failure(lockError);

        // Check parent lock
        var template = await _repository.GetTemplateByIdAsync(existing.TemplateId, cancellationToken);
        if (template?.ParentTemplateId != null)
        {
            var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
            var parentMember = TemplateResolver.FindMemberByCanonicalName(
                existing.Name, template.ParentTemplateId.Value, allTemplates);
            if (parentMember != null && parentMember.IsLocked)
                return Result<TemplateScript>.Failure(
                    $"Script '{existing.Name}' is locked in parent and cannot be overridden.");

            // Derived templates may not override scripts the base marked LockedInDerived.
            if (template.IsDerived)
            {
                var baseTemplate = await _repository.GetTemplateByIdAsync(template.ParentTemplateId.Value, cancellationToken);
                var baseScript = baseTemplate?.Scripts.FirstOrDefault(s => s.Name == existing.Name);
                if (baseScript != null && baseScript.LockedInDerived)
                    return Result<TemplateScript>.Failure(
                        $"Script '{existing.Name}' is locked by base template '{baseTemplate!.Name}' and cannot be overridden.");
            }
        }

        // Validate fixed fields
        var overrideError = LockEnforcer.ValidateScriptOverride(existing, proposed);
        if (overrideError != null)
            return Result<TemplateScript>.Failure(overrideError);

        // Apply overridable fields
        existing.Code = proposed.Code;
        existing.TriggerType = proposed.TriggerType;
        existing.TriggerConfiguration = proposed.TriggerConfiguration;
        existing.MinTimeBetweenRuns = proposed.MinTimeBetweenRuns;
        existing.ParameterDefinitions = proposed.ParameterDefinitions;
        existing.ReturnDefinition = proposed.ReturnDefinition;
        existing.IsLocked = proposed.IsLocked;
        if (template?.IsDerived == true)
            existing.IsInherited = proposed.IsInherited;
        else
            existing.LockedInDerived = proposed.LockedInDerived;
        // Name is NOT updated (fixed)

        await _repository.UpdateTemplateScriptAsync(existing, cancellationToken);
        await _auditService.LogAsync(user, "Update", "TemplateScript", scriptId.ToString(), existing.Name, existing, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        return Result<TemplateScript>.Success(existing);
    }

    public async Task<Result<bool>> DeleteScriptAsync(
        int scriptId,
        string user,
        CancellationToken cancellationToken = default)
    {
        var script = await _repository.GetTemplateScriptByIdAsync(scriptId, cancellationToken);
        if (script == null)
            return Result<bool>.Failure($"Script with ID {scriptId} not found.");

        var template = await _repository.GetTemplateByIdAsync(script.TemplateId, cancellationToken);
        if (template?.ParentTemplateId != null)
        {
            var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
            var parentMember = TemplateResolver.FindMemberByCanonicalName(
                script.Name, template.ParentTemplateId.Value, allTemplates);
            if (parentMember != null)
                return Result<bool>.Failure(
                    $"Cannot remove script '{script.Name}': it is inherited from parent template.");
        }

        await _repository.DeleteTemplateScriptAsync(scriptId, cancellationToken);
        await _auditService.LogAsync(user, "Delete", "TemplateScript", scriptId.ToString(), script.Name, null, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        return Result<bool>.Success(true);
    }

    // ========================================================================
    // WP-6: Composition with Recursive Nesting
    // ========================================================================

    public async Task<Result<TemplateComposition>> AddCompositionAsync(
        int templateId,
        int composedTemplateId,
        string instanceName,
        string user,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(instanceName))
            return Result<TemplateComposition>.Failure("Instance name is required for composition.");

        var template = await _repository.GetTemplateByIdAsync(templateId, cancellationToken);
        if (template == null)
            return Result<TemplateComposition>.Failure($"Template with ID {templateId} not found.");

        var baseTemplate = await _repository.GetTemplateByIdAsync(composedTemplateId, cancellationToken);
        if (baseTemplate == null)
            return Result<TemplateComposition>.Failure($"Composed template with ID {composedTemplateId} not found.");

        // Only base templates can be composed; derived templates are slot-owned.
        if (baseTemplate.IsDerived)
            return Result<TemplateComposition>.Failure(
                $"Cannot compose template '{baseTemplate.Name}': it is a derived template. Compose its base instead.");

        // Check for duplicate instance name
        if (template.Compositions.Any(c => c.InstanceName == instanceName))
            return Result<TemplateComposition>.Failure(
                $"Composition instance name '{instanceName}' already exists on template '{template.Name}'.");

        // Acyclicity is checked against the base, not the to-be-created derived template —
        // the derived inherits from the base, so a base→base cycle is the meaningful check.
        var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
        var cycleError = CycleDetector.DetectCompositionCycle(templateId, composedTemplateId, allTemplates);
        if (cycleError != null)
            return Result<TemplateComposition>.Failure(cycleError);

        var crossCycleError = CycleDetector.DetectCrossGraphCycle(templateId, null, composedTemplateId, allTemplates);
        if (crossCycleError != null)
            return Result<TemplateComposition>.Failure(crossCycleError);

        var probeComposition = new TemplateComposition(instanceName)
        {
            TemplateId = templateId,
            ComposedTemplateId = composedTemplateId
        };

        var testTemplate = CloneTemplateWithNewComposition(template, probeComposition);
        var collisions = CollisionDetector.DetectCollisions(testTemplate, allTemplates);
        if (collisions.Count > 0)
            return Result<TemplateComposition>.Failure(string.Join(" ", collisions));

        // Derived template name uses dot-separated path: "<parent>.<slot>". The
        // cascade may create additional derived templates one level per slot
        // (composing $Sensor with a Probe1 slot into $Pump produces both
        // $Pump.TempSensor and $Pump.TempSensor.Probe1). Pre-check every name
        // the cascade is about to introduce so a deep collision aborts before
        // any rows mutate.
        var byId = allTemplates.ToDictionary(t => t.Id);
        var cascadeNames = EnumerateCascadeNames(template.Name, instanceName, baseTemplate, byId).ToList();
        var existingNames = allTemplates.Select(t => t.Name).ToHashSet(StringComparer.Ordinal);
        var nameCollision = cascadeNames.FirstOrDefault(n => existingNames.Contains(n));
        if (nameCollision != null)
            return Result<TemplateComposition>.Failure(
                $"Cannot create derived template '{nameCollision}': a template with that name already exists.");

        var composition = await CreateCascadedCompositionAsync(template, baseTemplate, instanceName, user, cancellationToken);
        await _auditService.LogAsync(user, "Create", "TemplateComposition", "0", instanceName, composition, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        return Result<TemplateComposition>.Success(composition);
    }

    /// <summary>
    /// Creates a derived template under <paramref name="outerTemplate"/> that
    /// wraps <paramref name="source"/>, then recursively cascades the source's
    /// own compositions so the slot graph is replicated under the new derived.
    /// Used both for the user-initiated top-level compose and the recursive
    /// children — neither path re-validates (caller pre-flights).
    /// </summary>
    private async Task<TemplateComposition> CreateCascadedCompositionAsync(
        Template outerTemplate,
        Template source,
        string instanceName,
        string user,
        CancellationToken cancellationToken)
    {
        var derivedName = $"{outerTemplate.Name}.{instanceName}";
        var derived = BuildDerivedTemplate(source, derivedName);

        await _repository.AddTemplateAsync(derived, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        var composition = new TemplateComposition(instanceName)
        {
            TemplateId = outerTemplate.Id,
            ComposedTemplateId = derived.Id
        };
        await _repository.AddTemplateCompositionAsync(composition, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        derived.OwnerCompositionId = composition.Id;
        await _repository.UpdateTemplateAsync(derived, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        // Cascade — replicate each of the source's compositions onto the new
        // derived. The child's ParentTemplateId points at the source-side
        // child (so override chains stay intact across nesting).
        foreach (var childComp in source.Compositions.ToList())
        {
            var childSource = await _repository.GetTemplateByIdAsync(childComp.ComposedTemplateId, cancellationToken);
            if (childSource == null) continue;
            await CreateCascadedCompositionAsync(derived, childSource, childComp.InstanceName, user, cancellationToken);
        }

        return composition;
    }

    private static IEnumerable<string> EnumerateCascadeNames(
        string outerName, string instanceName, Template source, IReadOnlyDictionary<int, Template> byId)
    {
        var derivedName = $"{outerName}.{instanceName}";
        yield return derivedName;
        foreach (var comp in source.Compositions)
        {
            if (!byId.TryGetValue(comp.ComposedTemplateId, out var child)) continue;
            foreach (var name in EnumerateCascadeNames(derivedName, comp.InstanceName, child, byId))
                yield return name;
        }
    }

    public async Task<Result<TemplateComposition>> RenameCompositionAsync(
        int compositionId,
        string newInstanceName,
        string user,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(newInstanceName))
            return Result<TemplateComposition>.Failure("Slot name is required.");

        var composition = await _repository.GetTemplateCompositionByIdAsync(compositionId, cancellationToken);
        if (composition == null)
            return Result<TemplateComposition>.Failure($"Composition with ID {compositionId} not found.");

        if (composition.InstanceName == newInstanceName) return Result<TemplateComposition>.Success(composition);

        var owner = await _repository.GetTemplateByIdAsync(composition.TemplateId, cancellationToken);
        if (owner == null)
            return Result<TemplateComposition>.Failure($"Owning template with ID {composition.TemplateId} not found.");

        if (owner.Compositions.Any(c => c.Id != compositionId && c.InstanceName == newInstanceName))
            return Result<TemplateComposition>.Failure(
                $"Slot name '{newInstanceName}' already exists on '{owner.Name}'.");

        var derived = await _repository.GetTemplateByIdAsync(composition.ComposedTemplateId, cancellationToken);
        if (derived != null && derived.IsDerived && derived.OwnerCompositionId == compositionId)
        {
            var newDerivedName = $"{owner.Name}.{newInstanceName}";
            var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
            if (allTemplates.Any(t => t.Id != derived.Id && t.Name == newDerivedName))
                return Result<TemplateComposition>.Failure(
                    $"Cannot rename derived template to '{newDerivedName}': a template with that name already exists.");

            derived.Name = newDerivedName;
            await _repository.UpdateTemplateAsync(derived, cancellationToken);
        }

        composition.InstanceName = newInstanceName;
        await _repository.UpdateTemplateCompositionAsync(composition, cancellationToken);
        await _auditService.LogAsync(user, "Update", "TemplateComposition", compositionId.ToString(), newInstanceName, composition, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        return Result<TemplateComposition>.Success(composition);
    }

    public async Task<Result<bool>> DeleteCompositionAsync(
        int compositionId,
        string user,
        CancellationToken cancellationToken = default)
    {
        var composition = await _repository.GetTemplateCompositionByIdAsync(compositionId, cancellationToken);
        if (composition == null)
            return Result<bool>.Failure($"Composition with ID {compositionId} not found.");

        // Identify the slot-owned derived template (post Phase-3 migration this is the
        // typical case; pre-migration the composition may still point at a base).
        var composedTemplate = await _repository.GetTemplateByIdAsync(composition.ComposedTemplateId, cancellationToken);

        await _repository.DeleteTemplateCompositionAsync(compositionId, cancellationToken);
        await _auditService.LogAsync(user, "Delete", "TemplateComposition", compositionId.ToString(), composition.InstanceName, null, cancellationToken);

        if (composedTemplate != null && composedTemplate.IsDerived && composedTemplate.OwnerCompositionId == compositionId)
        {
            await CascadeDeleteDerivedAsync(composedTemplate, user, cancellationToken);
        }

        await _repository.SaveChangesAsync(cancellationToken);

        return Result<bool>.Success(true);
    }

    /// <summary>
    /// Recursively deletes a derived template along with the cascade of inner
    /// derived templates the compose flow created. Each composition row on the
    /// derived has its slot-owned child template removed first, then the row,
    /// then the derived itself.
    /// </summary>
    private async Task CascadeDeleteDerivedAsync(Template derived, string user, CancellationToken cancellationToken)
    {
        foreach (var child in derived.Compositions.ToList())
        {
            var childDerived = await _repository.GetTemplateByIdAsync(child.ComposedTemplateId, cancellationToken);
            await _repository.DeleteTemplateCompositionAsync(child.Id, cancellationToken);
            await _auditService.LogAsync(user, "Delete", "TemplateComposition", child.Id.ToString(), child.InstanceName, null, cancellationToken);
            if (childDerived != null && childDerived.IsDerived && childDerived.OwnerCompositionId == child.Id)
                await CascadeDeleteDerivedAsync(childDerived, user, cancellationToken);
        }

        await _repository.DeleteTemplateAsync(derived.Id, cancellationToken);
        await _auditService.LogAsync(user, "Delete", "Template", derived.Id.ToString(), derived.Name, null, cancellationToken);
    }

    private static Template BuildDerivedTemplate(Template baseTemplate, string derivedName)
    {
        var derived = new Template(derivedName)
        {
            Description = baseTemplate.Description,
            ParentTemplateId = baseTemplate.Id,
            IsDerived = true,
        };

        foreach (var attr in baseTemplate.Attributes)
        {
            derived.Attributes.Add(new TemplateAttribute(attr.Name)
            {
                Value = attr.Value,
                DataType = attr.DataType,
                IsLocked = attr.IsLocked,
                Description = attr.Description,
                DataSourceReference = attr.DataSourceReference,
                IsInherited = true,
                LockedInDerived = false,
            });
        }

        foreach (var alarm in baseTemplate.Alarms)
        {
            derived.Alarms.Add(new TemplateAlarm(alarm.Name)
            {
                Description = alarm.Description,
                PriorityLevel = alarm.PriorityLevel,
                IsLocked = alarm.IsLocked,
                TriggerType = alarm.TriggerType,
                TriggerConfiguration = alarm.TriggerConfiguration,
                OnTriggerScriptId = alarm.OnTriggerScriptId,
                IsInherited = true,
                LockedInDerived = false,
            });
        }

        foreach (var script in baseTemplate.Scripts)
        {
            derived.Scripts.Add(new TemplateScript(script.Name, script.Code)
            {
                IsLocked = script.IsLocked,
                TriggerType = script.TriggerType,
                TriggerConfiguration = script.TriggerConfiguration,
                ParameterDefinitions = script.ParameterDefinitions,
                ReturnDefinition = script.ReturnDefinition,
                MinTimeBetweenRuns = script.MinTimeBetweenRuns,
                IsInherited = true,
                LockedInDerived = false,
            });
        }

        return derived;
    }

    // ========================================================================
    // WP-7: Path-Qualified Canonical Naming (via TemplateResolver)
    // ========================================================================

    /// <summary>
    /// Resolves all effective members for a template using canonical (path-qualified) names.
    /// </summary>
    public async Task<IReadOnlyList<TemplateResolver.ResolvedTemplateMember>> ResolveTemplateMembersAsync(
        int templateId,
        CancellationToken cancellationToken = default)
    {
        var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
        return TemplateResolver.ResolveAllMembers(templateId, allTemplates);
    }

    // ========================================================================
    // WP-8/9/10/11: Override validation (integrated into Update* methods above)
    // ========================================================================

    /// <summary>
    /// Validates whether overriding a member by canonical name is allowed.
    /// Used for composition overrides (WP-11) and inheritance overrides (WP-10).
    /// </summary>
    public async Task<Result<bool>> ValidateOverrideAsync(
        int templateId,
        string canonicalName,
        CancellationToken cancellationToken = default)
    {
        var template = await _repository.GetTemplateByIdAsync(templateId, cancellationToken);
        if (template == null)
            return Result<bool>.Failure($"Template with ID {templateId} not found.");

        var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
        var members = TemplateResolver.ResolveAllMembers(templateId, allTemplates);
        var member = members.FirstOrDefault(m => m.CanonicalName == canonicalName);

        if (member == null)
            return Result<bool>.Failure($"No member found with canonical name '{canonicalName}'.");

        if (member.IsLocked)
            return Result<bool>.Failure($"Member '{canonicalName}' is locked and cannot be overridden.");

        return Result<bool>.Success(true);
    }

    // ========================================================================
    // WP-12: Naming Collision Detection
    // ========================================================================

    /// <summary>
    /// Checks a template for naming collisions across all its members.
    /// </summary>
    public async Task<IReadOnlyList<string>> DetectCollisionsAsync(
        int templateId,
        CancellationToken cancellationToken = default)
    {
        var template = await _repository.GetTemplateByIdAsync(templateId, cancellationToken);
        if (template == null)
            return new[] { $"Template with ID {templateId} not found." };

        var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
        return CollisionDetector.DetectCollisions(template, allTemplates);
    }

    // ========================================================================
    // WP-13: Graph Acyclicity
    // ========================================================================

    /// <summary>
    /// Validates that a proposed inheritance or composition change does not create a cycle.
    /// </summary>
    public async Task<Result<bool>> ValidateAcyclicityAsync(
        int templateId,
        int? proposedParentId,
        int? proposedComposedTemplateId,
        CancellationToken cancellationToken = default)
    {
        var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);

        if (proposedParentId.HasValue)
        {
            var inheritanceCycle = CycleDetector.DetectInheritanceCycle(templateId, proposedParentId.Value, allTemplates);
            if (inheritanceCycle != null)
                return Result<bool>.Failure(inheritanceCycle);
        }

        if (proposedComposedTemplateId.HasValue)
        {
            var compositionCycle = CycleDetector.DetectCompositionCycle(templateId, proposedComposedTemplateId.Value, allTemplates);
            if (compositionCycle != null)
                return Result<bool>.Failure(compositionCycle);
        }

        var crossCycle = CycleDetector.DetectCrossGraphCycle(templateId, proposedParentId, proposedComposedTemplateId, allTemplates);
        if (crossCycle != null)
            return Result<bool>.Failure(crossCycle);

        return Result<bool>.Success(true);
    }

    // ========================================================================
    // Helper methods
    // ========================================================================

    private async Task<string?> ValidateCollisionsAsync(Template template, CancellationToken cancellationToken)
    {
        var allTemplates = await _repository.GetAllTemplatesAsync(cancellationToken);
        var collisions = CollisionDetector.DetectCollisions(template, allTemplates);
        if (collisions.Count > 0)
            return string.Join(" ", collisions);
        return null;
    }

    private static Template CloneTemplateWithNewAttribute(Template original, TemplateAttribute newAttr)
    {
        var clone = new Template(original.Name)
        {
            Id = original.Id,
            ParentTemplateId = original.ParentTemplateId,
            Description = original.Description,
        };
        foreach (var a in original.Attributes) clone.Attributes.Add(a);
        clone.Attributes.Add(newAttr);
        foreach (var a in original.Alarms) clone.Alarms.Add(a);
        foreach (var s in original.Scripts) clone.Scripts.Add(s);
        foreach (var c in original.Compositions) clone.Compositions.Add(c);
        return clone;
    }

    private static Template CloneTemplateWithNewAlarm(Template original, TemplateAlarm newAlarm)
    {
        var clone = new Template(original.Name)
        {
            Id = original.Id,
            ParentTemplateId = original.ParentTemplateId,
            Description = original.Description,
        };
        foreach (var a in original.Attributes) clone.Attributes.Add(a);
        foreach (var a in original.Alarms) clone.Alarms.Add(a);
        clone.Alarms.Add(newAlarm);
        foreach (var s in original.Scripts) clone.Scripts.Add(s);
        foreach (var c in original.Compositions) clone.Compositions.Add(c);
        return clone;
    }

    private static Template CloneTemplateWithNewScript(Template original, TemplateScript newScript)
    {
        var clone = new Template(original.Name)
        {
            Id = original.Id,
            ParentTemplateId = original.ParentTemplateId,
            Description = original.Description,
        };
        foreach (var a in original.Attributes) clone.Attributes.Add(a);
        foreach (var a in original.Alarms) clone.Alarms.Add(a);
        foreach (var s in original.Scripts) clone.Scripts.Add(s);
        clone.Scripts.Add(newScript);
        foreach (var c in original.Compositions) clone.Compositions.Add(c);
        return clone;
    }

    private static Template CloneTemplateWithNewComposition(Template original, TemplateComposition newComp)
    {
        var clone = new Template(original.Name)
        {
            Id = original.Id,
            ParentTemplateId = original.ParentTemplateId,
            Description = original.Description,
        };
        foreach (var a in original.Attributes) clone.Attributes.Add(a);
        foreach (var a in original.Alarms) clone.Alarms.Add(a);
        foreach (var s in original.Scripts) clone.Scripts.Add(s);
        foreach (var c in original.Compositions) clone.Compositions.Add(c);
        clone.Compositions.Add(newComp);
        return clone;
    }
}
