<?php
/**
 * MainWP Module Cost Tracker DB class.
 *
 * @package MainWP\Dashboard
 * @version 4.6
 */

namespace MainWP\Dashboard\Module\CostTracker;

use MainWP\Dashboard\MainWP_Install;
use MainWP\Dashboard\MainWP_Utility;
use MainWP\Dashboard\MainWP_DB;
use MainWP\Dashboard\MainWP_DB_Client;
use MainWP\Dashboard\MainWP_Exception;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Class Cost_Tracker_DB
 */
class Cost_Tracker_DB extends MainWP_Install {
    //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
    /**
     * Variable to hold the db version.
     *
     * @var string Version.
     */
    private $cost_tracker_db_version = '1.0.18';


    /**
     * Static variable to hold the single instance of the class.
     *
     * @static
     *
     * @var mixed Default null
     */
    private static $instance = null;

    /**
     * Get Instance
     *
     * Creates public static instance.
     *
     * @static
     *
     * @return Cost_Tracker_DB
     */
    public static function get_instance() {
        if ( null === static::$instance ) {
            static::$instance = new self();
        }
        return static::$instance;
    }

    /**
     * Init functions.
     */
    public function init() {
        $this->install();
    }

    /**
     * Install db.
     */
    public function install() {
        global $wpdb;

        $currentVersion = get_site_option( 'mainwp_module_cost_tracker_db_version' );

        $rslt = $this->query( "SHOW TABLES LIKE '" . $this->table_name( 'cost_tracker' ) . "'" );
        if ( empty( static::num_rows( $rslt ) ) ) {
            $currentVersion = false;
        }

        if ( $currentVersion === $this->cost_tracker_db_version ) {
            return;
        }

        $charset_collate = $wpdb->get_charset_collate();
        $sql             = array();

        $tbl = 'CREATE TABLE `' . $this->table_name( 'cost_tracker' ) . '` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` text NOT NULL,
`url` text NOT NULL,
`type` varchar(20) NOT NULL,
`product_type` varchar(20) NOT NULL,
`slug` varchar(191) NOT NULL DEFAULT "",
`license_type` varchar(20) NOT NULL,
`cost_status` varchar(20) NOT NULL,
`payment_method` varchar(50) NOT NULL,
`price` decimal(26,8) NOT NULL,
`renewal_type` varchar(20) NOT NULL,
`last_renewal` int(11) NOT NULL,
`next_renewal` int(11) NOT NULL,
`next_renewal_today` int(11) NOT NULL,
`last_alert` int(11) NOT NULL,
`cost_icon` varchar(64) NOT NULL DEFAULT "",
`cost_color` varchar(64) NOT NULL DEFAULT "",
`sites` text NOT NULL,
`groups` text NOT NULL,
`clients` text NOT NULL,
`note` text NOT NULL';

        if ( empty( $currentVersion ) ) {
            $tbl .= ',
PRIMARY KEY  (`id`)  ';
        }
        $tbl  .= ') ' . $charset_collate;
        $sql[] = $tbl;

        require_once ABSPATH . 'wp-admin/includes/upgrade.php'; // NOSONAR - WP compatible.
        foreach ( $sql as $query ) {
            dbDelta( $query );
        }
        $this->update_db_cost( $currentVersion );
        update_option( 'mainwp_module_cost_tracker_db_version', $this->cost_tracker_db_version );
    }


    /**
     * Method update_db_cost().
     *
     * @param array $current_version DB version number.
     *
     * @return void
     */
    public function update_db_cost( $current_version ) { //phpcs:ignore -- NOSONAR - complex.
        if ( ! empty( $current_version ) ) {
            if ( version_compare( $current_version, '1.0.8', '<' ) ) {
                $table = esc_sql( $this->table_name( 'cost_tracker' ) );
                $this->wpdb->query( "ALTER TABLE {$table} MODIFY COLUMN price decimal(26,8)" ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            }
            if ( version_compare( $current_version, '1.0.9', '<' ) ) {
                $table = esc_sql( $this->table_name( 'cost_tracker' ) );
                $this->wpdb->query( "ALTER TABLE {$table} DROP COLUMN author" ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            }

            if ( version_compare( $current_version, '1.0.13', '<' ) ) {
                $costs = $this->get_cost_tracker_by( 'all' );

                foreach ( $costs as $cost ) {
                    $obj_name = '';
                    if ( ! empty( $cost->sites ) ) {
                        $items    = json_decode( $cost->sites, true );
                        $obj_name = 'site';
                    } elseif ( ! empty( $cost->groups ) ) {
                        $items    = json_decode( $cost->groups, true );
                        $obj_name = 'tag';
                    } elseif ( ! empty( $cost->clients ) ) {
                        $items    = json_decode( $cost->clients, true );
                        $obj_name = 'client';
                    } else {
                        continue;
                    }

                    if ( ! empty( $items ) ) {
                        foreach ( $items as $it_id ) {
                            $this->wpdb->insert(
                                $this->table_name( 'lookup_item_objects' ),
                                array(
                                    'item_id'     => $cost->id,
                                    'item_name'   => 'cost',
                                    'object_id'   => $it_id,
                                    'object_name' => $obj_name,
                                )
                            );

                        }
                    }
                }
            }
        }
    }

    /**
     * Method update_cost_tracker().
     *
     * @param array $update Cost tracker array data.
     *
     * @throws \MainWP_Exception Existed cost tracker error.
     * @return mixed Result
     */
    public function update_cost_tracker( $update ) { //phpcs:ignore -- NOSONAR - complex.
        /**
         * WP database.
         *
         * @global object
         */
        global $wpdb;

        if ( ! is_array( $update ) ) {
            return false;
        }

        $id = isset( $update['id'] ) ? $update['id'] : 0;

        if ( ! empty( $update['product_type'] ) && ! empty( $update['slug'] ) && in_array( $update['product_type'], array( 'plugin', 'theme' ), true ) ) {
            // check existed cost tracker for this plugin / theme .
            $current = $this->get_cost_tracker_by( 'slug', $update['slug'], $update['product_type'] );
            if ( is_array( $current ) && ! empty( $current ) ) {
                $existed = false;
                if ( 1 === count( $current ) ) {
                    $current = current( $current );
                    if ( is_object( $current ) ) {
                        if ( ! empty( $update['id'] ) ) {
                            if ( ! empty( $current->id ) && (int) $current->id !== (int) $update['id'] ) {
                                $existed = true; // to fix.
                            } elseif ( ! empty( $current->id ) ) {
                                $id = $current->id; // to update.
                            }
                        } else {
                            $existed = true; // to fix: existed one.
                        }
                    }
                } else {
                    $existed = true; // to fix found multi items.
                }

                if ( $existed ) {
                    $error = esc_html__( 'A cost tracker for this plugin already exists.', 'mainwp' );
                    if ( 'theme' === $update['product_type'] ) {
                        $error = esc_html__( 'A cost tracker for this theme already exists.', 'mainwp' );
                    }
                    throw new MainWP_Exception( esc_html( $error ) );
                }
            }
        }

        if ( isset( $update['id'] ) ) {
            unset( $update['id'] );
        }

        if ( ! empty( $id ) ) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching -- write operation; caching not applicable for UPDATE.
            $wpdb->update( $this->table_name( 'cost_tracker' ), $update, array( 'id' => intval( $id ) ) );
            $this->invalidate_cost_tracker_caches( $id );
            return $this->get_cost_tracker_by( 'id', $id );
        } else {
            if ( isset( $update['id'] ) ) {
                unset( $update['id'] );
            }

            if ( $wpdb->insert( $this->table_name( 'cost_tracker' ), $update ) ) {
                $this->invalidate_cost_tracker_caches();
                return $this->get_cost_tracker_by( 'id', $wpdb->insert_id );
            }
        }
        return false;
    }

    /**
     * Method update_selected_lookup_cost().
     *
     * @param int   $item_id Cost id.
     * @param array $selected_sites selected sites.
     * @param array $selected_groups selected tags.
     * @param array $selected_clients selected clients.
     *
     * @return mixed Result
     */
    public function update_selected_lookup_cost( $item_id, $selected_sites = false, $selected_groups = false, $selected_clients = false ) {

        if ( empty( $item_id ) ) {
            return false;
        }

        $obj_name = '';

        $new_obj_ids = array();

        if ( ! empty( $selected_sites ) ) {
            $obj_name    = 'site';
            $new_obj_ids = $selected_sites;
        } elseif ( ! empty( $selected_groups ) ) {
            $obj_name    = 'tag';
            $new_obj_ids = $selected_groups;
        } elseif ( ! empty( $selected_clients ) ) {
            $obj_name    = 'client';
            $new_obj_ids = $selected_clients;
        }

        if ( ! empty( $obj_name ) ) {
            static::get_instance()->update_lookup_cost( $item_id, $obj_name, $new_obj_ids );
        } else {
            // to support saving cost without selected sites, tags, clients.
            MainWP_DB::instance()->delete_lookup_items(
                'object_name',
                array(
                    'item_name'    => 'cost',
                    'item_id'      => $item_id,
                    'object_names' => array( 'site', 'tag', 'client' ),
                )
            );

        }
        return true;
    }

    /**
     * Method update_lookup_cost().
     *
     * @param int    $item_id item id to insert lookup value.
     * @param string $obj_name loockup object name.
     * @param array  $new_obj_ids New|Update object ids.
     *
     * @return mixed Result
     */
    public function update_lookup_cost( $item_id, $obj_name, $new_obj_ids ) { //phpcs:ignore -- NOSONAR - complex.

        $allows = array( 'site', 'tag', 'client' );

        if ( empty( $item_id ) || ! is_array( $new_obj_ids ) || ! in_array( $obj_name, $allows ) ) {
            return false;
        }

        $remove_obj_names = array_diff( $allows, array( $obj_name ) );

        $found_look_ids  = array();
        $existed_look_id = array();

        $results = MainWP_DB::instance()->get_lookup_items( 'cost', $item_id, $obj_name );
        if ( $results ) {
            foreach ( $results as $item ) {
                $found_look_ids[] = $item->lookup_id;
                if ( ! empty( $new_obj_ids ) && in_array( $item->object_id, $new_obj_ids ) ) {
                    $existed_look_id[ $item->object_id ] = $item->lookup_id;
                }
            }
        }

        $new_look_ids = array();

        foreach ( $new_obj_ids as $obj_id ) {
            if ( isset( $existed_look_id[ $obj_id ] ) ) {
                $new_look_ids[] = $existed_look_id[ $obj_id ];
                continue;
            }
            $insert_id = MainWP_DB::instance()->insert_lookup_item( 'cost', $item_id, $obj_name, $obj_id );
            if ( $insert_id ) {
                $new_look_ids[] = $this->wpdb->insert_id;
            }
        }
        $remove_ids = array_diff( $found_look_ids, $new_look_ids );
        if ( $remove_ids ) {
            MainWP_DB::instance()->delete_lookup_items( 'lookup_id', array( 'lookup_ids' => $remove_ids ) );
        }

        if ( ! empty( $remove_obj_names ) ) {
            MainWP_DB::instance()->delete_lookup_items(
                'object_name',
                array(
                    'item_name'    => 'cost',
                    'item_id'      => $item_id,
                    'object_names' => $remove_obj_names,

                )
            );
        }
    }

    /**
     * Method get_cost_tracker_by().
     *
     * @param string $by Get by.
     * @param mixed  $value Value.
     * @param array  $params Others params.
     *
     * @return mixed Result
     */
    public function get_cost_tracker_by( $by = 'id', $value = null, $params = array() ) { //phpcs:ignore -- NOSONAR - complex method.
        global $wpdb;

        if ( ! is_array( $params ) ) {
            $params = array();
        }

        $s       = '';
        $exclude = array();
        $include = array();
        $status  = array();
        $types   = array();

        $where = '';
        $limit = '';

        if ( $params && is_array( $params ) ) {
            $s       = isset( $params['s'] ) ? $params['s'] : '';
            $exclude = isset( $params['exclude'] ) && ! empty( $params['exclude'] ) ? wp_parse_id_list( $params['exclude'] ) : array();
            $include = isset( $params['include'] ) && ! empty( $params['include'] ) ? wp_parse_id_list( $params['include'] ) : array();

            $status       = isset( $params['status'] ) && ! empty( $params['status'] ) ? wp_parse_list( $params['status'] ) : array();
            $product_type = isset( $params['category'] ) && ! empty( $params['category'] ) ? wp_parse_list( $params['category'] ) : array();
            $types        = isset( $params['type'] ) && ! empty( $params['type'] ) ? wp_parse_list( $params['type'] ) : array();

            $page     = isset( $params['paged'] ) ? intval( $params['paged'] ) : false;
            $per_page = isset( $params['items_per_page'] ) ? intval( $params['items_per_page'] ) : false;

            if ( ! empty( $s ) ) {
                $where .= ' AND ( ct.id LIKE "%' . $this->escape( $s ) . '%" OR ct.name LIKE "%' . $this->escape( $s ) . '%" ) ';
            }

            if ( ! empty( $exclude ) ) {
                $where .= ' AND  ct.id NOT IN (' . implode( ',', $exclude ) . ') ';
            }

            if ( ! empty( $include ) ) {
                $where .= ' AND  ct.id IN (' . implode( ',', $include ) . ') ';
            }

            if ( ! empty( $status ) ) {
                $where .= ' AND  ct.cost_status IN (' . $this->prepare_fields_array( $status ) . ') ';
            }

            if ( ! empty( $product_type ) && ! in_array( 'any', $product_type ) ) {
                $where .= ' AND  ct.product_type IN (' . $this->prepare_fields_array( $product_type ) . ') ';
            }

            if ( ! empty( $types ) && ! in_array( 'any', $types ) ) {
                $where .= ' AND  ct.type IN (' . $this->prepare_fields_array( $types ) . ') ';
            }

            if ( ! empty( $page ) && ! empty( $per_page ) ) {
                $limit = ' LIMIT ' . ( $page - 1 ) * $per_page . ',' . $per_page;
            }
        }

        $product_type = isset( $params['product_type'] ) ? $params['product_type'] : '';
        $selected_ids = isset( $params['selected_ids'] ) ? $params['selected_ids'] : array();

        if ( is_array( $selected_ids ) ) {
            $selected_ids = MainWP_Utility::array_numeric_filter( $selected_ids );
        } else {
            $selected_ids = array();
        }

        if ( 'all' === $by ) {
            if ( ! empty( $selected_ids ) ) {
                $where .= ' AND ct.id IN (' . implode( ',', $selected_ids ) . ') ';
            }
            $table = esc_sql( $this->table_name( 'cost_tracker' ) );
            // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $table is escaped with esc_sql(); $where and $limit are from internal arrays only
            return $wpdb->get_results( "SELECT * FROM {$table} ct WHERE 1 {$where}{$limit}" );
        }

        $where = '';
        $limit = '';

        $sql = '';
        if ( 'id' === $by && is_numeric( $value ) ) {
            $cache_key = $this->get_cache_key_for_cost_tracker( $by, $value, $params );
            $cached    = wp_cache_get( $cache_key, 'mainwp_cost_tracker' );
            if ( false !== $cached ) {
                return $cached;
            }

            $table = esc_sql( $this->table_name( 'cost_tracker' ) );
            $sql = $wpdb->prepare( "SELECT * FROM {$table} WHERE `id`=%d ", $value );
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql is safely constructed with $wpdb->prepare() above; table name is escaped with esc_sql().
            $result = $wpdb->get_row( $sql, OBJECT );
            wp_cache_set( $cache_key, $result, 'mainwp_cost_tracker' );
            return $result;
        } elseif ( 'count' === $by ) {
            $cache_key = 'mainwp_cost_tracker_' . md5( wp_json_encode( array( 'method' => 'get_cost_tracker_by', 'by' => 'count' ) ) ); // NOSONAR - MD5 used for cache key generation only, not cryptographic (security) purposes.
            $cached    = wp_cache_get( $cache_key, 'mainwp_cost_tracker' );
            if ( false !== $cached ) {
                return $cached;
            }

            $table = esc_sql( $this->table_name( 'cost_tracker' ) );
            $sql = "SELECT count(*) FROM {$table}";
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql is safely constructed with static table name that is escaped with esc_sql(); no dynamic input.
            $result = $wpdb->get_var( $sql );
            wp_cache_set( $cache_key, $result, 'mainwp_cost_tracker' );
            return $result;
        } elseif ( 'id' === $by && false !== strpos( $value, ',' ) ) {
            $cost_ids = explode( ',', $value );
            $cost_ids = MainWP_Utility::array_numeric_filter( $cost_ids );
            if ( ! empty( $cost_ids ) ) {
                $table = esc_sql( $this->table_name( 'cost_tracker' ) );
                $sql = "SELECT * FROM {$table} WHERE `id` IN (" . implode( ',', $cost_ids ) . ' )';
            }
        } elseif ( 'site_id' === $by || 'client_id' === $by ) {
            $table = esc_sql( $this->table_name( 'cost_tracker' ) );
            $sql = "SELECT * FROM {$table}";
        } elseif ( 'slug' === $by && is_string( $value ) ) {
            if ( in_array( $product_type, array( 'plugin', 'theme' ), true ) ) {
                $cache_key = $this->get_cache_key_for_cost_tracker( $by, $value, array( 'product_type' => $product_type ) );
                $cached    = wp_cache_get( $cache_key, 'mainwp_cost_tracker' );
                if ( false !== $cached ) {
                    return $cached;
                }

                $table = esc_sql( $this->table_name( 'cost_tracker' ) );
                $sql = $wpdb->prepare( "SELECT * FROM {$table} WHERE `slug`=%s AND product_type = %s ", $value, $product_type );
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql is safely constructed with $wpdb->prepare() above; table name is escaped with esc_sql().
                $result = $wpdb->get_row( $sql, OBJECT );
                wp_cache_set( $cache_key, $result, 'mainwp_cost_tracker' );
                return $result;
            } else {
                $where = '';
                if ( ! empty( $product_type ) ) {
                    $where = $wpdb->prepare( ' AND product_type = %s ', $product_type );
                }
                $table = esc_sql( $this->table_name( 'cost_tracker' ) );
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $where is itself from $wpdb->prepare(); concatenation into prepare is safe here.
                $sql = $wpdb->prepare( "SELECT * FROM {$table} WHERE `slug`=%s " . $where, $value );
            }
        }

        $data = array();
        if ( ! empty( $sql ) ) {
            $cache_key = $this->get_cache_key_for_cost_tracker( $by, $value, $params );
            $cached    = wp_cache_get( $cache_key, 'mainwp_cost_tracker' );
            if ( false !== $cached ) {
                return $cached;
            }

            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql is safely constructed with $wpdb->prepare() above; table name is escaped with esc_sql().
            $result = $wpdb->get_results( $sql, OBJECT );
            wp_cache_set( $cache_key, $result, 'mainwp_cost_tracker' );
            if ( $result ) {
                if ( 'site_id' === $by ) {
                    $site_id = intval( $value );
                    foreach ( $result as $cost ) {
                        if ( empty( $cost->id ) ) {
                            continue;
                        }
                        $sites = ! empty( $cost->sites ) ? json_decode( $cost->sites, true ) : array();
                        if ( is_array( $sites ) && in_array( $site_id, $sites ) ) {
                            $data[] = $cost;
                        }
                    }
                } elseif ( 'client_id' === $by ) {
                    $client_id = intval( $value );
                    foreach ( $result as $cost ) {
                        if ( empty( $cost->id ) ) {
                            continue;
                        }
                        $clients = ! empty( $cost->clients ) ? json_decode( $cost->clients, true ) : array();
                        if ( is_array( $clients ) && in_array( $client_id, $clients ) ) {
                            $data[] = $cost;
                        }
                    }
                } else {
                    foreach ( $result as $cost ) {
                        if ( empty( $cost->id ) ) {
                            continue;
                        }
                        $data[] = $cost;
                    }
                }
            }
        }
        return $data;
    }

    /**
     * Method prepare_fields_array().
     *
     * @param array $values array of string.
     *
     * @return string Escaped string result.
     */
    public function prepare_fields_array( $values = array() ) {
        $tmp = '';
        foreach ( $values as $value ) {
            $tmp .= '"' . $this->escape( $value ) . '",';
        }
        return rtrim( $tmp, ',' );
    }

    /**
     * Method delete_cost_tracker().
     *
     * @param string $by Delete by.
     * @param mixed  $value Value.
     *
     * @return mixed Result
     */
    public function delete_cost_tracker( $by = 'id', $value = null ) {
        global $wpdb;

        if ( empty( $value ) ) {
            return false;
        }

        if ( 'id' !== $by && 'site_id' !== $by ) {
            return false;
        }
        $deleted = false;

        if ( 'id' === $by ) {
            $table = esc_sql( $this->table_name( 'cost_tracker' ) );
            if ( $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE id=%d ", $value ) ) ) { //phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching -- delete operation that invalidates caches below.
                $deleted = true;
            }
        } else {
            $table = esc_sql( $this->table_name( 'cost_tracker' ) );
            if ( $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE site_id=%d ", $value ) ) ) { //phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching -- delete operation that invalidates caches below.
                $deleted = true;
            }
        }

        if ( $deleted ) {
            $lookup_table = esc_sql( $this->table_name( 'lookup_item_objects' ) );
            $wpdb->query( $wpdb->prepare( "DELETE FROM {$lookup_table} WHERE item_id=%d AND item_name = \"cost\"", $value ) ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching -- delete operation; caching is not applicable for write operations.
            $this->invalidate_cost_tracker_caches( 'id' === $by ? $value : null );
        }

        $snapshot_info = Cost_Tracker_Admin::get_costs_snapshot_info();

        /**
         * Action mainwp_cost_tracker_deleted.
         *
         * @param string $by Delete by.
         * @param mixed $value Value.
         * @param bool $deleted Deleted.
         * @param array $snapshot_info costs snapshot info.
         *
         * @since 6.0
         *
         */
        do_action( 'mainwp_cost_tracker_deleted', $by, $value, $deleted, $snapshot_info );

        return $deleted;
    }

    /**
     * Method get_all_cost_trackers_by_clients().
     *
     * @param array $client_ids Client ids.
     * @param array $params Orther params.
     *
     * @return mixed Result
     */
    public function get_all_cost_trackers_by_clients( $client_ids, $params = array() ) { //phpcs:ignore -- NOSONAR - complex method.

        if ( is_string( $client_ids ) ) {
            $client_ids = explode( ',', $client_ids );
        }

        if ( ! is_array( $client_ids ) || empty( $client_ids ) ) {
            return array();
        }

        if ( ! is_array( $params ) ) {
            $params = array();
        }

        $get_sites_of_cost = isset( $params['get_cost_sites'] ) && $params['get_cost_sites'] ? true : false;
        $clients_site_ids  = array();

        $client_sites = MainWP_DB_Client::instance()->get_websites_by_client_ids( $client_ids );
        if ( $client_sites ) {
            foreach ( $client_sites as $website ) {
                if ( empty( $website->client_id ) ) {
                    continue;
                }
                if ( ! isset( $clients_site_ids[ $website->client_id ] ) ) {
                    $clients_site_ids[ $website->client_id ] = array();
                }
                $clients_site_ids[ $website->client_id ][] = $website->id;
            }
        }

        global $wpdb;

        $clients_costs     = array();
        $clients_costs_ids = array();

        $cache_key = 'mainwp_cost_tracker_all';
        $result    = wp_cache_get( $cache_key, 'mainwp_cost_tracker' );

        if ( false === $result ) {
            $table  = esc_sql( $this->table_name( 'cost_tracker' ) );
            $sql    = "SELECT * FROM {$table}";
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql is safely constructed with $wpdb->prepare() above; table name is escaped with esc_sql(); results cached immediately after retrieval
            $result = $wpdb->get_results( $sql, OBJECT );
            wp_cache_set( $cache_key, $result, 'mainwp_cost_tracker' );
        }

        if ( $result ) {
            foreach ( $result as $cost ) {
                $sites   = ! empty( $cost->sites ) ? json_decode( $cost->sites, true ) : array();
                $groups  = ! empty( $cost->groups ) ? json_decode( $cost->groups, true ) : array();
                $clients = ! empty( $cost->clients ) ? json_decode( $cost->clients, true ) : array();

                if ( ! is_array( $sites ) ) {
                    $sites = array();
                }
                if ( ! is_array( $groups ) ) {
                    $groups = array();
                }
                if ( ! is_array( $clients ) ) {
                    $clients = array();
                }

                if ( empty( $sites ) && empty( $groups ) && empty( $clients ) ) {
                    continue;
                }

                // to reduce db  queries for this case.
                if ( ! empty( $sites ) && empty( $groups ) && empty( $clients ) ) {
                    foreach ( $clients_site_ids as $client_id => $client_site_ids ) {
                        foreach ( $sites as $siteid ) {
                            if ( in_array( $siteid, $client_site_ids ) ) {
                                if ( ! isset( $clients_costs[ $client_id ] ) ) {
                                    $clients_costs[ $client_id ] = array();
                                }

                                if ( ! isset( $clients_costs_ids[ $client_id ] ) ) {
                                    $clients_costs_ids[ $client_id ] = array();
                                }

                                if ( $get_sites_of_cost ) {
                                    $cost->cost_sites_ids = $sites; // sites ids.
                                }
                                if ( ! in_array( $cost->id, $clients_costs_ids[ $client_id ] ) ) {
                                    $cost->count_sites                 = count( $sites );
                                    $cost->number_client_costs_sites   = array_intersect( $client_site_ids, $sites );
                                    $clients_costs[ $client_id ][]     = $cost;
                                    $clients_costs_ids[ $client_id ][] = $cost->id;
                                }
                                break;
                            }
                        }
                    }
                    continue;
                }

                $params = array(
                    'sites'   => $sites,
                    'groups'  => $groups,
                    'clients' => $clients,
                );

                // to do: should create relation of: costs, sites, groups, clients in other way.
                // to make it more easier in queries.
                $cost_sites     = MainWP_DB::instance()->get_db_sites( $params ); // get sites of cost tracker.
                $cost_sites_ids = array();

                if ( is_array( $cost_sites ) ) {

                    foreach ( $cost_sites as $cost_site ) {
                        $cost_sites_ids[] = $cost_site->id; // sites ids of cost tracker.
                    }

                    foreach ( $clients_site_ids as $client_id => $client_site_ids ) {
                        foreach ( $cost_sites as $cost_site ) {
                            if ( in_array( $cost_site->id, $client_site_ids ) ) {
                                if ( ! isset( $clients_costs[ $client_id ] ) ) {
                                    $clients_costs[ $client_id ] = array();
                                }
                                if ( ! isset( $clients_costs_ids[ $client_id ] ) ) {
                                    $clients_costs_ids[ $client_id ] = array();
                                }
                                if ( $get_sites_of_cost ) {
                                    $cost->cost_sites_ids = $cost_sites_ids;
                                }
                                if ( ! in_array( $cost->id, $clients_costs_ids[ $client_id ] ) ) {
                                    $cost->count_sites                 = count( $cost_sites );
                                    $cost->number_client_costs_sites   = array_intersect( $client_site_ids, $cost_sites_ids );
                                    $clients_costs[ $client_id ][]     = $cost;
                                    $clients_costs_ids[ $client_id ][] = $cost->id;
                                }
                                break;
                            }
                        }
                    }
                }
            }
        }

        return $clients_costs;
    }

    /**
     * Method get_sites_of_cost().
     *
     * Get sites of cost.
     *
     * @param object $cost Cost data.
     *
     * @return mixed Result
     */
    public function get_sites_of_cost( $cost ) { //phpcs:ignore -- NOSONAR - complex method.

        if ( empty( $cost ) || ! is_object( $cost ) ) {
            return array();
        }

        $sites   = ! empty( $cost->sites ) ? json_decode( $cost->sites, true ) : array(); // NOSONAR -- duplicate.
        $groups  = ! empty( $cost->groups ) ? json_decode( $cost->groups, true ) : array(); // NOSONAR -- duplicate.
        $clients = ! empty( $cost->clients ) ? json_decode( $cost->clients, true ) : array(); // NOSONAR -- duplicate.

        if ( ! is_array( $sites ) ) { // NOSONAR -- duplicate.
            $sites = array(); // NOSONAR -- duplicate.
        }
        if ( ! is_array( $groups ) ) { // NOSONAR -- duplicate.
            $groups = array(); // NOSONAR -- duplicate.
        }
        if ( ! is_array( $clients ) ) { // NOSONAR -- duplicate.
            $clients = array(); // NOSONAR -- duplicate.
        }

        if ( empty( $sites ) && empty( $groups ) && empty( $clients ) ) { // NOSONAR -- duplicate.
            return array(); // NOSONAR -- duplicate.
        }

        $params = array( // NOSONAR -- duplicate.
            'sites'   => $sites, // NOSONAR -- duplicate.
            'groups'  => $groups, // NOSONAR -- duplicate.
            'clients' => $clients, // NOSONAR -- duplicate.
        );

        return MainWP_DB::instance()->get_db_sites( $params ); // NOSONAR - get sites of cost tracker.
    }


    /**
     * Method get_all_cost_trackers_by_sites().
     *
     * @param array $sites_ids Sites ids.
     *
     * @return mixed Result
     */
    public function get_all_cost_trackers_by_sites( $sites_ids ) { //phpcs:ignore -- NOSONAR - complex method.

        if ( is_string( $sites_ids ) ) {
            $sites_ids = explode( ',', $sites_ids );
        }

        if ( ! is_array( $sites_ids ) || empty( $sites_ids ) ) {
            return array();
        }

        $sites_ids = array_map( 'intval', $sites_ids );

        global $wpdb;

        $sites_costs     = array();
        $sites_costs_ids = array();

        $cache_key = 'mainwp_cost_tracker_all';
        $result    = wp_cache_get( $cache_key, 'mainwp_cost_tracker' );

        if ( false === $result ) {
            $table  = esc_sql( $this->table_name( 'cost_tracker' ) );
            $sql    = "SELECT * FROM {$table}";
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql is safely constructed with $wpdb->prepare() above; table name is escaped with esc_sql(); results cached immediately after retrieval
            $result = $wpdb->get_results( $sql, OBJECT );
            wp_cache_set( $cache_key, $result, 'mainwp_cost_tracker' );
        }

        if ( $result ) {
            foreach ( $result as $cost ) {
                $sites   = ! empty( $cost->sites ) ? json_decode( $cost->sites, true ) : array();
                $groups  = ! empty( $cost->groups ) ? json_decode( $cost->groups, true ) : array();
                $clients = ! empty( $cost->clients ) ? json_decode( $cost->clients, true ) : array();

                if ( ! is_array( $sites ) ) {
                    $sites = array();
                }
                if ( ! is_array( $groups ) ) {
                    $groups = array();
                }
                if ( ! is_array( $clients ) ) {
                    $clients = array();
                }

                if ( empty( $sites ) && empty( $groups ) && empty( $clients ) ) {
                    continue;
                }

                // to reduce db  queries for this case.
                if ( ! empty( $sites ) && empty( $groups ) && empty( $clients ) ) {
                    foreach ( $sites_ids as $site_id ) {
                        if ( in_array( $site_id, $sites ) ) {
                            if ( ! isset( $sites_costs[ $site_id ] ) ) {
                                $sites_costs[ $site_id ] = array();
                            }

                            if ( ! isset( $sites_costs_ids[ $site_id ] ) ) {
                                $sites_costs_ids[ $site_id ] = array();
                            }

                            if ( ! in_array( $cost->id, $sites_costs_ids[ $site_id ] ) ) {
                                $cost->count_sites             = count( $sites );
                                $sites_costs[ $site_id ][]     = $cost;
                                $sites_costs_ids[ $site_id ][] = $cost->id;
                            }
                        }
                    }
                    continue;
                }

                $params = array(
                    'sites'   => $sites,
                    'groups'  => $groups,
                    'clients' => $clients,
                );

                // to do: should create relation of: costs, sites, groups, clients in other way.
                // to make it more easier in queries.
                $cost_sites = MainWP_DB::instance()->get_db_sites( $params );

                if ( is_array( $cost_sites ) ) {
                    foreach ( $sites_ids as $site_id ) {
                        foreach ( $cost_sites as $cost_site ) {
                            if ( (int) $cost_site->id === $site_id ) {
                                if ( ! isset( $sites_costs[ $site_id ] ) ) {
                                    $sites_costs[ $site_id ] = array();
                                }
                                if ( ! isset( $sites_costs_ids[ $site_id ] ) ) {
                                    $sites_costs_ids[ $site_id ] = array();
                                }
                                if ( ! in_array( $cost->id, $sites_costs_ids[ $site_id ] ) ) {
                                    $cost->count_sites             = count( $cost_sites );
                                    $sites_costs[ $site_id ][]     = $cost;
                                    $sites_costs_ids[ $site_id ][] = $cost->id;
                                }
                            }
                        }
                    }
                }
            }
        }

        return $sites_costs;
    }

    /**
     * Method get_summary_data().
     *
     * @param array $params Params.
     *
     * @return mixed Result
     */
    public function get_summary_data( $params = array() ) {
        if ( ! is_array( $params ) ) {
            $params = array();
        }

        global  $wpdb;

        $sum_data = isset( $params['sum_data'] ) ? $params['sum_data'] : '';
        $where    = '';
        $sql      = '';
        if ( 'all' === $sum_data ) {
            $cache_key = 'mainwp_cost_tracker_' . md5( wp_json_encode( array( 'method' => 'get_summary_data', 'sum_data' => 'all' ) ) ); // NOSONAR - MD5 used for cache key generation only, not cryptographic (security) purposes.
            $cached    = wp_cache_get( $cache_key, 'mainwp_cost_tracker' );
            if ( false !== $cached ) {
                return $cached;
            }

            $where .= ' AND co.cost_status = "active" AND co.type = "subscription" ';
            $table = esc_sql( $this->table_name( 'cost_tracker' ) );
            $sql = "SELECT * FROM {$table} co WHERE 1 {$where} ORDER BY co.next_renewal ASC ";
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql is safely constructed with $wpdb->prepare() above; table name is escaped with esc_sql().
            $result = $wpdb->get_results( $sql );
            wp_cache_set( $cache_key, $result, 'mainwp_cost_tracker' );
            return $result;
        } else {
            return false;
        }
    }

    /**
     * Method update_next_renewal_today()
     *
     * @return void
     */
    public function update_next_renewal_today() {

        $costs = $this->get_cost_tracker_by( 'all' );
        if ( $costs ) {
            foreach ( $costs as $cost ) {
                $next_renewal = Cost_Tracker_Admin::get_next_renewal( $cost->last_renewal, $cost->renewal_type );
                $next_today   = Cost_Tracker_Admin::calc_next_renewal_today( $cost, $next_renewal );
                if ( $next_today !== $cost->next_renewal_today ) {
                    $update = array(
                        'id'                 => $cost->id,
                        'next_renewal_today' => $next_today,
                    );
                    $this->update_cost_tracker( $update );
                }
            }
        }
        update_option( 'module_cost_tracker_calc_today_next_renewal', gmdate( 'Y-m-d' ) );
    }

    /**
     * Generate cache key for get_cost_tracker_by queries.
     *
     * @param string $by Query type.
     * @param mixed  $value Query value.
     * @param array  $params Additional parameters.
     *
     * @return string Cache key.
     */
    private function get_cache_key_for_cost_tracker( $by, $value, $params = array() ) {
        $identifier = wp_json_encode(
            array(
                'method'   => 'get_cost_tracker_by',
                'by'       => $by,
                'value'    => $value,
                'params'   => $params,
            )
        );
        return 'mainwp_cost_tracker_' . md5( $identifier ); // NOSONAR - MD5 used for cache key generation only, not cryptographic (security) purposes.
    }

    /**
     * Invalidate cost tracker caches.
     *
     * @param int|null $cost_id Cost ID to invalidate specific caches, or null for all caches.
     *
     * @return void
     */
    private function invalidate_cost_tracker_caches( $cost_id = null ) {
        wp_cache_delete( 'mainwp_cost_tracker_' . md5( wp_json_encode( array( 'method' => 'get_cost_tracker_by', 'by' => 'count' ) ) ), 'mainwp_cost_tracker' ); // NOSONAR - MD5 used for cache key generation only, not cryptographic (security) purposes.
        wp_cache_delete( 'mainwp_cost_tracker_all', 'mainwp_cost_tracker' );
        wp_cache_delete( 'mainwp_cost_tracker_' . md5( wp_json_encode( array( 'method' => 'get_summary_data', 'sum_data' => 'all' ) ) ), 'mainwp_cost_tracker' ); // NOSONAR - MD5 used for cache key generation only, not cryptographic (security) purposes.

        if ( $cost_id ) {
            $cost_id = absint( $cost_id );
            wp_cache_delete( 'mainwp_cost_tracker_' . md5( wp_json_encode( array( 'method' => 'get_cost_tracker_by', 'by' => 'id', 'value' => $cost_id ) ) ), 'mainwp_cost_tracker' ); // NOSONAR - MD5 used for cache key generation only, not cryptographic (security) purposes.
        }
    }
}
