<?php
/**
 * BookiFlex Core
 *
 * @package    BookiFlex
 * @subpackage Core
 * @author     BookiFlex
 * @copyright  Copyright (c) 2025 BookiFlex
 * @license    GPL-2.0-or-later
 *
 * @link       https://bookiflex.com
 */

declare(strict_types=1);

namespace BookiFlex\Core\ORM\DataProvider;

use BookiFlex\Core\Admin\CPT\AccommodationType;
use BookiFlex\Core\Admin\CPT\CancellationPolicy;
use BookiFlex\Core\Admin\CPT\PaymentType;
use BookiFlex\Core\Admin\CPT\RatePlan;

defined( 'ABSPATH' ) || exit;

/**
 * Provides Custom post type's data (from post and postmeta tables) in a normalized by schema format.
 * Loading is optimized for bulk loading of nested CPTs.
 *
 * SECURITY: All database queries use prepared statements.
 * Post statuses from whitelist constants, IDs are integers with %d placeholders.
 * Dynamic IN clause placeholders match array lengths via array_fill().
 * phpcs:ignore suppressions are safe - queries cannot be exploited.
 */
class CustomPostTypeDataProvider
{
	protected static array $allowedPostTypes = [
		AccommodationType::POST_TYPE,
		RatePlan::POST_TYPE,
		PaymentType::POST_TYPE,
		CancellationPolicy::POST_TYPE,
		'attachment'
	];
    public const STATUSES        = ['publish', 'pending', 'future', 'private', 'draft'];
    public const ACTIVE_STATUSES = ['publish'];
    protected static array $schema = [
        AccommodationType::POST_TYPE => [
            "id"                    => "ID",
            "description"           => "post_excerpt",
            "name"                  => "post_title",
            "content"               => "post_content",
            "active"                => "post_status",
            "shortCode"             => AccommodationType::FIELD_SHORT_CODE,
            "accommodationCategory"                  => AccommodationType::FIELD_ACCOMMODATION_CATEGORY,
            "livingSpace"           => AccommodationType::FIELD_LIVING_SPACE,
	        "roomsCount" => AccommodationType::FIELD_ROOMS_COUNT,
            "mainSleepingCapacity"        => AccommodationType::FIELD_MAIN_SLEEPING_CAPACITY,
            "additionalSleepingPlaces"       => AccommodationType::FIELD_ADDITIONAL_SLEEPING_PLACES,
            "gallery"               => ["meta_field" => AccommodationType::FIELD_ACCOMMODATION_GALLERY, "cpt" => "attachment"],
            "thumbnail"             => ["meta_field" => AccommodationType::FIELD_THUMBNAIL, "cpt" => "attachment"],
            "amenities"             => AccommodationType::FIELD_KEY_AMENITIES,
            "mainSleepingAllowedOccupancy"  => AccommodationType::FIELD_MAIN_SLEEPING_ALLOWED_OCCUPANCY,
            "enableExtraOccupancy" => AccommodationType::FIELD_ENABLE_EXTRA_OCCUPANCY,
            "childAllowedOnMainPlace"  => AccommodationType::FIELD_CHILD_ALLOWED_ON_MAIN_PLACE,
            "childAllowedOnExtraPlace" => AccommodationType::FIELD_CHILD_ALLOWED_ON_EXTRA_PLACE,
            "childAllowedWithoutPlace" => AccommodationType::FIELD_CHILD_ALLOWED_WITHOUT_PLACE,
            'accommodationAvailableUnits'      => AccommodationType::FIELD_ACCOMMODATION_AVAILABLE_UNITS,
            "address" => AccommodationType::FIELD_ADDRESS,
            "phone" => AccommodationType::FIELD_PHONE,
            "email" => AccommodationType::FIELD_EMAIL,
            "latitude" => AccommodationType::FIELD_LATITUDE,
            "longitude" => AccommodationType::FIELD_LONGITUDE,
	        "checkinInstructions" => AccommodationType::FIELD_CHECKIN_INSTRUCTIONS,
        ],
        RatePlan::POST_TYPE => [
            "id"                          => "ID",
            "name"                        => "post_title",
            "description"                 => "post_excerpt",
            "content"                     => "post_content",
            "active"                      => "post_status",
            "parent"                      => "post_parent",
            "accommodationTypes"          => ["meta_field" => RatePlan::FIELD_ACCOMMODATION_TYPES, "cpt" => AccommodationType::POST_TYPE],
            "cancellationPolicy"          => ["meta_field" => RatePlan::FIELD_CANCELLATION_POLICY, "cpt" => CancellationPolicy::POST_TYPE],
            "paymentTypes"                => ["meta_field" => RatePlan::FIELD_PAYMENT_TYPES, "cpt" => PaymentType::POST_TYPE],
            "boardType"                   => RatePlan::FIELD_BOARD_TYPE,
            "code"                        => RatePlan::FIELD_SHORT_CODE,
            "inheritancePriceRules"       => RatePlan::FIELD_INHERITANCE_PRICE_RULES,
            "inheritanceRestrictionRules" => RatePlan::FIELD_INHERITANCE_RESTRICTIONS,
            "hiddenFromFrontend"          => RatePlan::FIELD_HIDDEN_FROM_FRONTEND,
        ],
        PaymentType::POST_TYPE => [
            "id"                   => "ID",
            "name"                 => "post_title",
            "description"          => "post_excerpt",
            "active"               => "post_status",
            "type"                 => PaymentType::FIELD_CALCULATOR_TYPE,
            "prepaymentType"       => PaymentType::FIELD_PREPAYMENT_TYPE,
            "prepaymentValue"      => PaymentType::FIELD_PREPAYMENT_VALUE,
            "ABAPeriod"            => PaymentType::FIELD_PAYMENT_AVAILABILITY_PERIOD,
            "ABACondition"         => PaymentType::FIELD_PAYMENT_AVAILABILITY_CONDITION,
            "ABAValue"             => PaymentType::FIELD_PAYMENT_AVAILABILITY_VALUE,
            "payable"              => PaymentType::FIELD_PAYMENT_PAYABLE,
            "paymentAuthorization" => PaymentType::FIELD_PAYMENT_AUTHORIZATION,
            "gatewayName"          => PaymentType::FIELD_PAYMENT_SYSTEM,
            "timeForConfirmation"  => PaymentType::FIELD_TIME_FOR_CONFIRMATION,
        ],
        CancellationPolicy::POST_TYPE => [
            "id"     => "ID",
            "name"   => "post_title",
            "description" => "post_excerpt",
            "active" => "post_status",
            "rules"  => CancellationPolicy::FIELD_RULES,
            "predefinedRule" => CancellationPolicy::FIELD_PREDEFINED_RULE,
	        "strictTimeWindow" => CancellationPolicy::FIELD_STRICT_TIME_WINDOW,
	        "strictPenaltyPercent" => CancellationPolicy::FIELD_STRICT_PENALTY_PERCENT,
        ],
        'attachment' => [
            "id"      => "ID",
            "name"    => "post_title",
            "caption" => "post_excerpt",
            "url"     => "guid",
            "mime"    => "post_mime_type",
        ],
    ];

    public function __construct(
        private readonly CarbonFieldsParser $fieldsParser
    ) {
    }

    /**
     * Register an additional post type with its schema mapping.
     */
    public static function registerPostType(string $postType, array $schema): void
    {
        if (!in_array($postType, static::$allowedPostTypes, true)) {
            static::$allowedPostTypes[] = $postType;
        }
        static::$schema[$postType] = $schema;
    }

    /**
     * Add a single field mapping to an existing post-type schema without replacing it.
     */
    public static function registerSchemaField(string $postType, string $propertyName, string|array $fieldMapping): void
    {
        static::$schema[$postType][$propertyName] = $fieldMapping;
    }

    /**
     * Get data for specified custom post type and IDs
     *
     * @param string $customPostType
     * @param array $ids
     * @param bool $onlyActive
     * @return array|null
     */
    public function getData(string $customPostType, array $ids = [], bool $onlyActive = true): ?array
    {
        // Base query to get posts
        if (!$posts = $this->loadPosts($customPostType, $ids, $onlyActive)) {
            return null;
        }

        if ($customPostType !== 'attachment') {
            // Check for and load posts with post_parent not yet included
            $posts = $this->loadParentPosts($customPostType, $posts, $onlyActive);

            // Fetch meta fields
            $metaData = $this->loadMetaData($customPostType, $posts);
        }

        // Map post and meta data to a specific template
        $result = [];
        foreach ($posts as $post) {
            $result[] = $this->mapData($customPostType, $post, $metaData[$post['ID']] ?? [], $posts);
        }

        // Aggregate nested CPT IDs for bulk loading
        $this->loadNestedPosts($customPostType, $result, $onlyActive);

        // Enrich attachment data with optimized image sizes
        $this->enrichAttachmentSizes($customPostType, $result);

        // Map parent post to the result
        foreach ($result as &$item) {
            if (!isset($item['parent'])) {
                continue;
            }

            foreach ($result as $parent) {
                if ($parent['id'] === $item['parent']) {
                    $item['parent'] = $parent;
                    break;
                }
            }
        }

        // Filter by passed IDs
        if (!empty($ids)) {
            $result = array_values(array_filter($result, fn ($item) => in_array($item['id'], $ids)));
        }

        return $result;
    }

    /**
     * Map post and meta data to a specific template
     *
     * @param string $customPostType
     * @param array $post
     * @param array $metaData
     * @param array $posts
     * @return array
     */
    private function mapData(string $customPostType, array $post, array $metaData, array &$posts): array
    {
        $template = static::$schema[$customPostType] ?? [];
        $mapped   = [];

        foreach ($template as $key => $value) {
            if (is_array($value)) {
                $mapped[$key] = $metaData[$value['meta_field']] ?? [];
            } elseif (isset($post[$value])) {
                $mapped[$key] = $this->normalize('post', $key, $post[$value]);
            } else {
                $mapped[$key] = $this->normalize('meta', $key, $metaData[$value] ?? null);
            }
        }

        return $mapped;
    }

    /**
     * Normalize the value based on the key and type
     *
     * @param string $type
     * @param string $key
     * @param mixed $value
     * @return mixed
     */
    private function normalize(string $type, string $key, mixed $value): mixed
    {
        if ($type === 'post') {
            return match ($key) {
                'id'     => (int) $value,
                'active' => in_array($value, self::ACTIVE_STATUSES),
                'parent' => empty($value) ? null : (int) $value,
                default  => $value,
            };
        }

        return $value ?? null;
    }

    /**
     * Load posts by custom post type and IDs
     *
     * @param string $customPostType
     * @param array $ids
     * @param bool $onlyActive
     * @return array|null
     */
    private function loadPosts(string $customPostType, array $ids, bool $onlyActive): ?array
    {
        global $wpdb;

	    if (!in_array($customPostType, static::$allowedPostTypes, true)) {
		    return null;
	    }

	    $statuses = $onlyActive ? self::ACTIVE_STATUSES : self::STATUSES;
	    if ($customPostType === 'attachment') {
		    $statuses = ['inherit'];
	    }

	    $ids = array_map('intval', $ids);

	    $sql  = "SELECT * FROM {$wpdb->posts} WHERE post_type = %s";
	    $args = [$customPostType];

	    // post_status IN (...)
	    $statusPlaceholders = implode(', ', array_fill(0, count($statuses), '%s'));
	    $sql                .= " AND post_status IN ($statusPlaceholders)";
	    $args                = array_merge($args, $statuses);

	    // ID IN (...)
	    if (!empty($ids)) {
		    $id_placeholders = implode(', ', array_fill(0, count($ids), '%d'));
		    $sql            .= " AND ID IN ($id_placeholders)";
		    $args            = array_merge($args, $ids);
	    }

	    $sql .= ' ORDER BY menu_order ASC';

        return $wpdb->get_results(
	        $wpdb->prepare($sql, $args),
	        ARRAY_A
        );
    }

    /**
     * Load parent posts for the given posts
     *
     * @param string $customPostType
     * @param array $posts
     * @param bool $onlyActive
     * @return array
     */
    private function loadParentPosts(string $customPostType, array $posts, bool $onlyActive): array
    {
        $postIds          = array_column($posts, 'ID');
        $parentIds        = array_filter(array_column($posts, 'post_parent'));
        $missingParentIds = array_diff($parentIds, $postIds);
        if (!empty($missingParentIds)) {
            $parentPosts = $this->loadPosts($customPostType, $missingParentIds, $onlyActive);
            $posts       = array_merge($posts, $parentPosts);
        }

        return $posts;
    }

    /**
     * Load meta data for the given posts
     *
     * @param string $customPostType
     * @param array $posts
     * @return array|null
     */
    private function loadMetaData(string $customPostType, array $posts): ?array
    {
        global $wpdb;

        $postIds      = array_column($posts, 'ID');
        $placeholders = implode(', ', array_fill(0, count($postIds), '%d'));
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
        $metaQuery    = $wpdb->prepare(
            "SELECT post_id, meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id IN ($placeholders)",
            ...$postIds
        );

        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter
        $metaResults = $wpdb->get_results($metaQuery, ARRAY_A);
        return $this->fieldsParser->parse($customPostType, $metaResults);
    }

    /**
     * Extract nested CPTs and their IDs from the mapped data
     *
     * @param string $customPostType
     * @param array $mappedData
     * @return array
     */
    private function getNestedCPTsFromMapping(string $customPostType, array $mappedData): array
    {
        $template   = static::$schema[$customPostType] ?? [];
        $nestedCPTs = [];

        foreach ($template as $key => $value) {
            if (is_array($value) && isset($mappedData[$key])) {
                $data = !is_array($mappedData[$key])
                    ? [$mappedData[$key]]
                    : $mappedData[$key];
                $nestedCPTs[$value['cpt']] = array_merge($nestedCPTs[$value['cpt']] ?? [], $data);
            }
        }

        return $nestedCPTs;
    }

    private function getPropertyByNestedKey(string $customPostType, string $nestedKey): array
    {
        $template = static::$schema[$customPostType] ?? [];

        $result = [];
        foreach ($template as $key => $value) {
            if (is_array($value) && $value['cpt'] === $nestedKey) {
                $result[] = $key;
            }
        }

        return $result;
    }

    /**
     * If there are nested CPTs, load their data in bulk
     *
     * @param string $customPostType
     * @param array $result
     * @param bool $onlyActive
     * @return void
     */
    private function loadNestedPosts(string $customPostType, array &$result, bool $onlyActive): void
    {
        // Identify nested CPTs from mapping
        $nestedCPTs = [];
        foreach ($result as $item) {
            foreach ($this->getNestedCPTsFromMapping($customPostType, $item) as $nestedCPT => $ids) {
                $nestedCPTs[$nestedCPT] = array_merge($nestedCPTs[$nestedCPT] ?? [], $ids);
            }
        }

        // Load nested CPT data in bulk
        foreach ($nestedCPTs as $nestedCPT => $ids) {
            $nestedData = $this->getData($nestedCPT, array_unique($ids), $onlyActive);
            $this->sortArrayByIdOrder($ids, $nestedData);

            if ($nestedData === null) {
                // No active nested posts found — clear raw IDs to prevent ORM cast errors
                $propertyNames = $this->getPropertyByNestedKey($customPostType, $nestedCPT);
                foreach ($result as &$item) {
                    foreach ($propertyNames as $propertyName) {
                        $item[$propertyName] = [];
                    }
                }
                unset($item);
                continue;
            }
            // Assign nested data back to the result
            foreach ($result as &$item) {
                foreach ($this->getNestedCPTsFromMapping($customPostType, $item) as $nestedKey => $cptIds) {
                    if ($nestedKey === $nestedCPT) {
                        $propertyNames = $this->getPropertyByNestedKey($customPostType, $nestedKey);
                        foreach ($propertyNames as $propertyName) {
                            $propertyIds         = is_array($item[$propertyName]) ? $item[$propertyName] : [$item[$propertyName]];
                            $item[$propertyName] = array_values(
                                array_filter($nestedData, fn ($nestedItem) => in_array($nestedItem['id'], $propertyIds))
                            );
                        }
                    }
                }
            }
        }
    }

    private function sortArrayByIdOrder(array $firstArray, ?array &$secondArray): void
    {
        if (empty($firstArray) || empty($secondArray)) {
            return;
        }

        $idOrder = array_flip($firstArray);
        usort($secondArray, function ($a, $b) use ($idOrder) {
            return $idOrder[$a['id']] <=> $idOrder[$b['id']];
        });
    }

    /**
     * Enrich attachment fields with WordPress image sizes
     */
    private function enrichAttachmentSizes(string $customPostType, array &$result): void
    {
        $template = static::$schema[$customPostType] ?? [];
        $attachmentFields = [];

        foreach ($template as $key => $value) {
            if (is_array($value) && $value['cpt'] === 'attachment') {
                $attachmentFields[] = $key;
            }
        }

        if (empty($attachmentFields)) {
            return;
        }

        foreach ($result as &$item) {
            foreach ($attachmentFields as $field) {
                if (empty($item[$field]) || !is_array($item[$field])) {
                    continue;
                }

                foreach ($item[$field] as &$attachment) {
                    if (empty($attachment['id'])) {
                        continue;
                    }

                    $id = (int) $attachment['id'];
                    $fallback = $attachment['url'] ?? '';

                    $attachment['sizes'] = [];
                    foreach (['card' => 'bflex-card', 'card_lg' => 'bflex-card-lg', 'gallery' => 'bflex-gallery', 'full' => 'full'] as $key => $sizeName) {
                        $src = wp_get_attachment_image_src($id, $sizeName);
                        $attachment['sizes'][$key] = $src
                            ? ['url' => $src[0], 'width' => (int) $src[1]]
                            : ['url' => $fallback, 'width' => 0];
                    }
                }
            }
        }
    }
}
