using System.Globalization;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Driver.Cli.Common;

/// <summary>
///     Renders <see cref="DataValueSnapshot"/> + <see cref="WriteResult"/> payloads as the
///     plain-text lines every driver CLI prints to its console. Matches the one-field-per-line
///     style the existing OPC UA <c>otopcua-cli</c> uses so combined runs (read a tag via both
///     CLIs side-by-side) look coherent.
/// </summary>
public static class SnapshotFormatter
{
    /// <summary>
    ///     Single-tag multi-line render. Shape:
    ///     <code>
    ///     Tag:         &lt;name&gt;
    ///     Value:       &lt;value&gt;
    ///     Status:      0x... (Good|BadCommunicationError|...)
    ///     Source Time: 2026-04-21T12:34:56.789Z
    ///     Server Time: 2026-04-21T12:34:56.790Z
    ///     </code>
    /// </summary>
    /// <param name="tagName">The tag name to include in the output.</param>
    /// <param name="snapshot">The data value snapshot to format.</param>
    /// <returns>The multi-line rendered snapshot.</returns>
    public static string Format(string tagName, DataValueSnapshot snapshot)
    {
        ArgumentNullException.ThrowIfNull(snapshot);
        var lines = new[]
        {
            $"Tag:         {tagName}",
            $"Value:       {FormatValue(snapshot.Value)}",
            $"Status:      {FormatStatus(snapshot.StatusCode)}",
            $"Source Time: {FormatTimestamp(snapshot.SourceTimestampUtc)}",
            $"Server Time: {FormatTimestamp(snapshot.ServerTimestampUtc)}",
        };
        return string.Join(Environment.NewLine, lines);
    }

    /// <summary>
    ///     Write-result render, one line: <c>Write &lt;tag&gt;: 0x... (Good|...)</c>.
    /// </summary>
    /// <param name="tagName">The tag name to include in the output.</param>
    /// <param name="result">The write result to format.</param>
    /// <returns>The rendered write-result line.</returns>
    public static string FormatWrite(string tagName, WriteResult result)
    {
        ArgumentNullException.ThrowIfNull(result);
        return $"Write {tagName}: {FormatStatus(result.StatusCode)}";
    }

    /// <summary>
    ///     Table-style render for batch reads. Emits an aligned 4-column layout:
    ///     tag / value / status / source-time.
    /// </summary>
    /// <param name="tagNames">The list of tag names to include as rows.</param>
    /// <param name="snapshots">The list of data value snapshots to format.</param>
    /// <returns>The rendered table as a multi-line string.</returns>
    public static string FormatTable(
        IReadOnlyList<string> tagNames, IReadOnlyList<DataValueSnapshot> snapshots)
    {
        ArgumentNullException.ThrowIfNull(tagNames);
        ArgumentNullException.ThrowIfNull(snapshots);
        if (tagNames.Count != snapshots.Count)
            throw new ArgumentException(
                $"tagNames ({tagNames.Count}) and snapshots ({snapshots.Count}) must be the same length");

        var rows = tagNames.Select((t, i) => new
        {
            Tag = t,
            Value = FormatValue(snapshots[i].Value),
            Status = FormatStatus(snapshots[i].StatusCode),
            Time = FormatTimestamp(snapshots[i].SourceTimestampUtc),
        }).ToArray();

        int tagW = rows.Length == 0 ? "TAG".Length : Math.Max("TAG".Length, rows.Max(r => r.Tag.Length));
        int valW = rows.Length == 0 ? "VALUE".Length : Math.Max("VALUE".Length, rows.Max(r => r.Value.Length));
        int statW = rows.Length == 0 ? "STATUS".Length : Math.Max("STATUS".Length, rows.Max(r => r.Status.Length));
        // source-time is the right-most column, so it is intentionally not measured or padded;
        // when a snapshot has a non-null SourceTimestampUtc the cell is 24 chars (ISO-8601 to ms),
        // and when the timestamp is null FormatTimestamp emits "-" — the resulting unalignment is
        // harmless because nothing is appended after this column.

        var sb = new System.Text.StringBuilder();
        sb.Append("TAG".PadRight(tagW)).Append("  ")
          .Append("VALUE".PadRight(valW)).Append("  ")
          .Append("STATUS".PadRight(statW)).Append("  ")
          .Append("SOURCE TIME").AppendLine();
        sb.Append(new string('-', tagW)).Append("  ")
          .Append(new string('-', valW)).Append("  ")
          .Append(new string('-', statW)).Append("  ")
          .Append(new string('-', "SOURCE TIME".Length)).AppendLine();
        foreach (var r in rows)
        {
            sb.Append(r.Tag.PadRight(tagW)).Append("  ")
              .Append(r.Value.PadRight(valW)).Append("  ")
              .Append(r.Status.PadRight(statW)).Append("  ")
              .Append(r.Time).AppendLine();
        }
        return sb.ToString().TrimEnd();
    }

    /// <summary>Formats a value for console output, handling null, bool, string, and formattable types.</summary>
    /// <param name="value">The value to format.</param>
    /// <returns>A formatted string representation of the value.</returns>
    public static string FormatValue(object? value) => value switch
    {
        null => "<null>",
        bool b => b ? "true" : "false",
        string s => $"\"{s}\"",
        IFormattable f => f.ToString(null, CultureInfo.InvariantCulture),
        _ => value.ToString() ?? "<null>",
    };

    /// <summary>Formats an OPC UA status code as a hexadecimal string with named severity classification.</summary>
    /// <param name="statusCode">The OPC UA status code to format.</param>
    /// <returns>A formatted status string like "0x00000000 (Good)" or "0x80050000 (BadCommunicationError)".</returns>
    public static string FormatStatus(uint statusCode)
    {
        // OPC UA status codes carry sub-code and flag bits in the low 16 bits (info type,
        // structure-changed, semantics-changed, limit bits, overflow, etc.).  To ensure
        // that e.g. 0x80050001 still reads as "BadCommunicationError" rather than bare hex,
        // named codes are matched against the high-word mask (code & 0xFFFF0000).  When no
        // named match is found the severity class (top 2 bits) provides a meaningful fallback
        // so operators always see at least Good / Uncertain / Bad rather than raw hex.
        // Numeric codes are the canonical values from the OPC Foundation Opc.Ua.StatusCodes
        // table; keep them in sync with that table if this list is extended.
        var masked = statusCode & 0xFFFF0000u;
        var name = masked switch
        {
            0x00000000u => "Good",
            0x80000000u => "Bad",
            0x80020000u => "BadInternalError",
            0x80050000u => "BadCommunicationError",
            0x800A0000u => "BadTimeout",
            0x80310000u => "BadNoCommunication",
            0x80320000u => "BadWaitingForInitialData",
            0x80340000u => "BadNodeIdUnknown",
            0x80330000u => "BadNodeIdInvalid",
            0x803B0000u => "BadNotWritable",
            0x803C0000u => "BadOutOfRange",
            0x803D0000u => "BadNotSupported",
            0x808B0000u => "BadDeviceFailure",
            0x80740000u => "BadTypeMismatch",
            0x40000000u => "Uncertain",
            _ => null,
        };

        if (name is null)
        {
            // Severity fallback: top 2 bits identify the quality class even for unknown
            // sub-codes.  0x80000000 and 0xC0000000 (reserved quality) both map to "Bad".
            name = (statusCode & 0xC0000000u) switch
            {
                0x00000000u => "Good",
                0x40000000u => "Uncertain",
                _ => "Bad",
            };
        }

        // name is always non-null here: the named shortlist assigns it directly, and
        // the severity-class fallback above assigns one of "Good" / "Uncertain" / "Bad".
        return $"0x{statusCode:X8} ({name})";
    }

    /// <summary>Formats a UTC timestamp as an ISO 8601 string, or "-" if null.</summary>
    /// <param name="ts">The timestamp to format, or null.</param>
    /// <returns>An ISO 8601 formatted string or "-".</returns>
    public static string FormatTimestamp(DateTime? ts)
    {
        if (ts is null) return "-";
        var utc = ts.Value.Kind == DateTimeKind.Utc ? ts.Value : ts.Value.ToUniversalTime();
        return utc.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture);
    }
}
