import { resolver } from 'hono-openapi'
import { z } from 'zod'

import type { OpenAPIV3_1 } from 'openapi-types'

/**
 * OpenAPI schemas for the discovery worker.
 *
 * IMPORTANT: these are DESCRIPTIVE ONLY. They are passed to `describeRoute` to generate
 * the spec and are never wired into `hono-openapi`'s `validator()`. Same rationale as
 * the other workers: a reverse-engineered protocol, lenient handlers, no runtime
 * validation.
 *
 * Do NOT add `.meta({ id })` to these schemas — with this hono-openapi + zod v4 setup a
 * meta'd schema used in a response emits a `$ref` the framework doesn't always hoist into
 * `components.schemas`, leaving a dangling reference. Leaving meta off makes every schema
 * inline, which renders correctly in any tool.
 */

/** Emit a zod schema as an `application/json` response body. */
export function json(schema: z.ZodType, description: string) {
	return { description, content: { 'application/json': { schema: resolver(schema) } } }
}

/** The `{type}` path parameter, which is the filename in `static/`. */
export const PAGE_SOURCE_PARAM: OpenAPIV3_1.ParameterObject = {
	name: 'type',
	in: 'path',
	required: true,
	description: [
		'The page source — `WatchHome`, `PlayHighlight`, `CommunityBoard`, `PlayMenuTabs`,',
		'`PlayCategories`, `StoreCategories`, `StoreFeatured`, `StoreClothing`,',
		'`StoreConsumables` at the time of writing. It names a file in `static/`',
		'(`<type>.json`) and is matched exactly, case included, so the set is whatever is',
		'published rather than anything this worker enumerates.',
		'',
		'`sections` is a file in `static/` too but is not one of these: it is the id-keyed',
		'catalogue `/sections/bulk` filters, not a page anything draws.',
	].join(' '),
	// Deliberately not an `enum`: the accepted values are the published files, and a spec
	// that froze today's list would be wrong the moment one is added.
	schema: { type: 'string', example: 'WatchHome' },
}

/**
 * The repeated `?id=` query the bulk lookup selects on. Repetition, not a delimiter: the
 * client sends `?id=A&id=B&id=C`, so this is `explode: true` form style rather than a
 * single comma-joined value.
 */
export const SECTION_IDS_PARAM: OpenAPIV3_1.ParameterObject = {
	name: 'id',
	in: 'query',
	required: false,
	description: [
		'A section id to look up, repeated once per section wanted. Ids that match nothing are',
		'skipped rather than erroring, and repeating one still yields it once — the answer is',
		'the catalogue filtered, so it can only ever be a subset of it. Omitting the parameter',
		'entirely answers `[]`.',
	].join(' '),
	style: 'form',
	explode: true,
	schema: { type: 'array', items: { type: 'string' } },
	example: [
		'Rooms_New_PlayHighlight_TabsTest_Explore',
		'RoomCategories_MoodPlaylists_FeelingLucky',
	],
}

// ---- Response schemas ------------------------------------------------------

/** `GET /` — the liveness probe body. */
export const ServiceStatus = z.object({
	service: z.literal('discovery'),
	status: z.literal('ok'),
})

/**
 * How one carousel on a discovery page is filled and drawn. `source`/`sourceMetadata`
 * name a feed (`Hot`, `Recent`, `PlaylistById` + an id, `CarouselEndpoint` + a slug, …)
 * that the client resolves against the `rooms`/`api` workers itself — this worker only
 * says WHICH carousels a page has and in what order, never their contents.
 */
export const DiscoverySection = z.object({
	id: z.string().describe('Unique id of this section on this page, e.g. `Rooms_RRO_WatchHome`'),
	sectionType: z
		.int()
		.describe(
			'What the section lists: 0 RoomsSection · 1 AccountsSection · 2 InventionsSection · ' +
				'3 ClubsSection · 4 StoreItemsSection · 5 EventsSection · 6 RoomBanner · 7 Top5Section · ' +
				'8 CustomAvatarItemsSection · 9 AdsSection · 10 SkusSection · 11 RoomCategorySection · ' +
				'12 RoomCategoryListSection · 13 DiscoverySection'
		),
	sectionSubType: z.string().describe('The section’s kind, shared across pages, e.g. `Rooms_RRO`'),
	source: z.string().describe('The feed that fills the section'),
	sourceMetadata: z
		.string()
		.nullable()
		.describe('Argument to `source` — a carousel slug, a playlist id, … `null` when it takes none'),
	displayMetadata: z
		.string()
		.nullable()
		.describe(
			'How the section is drawn (`DisplayTitle`, `numRows`, `backgroundColor`, …), as an ' +
				'embedded JSON *string* the client parses itself — not an object. Its booleans and ' +
				'numbers are quoted in most sections and bare in some; both forms are live in the ' +
				'captures, so the client evidently takes either. `null` where a section is drawn ' +
				'however its type says (several store and play-highlight sections).'
		),
})

/**
 * A list of sections — `GET /sections/pagesource/{type}` in the order a page draws them,
 * or `GET /sections/bulk` in the catalogue's order.
 *
 * The DTO accepts anything (its validator is a no-op), but the STORE page builder is much
 * stricter and drops a section it doesn't like SILENTLY — no error reaches the client, the
 * carousel simply isn't there. A section it keeps must have:
 *
 * - a non-empty `displayMetadata` that parses, and whose gating (platform, junior account,
 *   account age) says it's supported;
 * - `sectionType` 4 (StoreItemsSection → the product carousel) or 13 (DiscoverySection).
 *   Anything else logs "Unhandled SectionType {0}, skipping";
 * - for 13 only, a `source` of exactly `CuratedList` or `PageSource`, with `sourceMetadata`
 *   carrying the argument — a curated-list id, or another page source to recurse into.
 *
 * So a store section that renders nothing is worth checking against these before assuming
 * the feed behind it is empty.
 */
export const DiscoverySections = DiscoverySection.array()
