/**
 * The wire contract — the exact shape of what `admin-ajax.php` answers with.
 *
 * These are DTOs, not engine types: one type per PHP class in
 * `lib/weGraph-wp-plugin-libs/src/`, named for the wire and mapped into the
 * engine's own types by `src/adapter/WorkspaceAdapter.ts`. Nothing outside
 * `src/fetch/` and `src/adapter/` should refer to them — the rest of the
 * frontend sees engine types only.
 *
 * Serialisation is plain `wp_send_json($response)`, so a public PHP property is
 * a key here and nothing else is (see README → Routing, authz and
 * serialisation). **Anything added to the PHP payload classes changes these
 * types**; keep them in step.
 */

/**
 * A PHP array keyed by id, as JSON.
 *
 * Note the one edge case: an *empty* PHP array serialises as `[]`, not `{}`, so
 * an empty library arrives as `records: []`. `Object.entries()` reads both, and
 * is what `weBWireEntries` is built on.
 */
export type WeBWireMap< ItemType > = { [ id: string ]: ItemType };

/* -------------------------------------------------------------------------- */
/* Errors                                                                      */
/* -------------------------------------------------------------------------- */

/**
 * `WeBWPResponseError` — every failure except one, at 400/401/403/404/500/503.
 *
 * `code` names the failure where a message alone cannot be acted on:
 * `not-connected` (401), `not-registered` / `consent_required` / `forbidden`
 * (403), `not-found` (404), `invalid` (400), `unavailable` (503). It is `''`
 * where nothing more specific applies, and absent from responders which
 * predate it — hence optional.
 */
export type WeBWireError = {
	code?: string;
	error: true;
	message: string;
};

/**
 * The exception: a failed nonce answers `wp_send_json_error()`'s default shape
 * at **HTTP 200**, so a client which tests the status alone reads it as a
 * successful response. See `ButtercupWP::api_call()`.
 */
export type WeBWireNonceError = {
	success: false;
	data: string;
};

/* -------------------------------------------------------------------------- */
/* Library (`libraryProps`) — WeBLibrary and its four maps                     */
/* -------------------------------------------------------------------------- */

/**
 * An attribute value. Scalars only — PHP emits whatever the site held, so a
 * key whose value arrived as an array or an object is not one of these, and
 * `weBWireScalarEntries()` drops it.
 */
export type WeBWireAttributeValue = string | number | boolean | null;

/**
 * `WeBAttributeType`. `kind` is a string-backed enum in PHP, in the engine's
 * own spellings (`date` / `string` / `number` / `boolean`), and is typed as a
 * plain string here because the wire cannot promise which of them arrived.
 */
export type WeBWireAttributeType = {
	id: string;
	kind: string;
	label: string;
};

export type WeBWireRecord = {
	/** The record's own facts, keyed by attribute id */
	attributes: WeBWireMap< WeBWireAttributeValue >;
	id: string;
	label: string;
	/** The WordPress id behind a composite record id, e.g. 12 for `post-12` */
	originId: string | number | null;
	typeId: string;
	url: string | null;
};

export type WeBWireRecordType = {
	/** What a record of this type may carry, keyed by attribute id */
	attributeTypes: WeBWireMap< WeBWireAttributeType >;
	/** Six-character hexadecimal — the engine calls this `colour` */
	colourHex: string;
	id: string;
	/**
	 * `post` / `term` / `user` — a string-backed enum in PHP (`WeBRecordKind`),
	 * typed as a plain string here like `WeBWireAttributeType.kind` because the
	 * wire cannot promise which arrived. Absent from a server which predates it.
	 */
	kind?: string;
	label: string;
	originId: string | number | null;
	/** Names an `attributeTypes` entry of kind `date`, or is null */
	timeAttributeId: string | null;
};

export type WeBWireConnection = {
	id: string;
	recordAId: string;
	recordBId: string;
	typeId: string;
};

/** `WeBConnectionEndpoint`. The engine has no notion of multiplicity. */
export type WeBWireConnectionEndpoint = {
	label: string;
	multiplicity: string;
	/** A record type id */
	typeId: string;
};

export type WeBWireConnectionType = {
	id: string;
	/** `association` or `hierarchy`; anything else adapts to `association` */
	kind: string;
	label: string;
	recordA: WeBWireConnectionEndpoint;
	recordB: WeBWireConnectionEndpoint;
};

export type WeBWireLibrary = {
	connections: WeBWireMap< WeBWireConnection >;
	connectionTypes: WeBWireMap< WeBWireConnectionType >;
	records: WeBWireMap< WeBWireRecord >;
	recordTypes: WeBWireMap< WeBWireRecordType >;
};

/* -------------------------------------------------------------------------- */
/* Map (`mapProps`) — derived from the library by WeBLibrary::getMap()         */
/* -------------------------------------------------------------------------- */

export type WeBWireCoordinate = {
	x: number;
	y: number;
};

/** A node id **is** its record id. */
export type WeBWireNode = {
	coordinate: WeBWireCoordinate;
	id: string;
	label: string;
	typeId: string;
};

export type WeBWireEdge = {
	id: string;
	nodeAId: string;
	nodeBId: string;
	typeId: string;
};

export type WeBWireNodeType = {
	colourHex: string;
	id: string;
	label: string;
};

export type WeBWireEdgeType = WeBWireNodeType;

export type WeBWireMapProps = {
	edges: WeBWireMap< WeBWireEdge >;
	edgeTypes: WeBWireMap< WeBWireEdgeType >;
	nodes: WeBWireMap< WeBWireNode >;
	nodeTypes: WeBWireMap< WeBWireNodeType >;
};

/* -------------------------------------------------------------------------- */
/* Config and funnel                                                           */
/* -------------------------------------------------------------------------- */

/**
 * `WeBConfig`. Only `coordinateRadius`, `nodeDetail` and `nodeRadius` have
 * engine equivalents — the engine's `WeBConfigProps` has no colour keys at all,
 * and the label belongs to the workspace, not to the graph.
 *
 * `nodeDetail` is the reader's, saved and returned with the save; the rest of
 * this is the site's.
 */
export type WeBWireConfig = {
	accentColourHex: string;
	bgColourHex: string;
	coordinateRadius: number;
	label: string;
	nodeDetail: number;
	nodeRadius: number;
};

/**
 * `WeBFilter`. `mode` is a string-backed enum on the wire, in the same
 * spellings the engine's `WeBFunnelFilterProps` uses.
 *
 * `recordIds` is meaningful only for `include` and `exclude`; a filter naming
 * no `recordTypeId` is transparent, and `related` is held false on the filter
 * at the head of the stack.
 *
 * The PHP is on this shape too, since #25661: `WeBFilter` carries
 * `mode`/`related` and the old `filterType` enum is gone.
 */
export type WeBWireFilter = {
	mode: 'all' | 'include' | 'exclude';
	recordIds: string[];
	recordTypeId: string;
	related: boolean;
};

/** Filters stack in order, so this is a list rather than a map. */
export type WeBWireFunnel = {
	filters: WeBWireFilter[];
};

/* -------------------------------------------------------------------------- */
/* Presentation (`presentationProps`) — what the product offers                */
/* -------------------------------------------------------------------------- */

/**
 * One layout a product offers. `kind` is the engine's closed set and `id` is
 * the product's own, so two trees under two ids are two layouts.
 *
 * Flat rather than a union on the wire: PHP emits one class per layout with the
 * keys its kind uses, and a key another kind would use is simply absent.
 *
 * A time series' two rows are **record types**, and they are the filter stack
 * the layout opens with rather than options of it; so are a cluster's member
 * types. Since #25946 PHP emits every key for every kind, null or `[]` where
 * the kind has no use for it, so a reader need not ask which kind it has
 * before reading a key — but each stays optional here, because a server which
 * predates a key sends nothing under it.
 */
export type WeBWirePresentationLayout = {
	aboveRecordTypeId?: string | null;
	/**
	 * `cluster`: one of the engine's arrangements — `islands`, `hubs` or
	 * `columns` — as the product wrote it, typed as a plain string because the
	 * wire cannot promise which arrived. The product's, not the reader's: it
	 * is on no save-option whitelist (#25946).
	 */
	arrangement?: string | null;
	axisRecordTypeId?: string | null;
	belowRecordTypeId?: string | null;
	/**
	 * `timeSeries`: what the axis is laid along — `time` or `label` — as the
	 * product wrote it (#25970). Typed `unknown` like the member list: the
	 * adapter reads the one word it knows and defaults the rest to `time`,
	 * which is the dimension a server from before the key sent without saying.
	 */
	dimension?: unknown;
	/**
	 * `cluster`: the hub type for `hubs`; null for `islands` and `columns`,
	 * which group by record type. `tree`: the type whose records the roots are
	 * grouped by, each group in a labelled region, or null for one forest
	 * (#25983).
	 */
	groupRecordTypeId?: string | null;
	hierarchyConnectionTypeId?: string | null;
	id: string;
	kind: string;
	label: string;
	/** `timeSeries`: the record type the axis is split into lanes by, or null for one lane (#25945) */
	laneRecordTypeId?: string | null;
	/**
	 * `cluster`: the record types the stack is seeded with, in order. A
	 * string list from PHP, typed `unknown` because a filter-written manifest
	 * may put anything there; the adapter keeps the strings.
	 */
	memberRecordTypeIds?: unknown;
	/**
	 * `cluster`: the members whose filter opens with `related` on, a subset of
	 * `memberRecordTypeIds` (#25970). Absent on a server from before the key,
	 * and the adapter leaves it absent then rather than sending `[]`: the
	 * engine's validator reads an absent list as "what the arrangement seeded
	 * before there was one" — every member for `hubs`, none for the rest —
	 * whereas `[]` is a product saying to relate nothing.
	 */
	relatedRecordTypeIds?: unknown;
	treeRecordTypeId?: string | null;
};

export type WeBWirePresentation = {
	defaultLayoutId: string;
	layouts: WeBWirePresentationLayout[];
	/** The detail level the graph opens at — the whole of what a product says about node shape */
	node: {
		detail: number;
	};
};

/* -------------------------------------------------------------------------- */
/* Route responses                                                             */
/* -------------------------------------------------------------------------- */

/**
 * `WeBWorkspaceWithFunnel` — the 200 of `library/get`: the live library laid
 * out as a draft, so **`id` is `''`**. The adapter reads that as "no identity
 * yet" (`WeBWorkspacePopulate.id: null`).
 */
export type WeBWorkspaceResponse = {
	configProps: WeBWireConfig;
	funnelProps: WeBWireFunnel;
	id: string;
	libraryProps: WeBWireLibrary;
	mapProps: WeBWireMapProps;
	/**
	 * What the product offers. Absent from a server which predates it, which
	 * the adapter reads as the default manifest — the free layout and circles.
	 */
	presentationProps?: WeBWirePresentation;
};

/**
 * `WeBWPWorkspaceSaved` — the 200 of `workspace/get`: the live library with
 * the latest save's overlays applied (or a named older save's, when the
 * request carried `saveId`). The saved cells arrive twice — on
 * `mapProps.nodes[*].coordinate` and as this top-level `coordinates` map — and
 * the adapter prefers the map, because a record present in the library but
 * absent from it is one the save never placed.
 */
export type WeBWorkspaceSavedResponse = WeBWorkspaceResponse & {
	/** The saved layout: which was active, what was set on each, and each one's cells */
	layout?: {
		activeLayoutId: string;
		coordinates: WeBWireMap< WeBWireMap< WeBWireCoordinate > >;
		options: WeBWireMap< { [ key: string ]: string } >;
	};
	saveId?: string;
	/** ISO-8601 */
	savedAt?: string;
};

export type WeBWorkspaceListItem = {
	id: string;
	label: string;
	/** ISO-8601; the latest save's time */
	updatedAt?: string;
};

/**
 * `workspace/list`. **`items` is an array here** — the store appends with
 * `$items[]` — where the record-info list's `items` is keyed by id. The two are
 * typed separately for that reason.
 */
export type WeBWorkspaceListResponse = {
	items: WeBWorkspaceListItem[];
};

/**
 * `WeBWPSaveItem` — one complete save in a workspace's history. The counts
 * are the snapshot's (the visible graph as saved); null when the server did
 * not know them.
 */
export type WeBWorkspaceSaveListItem = {
	connectionCount: number | null;
	id: string;
	recordCount: number | null;
	/** ISO-8601; null when the server did not say */
	savedAt: string | null;
};

/**
 * `WeBWPSaveList` — the 200 of `workspace/saves`. Newest first; the first
 * item is the save `workspace/get` opens when given no `saveId`.
 */
export type WeBWorkspaceSaveListResponse = {
	items: WeBWorkspaceSaveListItem[];
};

/* -------------------------------------------------------------------------- */
/* Save (`workspace/save`)                                                     */
/* -------------------------------------------------------------------------- */

/**
 * What the browser posts as `state` — the only thing the server cannot rebuild
 * from the site. Read by `WeBWPSaveState` (PHP), which validates field by
 * field and falls back per field, so a key it does not recognise is skipped
 * rather than fatal.
 *
 * Produced by `src/adapter/SaveAdapter.ts` from `WeBWorkspace.saveGet()`; the
 * one wire type that travels the other way. The filter shape is the same
 * `WeBWireFilter` the funnel arrives as.
 */
export type WeBWireSave = {
	config: {
		colourSchemeKey: 'auto' | 'dark' | 'light';
		coordinateRadius: number;
		/** The reader's detail level, which comes back on `configProps` at reopen */
		nodeDetail: number;
		nodeRadius: number;
	};
	funnel: WeBWireFunnel;
	/**
	 * The active layout, every layout's options, and every layout's cells.
	 *
	 * Keyed by layout id rather than flat, because a cell address means nothing
	 * without the grid it is addressed in and two layouts may be on two grids.
	 */
	layout: {
		activeLayoutId: string;
		coordinates: WeBWireMap< WeBWireMap< WeBWireCoordinate > >;
		options: WeBWireMap< { [ key: string ]: string } >;
	};
	label: string;
	/** `#rrggbb`, keyed by record type id — browser-only edits that must travel */
	recordTypeColours: WeBWireMap< string >;
	/**
	 * The records on screen. The server cuts the snapshot's `library.records`
	 * (and the connections and coordinates that hang off them) down to these;
	 * a client that omits the key gets the whole library, as before.
	 */
	visibleRecordIds: string[];
};

/**
 * The 200 of `workspace/save`. `id` and `workspaceId` are the same value — the
 * workspace's identity, minted when `graphId` was absent from the request.
 * `deduplicated` is true when the cloud already held an identical latest save
 * and made no new one, in which case `saveId` is that save's.
 *
 * `status` is `processing` when the cloud is still validating a large save on
 * its queue and the server's own poll ran out of time; `savedAt` is then null
 * and the browser polls `workspace/save/status` with `saveId`.
 */
export type WeBWorkspaceSaveResponse = {
	deduplicated: boolean;
	id: string;
	label: string;
	saveId: string;
	/** ISO-8601; null while `processing` */
	savedAt: string | null;
	status: 'complete' | 'processing';
	workspaceId: string;
};

/**
 * `WeBWPSaveStatus` — the 200 of `workspace/save/status`. `status` is the
 * cloud's own; `savedAt` is set once `complete`, `failureCode` and
 * `failureMessage` once `failed`, and all three are null otherwise.
 */
export type WeBWorkspaceSaveStatusResponse = {
	failureCode: string | null;
	failureMessage: string | null;
	saveId: string;
	/** ISO-8601 */
	savedAt: string | null;
	status: 'complete' | 'failed' | 'processing' | 'uploading';
};

/**
 * `workspace/library/record/get` — per-record display data the library does not
 * carry. An id which resolves to nothing is **omitted** rather than given an
 * empty entry, so a consumer must handle a missing key.
 *
 * This is also the contract the info bar consumes (`WeBRecordInfoSource`),
 * which is why it lives here and is re-exported by `RecordInfo.stub.ts`: the
 * stub and the real fetch answer with the same shape.
 */
export type WeBResponseRecordInfoItem = {
	description: string;
	/** The `thumbnail` size (150px) for a post's card; null for a post without one, and for a term or a user */
	thumbnailSmallSrc?: string | null;
	thumbnailSrc?: string | null;
};

export type WeBResponseRecordInfoList = {
	items: { [ id: string ]: WeBResponseRecordInfoItem };
};

export type WeBRecordInfoResponse = WeBResponseRecordInfoList;

/**
 * The one method the info bar needs. `WeBRoutes` implements it alongside its
 * workspace methods, so the info bar can be handed either it or the stub.
 */
export interface WeBRecordInfoSource {
	getRecordInfo: (
		recordIds: readonly string[]
	) => Promise< WeBResponseRecordInfoList >;
}
