using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;


[assembly: InternalsVisibleTo("AIRuntime.Tests")]


namespace AIRuntime.Editor
{
    /// <summary>
    /// Immutable-enough projection used by the Kanban host to reconcile graph
    /// relationships without coupling the policy to EditorWindow state.
    /// </summary>
    internal sealed class AIRuntimeKanbanAssociationTaskSnapshot
    {
        public string Id { get; set; }

        public string ParentTaskId { get; set; }

        public string RootTaskId { get; set; }

        public string Title { get; set; }

        public string Description { get; set; }

        public string CreatedAt { get; set; }

        public IReadOnlyList<string> ProducedArtifactPaths { get; set; } =
            Array.Empty<string>();

        public IReadOnlyList<string> ConsumedArtifactPaths { get; set; } =
            Array.Empty<string>();

        public IReadOnlyList<string> MutatedResourceKeys { get; set; } =
            Array.Empty<string>();

        /// <summary>
        /// Dependencies explicitly supplied by the host. Reconciliation adds
        /// derived relationships but never removes these IDs.
        /// </summary>
        public IReadOnlyList<string> DependencyTaskIds { get; set; } =
            Array.Empty<string>();

        public bool IsFinalVerifier { get; set; }
    }


    internal sealed class AIRuntimeKanbanAssociationTaskResult
    {
        public string Id { get; set; }

        public IReadOnlyList<string> DependencyTaskIds { get; set; } =
            Array.Empty<string>();

        public IReadOnlyList<string> AssociatedTaskIds { get; set; } =
            Array.Empty<string>();
    }


    /// <summary>
    /// Rebuilds deterministic, structured Kanban relationships. This service
    /// deliberately does not use fuzzy title/description matching or a model.
    /// Exact artifact and resource declarations remain the source of truth.
    /// </summary>
    internal sealed class AIRuntimeKanbanAssociationReconciler
    {
        private static readonly StringComparer IdComparer =
            StringComparer.Ordinal;

        private static readonly StringComparer KeyComparer =
            StringComparer.OrdinalIgnoreCase;


        public IReadOnlyList<AIRuntimeKanbanAssociationTaskResult> Reconcile(
            IEnumerable<AIRuntimeKanbanAssociationTaskSnapshot> snapshots)
        {
            List<AIRuntimeKanbanAssociationTaskSnapshot> tasks =
                (snapshots ?? Enumerable.Empty<AIRuntimeKanbanAssociationTaskSnapshot>())
                    .Where(task => task != null)
                    .ToList();

            ValidateTaskIds(tasks);

            Dictionary<string, AIRuntimeKanbanAssociationTaskSnapshot> byId =
                tasks.ToDictionary(task => task.Id, IdComparer);

            List<AIRuntimeKanbanAssociationTaskSnapshot> orderedTasks =
                tasks
                    .OrderBy(GetCreatedAtSortValue)
                    .ThenBy(task => task.Id, IdComparer)
                    .ToList();

            Dictionary<string, HashSet<string>> dependencies =
                tasks.ToDictionary(
                    task => task.Id,
                    task => new HashSet<string>(
                        NormalizeIds(task.DependencyTaskIds, task.Id),
                        IdComparer),
                    IdComparer);

            Dictionary<string, HashSet<string>> associations =
                tasks.ToDictionary(
                    task => task.Id,
                    _ => new HashSet<string>(IdComparer),
                    IdComparer);

            AssociateParentsAndExplicitDependencies(
                orderedTasks,
                byId,
                dependencies,
                associations);

            // Establish verifier sinks first. Later derived artifact/resource
            // edges cannot point back out of a verifier when that would cycle.
            ReconcileFinalVerifiers(
                orderedTasks,
                dependencies,
                associations);

            ReconcileArtifacts(
                orderedTasks,
                byId,
                dependencies,
                associations);

            ReconcileMutableResources(
                orderedTasks,
                byId,
                dependencies,
                associations);

            return orderedTasks
                .Select(
                    task =>
                        new AIRuntimeKanbanAssociationTaskResult
                        {
                            Id = task.Id,
                            DependencyTaskIds = dependencies[task.Id]
                                .OrderBy(id => id, IdComparer)
                                .ToArray(),
                            AssociatedTaskIds = associations[task.Id]
                                .OrderBy(id => id, IdComparer)
                                .ToArray()
                        })
                .ToArray();
        }


        private static void ValidateTaskIds(
            IReadOnlyList<AIRuntimeKanbanAssociationTaskSnapshot> tasks)
        {
            var ids = new HashSet<string>(IdComparer);

            foreach (AIRuntimeKanbanAssociationTaskSnapshot task in tasks)
            {
                if (string.IsNullOrWhiteSpace(task.Id))
                {
                    throw new ArgumentException(
                        "Every Kanban association snapshot requires a task ID.",
                        nameof(tasks));
                }

                if (!string.Equals(task.Id, task.Id.Trim(), StringComparison.Ordinal))
                {
                    throw new ArgumentException(
                        $"Kanban task ID '{task.Id}' cannot contain leading or trailing whitespace.",
                        nameof(tasks));
                }

                if (!ids.Add(task.Id))
                {
                    throw new InvalidOperationException(
                        $"Duplicate Kanban task ID '{task.Id}' cannot be reconciled.");
                }
            }
        }


        private static IEnumerable<string> NormalizeIds(
            IEnumerable<string> ids,
            string taskId)
        {
            return (ids ?? Enumerable.Empty<string>())
                .Where(id => !string.IsNullOrWhiteSpace(id))
                .Select(id => id.Trim())
                .Where(id => !string.Equals(id, taskId, StringComparison.Ordinal))
                .Distinct(IdComparer);
        }


        private static void AssociateParentsAndExplicitDependencies(
            IEnumerable<AIRuntimeKanbanAssociationTaskSnapshot> tasks,
            IReadOnlyDictionary<string, AIRuntimeKanbanAssociationTaskSnapshot> byId,
            IReadOnlyDictionary<string, HashSet<string>> dependencies,
            IReadOnlyDictionary<string, HashSet<string>> associations)
        {
            foreach (AIRuntimeKanbanAssociationTaskSnapshot task in tasks)
            {
                if (!string.IsNullOrWhiteSpace(task.ParentTaskId) &&
                    byId.ContainsKey(task.ParentTaskId))
                {
                    Associate(task.Id, task.ParentTaskId, associations);
                }

                foreach (string dependencyId in dependencies[task.Id])
                {
                    if (byId.ContainsKey(dependencyId))
                    {
                        Associate(task.Id, dependencyId, associations);
                    }
                }
            }
        }


        private static void ReconcileFinalVerifiers(
            IEnumerable<AIRuntimeKanbanAssociationTaskSnapshot> tasks,
            IReadOnlyDictionary<string, HashSet<string>> dependencies,
            IReadOnlyDictionary<string, HashSet<string>> associations)
        {
            AIRuntimeKanbanAssociationTaskSnapshot[] all = tasks.ToArray();

            foreach (AIRuntimeKanbanAssociationTaskSnapshot verifier in
                     all.Where(task => task.IsFinalVerifier))
            {
                IEnumerable<AIRuntimeKanbanAssociationTaskSnapshot> siblings =
                    all.Where(
                        candidate =>
                            !string.Equals(candidate.Id, verifier.Id, StringComparison.Ordinal) &&
                            string.Equals(
                                candidate.RootTaskId ?? string.Empty,
                                verifier.RootTaskId ?? string.Empty,
                                StringComparison.Ordinal) &&
                            string.Equals(
                                candidate.ParentTaskId ?? string.Empty,
                                verifier.ParentTaskId ?? string.Empty,
                                StringComparison.Ordinal));

                foreach (AIRuntimeKanbanAssociationTaskSnapshot sibling in siblings)
                {
                    Associate(verifier.Id, sibling.Id, associations);
                    TryAddDerivedDependency(
                        verifier.Id,
                        sibling.Id,
                        dependencies);
                }
            }
        }


        private static void ReconcileArtifacts(
            IReadOnlyList<AIRuntimeKanbanAssociationTaskSnapshot> tasks,
            IReadOnlyDictionary<string, AIRuntimeKanbanAssociationTaskSnapshot> byId,
            IReadOnlyDictionary<string, HashSet<string>> dependencies,
            IReadOnlyDictionary<string, HashSet<string>> associations)
        {
            Dictionary<string, List<AIRuntimeKanbanAssociationTaskSnapshot>> producers =
                BuildKeyIndex(tasks, task => task.ProducedArtifactPaths, NormalizePath);

            Dictionary<string, List<AIRuntimeKanbanAssociationTaskSnapshot>> consumers =
                BuildKeyIndex(tasks, task => task.ConsumedArtifactPaths, NormalizePath);

            // Review and consumer cards which inspect the same exact artifact
            // remain associated even when its producer is not on the board.
            foreach (List<AIRuntimeKanbanAssociationTaskSnapshot> pathConsumers in
                     consumers.Values)
            {
                AssociateEveryPair(pathConsumers, associations);
            }

            foreach (KeyValuePair<string, List<AIRuntimeKanbanAssociationTaskSnapshot>> entry
                     in producers.OrderBy(pair => pair.Key, KeyComparer))
            {
                List<AIRuntimeKanbanAssociationTaskSnapshot> owners = entry.Value;

                // Duplicate exact ownership is itself a useful association and
                // is the deterministic signal used by the host's loop guard.
                AssociateEveryPair(owners, associations);

                if (!consumers.TryGetValue(entry.Key, out var pathConsumers))
                {
                    continue;
                }

                foreach (AIRuntimeKanbanAssociationTaskSnapshot consumer in pathConsumers)
                {
                    foreach (AIRuntimeKanbanAssociationTaskSnapshot owner in owners)
                    {
                        Associate(consumer.Id, owner.Id, associations);
                    }

                    AIRuntimeKanbanAssociationTaskSnapshot canonicalOwner =
                        owners.FirstOrDefault(
                            owner =>
                                !string.Equals(owner.Id, consumer.Id, StringComparison.Ordinal) &&
                                !AreAncestorRelated(owner.Id, consumer.Id, byId));

                    if (canonicalOwner != null)
                    {
                        TryAddDerivedDependency(
                            consumer.Id,
                            canonicalOwner.Id,
                            dependencies);
                    }
                }
            }
        }


        private static void ReconcileMutableResources(
            IReadOnlyList<AIRuntimeKanbanAssociationTaskSnapshot> tasks,
            IReadOnlyDictionary<string, AIRuntimeKanbanAssociationTaskSnapshot> byId,
            IReadOnlyDictionary<string, HashSet<string>> dependencies,
            IReadOnlyDictionary<string, HashSet<string>> associations)
        {
            Dictionary<string, List<AIRuntimeKanbanAssociationTaskSnapshot>> writers =
                BuildKeyIndex(tasks, task => task.MutatedResourceKeys, NormalizeResourceKey);

            foreach (KeyValuePair<string, List<AIRuntimeKanbanAssociationTaskSnapshot>> entry
                     in writers.OrderBy(pair => pair.Key, KeyComparer))
            {
                List<AIRuntimeKanbanAssociationTaskSnapshot> orderedWriters = entry.Value;

                AssociateEveryPair(orderedWriters, associations);

                for (int index = 1; index < orderedWriters.Count; index++)
                {
                    AIRuntimeKanbanAssociationTaskSnapshot writer =
                        orderedWriters[index];

                    AIRuntimeKanbanAssociationTaskSnapshot predecessor =
                        orderedWriters
                            .Take(index)
                            .Reverse()
                            .FirstOrDefault(
                                candidate =>
                                    !AreAncestorRelated(candidate.Id, writer.Id, byId));

                    if (predecessor != null)
                    {
                        TryAddDerivedDependency(
                            writer.Id,
                            predecessor.Id,
                            dependencies);
                    }
                }
            }
        }


        private static Dictionary<string, List<AIRuntimeKanbanAssociationTaskSnapshot>>
            BuildKeyIndex(
                IEnumerable<AIRuntimeKanbanAssociationTaskSnapshot> tasks,
                Func<AIRuntimeKanbanAssociationTaskSnapshot, IEnumerable<string>> selector,
                Func<string, string> normalize)
        {
            var index =
                new Dictionary<string, List<AIRuntimeKanbanAssociationTaskSnapshot>>(
                    KeyComparer);

            foreach (AIRuntimeKanbanAssociationTaskSnapshot task in tasks)
            {
                IEnumerable<string> keys =
                    (selector(task) ?? Enumerable.Empty<string>())
                        .Select(normalize)
                        .Where(key => !string.IsNullOrWhiteSpace(key))
                        .Distinct(KeyComparer);

                foreach (string key in keys)
                {
                    if (!index.TryGetValue(key, out var owners))
                    {
                        owners = new List<AIRuntimeKanbanAssociationTaskSnapshot>();
                        index.Add(key, owners);
                    }

                    owners.Add(task);
                }
            }

            return index;
        }


        private static string NormalizePath(string path)
        {
            return string.IsNullOrWhiteSpace(path)
                ? string.Empty
                : path.Trim().Replace('\\', '/');
        }


        private static string NormalizeResourceKey(string key)
        {
            return string.IsNullOrWhiteSpace(key)
                ? string.Empty
                : key.Trim().Replace('\\', '/');
        }


        private static void AssociateEveryPair(
            IReadOnlyList<AIRuntimeKanbanAssociationTaskSnapshot> tasks,
            IReadOnlyDictionary<string, HashSet<string>> associations)
        {
            for (int first = 0; first < tasks.Count; first++)
            {
                for (int second = first + 1; second < tasks.Count; second++)
                {
                    Associate(tasks[first].Id, tasks[second].Id, associations);
                }
            }
        }


        private static void Associate(
            string firstTaskId,
            string secondTaskId,
            IReadOnlyDictionary<string, HashSet<string>> associations)
        {
            if (string.IsNullOrWhiteSpace(firstTaskId) ||
                string.IsNullOrWhiteSpace(secondTaskId) ||
                string.Equals(firstTaskId, secondTaskId, StringComparison.Ordinal) ||
                !associations.ContainsKey(firstTaskId) ||
                !associations.ContainsKey(secondTaskId))
            {
                return;
            }

            associations[firstTaskId].Add(secondTaskId);
            associations[secondTaskId].Add(firstTaskId);
        }


        private static bool TryAddDerivedDependency(
            string taskId,
            string dependencyId,
            IReadOnlyDictionary<string, HashSet<string>> dependencies)
        {
            if (string.IsNullOrWhiteSpace(taskId) ||
                string.IsNullOrWhiteSpace(dependencyId) ||
                string.Equals(taskId, dependencyId, StringComparison.Ordinal) ||
                !dependencies.ContainsKey(taskId) ||
                !dependencies.ContainsKey(dependencyId) ||
                dependencies[taskId].Contains(dependencyId))
            {
                return false;
            }

            // Edges point from a task to its prerequisites. Adding task ->
            // dependency would cycle when dependency already reaches task.
            if (HasDependencyPath(
                    dependencyId,
                    taskId,
                    dependencies,
                    new HashSet<string>(IdComparer)))
            {
                return false;
            }

            dependencies[taskId].Add(dependencyId);
            return true;
        }


        private static bool HasDependencyPath(
            string currentId,
            string targetId,
            IReadOnlyDictionary<string, HashSet<string>> dependencies,
            ISet<string> visited)
        {
            if (string.Equals(currentId, targetId, StringComparison.Ordinal))
            {
                return true;
            }

            if (!visited.Add(currentId) ||
                !dependencies.TryGetValue(currentId, out var next))
            {
                return false;
            }

            return next.Any(
                dependencyId =>
                    HasDependencyPath(
                        dependencyId,
                        targetId,
                        dependencies,
                        visited));
        }


        private static bool AreAncestorRelated(
            string firstId,
            string secondId,
            IReadOnlyDictionary<string, AIRuntimeKanbanAssociationTaskSnapshot> byId)
        {
            return IsAncestor(firstId, secondId, byId) ||
                   IsAncestor(secondId, firstId, byId);
        }


        private static bool IsAncestor(
            string possibleAncestorId,
            string taskId,
            IReadOnlyDictionary<string, AIRuntimeKanbanAssociationTaskSnapshot> byId)
        {
            var visited = new HashSet<string>(IdComparer);
            string currentId = taskId;

            while (!string.IsNullOrWhiteSpace(currentId) &&
                   visited.Add(currentId) &&
                   byId.TryGetValue(currentId, out var current))
            {
                if (string.Equals(
                        current.ParentTaskId,
                        possibleAncestorId,
                        StringComparison.Ordinal))
                {
                    return true;
                }

                currentId = current.ParentTaskId;
            }

            return false;
        }


        private static DateTimeOffset GetCreatedAtSortValue(
            AIRuntimeKanbanAssociationTaskSnapshot task)
        {
            return DateTimeOffset.TryParse(
                    task?.CreatedAt,
                    CultureInfo.InvariantCulture,
                    DateTimeStyles.AssumeUniversal |
                    DateTimeStyles.AdjustToUniversal,
                    out DateTimeOffset createdAt)
                ? createdAt
                : DateTimeOffset.MaxValue;
        }
    }
}
