<?php
/**
 * TitanCart Pages REST Controller
 *
 * Admin-only surface powering the Storefront → Pages manager (roadmap #2,
 * Page Manager — Session 2). Wraps the canonical PageRegistry (S343) so the
 * admin SPA can:
 *   - list every TitanCart-aware storefront page with its live hosting status,
 *   - bulk / single "create missing" (idempotent adopt-or-create), and
 *   - re-point a shortcode feature page at an existing published WP page.
 *
 *   GET  /titancart/v1/pages
 *        → { pages:[ decorated row ], available:[ {id,title} ] }
 *   POST /titancart/v1/pages/ensure
 *        → body optional { key }; no key = bulk create-missing over the
 *          option-bound shortcode pages, a key = single ensure of that page.
 *          Returns { ok, summary:{created,adopted}, pages, available }.
 *   PUT  /titancart/v1/pages/(?P<key>[A-Za-z0-9_-]+)/assign
 *        → body { page_id }; binds the descriptor's titancart_page_* option to
 *          an existing published page. Returns { ok, row, pages, available }.
 *
 * manage_options-gated. Does NOT extend WP_REST_Controller (project REST
 * gotcha), so its custom handlers keep typed WP_REST_Request params safely.
 *
 * PORTABILITY (Rule 4): all page-hosting knowledge lives in PageRegistry's
 * isolated hosting layer; this controller only shapes the REST envelope and
 * reads a small set of WP page facts (title/permalink/edit link) for display.
 *
 * @package TitanCart\REST\Pages
 */

namespace TitanCart\REST\Pages;

use TitanCart\Modules\Pages\PageRegistry;
use WP_REST_Request;
use WP_REST_Response;

defined( 'ABSPATH' ) || exit;

class PagesController {

	private string $ns = 'titancart/v1';

	public function admin_check(): bool {
		return current_user_can( 'manage_options' );
	}

	public function register_routes(): void {

		register_rest_route( $this->ns, '/pages', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'list_pages' ],
			'permission_callback' => [ $this, 'admin_check' ],
		] );

		register_rest_route( $this->ns, '/pages/ensure', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'ensure_pages' ],
			'permission_callback' => [ $this, 'admin_check' ],
		] );

		register_rest_route( $this->ns, '/pages/(?P<key>[A-Za-z0-9_\-]+)/assign', [
			'methods'             => 'PUT',
			'callback'            => [ $this, 'assign_page' ],
			'permission_callback' => [ $this, 'admin_check' ],
		] );
	}

	/**
	 * GET /pages — the full registry decorated with live hosting status +
	 * the list of published pages that populate the assignment selects.
	 */
	public function list_pages( WP_REST_Request $req ): WP_REST_Response {
		return new WP_REST_Response( [
			'pages'     => $this->decorated_rows(),
			'available' => $this->available_pages(),
		] );
	}

	/**
	 * POST /pages/ensure — idempotent create-missing.
	 *
	 * No key = bulk: ensure every creatable OPTION-BOUND page (the shortcode
	 * feature pages). Structural pages (homepage/blog) are deliberately skipped
	 * here — their reading-settings binding is owned by Migration 23 — mirroring
	 * PageRegistry::ensure_all( bound_only:true ).
	 *
	 * A key = single ensure of that descriptor. Rewrite (virtual) and structural
	 * pages are rejected: rewrite pages are never WP pages, and structural pages
	 * are managed via the WordPress reading settings (v1: read-only in the UI).
	 */
	public function ensure_pages( WP_REST_Request $req ): WP_REST_Response {
		$key = sanitize_key( (string) $req->get_param( 'key' ) );

		if ( '' !== $key ) {
			$descriptor = PageRegistry::get( $key );
			if ( ! $descriptor ) {
				return $this->error( 'tc_pages_unknown_key', 'Unknown page key.', 404 );
			}
			if ( ! PageRegistry::is_creatable( $descriptor ) ) {
				return $this->error( 'tc_pages_not_creatable', 'This page is served by rewrite rules and does not need a WordPress page.', 400 );
			}
			if ( 'structural' === $descriptor['type'] ) {
				return $this->error( 'tc_pages_structural', 'Homepage and Blog are bound via the WordPress reading settings and are managed there.', 400 );
			}

			$before  = PageRegistry::resolve_page_id( $descriptor );
			$page_id  = PageRegistry::ensure( $descriptor );
			$summary  = [
				'created' => ( $before <= 0 && $page_id > 0 ) ? 1 : 0,
				'adopted' => ( $before > 0 && $page_id > 0 ) ? 1 : 0,
			];
		} else {
			$created = 0;
			$adopted = 0;
			foreach ( PageRegistry::all() as $descriptor ) {
				if ( ! PageRegistry::is_creatable( $descriptor ) ) {
					continue;
				}
				// Structural pages carry no titancart_page_* option — skip them so
				// the front-page / posts-page binding (Migration 23) is untouched.
				if ( '' === $descriptor['option'] ) {
					continue;
				}
				$before  = PageRegistry::resolve_page_id( $descriptor );
				$page_id = PageRegistry::ensure( $descriptor );
				if ( $page_id > 0 ) {
					if ( $before > 0 ) {
						$adopted++;
					} else {
						$created++;
					}
				}
			}
			$summary = [ 'created' => $created, 'adopted' => $adopted ];
		}

		return new WP_REST_Response( [
			'ok'        => true,
			'summary'   => $summary,
			'pages'     => $this->decorated_rows(),
			'available' => $this->available_pages(),
		] );
	}

	/**
	 * PUT /pages/{key}/assign — re-point a feature at an existing published page
	 * by writing its titancart_page_* option.
	 *
	 * Only valid for option-bound shortcode pages. Rewrite pages and structural
	 * (homepage/blog) pages are option-less and therefore not re-assignable here
	 * (structural reassignment would rebind the WordPress reading settings, which
	 * v1 defers to keep the front page safe).
	 */
	public function assign_page( WP_REST_Request $req ): WP_REST_Response {
		$key        = sanitize_key( (string) $req['key'] );
		$descriptor = PageRegistry::get( $key );

		if ( ! $descriptor ) {
			return $this->error( 'tc_pages_unknown_key', 'Unknown page key.', 404 );
		}
		if ( '' === $descriptor['option'] || 'shortcode' !== $descriptor['type'] ) {
			return $this->error( 'tc_pages_not_assignable', 'This page cannot be re-assigned to a WordPress page.', 400 );
		}

		$page_id = (int) $req->get_param( 'page_id' );
		if ( $page_id <= 0 ) {
			return $this->error( 'tc_pages_bad_page', 'A published page must be selected.', 400 );
		}

		$post = get_post( $page_id );
		if ( ! $post || 'page' !== $post->post_type || 'publish' !== $post->post_status ) {
			return $this->error( 'tc_pages_bad_page', 'The selected page must be a published WordPress page.', 400 );
		}

		update_option( $descriptor['option'], $page_id, false );

		return new WP_REST_Response( [
			'ok'        => true,
			'row'       => $this->decorate( $descriptor ),
			'pages'     => $this->decorated_rows(),
			'available' => $this->available_pages(),
		] );
	}

	// ══════════════════════════════════════════════════════════════════════════
	// Helpers
	// ══════════════════════════════════════════════════════════════════════════

	/**
	 * Every registry descriptor, decorated with live hosting status.
	 *
	 * @return array<int,array<string,mixed>>
	 */
	private function decorated_rows(): array {
		$rows = [];
		foreach ( PageRegistry::all() as $descriptor ) {
			$rows[] = $this->decorate( $descriptor );
		}
		return $rows;
	}

	/**
	 * One decorated row: the registry descriptor plus its resolved host id,
	 * status, page title, edit/view URLs, and the creatable/assignable flags
	 * the SPA branches on.
	 *
	 *   status: publish|draft|pending|private|trash|missing for creatable pages;
	 *           'rewrite' for virtual (rewrite-rule-backed) pages.
	 *   assignable: true only for option-bound shortcode pages (the sole kind the
	 *               admin can re-point). Structural + rewrite are read-only.
	 *
	 * @param  array<string,string> $descriptor
	 * @return array<string,mixed>
	 */
	private function decorate( array $descriptor ): array {
		$creatable = PageRegistry::is_creatable( $descriptor );
		$page_id   = $creatable ? PageRegistry::resolve_page_id( $descriptor ) : 0;
		$status    = $creatable ? PageRegistry::page_status( $page_id ) : 'rewrite';

		$assignable = ( 'shortcode' === $descriptor['type'] && '' !== $descriptor['option'] );

		$edit_url   = '';
		$view_url   = '';
		$page_title = '';
		if ( $page_id > 0 ) {
			$edit_link  = get_edit_post_link( $page_id, 'raw' );
			$edit_url   = $edit_link ? (string) $edit_link : '';
			$permalink  = get_permalink( $page_id );
			$view_url   = $permalink ? (string) $permalink : '';
			$page_title = (string) get_the_title( $page_id );
		}

		return [
			'key'         => $descriptor['key'],
			'label'       => $descriptor['label'],
			'description' => $descriptor['description'],
			'type'        => $descriptor['type'],
			'shortcode'   => $descriptor['shortcode'],
			'option'      => $descriptor['option'],
			'owner'       => $descriptor['owner'],
			'status'      => $status,
			'page_id'     => $page_id,
			'page_title'  => $page_title,
			'edit_url'    => $edit_url,
			'view_url'    => $view_url,
			'creatable'   => $creatable,
			'assignable'  => $assignable,
		];
	}

	/**
	 * Published WP pages for the assignment selects: { id, title }, title-sorted.
	 * Capped at 500 so a pathological page count can't bloat the payload.
	 *
	 * @return array<int,array<string,mixed>>
	 */
	private function available_pages(): array {
		$posts = get_posts( [
			'post_type'   => 'page',
			'post_status' => 'publish',
			'numberposts' => 500,
			'orderby'     => 'title',
			'order'       => 'ASC',
		] );

		$out = [];
		foreach ( $posts as $p ) {
			$title = '' !== trim( (string) $p->post_title ) ? (string) $p->post_title : '(no title)';
			$out[] = [
				'id'    => (int) $p->ID,
				'title' => $title . ' (ID: ' . (int) $p->ID . ')',
			];
		}
		return $out;
	}

	private function error( string $code, string $message, int $status ): WP_REST_Response {
		return new WP_REST_Response( [ 'ok' => false, 'code' => $code, 'message' => $message ], $status );
	}
}
