using DeepDrftModels.DTOs;
using DeepDrftModels.Enums;

namespace DeepDrftPublic.Client.Common;

/// <summary>
/// The OG <c>og:type</c> for a page. Releases map per medium (§3.4); everything else is a website.
/// </summary>
public enum SeoOgType
{
    Website,
    MusicAlbum,
    MusicSong,
}

/// <summary>
/// The typed per-page SEO input (Phase 22). A page hands <c>SeoHead</c> one model instead of ~15 loose
/// parameters; the named factories below encode the per-page / per-medium mapping (title, description,
/// canonical path, og:type, JSON-LD node) in exactly one place each (DRY, §4.1/§4.2). The factories are
/// pure functions over DTOs the page already holds — unit-testable without rendering.
///
/// <para>
/// <see cref="CanonicalPath"/> is site-relative; <c>SeoHead</c> absolutises it against
/// <see cref="SeoOptions.BaseUrl"/>. Release pages pass <see cref="ReleaseRoutes.DetailHref"/> so the
/// canonical is the dedicated route regardless of alias/query routes (AC7). A null cover means the model
/// carries no <see cref="ImagePath"/> and <c>SeoHead</c> falls back to the default OG image (C6/AC4).
/// </para>
/// </summary>
public sealed record SeoModel
{
    /// <summary>Bare page title, no site suffix. <c>SeoHead</c> composes <c>"{Title} · {suffix}"</c>.</summary>
    public required string Title { get; init; }

    /// <summary>Meta/OG description. Null falls back to <see cref="SeoOptions.DefaultDescription"/>.</summary>
    public string? Description { get; init; }

    /// <summary>Site-relative canonical path. Null defaults to the current path in <c>SeoHead</c>.</summary>
    public string? CanonicalPath { get; init; }

    /// <summary>Relative cover <c>ImagePath</c>. Null → the default OG image.</summary>
    public string? ImagePath { get; init; }

    public SeoOgType OgType { get; init; } = SeoOgType.Website;

    /// <summary>Robots directive. Null falls back to <see cref="SeoOptions.DefaultRobots"/>.</summary>
    public string? Robots { get; init; }

    /// <summary>Pre-serialised JSON-LD script body, or null to emit no structured-data script.</summary>
    public string? JsonLd { get; init; }

    // --- Music-vertical OG, release pages only (null elsewhere → tags omitted) ---
    public string? Artist { get; init; }
    public DateOnly? ReleaseDate { get; init; }
    public double? DurationSeconds { get; init; }

    // ------------------------------------------------------------------ Factories

    /// <summary>Home page: the collective entity (MusicGroup JSON-LD), site-level OG.</summary>
    public static SeoModel ForHome(SeoOptions options) => new()
    {
        Title = "Electronic Music Collective",
        Description = options.DefaultDescription,
        CanonicalPath = "/",
        OgType = SeoOgType.Website,
        JsonLd = SeoJsonLd.Serialize(MusicGroup(options)),
    };

    /// <summary>About page: the collective again, with the bio lede as description.</summary>
    public static SeoModel ForAbout(SeoOptions options) => new()
    {
        Title = "The Collective",
        Description =
            "Two people, many hats. Deep DRFT brings the heart and soul of Midwest deep house to " +
            "Charleston — informed by the founders of the style, and promising to push it forward.",
        CanonicalPath = "/about",
        OgType = SeoOgType.Website,
        JsonLd = SeoJsonLd.Serialize(MusicGroup(options) with
        {
            Description =
                "Two people, many hats. Deep DRFT brings the heart and soul of Midwest deep house to " +
                "Charleston — informed by the founders of the style, and promising to push it forward.",
        }),
    };

    /// <summary>A browse surface: <c>CollectionPage</c> JSON-LD, website OG.</summary>
    public static SeoModel ForBrowse(SeoOptions options, ReleaseMedium? medium, string path)
    {
        var (title, description) = BrowseCopy(medium);
        return new SeoModel
        {
            Title = title,
            Description = description,
            CanonicalPath = path,
            OgType = SeoOgType.Website,
            JsonLd = SeoJsonLd.Serialize(new CollectionPageNode
            {
                Name = title,
                Description = description,
                Url = SeoUrls.Absolute(options, path),
            }),
        };
    }

    /// <summary>The 404 page: no canonical, <c>noindex,follow</c>, no JSON-LD.</summary>
    public static SeoModel ForNotFound(SeoOptions options) => new()
    {
        Title = "Not Found",
        Description = options.DefaultDescription,
        Robots = "noindex,follow",
        OgType = SeoOgType.Website,
    };

    /// <summary>
    /// A release detail page. The medium picks the schema (cut/session → MusicAlbum, mix → MusicRecording),
    /// the og:type, and the music-vertical OG fields; the canonical is the dedicated route. The optional
    /// <paramref name="tracks"/> seed the album's ordered <c>track</c> list (cut). <b>One call site, all tags.</b>
    /// </summary>
    public static SeoModel ForRelease(SeoOptions options, ReleaseDto release, IReadOnlyList<TrackDto>? tracks = null)
    {
        var canonicalPath = ReleaseRoutes.DetailHref(release.EntryKey, release.Medium);
        var image = SeoUrls.CoverOrDefault(options, release.ImagePath);
        // byArtist reflects the release's own artist, consistent with the music:musician OG tag (Daniel's
        // call) — not the collective name. Album sub-recordings share it: the tracks are by this artist.
        var artist = new ArtistRef { Name = release.Artist };
        var description = string.IsNullOrWhiteSpace(release.Description) ? options.DefaultDescription : release.Description;

        // A mix is a single recording; its duration comes from the (single) track when present.
        var mixDurationSeconds = release.Medium == ReleaseMedium.Mix
            ? tracks?.FirstOrDefault()?.DurationSeconds
            : null;

        JsonLdNode node = release.Medium switch
        {
            ReleaseMedium.Mix => new MusicRecordingNode
            {
                Name = release.Title,
                ByArtist = artist,
                Duration = SeoUrls.IsoDuration(mixDurationSeconds),
                Genre = release.Genre,
                Image = image,
                Url = SeoUrls.Absolute(options, canonicalPath),
            },
            // Cut and Session are both albums; the production type distinguishes a live session.
            _ => new MusicAlbumNode
            {
                Name = release.Title,
                ByArtist = artist,
                AlbumProductionType = release.Medium == ReleaseMedium.Session
                    ? "https://schema.org/LiveAlbum"
                    : "https://schema.org/StudioAlbum",
                DatePublished = release.ReleaseDate?.ToString("yyyy-MM-dd"),
                Genre = release.Genre,
                Image = image,
                Url = SeoUrls.Absolute(options, canonicalPath),
                Track = AlbumTracks(options, artist, tracks),
            },
        };

        return new SeoModel
        {
            Title = release.Title,
            Description = description,
            CanonicalPath = canonicalPath,
            ImagePath = release.ImagePath,
            OgType = release.Medium == ReleaseMedium.Mix ? SeoOgType.MusicSong : SeoOgType.MusicAlbum,
            Artist = release.Artist,
            ReleaseDate = release.ReleaseDate,
            DurationSeconds = mixDurationSeconds,
            JsonLd = SeoJsonLd.Serialize(node),
        };
    }

    // The collective entity, built once from config — the home/about JSON-LD root.
    private static MusicGroupNode MusicGroup(SeoOptions options) => new()
    {
        Name = options.SiteName,
        Url = SeoUrls.Absolute(options, "/"),
        Genre = options.Genre,
        Description = options.DefaultDescription,
        Logo = SeoUrls.Absolute(options, options.DefaultImageUrl),
        SameAs = options.SameAs.Count > 0 ? options.SameAs : null,
    };

    // Ordered recording list for an album's `track` property. Null when there are no tracks so the
    // property is omitted rather than emitting an empty array (C6).
    private static IReadOnlyList<MusicRecordingNode>? AlbumTracks(
        SeoOptions options, ArtistRef artist, IReadOnlyList<TrackDto>? tracks)
    {
        if (tracks is null || tracks.Count == 0) return null;

        return tracks
            .OrderBy(t => t.TrackNumber)
            .Select(t => new MusicRecordingNode
            {
                Name = t.TrackName,
                ByArtist = artist,
                Duration = SeoUrls.IsoDuration(t.DurationSeconds),
            })
            .ToList();
    }

    private static (string Title, string Description) BrowseCopy(ReleaseMedium? medium) => medium switch
    {
        ReleaseMedium.Cut => ("Cuts", "Studio cuts from Deep DRFT — composed, layered, and finished."),
        ReleaseMedium.Session => ("Sessions", "Live sessions from Deep DRFT — performances caught in the moment, unrepeatable and unedited."),
        ReleaseMedium.Mix => ("Mixes", "DJ mixes from Deep DRFT — uninterrupted sets, one track bleeding into the next."),
        _ => ("Archive", "The full Deep DRFT catalogue — cuts, sessions, and mixes, indexed and always expanding."),
    };
}
