<?php
//  ---------------------------------------------------------------------------------
//    _____   _             _                   _   _____
//   |  __ \ | |           | |    /\           (_) |  __ \
//   | |__) || |__    __ _ | |   /  \    _ __   _  | |__) |_ __  ___
//   |  ___/ | '_ \  / _` || |  / /\ \  | '_ \ | | |  ___/| '__|/ _ \
//   | |     | | | || (_| || | / ____ \ | |_) || | | |    | |  | (_) |
//   |_|     |_| |_| \__,_||_|/_/    \_\| .__/ |_| |_|    |_|   \___/
//                                      | |
//                                      |_|
//   PhalApi Pro 专业版
//   广州果创网络科技有限公司
//
//  ---------------------------------------------------------------------------------
//
//   一、协议的许可和权利
//    1. 您可以在完全遵守本协议的基础上，将本软件应用于商业用途；
//    2. 您可以在协议规定的约束和限制范围内修改本产品源代码或界面风格以适应您的要求；
//    3. 您拥有使用本产品中的全部内容资料、商品信息及其他信息的所有权，并独立承担与其内容相关的
//       法律义务；
//    4. 获得商业授权之后，您可以将本软件应用于商业用途，自授权时刻起，在技术支持期限内拥有通过
//       指定的方式获得指定范围内的技术支持服务；
//
//   二、协议的约束和限制
//    1. 未获商业授权之前，禁止将本软件用于商业用途（包括但不限于企业法人经营的产品、经营性产品
//       以及以盈利为目的或实现盈利产品）；
//    2. 未获商业授权之前，禁止在本产品的整体或在任何部分基础上发展任何派生版本、修改版本或第三
//       方版本用于重新开发；
//    3. 如果您未能遵守本协议的条款，您的授权将被终止，所被许可的权利将被收回并承担相应法律责任；
//
//   三、有限担保和免责声明
//    1. 本软件及所附带的文件是作为不提供任何明确的或隐含的赔偿或担保的形式提供的；
//    2. 用户出于自愿而使用本软件，您必须了解使用本软件的风险，在尚未获得商业授权之前，我们不承
//       诺提供任何形式的技术支持、使用担保，也不承担任何因使用本软件而产生问题的相关责任；
//    3. 广州果创网络科技有限公司不对使用本产品构建的商城中的内容信息承担责任，但在不侵犯用户隐
//       私信息的前提下，保留以任何方式获取用户信息及商品信息的权利；
//
//   有关本产品最终用户授权协议、商业授权与技术服务的详细内容，均由广州果创网络科技有限公司独家
//   提供。广州果创网络科技有限公司拥有在不事先通知的情况下，修改授权协议的权力，修改后的协议对
//   改变之日起的新授权用户生效。电子文本形式的授权协议如同双方书面签署的协议一样，具有完全的和
//   等同的法律效力。您一旦开始修改、安装或使用本产品，即被视为完全理解并接受本协议的各项条款，
//   在享有上述条款授予的权力的同时，受到相关的约束和限制。协议许可范围以外的行为，将直接违反本
//   授权协议并构成侵权，我们有权随时终止授权，责令停止损害，并保留追究相关责任的权力。
//
//  ---------------------------------------------------------------------------------

namespace Base\Model;
use Base\Common\TimeUtil;
use Elasticsearch\Client;
use Elasticsearch\ClientBuilder;
use Throwable;

class EsBase extends Base {

    /**
     * @var $client null es实例
     */
    private static $client = null;

    /**
     * @var string|null
     */
    private static $error = null;


    /**
     * @var array mysql字段类型与es转换
     */
    public static $esTypeMap = [
        'varchar' => [
            'type' => 'text',
        ],
        'text' => [
            'type' => 'text',
        ],
        'char' => [
            'type' => 'text',
        ],

        'int' => [
            'type' => 'integer',
        ],
        'tinyint' => [
            'type' => 'byte',
        ],
        'smallint' => [
            'type' => 'short',
        ],
        'bigint' => [
            'type' => 'long',
        ],

        'float' => [
            'type' => 'float',
        ],
        'double' => [
            'type' => 'double',
        ],
        'decimal' => [
            'type' => 'double',
        ],
        'datetime' => [
            'type' => 'date',
            'format' => 'yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis'
        ],
        'timestamp' => [
            'type' => 'date',
            'format' => 'yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis'
        ],
        'time' => [
            'type' => 'date',
            'format' => 'HH:mm:ss||epoch_millis'
        ],
        'date' => [
            'type' => 'date',
            'format' => 'yyyy-MM-dd||epoch_millis'
        ],
    ];

    /**
     * 获取es client
     * @return Client|null
     */
    public static function getEsClient()
    {
        $esConfig = \PhalApi\DI()->config->get('sys.es.host');
        if (is_null(self::$client)) {
            self::$client = ClientBuilder::create()->setHosts(array($esConfig))->build();
        }
        return self::$client;
    }

    /**
     * 获取错误信息
     * @param bool $clearError
     * @return null|string
     */
    public static function getError($clearError = true)
    {
        $error = static::$error;
        if ($clearError) {
            static::$error = null;
        }
        return $error;
    }

    /**
     * 设置错误信息
     * @param string $msg
     */
    private static function setError($msg)
    {
        static::$error = $msg;
    }


    /**
     * 创建索引
     * @param string $index 索引名称
     * @param string $type  mappings的type，默认index也行
     * @param array $mappings
     * @param array|null $settings
     * @return bool
     */
    public static function createIndex($index, $type, $mappings, $settings = null)
    {
        if (self::exists($index)) {
            self::deleteIndex($index);
        }

        $defaultSettings = array(
            'analysis' => [
                'analyzer' => [
                    'ik' => [
                        'tokenizer' => 'ik_smart',
                    ],
                    'my_analyzer' => [
                        'tokenizer' => 'my_tokenizer',
                    ],
                ],
                'tokenizer' => [
                    'my_tokenizer' => [
                        'type' => 'pattern',
                        'pattern' => ',|，',
                    ],
                ],
            ],
        );

        $params = [
            'index' => $index,
            'body' => [
                'settings' => is_null($settings) ? $defaultSettings : $settings,
                'mappings' => [$type => $mappings],
            ]
        ];
        try {
            static::getEsClient()->indices()->create($params);
            return true;
        } catch (Throwable $e) {
            static::setError($e->getMessage());
            return false;
        }

    }


    /**
     * 判断es某个索引是否存在
     * @param string $index 索引名称
     * @return bool
     */
    public static function exists($index)
    {
        return static::getEsClient()->indices()->exists(array('index' => $index));
    }

    /**
     * 索引不存在就新建插入
     * @param string $index 索引名称
     * @param string $type  mapping中的type
     * @param array $esParams
     * @param array $mappings
     * @param null $settings
     * @return bool
     */
    public static function batchCreateInsert($index, $type, $esParams, $mappings, $settings = null)
    {
        if (!static::exists($index)) {
            $isSuccess = static::createIndex($index, $type, $mappings, $settings);
            if (!$isSuccess) {
                return false;
            }
        }
        $res = static::getEsClient()->bulk($esParams);
        if ($res['errors'] ?? null) {
            static::setError(json_encode(self::getErrorRows($res['items']), JSON_UNESCAPED_UNICODE));
            return false;
        }
        return true;
    }

    /**
     * 往es批量插入数据
     * @param array $docs
     * @return bool
     */
    public static function batchInsert($docs)
    {
        try {
            $res = static::getEsClient()->bulk($docs);
            if (isset($res['errors']) && $res['errors']) {
                static::setError(json_encode(self::getErrorRows($res['items']), JSON_UNESCAPED_UNICODE));
                return false;
            }
            return true;
        } catch (Throwable $e) {
            static::setError($e->getMessage());
            return false;
        }
    }

    /**
     * 批量删除文档
     * @param string $index
     * @param string $type
     * @param array $docIds
     * @return bool
     */
    public static function batchDelete($index, $type, $docIds)
    {
        if (!self::exists($index)) {
            return true;
        }

        try {
            $params = array(
                'index' => $index,
                'type' => $type,
                'body' => array(
                    'query' => array(
                        'constant_score' => array(
                            'filter' => array(
                                'terms' => array(
                                    'id' => $docIds
                                ),
                            ),
                        ),
                    ),
                ),
            );
            $res = static::getEsClient()->deleteByQuery($params);

            if ($res['failures'] ?? null) {
                static::setError(json_encode($res['failures'], JSON_UNESCAPED_UNICODE));
                return false;
            }
            return true;
        } catch (Throwable $e) {
            static::setError($e->getMessage());
            return false;
        }
    }

    /**
     * 查找文档
     * @param string $index
     * @param string|null $type
     * @param string $id
     * @return array|null
     */
    public static function findById($index, $type, $id)
    {
        if (is_null($type)) {
            $type = '_all';
        }
        $params = array('id' => $id, 'index' => $index, 'type' => $type);
        try {
            return self::getEsClient()->get($params);
        } catch (Throwable $e) {
            static::setError($e->getMessage());
            return null;
        }
    }

    /**
     * 删除文档
     * @param string $index
     * @param string $type
     * @param string $id
     * @return bool
     */
    public static function deleteById($index, $type, $id) : bool
    {
        $params = array('id' => $id, 'index' => $index, 'type' => $type);
        try {
            self::getEsClient()->delete($params);
            return true;

        } catch (Throwable $e) {
            static::setError($e->getMessage());
            return false;
        }
    }

    /**
     * 删除es索引
     * @param string $index
     * @return array|bool
     */
    public static function deleteIndex($index): bool
    {
        if (!static::exists($index)) {
            return true;
        }
        try {
            self::getEsClient()->indices()->delete(array('index' => $index));
            return true;
        } catch (Throwable $e) {
            static::setError($e->getMessage());
            return false;
        }
    }

    /**
     * 获取嵌套查询语句
     * @param string $nestedField
     * @param array $subCondition
     * as
     * array(
     *    'match_phrase' => array(
     *       'XXX' => array(
     *           'query' => 'searchXXX',
     *           'slop'  => 10,
     *       )
     *    )
     * )
     * or other search condition
     * @return array
     */
    private static function getNestedCondition(string $nestedField, array $subCondition)
    {
        $position = strrpos($nestedField, '.');
        $nestedPath = substr($nestedField, 0, $position);

        return array(
            'nested' => array(
                'path' => $nestedPath,
                'query' => array(
                    'bool' => array(
                        'must' => [
                            $subCondition
                        ]
                    ),
                ),
            ),
        );
    }

    /**
     * @param $index
     * @param $type
     * @param string $search
     * @param array $fields
     * @param int $limit
     * @param bool $returnFields
     * @return array|null
     */
    public static function searchByFields($index, $type, string $search, array $fields, int $limit = 10000, $returnFields = false)
    {
        $matchConditions = [];
        foreach ($fields as $field) {
            $condition = array(
                'match_phrase' => array(
                    $field => array(
                        'query' => $search,
                        'slop' => 10,
                    ),
                ),
            );
            if (false !== strpos($field, '.')) {
                $matchConditions[] = self::getNestedCondition($field, $condition);
            } else {
                $matchConditions[] = $condition;
            }
        }

        $params = array(
            'index' => $index,
            'type' => $type,
            'body' => array(
                'query' => array(
                    'bool' => array(
                        'should' => $matchConditions,
                    ),
                ),
            ),
            'size' => $limit,
        );

        if (is_array($returnFields) && !empty($returnFields)) {
            $params['_source'] = $returnFields;
        } else {
            $params['_source'] = false;
        }

        try {
            $result = static::getEsClient()->search($params);
            $total = $result['hits']['total'] ?? 0;
            if ($total == 0) {
                return [];
            }
            return $result['hits']['hits'];

        } catch (Throwable $e) {
            static::setError($e->getMessage());
            return null;
        }
    }

    /**
     * 获取错误的行
     * @param array $items
     * @return array
     */
    private static function getErrorRows(array $items): array
    {
        $errorRows = [];
        foreach ($items as $item) {
            if (!empty($item['index']['error'])) {
                $errorRows[] = array(
                    'doc_id' => $item['index']['_id'],
                    'error' => $item['index']['error'],
                );
            }
        }

        return $errorRows;
    }


    /**
     * 解析ES条件
     * @param array $query
     * @return array
     */
    public static function parseEsCondition($query)
    {
        $must = [];
        foreach ($query as $field => $value) {
            if ($value === '' || $value === null) {
                continue;
            }
            if (is_array($value)) {
                $isObj = is_object(json_decode(json_encode($value)));
                if (!$isObj) {
                    // 普通数组，使用 in
                    if (count($value) > 0) {
                        $must[] = [
                            'terms' => [
                                $field => $value
                            ]
                        ];
                    }
                } else {
                    //日期时间范围查询
                    $start = $value['start'] ?? '';
                    $end = $value['end'] ?? '';
                    if ($start || $end) {
                        $s = TimeUtil::formatStartTime($start, '00:00:00');
                        $e = TimeUtil::formatStartTime($end, '23:59:59');
                        $range = [
                            $field => [],
                        ];

                        if ($start) {
                            $range[$field]['gte'] = $s;
                        }

                        if ($end) {
                            $range[$field]['lte'] = $e;
                        }
                        $must[] = array('range' => $range);
                    }
                }
            } else {
                $must[] = array('match' => array(
                    $field => array(
                        'query' => $value
                    )
                ));
            }
        }
        return array('bool' => array('must' => $must));
    }


    /**
     * 将表字段转换成ES Mapping
     * @param array $columns 字段的名称和类型
     * @param string $analyzer 分词器
     * @param bool $applySearchAnalyzer
     * @return array
     */
    public static function esMappingProperties($columns, $analyzer = 'ik_smart', $applySearchAnalyzer = true)
    {
        $mapping = [];
        foreach ($columns as $column) {
            $columnName = $column['COLUMN_NAME'];
            $columnType = $column['DATA_TYPE'];
            $esTypeMap = EsBase::$esTypeMap;
            $mapping[$columnName] = $esTypeMap[$columnType] ?? ['type' => null];

            if ($analyzer && $mapping[$columnName]['type'] == 'text') {
                $mapping[$columnName]['analyzer'] = $analyzer;
                if ($applySearchAnalyzer) {
                    $mapping[$columnName]['search_analyzer'] = $analyzer;
                }
            }
        }
        return $mapping;
    }
}
