<?php
/**
 * View-model → array dumper.
 *
 * Pure-function service that converts the §8.2
 * {@see \CommerceLinkBuilder\Application\Render\ViewModel\ProductCardViewModel}
 * and §8.4
 * {@see \CommerceLinkBuilder\Application\Render\ViewModel\FallbackCardViewModel}
 * aggregator value objects into the read-only associative-array
 * shapes templates consume per `specifications/rendering-and-template-spec.md`
 * §6.5 ("`$viewModel` は読取専用の配列コピー") and §8.2 / §8.4
 * (the canonical structure listings).
 *
 * The dumper isolates serialization from rendering so:
 *
 *   - Templates only ever see plain associative arrays. They
 *     never receive a ViewModel object reference, so accidental
 *     mutation of renderer-internal state from a template is
 *     structurally impossible.
 *
 *   - The {@see \CommerceLinkBuilder\Application\Render\ProductCardRenderer}
 *     can be unit-tested by inspecting the array the dumper
 *     produced rather than parsing the rendered HTML.
 *
 *   - The §8.2 / §8.4 ViewModels stay free of templating
 *     concerns: no `toArray()` method on the aggregator means a
 *     spec change to the array shape lands here, not on the
 *     ViewModel.
 *
 * The top-level key set and ordering of every dumped array is
 * the §8.2 / §8.4 spec contract. Adding, removing, or reordering
 * a top-level key is a breaking change for theme template
 * overrides and MUST be reflected in the spec first.
 *
 * The "deeply read-only" property is enforced by convention, not
 * by the type system: PHP has no language-level support for
 * read-only arrays at runtime. Callers (the renderer and the
 * `clb_template()` helper) pass the array by value into the
 * template, and templates do not keep references back into the
 * dumper's output, so mutation cannot escape a single render.
 *
 * The dumper additionally produces the §8.3
 * {@see \CommerceLinkBuilder\Application\Render\ViewModel\ProductGridViewModel}
 * array shape, which composes per-item card / fallback shapes via
 * {@see self::dump()} so the same serializer drives both the
 * single-card and multi-card surfaces.
 *
 * @package CommerceLinkBuilder\Application\Render
 */

declare(strict_types=1);

namespace CommerceLinkBuilder\Application\Render;

use CommerceLinkBuilder\Application\Render\ViewModel\FallbackCardViewModel;
use CommerceLinkBuilder\Application\Render\ViewModel\Parts\BadgeView;
use CommerceLinkBuilder\Application\Render\ViewModel\Parts\ButtonView;
use CommerceLinkBuilder\Application\Render\ViewModel\Parts\CssClasses;
use CommerceLinkBuilder\Application\Render\ViewModel\Parts\ImageSet;
use CommerceLinkBuilder\Application\Render\ViewModel\Parts\ImageView;
use CommerceLinkBuilder\Application\Render\ViewModel\ProductCardViewModel;
use CommerceLinkBuilder\Application\Render\ViewModel\ProductGridViewModel;

/**
 * Stateless serializer that produces the §8.2 / §8.3 / §8.4
 * read-only array shapes templates consume.
 *
 * See the file-level docblock for the rationale and the
 * spec-contract constraints on the produced shapes.
 */
final class ViewModelArrayDumper {

	/**
	 * Convert a §8.2, §8.3, or §8.4 view model into the spec
	 * array shape.
	 *
	 * The returned array is the canonical read-only payload the
	 * §6.5 `clb_template()` helper and the
	 * {@see \CommerceLinkBuilder\Application\Render\ProductCardRenderer}
	 * / {@see \CommerceLinkBuilder\Application\Render\ProductGridRenderer}
	 * forward to the resolved template file. The top-level key
	 * order matches §8.2 / §8.3 / §8.4 verbatim; deviating from
	 * that order is a breaking change for theme overrides.
	 *
	 * @param ProductCardViewModel|FallbackCardViewModel|ProductGridViewModel $viewModel View model to serialize.
	 *
	 * @return array<string, mixed>
	 */
	public function dump( ProductCardViewModel|FallbackCardViewModel|ProductGridViewModel $viewModel ): array {
		if ( $viewModel instanceof ProductCardViewModel ) {
			return $this->dumpProductCard( $viewModel );
		}

		if ( $viewModel instanceof ProductGridViewModel ) {
			return $this->dumpProductGrid( $viewModel );
		}

		return $this->dumpFallbackCard( $viewModel );
	}

	/**
	 * Produce the §8.3 ProductGridViewModel array shape.
	 *
	 * Each entry of the §8.3 `items` slot is recursively
	 * serialized via {@see self::dump()} so the per-item shape
	 * agrees with the §8.2 / §8.4 single-card output verbatim.
	 * The `meta.count` field mirrors `count(items)` so the
	 * derived count cannot drift out of sync with the items list.
	 *
	 * @param ProductGridViewModel $vm Source view model.
	 *
	 * @return array<string, mixed>
	 */
	private function dumpProductGrid( ProductGridViewModel $vm ): array {
		$meta  = $vm->meta();
		$items = array();
		foreach ( $vm->items() as $item ) {
			$items[] = $this->dump( $item );
		}

		$classes = $vm->classes();
		$layout  = $vm->layout();

		return array(
			'type'        => $vm->type(),
			'template'    => $vm->template(),
			'layout'      => array(
				'columns' => $layout->columns(),
				'gap'     => $layout->gap(),
			),
			'items'       => $items,
			'meta'        => array(
				'count'               => count( $items ),
				'show_brand'          => $meta->showBrand(),
				'show_description'    => $meta->showDescription(),
				'show_price'          => $meta->showPrice(),
				'show_provider_badge' => $meta->showProviderBadge(),
			),
			'classes'     => array(
				'root' => $classes->root(),
				'item' => $classes->item(),
			),
			'is_fallback' => false,
		);
	}

	/**
	 * Produce the §8.2 ProductCardViewModel array shape.
	 *
	 * @param ProductCardViewModel $vm Source view model.
	 *
	 * @return array<string, mixed>
	 */
	private function dumpProductCard( ProductCardViewModel $vm ): array {
		$product = $vm->product();
		$price   = $vm->price();
		$avail   = $vm->availability();
		$meta    = $vm->meta();

		return array(
			'type'         => $vm->type(),
			'template'     => $vm->template(),
			'product'      => array(
				'id'               => $product->id(),
				'provider'         => $product->provider(),
				'provider_label'   => $product->providerLabel(),
				'provider_item_id' => $product->providerItemId(),
				'title'            => $product->title(),
				'brand'            => $product->brand(),
				'description'      => $product->description(),
				'detail_url'       => $product->detailUrl(),
				'affiliate_url'    => $product->affiliateUrl(),
			),
			'images'       => $this->dumpImageSet( $vm->images() ),
			'price'        => array(
				'visible'             => $price->visible(),
				'label'               => $price->label(),
				'amount'              => $price->amount(),
				'currency'            => $price->currency(),
				'observed_at_label'   => $price->observedAtLabel(),
				'disclaimer_required' => $price->disclaimerRequired(),
			),
			'availability' => array(
				'visible' => $avail->visible(),
				'status'  => $avail->status(),
				'label'   => $avail->label(),
			),
			'buttons'      => $this->dumpButtons( $vm->buttons() ),
			'badges'       => $this->dumpBadges( $vm->badges() ),
			'disclaimers'  => array_values( $vm->disclaimers() ),
			'meta'         => array(
				'show_brand'          => $meta->showBrand(),
				'show_description'    => $meta->showDescription(),
				'show_price'          => $meta->showPrice(),
				'show_provider_badge' => $meta->showProviderBadge(),
			),
			'classes'      => $this->dumpClasses( $vm->classes() ),
			'is_fallback'  => $vm->isFallback(),
		);
	}

	/**
	 * Produce the §8.4 FallbackCardViewModel array shape.
	 *
	 * @param FallbackCardViewModel $vm Source view model.
	 *
	 * @return array<string, mixed>
	 */
	private function dumpFallbackCard( FallbackCardViewModel $vm ): array {
		$primary = $vm->primaryButton();

		return array(
			'type'           => $vm->type(),
			'template'       => $vm->template(),
			'reason'         => $vm->reason(),
			'title'          => $vm->title(),
			'primary_button' => $primary === null ? null : $this->dumpButton( $primary ),
			'note'           => $vm->note(),
			'classes'        => $this->dumpClasses( $vm->classes() ),
			'is_fallback'    => $vm->isFallback(),
		);
	}

	/**
	 * Produce the §8.2 `images` array shape.
	 *
	 * @param ImageSet $images Source image set.
	 *
	 * @return array{primary: array<string, mixed>|null, gallery: list<array<string, mixed>>}
	 */
	private function dumpImageSet( ImageSet $images ): array {
		$primary = $images->primary();
		$gallery = array();
		foreach ( $images->gallery() as $image ) {
			$gallery[] = $this->dumpImage( $image );
		}

		return array(
			'primary' => $primary === null ? null : $this->dumpImage( $primary ),
			'gallery' => $gallery,
		);
	}

	/**
	 * Produce the §8.2 single-image array shape.
	 *
	 * @param ImageView $image Source image view.
	 *
	 * @return array{url: string, alt: string, width: int|null, height: int|null, loading: string}
	 */
	private function dumpImage( ImageView $image ): array {
		return array(
			'url'     => $image->url(),
			'alt'     => $image->alt(),
			'width'   => $image->width(),
			'height'  => $image->height(),
			'loading' => $image->loading(),
		);
	}

	/**
	 * Produce the §8.2 buttons-list array shape.
	 *
	 * @param list<ButtonView> $buttons Source button list.
	 *
	 * @return list<array<string, mixed>>
	 */
	private function dumpButtons( array $buttons ): array {
		$result = array();
		foreach ( $buttons as $button ) {
			$result[] = $this->dumpButton( $button );
		}
		return $result;
	}

	/**
	 * Produce the §8.2 single-button array shape.
	 *
	 * @param ButtonView $button Source button view.
	 *
	 * @return array<string, mixed>
	 */
	private function dumpButton( ButtonView $button ): array {
		return array(
			'provider'   => $button->provider(),
			'label'      => $button->label(),
			'url'        => $button->url(),
			'target'     => $button->target(),
			'rel'        => $button->rel(),
			'class_name' => $button->className(),
			'aria_label' => $button->ariaLabel(),
		);
	}

	/**
	 * Produce the §8.2 badges-list array shape.
	 *
	 * @param list<BadgeView> $badges Source badge list.
	 *
	 * @return list<array<string, mixed>>
	 */
	private function dumpBadges( array $badges ): array {
		$result = array();
		foreach ( $badges as $badge ) {
			$result[] = array(
				'kind'       => $badge->kind(),
				'label'      => $badge->label(),
				'class_name' => $badge->className(),
			);
		}
		return $result;
	}

	/**
	 * Produce the §8.2 / §8.4 `classes` array shape.
	 *
	 * @param CssClasses $classes Source classes view.
	 *
	 * @return array{root: string, image: string, body: string, title: string, price: string, buttons: string}
	 */
	private function dumpClasses( CssClasses $classes ): array {
		return array(
			'root'    => $classes->root(),
			'image'   => $classes->image(),
			'body'    => $classes->body(),
			'title'   => $classes->title(),
			'price'   => $classes->price(),
			'buttons' => $classes->buttons(),
		);
	}
}
