<?php namespace Tussendoor\Reloadify\Controllers\AbandonedCart;

use ReloadifyScoped\Carbon\Carbon;
use ReloadifyScoped\Illuminate\Database\Eloquent\ModelNotFoundException;
use ReloadifyScoped\Illuminate\Database\QueryException;
use Tussendoor\Reloadify\Concerns\HasActions;
use Tussendoor\Reloadify\Helpers\Request;
use Tussendoor\Reloadify\Models\AbandonedCart\AbandonedCart;
use Tussendoor\Reloadify\Models\AbandonedCart\AbandonedCartMeta;
use Tussendoor\Reloadify\Plugin;

final class AbandonedCartController extends AbandonedCartMetaController
{
    use HasActions;

    public function register()
    {
        $this->addActions();
    }

    public function addActions()
    {
        /**
         * Action to watch the cart contents. Fires atleast on:
         * - Add to cart
         * - Item quantity update
         * - Coupon applied / removed
         */
        add_action('woocommerce_after_calculate_totals', [$this, 'middlewareCreateOrUpdate']);

        /**
         * Cart could be empty again
         */
        add_action('woocommerce_cart_item_removed', [$this, 'handleCartOnItemRemoved']);

        /**
         * Cart is not abandoned
         * @since 1.3.0 disabled the functionality to remove abandoned carts when a new order is placed. This is handled at Reloadify's end.
         */
        // add_action('woocommerce_new_order', [$this, 'deleteAbandonedCart']);
        add_action('woocommerce_new_order', [$this, 'handleSuccessfulOrderPlacement']);

        /**
         * Ajax action to save CartMeta when user enters its information
         */
        add_action('wp_ajax_' . self::$saveCheckoutUserMeta, [$this, 'parseCheckoutRequest']);
        add_action('wp_ajax_nopriv_' . self::$saveCheckoutUserMeta, [$this, 'parseCheckoutRequest']);

        /**
         * Recurring task that is set in our config file and executed by our ScheduleController
         *
         * @see /config/tasks.php
         */
        add_action('reloadify_recurring_delete_old_abandoned_carts', [$this, 'deleteOldAbandonedCarts']);
    }

    /**
     * Middleware method so we can use an non-required parameter in the method
     *
     * @see $this->createOrUpdateAbandonedCart()
     *
     * @param  \WC_Cart $cart
     * @return bool
     */
    public function middlewareCreateOrUpdate(\WC_Cart $cart) : bool
    {
        return $this->createOrUpdateAbandonedCart();
    }

    /**
     * Save new or update existing model based on our custom session hash
     *
     * @since 1.3.0 removed check on dontWantToSaveAbandonedCart() because we want to save the cart even when the customer data is empty
     *
     * @param array $checkoutCustomerData - when called from the ajax request we send parsed customer data
     * @return bool
     */
    public function createOrUpdateAbandonedCart(array $checkoutCustomerData = []) : bool
    {
        if ($this->cantSaveAbandonedCart()) return false;

        $reloadifyHash = $this->getReloadifyHash();

        try {
            $findExistingCartOn         = $this->getFindExistingCartOnData($reloadifyHash, $checkoutCustomerData);
            $updateOrCreateCartWithData = $this->updateOrCreateCartWithData($reloadifyHash, $checkoutCustomerData);

            $cart = AbandonedCart::updateOrCreate(
                $findExistingCartOn,
                $updateOrCreateCartWithData
            );
        } catch (QueryException $e) {
            return false;
        }

        /**
         * When this method is called via our custom ajax call from the checkout
         * We get the correct meta data from the filled in fields in the checkout form
         *
         * @see $this->parseCheckoutRequest(); This is where the information is parsed
         */
        if (!empty($checkoutCustomerData)) {
            return $this->updateCreateCartMeta($checkoutCustomerData, $cart->id);
        }

        /**
         * If the user data has allready been added to the WC Session
         */
        if ($this->hasCustomerSession()) {
            return $this->parseCustomerMetaData($cart->id);
        }

        /**
         * When a user is logged in we can save the user data as meta data
         */
        if (is_user_logged_in()) {
            return $this->parseUserMetaData($cart->id);
        }

        return true;
    }

    /**
     * Will return array of data to find an existing abandoned cart with
     * When customerdata is given we can also find existing carts based on the provided email in the meta data
     * In that case we will find the cart on the cart_id and not on the hash, if hash does not match the hash will be added to the updateArray
     *
     * @see ->updateOrCreateCartWithData() Where we will add current reloadifyHash to the update data
     *
     * @param  string $reloadifyHash
     * @param  array $existingCustomerData
     * @return array
     */
    private function getFindExistingCartOnData(string $reloadifyHash, array $existingCustomerData)
    {
        $findExistingCartOn = [
            'hash' => sanitize_key($reloadifyHash)
        ];

        if (empty($existingCustomerData)) return $findExistingCartOn;

        try {
            $meta = AbandonedCartMeta::where('meta_key', 'email')->where('meta_value', esc_html($existingCustomerData['email']))->firstOrFail();
        } catch (QueryException $e) {
            return $findExistingCartOn;
        }

        $findExistingCartOn['id'] = $meta->cart_id;
        unset($findExistingCartOn['hash']);

        return $findExistingCartOn;
    }

    /**
     * Will return the array of data to update an existing abandoned cart with
     * When customerdata is given we can also find existing carts based on the provided email in the meta data
     * In that case we will find the cart on the cart_id and not on the hash and the current reloadify hash will be added to the update data array
     *
     * @param  string $reloadifyHash
     * @param  array $existingCustomerData
     * @return array
     */
    private function updateOrCreateCartWithData(string $reloadifyHash, array $existingCustomerData)
    {
        $updateOrCreateCartWithData = [
            'currency'      => get_woocommerce_currency(),
            'price'         => $this->getCartTotalPriceIncludingTaxes(),
            'recovery_url'  => $this->getRecoveryUrl(),
            'product_ids'   => json_encode($this->getProductIDs()),
            'product_data'  => json_encode($this->getProductData()),
        ];

        if (empty($existingCustomerData)) return $updateOrCreateCartWithData;

        try {
            $meta = AbandonedCartMeta::where('meta_key', 'email')->where('meta_value', esc_html($existingCustomerData['email']))->firstOrFail();
        } catch (QueryException $th) {
            return $updateOrCreateCartWithData;
        }

        $updateOrCreateCartWithData['hash'] = $reloadifyHash;
        return $updateOrCreateCartWithData;
    }

    /**
     * When an item is removed we check if the cart is empty
     * If it is we delete the abandoned cart from the DB
     * If not; we update the existing saved cart
     *
     * @return bool
     */
    public function handleCartOnItemRemoved() : bool
    {
        if (empty(WC()->session) || is_null(WC()->cart)) {
            error_log('AbandonedCartController::handleCartOnItemRemoved - WC Cart or Session is null.');
            return false;
        }

        if (empty(WC()->session->get('reloadify_hash'))) return false;

        if (empty(WC()->cart) || empty(WC()->cart->get_cart_contents())) {
            return $this->deleteAbandonedCart();
        }

        return $this->createOrUpdateAbandonedCart();
    }

    /**
     * Get the reloadify hash
     *
     * @return string
     */
    protected function getReloadifyHash() : string
    {
        if (empty(WC()->session)) {
            error_log('AbandonedCartController::getReloadifyHash - WC session is null.');
            return ''; // Return a default/fallback value to avoid breaking the flow.
        }


        $request = Request::fromGlobal();

        if ($request->has(Plugin::config('plugin.recovery_parameter'))) {
            return $request->getKey(Plugin::config('plugin.recovery_parameter'));
        }

        return WC()->session->get('reloadify_hash');
    }

    /**
     * Get the product ID's that are inside the current cart
     *
     * @return array
     */
    public function getProductIDs() : array
    {
        $product_ids = [];
        foreach (WC()->cart->get_cart() as $cart_item_key => $cart_item) {
            if (empty($cart_item['product_id'])) continue;

            $product_ids[] = $cart_item['product_id'];
        }

        return $product_ids;
    }

    /**
     * Get the product ID's, the product quantities and return them as an associative array
     *
     * @return array
     */
    public function getProductData() : array
    {
        $productData = [];
        foreach (WC()->cart->get_cart() as $cart_item_key => $cart_item) {
            if (empty($cart_item['product_id']) || empty($cart_item['quantity'])) continue;

            $is_variable = (!empty($cart_item['variation_id']) && !empty($cart_item['variation']));

            $productData[$cart_item['product_id']] = [
                'quantity'      => $cart_item['quantity'],
                'is_variable'   => $is_variable,
                'variation_id'  => $cart_item['variation_id'],
                // Add this line to capture all cart item data
                'cart_item_data' => array_diff_key($cart_item, array_flip(['product_id', 'quantity', 'variation_id', 'variation']))
            ];

            if ($is_variable) {
                $productData[$cart_item['product_id']]['variation_data'] = $cart_item['variation'];
            }

        }

        return $productData;
    }


    /**
     * Get the total cart price as a float. This total includes taxes and discounts,
     * providing a numeric representation of the final amount the customer will be charged.
     * Suitable for database storage without formatting.
     *
     * @return float The total cart price including taxes and discounts.
     */
    protected function getCartTotalPriceIncludingTaxes() : float {
        // Ensure WC Cart is available to prevent potential errors
        if (!is_a(WC()->cart, 'WC_Cart')) {
            return 0.00; // Fallback value or handle the error as appropriate
        }

        // Retrieve the total amount as a float. The 'false' parameter ensures that
        // the total is returned as a numeric value without currency formatting.
        // This includes taxes, discounts, and adjusts for any WooCommerce settings.
        $total = WC()->cart->get_total(false);

        // Cast to float to ensure the return type is correct, even though
        // get_total already returns a float. This is just an extra precaution.
        return (float) $total;
    }


    /**
     * Get the cart totals price as the formatted number.
     * We will get the price based on the action woocommerce_after_calculate_totals, this is as late as possible so after applying coupons and everything.
     *
     * @return string
     *
     * @deprecated since version 1.5.3. Use getCartTotalPriceAsFloat() instead for a more accurate representation of the cart total.
     */
    protected function getFormattedCartTotalPrice() : string
    {
        // Trigger a deprecation warning when this method is used.
        trigger_error('Method ' . __METHOD__ . ' is deprecated since version 1.5.1. Use getCartTotalPriceAsFloat() instead.', E_USER_DEPRECATED);

        // You can either call the new method directly or keep the old logic here.
        // If you choose to call the new method, ensure it provides compatible functionality or adjust as needed.
        return (string) $this->getCartTotalPriceIncludingTaxes();

        // Old logic is commented out or removed to prevent execution, but kept for historical reference or until completely removed in a future version.
        /*
        $float      = (float) WC()->cart->get_cart_contents_total();
        $decimals   = wc_get_price_decimals();
        $cartTotals = number_format($float, $decimals, '.', ',');
        return (string) $cartTotals;
        */
    }

    /**
     * Complite the recovery url and return as string
     *
     * @return string
     */
    protected function getRecoveryUrl() : string
    {
        $hash   = sanitize_key(WC()->session->get('reloadify_hash'));
        $string = wc_get_cart_url() . '?' . Plugin::config('plugin.recovery_parameter') . '=' . $hash;

        return (string) $string;
    }

    /**
     * Delete an abandoned cart model
     *
     * @return bool
     */
    public function deleteAbandonedCart() : bool
    {
        if (is_admin() || empty(WC()) || empty(WC()->session)) return false;

        try {
            $hash = sanitize_key(WC()->session->get('reloadify_hash'));
            $cart = AbandonedCart::where('hash', $hash)->firstOrFail();
        } catch (ModelNotFoundException $e) {
            return false;
        }

        $cart->delete();

        // Unset our custom hash so we can make a new one when needed
        WC()->session->__unset('reloadify_hash');

        return true;
    }

    /**
     * Parse the request that was send from the checkout page
     * Find the customer and save the information as cart meta
     *
     * @since 1.3.0 removed the array filter on $checkoutCustomerData so when a customer empties a field we will empty it too.
     *
     * @return bool
     */
    public function parseCheckoutRequest() : bool
    {
        check_ajax_referer('woocommerce-process_checkout');

        $request = new Request($_POST);

        $checkoutCustomerData = [
            'first_name'    => $request->getString('first_name'),
            'last_name'     => $request->getString('last_name'),
            'email'         => $request->getEmail('email'),
            // Billing information
            'billing_street'    => $request->getString('billing_street'),
            'billing_city'      => $request->getString('billing_city'),
            'billing_state'     => $request->getString('billing_state'),
            'billing_postcode'  => $request->getString('billing_postcode'),
            'billing_country'   => $request->getString('billing_country'), // Assuming you also need country
            // Shipping information
            'shipping_street'   => $request->getString('shipping_street', $request->getString('billing_street')),
            'shipping_city'     => $request->getString('shipping_city', $request->getString('billing_city')),
            'shipping_state'    => $request->getString('shipping_state', $request->getString('billing_state')),
            'shipping_postcode' => $request->getString('shipping_postcode', $request->getString('billing_postcode')),
            'shipping_country'  => $request->getString('shipping_country', $request->getString('billing_country')), // Assuming similar logic for country
        ];

        /**
         * No hash was saved yet so we are sure we need to create a new entry
         */
        if (!WC()->session->__isset('reloadify_hash')) {
            return $this->createOrUpdateAbandonedCart($checkoutCustomerData);
        }

        /**
         * Try to find an existing entry based on the custom session hash
         * When the entry does not exist, create a new one
         */
        try {
            $hash = sanitize_key(WC()->session->get('reloadify_hash'));
            $cart = AbandonedCart::where('hash', $hash)->firstOrFail();
        } catch (ModelNotFoundException $e) {
            return $this->createOrUpdateAbandonedCart($checkoutCustomerData);
        }

        /**
         * Entry exists so we only need to update the meta data of this abandoned cart based on the checkout data
         */
        return $this->updateCreateCartMeta($checkoutCustomerData, $cart->id);
    }

    /**
     * Return true when there was no cart data and there is no cart data
     *
     * @since 1.4.0 also return true when this is an admin recovery session
     *
     * @return bool
     */
    private function cantSaveAbandonedCart() : bool
    {
        return empty(WC()->session->get('reloadify_hash')) || empty(WC()->cart) || empty(WC()->cart->get_cart_contents());
    }

    /**
     * Return true when there is no customer data to save as meta
     * When there is no customer data (yet) we do not want to save the cart
     *
     * @since 1.1.5 We do not want to save abandoned carts if the action was fired from the admin
     *
     * @return bool
     */
    private function dontWantToSaveAbandonedCart(array $customerData) : bool
    {
        return !is_user_logged_in() && !$this->hasCustomerSession() && empty($customerData) && !is_admin();
    }

    /**
     * Helper method to check if the customer has been saved to the session
     *
     * @since 1.0.5 Removed necessary first_name and last_name
     *
     * @return bool
     */
    protected function hasCustomerSession() : bool
    {
        if (empty(WC()->session->get('customer'))) return false;

        $customer   = WC()->session->get('customer');
        $request    = new Request($customer);

        return $request->isNotEmpty('email');
    }

    /**
     * Delete abandoned carts older then the given date
     *
     * @param  string $field
     * @param  string $subOf
     * @param  int $amount
     * @return bool
     */
    public function deleteOldAbandonedCarts(string $field = 'created_at', string $subOf = 'month', int $amount = 3) : bool
    {
        switch ($subOf) {
            case 'day':
                $interval = Carbon::today()->subDays($amount)->toDateString();
                break;

            case 'week':
                $interval = Carbon::today()->subWeeks($amount)->toDateString();
                break;

            default:
                $interval = Carbon::today()->subMonths($amount)->toDateString();
                break;
        }

        return AbandonedCart::whereDate($field, '<', $interval)->delete();
    }

    /**
     * Handle successful order placement
     * For now, only the hash will be removed from the session, allowing the same customer to create a new abandoned cart.
     *
     * Note: There are rare scenarios where WC()->session might be null, prompting the inclusion of a null check:
     * 1. Cron Jobs or WP-CLI Commands: Sessions might not be initialized in non-web contexts like cron jobs or WP-CLI commands.
     * 2. Custom Session Handling: Modifications by plugins or custom code that affect session initialization or management could lead to WC()->session being null.
     * 3. Early Hook Invocation: If the 'woocommerce_new_order' action is triggered earlier than expected before WooCommerce initializes the session.
     * 4. Errors or Conflicts: Plugin conflicts or errors that disrupt the normal session initialization process.
     * These edge cases, while uncommon, justify a precautionary approach to ensure robustness and aid in debugging.
     *
     * @since 1.4.0
     */
    public function handleSuccessfulOrderPlacement()
    {
        if (is_null(WC()->session)) {
            // Log the situation for debugging purposes, as the session being null is unexpected here
            error_log('Warning: WC session is null in handleSuccessfulOrderPlacement. This could indicate that the WooCommerce session was not properly initiated or an edge case scenario occurred.');
        } else {
            WC()->session->__unset('reloadify_hash');
            WC()->session->__unset('reloadify_admin_recovery');
        }
    }




}
