using ScadaLink.Commons.Entities.Instances;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Commons.Types;
using ScadaLink.Commons.Types.Enums;

namespace ScadaLink.TemplateEngine.Services;

/// <summary>
/// Instance CRUD operations.
/// - Create instance from template at site
/// - Assign to area
/// - Override non-locked attribute values
/// - Cannot add or remove attributes (only override existing ones)
/// - Per-attribute connection binding (bulk assignment support)
/// - Enabled/disabled state with optimistic concurrency
/// - Audit logging
/// </summary>
public class InstanceService
{
    private readonly ITemplateEngineRepository _repository;
    private readonly IAuditService _auditService;

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

    /// <summary>
    /// Creates a new instance from a template at a site.
    /// </summary>
    public async Task<Result<Instance>> CreateInstanceAsync(
        string uniqueName,
        int templateId,
        int siteId,
        int? areaId,
        string user,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(uniqueName))
            return Result<Instance>.Failure("Instance unique name is required.");

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

        // Check for duplicate unique name
        var existing = await _repository.GetInstanceByUniqueNameAsync(uniqueName, cancellationToken);
        if (existing != null)
            return Result<Instance>.Failure($"Instance with unique name '{uniqueName}' already exists.");

        // Verify area exists if specified
        if (areaId.HasValue)
        {
            var area = await _repository.GetAreaByIdAsync(areaId.Value, cancellationToken);
            if (area == null)
                return Result<Instance>.Failure($"Area with ID {areaId.Value} not found.");
            if (area.SiteId != siteId)
                return Result<Instance>.Failure($"Area with ID {areaId.Value} does not belong to site {siteId}.");
        }

        var instance = new Instance(uniqueName)
        {
            TemplateId = templateId,
            SiteId = siteId,
            AreaId = areaId,
            State = InstanceState.Disabled // New instances start disabled
        };

        await _repository.AddInstanceAsync(instance, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        await _auditService.LogAsync(user, "Create", "Instance", instance.Id.ToString(),
            uniqueName, instance, cancellationToken);

        return Result<Instance>.Success(instance);
    }

    /// <summary>
    /// Assigns an instance to an area.
    /// </summary>
    public async Task<Result<Instance>> AssignToAreaAsync(
        int instanceId,
        int? areaId,
        string user,
        CancellationToken cancellationToken = default)
    {
        var instance = await _repository.GetInstanceByIdAsync(instanceId, cancellationToken);
        if (instance == null)
            return Result<Instance>.Failure($"Instance with ID {instanceId} not found.");

        if (areaId.HasValue)
        {
            var area = await _repository.GetAreaByIdAsync(areaId.Value, cancellationToken);
            if (area == null)
                return Result<Instance>.Failure($"Area with ID {areaId.Value} not found.");
            if (area.SiteId != instance.SiteId)
                return Result<Instance>.Failure($"Area with ID {areaId.Value} does not belong to the instance's site.");
        }

        instance.AreaId = areaId;
        await _repository.UpdateInstanceAsync(instance, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        await _auditService.LogAsync(user, "AssignArea", "Instance", instance.Id.ToString(),
            instance.UniqueName, instance, cancellationToken);

        return Result<Instance>.Success(instance);
    }

    /// <summary>
    /// Sets an attribute override for an instance. Only non-locked attributes can be overridden.
    /// Cannot add or remove attributes — only override values of existing template attributes.
    /// </summary>
    public async Task<Result<InstanceAttributeOverride>> SetAttributeOverrideAsync(
        int instanceId,
        string attributeName,
        string? overrideValue,
        string user,
        CancellationToken cancellationToken = default)
    {
        var instance = await _repository.GetInstanceByIdAsync(instanceId, cancellationToken);
        if (instance == null)
            return Result<InstanceAttributeOverride>.Failure($"Instance with ID {instanceId} not found.");

        // Verify attribute exists in the template and is not locked
        var templateAttrs = await _repository.GetAttributesByTemplateIdAsync(instance.TemplateId, cancellationToken);
        var templateAttr = templateAttrs.FirstOrDefault(a => a.Name == attributeName);
        if (templateAttr == null)
            return Result<InstanceAttributeOverride>.Failure(
                $"Attribute '{attributeName}' does not exist in template {instance.TemplateId}. Cannot add new attributes via overrides.");

        if (templateAttr.IsLocked)
            return Result<InstanceAttributeOverride>.Failure(
                $"Attribute '{attributeName}' is locked and cannot be overridden.");

        // Find existing override or create new one
        var overrides = await _repository.GetOverridesByInstanceIdAsync(instanceId, cancellationToken);
        var existingOverride = overrides.FirstOrDefault(o => o.AttributeName == attributeName);

        if (existingOverride != null)
        {
            existingOverride.OverrideValue = overrideValue;
            await _repository.UpdateInstanceAttributeOverrideAsync(existingOverride, cancellationToken);
            await _repository.SaveChangesAsync(cancellationToken);

            await _auditService.LogAsync(user, "UpdateOverride", "InstanceAttributeOverride",
                existingOverride.Id.ToString(), attributeName, existingOverride, cancellationToken);

            return Result<InstanceAttributeOverride>.Success(existingOverride);
        }
        else
        {
            var newOverride = new InstanceAttributeOverride(attributeName)
            {
                InstanceId = instanceId,
                OverrideValue = overrideValue
            };

            await _repository.AddInstanceAttributeOverrideAsync(newOverride, cancellationToken);
            await _repository.SaveChangesAsync(cancellationToken);

            await _auditService.LogAsync(user, "CreateOverride", "InstanceAttributeOverride",
                newOverride.Id.ToString(), attributeName, newOverride, cancellationToken);

            return Result<InstanceAttributeOverride>.Success(newOverride);
        }
    }

    /// <summary>
    /// Sets a per-instance alarm override. The alarm must exist on the
    /// template and must not be locked. For HiLo alarms, the override JSON
    /// merges into the inherited TriggerConfiguration setpoint-by-setpoint;
    /// for binary trigger types, it replaces the whole config.
    /// </summary>
    public async Task<Result<InstanceAlarmOverride>> SetAlarmOverrideAsync(
        int instanceId,
        string alarmCanonicalName,
        string? triggerConfigurationOverride,
        int? priorityLevelOverride,
        string user,
        CancellationToken cancellationToken = default)
    {
        var instance = await _repository.GetInstanceByIdAsync(instanceId, cancellationToken);
        if (instance == null)
            return Result<InstanceAlarmOverride>.Failure($"Instance with ID {instanceId} not found.");

        // Verify alarm exists in the template and is not locked. Only direct
        // template alarms are checked here — composed-member overrides go
        // through but are silently ignored at runtime if the name doesn't
        // match (same behavior as attribute overrides).
        var templateAlarms = await _repository.GetAlarmsByTemplateIdAsync(instance.TemplateId, cancellationToken);
        var templateAlarm = templateAlarms.FirstOrDefault(a => a.Name == alarmCanonicalName);
        if (templateAlarm != null && templateAlarm.IsLocked)
        {
            return Result<InstanceAlarmOverride>.Failure(
                $"Alarm '{alarmCanonicalName}' is locked and cannot be overridden.");
        }

        var existingOverride = await _repository.GetAlarmOverrideAsync(
            instanceId, alarmCanonicalName, cancellationToken);

        if (existingOverride != null)
        {
            existingOverride.TriggerConfigurationOverride = triggerConfigurationOverride;
            existingOverride.PriorityLevelOverride = priorityLevelOverride;
            await _repository.UpdateInstanceAlarmOverrideAsync(existingOverride, cancellationToken);
            await _repository.SaveChangesAsync(cancellationToken);

            await _auditService.LogAsync(user, "UpdateAlarmOverride", "InstanceAlarmOverride",
                existingOverride.Id.ToString(), alarmCanonicalName, existingOverride, cancellationToken);

            return Result<InstanceAlarmOverride>.Success(existingOverride);
        }
        else
        {
            var newOverride = new InstanceAlarmOverride(alarmCanonicalName)
            {
                InstanceId = instanceId,
                TriggerConfigurationOverride = triggerConfigurationOverride,
                PriorityLevelOverride = priorityLevelOverride
            };

            await _repository.AddInstanceAlarmOverrideAsync(newOverride, cancellationToken);
            await _repository.SaveChangesAsync(cancellationToken);

            await _auditService.LogAsync(user, "CreateAlarmOverride", "InstanceAlarmOverride",
                newOverride.Id.ToString(), alarmCanonicalName, newOverride, cancellationToken);

            return Result<InstanceAlarmOverride>.Success(newOverride);
        }
    }

    /// <summary>
    /// Removes a per-instance alarm override. After removal the instance
    /// inherits the template alarm config unchanged.
    /// </summary>
    public async Task<Result<bool>> DeleteAlarmOverrideAsync(
        int instanceId,
        string alarmCanonicalName,
        string user,
        CancellationToken cancellationToken = default)
    {
        var existing = await _repository.GetAlarmOverrideAsync(
            instanceId, alarmCanonicalName, cancellationToken);
        if (existing == null)
            return Result<bool>.Failure($"No alarm override for '{alarmCanonicalName}' on instance {instanceId}.");

        await _repository.DeleteInstanceAlarmOverrideAsync(existing.Id, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        await _auditService.LogAsync(user, "DeleteAlarmOverride", "InstanceAlarmOverride",
            existing.Id.ToString(), alarmCanonicalName, existing, cancellationToken);

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

    /// <summary>
    /// Sets connection bindings for an instance in bulk.
    /// </summary>
    public async Task<Result<IReadOnlyList<InstanceConnectionBinding>>> SetConnectionBindingsAsync(
        int instanceId,
        IReadOnlyList<(string AttributeName, int DataConnectionId)> bindings,
        string user,
        CancellationToken cancellationToken = default)
    {
        var instance = await _repository.GetInstanceByIdAsync(instanceId, cancellationToken);
        if (instance == null)
            return Result<IReadOnlyList<InstanceConnectionBinding>>.Failure($"Instance with ID {instanceId} not found.");

        var existingBindings = await _repository.GetBindingsByInstanceIdAsync(instanceId, cancellationToken);
        var existingMap = existingBindings.ToDictionary(b => b.AttributeName, StringComparer.Ordinal);

        var results = new List<InstanceConnectionBinding>();

        foreach (var (attrName, connId) in bindings)
        {
            if (existingMap.TryGetValue(attrName, out var existing))
            {
                existing.DataConnectionId = connId;
                await _repository.UpdateInstanceConnectionBindingAsync(existing, cancellationToken);
                results.Add(existing);
            }
            else
            {
                var binding = new InstanceConnectionBinding(attrName)
                {
                    InstanceId = instanceId,
                    DataConnectionId = connId
                };
                await _repository.AddInstanceConnectionBindingAsync(binding, cancellationToken);
                results.Add(binding);
            }
        }

        await _repository.SaveChangesAsync(cancellationToken);

        await _auditService.LogAsync(user, "SetConnectionBindings", "Instance",
            instance.Id.ToString(), instance.UniqueName, bindings, cancellationToken);

        return Result<IReadOnlyList<InstanceConnectionBinding>>.Success(results);
    }

    /// <summary>
    /// Enables an instance.
    /// </summary>
    public async Task<Result<Instance>> EnableAsync(int instanceId, string user, CancellationToken cancellationToken = default)
    {
        var instance = await _repository.GetInstanceByIdAsync(instanceId, cancellationToken);
        if (instance == null)
            return Result<Instance>.Failure($"Instance with ID {instanceId} not found.");

        instance.State = InstanceState.Enabled;
        await _repository.UpdateInstanceAsync(instance, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        await _auditService.LogAsync(user, "Enable", "Instance", instance.Id.ToString(),
            instance.UniqueName, instance, cancellationToken);

        return Result<Instance>.Success(instance);
    }

    /// <summary>
    /// Disables an instance.
    /// </summary>
    public async Task<Result<Instance>> DisableAsync(int instanceId, string user, CancellationToken cancellationToken = default)
    {
        var instance = await _repository.GetInstanceByIdAsync(instanceId, cancellationToken);
        if (instance == null)
            return Result<Instance>.Failure($"Instance with ID {instanceId} not found.");

        instance.State = InstanceState.Disabled;
        await _repository.UpdateInstanceAsync(instance, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        await _auditService.LogAsync(user, "Disable", "Instance", instance.Id.ToString(),
            instance.UniqueName, instance, cancellationToken);

        return Result<Instance>.Success(instance);
    }

    /// <summary>
    /// Deletes an instance.
    /// </summary>
    public async Task<Result<bool>> DeleteAsync(int instanceId, string user, CancellationToken cancellationToken = default)
    {
        var instance = await _repository.GetInstanceByIdAsync(instanceId, cancellationToken);
        if (instance == null)
            return Result<bool>.Failure($"Instance with ID {instanceId} not found.");

        await _repository.DeleteInstanceAsync(instanceId, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        await _auditService.LogAsync(user, "Delete", "Instance", instanceId.ToString(),
            instance.UniqueName, null, cancellationToken);

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

    /// <summary>
    /// Gets an instance by ID.
    /// </summary>
    public async Task<Instance?> GetByIdAsync(int instanceId, CancellationToken cancellationToken = default) =>
        await _repository.GetInstanceByIdAsync(instanceId, cancellationToken);

    /// <summary>
    /// Gets all instances for a site.
    /// </summary>
    public async Task<IReadOnlyList<Instance>> GetBySiteIdAsync(int siteId, CancellationToken cancellationToken = default) =>
        await _repository.GetInstancesBySiteIdAsync(siteId, cancellationToken);
}
