<?php

declare(strict_types=1);

namespace Numinix\SeekmodoWordPress\Rest;

use Numinix\SeekmodoWordPress\Config\DomainLock;
use Numinix\SeekmodoWordPress\Config\Options;
use Numinix\SeekmodoWordPress\Gateway\Client;
use Numinix\SeekmodoWordPress\Gateway\RecommendationsCascade;
use Numinix\SeekmodoWordPress\Gateway\ShopperContext;
use WP_REST_Request;
use WP_REST_Response;

/**
 * Storefront recommendations proxy — HMAC-signed gateway calls.
 *
 *   GET|POST /wp-json/seekmodo/v1/recommendations
 *
 * Supports AKS-style cascades (`pdp-cascade`, `cart`) plus legacy
 * single-algorithm placements. Always HTTP 200 with a fail-soft
 * envelope (except wrong HTTP method).
 *
 * @see seekmodo.com/docs/connectors/recommendations-pdp-cart.md
 */
final class RecommendationsRoute
{
    /** @var array<string,array{algo:string,needs_doc:bool,cascade?:bool}> */
    private const PLACEMENT_MAP = [
        'pdp-related'        => ['algo' => 'related',        'needs_doc' => true],
        'pdp-also-bought'    => ['algo' => 'also_bought',    'needs_doc' => true],
        'pdp-also-viewed'    => ['algo' => 'also_viewed',    'needs_doc' => true],
        'pdp-bundle'         => ['algo' => 'bundle.suggest', 'needs_doc' => true],
        'pdp-cascade'        => ['algo' => 'pdp',            'needs_doc' => true, 'cascade' => true],
        'pdp'                => ['algo' => 'pdp',            'needs_doc' => true, 'cascade' => true],
        'cart'               => ['algo' => 'cart',           'needs_doc' => false, 'cascade' => true],
        'cart_below'         => ['algo' => 'cart',           'needs_doc' => false, 'cascade' => true],
        'cart-also-bought'   => ['algo' => 'also_bought',    'needs_doc' => true],
        'cart-bundle'        => ['algo' => 'bundle.suggest', 'needs_doc' => true],
        'home-trending'      => ['algo' => 'trending',       'needs_doc' => false],
        'category-trending'  => ['algo' => 'trending',       'needs_doc' => false],
    ];

    public static function register(): void
    {
        if (!function_exists('add_action')) {
            return;
        }
        add_action('rest_api_init', [self::class, 'registerRoutes']);
    }

    public static function registerRoutes(): void
    {
        if (!function_exists('register_rest_route')) {
            return;
        }
        register_rest_route(Routes::NAMESPACE_NAME, '/recommendations', [
            [
                'methods'             => 'GET',
                'callback'            => [self::class, 'handle'],
                'permission_callback' => '__return_true',
            ],
            [
                'methods'             => 'POST',
                'callback'            => [self::class, 'handle'],
                'permission_callback' => '__return_true',
            ],
        ]);
    }

    /**
     * @param WP_REST_Request $request
     * @return WP_REST_Response|array<string,mixed>
     */
    public static function handle($request)
    {
        $params = self::paramsFromRequest($request);

        if (!Options::recommendationsEnabled()) {
            return self::fail('disabled');
        }
        if (!Options::isPaired()) {
            return self::fail('locked_out');
        }
        if (!DomainLock::matches()) {
            return self::fail('locked_out');
        }

        $placement = strtolower(trim((string) ($params['placement'] ?? '')));
        if ($placement === '' && isset($params['algorithm'])) {
            $algoAlias = strtolower(trim((string) $params['algorithm']));
            if ($algoAlias === 'pdp') {
                $placement = 'pdp-cascade';
            } elseif ($algoAlias === 'cart') {
                $placement = 'cart';
            }
        }
        if ($placement === '' || !isset(self::PLACEMENT_MAP[$placement])) {
            return self::fail('bad_placement');
        }

        $cfg = self::PLACEMENT_MAP[$placement];
        $algorithm = $cfg['algo'];
        $isCascade = !empty($cfg['cascade']);

        $docId = self::firstDocId($params);
        if (!empty($cfg['needs_doc']) && ($docId === '' || !preg_match('/^[A-Za-z0-9_\-]{1,64}$/', $docId))) {
            return self::fail('missing_doc_id');
        }

        $limitDefault = ($isCascade && $algorithm === 'cart') ? 12 : 8;
        $limit = isset($params['limit']) ? (int) $params['limit'] : $limitDefault;
        $limit = max(1, min(24, $limit));

        $payload = array_merge(ShopperContext::forGateway(), ['limit' => $limit]);
        if (!empty($cfg['needs_doc'])) {
            $payload['anchor_doc_id'] = $docId;
        }
        if ($algorithm === 'bundle.suggest') {
            $size = isset($params['bundle_size']) ? (int) $params['bundle_size'] : 3;
            $payload['bundle_size'] = max(2, min(5, $size));
        }

        $recommendFn = static function (string $algo, array $body): ?array {
            return Client::recommend($algo, $body);
        };
        $hydrateFn = [self::class, 'hydrateRows'];

        if ($isCascade && $algorithm === 'pdp') {
            $peers = self::categoryPeers([(int) $docId], $limit);
            $placements = RecommendationsCascade::runPdp(
                $docId,
                $limit,
                $payload,
                $recommendFn,
                $hydrateFn,
                $peers
            );

            return self::ok([
                'placement'     => 'pdp-cascade',
                'anchor_doc_id' => $docId,
                'algorithm'     => 'pdp',
                'placements'    => [
                    'bought'  => $placements['bought'],
                    'related' => $placements['related'],
                    'popular' => $placements['popular'],
                ],
                'recommendations' => $placements['related'],
                'meta'            => $placements['meta'],
                'total'           => count($placements['bought']) + count($placements['related']) + count($placements['popular']),
            ]);
        }

        if ($isCascade && $algorithm === 'cart') {
            $anchors = self::parseIdList($params['doc_ids'] ?? $params['anchor_doc_ids'] ?? '');
            if ($anchors === [] && $docId !== '') {
                $anchors = [$docId];
            }
            $exclude = self::parseIdList($params['exclude_doc_ids'] ?? '');
            if ($exclude === []) {
                $exclude = $anchors;
            }
            if ($anchors === []) {
                return self::fail('missing_doc_id');
            }
            $peerSeed = array_map('intval', array_merge($anchors, $exclude));
            $peers = self::categoryPeers($peerSeed, $limit);
            $result = RecommendationsCascade::runCart(
                $anchors,
                $exclude,
                $limit,
                $payload,
                $recommendFn,
                $hydrateFn,
                $peers
            );

            return self::ok([
                'placement'       => 'cart',
                'algorithm'       => 'cart',
                'recommendations' => $result['recommendations'],
                'meta'            => $result['meta'],
                'total'           => count($result['recommendations']),
            ]);
        }

        $resp = Client::recommend($algorithm, $payload);
        if (!is_array($resp)) {
            // 402 / null → fail-soft empty (contract allows ok:false or empty lists).
            return self::fail('gateway_failure');
        }

        $recs = self::hydrateRows(RecommendationsCascade::extractRows($resp));

        return self::ok([
            'placement'       => $placement,
            'anchor_doc_id'   => $docId,
            'algorithm'       => $algorithm,
            'recommendations' => $recs,
            'total'           => count($recs),
        ]);
    }

    /**
     * @param list<array<string,mixed>> $rows
     * @return list<array<string,mixed>>
     */
    public static function hydrateRows(array $rows): array
    {
        $out = [];
        foreach ($rows as $row) {
            if (!is_array($row)) {
                continue;
            }
            $docIdR = (string) ($row['doc_id'] ?? '');
            if ($docIdR === '') {
                continue;
            }
            $pid = (int) $docIdR;
            if ($pid > 0 && function_exists('wc_get_product')) {
                $product = wc_get_product($pid);
                if ($product && is_object($product)) {
                    $row['products_id'] = $pid;
                    if (empty($row['name']) && method_exists($product, 'get_name')) {
                        $row['name'] = (string) $product->get_name();
                    }
                    if (method_exists($product, 'get_permalink')) {
                        $row['url'] = (string) $product->get_permalink();
                    }
                    if (empty($row['image']) && function_exists('get_the_post_thumbnail_url')) {
                        $img = get_the_post_thumbnail_url($pid, 'woocommerce_thumbnail');
                        if (is_string($img) && $img !== '') {
                            $row['image'] = $img;
                        }
                    }
                    if (!isset($row['price']) && method_exists($product, 'get_price')) {
                        $price = $product->get_price();
                        if ($price !== '' && $price !== null) {
                            $row['price'] = (float) $price;
                        }
                    }
                    if (function_exists('wc_price') && isset($row['price'])) {
                        $row['price_formatted'] = wp_strip_all_tags((string) wc_price((float) $row['price']));
                    }
                }
            }
            $out[] = $row;
        }

        return $out;
    }

    /**
     * @param list<int> $productIds
     * @return list<array<string,mixed>>
     */
    public static function categoryPeers(array $productIds, int $limit): array
    {
        $productIds = array_values(array_unique(array_filter(array_map('intval', $productIds), static fn ($id) => $id > 0)));
        if ($productIds === [] || $limit <= 0 || !function_exists('wp_get_post_terms')) {
            return [];
        }

        $catIds = [];
        foreach ($productIds as $pid) {
            $terms = wp_get_post_terms($pid, 'product_cat', ['fields' => 'ids']);
            if (is_wp_error($terms) || !is_array($terms)) {
                continue;
            }
            foreach ($terms as $tid) {
                $tid = (int) $tid;
                if ($tid > 0) {
                    $catIds[$tid] = true;
                }
            }
        }
        $catIds = array_keys($catIds);
        if ($catIds === []) {
            return [];
        }

        $query = new \WP_Query([
            'post_type'              => 'product',
            'post_status'            => 'publish',
            'posts_per_page'         => max(1, min(50, $limit * 3)),
            'post__not_in'           => $productIds,
            'fields'                 => 'ids',
            'no_found_rows'          => true,
            'update_post_meta_cache' => false,
            'update_post_term_cache' => false,
            'tax_query'              => [
                [
                    'taxonomy' => 'product_cat',
                    'field'    => 'term_id',
                    'terms'    => $catIds,
                ],
            ],
            'orderby'                => 'date',
            'order'                  => 'DESC',
        ]);

        $rows = [];
        foreach ((array) $query->posts as $pid) {
            $pid = (int) $pid;
            if ($pid > 0) {
                $rows[] = [
                    'doc_id' => (string) $pid,
                    'score'  => 0.0,
                    'source' => 'category_peer',
                ];
            }
        }

        return self::hydrateRows($rows);
    }

    /**
     * @param mixed $raw
     * @return list<string>
     */
    private static function parseIdList($raw): array
    {
        if (is_array($raw)) {
            $parts = $raw;
        } else {
            $parts = preg_split('/[,\s]+/', (string) $raw) ?: [];
        }
        $out = [];
        foreach ($parts as $part) {
            $part = trim((string) $part);
            if ($part !== '' && ctype_digit($part) && (int) $part > 0) {
                $out[] = $part;
            }
        }

        return array_values(array_unique($out));
    }

    /**
     * @param array<string,mixed> $params
     */
    private static function firstDocId(array $params): string
    {
        foreach (['doc_id', 'anchor_doc_id', 'source_doc_id'] as $key) {
            if (!empty($params[$key])) {
                return (string) $params[$key];
            }
        }
        $ids = self::parseIdList($params['doc_ids'] ?? $params['anchor_doc_ids'] ?? '');

        return $ids[0] ?? '';
    }

    /**
     * @param WP_REST_Request|mixed $request
     * @return array<string,mixed>
     */
    private static function paramsFromRequest($request): array
    {
        $params = [];
        if (is_object($request) && method_exists($request, 'get_params')) {
            $params = (array) $request->get_params();
        }
        if (is_object($request) && method_exists($request, 'get_json_params')) {
            $json = $request->get_json_params();
            if (is_array($json)) {
                $params = array_merge($params, $json);
            }
        }

        return $params;
    }

    /**
     * @param array<string,mixed> $body
     * @return WP_REST_Response|array<string,mixed>
     */
    private static function ok(array $body)
    {
        $body['ok'] = true;
        if (class_exists(WP_REST_Response::class)) {
            return new WP_REST_Response($body, 200);
        }

        return $body;
    }

    /**
     * @return WP_REST_Response|array<string,mixed>
     */
    private static function fail(string $reason)
    {
        $body = ['ok' => false, 'reason' => $reason];
        if (class_exists(WP_REST_Response::class)) {
            return new WP_REST_Response($body, 200);
        }

        return $body;
    }
}
