using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using RackPeek.Domain.Resources;
using RackPeek.Domain.Resources.AccessPoints;
using RackPeek.Domain.Resources.Connections;
using RackPeek.Domain.Resources.Desktops;
using RackPeek.Domain.Resources.Firewalls;
using RackPeek.Domain.Resources.Hardware;
using RackPeek.Domain.Resources.Laptops;
using RackPeek.Domain.Resources.Routers;
using RackPeek.Domain.Resources.Servers;
using RackPeek.Domain.Resources.Services;
using RackPeek.Domain.Resources.SystemResources;
using RackPeek.Domain.Resources.UpsUnits;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using Switch = RackPeek.Domain.Resources.Switches.Switch;

namespace RackPeek.Domain.Persistence.Yaml;

public class ResourceCollection {
    public readonly SemaphoreSlim FileLock = new(1, 1);
    public List<Resource> Resources { get; } = new();
    public List<Connection> Connections { get; } = new();
}

public sealed class YamlResourceCollection(
    string filePath,
    ITextFileStore fileStore,
    ResourceCollection resourceCollection,
    IResourceYamlMigrationService migrationService)
    : IResourceCollection {
    // Bump this when your YAML schema changes, and add a migration step below.
    private static readonly int _currentSchemaVersion = RackPeekConfigMigrationDeserializer.ListOfMigrations.Count;

    public Task<bool> Exists(string name) {
        return Task.FromResult(resourceCollection.Resources.Exists(r =>
            r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)));
    }

    public Task<string?> GetKind(string? name) {
        return Task.FromResult(resourceCollection.Resources.FirstOrDefault(r =>
            r.Name.Equals(name, StringComparison.OrdinalIgnoreCase))?.Kind);
    }

    public Task<IReadOnlyList<(Resource, string)>> GetByLabelAsync(string name) {
        ReadOnlyCollection<(Resource r, string)> result = resourceCollection.Resources
            .Where(r => r.Labels != null && r.Labels.TryGetValue(name, out _))
            .Select(r => (r, r.Labels![name]))
            .ToList()
            .AsReadOnly();

        return Task.FromResult<IReadOnlyList<(Resource, string)>>(result);
    }

    public Task<Dictionary<string, int>> GetLabelsAsync() {
        var result = resourceCollection.Resources
            .SelectMany(r => r.Labels ?? Enumerable.Empty<KeyValuePair<string, string>>())
            .Where(kvp => !string.IsNullOrWhiteSpace(kvp.Key))
            .GroupBy(kvp => kvp.Key)
            .ToDictionary(g => g.Key, g => g.Count());

        return Task.FromResult(result);
    }

    public Task<IReadOnlyList<(Resource, string)>> GetResourceIpsAsync() {
        var result = new List<(Resource, string)>();

        List<Resource> allResources = resourceCollection.Resources;

        // Build fast lookup for systems
        var systemsByName = allResources
            .OfType<SystemResource>()
            .ToDictionary(s => s.Name, StringComparer.OrdinalIgnoreCase);

        // Cache resolved system IPs (prevents repeated recursion)
        var resolvedSystemIps = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);

        foreach (Resource resource in allResources)
            switch (resource) {
                case SystemResource system: {
                        var ip = ResolveSystemIp(system, systemsByName, resolvedSystemIps);
                        if (!string.IsNullOrWhiteSpace(ip))
                            result.Add((system, ip));
                        break;
                    }

                case Service service: {
                        var ip = ResolveServiceIp(service, systemsByName, resolvedSystemIps);
                        if (!string.IsNullOrWhiteSpace(ip))
                            result.Add((service, ip));
                        break;
                    }
            }

        return Task.FromResult((IReadOnlyList<(Resource, string)>)result);
    }

    public Task<Dictionary<string, int>> GetTagsAsync() {
        var result = resourceCollection.Resources
            .SelectMany(r => r.Tags) // flatten all tag arrays
            .Where(t => !string.IsNullOrWhiteSpace(t))
            .GroupBy(t => t)
            .ToDictionary(g => g.Key, g => g.Count());

        return Task.FromResult(result);
    }

    public Task<IReadOnlyList<T>> GetAllOfTypeAsync<T>() =>
        Task.FromResult<IReadOnlyList<T>>(resourceCollection.Resources.OfType<T>().ToList());

    public Task<IReadOnlyList<Resource>> GetDependantsAsync(string name) {
        var result = resourceCollection.Resources
            .Where(r => r.RunsOn.Any(p => p.Equals(name, StringComparison.OrdinalIgnoreCase)))
            .ToList();

        return Task.FromResult<IReadOnlyList<Resource>>(result);
    }

    public async Task Merge(string incomingYaml, MergeMode mode) {
        if (string.IsNullOrWhiteSpace(incomingYaml))
            return;

        await resourceCollection.FileLock.WaitAsync();
        try {
            YamlRoot incomingRoot = await migrationService.DeserializeAsync(incomingYaml);

            List<Resource> incomingResources = incomingRoot.Resources ?? new List<Resource>();
            List<Resource> merged = ResourceCollectionMerger.Merge(
                resourceCollection.Resources,
                incomingResources,
                mode);

            resourceCollection.Resources.Clear();
            resourceCollection.Resources.AddRange(merged);

            var rootToSave = new YamlRoot {
                Version = RackPeekConfigMigrationDeserializer.ListOfMigrations.Count,
                Resources = resourceCollection.Resources,
                Connections = resourceCollection.Connections
            };

            await SaveRootAsync(rootToSave);
        }
        finally {
            resourceCollection.FileLock.Release();
        }
    }

    public Task<IReadOnlyList<Resource>> GetByTagAsync(string name) {
        return Task.FromResult<IReadOnlyList<Resource>>(
            resourceCollection.Resources
                .Where(r => r.Tags.Contains(name))
                .ToList()
        );
    }

    public IReadOnlyList<Hardware> HardwareResources =>
        resourceCollection.Resources.OfType<Hardware>().ToList();

    public IReadOnlyList<SystemResource> SystemResources =>
        resourceCollection.Resources.OfType<SystemResource>().ToList();

    public IReadOnlyList<Service> ServiceResources =>
        resourceCollection.Resources.OfType<Service>().ToList();

    public Task<Resource?> GetByNameAsync(string name) {
        return Task.FromResult(resourceCollection.Resources.FirstOrDefault(r =>
            r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)));
    }

    public Task<T?> GetByNameAsync<T>(string name) where T : Resource {
        Resource? resource =
            resourceCollection.Resources.FirstOrDefault(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
        return Task.FromResult(resource as T);
    }

    public Resource? GetByName(string name) {
        return resourceCollection.Resources.FirstOrDefault(r =>
            r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
    }

    public async Task LoadAsync() {
        // Routes.razor calls LoadAsync on every Blazor circuit init, so
        // multiple tabs / fresh page loads can run this concurrently. Without
        // the lock, two callers can interleave Resources.Clear() and
        // AddRange() and corrupt the List<T>'s internal _size, producing
        // "Index was outside the bounds of the array" out of List.Clear.
        await resourceCollection.FileLock.WaitAsync();
        try {
            var yaml = await fileStore.ReadAllTextAsync(filePath);

            YamlRoot root = await migrationService.DeserializeAsync(
                yaml,
                async originalYaml => await BackupOriginalAsync(originalYaml),
                async migratedRoot => await SaveRootAsync(migratedRoot)
            );

            resourceCollection.Resources.Clear();

            if (root.Resources != null)
                resourceCollection.Resources.AddRange(root.Resources);

            resourceCollection.Connections.Clear();

            if (root.Connections != null)
                resourceCollection.Connections.AddRange(root.Connections);
        }
        finally {
            resourceCollection.FileLock.Release();
        }
    }

    public Task AddAsync(Resource resource) {
        return UpdateWithLockAsync(list => {
            if (list.Any(r => r.Name.Equals(resource.Name, StringComparison.OrdinalIgnoreCase)))
                throw new InvalidOperationException($"'{resource.Name}' already exists.");

            resource.Kind = GetKind(resource);
            list.Add(resource);
        });
    }

    public Task UpdateAsync(Resource resource) {
        return UpdateWithLockAsync(list => {
            var index = list.FindIndex(r => r.Name.Equals(resource.Name, StringComparison.OrdinalIgnoreCase));
            if (index == -1) throw new InvalidOperationException("Not found.");

            resource.Kind = GetKind(resource);
            list[index] = resource;
        });
    }

    public Task DeleteAsync(string name) {
        return UpdateWithLockAsync(list =>
            list.RemoveAll(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)));
    }

    public Task AddConnectionAsync(Connection connection) => UpdateConnectionsWithLockAsync(list => { list.Add(connection); });

    public Task RemoveConnectionAsync(Connection connection) {
        return UpdateConnectionsWithLockAsync(list => {
            list.RemoveAll(c =>
                (PortsMatch(c.A, connection.A) && PortsMatch(c.B, connection.B)) ||
                (PortsMatch(c.A, connection.B) && PortsMatch(c.B, connection.A)));
        });
    }

    public Task RemoveConnectionsForPortAsync(PortReference port) {
        return UpdateConnectionsWithLockAsync(list => {
            list.RemoveAll(c =>
                PortsMatch(c.A, port) ||
                PortsMatch(c.B, port));
        });
    }

    public Task<IReadOnlyList<Connection>> GetConnectionsAsync() {
        IReadOnlyList<Connection> result =
            resourceCollection.Connections
                .ToList()
                .AsReadOnly();

        return Task.FromResult(result);
    }

    public Task<IReadOnlyList<Connection>> GetConnectionsForResourceAsync(string resource) {
        IReadOnlyList<Connection> result =
            resourceCollection.Connections
                .Where(c =>
                    c.A.Resource.Equals(resource, StringComparison.OrdinalIgnoreCase) ||
                    c.B.Resource.Equals(resource, StringComparison.OrdinalIgnoreCase))
                .ToList()
                .AsReadOnly();

        return Task.FromResult(result);
    }

    public Task<Connection?> GetConnectionForPortAsync(PortReference port) {
        Connection? connection =
            resourceCollection.Connections
                .FirstOrDefault(c =>
                    PortsMatch(c.A, port) ||
                    PortsMatch(c.B, port));

        return Task.FromResult(connection);
    }

    private string? ResolveSystemIp(
        SystemResource system,
        Dictionary<string, SystemResource> systemsByName,
        Dictionary<string, string?> cache) {
        // Return cached result if already resolved
        if (cache.TryGetValue(system.Name, out var cached))
            return cached;

        // Direct IP wins
        if (!string.IsNullOrWhiteSpace(system.Ip)) {
            cache[system.Name] = system.Ip;
            return system.Ip;
        }

        // Must have exactly one parent
        if (system.RunsOn?.Count != 1) {
            cache[system.Name] = null;
            return null;
        }

        var parentName = system.RunsOn.First();

        if (!systemsByName.TryGetValue(parentName, out SystemResource? parent)) {
            cache[system.Name] = null;
            return null;
        }

        var resolved = ResolveSystemIp(parent, systemsByName, cache);
        cache[system.Name] = resolved;

        return resolved;
    }

    private string? ResolveServiceIp(
        Service service,
        Dictionary<string, SystemResource> systemsByName,
        Dictionary<string, string?> cache) {
        // Direct IP wins
        if (!string.IsNullOrWhiteSpace(service.Network?.Ip))
            return service.Network!.Ip;

        // Must have exactly one parent
        if (service.RunsOn?.Count != 1)
            return null;

        var parentName = service.RunsOn.First();

        if (!systemsByName.TryGetValue(parentName, out SystemResource? parent))
            return null;

        return ResolveSystemIp(parent, systemsByName, cache);
    }

    private async Task UpdateWithLockAsync(Action<List<Resource>> action) {
        await resourceCollection.FileLock.WaitAsync();
        try {
            action(resourceCollection.Resources);

            // Always write current schema version when app writes the file.
            var root = new YamlRoot {
                Version = _currentSchemaVersion,
                Resources = resourceCollection.Resources,
                Connections = resourceCollection.Connections
            };

            await SaveRootAsync(root);
        }
        finally {
            resourceCollection.FileLock.Release();
        }
    }

    // ----------------------------
    // Versioning + migration
    // ----------------------------

    private async Task BackupOriginalAsync(string originalYaml) {
        // Timestamped backup for safe rollback
        var backupPath = $"{filePath}.bak.{DateTime.UtcNow:yyyyMMddHHmmss}";
        await fileStore.WriteAllTextAsync(backupPath, originalYaml);
    }

    private async Task SaveRootAsync(YamlRoot? root) {
        var contents = SerializeRootAsync(root);
        await fileStore.WriteAllTextAsync(filePath, contents);
    }

    public static string SerializeRootAsync(YamlRoot? root) {
        ISerializer serializer = new SerializerBuilder()
            .WithNamingConvention(CamelCaseNamingConvention.Instance)
            .WithTypeConverter(new StorageSizeYamlConverter())
            .WithTypeConverter(new NotesStringYamlConverter())
            .ConfigureDefaultValuesHandling(
                DefaultValuesHandling.OmitNull |
                DefaultValuesHandling.OmitEmptyCollections
            )
            .Build();

        // Preserve ordering: version first, then resources
        Debug.Assert(root != null, nameof(root) + " != null");

        var payload = new OrderedDictionary {
            ["version"] = root.Version,
            ["resources"] = (root.Resources ?? new List<Resource>()).Select(SerializeResource).ToList(),
            ["connections"] = root.Connections ?? new List<Connection>()
        };

        return serializer.Serialize(payload);
    }


    private static string GetKind(Resource resource) {
        return resource switch {
            Server => "Server",
            Switch => "Switch",
            Firewall => "Firewall",
            Router => "Router",
            Desktop => "Desktop",
            Laptop => "Laptop",
            AccessPoint => "AccessPoint",
            Ups => "Ups",
            SystemResource => "System",
            Service => "Service",
            _ => throw new InvalidOperationException($"Unknown resource type: {resource.GetType().Name}")
        };
    }

    public static OrderedDictionary SerializeResource(Resource resource) {
        var map = new OrderedDictionary {
            ["kind"] = GetKind(resource)
        };

        ISerializer serializer = new SerializerBuilder()
            .WithNamingConvention(CamelCaseNamingConvention.Instance)
            .WithTypeConverter(new NotesStringYamlConverter())
            .ConfigureDefaultValuesHandling(
                DefaultValuesHandling.OmitNull |
                DefaultValuesHandling.OmitEmptyCollections
            )
            .Build();

        var yaml = serializer.Serialize(resource);

        Dictionary<string, object?> props = new DeserializerBuilder()
            .Build()
            .Deserialize<Dictionary<string, object?>>(yaml);

        foreach ((var key, var value) in props)
            if (!string.Equals(key, "kind", StringComparison.OrdinalIgnoreCase))
                map[key] = value;

        return map;
    }

    private static bool PortsMatch(PortReference a, PortReference b) {
        return a.Resource.Equals(b.Resource, StringComparison.OrdinalIgnoreCase)
               && a.PortGroup == b.PortGroup
               && a.PortIndex == b.PortIndex;
    }

    private async Task UpdateConnectionsWithLockAsync(Action<List<Connection>> action) {
        await resourceCollection.FileLock.WaitAsync();
        try {
            action(resourceCollection.Connections);

            var root = new YamlRoot {
                Version = _currentSchemaVersion,
                Resources = resourceCollection.Resources,
                Connections = resourceCollection.Connections
            };

            await SaveRootAsync(root);
        }
        finally {
            resourceCollection.FileLock.Release();
        }
    }
}

public class YamlRoot {
    public int Version { get; set; }
    public List<Resource>? Resources { get; set; }

    public List<Connection>? Connections { get; set; }
}
