<?php

namespace Hura8\System\Model;

use Hura8\Database\iConnectDB;
use Hura8\Database\MysqlValue;
use Hura8\Interfaces\iSearch;


abstract class aEntityBaseModel
{


    /* @var iConnectDB $db */
    protected $db;

    protected $entity_type = '';
    protected $tb_entity = "";
    protected $allow_richtext_fields = []; // only table's column fields in this list allowed to retain html tags, else strip all


    protected $base_filter_condition_keys = [
        'q'         => '', // keyword search
        'q_options'  => [
            'field_filters' => [],
            'fulltext_fields' => ['keywords'],
            'limit_result' => 2000
        ], // q_options as in iSearch->find($keyword, array $field_filters = [], array $fulltext_fields = ["keywords"], $limit_result = 2000)
        'featured'  => '', // 1|-1
        'status'    => '', // 1|-1
        'excluded_ids' => null, // [id1, id2, ...] or null
        'included_ids' => null, // [id1, id2, ...] or null
    ];

    /* @var ?iSearch $iSearchModel */
    protected $iSearchModel;

    public function __construct(
        $entity_type,
        $tb_entity = "",
        $iSearchModel = null,
        $allow_richtext_fields = []
    ) {

        $this->entity_type              = $entity_type;

        $this->db                       = get_db('', ENABLE_DB_DEBUG);
        $this->iSearchModel             = $iSearchModel;
        $this->allow_richtext_fields    = $allow_richtext_fields;

        if($tb_entity) {
            $this->tb_entity = $tb_entity;
        }else{
            //auto infer
            $tb_name = str_replace("-", "_", preg_replace("/[^a-z0-9_\-]/i", "", $entity_type));
            $this->tb_entity = "tb_".strtolower($tb_name);
        }
    }


    /**
     * @description to be extended by extending class
     * besides the $base_filter_condition_keys which allows to find items in ::getList($condition)
     * example:
      [
            'category' => array[],
            'brand' => array[],
      ]
     * @return array
     */
    abstract protected function extendedFilterOptions() : array ;


    /**
     * @description utility to get all possible entity filter during module development
     * @return array
     */
    public function getAllowedFilterConditionKeys(): array
    {
        return array_merge(
            $this->extendedFilterOptions(),
            $this->base_filter_condition_keys
        );
    }


    public function setRichtextFields(array $allow_richtext_fields = []) {
        $this->allow_richtext_fields = $allow_richtext_fields;
    }




    /**
     * @description
     * @param array $filter_condition
     * @return array|null array($string_where, $bind_types, $bind_values );
     */
    protected function _buildQueryConditionExtend(array $filter_condition): ?array {
        return [];
    }


    /**
     * @param string $sort_by
     * @return string
     */
    protected function _buildQueryOrderBy(string $sort_by = "new") {
        return " `id` DESC ";
    }


    /**
     * @param array $item_info
     * @return array
     */
    protected function formatItemInList(array $item_info) : array {
        return $item_info;
    }


    /**
     * @param array $item_info
     * @return array|null
     */
    protected function formatItemInfo(array $item_info) : ?array {
        return $item_info;
    }



    public function getEntityType() : string {
        return $this->entity_type;
    }


    /**
     * @description prevent xss attack by stripping all html tags from un-permitted fields (which mostly are)
     * - only fields in ::allow_richtext_fields can keep html tags. Example: description
     * - if value is array (example data=['title' => '', 'description' => html tags ]) ...
        then to keep `description`, both `data` and `description` must be in the ::allow_richtext_fields
        because this method checks on array value recursively
     * @param array $input_info
     * @return array
     */
    protected function cleanRichtextFields(array $input_info) : array {
        $cleaned_info = [];
        foreach ($input_info as $key => $value) {
            if($value instanceof MysqlValue) {
                $cleaned_info[$key] = $value;
            } else {
                if(in_array($key, $this->allow_richtext_fields)) {
                    $cleaned_info[$key] = (is_array($value)) ? $this->cleanRichtextFields($value) : $value;
                }else{
                    $cleaned_info[$key] = (is_array($value)) ? $this->cleanRichtextFields($value) : strip_tags($value);
                }
            }
        }

        return $cleaned_info;
    }



    public function getListByIds(array $list_id, array $condition = array()) : array
    {
        if(!sizeof($list_id)) {
            return [];
        }

        list($parameterized_ids, $bind_types) = create_bind_sql_parameter_from_value_list($list_id, 'int');

        $where_clause = "";
        $bind_values = $list_id;
        if(isset($condition['status'])) {
            $where_clause .= " AND `status` = ? ";
            $bind_types[] = 'd';
            $bind_values[] = intval($condition['status']);
        }

        $query = $this->db->runQuery(
            " SELECT * FROM ".$this->tb_entity."  WHERE `id` IN (".$parameterized_ids.") ".$where_clause,
            $bind_types,
            $bind_values
        );

        $item_list = [];
        foreach ($this->db->fetchAll($query) as $item) {
            $item_list[$item['id']] = $this->formatItemInList($item);
        }

        return $item_list;
    }


    public function getInfo($id) : ?array
    {
        $query = $this->db->runQuery("SELECT * FROM `".$this->tb_entity."` WHERE `id` = ? LIMIT 1 ", ['d'], [$id]) ;
        if( $item_info = $this->db->fetchAssoc($query)){
            return $this->formatItemInfo($item_info);
        }

        return null;
    }


    /**
     * @description utility to inspect the actual filters which will be used in getList by Model
     * @param array $condition
     * @return array
     */
    public function getQueryCondition(array $condition) : array
    {
        return $this->_buildQueryCondition($condition);
    }


    public function getTotal(array $condition) : int
    {
        list( $where_clause, $bind_types, $bind_values) = $this->_buildQueryCondition($condition);

        $query = $this->db->runQuery(
            " SELECT COUNT(*) as total FROM `".$this->tb_entity."` WHERE 1 " . $where_clause,
            $bind_types, $bind_values
        );

        $total = 0;
        if ($rs = $this->db->fetchAssoc($query)) {
            $total = $rs['total'];
        }

        return $total;
    }


    public function getList(array $condition) : array
    {
        list( $where_clause, $bind_types, $bind_values) =  $this->_buildQueryCondition($condition);

        //debug_var([$where_clause, $bind_types, $bind_values]);

        $numPerPage = (isset($condition['numPerPage']) && $condition['numPerPage'] > 0 ) ? intval($condition['numPerPage']) : 20 ;
        $page       = (isset($condition['page']) && $condition['page'] > 0 ) ? intval($condition['page']) : 1 ;

        $sort_by    = (isset($condition['sort_by']) && $condition['sort_by']) ? $condition['sort_by'] : 'new' ;
        $order_by   = $this->_buildQueryOrderBy($sort_by);

        $query = $this->db->runQuery(
            "SELECT * FROM ".$this->tb_entity." WHERE 1 ". $where_clause . "  
                    ORDER BY ".$order_by."
                    LIMIT  ".(($page-1) * $numPerPage).", ".$numPerPage ,
            $bind_types,
            $bind_values
        ) ;

        $item_list = array();
        $counter = ($page-1) * $numPerPage;

        foreach ( $this->db->fetchAll($query) as $item_info ) {
            $counter += 1;

            $item = $item_info;
            $item['counter'] = $counter;

            $item_list[] = $this->formatItemInList($item);
        }

        return $item_list;
    }


    //to avoid duplicate codes, extended class should implement this method for extra filter keys
    // which are not in the base filter ::_buildQueryConditionBase
    protected function _buildQueryCondition(array $filter_condition)
    {
        // these keys are for pagination and ordering list, which are commonly included in the $filter_condition
        $excluded_keys = [
            'numPerPage', 'page', 'sort_by'
        ];


        $base_filter_conditions = [];
        $extend_filter_conditions = [];
        foreach ($filter_condition as $key => $value) {
            if(in_array($key, $excluded_keys)) {
               continue;
            }

            if(array_key_exists($key, $this->base_filter_condition_keys) ) {
                $base_filter_conditions[$key] = $value;
            }else{
                $extend_filter_conditions[$key] = $value;
            }
        }

        list($base_where, $base_bind_types, $base_bind_values) = $this->_buildQueryConditionBase($base_filter_conditions);

        list(
                $extend_where,
                $extend_bind_types,
                $extend_bind_values
            ) = $this->_buildQueryConditionExtend($extend_filter_conditions) ?: [ '', null, null];


        if($extend_where) {
            return array(
                join(" ", [$base_where, $extend_where]),
                array_merge($base_bind_types, $extend_bind_types),
                array_merge($base_bind_values, $extend_bind_values),
            );
        }

        return array(
            $base_where,
            $base_bind_types,
            $base_bind_values
        );

    }


    protected function _buildQueryConditionBase(array $filter_conditions)
    {
        /*
        $filter_conditions = [
            'q'         => '', // keyword search
            'q_options'  => [
                'field_filters' => [],
                'fulltext_fields' => ['keywords'],
                'limit_result' => 2000
            ], // q_options as in iSearch->find($keyword, array $field_filters = [], array $fulltext_fields = ["keywords"], $limit_result = 2000)

            'featured'  => '', // 1|-1
            'status'    => '', // 1|-1
            'excluded_ids' => [], // [id1, id2, ...] or null
            'included_ids' => [], // [id1, id2, ...] or null
        ];
        */

        $catCondition = [];
        $bind_types = [];
        $bind_values = [];

        //Tim kiem theo tu khoa
        if(isset($filter_conditions["q"]) && $filter_conditions["q"] && $this->iSearchModel ){

            $q_options          = $filter_conditions["q_options"] ?? [];
            $field_filters      = $q_options['field_filters'] ?? [];
            $fulltext_fields    = $q_options['fulltext_fields'] ?? [];
            $limit_result       = $q_options['limit_result'] ?? 2000;

            $match_result = $this->iSearchModel->find($filter_conditions["q"], $field_filters, $fulltext_fields, $limit_result);

            list($parameterized_ids, $sub_bind_types) = create_bind_sql_parameter_from_value_list($match_result, 'int');

            if($parameterized_ids) {
                $catCondition[] = " AND `id` IN (".$parameterized_ids.") ";
                $bind_types     = array_merge($bind_types, $sub_bind_types);
                $bind_values    = array_merge($bind_values, $match_result);
            }else{
                $catCondition[] = " AND `id` = -1  ";
            }
        }

        //loc theo featured
        if(isset($filter_conditions["featured"]) && $filter_conditions["featured"]) {
            $catCondition[] = " AND `is_featured` = ? ";
            $bind_types[]     = 'd';
            $bind_values[]    = ($filter_conditions["featured"] == 1) ? 1 : 0;
        }

        //loc theo status
        if(isset($filter_conditions["status"]) && $filter_conditions["status"]) {
            $catCondition[] = " AND `status` = ? ";
            $bind_types[]     = 'd';
            $bind_values[]    = ($filter_conditions["status"] == 1) ? 1 : 0;
        }

        // excluded_ids
        if(isset($filter_conditions['excluded_ids']) && is_array($filter_conditions['excluded_ids']) ) {
            list($parameterized_ids, $sub_bind_types) = create_bind_sql_parameter_from_value_list($filter_conditions['excluded_ids'], 'int');

            if($parameterized_ids) {
                $catCondition[] = " AND `id` NOT IN (".$parameterized_ids.") ";
                $bind_types     = array_merge($bind_types, $sub_bind_types);
                $bind_values    = array_merge($bind_values, $filter_conditions['excluded_ids']);
            }
        }

        // included_ids
        if(isset($filter_conditions['included_ids']) && is_array($filter_conditions['included_ids']) ) {
            list($parameterized_ids, $sub_bind_types) = create_bind_sql_parameter_from_value_list($filter_conditions['included_ids'], 'int');

            if($parameterized_ids) {
                $catCondition[] = " AND `id` IN (".$parameterized_ids.") ";
                $bind_types     = array_merge($bind_types, $sub_bind_types);
                $bind_values    = array_merge($bind_values, $filter_conditions['included_ids']);
            }else{
                $catCondition[] = " AND `id` = -1 ";
            }
        }

        return array(
            join(" ", $catCondition),
            $bind_types,
            $bind_values
        );
    }


    // get empty/default item for form
    public function getEmptyInfo($addition_field_value = []) : array
    {
        $table_info = $this->db->getTableInfo($this->tb_entity);
        $empty_info = [];
        foreach ($table_info as $field => $field_info) {
            $empty_info[$field] = ( in_array($field_info['DATA_TYPE'], ['int', 'float', 'mediumint', 'smallint', 'tinyint']) ) ? 0 : '' ;
        }

        if(sizeof($addition_field_value)) {
            return array_merge($empty_info, $addition_field_value);
        }

        return $empty_info;
    }


}
