<?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\Service\CptCreator;

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

defined( 'ABSPATH' ) || exit;

/**
 * Service for programmatic creation of Custom Post Types.
 *
 * This service is designed for special cases like:
 * - Seeding demo/sample data
 * - Automated tests with fixtures
 * - Onboarding flows
 * - Import from external systems
 *
 * NOT intended for regular CRUD operations - use WordPress Admin UI for that.
 */
final class CptCreator implements CptCreatorInterface
{
    /**
     * {@inheritDoc}
     */
    public function createAccommodationType(array $data): int
    {
        return $this->executeInTransaction(fn() => $this->doCreateAccommodationType($data));
    }

    /**
     * {@inheritDoc}
     */
    public function createRatePlan(array $data): int
    {
        return $this->executeInTransaction(fn() => $this->doCreateRatePlan($data));
    }

    /**
     * {@inheritDoc}
     */
    public function createCancellationPolicy(array $data): int
    {
        return $this->executeInTransaction(fn() => $this->doCreateCancellationPolicy($data));
    }

    /**
     * {@inheritDoc}
     */
    public function createPaymentType(array $data): int
    {
        return $this->executeInTransaction(fn() => $this->doCreatePaymentType($data));
    }

    /**
     * Create multiple CPT entities in a single transaction.
     *
     * Creates entities in the correct order: AccommodationType → CancellationPolicy → PaymentType → RatePlan
     *
     * Supports two types of references for linking entities in rate plans:
     * 1. Index references (0, 1, 2, ...): For entities created in the same batch
     * 2. Existing IDs (['existing' => ID]): For predefined entities already in database
     *
     * Example usage:
     * ```php
     * $cptCreator->createBatch([
     *     'accommodation_types' => [
     *         ['title' => 'Room A', ...],
     *         ['title' => 'Room B', ...],
     *     ],
     *     'rate_plans' => [
     *         [
     *             'title' => 'Base Rate',
     *             'accommodation_types' => [0, 1], // References batch indices
     *             'payment_types' => ['existing' => [123, 456]], // Existing IDs from DB
     *             'cancellation_policy' => ['existing' => 789], // Existing ID from DB
     *         ]
     *     ]
     * ]);
     * ```
     *
     * @param  array{
     *     accommodation_types?: array<array>,
     *     cancellation_policies?: array<array>,
     *     payment_types?: array<array>,
     *     rate_plans?: array<array>
     * } $data Batch data for all CPT types
     * @return array{
     *     accommodation_type_ids: array<int>,
     *     cancellation_policy_ids: array<int>,
     *     payment_type_ids: array<int>,
     *     rate_plan_ids: array<int>
     * } Created post IDs
     * @throws RuntimeException If batch creation fails
     */
    public function createBatch(array $data): array
    {
        global $wpdb;

        // Start transaction
        $wpdb->query('START TRANSACTION');

        try {
            $createdIds = [
                'accommodation_type_ids'    => [],
                'cancellation_policy_ids'   => [],
                'payment_type_ids'          => [],
                'rate_plan_ids'             => [],
            ];

            // 1. Create AccommodationTypes
            if (!empty($data['accommodation_types'])) {
                foreach ($data['accommodation_types'] as $accommodationTypeData) {
                    $postId = $this->doCreateAccommodationType($accommodationTypeData);
                    $createdIds['accommodation_type_ids'][] = $postId;
                }
            }

            // 2. Create CancellationPolicies
            if (!empty($data['cancellation_policies'])) {
                foreach ($data['cancellation_policies'] as $policyData) {
                    $postId = $this->doCreateCancellationPolicy($policyData);
                    $createdIds['cancellation_policy_ids'][] = $postId;
                }
            }

            // 3. Create PaymentTypes
            if (!empty($data['payment_types'])) {
                foreach ($data['payment_types'] as $paymentTypeData) {
                    $postId = $this->doCreatePaymentType($paymentTypeData);
                    $createdIds['payment_type_ids'][] = $postId;
                }
            }

            // 4. Create RatePlans with resolved references
            if (!empty($data['rate_plans'])) {
                foreach ($data['rate_plans'] as $ratePlanData) {
                    // Resolve index-based references to actual IDs
                    $resolvedData = $this->resolveRatePlanReferences($ratePlanData, $createdIds);
                    $postId = $this->doCreateRatePlan($resolvedData);
                    $createdIds['rate_plan_ids'][] = $postId;
                }
            }

            // Commit transaction
            $wpdb->query('COMMIT');

            return $createdIds;
        } catch (\Exception $e) {
            // Rollback transaction
            $wpdb->query('ROLLBACK');

            throw new RuntimeException(
                sprintf('Batch creation failed: %s', $e->getMessage()),
                0,
                $e
            );
        }
    }

    /**
     * Resolve references in rate plan data to actual post IDs.
     *
     * Supports two types of references:
     * 1. Index references (integer): For entities created in the same batch (0, 1, 2, ...)
     * 2. Existing IDs (['existing' => ID|ID[]]): For predefined entities already in database
     *
     * Examples:
     * - accommodation_types: [0, 1, 2] → resolves to IDs from current batch
     * - payment_types: ['existing' => [123, 456]] → uses IDs 123, 456 directly from DB
     * - cancellation_policy: ['existing' => 789] → uses ID 789 directly from DB
     *
     * @param  array<string, mixed> $ratePlanData Rate plan data with references
     * @param  array{
     *     accommodation_type_ids: array<int>,
     *     cancellation_policy_ids: array<int>,
     *     payment_type_ids: array<int>,
     *     rate_plan_ids: array<int>
     * } $createdIds IDs of entities created in current batch
     * @return array<string, mixed> Rate plan data with resolved IDs
     */
    private function resolveRatePlanReferences(array $ratePlanData, array $createdIds): array
    {
        // Resolve accommodation_types references to IDs
        if (isset($ratePlanData['accommodation_types'])) {
            $ratePlanData['accommodation_types'] = $this->resolveReferences(
                $ratePlanData['accommodation_types'],
                $createdIds['accommodation_type_ids']
            );
        }

        // Resolve payment_types references to IDs
        if (isset($ratePlanData['payment_types'])) {
            $ratePlanData['payment_types'] = $this->resolveReferences(
                $ratePlanData['payment_types'],
                $createdIds['payment_type_ids']
            );
        }

        // Resolve cancellation_policy reference to ID
        if (isset($ratePlanData['cancellation_policy'])) {
            $resolved = $this->resolveReferences(
                $ratePlanData['cancellation_policy'],
                $createdIds['cancellation_policy_ids']
            );
            // Cancellation policy is a single value, not array
            $ratePlanData['cancellation_policy'] = is_array($resolved) ? $resolved[0] : $resolved;
        }

        return $ratePlanData;
    }

    /**
     * Resolve references (indices or existing IDs) to actual post IDs.
     *
     * Handles two formats:
     * 1. Array of indices: [0, 1, 2] → look up in $batchIds
     * 2. Existing IDs wrapper: ['existing' => [123, 456]] or ['existing' => 123] → use directly
     *
     * @param  mixed      $reference Reference value (can be int, array of ints, or ['existing' => ...])
     * @param  array<int> $batchIds  IDs created in current batch (indexed array)
     * @return int|array<int> Resolved ID(s)
     */
    private function resolveReferences(mixed $reference, array $batchIds): int|array
    {
        // Case 1: Existing IDs wrapper ['existing' => ID or [IDs]]
        if (is_array($reference) && isset($reference['existing'])) {
            return $reference['existing'];
        }

        // Case 2: Single index reference (integer)
        if (is_int($reference)) {
            if (!isset($batchIds[$reference])) {
                throw new RuntimeException(
                    sprintf('Invalid batch index reference: %d. Only %d entities created.', $reference, count($batchIds))
                );
            }
            return $batchIds[$reference];
        }

        // Case 3: Array of index references [0, 1, 2]
        if (is_array($reference)) {
            $resolvedIds = [];
            foreach ($reference as $index) {
                if (!is_int($index)) {
                    throw new RuntimeException(
                        sprintf('Invalid reference type: expected int or ["existing" => ...], got %s', gettype($index))
                    );
                }
                if (!isset($batchIds[$index])) {
                    throw new RuntimeException(
                        sprintf('Invalid batch index reference: %d. Only %d entities created.', $index, count($batchIds))
                    );
                }
                $resolvedIds[] = $batchIds[$index];
            }
            return $resolvedIds;
        }

        throw new RuntimeException(
            sprintf('Invalid reference format: expected int, array, or ["existing" => ...], got %s', gettype($reference))
        );
    }

    /**
     * Create AccommodationType without transaction wrapper (for use in batch).
     *
     * @param  array<string, mixed> $data
     * @return int
     * @throws RuntimeException
     */
    private function doCreateAccommodationType(array $data): int
    {
        // Validate required fields
        $this->validateRequired($data, ['title', 'short_code', 'accommodation_category', 'accommodation_available_units', 'main_sleeping_capacity', 'main_sleeping_allowed_occupancy']);

        // Create post
        $postId = wp_insert_post(
            [
            'post_type'    => AccommodationType::POST_TYPE,
            'post_title'   => $data['title'],
            'post_excerpt' => $data['excerpt'] ?? '',
            'post_status'  => $data['status'] ?? 'publish',
            'menu_order'   => $data['menu_order'] ?? 0,
            ], true
        );

        if (is_wp_error($postId)) {
            throw new RuntimeException(
                sprintf('Failed to create AccommodationType: %s', $postId->get_error_message())
            );
        }

        // Set thumbnail if provided
        if (isset($data['thumbnail_id'])) {
            set_post_thumbnail($postId, $data['thumbnail_id']);
        }

        // Set Carbon Fields metadata
        carbon_set_post_meta($postId, AccommodationType::FIELD_SHORT_CODE, $data['short_code']);
        carbon_set_post_meta($postId, AccommodationType::FIELD_ACCOMMODATION_CATEGORY, $data['accommodation_category']);
        carbon_set_post_meta($postId, AccommodationType::FIELD_ACCOMMODATION_AVAILABLE_UNITS, (string) $data['accommodation_available_units']);
        carbon_set_post_meta($postId, AccommodationType::FIELD_MAIN_SLEEPING_CAPACITY, (string) $data['main_sleeping_capacity']);
        carbon_set_post_meta($postId, AccommodationType::FIELD_MAIN_SLEEPING_ALLOWED_OCCUPANCY, $data['main_sleeping_allowed_occupancy']);

        // Optional fields
        if (isset($data['living_space'])) {
            carbon_set_post_meta($postId, AccommodationType::FIELD_LIVING_SPACE, $data['living_space']);
        }

        if (isset($data['key_amenities'])) {
            carbon_set_post_meta($postId, AccommodationType::FIELD_KEY_AMENITIES, $data['key_amenities']);
        }

        if (isset($data['additional_sleeping_places'])) {
            carbon_set_post_meta($postId, AccommodationType::FIELD_ADDITIONAL_SLEEPING_PLACES, (string) $data['additional_sleeping_places']);
        }

        if (isset($data['enable_extra_occupancy']) && $data['enable_extra_occupancy']) {
            carbon_set_post_meta($postId, AccommodationType::FIELD_ENABLE_EXTRA_OCCUPANCY, ['1']);
        }

        if (!empty($data['child_allowed_on_main_place'])) {
            carbon_set_post_meta($postId, AccommodationType::FIELD_CHILD_ALLOWED_ON_MAIN_PLACE, $data['child_allowed_on_main_place']);
        }

        if (!empty($data['child_allowed_on_extra_place'])) {
            carbon_set_post_meta($postId, AccommodationType::FIELD_CHILD_ALLOWED_ON_EXTRA_PLACE, $data['child_allowed_on_extra_place']);
        }

        if (!empty($data['child_allowed_without_place'])) {
            carbon_set_post_meta($postId, AccommodationType::FIELD_CHILD_ALLOWED_WITHOUT_PLACE, $data['child_allowed_without_place']);
        }

        if (isset($data['gallery'])) {
            carbon_set_post_meta($postId, AccommodationType::FIELD_ACCOMMODATION_GALLERY, $data['gallery']);
        }

        // Contact information (for rental unit)
        if (isset($data['address'])) {
            carbon_set_post_meta($postId, AccommodationType::FIELD_ADDRESS, $data['address']);
        }

        if (isset($data['phone'])) {
            carbon_set_post_meta($postId, AccommodationType::FIELD_PHONE, $data['phone']);
        }

        if (isset($data['email'])) {
            carbon_set_post_meta($postId, AccommodationType::FIELD_EMAIL, $data['email']);
        }

        if (isset($data['longitude'])) {
            carbon_set_post_meta($postId, AccommodationType::FIELD_LONGITUDE, $data['longitude']);
        }

        if (isset($data['latitude'])) {
            carbon_set_post_meta($postId, AccommodationType::FIELD_LATITUDE, $data['latitude']);
        }

        return $postId;
    }

    /**
     * Create RatePlan without transaction wrapper (for use in batch).
     *
     * @param  array<string, mixed> $data
     * @return int
     * @throws RuntimeException
     */
    private function doCreateRatePlan(array $data): int
    {
        // Validate required fields
        $this->validateRequired($data, ['title', 'board_type', 'short_code', 'accommodation_types', 'payment_types', 'cancellation_policy']);

        // Create post
        $postId = wp_insert_post(
            [
            'post_type'    => RatePlan::POST_TYPE,
            'post_title'   => $data['title'],
            'post_excerpt' => $data['excerpt'] ?? '',
            'post_status'  => $data['status'] ?? 'publish',
            'post_parent'  => $data['parent_id'] ?? 0,
            'menu_order'   => $data['menu_order'] ?? 0,
            ], true
        );

        if (is_wp_error($postId)) {
            throw new RuntimeException(
                sprintf('Failed to create RatePlan: %s', $postId->get_error_message())
            );
        }

        // Set Carbon Fields metadata
        carbon_set_post_meta($postId, RatePlan::FIELD_BOARD_TYPE, $data['board_type']);
        carbon_set_post_meta($postId, RatePlan::FIELD_SHORT_CODE, $data['short_code']);

        // Set associations (Carbon Fields handles arrays of IDs automatically)
        carbon_set_post_meta($postId, RatePlan::FIELD_ACCOMMODATION_TYPES, $data['accommodation_types']);
        carbon_set_post_meta($postId, RatePlan::FIELD_PAYMENT_TYPES, $data['payment_types']);
        carbon_set_post_meta($postId, RatePlan::FIELD_CANCELLATION_POLICY, [$data['cancellation_policy']]);

        // Inheritance rules (if child rate plan)
        if (!empty($data['inheritance_price_rules'])) {
            $formattedRules = [];
            foreach ($data['inheritance_price_rules'] as $rule) {
                $formattedRules[] = [
                    RatePlan::FIELD_INHERITANCE_PRICE_RULES_PLACEMENTS          => $rule['placements'],
                    RatePlan::FIELD_INHERITANCE_PRICE_RULES_STRATEGY_TYPE       => $rule['strategy_type'],
                    RatePlan::FIELD_INHERITANCE_PRICE_RULES_STRATEGY_ADJUSTMENT => $rule['strategy_adjustment'],
                    RatePlan::FIELD_INHERITANCE_PRICE_RULES_STRATEGY_VALUE      => (string) $rule['strategy_value'],
                ];
            }
            carbon_set_post_meta($postId, RatePlan::FIELD_INHERITANCE_PRICE_RULES, $formattedRules);
        }

        if (isset($data['inheritance_restrictions'])) {
            carbon_set_post_meta($postId, RatePlan::FIELD_INHERITANCE_RESTRICTIONS, $data['inheritance_restrictions']);
        }

        return $postId;
    }

    /**
     * Create CancellationPolicy without transaction wrapper (for use in batch).
     *
     * @param  array<string, mixed> $data
     * @return int
     * @throws RuntimeException
     */
    private function doCreateCancellationPolicy(array $data): int
    {
        // Validate required fields
        $this->validateRequired($data, ['title']);

        // Create post
        $postId = wp_insert_post(
            [
            'post_type'    => CancellationPolicy::POST_TYPE,
            'post_title'   => $data['title'],
            'post_excerpt' => $data['excerpt'] ?? '',
            'post_status'  => $data['status'] ?? 'publish',
            'menu_order'   => $data['menu_order'] ?? 0,
            ], true
        );

        if (is_wp_error($postId)) {
            throw new RuntimeException(
                sprintf('Failed to create CancellationPolicy: %s', $postId->get_error_message())
            );
        }

        // Store preset key for identification by booking terms presets
        if (!empty($data['cancellation_key'])) {
            update_post_meta($postId, '_bflex_preset_key', $data['cancellation_key']);
        }

        // Set predefined rule (default to fully flexible if not specified)
        $predefinedRule = $data['predefined_rule'] ?? CancellationPolicy::RULE_FULLY_FLEXIBLE;
        carbon_set_post_meta($postId, CancellationPolicy::FIELD_PREDEFINED_RULE, $predefinedRule);

        // Save strict policy configuration fields if provided
        if ($predefinedRule === CancellationPolicy::RULE_STRICT) {
            if (isset($data['strict_time_window'])) {
                carbon_set_post_meta(
                    $postId,
                    CancellationPolicy::FIELD_STRICT_TIME_WINDOW,
                    $data['strict_time_window']
                );
            }

            if (isset($data['strict_penalty_percent'])) {
                carbon_set_post_meta(
                    $postId,
                    CancellationPolicy::FIELD_STRICT_PENALTY_PERCENT,
                    $data['strict_penalty_percent']
                );
            }
        }

        // Set custom rules if provided (only for custom rule type)
        if (!empty($data['rules']) && $predefinedRule === CancellationPolicy::RULE_CUSTOM) {
            $formattedRules = [];
            foreach ($data['rules'] as $rule) {
                $formattedRules[] = [
                    CancellationPolicy::FIELD_CANCELLED_MIN_HOURS_UNTIL_START => isset($rule['cancelled_min_hours_until_start']) ? (string) $rule['cancelled_min_hours_until_start'] : '',
                    CancellationPolicy::FIELD_CANCELLED_MAX_HOURS_UNTIL_START => isset($rule['cancelled_max_hours_until_start']) ? (string) $rule['cancelled_max_hours_until_start'] : '',
                    CancellationPolicy::FIELD_PENALTY_TYPE => $rule['penalty_type'],
                    CancellationPolicy::FIELD_PERCENTAGE_OF_BOOKING_AMOUNT => isset($rule['percentage_amount']) ? (string) $rule['percentage_amount'] : '',
                    CancellationPolicy::FIELD_FIXED_AMOUNT => isset($rule['fixed_amount']) ? (string) $rule['fixed_amount'] : '',
                    CancellationPolicy::FIELD_CHARGE_X_NIGHTS => isset($rule['charge_x_nights']) ? (string) $rule['charge_x_nights'] : '',
                ];
            }
            carbon_set_post_meta($postId, CancellationPolicy::FIELD_RULES, $formattedRules);
        }

        return $postId;
    }

    /**
     * Create PaymentType without transaction wrapper (for use in batch).
     *
     * @param  array<string, mixed> $data
     * @return int
     * @throws RuntimeException
     */
    private function doCreatePaymentType(array $data): int
    {
        // Validate required fields
        $this->validateRequired($data, ['title', 'calculator_type']);

        // Create post
        $postId = wp_insert_post(
            [
            'post_type'    => PaymentType::POST_TYPE,
            'post_title'   => $data['title'],
            'post_excerpt' => $data['excerpt'] ?? '',
            'post_status'  => $data['status'] ?? 'publish',
            'menu_order'   => $data['menu_order'] ?? 0,
            ], true
        );

        if (is_wp_error($postId)) {
            throw new RuntimeException(
                sprintf('Failed to create PaymentType: %s', $postId->get_error_message())
            );
        }

        // Store preset key for identification by booking terms presets
        if (!empty($data['payment_key'])) {
            update_post_meta($postId, '_bflex_preset_key', $data['payment_key']);
        }

        // Set Carbon Fields metadata
        carbon_set_post_meta($postId, PaymentType::FIELD_CALCULATOR_TYPE, $data['calculator_type']);

        // Optional fields
        if (isset($data['payment_payable'])) {
            carbon_set_post_meta($postId, PaymentType::FIELD_PAYMENT_PAYABLE, $data['payment_payable']);
        }

        if (isset($data['payment_system'])) {
            carbon_set_post_meta($postId, PaymentType::FIELD_PAYMENT_SYSTEM, $data['payment_system']);
        }

        if (isset($data['payment_authorization'])) {
            carbon_set_post_meta($postId, PaymentType::FIELD_PAYMENT_AUTHORIZATION, $data['payment_authorization']);
        }

        if (isset($data['time_for_confirmation'])) {
            carbon_set_post_meta($postId, PaymentType::FIELD_TIME_FOR_CONFIRMATION, (string) $data['time_for_confirmation']);
        }

        if (isset($data['prepayment_type'])) {
            carbon_set_post_meta($postId, PaymentType::FIELD_PREPAYMENT_TYPE, $data['prepayment_type']);
        }

        if (isset($data['prepayment_value'])) {
            carbon_set_post_meta($postId, PaymentType::FIELD_PREPAYMENT_VALUE, (string) $data['prepayment_value']);
        }

        if (isset($data['payment_availability_condition'])) {
            carbon_set_post_meta($postId, PaymentType::FIELD_PAYMENT_AVAILABILITY_CONDITION, $data['payment_availability_condition']);
        }

        if (isset($data['payment_availability_value'])) {
            carbon_set_post_meta($postId, PaymentType::FIELD_PAYMENT_AVAILABILITY_VALUE, (string) $data['payment_availability_value']);
        }

        if (isset($data['payment_availability_period'])) {
            carbon_set_post_meta($postId, PaymentType::FIELD_PAYMENT_AVAILABILITY_PERIOD, $data['payment_availability_period']);
        }

        return $postId;
    }

    /**
     * Execute an operation within a database transaction.
     *
     * @param  callable(): int $operation Operation to execute
     * @return int Result from operation
     * @throws RuntimeException If operation fails
     */
    private function executeInTransaction(callable $operation): int
    {
        global $wpdb;

        // Start transaction
        $wpdb->query('START TRANSACTION');

        try {
            $result = $operation();

            // Commit transaction
            $wpdb->query('COMMIT');

            return $result;
        } catch (\Exception $e) {
            // Rollback transaction
            $wpdb->query('ROLLBACK');

            throw $e;
        }
    }

    /**
     * Validate required fields are present in data array.
     *
     * @param  array<string, mixed> $data
     * @param  array<string>        $required
     * @throws RuntimeException
     */
    private function validateRequired(array $data, array $required): void
    {
        foreach ($required as $field) {
            if (!isset($data[$field])) {
                throw new RuntimeException(
                    sprintf('Missing required field: %s', $field)
                );
            }
        }
    }
}
