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

namespace ScadaLink.TemplateEngine.Services;

/// <summary>
/// Hierarchical area management per site.
/// - CRUD for areas with parent-child relationships
/// - Deletion constrained if instances are assigned
/// - Audit logging
/// </summary>
public class AreaService
{
    private readonly ITemplateEngineRepository _repository;
    private readonly IAuditService _auditService;

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

    /// <summary>
    /// Creates a new area within a site.
    /// </summary>
    public async Task<Result<Area>> CreateAreaAsync(
        string name, int siteId, int? parentAreaId, string user,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(name))
            return Result<Area>.Failure("Area name is required.");

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

        // Check for duplicate name at the same level
        var siblings = await _repository.GetAreasBySiteIdAsync(siteId, cancellationToken);
        var duplicate = siblings.FirstOrDefault(a =>
            a.ParentAreaId == parentAreaId &&
            string.Equals(a.Name, name, StringComparison.OrdinalIgnoreCase));
        if (duplicate != null)
            return Result<Area>.Failure($"An area named '{name}' already exists at this level in site {siteId}.");

        var area = new Area(name)
        {
            SiteId = siteId,
            ParentAreaId = parentAreaId
        };

        await _repository.AddAreaAsync(area, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        await _auditService.LogAsync(user, "Create", "Area", area.Id.ToString(), name, area, cancellationToken);

        return Result<Area>.Success(area);
    }

    /// <summary>
    /// Updates an area's name.
    /// </summary>
    public async Task<Result<Area>> UpdateAreaAsync(
        int areaId, string name, string user,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(name))
            return Result<Area>.Failure("Area name is required.");

        var area = await _repository.GetAreaByIdAsync(areaId, cancellationToken);
        if (area == null)
            return Result<Area>.Failure($"Area with ID {areaId} not found.");

        // Check for duplicate name at the same level
        var siblings = await _repository.GetAreasBySiteIdAsync(area.SiteId, cancellationToken);
        var duplicate = siblings.FirstOrDefault(a =>
            a.Id != areaId &&
            a.ParentAreaId == area.ParentAreaId &&
            string.Equals(a.Name, name, StringComparison.OrdinalIgnoreCase));
        if (duplicate != null)
            return Result<Area>.Failure($"An area named '{name}' already exists at this level.");

        area.Name = name;
        await _repository.UpdateAreaAsync(area, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        await _auditService.LogAsync(user, "Update", "Area", area.Id.ToString(), name, area, cancellationToken);

        return Result<Area>.Success(area);
    }

    /// <summary>
    /// Re-parents an area within its site. `newParentAreaId == null` moves the area to the site root.
    /// Rejects: self-parent, descendant-parent (cycle), cross-site parent, name collision at new level.
    /// </summary>
    public async Task<Result<Area>> MoveAreaAsync(
        int areaId, int? newParentAreaId, string user,
        CancellationToken cancellationToken = default)
    {
        var area = await _repository.GetAreaByIdAsync(areaId, cancellationToken);
        if (area == null)
            return Result<Area>.Failure($"Area with ID {areaId} not found.");

        if (newParentAreaId == areaId)
            return Result<Area>.Failure("An area cannot be its own parent.");

        if (newParentAreaId.HasValue)
        {
            var newParent = await _repository.GetAreaByIdAsync(newParentAreaId.Value, cancellationToken);
            if (newParent == null)
                return Result<Area>.Failure($"Target parent area with ID {newParentAreaId.Value} not found.");
            if (newParent.SiteId != area.SiteId)
                return Result<Area>.Failure("Areas can only be moved within the same site.");
        }

        var siblings = await _repository.GetAreasBySiteIdAsync(area.SiteId, cancellationToken);

        // Cycle prevention: the new parent must not be a descendant of the area being moved.
        if (newParentAreaId.HasValue)
        {
            var descendants = GetDescendantAreaIds(areaId, siblings);
            if (descendants.Contains(newParentAreaId.Value))
                return Result<Area>.Failure(
                    $"Cannot move area '{area.Name}' under one of its own descendants.");
        }

        if (newParentAreaId == area.ParentAreaId)
            return Result<Area>.Success(area);

        var collision = siblings.FirstOrDefault(a =>
            a.Id != areaId &&
            a.ParentAreaId == newParentAreaId &&
            string.Equals(a.Name, area.Name, StringComparison.OrdinalIgnoreCase));
        if (collision != null)
            return Result<Area>.Failure(
                $"An area named '{area.Name}' already exists at the target level.");

        area.ParentAreaId = newParentAreaId;
        await _repository.UpdateAreaAsync(area, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        await _auditService.LogAsync(user, "Move", "Area", area.Id.ToString(), area.Name, area, cancellationToken);

        return Result<Area>.Success(area);
    }

    /// <summary>
    /// Deletes an area. Blocked if instances are assigned to this area or any descendant area.
    /// </summary>
    public async Task<Result<bool>> DeleteAreaAsync(
        int areaId, string user,
        CancellationToken cancellationToken = default)
    {
        var area = await _repository.GetAreaByIdAsync(areaId, cancellationToken);
        if (area == null)
            return Result<bool>.Failure($"Area with ID {areaId} not found.");

        // Check for instances assigned to this area
        var allInstances = await _repository.GetInstancesBySiteIdAsync(area.SiteId, cancellationToken);
        var allAreas = await _repository.GetAreasBySiteIdAsync(area.SiteId, cancellationToken);

        // Collect this area and all descendant area IDs
        var descendantIds = GetDescendantAreaIds(areaId, allAreas);
        descendantIds.Add(areaId);

        var assignedInstances = allInstances
            .Where(i => i.AreaId.HasValue && descendantIds.Contains(i.AreaId.Value))
            .ToList();

        if (assignedInstances.Count > 0)
        {
            var names = string.Join(", ", assignedInstances.Select(i => i.UniqueName).Take(5));
            return Result<bool>.Failure(
                $"Cannot delete area '{area.Name}': {assignedInstances.Count} instance(s) are assigned to it or its sub-areas ({names}{(assignedInstances.Count > 5 ? "..." : "")}).");
        }

        // Check for child areas (must delete children first, or we delete recursively)
        var childAreas = allAreas.Where(a => a.ParentAreaId == areaId).ToList();
        if (childAreas.Count > 0)
        {
            var childNames = string.Join(", ", childAreas.Select(a => a.Name));
            return Result<bool>.Failure(
                $"Cannot delete area '{area.Name}': it has child areas ({childNames}). Delete child areas first.");
        }

        await _repository.DeleteAreaAsync(areaId, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        await _auditService.LogAsync(user, "Delete", "Area", areaId.ToString(), area.Name, null, cancellationToken);

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

    /// <summary>
    /// Gets all areas for a site.
    /// </summary>
    public async Task<IReadOnlyList<Area>> GetAreasBySiteIdAsync(int siteId, CancellationToken cancellationToken = default) =>
        await _repository.GetAreasBySiteIdAsync(siteId, cancellationToken);

    /// <summary>
    /// Gets a single area by ID.
    /// </summary>
    public async Task<Area?> GetAreaByIdAsync(int areaId, CancellationToken cancellationToken = default) =>
        await _repository.GetAreaByIdAsync(areaId, cancellationToken);

    private static HashSet<int> GetDescendantAreaIds(int parentId, IReadOnlyList<Area> allAreas)
    {
        var result = new HashSet<int>();
        var queue = new Queue<int>();
        queue.Enqueue(parentId);

        while (queue.Count > 0)
        {
            var current = queue.Dequeue();
            foreach (var child in allAreas.Where(a => a.ParentAreaId == current))
            {
                if (result.Add(child.Id))
                    queue.Enqueue(child.Id);
            }
        }

        return result;
    }
}
