using System.Globalization;
using System.Text;
using Microsoft.AspNetCore.Components;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;

/// <summary>
/// Reusable dependency-free SVG trend chart for "KPI History &amp; Trends".
/// Renders a <see cref="KpiSeriesPoint"/> series as a single muted
/// polyline inside a Bootstrap card, with a baseline (x-axis) and value/time
/// labels. No third-party charting library — the project rule is custom Blazor
/// components on Bootstrap CSS only.
/// </summary>
/// <remarks>
/// <para>
/// <b>Purely presentational.</b> The component never fetches data; the parent
/// page supplies a fresh <see cref="Points"/> list and an
/// <see cref="IsAvailable"/> flag, mirroring how <c>AuditKpiTiles</c> and
/// <c>SiteCallKpiTiles</c> consume a snapshot. The same card chrome and
/// <c>small text-muted</c> label styling keep the chart visually consistent
/// with the KPI tiles it sits beside.
/// </para>
/// <para>
/// <b>Coordinate math.</b> X is normalised across the time range
/// <c>[min(BucketStartUtc), max(BucketStartUtc)]</c>; Y is normalised across
/// <c>[0, max(Value)]</c> with the baseline pinned at 0. Every guard that could
/// otherwise divide by zero is explicit: an empty / single-point series never
/// reaches the polyline math, an all-equal-timestamp series falls back to even
/// horizontal spacing, and an all-zero (or single-distinct-value) series draws a
/// flat line at the baseline. All coordinate doubles are formatted with
/// <see cref="CultureInfo.InvariantCulture"/> so locales with comma decimals do
/// not corrupt the SVG <c>points</c> attribute.
/// </para>
/// </remarks>
public partial class KpiTrendChart
{
    // The fixed user-space coordinate system the polyline is computed in. The
    // SVG is rendered with width="100%" + preserveAspectRatio="none" so it
    // scales responsively to its container while the math stays integer-clean.
    internal const int ViewBoxWidth = 600;
    internal const int ViewBoxHeight = 140;

    // Inner plot inset so the polyline + baseline never touch the card edge and
    // the top sample is not clipped by the 1px stroke.
    private const double PadX = 4;
    private const double PadTop = 6;
    private const double PadBottom = 6;

    /// <summary>
    /// The series to plot, assumed ordered ascending by
    /// <see cref="KpiSeriesPoint.BucketStartUtc"/>. <c>null</c> or empty renders
    /// the unavailable placeholder rather than an empty chart.
    /// <para>
    /// The per-point value's meaning depends on the metric's
    /// <see cref="ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi.KpiMetricAggregationCatalog"/>
    /// classification: a Rate-classified series carries <b>per-bucket totals</b> (the
    /// bucketer sums each bucket's deltas), while a Gauge series carries
    /// last-value-per-bucket readings. The chart is agnostic to which — it plots the
    /// supplied values verbatim (arch-review 04 round 2, R3).
    /// </para>
    /// </summary>
    [Parameter] public IReadOnlyList<KpiSeriesPoint>? Points { get; set; }

    /// <summary>Card title — also used for the SVG <c>&lt;title&gt;</c> and the
    /// <c>data-test</c> slug. Required-ish; defaults to "Trend" if blank.</summary>
    [Parameter] public string Title { get; set; } = "Trend";

    /// <summary>Optional unit suffix appended to value labels (e.g. "s"). For a
    /// Rate-classified metric the supplied text must state the per-bucket-total
    /// semantics (e.g. "delivered/bucket"), not a per-minute/per-interval reading,
    /// so the label matches the summed values in <see cref="Points"/> (R3).</summary>
    [Parameter] public string? Unit { get; set; }

    /// <summary>
    /// True when the supplied series is a successful query result. False renders
    /// the em-dash placeholder (no SVG), mirroring the KPI tiles' unavailable
    /// state.
    /// </summary>
    [Parameter] public bool IsAvailable { get; set; } = true;

    /// <summary>Optional message rendered under the placeholder when
    /// <see cref="IsAvailable"/> is false.</summary>
    [Parameter] public string? ErrorMessage { get; set; }

    // ── Derived state ────────────────────────────────────────────────────────

    private IReadOnlyList<KpiSeriesPoint> Series => Points ?? Array.Empty<KpiSeriesPoint>();

    /// <summary>A drawable chart needs to be available and have ≥2 points.</summary>
    private bool HasChart => IsAvailable && Series.Count >= 2;

    /// <summary>Exactly one sample — show the single-sample note, not a polyline.</summary>
    private bool HasSingleSample => IsAvailable && Series.Count == 1;

    /// <summary>
    /// Stable Playwright hook: <c>kpi-trend-&lt;slug&gt;</c> where the slug is the
    /// title lowercased with each run of non-alphanumerics collapsed to a single
    /// dash and trimmed. Empty / symbol-only titles fall back to "chart".
    /// </summary>
    private string DataTest => $"kpi-trend-{Slugify(Title)}";

    /// <summary>Slugifies a title: lowercased, with each run of non-alphanumerics collapsed to a single dash and trimmed. Falls back to "chart" for an empty/symbol-only title.</summary>
    /// <param name="title">The title to slugify, or <c>null</c>.</param>
    /// <returns>The slugified title, or <c>"chart"</c> as a fallback.</returns>
    internal static string Slugify(string? title)
    {
        if (string.IsNullOrWhiteSpace(title))
        {
            return "chart";
        }

        var sb = new StringBuilder(title.Length);
        var lastWasDash = false;
        foreach (var ch in title)
        {
            if (char.IsLetterOrDigit(ch))
            {
                sb.Append(char.ToLowerInvariant(ch));
                lastWasDash = false;
            }
            else if (!lastWasDash)
            {
                sb.Append('-');
                lastWasDash = true;
            }
        }

        var slug = sb.ToString().Trim('-');
        return slug.Length == 0 ? "chart" : slug;
    }

    // ── Value/time range ─────────────────────────────────────────────────────

    private double MaxValue => Series.Count == 0 ? 0 : Series.Max(p => p.Value);

    private DateTime FirstUtc => Series[0].BucketStartUtc;
    private DateTime LastUtc => Series[^1].BucketStartUtc;

    // ── Label formatting ─────────────────────────────────────────────────────

    private string MaxValueLabel => FormatValue(MaxValue);
    private string BaselineLabel => FormatValue(0);

    private string FormatValue(double v)
    {
        var num = v.ToString("0.###", CultureInfo.InvariantCulture);
        return string.IsNullOrEmpty(Unit) ? num : $"{num} {Unit}";
    }

    // Time labels: include the date prefix when the range spans more than a day,
    // otherwise just HH:mm. Always UTC (all timestamps in the system are UTC).
    private string StartTimeLabel => FormatTime(FirstUtc);
    private string EndTimeLabel => FormatTime(LastUtc);

    private string FormatTime(DateTime utc)
    {
        var spansDays = Series.Count >= 2 && (LastUtc.Date != FirstUtc.Date);
        return utc.ToString(spansDays ? "MM-dd HH:mm" : "HH:mm", CultureInfo.InvariantCulture);
    }

    // ── Polyline geometry ────────────────────────────────────────────────────

    /// <summary>
    /// Builds the SVG <c>polyline points</c> string. Only called when
    /// <see cref="HasChart"/> is true (≥2 points). X spreads each point across
    /// the time range; if every timestamp is identical the range is zero and we
    /// fall back to even index-based spacing. Y is inverted (SVG origin is
    /// top-left) and normalised against <see cref="MaxValue"/>; a zero max draws
    /// every point on the baseline.
    /// </summary>
    private string BuildPoints()
    {
        var n = Series.Count;
        var plotW = ViewBoxWidth - (2 * PadX);
        var plotH = ViewBoxHeight - PadTop - PadBottom;
        var baselineY = ViewBoxHeight - PadBottom;

        var firstTicks = FirstUtc.Ticks;
        double timeSpanTicks = LastUtc.Ticks - firstTicks;
        var max = MaxValue;

        var sb = new StringBuilder(n * 12);
        for (var i = 0; i < n; i++)
        {
            var p = Series[i];

            // X — by time fraction, or even index spacing when all timestamps equal.
            // n >= 2 is guaranteed by the HasChart precondition (Series.Count >= 2),
            // so the (n - 1) divisor is always ≥ 1 and the n == 1 arm is unreachable.
            double xFrac = timeSpanTicks > 0
                ? (p.BucketStartUtc.Ticks - firstTicks) / timeSpanTicks
                : (double)i / (n - 1);
            var x = PadX + (xFrac * plotW);

            // Y — baseline at 0, top at max. Flat at baseline when max == 0.
            // Clamp to non-negative so a stale negative Value cannot push a point
            // above the baseline or outside the viewBox.
            double yFrac = max > 0 ? Math.Max(0, p.Value) / max : 0.0;
            var y = baselineY - (yFrac * plotH);

            if (i > 0)
            {
                sb.Append(' ');
            }
            sb.Append(x.ToString("0.##", CultureInfo.InvariantCulture));
            sb.Append(',');
            sb.Append(y.ToString("0.##", CultureInfo.InvariantCulture));
        }

        return sb.ToString();
    }

    private string BaselineYString =>
        (ViewBoxHeight - PadBottom).ToString("0.##", CultureInfo.InvariantCulture);

    private string PlotRightXString =>
        (ViewBoxWidth - PadX).ToString("0.##", CultureInfo.InvariantCulture);

    private string PlotLeftXString =>
        PadX.ToString("0.##", CultureInfo.InvariantCulture);

    private string ViewBox => $"0 0 {ViewBoxWidth} {ViewBoxHeight}";
}
