/*
    This file is part of jellything (https://codeberg.org/metamuffin/jellything)
    which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
    Copyright (C) 2026 metamuffin <metamuffin.org>
*/
pub mod models;

use crate::{helper::A, ui::error::MyResult};
use anyhow::anyhow;
use jellycommon::{
    api::{NodeFilterSort, SortOrder, SortProperty},
    routes::{u_asset, u_node_image},
    stream::{StreamContainer, StreamSpec},
    user::{NodeUserData, WatchedState},
    MediaInfo, Node, NodeID, NodeKind, PictureSlot, SourceTrack, SourceTrackKind, Visibility,
};
use jellylogic::{
    login::login_logic,
    node::{get_node, update_node_userdata_watched_progress},
    search::search,
    session::Session,
};
use jellyui::{get_brand, get_slogan, node_page::aspect_class};
use log::warn;
use models::*;
use rocket::{
    get,
    http::{Cookie, CookieJar},
    post,
    response::Redirect,
    serde::json::Json,
    FromForm,
};
use serde::Deserialize;
use serde_json::{json, Value};
use std::{collections::BTreeMap, net::IpAddr};

// these are both random values. idk what they are for
const SERVER_ID: &str = "1694a95daf70708147f16103ce7b7566";
const USER_ID: &str = "33f772aae6c2495ca89fe00340dbd17c";

const VERSION: &str = "10.10.0";
const LOCAL_ADDRESS: &str = "http://127.0.0.1:8000"; // TODO

#[get("/System/Info/Public")]
pub fn r_jellyfin_system_info_public_case() -> Json<Value> {
    r_jellyfin_system_info_public()
}

#[get("/system/info/public")]
pub fn r_jellyfin_system_info_public() -> Json<Value> {
    Json(json!({
        "LocalAddress": LOCAL_ADDRESS,
        "ServerName": get_brand(),
        "Version": VERSION,
        "ProductName": "Jellything",
        "OperatingSystem": "",
        "Id": SERVER_ID,
        "StartupWizardCompleted": true,
    }))
}

#[get("/Branding/Configuration")]
pub fn r_jellyfin_branding_configuration() -> Json<Value> {
    Json(json!({
        "LoginDisclaimer": format!("{} - {}", get_brand(), get_slogan()),
        "CustomCss": "",
        "SplashscreenEnabled": false,
    }))
}

#[get("/users/public")]
pub fn r_jellyfin_users_public() -> Json<Value> {
    Json(json!([]))
}

#[get("/Branding/Css")]
pub fn r_jellyfin_branding_css() -> String {
    "".to_string()
}

#[get("/QuickConnect/Enabled")]
pub fn r_jellyfin_quickconnect_enabled() -> Json<Value> {
    Json(json!(false))
}

#[get("/System/Endpoint")]
pub fn r_jellyfin_system_endpoint(_session: A<Session>) -> Json<Value> {
    Json(json!({
        "IsLocal": false,
        "IsInNetwork": false,
    }))
}

use rocket_ws::{Message, Stream, WebSocket};
#[get("/socket")]
pub fn r_jellyfin_socket(_session: A<Session>, ws: WebSocket) -> Stream!['static] {
    Stream! { ws =>
        for await message in ws {
            eprintln!("{message:?}");
        }
        yield Message::Text("test".to_string())
    }
}

#[get("/System/Info")]
pub fn r_jellyfin_system_info(_session: A<Session>) -> Json<Value> {
    Json(json!({
        "OperatingSystemDisplayName": "",
        "HasPendingRestart": false,
        "IsShuttingDown": false,
        "SupportsLibraryMonitor": true,
        "WebSocketPortNumber": 8096,
        "CompletedInstallations": [],
        "CanSelfRestart": true,
        "CanLaunchWebBrowser": false,
        "ProgramDataPath": "/path/to/data",
        "WebPath": "/path/to/web",
        "ItemsByNamePath": "/path/to/items",
        "CachePath": "/path/to/cache",
        "LogPath": "/path/to/log",
        "InternalMetadataPath": "/path/to/metadata",
        "TranscodingTempPath": "/path/to/transcodes",
        "CastReceiverApplications": [],
        "HasUpdateAvailable": false,
        "EncoderLocation": "System",
        "SystemArchitecture": "X64",
        "LocalAddress": LOCAL_ADDRESS,
        "ServerName": get_brand(),
        "Version": VERSION,
        "OperatingSystem": "",
        "Id": SERVER_ID
    }))
}

#[get("/DisplayPreferences/usersettings")]
pub fn r_jellyfin_displaypreferences_usersettings(_session: A<Session>) -> Json<Value> {
    Json(json!({
        "Id": "3ce5b65d-e116-d731-65d1-efc4a30ec35c",
        "SortBy": "SortName",
        "RememberIndexing": false,
        "PrimaryImageHeight": 250,
        "PrimaryImageWidth": 250,
        "CustomPrefs": false,
        "ScrollDirection": "Horizontal",
        "ShowBackdrop": true,
        "RememberSorting": false,
        "SortOrder": "Ascending",
        "ShowSidebar": false,
        "Client": "emby",
    }))
}

#[post("/DisplayPreferences/usersettings")]
pub fn r_jellyfin_displaypreferences_usersettings_post(_session: A<Session>) {}

#[get("/Users/<id>")]
pub fn r_jellyfin_users_id(session: A<Session>, id: &str) -> Json<Value> {
    let _ = id;
    Json(user_object(session.0.user.name))
}

#[get("/Items/<id>/Images/Primary?<fillWidth>&<tag>")]
#[allow(non_snake_case)]
pub fn r_jellyfin_items_image_primary(
    _session: A<Session>,
    id: &str,
    fillWidth: Option<usize>,
    tag: String,
) -> Redirect {
    if tag == "poster" {
        Redirect::permanent(u_node_image(
            id,
            PictureSlot::Cover,
            fillWidth.unwrap_or(1024),
        ))
    } else {
        Redirect::permanent(u_asset(&tag, fillWidth.unwrap_or(1024)))
    }
}

#[get("/Items/<id>/Images/Backdrop/0?<maxWidth>")]
#[allow(non_snake_case)]
pub fn r_jellyfin_items_images_backdrop(
    _session: A<Session>,
    id: &str,
    maxWidth: Option<usize>,
) -> Redirect {
    Redirect::permanent(u_node_image(
        id,
        PictureSlot::Backdrop,
        maxWidth.unwrap_or(1024),
    ))
}

#[get("/Items/<id>")]
#[allow(private_interfaces)]
pub fn r_jellyfin_items_item(session: A<Session>, id: &str) -> MyResult<Json<JellyfinItem>> {
    let r = get_node(
        &session.0,
        NodeID::from_slug(id),
        false,
        false,
        NodeFilterSort::default(),
    )?;
    Ok(Json(item_object(&r.node, &r.userdata)))
}

#[get("/Users/<uid>/Items/<id>")]
#[allow(private_interfaces)]
pub fn r_jellyfin_users_items_item(
    session: A<Session>,
    uid: &str,
    id: &str,
) -> MyResult<Json<JellyfinItem>> {
    let _ = uid;
    r_jellyfin_items_item(session, id)
}

#[derive(Debug, FromForm)]
#[allow(unused)] // TODO
struct JellyfinItemQuery {
    #[field(name = uncased("searchterm"))]
    search_term: Option<String>,
    #[field(name = uncased("limit"))]
    limit: usize,
    #[field(name = uncased("parentid"))]
    parent_id: Option<String>,
    #[field(name = uncased("startindex"))]
    start_index: Option<usize>,
    #[field(name = uncased("includeitemtypes"))]
    include_item_types: Option<String>,

    internal_artists: bool,
    internal_persons: bool,
}

#[get("/Users/<uid>/Items?<query..>")]
#[allow(private_interfaces)]
pub fn r_jellyfin_users_items(
    session: A<Session>,
    uid: &str,
    query: JellyfinItemQuery,
) -> MyResult<Json<JellyfinItemsResponse>> {
    let _ = uid;
    r_jellyfin_items(session, query)
}

#[get("/Artists?<query..>")]
#[allow(private_interfaces)]
pub fn r_jellyfin_artists(
    session: A<Session>,
    mut query: JellyfinItemQuery,
) -> MyResult<Json<JellyfinItemsResponse>> {
    query.internal_artists = true;
    r_jellyfin_items(session, query)?; // TODO
    Ok(Json(JellyfinItemsResponse::default()))
}

#[get("/Persons?<query..>")]
#[allow(private_interfaces)]
pub fn r_jellyfin_persons(
    session: A<Session>,
    mut query: JellyfinItemQuery,
) -> MyResult<Json<JellyfinItemsResponse>> {
    query.internal_persons = true;
    r_jellyfin_items(session, query)?; // TODO
    Ok(Json(JellyfinItemsResponse::default()))
}

#[get("/Items?<query..>")]
#[allow(private_interfaces)]
pub fn r_jellyfin_items(
    session: A<Session>,
    query: JellyfinItemQuery,
) -> MyResult<Json<JellyfinItemsResponse>> {
    // let (nodes, parent_kind) = if let Some(q) = query.search_term {
    //     (
    //         database
    //             .search(&q, query.limit, query.start_index.unwrap_or_default())?
    //             .1,
    //         None,
    //     )
    // } else if let Some(parent) = query.parent_id {
    //     let parent = NodeID::from_slug(&parent);
    //     (
    //         database
    //             .get_node_children(parent)?
    //             .into_iter()
    //             .skip(query.start_index.unwrap_or_default())
    //             .take(query.limit)
    //             .collect(),
    //         database.get_node(parent)?.map(|n| n.kind),
    //     )
    // } else {
    //     (vec![], None)
    // };

    // let filter_kind = query
    //     .include_item_types
    //     .map(|n| match n.as_str() {
    //         "Movie" => vec![FilterProperty::KindMovie],
    //         "Audio" => vec![FilterProperty::KindMusic],
    //         "Video" => vec![FilterProperty::KindVideo],
    //         "TvChannel" => vec![FilterProperty::KindChannel],
    //         _ => vec![],
    //     })
    //     .or(if query.internal_artists {
    //         Some(vec![])
    //     } else {
    //         None
    //     })
    //     .or(if query.internal_persons {
    //         Some(vec![])
    //     } else {
    //         None
    //     });

    // let mut nodes = nodes
    //     .into_iter()
    //     .map(|nid| database.get_node_with_userdata(nid, &session.0))
    //     .collect::<Result<Vec<_>, anyhow::Error>>()?;

    // filter_and_sort_nodes(
    //     &NodeFilterSort {
    //         sort_by: None,
    //         filter_kind,
    //         sort_order: None,
    //     },
    //     match parent_kind {
    //         Some(NodeKind::Channel) => (SortProperty::ReleaseDate, SortOrder::Descending),
    //         _ => (SortProperty::Title, SortOrder::Ascending),
    //     },
    //     &mut nodes,
    // );

    let nodes = if let Some(q) = query.search_term {
        search(&session.0, &q, query.start_index.map(|x| x / 50))?.results // TODO
    } else if let Some(parent) = query.parent_id {
        get_node(
            &session.0,
            NodeID::from_slug(&parent),
            true,
            false,
            NodeFilterSort::default(),
        )?
        .children
    } else {
        warn!("unknown items request");
        vec![]
    };

    // TODO reimplemnt filter behaviour

    let items = nodes
        .into_iter()
        .filter(|(n, _)| n.visibility >= Visibility::Reduced)
        .map(|(n, ud)| item_object(&n, &ud))
        .collect::<Vec<_>>();

    Ok(Json(JellyfinItemsResponse {
        total_record_count: items.len(),
        start_index: query.start_index.unwrap_or_default(),
        items,
    }))
}

#[get("/UserViews?<userId>")]
#[allow(non_snake_case)]
pub fn r_jellyfin_users_views(session: A<Session>, userId: &str) -> MyResult<Json<Value>> {
    let _ = userId;

    let items = get_node(
        &session.0,
        NodeID::from_slug("library"),
        false,
        true,
        NodeFilterSort {
            sort_by: Some(SortProperty::Index),
            sort_order: Some(SortOrder::Ascending),
            filter_kind: None,
        },
    )?
    .children
    .into_iter()
    .map(|(node, udata)| item_object(&node, &udata))
    .collect::<Vec<_>>();

    Ok(Json(json!({
        "Items": items,
        "TotalRecordCount": items.len(),
        "StartIndex": 0
    })))
}

#[get("/Items/<id>/Similar")]
pub fn r_jellyfin_items_similar(_session: A<Session>, id: &str) -> Json<Value> {
    let _ = id;
    Json(json!({
        "Items": [],
        "TotalRecordCount": 0,
        "StartIndex": 0
    }))
}

#[get("/LiveTv/Programs/Recommended")]
pub fn r_jellyfin_livetv_programs_recommended(_session: A<Session>) -> Json<Value> {
    Json(json!({
        "Items": [],
        "TotalRecordCount": 0,
        "StartIndex": 0
    }))
}

#[get("/Users/<uid>/Items/<id>/Intros")]
pub fn r_jellyfin_items_intros(_session: A<Session>, uid: &str, id: &str) -> Json<Value> {
    let _ = (uid, id);
    Json(json!({
        "Items": [],
        "TotalRecordCount": 0,
        "StartIndex": 0
    }))
}

#[get("/Shows/NextUp")]
pub fn r_jellyfin_shows_nextup(_session: A<Session>) -> Json<Value> {
    Json(json!({
        "Items": [],
        "TotalRecordCount": 0,
        "StartIndex": 0
    }))
}

#[post("/Items/<id>/PlaybackInfo")]
pub fn r_jellyfin_items_playbackinfo(session: A<Session>, id: &str) -> MyResult<Json<Value>> {
    let node = get_node(
        &session.0,
        NodeID::from_slug(id),
        false,
        false,
        NodeFilterSort::default(),
    )?
    .node;
    let media = node.media.as_ref().ok_or(anyhow!("node has no media"))?;
    let ms = media_source_object(&node, media);
    Ok(Json(json!({
        "MediaSources": [ms],
        "PlaySessionId": "why do we need this id?"
    })))
}

#[get("/Videos/<id>/stream.webm")]
pub fn r_jellyfin_video_stream(session: A<Session>, id: &str) -> MyResult<Redirect> {
    let node = get_node(
        &session.0,
        NodeID::from_slug(id),
        false,
        false,
        NodeFilterSort::default(),
    )?
    .node;
    let media = node.media.as_ref().ok_or(anyhow!("node has no media"))?;
    let params = StreamSpec::Remux {
        tracks: (0..media.tracks.len()).collect(),
        container: StreamContainer::WebM,
    }
    .to_query();
    Ok(Redirect::temporary(format!("/n/{id}/stream{params}")))
}

#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct JellyfinProgressData {
    item_id: String,
    position_ticks: f64,
}
#[post("/Sessions/Playing/Progress", data = "<data>")]
#[allow(private_interfaces)]
pub fn r_jellyfin_sessions_playing_progress(
    session: A<Session>,
    data: Json<JellyfinProgressData>,
) -> MyResult<()> {
    let position = data.position_ticks / 10_000_000.;
    update_node_userdata_watched_progress(&session.0, NodeID::from_slug(&data.item_id), position)?;
    Ok(())
}

#[post("/Sessions/Playing")]
pub fn r_jellyfin_sessions_playing(_session: A<Session>) {}

#[get("/Playback/BitrateTest?<Size>")]
#[allow(non_snake_case)]
pub fn r_jellyfin_playback_bitratetest(_session: A<Session>, Size: usize) -> Vec<u8> {
    vec![0; Size.min(1_000_000)]
}

#[post("/Sessions/Capabilities/Full")]
pub fn r_jellyfin_sessions_capabilities_full(_session: A<Session>) {}

#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct AuthData {
    pw: String,
    username: String,
}

#[post("/Users/AuthenticateByName", data = "<data>")]
#[allow(private_interfaces)]
pub fn r_jellyfin_users_authenticatebyname_case(
    client_addr: IpAddr,
    data: Json<AuthData>,
    jar: &CookieJar,
) -> MyResult<Json<Value>> {
    r_jellyfin_users_authenticatebyname(client_addr, data, jar)
}

#[post("/Users/authenticatebyname", data = "<data>")]
#[allow(private_interfaces)]
pub fn r_jellyfin_users_authenticatebyname(
    client_addr: IpAddr,
    data: Json<AuthData>,
    jar: &CookieJar,
) -> MyResult<Json<Value>> {
    let token = login_logic(&data.username, &data.pw, None, None)?;

    // setting the session cookie too because image requests carry no auth headers for some reason.
    // TODO find alternative, non-web clients might not understand cookies
    jar.add(
        Cookie::build(("session", token.clone()))
            .permanent()
            .build(),
    );

    Ok(Json(json!({
        "User": user_object(data.username.clone()),
        "SessionInfo": {
            "PlayState": {
                "CanSeek": false,
                "IsPaused": false,
                "IsMuted": false,
                "RepeatMode": "RepeatNone",
                "PlaybackOrder": "Default"
            },
            "AdditionalUsers": [],
            "Capabilities": {
                "PlayableMediaTypes": [
                    "Audio",
                    "Video"
                ],
                "SupportedCommands": [],
                // "MoveUp", "MoveDown", "MoveLeft", "MoveRight", "PageUp", "PageDown", "PreviousLetter", "NextLetter", "ToggleOsd", "ToggleContextMenu", "Select", "Back", "SendKey", "SendString", "GoHome", "GoToSettings", "VolumeUp", "VolumeDown", "Mute", "Unmute", "ToggleMute", "SetVolume", "SetAudioStreamIndex", "SetSubtitleStreamIndex", "DisplayContent", "GoToSearch", "DisplayMessage", "SetRepeatMode", "SetShuffleQueue", "ChannelUp", "ChannelDown", "PlayMediaSource", "PlayTrailers"
                "SupportsMediaControl": true,
                "SupportsPersistentIdentifier": false
            },
            "RemoteEndPoint": client_addr,
            "PlayableMediaTypes": [
                "Audio",
                "Video"
            ],
            "Id": "6e05fbb4fe33477b991455c97a57e25d",
            "UserId": USER_ID,
            "UserName": data.username.clone(),
            "Client": "Jellyfin Web",
            "LastActivityDate": "0001-01-01T00:00:00.0000000Z",
            "LastPlaybackCheckIn": "0001-01-01T00:00:00.0000000Z",
            "DeviceName": "blub blub blub",
            "DeviceId": "wagening",
            "ApplicationVersion": VERSION,
            "IsActive": true,
            "SupportsMediaControl": false,
            "SupportsRemoteControl": false,
            "NowPlayingQueue": [],
            "NowPlayingQueueFullItems": [],
            "HasCustomDeviceName": false,
            "ServerId": SERVER_ID,
            "SupportedCommands": []
            // "MoveUp",  "MoveDown",  "MoveLeft",  "MoveRight",  "PageUp",  "PageDown",  "PreviousLetter",  "NextLetter",  "ToggleOsd",  "ToggleContextMenu",  "Select",  "Back",  "SendKey",  "SendString",  "GoHome",  "GoToSettings",  "VolumeUp",  "VolumeDown",  "Mute",  "Unmute",  "ToggleMute",  "SetVolume",  "SetAudioStreamIndex",  "SetSubtitleStreamIndex",  "DisplayContent",  "GoToSearch",  "DisplayMessage",  "SetRepeatMode",  "SetShuffleQueue",  "ChannelUp",  "ChannelDown",  "PlayMediaSource",  "PlayTrailers"
        },
        "AccessToken": token,
        "ServerId": SERVER_ID
    })))
}

fn track_object(index: usize, track: &SourceTrack) -> JellyfinMediaStream {
    let fr = if let SourceTrackKind::Video { fps, .. } = &track.kind {
        Some(fps.unwrap_or_default())
    } else {
        None
    };
    JellyfinMediaStream {
        codec: match track.codec.as_str() {
            "V_HEVC" => "hevc",
            "V_AV1" => "av1",
            "V_VP8" => "vp8",
            "V_VP9" => "vp9",
            "A_AAC" => "aac",
            "A_OPUS" => "opus",
            _ => "unknown",
        }
        .to_string(),
        time_base: "1/1000".to_string(), // TODO unsure what that means
        video_range: if track.kind.letter() == 'v' {
            "SDR"
        } else {
            "Unknown"
        }
        .to_string(),
        video_range_type: if track.kind.letter() == 'v' {
            "SDR"
        } else {
            "Unknown"
        }
        .to_string(),
        audio_spatial_format: "None".to_string(),
        display_title: track.to_string(),
        is_interlaced: false, // TODO assuming that
        is_avc: track.codec.as_str() == "V_AVC",
        bit_rate: 5_000_000, // TODO totally
        bit_depth: 8,
        ref_frames: 1,
        is_default: true,
        is_forced: false,
        is_hearing_impaired: false,
        height: if let SourceTrackKind::Video { height, .. } = &track.kind {
            Some(*height)
        } else {
            None
        },
        width: if let SourceTrackKind::Video { width, .. } = &track.kind {
            Some(*width)
        } else {
            None
        },
        average_frame_rate: fr,
        real_frame_rate: fr,
        reference_frame_rate: fr,
        profile: "Main".to_string(),
        r#type: match track.kind {
            SourceTrackKind::Audio { .. } => JellyfinMediaStreamType::Audio,
            SourceTrackKind::Video { .. } => JellyfinMediaStreamType::Video,
            SourceTrackKind::Subtitle => JellyfinMediaStreamType::Subtitle,
        },
        aspect_ratio: "1:1".to_string(), // TODO aaa
        index,
        is_external: false,
        is_text_subtitle_stream: false,
        supports_external_stream: false,
        pixel_format: "yuv420p".to_string(),
        level: 150, // TODO what this mean?
        is_anamorphic: false,
        channel_layout: if let SourceTrackKind::Audio { .. } = &track.kind {
            Some("5.1".to_string()) // TODO aaa
        } else {
            None
        },
        channels: if let SourceTrackKind::Audio { channels, .. } = &track.kind {
            Some(*channels)
        } else {
            None
        },
        sample_rate: if let SourceTrackKind::Audio { sample_rate, .. } = &track.kind {
            Some(*sample_rate)
        } else {
            None
        },
        localized_default: "Default".to_string(),
        localized_external: "External".to_string(),
    }
}

fn media_source_object(node: &Node, m: &MediaInfo) -> JellyfinMediaSource {
    JellyfinMediaSource {
        protocol: JellyfinMediaSourceProtocol::File,
        id: node.slug.clone(),
        path: format!("/path/to/{}.webm", node.slug),
        r#type: JellyfinMediaSourceType::Default,
        container: "webm".to_string(),
        size: 1_000_000_000,
        name: node.slug.clone(),
        is_remote: false,
        e_tag: "blub".to_string(),
        run_time_ticks: m.duration * 10_000_000.,
        read_at_native_framerate: false,
        ignore_dts: false,
        ignore_index: false,
        gen_pts_input: false,
        supports_transcoding: true,
        supports_direct_stream: true,
        supports_direct_play: true,
        is_infinite_stream: false,
        use_most_compatible_transcoding_profile: false,
        requires_opening: false,
        requires_closing: false,
        requires_looping: false,
        supports_probing: true,
        video_type: JellyfinVideoType::VideoFile,
        media_streams: m
            .tracks
            .iter()
            .enumerate()
            .map(|(i, t)| track_object(i, t))
            .collect::<Vec<_>>(),
        media_attachments: Vec::new(),
        formats: Vec::new(),
        bitrate: 10_000_000,
        required_http_headers: BTreeMap::new(),
        transcoding_sub_protocol: "http".to_string(),
        default_audio_stream_index: 1,    // TODO
        default_subtitle_stream_index: 2, // TODO
        has_segments: false,
    }
}

fn item_object(node: &Node, userdata: &NodeUserData) -> JellyfinItem {
    let media_source = node.media.as_ref().map(|m| media_source_object(node, m));

    JellyfinItem {
        name: node.title.clone().unwrap_or_default(),
        server_id: SERVER_ID.to_owned(),
        id: node.slug.clone(),
        e_tag: "blob".to_owned(),
        date_created: "0001-01-01T00:00:00.0000000Z".to_owned(),
        can_delete: false,
        can_download: true,
        preferred_metadata_language: "".to_owned(),
        preferred_metadata_country_code: "".to_owned(),
        sort_name: node.slug.clone(),
        forced_sort_name: "".to_owned(),
        external_urls: vec![],
        enable_media_source_display: true,
        custom_rating: "".to_owned(),
        channel_id: None,
        overview: node.description.clone().unwrap_or_default(),
        taglines: vec![node.tagline.clone().unwrap_or_default()],
        genres: vec![],
        remote_trailers: vec![],
        provider_ids: BTreeMap::new(),
        is_folder: node.media.is_none(),
        parent_id: "todo-parent".to_owned(), // TODO
        r#type: match node.kind {
            NodeKind::Movie | NodeKind::Video | NodeKind::ShortFormVideo => JellyfinItemType::Movie,
            NodeKind::Collection => JellyfinItemType::CollectionFolder,
            _ => JellyfinItemType::CollectionFolder,
        },
        people: node
            .credits
            .iter()
            .flat_map(|(_pg, ps)| {
                ps.iter().map(|p| JellyfinPerson {
                    // TODO
                    id: String::new(),
                    name: String::new(),
                    primary_image_tag: String::new(),
                    // name: p..name.clone(),
                    // id: p..ids.tmdb.unwrap_or_default().to_string(),
                    // primary_image_tag: p..headshot.clone().map(|a| a.0).unwrap_or_default(),
                    role: p.characters.join(","),
                    r#type: JellyfinPersonType::Actor,
                })
            })
            .collect(),
        studios: vec![],
        genre_items: vec![],
        local_trailer_count: 0,
        special_feature_count: 0,
        child_count: 0,
        locked_fields: vec![],
        lock_data: false,
        tags: vec![],
        user_data: json!({
            "PlaybackPositionTicks": 0,
            "PlayCount": if userdata.watched == WatchedState::Watched { 1 } else { 0 },
            "IsFavorite": userdata.rating > 0,
            "Played": userdata.watched == WatchedState::Watched,
            "Key": "7a2175bc-cb1f-1a94-152c-bd2b2bae8f6d",
            "ItemId": "00000000000000000000000000000000"
        }),
        display_preferences_id: node.slug.clone(),
        primary_image_aspect_ratio: match aspect_class(node.kind) {
            "aspect-thumb" => 16. / 9.,
            "aspect-land" => 2f64.sqrt(),
            "aspect-port" => 1. / 2f64.sqrt(),
            "aspect-square" => 1.,
            _ => 1.,
        },
        collection_type: "unknown".to_owned(),
        image_tags: BTreeMap::from_iter([("Primary".to_string(), "poster".to_string())]),
        backdrop_image_tags: vec!["backdrop".to_string()],
        media_type: if node.media.is_some() {
            "Video".to_owned()
        } else {
            "Unknown".to_owned()
        },
        video_type: node.media.as_ref().map(|_| "VideoFile".to_owned()),
        location_type: node.media.as_ref().map(|_| "FileSystem".to_owned()),
        play_access: node.media.as_ref().map(|_| "Full".to_owned()),
        container: node.media.as_ref().map(|_| "webm".to_owned()),
        run_time_ticks: node
            .media
            .as_ref()
            .map(|m| (m.duration * 10_000_000.) as i64),
        media_sources: media_source.as_ref().map(|s| vec![s.clone()]),
        media_streams: media_source.as_ref().map(|s| s.media_streams.clone()),
        path: node
            .media
            .as_ref()
            .map(|_| format!("/path/to/{}.webm", node.slug)),
    }
}

fn user_object(username: String) -> Value {
    json!({
        "Name": username,
        "ServerId": SERVER_ID,
        "Id": USER_ID,
        "HasPassword": true,
        "HasConfiguredPassword": true,
        "HasConfiguredEasyPassword": false,
        "EnableAutoLogin": false,
        "LastLoginDate": "0001-01-01T00:00:00.0000000Z",
        "LastActivityDate": "0001-01-01T00:00:00.0000000Z",
        "Configuration": {
            "PlayDefaultAudioTrack": true,
            "SubtitleLanguagePreference": "",
            "DisplayMissingEpisodes": false,
            "GroupedFolders": [],
            "SubtitleMode": "Default",
            "DisplayCollectionsView": false,
            "EnableLocalPassword": false,
            "OrderedViews": [],
            "LatestItemsExcludes": [],
            "MyMediaExcludes": [],
            "HidePlayedInLatest": true,
            "RememberAudioSelections": true,
            "RememberSubtitleSelections": true,
            "EnableNextEpisodeAutoPlay": true,
            "CastReceiverId": "F007D354"
        },
        "Policy": {
            "IsAdministrator": false,
            "IsHidden": true,
            "EnableCollectionManagement": false,
            "EnableSubtitleManagement": false,
            "EnableLyricManagement": false,
            "IsDisabled": false,
            "BlockedTags": [],
            "AllowedTags": [],
            "EnableUserPreferenceAccess": true,
            "AccessSchedules": [],
            "BlockUnratedItems": [],
            "EnableRemoteControlOfOtherUsers": false,
            "EnableSharedDeviceControl": false,
            "EnableRemoteAccess": true,
            "EnableLiveTvManagement": false,
            "EnableLiveTvAccess": true,
            "EnableMediaPlayback": true,
            "EnableAudioPlaybackTranscoding": true,
            "EnableVideoPlaybackTranscoding": true,
            "EnablePlaybackRemuxing": true,
            "ForceRemoteSourceTranscoding": false,
            "EnableContentDeletion": false,
            "EnableContentDeletionFromFolders": [],
            "EnableContentDownloading": true,
            "EnableSyncTranscoding": true,
            "EnableMediaConversion": true,
            "EnabledDevices": [],
            "EnableAllDevices": true,
            "EnabledChannels": [],
            "EnableAllChannels": false,
            "EnabledFolders": [],
            "EnableAllFolders": true,
            "InvalidLoginAttemptCount": 0,
            "LoginAttemptsBeforeLockout": -1,
            "MaxActiveSessions": 0,
            "EnablePublicSharing": true,
            "BlockedMediaFolders": [],
            "BlockedChannels": [],
            "RemoteClientBitrateLimit": 0,
            "AuthenticationProviderId": "Jellyfin.Server.Implementations.Users.DefaultAuthenticationProvider",
            "PasswordResetProviderId": "Jellyfin.Server.Implementations.Users.DefaultPasswordResetProvider",
            "SyncPlayAccess": "CreateAndJoinGroups"
        }
    })
}
