<?php

/**
 * Abstract syncer class.
 *
 * @package northmill-online-shared
 */
declare (strict_types=1);
namespace Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Abstracts\Hub_API\Sync;

use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Abstract_Shared_Base;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Abstracts\Hub_API\Hub_Accounts\Models\Hub_Account_Model;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Abstracts\Hub_API\Hub_Accounts\WP_Repository;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Helpers\Hook_Helpers;
use Northmill_Online_Shared_Checkout\Northmill_Online_Shared\Modules\Loggers\Log_Level_Enum;
defined('ABSPATH') || exit;
/**
 * Abstract syncer class.
 */
abstract class Abstract_Syncer
{
    /**
     * Syncer type.
     * Can be 'cron' or 'ajax'.
     *
     * @var string $type
     */
    private string $type;
    /**
     * Number of items to get in each API request.
     * Used only for cron syncs. This number for ajax syncs is set in settings.
     *
     * @var int
     */
    protected int $page_size = 100;
    /**
     * If this is set to true, the sync page size will be forced to the value set for the class.
     * This is useful for syncers that need to get all items in one request.
     * For instance, the value set in settings for ajax syncs will be ignored.
     *
     * @var bool
     */
    protected bool $force_page_size = \false;
    /**
     * Hub accounts to sync.
     *
     * @var Hub_Account_Model[]
     */
    private array $backoffice_accounts;
    /**
     * Hub account in the current iteration of sync loop.
     *
     * @var Hub_Account_Model
     */
    protected Hub_Account_Model $current_backoffice_account;
    /**
     * Array of sync statuses.
     *
     * @var int[] $status
     */
    protected array $status = array('add' => 0, 'new' => 0, 'update' => 0, 'delete' => 0, 'error' => 0, 'ignore' => 0, 'page' => 0, 'total' => 0);
    /**
     * Array of parameters sent with API request.
     *
     * @var array $api_params
     */
    protected array $api_params;
    /**
     * Array of last synced times. There is an array key for main, ajax and cron.
     *
     * @var array $last_synced_times
     */
    private array $last_synced_times;
    /**
     * Constructs syncer. If no Hub accounts are given, the current main account is used.
     *
     * @param Hub_Account_Model[] $backoffice_accounts Hub accounts to sync.
     * @param string              $type Type of syncer. Can be 'cron' or 'ajax'.
     */
    public function __construct(array $backoffice_accounts = array(), string $type = 'cron')
    {
        $this->type = $type;
        if (empty($backoffice_accounts)) {
            $backoffice_accounts[] = WP_Repository::get_current_main_account();
        }
        $backoffice_accounts = array_filter($backoffice_accounts);
        if (!empty($backoffice_accounts)) {
            $this->backoffice_accounts = $backoffice_accounts;
            $this->set_current_hub_account(reset($backoffice_accounts));
        }
    }
    /**
     * Syncs objects from Apps to WooCommerce.
     *
     * @param array|null $args Arguments to pass to API, or null if no arguments.
     *
     *                                @key int      'page_number'      Starting at 0.
     *                                @key int      'page_size'        Must be greater than 0.
     *                                @key string   'updated_since'    DateTime string if it should only do partial update.
     *                                @key callable 'custom_callback'  Custom callback for handling request response.
     *
     * @param bool       $is_first_sync If this sync is the first page of the first Hub account.
     *
     * @return bool Whether sync was successful.
     */
    abstract public function sync(?array $args = null, bool $is_first_sync = \false): bool;
    /**
     * Get syncer Hub accounts.
     *
     * @return Hub_Account_Model[]
     */
    public function get_hub_accounts(): array
    {
        return $this->backoffice_accounts ?? array();
    }
    /**
     * Check if syncer has Hub accounts.
     *
     * @return bool
     */
    public function has_hub_accounts(): bool
    {
        return !empty($this->backoffice_accounts);
    }
    /**
     * Get the syncer type. Defaults to 'cron'.
     *
     * @return string
     */
    public function get_type(): string
    {
        return $this->type ?? 'cron';
    }
    /**
     * Set syncer type.
     *
     * @param string $type Type of syncer. Can be 'cron' or 'ajax'.
     */
    public function set_type(string $type): void
    {
        $allowed_types = array('cron', 'ajax');
        if (!in_array($type, $allowed_types, \true)) {
            Abstract_Shared_Base::base_instance()->logger->log('sync', 'Invalid syncer type', array('type' => $type), Log_Level_Enum::ERROR);
            wp_die(esc_html('Invalid syncer type: ' . $type));
        }
        $this->type = $type;
    }
    /**
     * Check if the current sync is of type 'cron'.
     *
     * @return bool
     */
    public function is_cron(): bool
    {
        return 'cron' === $this->get_type();
    }
    /**
     * Check if the current sync is of type 'ajax'.
     *
     * @return bool
     */
    public function is_ajax(): bool
    {
        return 'ajax' === $this->get_type();
    }
    /**
     * Get API request page size.
     *
     * @return int
     */
    public function get_page_size(): int
    {
        return $this->page_size;
    }
    /**
     * Set API request page size.
     *
     * @param int $page_size Page size.
     *
     * @return int New page size.
     */
    public function set_page_size(int $page_size): int
    {
        if (!$this->force_page_size) {
            $this->page_size = $page_size;
        }
        return $this->page_size;
    }
    /**
     * Check if syncer is active.
     *
     * @return bool
     */
    public function is_enabled(): bool
    {
        return \true;
    }
    /**
     * Get syncer status.
     *
     * @return int[]
     */
    public function get_status(): array
    {
        return $this->status;
    }
    /**
     * Update syncer status.
     *
     * @param string     $key    Key to set status for. If null, $status is the entire status array.
     * @param int|string $status Value to set. If $key is set, this is the value to set for that key. Otherwise, it is the entire status array.
     *
     * @return array Updated status array.
     */
    public function update_status(string $key, int|string $status): array
    {
        if (!is_numeric($status)) {
            $status = 0;
        }
        $this->status[$key] = (int) $status;
        return $this->status;
    }
    /**
     * Set syncer status.
     *
     * @param array $status Status array to replace current status with.
     *
     * @return int[]
     */
    public function set_status(array $status): array
    {
        $this->status = $status;
        return $this->status;
    }
    /**
     * Increase syncer status.
     *
     * @param string $key   Key to increase status for.
     * @param int    $value Value to increase status by.
     *
     * @return void
     */
    public function increase_status(string $key, int $value = 1): void
    {
        $this->status[$key] += $value;
    }
    /**
     * Sets and returns skip status
     */
    public function set_skip_status(): array
    {
        $this->status = array('total' => 0, 'skipped' => 1);
        return $this->get_status();
    }
    /**
     * Sets and returns skip status
     */
    public function set_cleaned_status(): array
    {
        $this->status = array('total' => 0, 'cleaned' => 1);
        return $this->get_status();
    }
    /**
     * Loop for sync.
     *
     * @param array|null $params       API params.
     */
    public function sync_loop(?array $params): bool
    {
        $this->before_sync_loop($params);
        $synced = $this->sync_loop_inner($params);
        $this->after_sync_loop();
        return $synced;
    }
    /**
     * Function to run before sync loop.
     *
     * @param array|null $args API params passed by reference.
     */
    public function before_sync_loop(?array &$args)
    {
    }
    /**
     * Loop for sync.
     *
     * @param ?array $params API params.
     *
     * @return bool Whether all syncs were successful.
     */
    public function sync_loop_inner(?array $params = array()): bool
    {
        $is_first = \true;
        foreach ($this->get_hub_accounts() as $backoffice_account) {
            $this->set_current_hub_account($backoffice_account);
            $this->parse_api_params($params);
            $synced = $this->sync($this->api_params, $is_first);
            $is_first = \false;
            if (!$synced) {
                Abstract_Shared_Base::base_instance()->logger->log('sync', 'Sync error in class', array('class' => get_class($this)), Log_Level_Enum::ERROR);
                return \false;
            }
            $status = $this->get_status();
            if (!empty($status['total']) && $status['total'] > $this->api_params['pageSize']) {
                $this->api_params['page'] = 1;
                $total_pages = (int) ceil($status['total'] / $this->api_params['pageSize']);
                $this->update_status('total_pages', $total_pages);
                while ($this->api_params['page'] < $total_pages) {
                    $synced = $this->sync($this->api_params);
                    if (!$synced) {
                        Abstract_Shared_Base::base_instance()->logger->log('sync', 'Sync error', array('class' => get_class($this)), Log_Level_Enum::ERROR);
                        return \false;
                    }
                    ++$this->api_params['page'];
                }
            }
        }
        return \true;
    }
    /**
     * Parses API params, adding default values if not present.
     *
     * @param null|array $params API params.
     *
     * @return array
     */
    public function parse_api_params(?array $params = array()): array
    {
        if (empty($params['page'])) {
            if (!empty($params['page_number'])) {
                $params['page'] = (int) $params['page_number'];
            } else {
                $params['page'] = 0;
            }
        }
        if (!empty($params['updated_since'])) {
            $params['UpdatedSince'] = $params['updated_since'];
        } elseif (!empty($params['last_updated_at'])) {
            $params['UpdatedSince'] = $params['last_updated_at'];
        }
        if (!empty($params['modified_since'])) {
            $params['modifiedSince'] = $params['modified_since'];
        }
        $params['pageSize'] = $this->page_size;
        $this->api_params = $params;
        return $params;
    }
    /**
     * Function to run after sync loop.
     * This will be run even if the sync loop is interrupted.
     *
     * @return void
     */
    public function after_sync_loop()
    {
    }
    /**
     * Gets the current Hub account.
     *
     * @return null|Hub_Account_Model
     */
    public function get_current_hub_account(): ?Hub_Account_Model
    {
        return $this->current_backoffice_account ?? null;
    }
    /**
     * Sets the current Hub account.
     *
     * @param mixed $current_backoffice_account Hub account to sync.
     */
    public function set_current_hub_account(mixed $current_backoffice_account): void
    {
        if (is_a($current_backoffice_account, Hub_Account_Model::class)) {
            $this->current_backoffice_account = $current_backoffice_account;
        } else {
            Abstract_Shared_Base::base_instance()->logger->log('sync', 'Invalid Hub account given to syncer', array('account' => $current_backoffice_account), Log_Level_Enum::ERROR);
        }
    }
    /**
     * Get 'updated since' date for the type.
     *
     * @param string $time_type Time type (main, ajax, cron).
     *
     * @return string
     */
    public function get_last_synced_wp_option(string $time_type = 'main'): string
    {
        if (!isset($this->last_synced_times)) {
            $times = array();
            $option_names = $this->get_all_last_synced_wp_option_names();
            foreach ($option_names as $key => $option_name) {
                $times[$key] = get_option($option_name, '');
            }
            $this->last_synced_times = $times;
        }
        return $this->last_synced_times[$time_type];
    }
    /**
     * Set 'updated since' date for the type.
     *
     * @param string      $date Date.
     * @param null|string $updated_from Source of update (ajax, cron).
     * @param bool        $got_all_data If true, all data was fetched, not only recent changes. Only works with $updated_from = 'ajax'.
     *
     * @return void
     */
    public function set_last_synced_wp_option(string $date, ?string $updated_from, bool $got_all_data = \false): void
    {
        $option_name = $this->get_last_synced_wp_option_name();
        update_option($option_name, $date);
        Hook_Helpers::do_scoped_action('northmill_online_shared_set_last_synced_wp_option', $option_name, $date);
        Hook_Helpers::do_scoped_action('northmill_online_shared_set_last_synced_wp_option_' . $option_name, $date);
        $full_option_name = $option_name . '_';
        if ('ajax' === $updated_from) {
            if ($got_all_data) {
                $full_option_name .= 'ajax_all';
            } else {
                $full_option_name .= 'ajax';
            }
        } elseif ('cron' === $updated_from) {
            $full_option_name .= 'cron';
        } else {
            Abstract_Shared_Base::base_instance()->logger->log('sync', 'Invalid updated from', array('updated_from' => $updated_from), Log_Level_Enum::ERROR);
            return;
        }
        update_option($full_option_name, $date);
        Hook_Helpers::do_scoped_action('northmill_online_shared_set_last_synced_wp_option', $full_option_name, $date);
        Hook_Helpers::do_scoped_action('northmill_online_shared_set_last_synced_wp_option_' . $full_option_name, $date);
    }
    /**
     * Clear 'updated since' date for the type.
     *
     * @param bool $only_main If true, only the main updated since time is cleared.
     *                        If false, all updated since times are cleared.
     *                        The main time is the one actually used in API requests.
     *
     * @return void
     *
     * @used Northmill Sync – Stock syncer.
     */
    public function clear_last_synced_wp_option(bool $only_main = \true): void
    {
        $option_names = $this->get_all_last_synced_wp_option_names();
        delete_option($option_names['main']);
        if (!$only_main) {
            unset($option_names['main']);
            foreach ($option_names as $option_name) {
                delete_option($option_name);
            }
        }
    }
    /**
     * Get the display name of the syncer.
     * Should in most cases be the name of the item that is synced in plural.
     * Like 'Orders' or 'Articles'.
     *
     * @return string
     */
    abstract public function get_display_name(): string;
    /**
     * Get the name of WP option storing last updated time.
     *
     * @return string
     */
    abstract public function get_last_synced_wp_option_name(): string;
    /**
     * Get all last synced WP option names.
     *
     * @return string[]
     */
    public function get_all_last_synced_wp_option_names(): array
    {
        $base_name = $this->get_last_synced_wp_option_name();
        return array('main' => $base_name, 'ajax' => $base_name . '_ajax', 'ajax_all' => $base_name . '_ajax_all', 'cron' => $base_name . '_cron');
    }
    /**
     * Get the name of API parameter used when getting only data updated since the last request to API.
     * Leave empty if not used.
     *
     * @return string|null
     */
    abstract public function get_last_synced_api_parameter(): ?string;
}
