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

namespace DeepDrftPublic.Client.Common;

/// <summary>
/// Typed schema.org JSON-LD nodes (Phase 22, OQ5 — the typed-builder option). Each record mirrors one
/// schema.org type; <see cref="SeoJsonLd.Serialize"/> renders a node to the <c>&lt;script type="application/ld+json"&gt;</c>
/// body. Keeping the shape in C# (not hand-written JSON in pages) is what makes the medium→type mapping
/// live in one place (DRY, §4.3) and the output unit-testable (AC5) rather than a manual validator pass.
///
/// <para>
/// All nodes share <see cref="JsonLdNode"/> so the <c>@context</c>/<c>@type</c> pair serialises first and
/// once. Null properties are omitted (the serializer ignores nulls) so partial data never emits an empty
/// or broken node (C6/AC4).
/// </para>
/// </summary>
public static class SeoJsonLd
{
    private static readonly JsonSerializerOptions Options = new()
    {
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        // schema.org keys are PascalCase ("@type", "byArtist", "datePublished"); JsonPropertyName drives
        // each. Encoder relaxed so the JSON sits inline in HTML without over-escaping apostrophes etc.
        // Note: the relaxed encoder leaves <, >, & raw — InlineSafe re-escapes exactly those before the
        // body is injected into the <script> element. See Serialize.
        Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
        WriteIndented = false,
    };

    /// <summary>
    /// Renders a node to its compact JSON-LD script body. The host component wraps it in the script tag.
    /// The body is run through <see cref="InlineSafe"/> so CMS-authored values containing
    /// <c>&lt;/script&gt;</c> or <c>&lt;</c> cannot break out of the inline script element (XSS).
    /// </summary>
    public static string Serialize<TNode>(TNode node) where TNode : JsonLdNode =>
        InlineSafe(JsonSerializer.Serialize(node, node.GetType(), Options));

    /// <summary>
    /// Escapes the three characters that can break out of an inline <c>&lt;script type="application/ld+json"&gt;</c>
    /// element. Replacing <c>&lt;</c>/<c>&gt;</c>/<c>&amp;</c> with their <c>\uXXXX</c> JSON escapes keeps the
    /// JSON byte-for-byte equivalent on parse (a JSON string treats <c><</c> and <c>&lt;</c> identically)
    /// while making <c>&lt;/script&gt;</c> impossible to emit raw — the documented safe pattern for inline JSON-LD.
    /// </summary>
    internal static string InlineSafe(string json) => json
        .Replace("<", "\\u003C")
        .Replace(">", "\\u003E")
        .Replace("&", "\\u0026");
}

/// <summary>Base for every schema.org node: emits <c>@context</c> and <c>@type</c> first.</summary>
public abstract record JsonLdNode
{
    [JsonPropertyName("@context")]
    [JsonPropertyOrder(-2)]
    public string Context => "https://schema.org";

    [JsonPropertyName("@type")]
    [JsonPropertyOrder(-1)]
    public abstract string Type { get; }
}

/// <summary>The Deep DRFT collective entity — the home/about node.</summary>
public sealed record MusicGroupNode : JsonLdNode
{
    [JsonPropertyName("@type")] public override string Type => "MusicGroup";

    [JsonPropertyName("name")] public string Name { get; init; } = string.Empty;
    [JsonPropertyName("url")] public string? Url { get; init; }
    [JsonPropertyName("genre")] public string? Genre { get; init; }
    [JsonPropertyName("description")] public string? Description { get; init; }
    [JsonPropertyName("logo")] public string? Logo { get; init; }
    [JsonPropertyName("sameAs")] public IReadOnlyList<string>? SameAs { get; init; }
}

/// <summary>A studio cut or a live session release. <c>AlbumProductionType</c> distinguishes them.</summary>
public sealed record MusicAlbumNode : JsonLdNode
{
    [JsonPropertyName("@type")] public override string Type => "MusicAlbum";

    [JsonPropertyName("name")] public string Name { get; init; } = string.Empty;
    [JsonPropertyName("byArtist")] public ArtistRef? ByArtist { get; init; }

    /// <summary>schema.org <c>MusicAlbumProductionType</c> URI, e.g. <c>StudioAlbum</c> or <c>LiveAlbum</c>.</summary>
    [JsonPropertyName("albumProductionType")] public string? AlbumProductionType { get; init; }

    [JsonPropertyName("datePublished")] public string? DatePublished { get; init; }
    [JsonPropertyName("genre")] public string? Genre { get; init; }
    [JsonPropertyName("image")] public string? Image { get; init; }
    [JsonPropertyName("url")] public string? Url { get; init; }

    /// <summary>Ordered list of the album's recordings (cut track list, in TrackNumber order).</summary>
    [JsonPropertyName("track")] public IReadOnlyList<MusicRecordingNode>? Track { get; init; }
}

/// <summary>A single recording — a mix release, or one track inside an album's <c>track</c> list.</summary>
public sealed record MusicRecordingNode : JsonLdNode
{
    [JsonPropertyName("@type")] public override string Type => "MusicRecording";

    [JsonPropertyName("name")] public string Name { get; init; } = string.Empty;
    [JsonPropertyName("byArtist")] public ArtistRef? ByArtist { get; init; }

    /// <summary>ISO-8601 duration (e.g. <c>PT1H2M3S</c>) from <c>DurationSeconds</c>.</summary>
    [JsonPropertyName("duration")] public string? Duration { get; init; }

    [JsonPropertyName("genre")] public string? Genre { get; init; }
    [JsonPropertyName("image")] public string? Image { get; init; }
    [JsonPropertyName("url")] public string? Url { get; init; }
}

/// <summary>A browse/index surface listing releases (cuts/sessions/mixes/archive).</summary>
public sealed record CollectionPageNode : JsonLdNode
{
    [JsonPropertyName("@type")] public override string Type => "CollectionPage";

    [JsonPropertyName("name")] public string Name { get; init; } = string.Empty;
    [JsonPropertyName("description")] public string? Description { get; init; }
    [JsonPropertyName("url")] public string? Url { get; init; }
}

/// <summary>A nested <c>byArtist</c> reference — the collective as a MusicGroup, by name.</summary>
public sealed record ArtistRef
{
    [JsonPropertyName("@type")] public string Type => "MusicGroup";
    [JsonPropertyName("name")] public string Name { get; init; } = string.Empty;
}
