//! Jellyfin → domain translation.
//!
//! The ONLY place Jellyfin's vocabulary touches the domain model. Adding a
//! second provider later means a sibling `from_<provider>.rs`; the domain types
//! and every consumer stay untouched.
//!
//! Spec: docs/specs/frontend-domain-model.md

use super::media::{MediaKind, StreamKind};

/// Classify a Jellyfin media-stream `Type` string into a neutral [`StreamKind`].
/// Total and panic-free.
pub fn stream_kind_from_jellyfin(stream_type: &str) -> StreamKind {
    match stream_type {
        "Audio" => StreamKind::Audio,
        "Video" => StreamKind::Video,
        "Subtitle" => StreamKind::Subtitle,
        _ => StreamKind::Other,
    }
}

/// Jellyfin ticks per second (10 million). A tick is 100 ns.
/// The frontend must never see ticks — this is where they die.
const TICKS_PER_MILLISECOND: i64 = 10_000;

/// Convert a Jellyfin `RunTimeTicks` value to milliseconds.
///
/// Domain durations are milliseconds; ticks are a Jellyfin unit and stop here.
pub fn ticks_to_ms(ticks: i64) -> i64 {
    ticks / TICKS_PER_MILLISECOND
}

/// Classify a Jellyfin `Type` string into a neutral [`MediaKind`].
///
/// **Total and panic-free**: any unrecognised string maps to [`MediaKind::Other`]
/// rather than failing. `is_folder` disambiguates the one Jellyfin type
/// (`ChannelFolderItem`) whose kind depends on whether it is a container.
///
/// The recognised set is every `item_type` the frontend audit found in use
/// (docs/specs/frontend-domain-model.md), plus the common cast/crew person
/// subtypes Jellyfin returns in `People[].Type`.
pub fn kind_from_jellyfin(item_type: &str, is_folder: bool) -> MediaKind {
    match item_type {
        // Music
        "Audio" | "MusicVideo" => MediaKind::Track,
        "MusicAlbum" => MediaKind::Album,
        "MusicArtist" | "AlbumArtist" => MediaKind::Artist,
        "Playlist" => MediaKind::Playlist,

        // Video
        "Movie" => MediaKind::Movie,
        "Series" => MediaKind::Series,
        "Season" => MediaKind::Season,
        "Episode" => MediaKind::Episode,
        // A bare video leaf with no richer classification.
        "Video" => MediaKind::Movie,

        // Cast / crew — Jellyfin uses both a "Person" item type and role-typed
        // people (Actor/Director/Writer/Composer/…) in People[].Type.
        "Person" | "Actor" | "Director" | "Writer" | "Composer" | "GuestStar" | "Producer" => {
            MediaKind::Person
        }

        // A live TV channel: playable, but a non-seekable live stream.
        "TvChannel" | "LiveTvChannel" => MediaKind::LiveChannel,
        // A bare channel is a container the user drills into.
        "Channel" => MediaKind::Channel,

        // Containers
        "Folder" | "CollectionFolder" | "UserView" | "BoxSet" => MediaKind::Folder,
        // ChannelFolderItem is a container when it is a folder, else a playable
        // channel leaf (distinct kind so the UI can route it to playback).
        "ChannelFolderItem" => {
            if is_folder {
                MediaKind::Folder
            } else {
                MediaKind::ChannelItem
            }
        }

        // Unknown → safe sink. Never panics.
        _ => {
            if is_folder {
                MediaKind::Folder
            } else {
                MediaKind::Other
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ticks_convert_to_milliseconds() {
        // 1 second = 10,000,000 ticks = 1000 ms
        assert_eq!(ticks_to_ms(10_000_000), 1000);
        // 90.5 s
        assert_eq!(ticks_to_ms(905_000_000), 90_500);
        assert_eq!(ticks_to_ms(0), 0);
        // Sub-millisecond truncates toward zero, not panics.
        assert_eq!(ticks_to_ms(9_999), 0);
    }

    #[test]
    fn music_types_map() {
        assert_eq!(kind_from_jellyfin("Audio", false), MediaKind::Track);
        assert_eq!(kind_from_jellyfin("MusicAlbum", true), MediaKind::Album);
        assert_eq!(kind_from_jellyfin("MusicArtist", true), MediaKind::Artist);
        assert_eq!(kind_from_jellyfin("Playlist", true), MediaKind::Playlist);
    }

    #[test]
    fn video_types_map() {
        assert_eq!(kind_from_jellyfin("Movie", false), MediaKind::Movie);
        assert_eq!(kind_from_jellyfin("Series", true), MediaKind::Series);
        assert_eq!(kind_from_jellyfin("Season", true), MediaKind::Season);
        assert_eq!(kind_from_jellyfin("Episode", false), MediaKind::Episode);
        assert_eq!(kind_from_jellyfin("Video", false), MediaKind::Movie);
    }

    #[test]
    fn person_and_role_types_map_to_person() {
        for t in ["Person", "Actor", "Director", "Writer", "Composer"] {
            assert_eq!(kind_from_jellyfin(t, false), MediaKind::Person, "{t}");
        }
    }

    #[test]
    fn channel_and_container_types_map() {
        assert_eq!(
            kind_from_jellyfin("TvChannel", false),
            MediaKind::LiveChannel
        );
        assert_eq!(kind_from_jellyfin("Channel", false), MediaKind::Channel);
        assert_eq!(
            kind_from_jellyfin("CollectionFolder", true),
            MediaKind::Folder
        );
        assert_eq!(kind_from_jellyfin("BoxSet", true), MediaKind::Folder);
    }

    #[test]
    fn channel_folder_item_disambiguates_on_is_folder() {
        assert_eq!(
            kind_from_jellyfin("ChannelFolderItem", true),
            MediaKind::Folder
        );
        assert_eq!(
            kind_from_jellyfin("ChannelFolderItem", false),
            MediaKind::ChannelItem
        );
    }

    #[test]
    fn stream_kinds_map() {
        assert_eq!(stream_kind_from_jellyfin("Audio"), StreamKind::Audio);
        assert_eq!(stream_kind_from_jellyfin("Video"), StreamKind::Video);
        assert_eq!(stream_kind_from_jellyfin("Subtitle"), StreamKind::Subtitle);
        assert_eq!(
            stream_kind_from_jellyfin("EmbeddedImage"),
            StreamKind::Other
        );
        assert_eq!(stream_kind_from_jellyfin(""), StreamKind::Other);
    }

    #[test]
    fn unknown_type_never_panics_and_falls_back() {
        // The whole point: garbage in, safe kind out, no panic.
        assert_eq!(kind_from_jellyfin("Epis0de", false), MediaKind::Other);
        assert_eq!(kind_from_jellyfin("", false), MediaKind::Other);
        assert_eq!(
            kind_from_jellyfin("SomeFutureType", true),
            MediaKind::Folder
        );
        assert_eq!(kind_from_jellyfin("🎵unicode", false), MediaKind::Other);
    }
}
