using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

using AIRuntime.Runtime.Services.Agent;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;


namespace AIRuntime.Editor
{
    /// <summary>
    /// Safe, user-facing categories for observable agent work. These values
    /// describe lifecycle activity; they never represent private reasoning.
    /// </summary>
    public enum AIRuntimeAgentActivityKind
    {
        Status,
        RunStarted,
        ToolStarted,
        ToolCompleted,
        ToolFailed,
        Retry,
        Review,
        RunCompleted,
        RunCancelled,
        RunTimedOut,
        RunFailed,
        Error
    }


    /// <summary>
    /// Context supplied by an Editor integration when it attaches runtime
    /// plugins to one run. The correlation ID normally contains a task ID.
    /// </summary>
    public sealed class AIRuntimeEditorRuntimePluginContext
    {
        private readonly object _gate =
            new object();

        private string _correlationId;
        private string _agent;
        private string _objective;
        private string _stage;


        public AIRuntimeEditorRuntimePluginContext(
            string source,
            string agent,
            string objective,
            string stage = null,
            string correlationId = null)
        {
            Source =
                NormalizeRequired(
                    source,
                    nameof(source));

            _agent =
                NormalizeOptional(
                    agent,
                    "Agent");

            _objective =
                NormalizeOptional(
                    objective,
                    "No objective supplied");

            _correlationId =
                string.IsNullOrWhiteSpace(
                    correlationId)
                    ? Guid.NewGuid().ToString("N")
                    : correlationId.Trim();

            _stage =
                NormalizeOptional(
                    stage,
                    "Preparing");
        }


        public string CorrelationId
        {
            get
            {
                lock (_gate)
                {
                    return _correlationId;
                }
            }
        }


        public string Source
        {
            get;
        }


        public string Agent
        {
            get
            {
                lock (_gate)
                {
                    return _agent;
                }
            }
        }


        public string Objective
        {
            get
            {
                lock (_gate)
                {
                    return _objective;
                }
            }
        }


        public string Stage
        {
            get
            {
                lock (_gate)
                {
                    return _stage;
                }
            }
        }


        /// <summary>
        /// Raised for explicit application-level activity such as a stage
        /// transition, retry, review hand-off, or orchestration error.
        /// Subscribers must treat the report as diagnostic information only.
        /// </summary>
        public event Action<
            AIRuntimeEditorRuntimePluginContext,
            AIRuntimeEditorRuntimeActivityReport>
            ActivityReported;


        /// <summary>
        /// Refreshes the metadata for a runtime that is reused across prompts.
        /// A null correlation ID preserves the existing task/conversation
        /// grouping; a non-empty value starts a new activity grouping.
        /// </summary>
        public void UpdateRun(
            string agent,
            string objective,
            string stage,
            string correlationId = null,
            string summary = null)
        {
            string normalizedAgent =
                NormalizeOptional(
                    agent,
                    "Agent");

            string normalizedObjective =
                NormalizeOptional(
                    objective,
                    "No objective supplied");

            string normalizedStage =
                NormalizeOptional(
                    stage,
                    "Preparing");

            lock (_gate)
            {
                _agent =
                    normalizedAgent;
                _objective =
                    normalizedObjective;
                _stage =
                    normalizedStage;

                if (!string.IsNullOrWhiteSpace(
                        correlationId))
                {
                    _correlationId =
                        correlationId.Trim();
                }
            }

            Report(
                AIRuntimeAgentActivityKind.Status,
                string.IsNullOrWhiteSpace(
                    summary)
                    ? $"Preparing {normalizedStage}."
                    : summary,
                string.Empty);
        }


        public void UpdateAgent(
            string agent,
            string summary = null)
        {
            string normalized =
                NormalizeOptional(
                    agent,
                    "Agent");

            lock (_gate)
            {
                _agent =
                    normalized;
            }

            Report(
                AIRuntimeAgentActivityKind.Status,
                string.IsNullOrWhiteSpace(
                    summary)
                    ? $"Agent: {normalized}"
                    : summary,
                string.Empty);
        }


        public void UpdateObjective(
            string objective,
            string summary = null)
        {
            string normalized =
                NormalizeOptional(
                    objective,
                    "No objective supplied");

            lock (_gate)
            {
                _objective =
                    normalized;
            }

            Report(
                AIRuntimeAgentActivityKind.Status,
                string.IsNullOrWhiteSpace(
                    summary)
                    ? "Objective updated."
                    : summary,
                string.Empty);
        }


        public void UpdateStage(
            string stage,
            string summary = null)
        {
            string normalized =
                NormalizeOptional(
                    stage,
                    "Working");

            lock (_gate)
            {
                _stage =
                    normalized;
            }

            Report(
                AIRuntimeAgentActivityKind.Status,
                string.IsNullOrWhiteSpace(
                    summary)
                    ? $"Stage: {normalized}"
                    : summary,
                string.Empty);
        }


        public void Report(
            AIRuntimeAgentActivityKind kind,
            string summary,
            string detail = null)
        {
            AIRuntimeEditorRuntimeActivityReport report =
                new AIRuntimeEditorRuntimeActivityReport(
                    kind,
                    summary,
                    detail,
                    DateTime.UtcNow);

            Delegate[] subscribers =
                ActivityReported
                    ?.GetInvocationList();

            if (subscribers ==
                null)
            {
                return;
            }

            foreach (
                Action<
                    AIRuntimeEditorRuntimePluginContext,
                    AIRuntimeEditorRuntimeActivityReport> subscriber
                in subscribers)
            {
                try
                {
                    subscriber(
                        this,
                        report);
                }
                catch
                {
                    // A diagnostics plugin must never break task execution.
                }
            }
        }


        internal AIRuntimeEditorRuntimePluginContextSnapshot Capture()
        {
            lock (_gate)
            {
                return new AIRuntimeEditorRuntimePluginContextSnapshot
                {
                    CorrelationId = _correlationId,
                    Source = Source,
                    Agent = _agent,
                    Objective = _objective,
                    Stage = _stage
                };
            }
        }


        private static string NormalizeRequired(
            string value,
            string parameterName)
        {
            if (string.IsNullOrWhiteSpace(
                    value))
            {
                throw new ArgumentException(
                    "A plugin context source is required.",
                    parameterName);
            }

            return value.Trim();
        }


        private static string NormalizeOptional(
            string value,
            string fallback)
        {
            return string.IsNullOrWhiteSpace(
                    value)
                ? fallback
                : value.Trim();
        }
    }


    internal sealed class AIRuntimeEditorRuntimePluginContextSnapshot
    {
        public string CorrelationId;
        public string Source;
        public string Agent;
        public string Objective;
        public string Stage;
    }


    public sealed class AIRuntimeEditorRuntimeActivityReport
    {
        public AIRuntimeEditorRuntimeActivityReport(
            AIRuntimeAgentActivityKind kind,
            string summary,
            string detail,
            DateTime timestampUtc)
        {
            Kind = kind;
            Summary = summary ?? string.Empty;
            Detail = detail ?? string.Empty;
            TimestampUtc = timestampUtc.Kind == DateTimeKind.Utc
                ? timestampUtc
                : timestampUtc.ToUniversalTime();
        }


        public AIRuntimeAgentActivityKind Kind
        {
            get;
        }


        public string Summary
        {
            get;
        }


        public string Detail
        {
            get;
        }


        public DateTime TimestampUtc
        {
            get;
        }
    }


    /// <summary>
    /// Immutable snapshot returned to activity UIs and diagnostics consumers.
    /// All text has already been redacted and bounded by the store.
    /// </summary>
    public sealed class AIRuntimeAgentActivityEntry
    {
        public long Sequence
        {
            get;
            internal set;
        }

        public DateTime TimestampUtc
        {
            get;
            internal set;
        }

        public string CorrelationId
        {
            get;
            internal set;
        }

        public string SessionId
        {
            get;
            internal set;
        }

        public string Source
        {
            get;
            internal set;
        }

        public string Agent
        {
            get;
            internal set;
        }

        public string Objective
        {
            get;
            internal set;
        }

        public string Stage
        {
            get;
            internal set;
        }

        public AIRuntimeAgentActivityKind Kind
        {
            get;
            internal set;
        }

        public string Summary
        {
            get;
            internal set;
        }

        public string Detail
        {
            get;
            internal set;
        }

        public string ToolName
        {
            get;
            internal set;
        }

        public int RepeatCount
        {
            get;
            internal set;
        }


        internal AIRuntimeAgentActivityEntry Clone()
        {
            return new AIRuntimeAgentActivityEntry
            {
                Sequence = Sequence,
                TimestampUtc = TimestampUtc,
                CorrelationId = CorrelationId,
                SessionId = SessionId,
                Source = Source,
                Agent = Agent,
                Objective = Objective,
                Stage = Stage,
                Kind = Kind,
                Summary = Summary,
                Detail = Detail,
                ToolName = ToolName,
                RepeatCount = RepeatCount
            };
        }
    }


    public interface IAIRuntimeAgentActivitySink
    {
        void Publish(
            AIRuntimeEditorRuntimePluginContext context,
            AIRuntimeAgentActivityKind kind,
            string summary,
            string detail = null,
            string sessionId = null,
            string toolName = null,
            DateTime? timestampUtc = null);
    }


    /// <summary>
    /// Process-local, thread-safe and bounded activity history. It stores only
    /// lifecycle summaries and sanitized tool details, never provider payloads
    /// or model reasoning fields.
    /// </summary>
    public sealed class AIRuntimeAgentActivityStore :
        IAIRuntimeAgentActivitySink
    {
        public const int DefaultMaximumEntries =
            512;

        public const int MaximumSummaryCharacters =
            320;

        public const int MaximumDetailCharacters =
            1800;

        public const int MaximumObjectiveCharacters =
            900;

        private const int MaximumMetadataCharacters =
            180;

        private readonly object _gate =
            new object();

        private readonly List<AIRuntimeAgentActivityEntry> _entries =
            new List<AIRuntimeAgentActivityEntry>();

        private long _nextSequence;


        public AIRuntimeAgentActivityStore(
            int maximumEntries = DefaultMaximumEntries)
        {
            if (maximumEntries <= 0)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(maximumEntries),
                    "The activity store capacity must be greater than zero.");
            }

            MaximumEntries =
                maximumEntries;
        }


        public static AIRuntimeAgentActivityStore Shared
        {
            get;
        } = new AIRuntimeAgentActivityStore();


        public int MaximumEntries
        {
            get;
        }


        /// <summary>
        /// May be raised from a provider or capability continuation thread.
        /// UI subscribers should only queue a repaint from this callback.
        /// </summary>
        public event Action Changed;


        public void Publish(
            AIRuntimeEditorRuntimePluginContext context,
            AIRuntimeAgentActivityKind kind,
            string summary,
            string detail = null,
            string sessionId = null,
            string toolName = null,
            DateTime? timestampUtc = null)
        {
            if (context == null)
            {
                throw new ArgumentNullException(
                    nameof(context));
            }

            DateTime timestamp =
                timestampUtc ??
                DateTime.UtcNow;

            if (timestamp.Kind !=
                DateTimeKind.Utc)
            {
                timestamp =
                    timestamp.ToUniversalTime();
            }

            AIRuntimeEditorRuntimePluginContextSnapshot contextSnapshot =
                context.Capture();

            AIRuntimeAgentActivityEntry entry =
                new AIRuntimeAgentActivityEntry
                {
                    TimestampUtc = timestamp,
                    CorrelationId =
                        SanitizeMetadata(
                            contextSnapshot.CorrelationId),
                    SessionId =
                        SanitizeMetadata(
                            sessionId),
                    Source =
                        SanitizeMetadata(
                            contextSnapshot.Source),
                    Agent =
                        SanitizeMetadata(
                            contextSnapshot.Agent),
                    Objective =
                        RedactAndBound(
                            contextSnapshot.Objective,
                            MaximumObjectiveCharacters),
                    Stage =
                        SanitizeMetadata(
                            contextSnapshot.Stage),
                    Kind = kind,
                    Summary =
                        IsFailureKind(kind)
                            ? SummarizeError(
                                summary,
                                MaximumSummaryCharacters)
                            : RedactAndBound(
                                summary,
                                MaximumSummaryCharacters),
                    Detail =
                        IsFailureKind(kind)
                            ? SummarizeError(
                                detail,
                                MaximumDetailCharacters)
                            : RedactAndBound(
                                detail,
                                MaximumDetailCharacters),
                    ToolName =
                        SanitizeMetadata(
                            toolName),
                    RepeatCount = 1
                };

            lock (_gate)
            {
                AIRuntimeAgentActivityEntry previous =
                    _entries.Count > 0
                        ? _entries[_entries.Count - 1]
                        : null;

                if (CanCollapse(
                        previous,
                        entry))
                {
                    previous.TimestampUtc =
                        entry.TimestampUtc;
                    previous.RepeatCount =
                        Math.Max(
                            1,
                            previous.RepeatCount) +
                        1;
                }
                else
                {
                    entry.Sequence =
                        ++_nextSequence;

                    _entries.Add(
                        entry);
                }

                if (_entries.Count >
                    MaximumEntries)
                {
                    _entries.RemoveRange(
                        0,
                        _entries.Count -
                        MaximumEntries);
                }
            }

            NotifyChanged();
        }


        public IReadOnlyList<AIRuntimeAgentActivityEntry> Snapshot()
        {
            lock (_gate)
            {
                return _entries
                    .Select(
                        entry =>
                            entry.Clone())
                    .ToArray();
            }
        }


        public void Clear()
        {
            bool changed;

            lock (_gate)
            {
                changed =
                    _entries.Count >
                    0;

                _entries.Clear();
            }

            if (changed)
            {
                NotifyChanged();
            }
        }


        internal static string SummarizeToolArguments(
            string arguments)
        {
            return AIRuntimeAgentActivitySanitizer
                .SummarizeToolPayload(
                    arguments,
                    arguments: true);
        }


        internal static string SummarizeToolResult(
            string result)
        {
            return AIRuntimeAgentActivitySanitizer
                .SummarizeToolPayload(
                    result,
                    arguments: false);
        }


        internal static string SummarizeError(
            string error,
            int maximumCharacters = MaximumSummaryCharacters)
        {
            string normalized =
                (error ?? string.Empty)
                    .Replace("\r\n", "\n")
                    .Replace('\r', '\n')
                    .Trim();

            if (string.IsNullOrEmpty(normalized))
            {
                return "No error details were provided.";
            }

            int lineBreak =
                normalized.IndexOf('\n');

            string firstLine =
                lineBreak >= 0
                    ? normalized.Substring(0, lineBreak).Trim()
                    : normalized;

            // This is a known harness diagnostic tag, not a JSON array or
            // provider payload. Preserve its meaning through error redaction.
            firstLine = Regex.Replace(firstLine, @"^\[ArtifactOwnership\]\s*", "Task ownership: ");
            firstLine = Regex.Replace(firstLine, @"\[(RateLimited|Authentication|Unavailable|ModelUnavailable)\]", "($1)");

            Match payloadMarker =
                Regex.Match(
                    firstLine,
                    @"(?i)(?:\b(?:response\s+body|response|payload|reasoning|content|raw)\b\s*[:=]|[\{\[])",
                    RegexOptions.CultureInvariant);

            bool omittedPayload =
                lineBreak >= 0 ||
                payloadMarker.Success;

            if (payloadMarker.Success)
            {
                firstLine =
                    firstLine.Substring(
                            0,
                            payloadMarker.Index)
                        .TrimEnd();
            }

            firstLine =
                AIRuntimeAgentActivitySanitizer.Redact(
                    firstLine);

            if (string.IsNullOrWhiteSpace(firstLine))
            {
                firstLine =
                    "Error details";
            }

            string safe =
                omittedPayload
                    ? firstLine + " [payload details omitted]"
                    : firstLine;

            if (safe.Length <= maximumCharacters)
            {
                return safe;
            }

            return safe.Substring(
                       0,
                       maximumCharacters) +
                   "…";
        }


        private static bool CanCollapse(
            AIRuntimeAgentActivityEntry previous,
            AIRuntimeAgentActivityEntry current)
        {
            return previous !=
                       null &&
                   previous.Kind ==
                       current.Kind &&
                   string.Equals(
                       previous.CorrelationId,
                       current.CorrelationId,
                       StringComparison.Ordinal) &&
                   string.Equals(
                       previous.SessionId,
                       current.SessionId,
                       StringComparison.Ordinal) &&
                   string.Equals(
                       previous.Stage,
                       current.Stage,
                       StringComparison.Ordinal) &&
                   string.Equals(
                       previous.Summary,
                       current.Summary,
                       StringComparison.Ordinal) &&
                   string.Equals(
                       previous.Detail,
                       current.Detail,
                       StringComparison.Ordinal) &&
                   string.Equals(
                       previous.ToolName,
                       current.ToolName,
                       StringComparison.Ordinal);
        }


        private static bool IsFailureKind(
            AIRuntimeAgentActivityKind kind)
        {
            return kind == AIRuntimeAgentActivityKind.ToolFailed ||
                   kind == AIRuntimeAgentActivityKind.RunTimedOut ||
                   kind == AIRuntimeAgentActivityKind.RunFailed ||
                   kind == AIRuntimeAgentActivityKind.Error;
        }


        private static string SanitizeMetadata(
            string value)
        {
            return RedactAndBound(
                value,
                MaximumMetadataCharacters);
        }


        private static string RedactAndBound(
            string value,
            int maximumCharacters)
        {
            string redacted =
                AIRuntimeAgentActivitySanitizer.Redact(
                    value ??
                    string.Empty);

            if (redacted.Length <=
                maximumCharacters)
            {
                return redacted;
            }

            return redacted.Substring(
                       0,
                       maximumCharacters) +
                   $"\n… [bounded; original characters={redacted.Length}]";
        }


        private void NotifyChanged()
        {
            Delegate[] subscribers =
                Changed
                    ?.GetInvocationList();

            if (subscribers ==
                null)
            {
                return;
            }

            foreach (
                Action subscriber
                in subscribers)
            {
                try
                {
                    subscriber();
                }
                catch
                {
                    // Activity consumers are plugins and must be isolated.
                }
            }
        }
    }


    internal static class AIRuntimeAgentActivitySanitizer
    {
        // Activity reporting runs synchronously on the agent execution thread.
        // Avoid constructing a JSON DOM for large tool payloads (for example a
        // write_file call containing an entire source file); the activity UI
        // only needs a bounded structural summary.
        private const int MaximumParsedPayloadCharacters =
            32768;

        private const int MaximumFields =
            12;

        private const int MaximumScalarCharacters =
            220;

        private static readonly Regex BearerPattern =
            new Regex(
                @"(?i)\bBearer\s+[A-Za-z0-9._~+/=-]+",
                RegexOptions.Compiled |
                RegexOptions.CultureInvariant);

        private static readonly Regex AssignmentPattern =
            new Regex(
                "(?i)(\\b(?:api[\\s_\\-]*key|authorization|access[\\s_\\-]*token|refresh[\\s_\\-]*token|password|secret|client[\\s_\\-]*secret|private[\\s_\\-]*key|credential)\\b\\s*[:=]\\s*)(?:\\\"[^\\\"]*\\\"|'[^']*'|[^\\s,;&}\\]]+)",
                RegexOptions.Compiled |
                RegexOptions.CultureInvariant);

        private static readonly HashSet<string> OmittedPayloadKeys =
            new HashSet<string>(
                new[]
                {
                    "arguments",
                    "body",
                    "code",
                    "content",
                    "data",
                    "filecontent",
                    "output",
                    "payload",
                    "raw",
                    "reasoning",
                    "response",
                    "result",
                    "source",
                    "text",
                    "valuejson"
                },
                StringComparer.Ordinal);


        internal static string Redact(
            string value)
        {
            string normalized =
                value ??
                string.Empty;

            if (string.IsNullOrEmpty(
                    normalized))
            {
                return string.Empty;
            }

            string jsonRedacted =
                TryRedactJson(
                    normalized);

            string redacted =
                BearerPattern.Replace(
                    jsonRedacted,
                    "Bearer [REDACTED]");

            return AssignmentPattern.Replace(
                redacted,
                match =>
                    match.Groups[1].Value +
                    "[REDACTED]");
        }


        internal static string SummarizeToolPayload(
            string payload,
            bool arguments)
        {
            string normalized =
                payload ??
                string.Empty;

            if (string.IsNullOrWhiteSpace(
                    normalized))
            {
                return arguments
                    ? "No arguments."
                    : "No result details.";
            }

            if (normalized.Length >
                MaximumParsedPayloadCharacters)
            {
                return arguments
                    ? $"Arguments supplied ({normalized.Length} characters; details omitted)."
                    : $"Result received ({normalized.Length} characters; details omitted).";
            }

            try
            {
                JToken token =
                    JToken.Parse(
                        normalized);

                if (token is JObject objectValue)
                {
                    return SummarizeObject(
                        objectValue);
                }

                if (token is JArray arrayValue)
                {
                    return arguments
                        ? $"{arrayValue.Count} argument item(s)."
                        : $"{arrayValue.Count} result item(s) returned.";
                }
            }
            catch (JsonException)
            {
                // Free-form payloads are described, not copied into the UI.
            }

            return arguments
                ? $"Arguments supplied ({normalized.Length} characters)."
                : $"Result received ({normalized.Length} characters).";
        }


        private static string SummarizeObject(
            JObject value)
        {
            StringBuilder builder =
                new StringBuilder();

            int fieldCount =
                0;

            // Keep object identity readable even when the rest of a nested
            // inspection is summarized. Never copy arbitrary nested payloads.
            if (value["GameObject"] is JObject inspectedObject)
            {
                foreach (var mapping in new[] {
                    new[] { "InstanceId", "GameObjectInstanceId" },
                    new[] { "Name", "GameObjectName" },
                    new[] { "HierarchyPath", "HierarchyPath" },
                    new[] { "ScenePath", "ScenePath" } })
                {
                    JToken identity = inspectedObject[mapping[0]];
                    if (identity == null || identity.Type == JTokenType.Null || identity is JContainer) continue;
                    if (builder.Length > 0) builder.Append(", ");
                    builder.Append(mapping[1]).Append('=').Append(SummarizeValue(identity));
                    fieldCount++;
                }
            }

            int identityFieldCount = fieldCount;
            foreach (
                JProperty property
                in value.Properties())
            {
                if (fieldCount >=
                    MaximumFields)
                {
                    break;
                }

                if (builder.Length >
                    0)
                {
                    builder.Append(
                        ", ");
                }

                builder.Append(
                    property.Name);
                builder.Append(
                    '=');

                string normalizedName =
                    NormalizeKey(
                        property.Name);

                if (IsSensitiveKey(
                        normalizedName))
                {
                    builder.Append(
                        "[REDACTED]");
                }
                else if (OmittedPayloadKeys.Contains(
                             normalizedName))
                {
                    builder.Append(
                        DescribeOmitted(
                            property.Value));
                }
                else
                {
                    builder.Append(
                        SummarizeValue(
                            property.Value));
                }

                fieldCount++;
            }

            int omittedFields =
                value.Properties().Count() -
                (fieldCount - identityFieldCount);

            if (omittedFields >
                0)
            {
                builder.Append(
                    $", … {omittedFields} more field(s)");
            }

            return Redact(
                builder.ToString());
        }


        private static string SummarizeValue(
            JToken token)
        {
            if (token ==
                    null ||
                token.Type ==
                    JTokenType.Null ||
                token.Type ==
                    JTokenType.Undefined)
            {
                return "null";
            }

            if (token is JArray array)
            {
                return $"[{array.Count} item(s)]";
            }

            if (token is JObject objectValue)
            {
                return $"{{{objectValue.Properties().Count()} field(s)}}";
            }

            string value =
                token.Type ==
                    JTokenType.String
                    ? token.Value<string>() ?? string.Empty
                    : token.ToString(
                        Formatting.None);

            value =
                Redact(
                    value);

            if (value.Length >
                MaximumScalarCharacters)
            {
                return value.Substring(
                           0,
                           MaximumScalarCharacters) +
                       $"… [{value.Length} chars]";
            }

            return value;
        }


        private static string DescribeOmitted(
            JToken token)
        {
            if (token is JArray array)
            {
                return $"[omitted; {array.Count} item(s)]";
            }

            if (token is JObject objectValue)
            {
                return $"[omitted; {objectValue.Properties().Count()} field(s)]";
            }

            string value =
                token?.ToString(
                    Formatting.None) ??
                string.Empty;

            return $"[omitted; {value.Length} characters]";
        }


        private static string TryRedactJson(
            string value)
        {
            try
            {
                JToken token =
                    JToken.Parse(
                        value);

                RedactToken(
                    token);

                return token.ToString(
                    Formatting.None);
            }
            catch (JsonException)
            {
                return value;
            }
        }


        private static void RedactToken(
            JToken token)
        {
            if (token is JObject objectValue)
            {
                foreach (
                    JProperty property
                    in objectValue.Properties().ToList())
                {
                    if (IsSensitiveKey(
                            NormalizeKey(
                                property.Name)))
                    {
                        property.Value =
                            "[REDACTED]";
                    }
                    else
                    {
                        RedactToken(
                            property.Value);
                    }
                }

                return;
            }

            if (token is JArray arrayValue)
            {
                foreach (JToken child in arrayValue)
                {
                    RedactToken(
                        child);
                }

                return;
            }

            if (token.Type ==
                JTokenType.String)
            {
                if (token is JValue value)
                {
                    value.Value =
                        BearerPattern.Replace(
                            value.Value<string>() ?? string.Empty,
                            "Bearer [REDACTED]");
                }
            }
        }


        private static bool IsSensitiveKey(
            string normalizedKey)
        {
            return normalizedKey ==
                       "apikey" ||
                   normalizedKey ==
                       "authorization" ||
                   normalizedKey ==
                       "password" ||
                   normalizedKey ==
                       "secret" ||
                   normalizedKey ==
                       "clientsecret" ||
                   normalizedKey ==
                       "privatekey" ||
                   normalizedKey ==
                       "credential" ||
                   normalizedKey.EndsWith(
                       "token",
                       StringComparison.Ordinal) ||
                   normalizedKey.EndsWith(
                       "secret",
                       StringComparison.Ordinal) ||
                   normalizedKey.EndsWith(
                       "password",
                       StringComparison.Ordinal);
        }


        private static string NormalizeKey(
            string value)
        {
            return new string(
                (value ?? string.Empty)
                    .Where(
                        char.IsLetterOrDigit)
                    .Select(
                        char.ToLowerInvariant)
                    .ToArray());
        }
    }


    /// <summary>
    /// UI-ready projection of entries sharing one contextual correlation ID.
    /// </summary>
    public sealed class AIRuntimeAgentActivityRun
    {
        public string CorrelationId { get; internal set; }
        public string Source { get; internal set; }
        public string Agent { get; internal set; }
        public string Objective { get; internal set; }
        public string Stage { get; internal set; }
        public string Summary { get; internal set; }
        public DateTime LatestTimestampUtc { get; internal set; }
        public bool IsActive { get; internal set; }
        public bool HasStarted { get; internal set; }
        public bool HasToolActivity { get; internal set; }
        public bool HasReviewActivity { get; internal set; }
        public bool HasFinished { get; internal set; }
        public bool HasError { get; internal set; }
        public IReadOnlyList<AIRuntimeAgentActivityEntry> Entries { get; internal set; }
    }


    public static class AIRuntimeAgentActivityProjection
    {
        public static IReadOnlyList<AIRuntimeAgentActivityRun> BuildRuns(
            IEnumerable<AIRuntimeAgentActivityEntry> entries)
        {
            return (entries ??
                    Enumerable.Empty<AIRuntimeAgentActivityEntry>())
                .Where(
                    entry =>
                        entry !=
                        null)
                .GroupBy(
                    GetCorrelationKey,
                    StringComparer.Ordinal)
                .Select(
                    BuildRun)
                .OrderByDescending(
                    run =>
                        run.IsActive)
                .ThenByDescending(
                    run =>
                        run.LatestTimestampUtc)
                .ToArray();
        }


        private static AIRuntimeAgentActivityRun BuildRun(
            IGrouping<string, AIRuntimeAgentActivityEntry> group)
        {
            List<AIRuntimeAgentActivityEntry> ordered =
                group
                    .OrderBy(
                        entry =>
                            entry.Sequence)
                    .ThenBy(
                        entry =>
                            entry.TimestampUtc)
                    .ToList();

            AIRuntimeAgentActivityEntry latest =
                ordered[ordered.Count - 1];

            int lastStarted =
                ordered.FindLastIndex(
                    entry =>
                        entry.Kind ==
                        AIRuntimeAgentActivityKind.RunStarted);

            int lastTerminal =
                ordered.FindLastIndex(
                    entry =>
                        IsTerminal(
                            entry.Kind));

            int lastWorkStarted =
                ordered.FindLastIndex(
                    IsWorkStartingEntry);

            bool hasStarted =
                lastStarted >=
                0;

            bool isActive =
                lastWorkStarted >
                lastTerminal;

            return new AIRuntimeAgentActivityRun
            {
                CorrelationId = latest.CorrelationId,
                Source = latest.Source,
                Agent = latest.Agent,
                Objective = latest.Objective,
                Stage = latest.Stage,
                Summary = latest.Summary,
                LatestTimestampUtc = latest.TimestampUtc,
                IsActive = isActive,
                HasStarted = hasStarted,
                HasToolActivity =
                    ordered.Any(
                        entry =>
                            entry.Kind == AIRuntimeAgentActivityKind.ToolStarted ||
                            entry.Kind == AIRuntimeAgentActivityKind.ToolCompleted ||
                            entry.Kind == AIRuntimeAgentActivityKind.ToolFailed),
                HasReviewActivity =
                    ordered.Any(
                        entry =>
                            entry.Kind == AIRuntimeAgentActivityKind.Review ||
                            (entry.Stage ?? string.Empty).IndexOf(
                                "review",
                                StringComparison.OrdinalIgnoreCase) >= 0),
                HasFinished =
                    lastTerminal >=
                    lastWorkStarted &&
                    lastTerminal >=
                    0,
                HasError =
                    ordered.Any(
                        entry =>
                            entry.Kind == AIRuntimeAgentActivityKind.ToolFailed ||
                            entry.Kind == AIRuntimeAgentActivityKind.RunFailed ||
                            entry.Kind == AIRuntimeAgentActivityKind.RunTimedOut ||
                            entry.Kind == AIRuntimeAgentActivityKind.Error),
                Entries = ordered
            };
        }


        private static string GetCorrelationKey(
            AIRuntimeAgentActivityEntry entry)
        {
            if (!string.IsNullOrWhiteSpace(
                    entry.CorrelationId))
            {
                return entry.CorrelationId;
            }

            return string.Join(
                "\u001f",
                entry.Source ?? string.Empty,
                entry.Agent ?? string.Empty,
                entry.Objective ?? string.Empty);
        }


        private static bool IsTerminal(
            AIRuntimeAgentActivityKind kind)
        {
            return kind ==
                       AIRuntimeAgentActivityKind.RunCompleted ||
                   kind ==
                       AIRuntimeAgentActivityKind.RunCancelled ||
                   kind ==
                       AIRuntimeAgentActivityKind.RunTimedOut ||
                   kind ==
                       AIRuntimeAgentActivityKind.RunFailed ||
                   kind ==
                       AIRuntimeAgentActivityKind.Error;
        }


        private static bool IsWorkStartingEntry(
            AIRuntimeAgentActivityEntry entry)
        {
            if (entry ==
                null)
            {
                return false;
            }

            if (entry.Kind ==
                    AIRuntimeAgentActivityKind.RunStarted ||
                entry.Kind ==
                    AIRuntimeAgentActivityKind.Retry ||
                entry.Kind ==
                    AIRuntimeAgentActivityKind.Review)
            {
                return true;
            }

            return entry.Kind ==
                       AIRuntimeAgentActivityKind.Status &&
                   !IsInactiveStage(
                       entry.Stage);
        }


        private static bool IsInactiveStage(
            string stage)
        {
            string normalized =
                stage?.Trim() ??
                string.Empty;

            return normalized.Equals("Complete", StringComparison.OrdinalIgnoreCase) ||
                   normalized.Equals("Completed", StringComparison.OrdinalIgnoreCase) ||
                   normalized.Equals("Done", StringComparison.OrdinalIgnoreCase) ||
                   normalized.Equals("Idle", StringComparison.OrdinalIgnoreCase) ||
                   normalized.Equals("Cancelled", StringComparison.OrdinalIgnoreCase) ||
                   normalized.Equals("Canceled", StringComparison.OrdinalIgnoreCase) ||
                   normalized.Equals("Failed", StringComparison.OrdinalIgnoreCase) ||
                   normalized.Equals("Timed out", StringComparison.OrdinalIgnoreCase);
        }
    }
}
