<?php

namespace DevOwl\SearchEnginePostType;

use DevOwl\Multilingual\AbstractLanguagePlugin;
use Exception;
use WP_CLI;

/**
 * Main class to initialize an search engine for a post type.
 */
class SearchEnginePostType {
    use UtilsProvider;

    /**
     * The WP CLI command for this post type.
     *
     * @var WPCli
     */
    private $command;

    /**
     * The provider implementation.
     *
     * @var AbstractSearchEngine
     */
    private $provider;

    /**
     * Auto updater for posts
     *
     * @var AbstractSearchEngine
     */
    private $autoUpdates;

    /**
     * Quota handler
     *
     * @var Quota
     */
    private $quota;

    private $args;

    /**
     * Initialize this class in your `init` action!
     *
     * Arguments:
     *
     * - `name`: Name for the search index
     * - `post_type`: Post type which should be used as data
     * - `provider`: Currently only `AlgoliaSearchEngine::class` is possible
     * - `[auto_updates]`: Enable auto updates so it can be incrementally updated (default: `true`)
     * - `[rest_service]`: Enable common REST endpoints for this search (default: `true`)
     * - `[environment]`: An environment like "development", "staging" or "production" (default: `wp_get_environment_type()`)
     * - `[abstractLanguagePlugin]`: An instance of `DevOwl\Multilingual\AbstractLanguagePlugin` so the index can be multilingual
     * - `[custom_fields]`: Set keys of custom post meta to keep in the records (default: `["_thumbnail_id"]`)
     * - `[modifyMeta]`: A callback to modify the meta for a given post (arguments: `&$postArray; WP_Post $post, SearchEnginePostType $searchEnginePostType`)
     * - `[modifyRecords]`: A callback to modify the records created for a single post (arguments: `&$records, $postArray, WP_Post $post, SearchEnginePostType $searchEnginePostType, AbstractSearchEngine $provider`)
     * - `[modifyRecord]`: A callback to modify a single record created for a single post before duplication (arguments: `&$postArray, WP_Post $post, SearchEnginePostType $searchEnginePostType, AbstractSearchEngine $provider`)
     * - `[quota]`: Set a maximum allowed search queries for this search
     *
     * Additional arguments for Algolia:
     * - `[appId]`: Your Algolia Application ID (default: `defined('ALGOLIA_APP_ID') ? constant('ALGOLIA_APP_ID') : ''`)
     * - `[apiKey]`: Your Algolia API key (default: `defined('ALGOLIA_API_KEY') ? constant('ALGOLIA_API_KEY') : ''`)
     * - `[configureIndex]`: A callback to modify the settings for the index (arguments: `&$settings; AlgoliaSearchEngine $search`)
     * - `[modifyHit]`: A callback to modify the hit for a search result (arguments: `Hit $hit; array $row, AlgoliaSearchEngine $search`)
     * - `[modifySearchArguments]`: A callback to modify the arguments for a search (arguments: `&$args; AlgoliaSearchEngine $search`)
     * - `[modifySearchTaxonomySlugs]`: A callback to modify the slugs for a taxonomy search (arguments: `&$args; $taxonomy, AlgoliaSearchEngine $search`)
     *
     * @param array $args See constructor description
     * @throws Exception
     * @codeCoverageIgnore
     */
    public function __construct($args = []) {
        $this->args = $args;
        $this->init();
    }

    /**
     * Search by a term and additional arguments. The arguments differ from provider to provider.
     *
     * @param string $term
     * @param array $args
     */
    public function search($term, $args = []) {
        return $this->getProvider()->search($term, $args);
    }

    /**
     * Initialize our post type.
     *
     * @throws Exception
     */
    protected function init() {
        // Parse and validate main arguments
        $this->args = $this->parseArguments($this->args);
        $this->validateArguments($this->args);

        // Create provider
        $providerClass = $this->getArg('provider');
        $provider = new $providerClass($this);

        if (!($provider instanceof AbstractSearchEngine)) {
            self::throw('Please provide an implementation provider extending from `AbstractSearchEngine`!');
        }

        // Prase and validate provider arguments
        $this->args = $provider->parseArguments($this->args);
        $provider->validateArguments($this->args);
        $this->provider = $provider;

        // Create auto updates
        if ($this->getArg('auto_updates')) {
            $this->autoUpdates = new AutoUpdates($this);
        }

        // Create REST services
        if ($this->getArg('rest_service')) {
            $this->autoUpdates = new RestService($this);
        }

        // Create REST services
        if ($this->getArg('quota') > 0) {
            $this->quota = new Quota($this);
        }

        // Create CLI command(s)
        if (self::isCLI()) {
            $this->command = new WPCli($this);
        }
    }

    /**
     * Parse arguments passed to the constructor.
     *
     * @param array $args
     */
    protected function parseArguments($args) {
        return wp_parse_args($args, [
            'name' => '',
            'post_type' => '',
            'provider' => '',
            'auto_updates' => true,
            'rest_service' => true,
            'environment' => wp_get_environment_type(),
            'abstractLanguagePlugin' => null,
            'custom_fields' => [],
            'modifyMeta' => null,
            'modifyRecords' => null,
            'quota' => 0,
        ]);
    }

    /**
     * Validate arguments.
     *
     * @param array $args
     * @throws Exception
     */
    protected function validateArguments($args) {
        if (empty($args['name'])) {
            self::throw('Please provide a name for the Algolia search index!');
        }

        if (empty($args['post_type'])) {
            self::throw('Please provide a post type which should be used as data!');
        }

        if (empty($args['provider']) || !class_exists($args['provider'])) {
            self::throw('Please provide an existing implementation provider for your data!');
        }

        if (empty($args['environment'])) {
            self::throw('Please provide an environment like "development", "staging" or "production"!');
        }

        if (
            !empty($args['abstractLanguagePlugin']) &&
            !($args['abstractLanguagePlugin'] instanceof AbstractLanguagePlugin)
        ) {
            self::throw(
                'Please provide a correct instance of `DevOwl\Multilingual\AbstractLanguagePlugin` for `abstractLanguagePlugin`!'
            );
        }
    }

    /**
     * Get the index name for the current post type.
     */
    public function getIndexName() {
        $compLanguage = $this->getCompLanguage();
        $indexName = sprintf('%s_%s', $this->getArg('environment'), $this->getArg('name'));

        if ($compLanguage !== null && $compLanguage->isActive()) {
            $indexName .= '_' . $compLanguage->getCurrentLanguage();
        }
        return $indexName;
    }

    /**
     * Getter.
     *
     * @return AbstractLanguagePlugin|null
     */
    public function getCompLanguage() {
        return $this->getArg('abstractLanguagePlugin');
    }

    /**
     * Getter.
     *
     * @param string $name The argument name like `environment`, see constructor
     * @codeCoverageIgnore
     */
    public function getArg($name) {
        return $this->args[$name] ?? null;
    }

    /**
     * Getter.
     *
     * @codeCoverageIgnore
     */
    public function getCommand() {
        return $this->command;
    }

    /**
     * Getter.
     *
     * @codeCoverageIgnore
     */
    public function getProvider() {
        return $this->provider;
    }

    /**
     * Getter.
     *
     * @codeCoverageIgnore
     */
    public function getAutoUpdates() {
        return $this->autoUpdates;
    }

    /**
     * Getter.
     *
     * @codeCoverageIgnore
     */
    public function getQuota() {
        return $this->quota;
    }

    /**
     * Check if current request is coming from WP CLI.
     *
     * @see https://wordpress.stackexchange.com/a/226163/83335
     */
    public static function isCLI() {
        return defined('WP_CLI') && constant('WP_CLI');
    }

    /**
     * Throw a message as exception or via `WP_CLI::error`.
     *
     * @param string $message
     * @throws Exception
     */
    public static function throw($message) {
        throw new Exception($message);
    }

    /**
     * Log a message (only in WP CLI).
     *
     * @param string $message
     */
    public static function log($message) {
        if (self::isCLI()) {
            WP_CLI::log(sprintf('[%s] [LOG] %s', self::microtimeDate(), $message));
        }
    }

    /**
     * Success a message (only in WP CLI).
     *
     * @param string $message
     */
    public static function success($message) {
        if (self::isCLI()) {
            WP_CLI::log(sprintf("[%s] \033[32m%s\033[0m %s", self::microtimeDate(), '[SUCCESS]', $message));
        }
    }

    /**
     * Get the date as formatted string with milliseconds.
     *
     * @see https://stackoverflow.com/a/17909891/5506547
     */
    public static function microtimeDate() {
        $micro_date = microtime();
        $date_array = explode(' ', $micro_date);
        $date = gmdate('Y-m-d H:i:s', $date_array[1]);
        return sprintf('%s:%s', $date, $date_array[0]);
    }
}
