<?php
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Free analytics event collector and storage primitive.
 *
 * Advanced reports, comparisons, revenue attribution and exports are owned by
 * Nex Directory Pro. Keeping the collection table in Free preserves historical
 * data and lets a later Pro activation report on events collected beforehand.
 */
class NEXDIRECTORY_Analytics_Service {

    /**
     * @var string
     */
    private $post_type;

    /**
     * @var string
     */
    private $analytics_table = 'nexdirectory_event_analytics';

    /**
     * Request-local table existence cache.
     *
     * @var array<string, bool>
     */
    private $table_exists_cache = [];

    /**
     * Constructor.
     *
     * @param string $post_type Event/listing post type.
     * @param mixed  $logger    Retained for bootstrap compatibility.
     */
    public function __construct( $post_type = 'nexdirectory_event', $logger = null ) {
        unset( $logger );
        $this->post_type = sanitize_key( (string) $post_type );
        if ( $this->post_type === '' ) {
            $this->post_type = 'nexdirectory_event';
        }
    }

    /**
     * Inserts one analytics event.
     *
     * @param int                  $event_id Event/listing ID.
     * @param string               $metric   Metric key.
     * @param array<string, mixed> $args     Event context.
     * @return bool
     */
    public function record_analytics_event( $event_id, $metric, array $args = [] ) {
        global $wpdb;

        $metric = sanitize_key( (string) $metric );
        if ( $metric === '' ) {
            return false;
        }

        $table = $this->get_analytics_table_name();
        $this->ensure_analytics_table_exists();
        if ( ! $this->table_exists( $table ) ) {
            return false;
        }

        $event_id = max( 0, absint( (int) $event_id ) );
        $context  = sanitize_key( (string) ( $args['context'] ?? '' ) );
        if ( $context === '' ) {
            $context = 'single';
        }

        $meta_key   = sanitize_key( (string) ( $args['meta_key'] ?? '' ) );
        $meta_value = sanitize_text_field( (string) ( $args['meta_value'] ?? '' ) );
        $value      = isset( $args['value'] ) ? (float) $args['value'] : 1.0;
        if ( ! is_finite( $value ) || $value <= 0 ) {
            $value = 1.0;
        }

        $user_id  = isset( $args['user_id'] ) ? absint( (int) $args['user_id'] ) : absint( (int) get_current_user_id() );
        $owner_id = isset( $args['owner_id'] ) ? absint( (int) $args['owner_id'] ) : 0;
        if ( $owner_id <= 0 && $event_id > 0 && get_post_type( $event_id ) === $this->post_type ) {
            $owner_id = absint( (int) get_post_field( 'post_author', $event_id ) );
        }

        $created_at_raw = sanitize_text_field( (string) ( $args['created_at'] ?? '' ) );
        $created_ts     = $created_at_raw !== '' ? strtotime( $created_at_raw ) : false;
        $created_at     = $created_ts ? gmdate( 'Y-m-d H:i:s', (int) $created_ts ) : gmdate( 'Y-m-d H:i:s' );

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Analytics events are stored in NexDirectory's purpose-built custom table rather than a WordPress core table.
        $inserted = $wpdb->insert(
            $table,
            [
                'event_id'  => $event_id,
                'owner_id'  => $owner_id,
                'user_id'   => $user_id,
                'metric'    => $metric,
                'context'   => $context,
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- This is a custom analytics column covered by the metric_meta_lookup composite index, not WordPress metadata.
                'meta_key'  => $meta_key,
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- This is a custom analytics column covered by the metric_meta_lookup composite index, not WordPress metadata.
                'meta_value'=> $meta_value,
                'value'     => $value,
                'created_at'=> $created_at,
            ],
            [ '%d', '%d', '%d', '%s', '%s', '%s', '%s', '%f', '%s' ]
        ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

        return $inserted !== false;
    }

    /**
     * Batch-inserts one analytics metric for a list of events.
     *
     * @param array<int, mixed>    $event_ids Event/listing IDs.
     * @param string               $metric    Metric key.
     * @param array<string, mixed> $args      Event context.
     * @return int
     */
    public function record_analytics_for_event_ids( array $event_ids, $metric, array $args = [] ) {
        global $wpdb;

        $ids    = array_values( array_unique( array_filter( array_map( 'absint', $event_ids ) ) ) );
        $metric = sanitize_key( (string) $metric );
        if ( empty( $ids ) || $metric === '' ) {
            return 0;
        }

        $table = $this->get_analytics_table_name();
        $this->ensure_analytics_table_exists();
        if ( ! $this->table_exists( $table ) ) {
            return 0;
        }

        $context = sanitize_key( (string) ( $args['context'] ?? '' ) );
        if ( $context === '' ) {
            $context = 'single';
        }
        $meta_key       = sanitize_key( (string) ( $args['meta_key'] ?? '' ) );
        $meta_value     = sanitize_text_field( (string) ( $args['meta_value'] ?? '' ) );
        $value          = isset( $args['value'] ) ? (float) $args['value'] : 1.0;
        $user_id        = isset( $args['user_id'] ) ? absint( (int) $args['user_id'] ) : absint( (int) get_current_user_id() );
        $fixed_owner_id = isset( $args['owner_id'] ) ? absint( (int) $args['owner_id'] ) : 0;
        if ( ! is_finite( $value ) || $value <= 0 ) {
            $value = 1.0;
        }

        $owner_ids = [];
        if ( $fixed_owner_id <= 0 ) {
            foreach ( $ids as $owner_event_id ) {
                $owner_post = get_post( $owner_event_id );
                if ( $owner_post instanceof WP_Post && $owner_post->post_type === $this->post_type ) {
                    $owner_ids[ $owner_event_id ] = absint( (int) $owner_post->post_author );
                }
            }
        }

        $created_at_raw = sanitize_text_field( (string) ( $args['created_at'] ?? '' ) );
        $created_ts     = $created_at_raw !== '' ? strtotime( $created_at_raw ) : false;
        $created_at     = $created_ts ? gmdate( 'Y-m-d H:i:s', (int) $created_ts ) : gmdate( 'Y-m-d H:i:s' );
        $placeholders   = [];
        $values         = [];

        foreach ( $ids as $event_id ) {
            $owner_id      = $fixed_owner_id > 0 ? $fixed_owner_id : absint( (int) ( $owner_ids[ $event_id ] ?? 0 ) );
            $placeholders[]= '(%d, %d, %d, %s, %s, %s, %s, %f, %s)';
            array_push( $values, $event_id, $owner_id, $user_id, $metric, $context, $meta_key, $meta_value, $value, $created_at );
        }

        $sql = "INSERT INTO {$table} (event_id, owner_id, user_id, metric, context, meta_key, meta_value, value, created_at) VALUES "
            . implode( ', ', $placeholders );
        $inserted = $wpdb->query( $wpdb->prepare( $sql, $values ) ); // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- The SQL targets NexDirectory's prefix-derived custom analytics table; all event values are prepared and write results are not cacheable.

        return $inserted === false ? 0 : min( count( $ids ), (int) $inserted );
    }

    /**
     * @return string
     */
    public function get_analytics_table_name() {
        global $wpdb;
        return (string) $wpdb->prefix . $this->analytics_table;
    }

    /**
     * @return bool
     */
    public function analytics_table_exists() {
        return $this->table_exists( $this->get_analytics_table_name() );
    }

    /**
     * Creates the shared collection table when needed.
     *
     * @return void
     */
    private function ensure_analytics_table_exists() {
        global $wpdb;

        $table = $this->get_analytics_table_name();
        if ( $this->table_exists( $table ) ) {
            return;
        }

        $charset = $wpdb->get_charset_collate();
        $sql = "CREATE TABLE {$table} (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            event_id bigint(20) unsigned NOT NULL DEFAULT 0,
            owner_id bigint(20) unsigned NOT NULL DEFAULT 0,
            user_id bigint(20) unsigned NOT NULL DEFAULT 0,
            metric varchar(64) NOT NULL,
            context varchar(64) NOT NULL DEFAULT '',
            meta_key varchar(64) NOT NULL DEFAULT '',
            meta_value varchar(191) NOT NULL DEFAULT '',
            value decimal(18,4) NOT NULL DEFAULT 1.0000,
            created_at datetime NOT NULL,
            PRIMARY KEY  (id),
            KEY event_metric_date (event_id, metric, created_at),
            KEY owner_metric_date (owner_id, metric, created_at),
            KEY user_metric_date (user_id, metric, created_at),
            KEY metric_date (metric, created_at),
            KEY context_metric_date (context, metric, created_at),
            KEY metric_meta_lookup (metric, meta_key, meta_value(64))
        ) {$charset};";

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta( $sql );
        $this->table_exists( $table, true );
    }

    /**
     * @param string $table   Table name.
     * @param bool   $refresh Force a database lookup.
     * @return bool
     */
    private function table_exists( $table, $refresh = false ) {
        global $wpdb;

        $table = sanitize_text_field( (string) $table );
        if ( $table === '' ) {
            return false;
        }
        if ( ! $refresh && array_key_exists( $table, $this->table_exists_cache ) ) {
            return (bool) $this->table_exists_cache[ $table ];
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom-table existence is schema introspection and is already cached for the request in $table_exists_cache.
        $found = (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) );
        $this->table_exists_cache[ $table ] = $found === $table;
        return $this->table_exists_cache[ $table ];
    }
}
