<?php

if (!defined('ABSPATH')) {
    die('You are not allowed to call this page directly.');
}

class MeprEvent extends MeprBaseModel
{
    /**
     * Event type for user related events.
     *
     * @var string
     */
    public static $users_str         = 'users';

    /**
     * Event type for transaction related events.
     *
     * @var string
     */
    public static $transactions_str  = 'transactions';

    /**
     * Event type for subscription related events.
     *
     * @var string
     */
    public static $subscriptions_str = 'subscriptions';

    /**
     * Event type for DRM related events.
     *
     * @var string
     */
    public static $drm_str           = 'drm';

    /**
     * Event name for login events.
     *
     * @var string
     */
    public static $login_event_str = 'login';

    /**
     * Cache for event data to avoid multiple file reads
     *
     * @var array
     */
    private static $events_config_cache = null;

    /**
     * Constructor for the MeprEvent class.
     *
     * @param mixed $obj The object to initialize the event with.
     */
    public function __construct($obj = null)
    {
        $this->initialize(
            [
                'id'          => 0,
                'args'        => null,
                'event'       => 'login',
                'evt_id'      => 0,
                'evt_id_type' => 'users',
                'created_at'  => null,
            ],
            $obj
        );
    }

    /**
     * Validate the event's properties.
     *
     * @return void
     */
    public function validate()
    {
        $this->validate_is_numeric($this->evt_id, 0, null, 'evt_id');
    }

    /**
     * Get a single event by ID.
     *
     * @param  integer $id          The ID of the event.
     * @param  string  $return_type The return type of the result.
     * @return object|null
     */
    public static function get_one($id, $return_type = OBJECT)
    {
        $mepr_db = new MeprDb();
        $args    = compact('id');
        return $mepr_db->get_one_record($mepr_db->events, $args, $return_type);
    }

    /**
     * Get a single event by event, event ID, and event ID type.
     *
     * @param  string  $event       The event name.
     * @param  integer $evt_id      The event ID.
     * @param  string  $evt_id_type The event ID type.
     * @param  string  $return_type The return type of the result.
     * @return object|null
     */
    public static function get_one_by_event_and_evt_id_and_evt_id_type($event, $evt_id, $evt_id_type, $return_type = OBJECT)
    {
        $mepr_db = new MeprDb();
        return $mepr_db->get_one_record($mepr_db->events, compact('event', 'evt_id', 'evt_id_type'), $return_type);
    }

    /**
     * Get the count of all events.
     *
     * @return integer
     */
    public static function get_count()
    {
        $mepr_db = new MeprDb();
        return $mepr_db->get_count($mepr_db->events);
    }

    /**
     * Get the count of events by event name.
     *
     * @param  string $event The event name.
     * @return integer
     */
    public static function get_count_by_event($event)
    {
        $mepr_db = new MeprDb();
        return $mepr_db->get_count($mepr_db->events, compact('event'));
    }

    /**
     * Get the count of events by event ID type.
     *
     * @param  string $evt_id_type The event ID type.
     * @return integer
     */
    public static function get_count_by_evt_id_type($evt_id_type)
    {
        $mepr_db = new MeprDb();
        return $mepr_db->get_count($mepr_db->events, compact('evt_id_type'));
    }

    /**
     * Get the count of events by event, event ID, and event ID type.
     *
     * @param  string  $event       The event name.
     * @param  integer $evt_id      The event ID.
     * @param  string  $evt_id_type The event ID type.
     * @return integer
     */
    public static function get_count_by_event_and_evt_id_and_evt_id_type($event, $evt_id, $evt_id_type)
    {
        $mepr_db = new MeprDb();
        return $mepr_db->get_count($mepr_db->events, compact('event', 'evt_id', 'evt_id_type'));
    }

    /**
     * Get all events.
     *
     * @param  string $order_by The order by clause.
     * @param  string $limit    The limit clause.
     * @return array
     */
    public static function get_all($order_by = '', $limit = '')
    {
        $mepr_db = new MeprDb();
        return $mepr_db->get_records($mepr_db->events, [], $order_by, $limit);
    }

    /**
     * Get all events by event name.
     *
     * @param  string $event    The event name.
     * @param  string $order_by The order by clause.
     * @param  string $limit    The limit clause.
     * @return array
     */
    public static function get_all_by_event($event, $order_by = '', $limit = '')
    {
        $mepr_db = new MeprDb();
        $args    = ['event' => $event];
        return $mepr_db->get_records($mepr_db->events, $args, $order_by, $limit);
    }

    /**
     * Get all events by event ID type.
     *
     * @param  string $evt_id_type The event ID type.
     * @param  string $order_by    The order by clause.
     * @param  string $limit       The limit clause.
     * @return array
     */
    public static function get_all_by_evt_id_type($evt_id_type, $order_by = '', $limit = '')
    {
        $mepr_db = new MeprDb();
        $args    = ['evt_id_type' => $evt_id_type];
        return $mepr_db->get_records($mepr_db->events, $args, $order_by, $limit);
    }

    /**
     * Store the event in the database.
     *
     * @return integer The ID of the stored event.
     */
    public function store()
    {
        $mepr_db = new MeprDb();

        MeprHooks::do_action('mepr_event_pre_store', $this);

        $this->use_existing_if_unique();

        $vals = (array)$this->rec;
        unset($vals['created_at']); // Let mepr_db handle this.

        if (isset($this->id) and (int)$this->id > 0) {
            $mepr_db->update_record($mepr_db->events, $this->id, $vals);
            MeprHooks::do_action('mepr_event_update', $this);
        } else {
            $this->id = $mepr_db->create_record($mepr_db->events, $vals);
            MeprHooks::do_action('mepr_event_create', $this);
            MeprHooks::do_action('mepr_event', $this);

            MeprHooks::do_action("mepr_evt_{$this->event}", $this); // DEPRECATED.
            MeprHooks::do_action("mepr_event_{$this->event}", $this);
        }

        MeprHooks::do_action('mepr_event_store', $this);

        return $this->id;
    }

    /**
     * Destroy the event from the database.
     *
     * @return boolean True on success, false on failure.
     */
    public function destroy()
    {
        $mepr_db = new MeprDb();

        $id   = $this->id;
        $args = compact('id');

        MeprHooks::do_action('mepr_event_destroy', $this);

        return MeprHooks::apply_filters('mepr_delete_event', $mepr_db->delete_records($mepr_db->events, $args), $args);
    }

    // TODO: This is a biggie ... we don't want to send the event object like this
    // we need to send the object associated with the event instead.
    /**
     * Get the data for the event.
     *
     * @return object|false
     */
    public function get_data()
    {
        $obj = false;
        switch ($this->evt_id_type) {
            case self::$users_str:
                $obj = new MeprUser($this->evt_id);

                // If member-deleted event is being passed, make sure we generate some data.
                if (!isset($obj->ID) || $obj->ID <= 0) {
                    if ($this->event === 'member-deleted') {
                          $obj->ID         = 0;
                          $obj->user_email = 'johndoe@email.com';
                          $obj->user_login = 'johndoe';
                          $obj->first_name = 'John';
                          $obj->last_name  = 'Doe';
                    }
                }

                break;
            case self::$transactions_str:
                $obj = new MeprTransaction($this->evt_id);
                break;
            case self::$subscriptions_str:
                $obj = new MeprSubscription($this->evt_id);
                break;
            default:
                return new WP_Error(__('An unsupported Event type was used', 'memberpress'));
        }

        return $obj;
    }

    /**
     * Get the arguments for the event.
     *
     * @return mixed
     */
    public function get_args()
    {
        if (!empty($this->args) && is_string($this->args)) {
            return json_decode($this->args);
        }
        return $this->args;
    }

    /**
     * Record an event.
     *
     * @param  string        $event The event name.
     * @param  MeprBaseModel $obj   The object associated with the event.
     * @param  mixed         $args  The arguments for the event.
     * @return void
     */
    public static function record($event, MeprBaseModel $obj, $args = '')
    {
        // Nothing to record? Hopefully this stops some ghost duplicate reminders we are seeing
        // Gotta use ->rec here to avoid weird shiz from happening hopefully.
        if ((!isset($obj->rec->id) || !$obj->rec->id) && (!isset($obj->rec->ID) || !$obj->rec->ID)) {
            return;
        }

        $e        = new MeprEvent();
        $e->event = $event;
        $e->args  = $args;

        // Just turn objects into json for fun.
        if (is_array($args) || is_object($args)) {
            $e->args = json_encode($args);
        }

        if ($obj instanceof MeprUser) {
            $e->evt_id      = $obj->rec->ID;
            $e->evt_id_type = self::$users_str;
        } elseif ($obj instanceof MeprTransaction) {
            $e->evt_id      = $obj->rec->id;
            $e->evt_id_type = self::$transactions_str;
        } elseif ($obj instanceof MeprSubscription) {
            $e->evt_id      = $obj->rec->id;
            $e->evt_id_type = self::$subscriptions_str;
        } elseif ($obj instanceof MeprDrm) {
            $e->evt_id      = $obj->rec->id;
            $e->evt_id_type = self::$drm_str;
        } else {
            return;
        }

        if (self::is_time_throttled($e)) {
            return;
        }

        $e->store();
    }

    /**
     * Get the latest object for a given event.
     *
     * @param  string $event The event name.
     * @return MeprEvent|false
     */
    public static function latest($event)
    {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $id = $wpdb->get_var(
            $wpdb->prepare("SELECT id FROM {$wpdb->mepr_events} WHERE event=%s ORDER BY id DESC LIMIT 1", $event)
        );

        if ($id) {
            return new MeprEvent($id);
        }

        return false;
    }

    /**
     * Get the tablename for the specific type of event.
     *
     * @param  string $event_type The event type.
     * @return string|null
     */
    public static function get_tablename($event_type)
    {
        global $wpdb;

        $mepr_db = MeprDb::fetch();

        if ($event_type === MeprEvent::$users_str) {
            return $wpdb->users;
        } elseif ($event_type === MeprEvent::$transactions_str) {
            return $mepr_db->transactions;
        } elseif ($event_type === MeprEvent::$subscriptions_str) {
            return $mepr_db->subscriptions;
        }
    }

    /**
     * Gets info from app/data/events.php if it exists.
     *
     * @return associative array if found or false if not found
     */
    private function event_info()
    {
        $event_data = self::get_events_config();

        if (isset($event_data[$this->event])) {
            return $event_data[$this->event];
        }

        return false;
    }

    /**
     * Uses app/data/events.php to determine if the current event is
     * unique -- if true, only one row can be stored for a given event,
     * evt_id & evt_id_type.
     *
     * @return true/false
     */
    private function is_unique()
    {
        $event_info = $this->event_info();
        return (false !== $event_info && isset($event_info->unique) && $event_info->unique);
    }

    /**
     * Copy an existing event id & args if the event is unique and another
     * event record with the same event, evt_id & evt_id_type already exists.
     *
     * @return void
     */
    private function use_existing_if_unique()
    {
        if ($this->is_unique()) {
            $existing_event = self::get_one_by_event_and_evt_id_and_evt_id_type($this->event, $this->evt_id, $this->evt_id_type);
            if (!empty($existing_event)) {
                $this->id   = $existing_event->id;
                $this->args = $existing_event->args;
            }
        }
    }

    /**
     * Get the latest object for a given event and elapsed days.
     *
     * @param  string  $event        The event name.
     * @param  integer $elapsed_days The number of elapsed days.
     * @return MeprEvent|false
     */
    public static function latest_by_elapsed_days($event, $elapsed_days)
    {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $id = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT id FROM {$wpdb->mepr_events} WHERE event=%s AND created_at >= %s - interval %d day ORDER BY id DESC LIMIT 1",
                $event,
                MeprUtils::db_now(),
                $elapsed_days
            )
        );

        if ($id) {
            return new MeprEvent($id);
        }

        return false;
    }

    /**
     * Load Event Configuration file into an array
     *
     * @return array|null
     */
    private static function get_events_config()
    {
        // Load and cache events data.
        if (self::$events_config_cache === null) {
            $events_file = MEPR_DATA_PATH . '/events.php';
            if (!file_exists($events_file)) {
                return []; // No events configuration, no throttling.
            }
            self::$events_config_cache = require($events_file);
        }

        return self::$events_config_cache;
    }

    /**
     * Check if an event should be throttled based on time window
     *
     * @param  MeprEvent $e Event name.
     * @return boolean True if event should be throttled (skipped)
     */
    private static function is_time_throttled(MeprEvent $e)
    {
        global $wpdb;

        $event_config = self::get_events_config();

        if (!isset($event_config[$e->event])) {
            return false;
        }

        $event_data = $event_config[$e->event];

        // Get and validate time limit.
        $time_limit = absint($event_data->time_limit ?? 0);

        /**
         * Filter the event rate limit
         *
         * @param  int $time_limit Current time limit in seconds.
         * @param  MeprEvent $e Event object being checked.
         * @param  stdClass $event_data Full event configuration data.
         * @return int Modified time limit
         */
        $time_limit = MeprHooks::apply_filters('mepr_event_rate_limit', $time_limit, $e, $event_data);

        if ($time_limit <= 0) {
            return false;
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $last_event_created_at = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT created_at FROM {$wpdb->mepr_events} WHERE `event` = %s AND evt_id = %d AND evt_id_type = %s ORDER BY created_at DESC LIMIT 1",
                $e->event,
                (int) $e->evt_id,
                $e->evt_id_type
            )
        );

        if (!empty($last_event_created_at)) {
            // Ensure we have a valid timestamp.
            $last_event_time = strtotime($last_event_created_at);
            if ($last_event_time === false) {
                return false; // Invalid date, don't throttle.
            }

            $time_since_last = time() - $last_event_time;

            /**
             * Filter whether an event should be throttled
             *
             * @param  boolean $should_throttle Current throttle decision.
             * @param  MeprEvent $e Current event being checked.
             * @param  string $last_event_created_at Event DateTime stamp.
             * @param  int $time_since_last Seconds since last event.
             * @param  int $time_limit Current throttle time limit.
             * @return boolean Modified throttle decision
             */
            $should_throttle = ($time_since_last < $time_limit);

            return MeprHooks::apply_filters(
                'mepr_should_throttle_event',
                $should_throttle,
                $e,
                $last_event_created_at,
                $time_since_last,
                $time_limit
            );
        }

        return false;
    }
}
