package com.ponzischeme89.memby.data.model

import com.ponzischeme89.memby.data.playback.DevicePlaybackCapabilities
import com.ponzischeme89.memby.data.playback.VideoDecoderCapabilities
import com.ponzischeme89.memby.data.playback.devicePlaybackCapabilities
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class AuthRequest(
    @SerialName("Username") val username: String,
    @SerialName("Pw") val pw: String,
)

@Serializable
data class AuthResult(
    @SerialName("User") val user: EmbyUser? = null,
    @SerialName("AccessToken") val accessToken: String? = null,
    @SerialName("ServerId") val serverId: String? = null,
)

@Serializable
data class EmbyUser(
    @SerialName("Id") val id: String,
    @SerialName("Name") val name: String? = null,
)

@Serializable
data class ItemsResult(
    @SerialName("Items") val items: List<BaseItem> = emptyList(),
    @SerialName("TotalRecordCount") val totalRecordCount: Int = 0,
)

@Serializable
data class UserItemData(
    @SerialName("IsFavorite") val isFavorite: Boolean = false,
    @SerialName("Played") val played: Boolean = false,
    @SerialName("PlaybackPositionTicks") val playbackPositionTicks: Long = 0,
    @SerialName("UnplayedItemCount") val unplayedItemCount: Int? = null,
    /** When this item was last played. What orders the merged Continue Watching row. */
    @SerialName("LastPlayedDate") val lastPlayedDate: String? = null,
)

@Serializable
data class PlaybackReport(
    @SerialName("ItemId") val itemId: String,
    @SerialName("MediaSourceId") val mediaSourceId: String,
    @SerialName("PlaySessionId") val playSessionId: String,
    @SerialName("PositionTicks") val positionTicks: Long = 0,
    @SerialName("IsPaused") val isPaused: Boolean = false,
    @SerialName("IsMuted") val isMuted: Boolean = false,
    @SerialName("CanSeek") val canSeek: Boolean = true,
    @SerialName("PlayMethod") val playMethod: String = "DirectPlay",
    @SerialName("EventName") val eventName: String? = null,
)

@Serializable
data class PlaybackInfoRequest(
    @SerialName("Id") val id: String,
    @SerialName("UserId") val userId: String,
    @SerialName("IsPlayback") val isPlayback: Boolean = true,
    @SerialName("EnableDirectPlay") val enableDirectPlay: Boolean = true,
    @SerialName("EnableDirectStream") val enableDirectStream: Boolean = true,
    @SerialName("EnableTranscoding") val enableTranscoding: Boolean = true,
    @SerialName("AllowVideoStreamCopy") val allowVideoStreamCopy: Boolean = true,
    @SerialName("AllowAudioStreamCopy") val allowAudioStreamCopy: Boolean = true,
    @SerialName("StartTimeTicks") val startTimeTicks: Long = 0,
    @SerialName("SubtitleStreamIndex") val subtitleStreamIndex: Int? = null,
    @SerialName("CurrentPlaySessionId") val currentPlaySessionId: String? = null,
    @SerialName("DeviceProfile") val deviceProfile: DeviceProfile = DeviceProfile.embyAndroidTv(),
)

@Serializable
data class DeviceProfile(
    @SerialName("Name") val name: String,
    @SerialName("SupportedMediaTypes") val supportedMediaTypes: String = "Video",
    @SerialName("SubtitleProfiles") val subtitleProfiles: List<SubtitleProfile>,
    @SerialName("DirectPlayProfiles") val directPlayProfiles: List<DirectPlayProfile>,
    @SerialName("TranscodingProfiles") val transcodingProfiles: List<TranscodingProfile>,
    @SerialName("CodecProfiles") val codecProfiles: List<CodecProfile> = emptyList(),
) {
    companion object {
        fun embyAndroidTv(
            capabilities: DevicePlaybackCapabilities = devicePlaybackCapabilities,
        ) = DeviceProfile(
            name = "Memby Android TV",
            subtitleProfiles = listOf(
                "srt", "subrip", "ass", "ssa", "vtt", "webvtt", "mov_text", "tx3g",
            ).map { SubtitleProfile(it, "External") } + listOf(
                "pgs", "pgssub", "sup", "vobsub", "dvdsub", "dvbsub",
            ).map { SubtitleProfile(it, "Encode") },
            directPlayProfiles = listOf(
                DirectPlayProfile(
                    // The broad codec declaration is bounded by CodecProfiles below.
                    container = "mkv,mp4,m4v,mov,ts,mpegts",
                    videoCodec = directPlayVideoCodecs(capabilities),
                    audioCodec = "aac,mp3",
                ),
            ),
            transcodingProfiles = listOf(
                TranscodingProfile(
                    container = "ts",
                    // Permits Emby to remux a supported HEVC/H.264 video stream while
                    // converting only incompatible audio or subtitles.
                    videoCodec = directPlayVideoCodecs(capabilities),
                    audioCodec = "aac",
                    protocol = "hls",
                ),
            ),
            codecProfiles = codecProfiles(capabilities),
        )

        /** Compatibility for callers and older tests that only know the HEVC boolean. */
        fun embyAndroidTv(supportsHevc: Boolean): DeviceProfile = embyAndroidTv(
            DevicePlaybackCapabilities(
                hevc = VideoDecoderCapabilities(
                    supported = supportsHevc,
                    profiles = if (supportsHevc) setOf("main") else emptySet(),
                ),
            ),
        )

        private fun directPlayVideoCodecs(capabilities: DevicePlaybackCapabilities): String =
            if (capabilities.hevc.supported) "h264,hevc" else "h264"

        private fun codecProfiles(capabilities: DevicePlaybackCapabilities): List<CodecProfile> =
            buildList {
                addVideoProfiles("h264", capabilities.h264)
                addVideoProfiles("hevc", capabilities.hevc)
            }

        private fun MutableList<CodecProfile>.addVideoProfiles(
            codec: String,
            capability: VideoDecoderCapabilities,
        ) {
            if (!capability.supported) return
            val allowedProfiles = when (codec) {
                "h264" -> capability.profiles.mapNotNull {
                    when (it) {
                        "baseline" -> "baseline"
                        "constrained_baseline" -> "constrained baseline"
                        "main" -> "main"
                        "high" -> "high"
                        "high10" -> "high 10"
                        else -> null
                    }
                }
                else -> capability.profiles.mapNotNull {
                    when (it) {
                        "main" -> "main"
                        "main10" -> "main 10"
                        else -> null
                    }
                }
            }
            if (allowedProfiles.isNotEmpty()) {
                add(
                    CodecProfile(
                        codec = codec,
                        conditions = listOf(
                            ProfileCondition("EqualsAny", "VideoProfile", allowedProfiles.joinToString("|")),
                        ),
                    ),
                )
            }
            if (capability.mainLevel > 0) {
                add(
                    CodecProfile(
                        codec = codec,
                        conditions = listOf(
                            ProfileCondition("LessThanEqual", "VideoLevel", capability.mainLevel.toString()),
                        ),
                        applyConditions = listOf(
                            ProfileCondition(
                                "EqualsAny",
                                "VideoProfile",
                                if (codec == "h264") "baseline|constrained baseline|main|high" else "main",
                            ),
                        ),
                    ),
                )
            }
            if (capability.tenBitLevel > 0) {
                add(
                    CodecProfile(
                        codec = codec,
                        conditions = listOf(
                            ProfileCondition("LessThanEqual", "VideoLevel", capability.tenBitLevel.toString()),
                        ),
                        applyConditions = listOf(
                            ProfileCondition(
                                "Equals",
                                "VideoProfile",
                                if (codec == "h264") "high 10" else "main 10",
                            ),
                        ),
                    ),
                )
            }
            if (capability.maxWidth > 0 && capability.maxHeight > 0) {
                add(
                    CodecProfile(
                        codec = codec,
                        conditions = listOf(
                            ProfileCondition("LessThanEqual", "Width", capability.maxWidth.toString()),
                            ProfileCondition("LessThanEqual", "Height", capability.maxHeight.toString()),
                        ),
                    ),
                )
            }
        }
    }
}

@Serializable
data class CodecProfile(
    @SerialName("Type") val type: String = "Video",
    @SerialName("Codec") val codec: String,
    @SerialName("Conditions") val conditions: List<ProfileCondition>,
    @SerialName("ApplyConditions") val applyConditions: List<ProfileCondition> = emptyList(),
)

@Serializable
data class ProfileCondition(
    @SerialName("Condition") val condition: String,
    @SerialName("Property") val property: String,
    @SerialName("Value") val value: String,
    @SerialName("IsRequired") val isRequired: Boolean = false,
)

@Serializable
data class DirectPlayProfile(
    @SerialName("Container") val container: String,
    @SerialName("VideoCodec") val videoCodec: String,
    @SerialName("AudioCodec") val audioCodec: String,
    @SerialName("Type") val type: String = "Video",
)

@Serializable
data class TranscodingProfile(
    @SerialName("Container") val container: String,
    @SerialName("VideoCodec") val videoCodec: String,
    @SerialName("AudioCodec") val audioCodec: String,
    @SerialName("Protocol") val protocol: String,
    @SerialName("Type") val type: String = "Video",
    @SerialName("Context") val context: String = "Streaming",
)

@Serializable
data class SubtitleProfile(
    @SerialName("Format") val format: String,
    @SerialName("Method") val method: String,
)

@Serializable
data class Studio(
    @SerialName("Name") val name: String = "",
)

@Serializable
data class MediaStream(
    @SerialName("Index") val index: Int = -1,
    @SerialName("Type") val type: String = "",
    @SerialName("Codec") val codec: String? = null,
    @SerialName("Title") val title: String? = null,
    @SerialName("DisplayTitle") val displayTitle: String? = null,
    @SerialName("Language") val language: String? = null,
    @SerialName("IsDefault") val isDefault: Boolean = false,
    @SerialName("IsForced") val isForced: Boolean = false,
    @SerialName("IsHearingImpaired") val isHearingImpaired: Boolean = false,
    @SerialName("IsExternal") val isExternal: Boolean = false,
    @SerialName("IsTextSubtitleStream") val isTextSubtitleStream: Boolean = false,
    @SerialName("SupportsExternalStream") val supportsExternalStream: Boolean = false,
    @SerialName("DeliveryUrl") val deliveryUrl: String? = null,
    @SerialName("DeliveryMethod") val deliveryMethod: String? = null,
    @SerialName("Width") val width: Int? = null,
    @SerialName("Height") val height: Int? = null,
    @SerialName("VideoRange") val videoRange: String? = null,
    @SerialName("VideoRangeType") val videoRangeType: String? = null,
    @SerialName("Channels") val channels: Int? = null,
)

@Serializable
data class PlaybackInfo(
    @SerialName("MediaSources") val mediaSources: List<MediaSourceInfo> = emptyList(),
    @SerialName("PlaySessionId") val playSessionId: String = "",
)

@Serializable
data class MediaSourceInfo(
    @SerialName("Id") val id: String = "",
    @SerialName("MediaStreams") val mediaStreams: List<MediaStream> = emptyList(),
    @SerialName("SupportsDirectPlay") val supportsDirectPlay: Boolean? = null,
    @SerialName("SupportsDirectStream") val supportsDirectStream: Boolean? = null,
    @SerialName("SupportsTranscoding") val supportsTranscoding: Boolean? = null,
    @SerialName("DirectStreamUrl") val directStreamUrl: String? = null,
    @SerialName("TranscodingUrl") val transcodingUrl: String? = null,
)

@Serializable
data class EmbyPerson(
    @SerialName("Id") val id: String = "",
    @SerialName("Name") val name: String = "",
    @SerialName("Role") val role: String? = null,
    @SerialName("Type") val type: String = "",
    @SerialName("PrimaryImageTag") val primaryImageTag: String? = null,
) {
    val isCastMember: Boolean get() = type.equals("Actor", ignoreCase = true)
}

/**
 * One entry of Emby's chapter list. Only two of its fields matter here: intro markers are
 * written as ordinary chapters carrying a [markerType], in playback order beside the real
 * ones, which is why finding the titles costs no request of its own.
 */
@Serializable
data class EmbyChapter(
    @SerialName("StartPositionTicks") val startPositionTicks: Long = 0L,
    @SerialName("MarkerType") val markerType: String = "",
    @SerialName("Name") val name: String = "",
)

@Serializable
data class BaseItem(
    @SerialName("Id") val id: String,
    @SerialName("Name") val name: String = "",
    @SerialName("Type") val type: String = "",
    @SerialName("Overview") val overview: String? = null,
    @SerialName("Taglines") val taglines: List<String> = emptyList(),
    @SerialName("ProductionYear") val productionYear: Int? = null,
    // The day a title first aired or was released, as Emby's ISO-8601 string. An episode's
    // is the one date that distinguishes it from its neighbours in a list, so it is asked
    // for by name in every episode query — it is not a default field.
    @SerialName("PremiereDate") val premiereDate: String? = null,
    @SerialName("OfficialRating") val officialRating: String? = null,
    @SerialName("CommunityRating") val communityRating: Double? = null,
    @SerialName("Studios") val studios: List<Studio> = emptyList(),
    @SerialName("RunTimeTicks") val runTimeTicks: Long? = null,
    @SerialName("RecursiveItemCount") val recursiveItemCount: Int? = null,
    @SerialName("Genres") val genres: List<String> = emptyList(),
    @SerialName("CollectionName") val collectionName: String? = null,
    @SerialName("MediaStreams") val mediaStreams: List<MediaStream> = emptyList(),
    @SerialName("People") val people: List<EmbyPerson> = emptyList(),
    @SerialName("PrimaryImageAspectRatio") val primaryImageAspectRatio: Double? = null,
    @SerialName("BackdropImageTags") val backdropImageTags: List<String> = emptyList(),
    @SerialName("ImageTags") val imageTags: Map<String, String> = emptyMap(),
    @SerialName("SeriesId") val seriesId: String? = null,
    @SerialName("SeriesName") val seriesName: String? = null,
    /**
     * Emby's production status for a series — "Continuing" or "Ended". Only the detail
     * call asks for it; a row item carries none, which is why [isOngoingSeries] treats
     * absence as "not known to be running" rather than guessing either way.
     */
    @SerialName("Status") val status: String? = null,
    // Emby returns these on episodes without being asked, so they cost no extra Fields.
    @SerialName("IndexNumber") val indexNumber: Int? = null,
    @SerialName("ParentIndexNumber") val parentIndexNumber: Int? = null,
    @SerialName("ParentBackdropItemId") val parentBackdropItemId: String? = null,
    @SerialName("ParentBackdropImageTags") val parentBackdropImageTags: List<String> = emptyList(),
    @SerialName("ParentLogoItemId") val parentLogoItemId: String? = null,
    @SerialName("ParentLogoImageTag") val parentLogoImageTag: String? = null,
    @SerialName("UserData") val userData: UserItemData? = null,
    /**
     * Chapter markers, which is where Emby records an episode's opening titles. Only the
     * direct path's intro lookup asks for them — every other query would be paying for a
     * couple of dozen entries per item to render nothing.
     */
    @SerialName("Chapters") val chapters: List<EmbyChapter> = emptyList(),
    // Server-authored schedule metadata. These fields are absent on normal Emby items.
    @SerialName("MembySource") val membySource: String? = null,
    @SerialName("MembyEpisodeTitle") val membyEpisodeTitle: String? = null,
    @SerialName("MembyEpisodeCode") val membyEpisodeCode: String? = null,
    @SerialName("MembyAirsAt") val membyAirsAt: String? = null,
    @SerialName("MembyAddedAt") val membyAddedAt: String? = null,
    @SerialName("MembyAirDayLabel") val membyAirDayLabel: String? = null,
    @SerialName("MembyAirLabel") val membyAirLabel: String? = null,
    @SerialName("MembyAvailability") val membyAvailability: String? = null,
    @SerialName("MembyAvailabilityText") val membyAvailabilityText: String? = null,
    // Sonarr's or Radarr's own lifecycle for the title — whether more episodes are coming,
    // whether the film has actually been released. Distinct from availability, which is
    // about the household's copy. The slug is what the badge colours by; the text is the
    // gateway's wording, so a status this build predates still reads correctly.
    @SerialName("MembyLifecycle") val membyLifecycle: String? = null,
    @SerialName("MembyLifecycleText") val membyLifecycleText: String? = null,
    @SerialName("MembyPlayable") val membyPlayable: Boolean = true,
    // The Emby series a schedule card stands for, when the library holds it. Absent for a
    // show Sonarr follows but Emby has never imported, so the card stays informational.
    @SerialName("MembySeriesItemId") val membySeriesItemId: String? = null,
    // Derived by the TV from the weekly schedule row and retained in the local home cache.
    @SerialName("MembyAiringToday") val membyAiringToday: Boolean = false,
    // Explainability supplied only by the gateway's dedicated For You endpoint.
    @SerialName("MembyRecommendationReason") val membyRecommendationReason: String? = null,
    @SerialName("MembyCompatibility") val membyCompatibility: String? = null,
    // Backend diagnostics for the shared explainable ranker. These remain optional so
    // direct-to-Emby mode and older cached payloads decode unchanged.
    @SerialName("MembyRecommendationScore") val membyRecommendationScore: Double? = null,
    @SerialName("MembyRecommendationComponents")
    val membyRecommendationComponents: Map<String, Double> = emptyMap(),
    @SerialName("MembyRecommendationReasonCodes")
    val membyRecommendationReasonCodes: List<String> = emptyList(),
    @SerialName("MembyExploration") val membyExploration: Boolean = false,
    // External ratings the gateway already had stored for this title. They ride on the
    // card so a row can draw its scores as it appears rather than when focus reaches it;
    // an item the gateway has never looked up carries none and the dedicated ratings
    // request still fills it in. Defaulted, so a cached home payload decodes unchanged.
    @SerialName("MembyRatings") val membyRatings: List<MediaRating> = emptyList(),
    // Why this card is leading the launcher, decided by the gateway from evidence the
    // television does not have — Radarr's digital release date, Sonarr's premieres and
    // the stored review scores. The wording is the server's for the usual reason: a kind
    // of hero card added tomorrow reads correctly on a build that predates it. Both are
    // absent on the direct path, where the client picks the hero itself.
    @SerialName("MembyHeroLabel") val membyHeroLabel: String? = null,
    @SerialName("MembyHeroReason") val membyHeroReason: String? = null,
) {
    val isMovie: Boolean get() = type.equals("Movie", ignoreCase = true)
    val isSeries: Boolean get() = type.equals("Series", ignoreCase = true)
    val isEpisode: Boolean get() = type.equals("Episode", ignoreCase = true)
    val isFavorite: Boolean get() = userData?.isFavorite == true
    val isTvSchedule: Boolean get() = membySource == "sonarr"
    val isMovieSchedule: Boolean get() = membySource == "radarr"
    val isSchedule: Boolean get() = isTvSchedule || isMovieSchedule

    /**
     * Whether more episodes are expected. Sonarr's answer wins where the gateway attached
     * one, since it knows about a season announced but not yet imported; Emby's own
     * [status] is the fallback and the only source the direct path has. Neither present
     * means no, which keeps the pace estimate saying "finish" — the weaker claim.
     */
    val isOngoingSeries: Boolean
        get() = when {
            membyLifecycle != null -> membyLifecycle.equals("continuing", ignoreCase = true)
            else -> status.equals("Continuing", ignoreCase = true)
        }
    val cast: List<EmbyPerson> get() = people.filter(EmbyPerson::isCastMember)

    /** "S2 · E5" when the season is known, "E5" when only the episode is, else null. */
    val episodeCode: String?
        get() {
            val episode = indexNumber ?: return null
            val season = parentIndexNumber
            return if (season != null) "S$season · E$episode" else "E$episode"
        }

    /** Runtime in whole minutes, or null when unknown. */
    val runtimeMinutes: Int?
        get() = runTimeTicks?.let { (it / 600_000_000L).toInt() }.takeIf { it != null && it > 0 }
}
