using System.Text;
using System.Text.Json;

namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;

/// <summary>
/// Arguments for an <c>audit tree</c> invocation.
/// </summary>
public sealed class AuditTreeArgs
{
    /// <summary>
    /// The execution ID (GUID) to look up. May be any node in the chain — the
    /// server walks to the root and returns the full tree.
    /// </summary>
    public string ExecutionId { get; set; } = string.Empty;
}

/// <summary>
/// Represents one execution node as returned by <c>GET /api/audit/tree</c>.
/// Property names match the server's camelCase JSON serialisation of
/// <c>ExecutionTreeNode</c>.
/// </summary>
internal sealed class AuditTreeNodeDto
{
    /// <summary>
    /// The execution ID (GUID) identifying this node in the tree.
    /// </summary>
    public Guid ExecutionId { get; init; }

    /// <summary>
    /// The execution ID of the spawning run, or <c>null</c> for a root node.
    /// </summary>
    public Guid? ParentExecutionId { get; init; }

    /// <summary>
    /// The number of audit log rows contributed by this execution.
    /// </summary>
    public int RowCount { get; init; }

    /// <summary>
    /// The set of audit channels touched by this execution.
    /// </summary>
    public string[] Channels { get; init; } = Array.Empty<string>();

    /// <summary>
    /// The set of distinct row statuses recorded for this execution.
    /// </summary>
    public string[] Statuses { get; init; } = Array.Empty<string>();

    /// <summary>
    /// The site identifier the execution originated from, or <c>null</c> if not site-scoped.
    /// </summary>
    public string? SourceSiteId { get; init; }

    /// <summary>
    /// The instance identifier the execution originated from, or <c>null</c> if not instance-scoped.
    /// </summary>
    public string? SourceInstanceId { get; init; }

    /// <summary>
    /// The timestamp (UTC) of the earliest audit row for this execution, if any.
    /// </summary>
    public DateTime? FirstOccurredAtUtc { get; init; }

    /// <summary>
    /// The timestamp (UTC) of the latest audit row for this execution, if any.
    /// </summary>
    public DateTime? LastOccurredAtUtc { get; init; }
}

/// <summary>
/// Pure helpers for the <c>audit tree</c> subcommand: builds the query string,
/// calls <c>GET /api/audit/tree</c>, and renders the result as either an
/// indented ASCII tree (table format) or raw JSON. Kept separate from the
/// command wiring so each piece is unit-testable without standing up the
/// command tree.
/// </summary>
public static class AuditTreeHelpers
{
    private static readonly JsonSerializerOptions JsonReadOptions = new()
    {
        PropertyNameCaseInsensitive = true,
    };

    private static readonly JsonSerializerOptions JsonWriteOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        WriteIndented = true,
    };

    /// <summary>
    /// Builds the query string for <c>GET /api/audit/tree</c>.
    /// </summary>
    /// <param name="executionId">The execution ID GUID.</param>
    /// <returns>A relative path + query string ready to append to the base URL.</returns>
    public static string BuildUrl(Guid executionId)
        => $"api/audit/tree?executionId={executionId:D}";

    /// <summary>
    /// Executes the tree lookup: GETs <c>/api/audit/tree</c> and renders the result
    /// in the requested format. Returns the process exit code (0 = success,
    /// 1 = error, 2 = authorization failure).
    /// </summary>
    /// <param name="client">The management HTTP client.</param>
    /// <param name="executionId">The execution ID to look up.</param>
    /// <param name="format">"table" (default) or "json".</param>
    /// <param name="output">The output writer for results.</param>
    /// <returns>A task that resolves to the process exit code.</returns>
    public static async Task<int> RunTreeAsync(
        ManagementHttpClient client,
        Guid executionId,
        string format,
        TextWriter output)
    {
        var url = BuildUrl(executionId);
        var response = await client.SendGetAsync(url, TimeSpan.FromSeconds(30));

        if (response.JsonData == null)
        {
            OutputFormatter.WriteError(
                response.Error ?? "Audit tree request failed.", response.ErrorCode ?? "ERROR");
            return CommandHelpers.IsAuthorizationFailure(response) ? 2 : 1;
        }

        var nodes = ParseNodes(response.JsonData);

        if (format == "json")
        {
            WriteJson(nodes, output);
        }
        else
        {
            WriteTable(nodes, executionId, output);
        }

        output.Flush();
        return 0;
    }

    /// <summary>
    /// Parses the JSON array from the server into an array of
    /// <see cref="AuditTreeNodeDto"/>.
    /// </summary>
    /// <param name="json">The raw JSON response body.</param>
    /// <returns>An array of deserialized tree nodes (empty on parse failure).</returns>
    internal static AuditTreeNodeDto[] ParseNodes(string json)
    {
        try
        {
            return JsonSerializer.Deserialize<AuditTreeNodeDto[]>(json, JsonReadOptions)
                   ?? Array.Empty<AuditTreeNodeDto>();
        }
        catch (JsonException)
        {
            return Array.Empty<AuditTreeNodeDto>();
        }
    }

    /// <summary>
    /// Renders the nodes as pretty-printed JSON to <paramref name="output"/>.
    /// </summary>
    /// <param name="nodes">The tree nodes to render.</param>
    /// <param name="output">The output writer for results.</param>
    internal static void WriteJson(AuditTreeNodeDto[] nodes, TextWriter output)
    {
        output.WriteLine(JsonSerializer.Serialize(nodes, JsonWriteOptions));
    }

    /// <summary>
    /// Renders the nodes as an indented ASCII tree. The root node (null
    /// <c>ParentExecutionId</c>) is printed first; each child is indented
    /// two spaces per depth level. The queried/entry-point node is marked
    /// with <c> [*]</c>.
    /// </summary>
    /// <param name="nodes">The tree nodes to render.</param>
    /// <param name="queriedExecutionId">The execution ID originally queried, used to mark the entry-point node.</param>
    /// <param name="output">The output writer for results.</param>
    internal static void WriteTable(
        AuditTreeNodeDto[] nodes,
        Guid queriedExecutionId,
        TextWriter output)
    {
        if (nodes.Length == 0)
        {
            output.WriteLine("(no execution tree found)");
            return;
        }

        // Build a parent → children lookup (keyed by non-null parent Guid).
        // Nodes whose ParentExecutionId is null are roots and are not placed in
        // the lookup; they are identified separately below.
        var childrenOf = new Dictionary<Guid, List<AuditTreeNodeDto>>();
        foreach (var node in nodes)
        {
            if (node.ParentExecutionId is { } parentId)
            {
                if (!childrenOf.ContainsKey(parentId))
                    childrenOf[parentId] = new List<AuditTreeNodeDto>();
                childrenOf[parentId].Add(node);
            }
        }

        // Identify roots: nodes whose ParentExecutionId is null, or whose parent
        // is not present in the node set (stub-root case).
        var nodeIds = new HashSet<Guid>(nodes.Select(n => n.ExecutionId));
        var roots = nodes
            .Where(n => n.ParentExecutionId == null || !nodeIds.Contains(n.ParentExecutionId.Value))
            .ToList();

        // Render depth-first.
        var sb = new StringBuilder();
        foreach (var root in roots)
        {
            RenderNode(root, depth: 0, childrenOf, queriedExecutionId, sb);
        }

        output.Write(sb.ToString());
    }

    private static void RenderNode(
        AuditTreeNodeDto node,
        int depth,
        Dictionary<Guid, List<AuditTreeNodeDto>> childrenOf,
        Guid queriedExecutionId,
        StringBuilder sb)
    {
        var indent = new string(' ', depth * 2);
        var marker = node.ExecutionId == queriedExecutionId ? " [*]" : string.Empty;
        var channels = node.Channels.Length > 0 ? string.Join(",", node.Channels) : "-";
        var statuses = node.Statuses.Length > 0 ? string.Join(",", node.Statuses) : "-";
        var site = node.SourceSiteId ?? "-";
        var instance = node.SourceInstanceId ?? "-";
        var first = node.FirstOccurredAtUtc.HasValue
            ? node.FirstOccurredAtUtc.Value.ToString("yyyy-MM-ddTHH:mm:ssZ")
            : "-";

        sb.AppendLine(
            $"{indent}{node.ExecutionId:D}{marker}  rows={node.RowCount}  channels=[{channels}]  statuses=[{statuses}]  site={site}  instance={instance}  first={first}");

        if (childrenOf.TryGetValue(node.ExecutionId, out var children))
        {
            foreach (var child in children)
            {
                RenderNode(child, depth + 1, childrenOf, queriedExecutionId, sb);
            }
        }
    }
}
