package com.ponzischeme89.memby.data.model

import kotlinx.serialization.Serializable

/**
 * Wire types for the Memby gateway.
 *
 * Item payloads are deliberately still [BaseItem]: the gateway forwards Emby's item JSON
 * untouched, so there is exactly one item schema in the system regardless of which
 * backend the client is talking to.
 */

@Serializable
data class GatewayLoginRequest(
    val username: String,
    val password: String,
    val deviceId: String,
    val deviceName: String,
)

@Serializable
data class GatewayLoginResponse(
    val token: String = "",
    val userId: String = "",
    val username: String = "",
    val serverId: String = "",
)

@Serializable
data class GatewayDevice(
    val deviceId: String,
    val deviceName: String,
    val clientVersion: String = "",
    val lastSeenAt: String = "",
    val current: Boolean = false,
)

@Serializable
data class GatewayDevices(
    val devices: List<GatewayDevice> = emptyList(),
)

@Serializable
data class GatewayDeviceNameRequest(val deviceName: String)

/**
 * One horizontal strip, described entirely by the server.
 *
 * [kind] drives card shape and the empty-state wording on the client; [id] is the stable
 * key Compose uses for the row. A server that starts sending a new row — a recommendation
 * strip, a seasonal collection — needs no client release, as long as its kind is one the
 * app already understands (unknown kinds fall back to poster cards).
 *
 * The same type is persisted in [com.ponzischeme89.memby.data.HomeCache], so a cold
 * start redraws the exact rows the server last sent.
 */
@Serializable
data class HomeRow(
    val id: String,
    val title: String,
    val kind: String = "",
    val items: List<BaseItem> = emptyList(),
)

/** Everything the launcher renders, in one response. */
@Serializable
data class GatewayHome(
    /** Server-composed rows, in display order. */
    val rows: List<HomeRow> = emptyList(),
    /** In progress, including the episode after one just finished — they are one row. */
    val continueWatching: List<BaseItem> = emptyList(),
    /**
     * Still sent, and still merged into [continueWatching] by the gateway. It remains on
     * the wire for televisions running a build that predates the merge, which compose a
     * Next Up row of their own from it.
     */
    val nextUp: List<BaseItem> = emptyList(),
    val favorites: List<BaseItem> = emptyList(),
    val latestMovies: List<BaseItem> = emptyList(),
    /** True when a row failed upstream; the rest of the payload is still usable. */
    val partial: Boolean = false,
)

/**
 * The gateway's verdict on this build, from `GET /v1/update`.
 *
 * [status] is `none`, `optional` or `mandatory`. Mandatory blocks the app before login or
 * home is composed — the operator has decided this version may no longer be used.
 */
@Serializable
data class GatewayUpdate(
    val status: String = STATUS_NONE,
    val version: String = "",
    val notes: String = "",
    val downloadUrl: String = "",
    val sha256: String = "",
    val sizeBytes: Long = 0,
) {
    val isMandatory: Boolean get() = status == STATUS_MANDATORY
    val isOptional: Boolean get() = status == STATUS_OPTIONAL

    /** Usable only if the gateway actually told us where the APK is. */
    val isActionable: Boolean get() = downloadUrl.isNotBlank() && status != STATUS_NONE

    companion object {
        const val STATUS_NONE = "none"
        const val STATUS_OPTIONAL = "optional"
        const val STATUS_MANDATORY = "mandatory"
    }
}

/** Lightweight live state returned even while normal gateway routes are in maintenance. */
@Serializable
data class GatewayServiceStatus(
    val maintenance: Boolean = false,
    val message: String = "",
    val alerts: List<GatewayAlert> = emptyList(),
    val compatible: Boolean = true,
    val compatibilityMessage: String = "",
    val clientVersion: String = "",
    val clientProtocol: String = "",
    val gatewayVersion: String = "",
    val serverProtocol: Int = 0,
    val featureSchemaVersion: Int = 0,
    val featureRevision: Long = 0,
    val safeMode: Boolean = false,
    val features: Map<String, Boolean> = emptyMap(),
    /**
     * Whether *Emby* is answering, as opposed to whether the gateway is. Absent from a
     * server older than this field, which is why it defaults to a healthy, unmonitored
     * reading rather than to null — an app that cannot tell must not claim an outage.
     */
    val emby: GatewayEmbyHealth = GatewayEmbyHealth(),
    /**
     * Revision of this viewer's server-held settings. The app compares it with what it
     * has and fetches the document only when they differ, so an operator's push arrives
     * on the poll the app is already making.
     */
    val preferencesRevision: Long = 0,
    /**
     * The colour scheme this viewer's televisions should be painted, as an id and a
     * revision rather than the palette itself — the [preferencesRevision] precedent, for
     * the same reason: this poll runs every ten seconds on every open set, and a palette
     * riding it would be eight colours repeated six times a minute to say nothing new.
     *
     * A server that predates this sends none, which decodes to a revision of "" and so
     * never triggers a fetch: an app that cannot be told its theme keeps the one it shipped
     * with, which is the palette everything looked like before themes existed.
     */
    val theme: GatewayThemeStatus = GatewayThemeStatus(),
)

/** The summary of a theme that rides the status poll. See [GatewayTheme] for the document. */
@Serializable
data class GatewayThemeStatus(
    val id: String = "",
    /**
     * Opaque, and compared only for equality. It moves when the palette would look
     * different — which includes the morning a season begins, an event no revision counter
     * in a table could produce because nobody wrote anything.
     */
    val revision: String = "",
    val seasonal: Boolean = false,
    val locked: Boolean = false,
)

/**
 * The gateway's live reading of its own connection to Emby.
 *
 * [monitored] is the load-bearing field: with the probe switched off nothing updates
 * [reachable], so a client that ignored this would show a permanent, wrong red bar. The
 * server also sends `reachable = true` in that case, but the app should not depend on both
 * halves of a defence.
 */
@Serializable
data class GatewayEmbyHealth(
    val monitored: Boolean = false,
    val reachable: Boolean = true,
    /** When the current state began, RFC3339. */
    val since: String = "",
    /** When the last probe ran, RFC3339. The retry countdown is measured from here. */
    val checkedAt: String = "",
    val retrySeconds: Int = 0,
) {
    /** An outage worth telling the viewer about: monitored, and currently failing. */
    val isOutage: Boolean get() = monitored && !reachable
}

/**
 * A viewer's settings as the gateway holds them, so they follow the person to whichever
 * television they sit in front of.
 *
 * [preferences] stays a [JsonObject] on the wire rather than a typed class on purpose: a
 * server that has learned a new setting must not make this response undecodable on an app
 * that has not. [com.ponzischeme89.memby.data.UserPreferences] is where it becomes typed,
 * and everything it does not recognise is carried through untouched.
 */
@Serializable
data class GatewayPreferences(
    val schemaVersion: Int = 0,
    val revision: Long = 0,
    val updatedAt: String = "",
    /** "device" or "admin" — who wrote it last. */
    val source: String = "",
    val preferences: kotlinx.serialization.json.JsonObject =
        kotlinx.serialization.json.JsonObject(emptyMap()),
)

/**
 * The colour scheme in force and the ones this viewer may choose between.
 *
 * Both halves come from the server and neither is compiled into the app. The catalogue is
 * per viewer, not per app: a theme the operator has withheld from somebody is not a greyed
 * row on their television, it is a row that was never sent — which is what makes the
 * per-user allowlist real rather than advisory.
 */
@Serializable
data class GatewayThemeDocument(
    val schemaVersion: Int = 0,
    val theme: GatewayTheme = GatewayTheme(),
    val available: List<GatewayTheme> = emptyList(),
)

/**
 * One theme. [palette] is the whole of what it changes — a theme never moves a control or
 * alters what a row contains, so the worst a scheme this build has never heard of can do is
 * look wrong.
 */
@Serializable
data class GatewayTheme(
    val id: String = "",
    val name: String = "",
    val description: String = "",
    /**
     * True for Halloween, Christmas and Easter. Never offered as a choice and never stored
     * as one; it is simply in force for its dates.
     */
    val seasonal: Boolean = false,
    /**
     * While true the picker shows the viewer's own choice but will not let them change it,
     * and [reason] is the sentence explaining why. Distinct from [seasonal] so a future
     * reason to pin a theme does not have to claim to be a season.
     */
    val locked: Boolean = false,
    /** The viewer's own selection, still theirs underneath a season. */
    val chosen: String = "",
    /** The server's wording for the lock, so a season invented later still reads correctly. */
    val reason: String = "",
    /**
     * What drifts over the launcher while this theme is on: "snow", "bats", "blossom", or
     * empty for the whole rest of the year and for every scheme somebody chose themselves.
     *
     * A slug rather than anything describing the animation — the drawing is the television's,
     * in `ui/seasonal/`. A build that does not recognise one draws nothing, so the gateway may
     * invent a decoration before the fleet has the release that knows it. Empty is also what
     * an operator who has turned decorations off gets, which is why the client must obey this
     * field rather than deriving the animation from the theme id.
     */
    val decoration: String = "",
    val revision: String = "",
    val palette: GatewayPalette = GatewayPalette(),
)

/**
 * The eight colours a theme sets, as `#AARRGGBB`. Alpha first because Android is the end
 * that has to parse thousands of these; the admin console reorders for CSS at its own end.
 *
 * Every field defaults to blank rather than to a colour: a missing one falls back to the
 * app's own token at the point of conversion, which is a theme drawn slightly wrong rather
 * than a screen drawn transparent.
 */
@Serializable
data class GatewayPalette(
    val surface: String = "",
    val surfaceRaised: String = "",
    val accent: String = "",
    val onSurface: String = "",
    val mutedText: String = "",
    val quietText: String = "",
    val hairline: String = "",
    val ratingsSurface: String = "",
)

/** A write of [GatewayPreferences]. [revision] is the one being edited, for conflict detection. */
@Serializable
data class GatewayPreferencesRequest(
    val revision: Long,
    val preferences: kotlinx.serialization.json.JsonObject,
)

@Serializable
data class GatewayFeature(
    val key: String,
    val name: String = "",
    val description: String = "",
    val area: String = "",
    val enabled: Boolean = false,
    val source: String = "default",
    val compatible: Boolean = true,
    val minimumProtocol: Int = 0,
    val capability: String = "",
    val recovery: String = "",
)

@Serializable
data class GatewayFeatures(
    val schemaVersion: Int = 0,
    val revision: Long = 0,
    val safeMode: Boolean = false,
    val canRollback: Boolean = false,
    val features: List<GatewayFeature> = emptyList(),
)

/**
 * An informational nudge riding along on the status poll — "this episode aired and is on
 * its way into Emby", or "Radarr just imported this film". It is never actionable: the
 * banner slides in, states the news and leaves. Unknown [kind] values still render, and
 * the server supplies the wording, so a new kind of news needs no app release.
 */
@Serializable
data class GatewayAlert(
    val id: String = "",
    val kind: String = "",
    /** Eyebrow text for the banner ("JUST AIRED", "NEW MOVIE ADDED"). May be absent. */
    val label: String = "",
    val title: String = "",
    val message: String = "",
    val itemId: String = "",
    val imageTag: String = "",
    val airedAt: String = "",
)

/** Response of `GET /v1/recommendations`. */
@Serializable
data class GatewayRows(
    val rows: List<HomeRow> = emptyList(),
)

/** Normalised third-party rating shared by cards, banners, and detail pages. */
@Serializable
data class MediaRating(
    val source: String = "",
    val name: String = "",
    val score: String = "",
    val scale: String = "",
)

@Serializable
data class GatewayMovieRatings(
    val ratings: List<MediaRating> = emptyList(),
)

@Deprecated("Use MediaRating")
typealias GatewayMovieRating = MediaRating

@Serializable
data class RecommendationOnboarding(
    val completed: Boolean = false,
    // Defaults true for compatibility with gateways released before prompting became
    // server-controlled; current gateways always send the field explicitly.
    val prompted: Boolean = true,
    val ratings: Map<String, Int> = emptyMap(),
    val items: List<BaseItem> = emptyList(),
    val movies: List<BaseItem> = emptyList(),
    val shows: List<BaseItem> = emptyList(),
    val actors: List<RecommendationPerson> = emptyList(),
    val actresses: List<RecommendationPerson> = emptyList(),
    val directors: List<RecommendationPerson> = emptyList(),
)

@Serializable
data class RecommendationPreferences(
    val ratings: Map<String, Int> = emptyMap(),
    val actors: List<String> = emptyList(),
    val actresses: List<String> = emptyList(),
    val directors: List<String> = emptyList(),
)

@Serializable
data class RecommendationPerson(
    val id: String = "",
    val name: String = "",
    val imageTag: String = "",
)

@Serializable
data class GatewayItems(
    val items: List<BaseItem> = emptyList(),
)

/**
 * One page of `GET /v1/genres/{genre}/items` or `GET /v1/library/items`.
 *
 * [total] is what ends the scroll. A page shorter than [limit] ends it too, but a genre
 * whose last page happens to divide evenly would otherwise cost one more empty request to
 * discover that, and that request lands exactly as somebody reaches the bottom of the grid.
 * It defaults to zero rather than to something optimistic: a gateway that answered without
 * it must leave the television believing it has everything, not asking forever.
 */
@Serializable
data class GatewayGenrePage(
    val genre: String = "",
    val items: List<BaseItem> = emptyList(),
    val offset: Int = 0,
    val limit: Int = 0,
    val total: Int = 0,
)

/**
 * Response of `GET /v1/items/{id}/related` — the detail page's two additions.
 *
 * [reasons] are short phrases from the recommendation engine explaining *this viewer's*
 * relationship to the title ("Because you watch Thriller"); [items] is the carousel of
 * what else is like it. Either half may be empty: a cold profile has no reasons to give,
 * and an obscure title has nothing beside it.
 */
@Serializable
data class GatewayRelated(
    val reasons: List<String> = emptyList(),
    val items: List<BaseItem> = emptyList(),
)

@Serializable
data class GatewaySearchHistory(
    val queries: List<String> = emptyList(),
)

@Serializable
data class GatewayRequestCandidate(
    val mediaType: String,
    val foreignId: Int,
    val title: String,
    val year: Int = 0,
    val overview: String = "",
    val posterUrl: String = "",
    val alreadyAdded: Boolean = false,
)

@Serializable
data class GatewayRequestLookup(
    val candidates: List<GatewayRequestCandidate> = emptyList(),
)

@Serializable
data class GatewayMediaRequest(
    val mediaType: String,
    val foreignId: Int,
)

@Serializable
data class GatewayMediaRequestResult(
    val status: String = "",
    val title: String = "",
)

@Serializable
data class GatewayPrerollSchedule(
    val today: List<GatewayPrerollEntry> = emptyList(),
    val thisWeek: List<GatewayPrerollEntry> = emptyList(),
)

@Serializable
data class GatewayPrerollEntry(
    val itemId: String = "",
    val imageType: String = "",
    val series: String = "",
    val episode: String = "",
    val episodeCode: String = "",
    val schedule: String = "",
    val availability: String = "",
)

@Serializable
data class GatewayPlayback(
    val itemId: String,
    val title: String = "",
    val overview: String = "",
    val seriesName: String = "",
    val episodeCode: String = "",
    val runtimeMs: Long = 0,
    val prerollEnabled: Boolean = true,
    val prerollDurationMs: Long = 6_500L,
    val url: String,
    val resumePositionMs: Long = 0,
    val subtitles: List<com.ponzischeme89.memby.data.PlayableSubtitle> = emptyList(),
    // Which of those tracks the gateway decided to turn on, from the viewer's synced
    // settings. Absent on an older gateway, which is what the defaults describe: subtitles
    // on, nothing chosen, so the television falls back to its own flag-driven pick.
    val subtitlesEnabled: Boolean = true,
    val selectedSubtitleId: String = "",
    val mediaSourceId: String = "",
    val playSessionId: String = "",
    val playMethod: String = "DirectPlay",
    // Whether this gateway can fetch a subtitle the title does not have. It rides on the
    // playback response rather than the status poll because the drop-up is the only thing
    // that asks and it already holds this. Absent on an older gateway, and the default
    // must stay false: a missing field must never conjure a row that cannot do anything.
    val subtitleDownloadAvailable: Boolean = false,
    // Whether this title has another readable text track against which subtitle timing can
    // be checked. False for an older gateway, so a missing field never creates a dead row.
    val subtitleFixAvailable: Boolean = false,
    // Whether it is worth asking this gateway for seek previews at all. Only the answer
    // rides here — the layout itself is its own request, off the critical path of
    // starting playback. Absent on an older gateway, and the default must stay false: a
    // missing field must never conjure a request the backend would 404.
    val trickplayAvailable: Boolean = false,
    // Whether it is worth asking this gateway where the title sequence is. Same shape and
    // same reasoning as the previews above: the segment itself is its own request, and a
    // missing field must never conjure one this backend would not answer.
    val skipIntroAvailable: Boolean = false,
    // Whether it is worth asking this gateway where the closing credits begin. Same shape
    // and same reasoning again, and deliberately its own field rather than a reuse of
    // [skipIntroAvailable]: they are separate features with separate switches, and a house
    // that turned the skip button off has not asked to lose the credits pane with it.
    val endCreditsAvailable: Boolean = false,
)

/**
 * Where an episode's opening titles sit, and where its closing credits begin, as the gateway
 * found them in Emby's markers.
 *
 * [available] is explicit rather than implied by a zero pair: an intro can legitimately
 * begin at the very start of the file, and that must stay distinguishable from an episode
 * that has no markers at all.
 *
 * Both answers share one response because they are in the same chapter list, so reading them
 * together costs the one Emby request the gateway was always going to make. [creditsAvailable]
 * is separate from [available] because an episode routinely has one and not the other — every
 * film Emby has found credits but no intro in would otherwise be lost.
 */
@Serializable
data class GatewayIntro(
    val available: Boolean = false,
    val startMs: Long = 0L,
    val endMs: Long = 0L,
    val creditsAvailable: Boolean = false,
    val creditsStartMs: Long = 0L,
)

/**
 * How a title's seek previews are laid out, as the gateway describes them.
 *
 * Frame URLs are not listed. There are hundreds of them and they are formed by a rule the
 * client already knows, so a list would be most of the response.
 */
@Serializable
data class GatewayTrickplay(
    val available: Boolean = false,
    val intervalMs: Long = 0L,
    val count: Int = 0,
    val width: Int = 0,
    val height: Int = 0,
)

/** One subtitle a viewer can choose to download, as the gateway offers it. */
@Serializable
data class GatewaySubtitleCandidate(
    // Which backend produced this row. It rides back with the token on the download call,
    // because the gateway dispatches on it — the providers' tokens are opaque in different
    // ways and handing one to the other is a mistake nothing could detect. Never derived
    // here: an empty value is a gateway that predates the second provider, and the server
    // reads that as Bazarr.
    val source: String = "",
    // The provider's opaque handle. It round-trips untouched — nothing on this side parses
    // it, and reconstructing it from the other fields would break the download.
    val token: String = "",
    val language: String = "",
    val languageLabel: String = "",
    val provider: String = "",
    val score: Int = 0,
    val forced: Boolean = false,
    val hearingImpaired: Boolean = false,
    val originalFormat: Boolean = false,
    // Whether nobody wrote this translation. It is on the wire rather than only in the
    // label because it is the one property that changes whether a viewer wants the row at
    // all, and the gateway's ranking sinks it below everything a person wrote.
    val machineOnly: Boolean = false,
    // What the row prints. Composed by the gateway so an app that predates a new wording
    // still renders it correctly, the same reason alert labels are the gateway's.
    val label: String = "",
)

@Serializable
data class GatewaySubtitleSearch(
    val results: List<GatewaySubtitleCandidate> = emptyList(),
    // Why there are none, when there are none. "Nothing was found" and "Memby could not
    // work out which title this is" are different answers and a viewer deserves to know
    // which one they got.
    val message: String = "",
)

@Serializable
data class GatewaySubtitleDownloadRequest(val candidate: GatewaySubtitleCandidate)

@Serializable
data class GatewaySubtitleFixRequest(val subtitleId: String)

/** The result of checking one subtitle's timing against another track on the title. */
@Serializable
data class GatewaySubtitleFix(
    val subtitleId: String = "",
    val message: String = "",
    val changed: Boolean = false,
    val offsetMs: Long = 0L,
    val reference: String = "",
)

/**
 * The result of a download: the item's tracks re-read from Emby after it was told to look
 * again, so the player can swap its media item and turn the new track on without a second
 * round trip.
 */
@Serializable
data class GatewaySubtitleDownload(
    val message: String = "",
    val subtitles: List<com.ponzischeme89.memby.data.PlayableSubtitle> = emptyList(),
    val selectedSubtitleId: String = "",
    val mediaSourceId: String = "",
    val playSessionId: String = "",
    val url: String = "",
)

/**
 * The episode that follows the one being watched. [item] is Emby's item JSON forwarded
 * verbatim, so it decodes into the same [BaseItem] used everywhere else.
 */
@Serializable
data class GatewayNextEpisode(
    val item: BaseItem,
    val title: String = "",
    val url: String,
    val resumePositionMs: Long = 0,
    val subtitles: List<com.ponzischeme89.memby.data.PlayableSubtitle> = emptyList(),
    val subtitlesEnabled: Boolean = true,
    val selectedSubtitleId: String = "",
    val mediaSourceId: String = "",
    val playSessionId: String = "",
    val playMethod: String = "DirectPlay",
)

@Serializable
data class GatewaySeasonFinale(
    val seasonFinale: Boolean = false,
    val seriesName: String = "",
    val seasonNumber: Int = 0,
    val episodeNumber: Int = 0,
)

@Serializable
data class GatewayFlagRequest(
    val value: Boolean,
)

@Serializable
data class MyShow(
    val itemId: String,
    val title: String,
    val year: Int? = null,
    val imageTag: String = "",
    val addedAt: String = "",
    val sonarrStatus: String = "Not found",
    val nextEpisode: String? = null,
    val lifecycle: String = "Unknown",
    val monitored: Boolean = false,
)

@Serializable
data class MyShowsResponse(
    val shows: List<MyShow> = emptyList(),
)

@Serializable
data class SaveMyShowRequest(
    val itemId: String,
    val title: String,
    val year: Int? = null,
    val imageTag: String = "",
)

@Serializable
data class NotificationPreferences(
    val enabled: Boolean = true,
    val showReturnAlerts: Boolean = true,
    val leadDays: Int = 7,
)

@Serializable
data class UserNotification(
    val id: Long,
    val kind: String = "",
    val itemId: String = "",
    val title: String = "",
    val message: String = "",
    val eventAt: String? = null,
    val createdAt: String = "",
    val readAt: String? = null,
) {
    val unread: Boolean get() = readAt.isNullOrBlank()
}

@Serializable
data class NotificationsResponse(
    val notifications: List<UserNotification> = emptyList(),
    val preferences: NotificationPreferences = NotificationPreferences(),
)

/** One row-engagement event. See `data/analytics/RowAnalytics.kt`. */
@Serializable
data class GatewayRowEvent(
    val rowId: String,
    val rowKind: String = "",
    val event: String,
    val itemId: String = "",
    val dwellMs: Long = 0,
    val occurredAt: String = "",
)

@Serializable
data class GatewayRowEvents(
    val events: List<GatewayRowEvent>,
)

@Serializable
data class GatewayPlaybackReport(
    val itemId: String,
    val positionMs: Long,
    val durationMs: Long = 0,
    val isPaused: Boolean = false,
    val mediaSourceId: String = "",
    val playSessionId: String = "",
    val playMethod: String = "DirectPlay",
    val eventName: String? = null,
)

@Serializable
data class GatewayPlaybackReportResponse(
    val autoFollowedShowTitle: String = "",
)
