<?php
/**
 * Leads Endpoint for BoostBot REST API.
 *
 * @package BoostBot
 */

// Prevent direct access to the file.
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if (!defined('ABSPATH')) {
    exit;
}

/**
 * BoostBot_Leads_Endpoint class.
 *
 * Handles CRUD operations for leads (Admin only).
 *
 * @since 1.0.0
 */
class BoostBot_Leads_Endpoint extends BoostBot_Base_Endpoint
{
    /**
     * The base path for this endpoint.
     *
     * @since 1.0.0
     * @access protected
     * @var string $rest_base
     */
    protected $rest_base = 'leads';

    /**
     * Register the routes for this endpoint.
     *
     * @since 1.0.0
     */
    public function register()
    {
        // GET /wp-json/boostbot/v1/leads - List all leads
        $this->register_route(
            '',
            'GET',
            [$this, 'get_leads'],
            [$this, 'admin_access'],
            [
                'search' => [
                    'type'              => 'string',
                    'sanitize_callback' => 'sanitize_text_field',
                ],
                'language' => [
                    'type'              => 'string',
                    'sanitize_callback' => 'sanitize_text_field',
                ],
            ]
        );

        // GET /wp-json/boostbot/v1/leads/stats - Quick stats
        $this->register_route(
            '/stats',
            'GET',
            [$this, 'get_stats'],
            [$this, 'admin_access']
        );

        // GET /wp-json/boostbot/v1/leads/{id} - Single lead
        $this->register_route(
            '/(?P<id>\d+)',
            'GET',
            [$this, 'get_lead'],
            [$this, 'admin_access']
        );

        // PUT /wp-json/boostbot/v1/leads/{id} - Update lead
        $this->register_route(
            '/(?P<id>\d+)',
            'PUT',
            [$this, 'update_lead'],
            [$this, 'admin_access']
        );

        // DELETE /wp-json/boostbot/v1/leads/{id} - Delete lead
        $this->register_route(
            '/(?P<id>\d+)',
            'DELETE',
            [$this, 'delete_lead'],
            [$this, 'admin_access']
        );

        // POST /wp-json/boostbot/v1/leads - Create lead (public)
        $this->register_route(
            '',
            'POST',
            [$this, 'create_lead'],
            [$this, 'public_with_validation']
        );
    }

    /**
     * Get list of leads with pagination and filtering.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request The REST request object.
     * @return WP_REST_Response The response with leads.
     */
    public function get_leads($request)
    {
        BoostBot_Logger::debug('Getting leads list');

        // Get pagination params
        $pagination = $this->get_pagination_params($request);

        // Get sorting params
        $allowed_sort_fields = ['id', 'name', 'email', 'lead_score', 'lead_status', 'created_at'];
        $sorting = $this->get_sorting_params($request, 'created_at', $allowed_sort_fields);

        // Build filters
        $filters = $this->build_filters($request);

        // Extract search (needs LIKE — not a simple column filter)
        $search = '';
        if ( ! empty( $filters['search'] ) ) {
            $search = $filters['search'];
            unset( $filters['search'] );
        }

        // Get leads
        $model = new BoostBot_Lead_Model();

        if ( ! empty( $search ) ) {
            $leads = $this->query_leads_with_search( $search, $filters, $sorting, $pagination );
            $total = $this->count_leads_with_search( $search, $filters );
        } else {
            $args = [
                'where'    => $filters,
                'orderby'  => $sorting['orderby'],
                'order'    => $sorting['order'],
                'limit'    => $pagination['per_page'],
                'offset'   => $pagination['offset'],
            ];

            $leads = $model->get_all($args);
            $total = $model->count($filters);
        }

        $leads = array_map( [ $this, 'decode_lead_json_fields' ], $leads );

        return $this->success([
            'leads'      => $leads,
            'pagination' => $this->build_pagination_response(
                $pagination['page'],
                $pagination['per_page'],
                $total
            ),
        ]);
    }

    /**
     * Get a single lead.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request The REST request object.
     * @return WP_REST_Response|WP_Error The response or error.
     */
    public function get_lead($request)
    {
        $id = absint($request->get_param('id'));

        BoostBot_Logger::debug('Getting lead', ['id' => $id]);

        $model = new BoostBot_Lead_Model();
        $lead = $model->get($id);

        if (!$lead) {
            return $this->not_found('Lead');
        }

        $lead = $this->decode_lead_json_fields( $lead );
        $lead = $this->enrich_lead_form_info( $lead );

        // Get associated conversation
        $conversation = null;
        if (!empty($lead->conversation_id)) {
            $conv_model = new BoostBot_Conversation_Model();
            $conversation = $conv_model->get_by_conversation_id($lead->conversation_id);
        }

        return $this->success([
            'lead'         => $lead,
            'conversation' => $conversation,
        ]);
    }

    /**
     * Update a lead.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request The REST request object.
     * @return WP_REST_Response|WP_Error The response or error.
     */
    public function update_lead($request)
    {
        $id = absint($request->get_param('id'));

        BoostBot_Logger::debug('Updating lead', ['id' => $id]);

        // Allowed fields to update
        $allowed_fields = [
            'name',
            'email',
            'phone',
            'company',
            'lead_status',
            'lead_score',
            'metadata',
            'conversation_summary',
        ];

        // Build update data
        $data = [];
        foreach ($allowed_fields as $field) {
            if ($request->has_param($field)) {
                $value = $request->get_param($field);

                // Sanitize based on field type
                switch ($field) {
                    case 'email':
                        $data[$field] = sanitize_email($value);
                        break;
                    case 'lead_score':
                        $data[$field] = absint($value);
                        break;
                    case 'metadata':
                        $data[$field] = is_array($value) ? $value : [];
                        break;
                    default:
                        $data[$field] = sanitize_text_field($value);
                }
            }
        }

        if (empty($data)) {
            return $this->error(__('No data to update', 'boostbot'), 400);
        }

        $model = new BoostBot_Lead_Model();

        // Check if lead exists
        if (!$model->get($id)) {
            return $this->not_found('Lead');
        }

        $result = $model->update($id, $data);

        if ($result === false) {
            return $this->error(__('Failed to update lead', 'boostbot'), 500);
        }

        BoostBot_Logger::info('Lead updated', [
            'id'     => $id,
            'fields' => array_keys($data),
        ]);

        return $this->success([
            'message' => __('Lead updated successfully', 'boostbot'),
            'lead'    => $this->decode_lead_json_fields( $model->get($id) ),
        ]);
    }

    /**
     * Delete a lead.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request The REST request object.
     * @return WP_REST_Response|WP_Error The response or error.
     */
    public function delete_lead($request)
    {
        $id = absint($request->get_param('id'));

        BoostBot_Logger::debug('Deleting lead', ['id' => $id]);

        $model = new BoostBot_Lead_Model();

        // Check if lead exists
        if (!$model->get($id)) {
            return $this->not_found('Lead');
        }

        $result = $model->delete($id);

        if (!$result) {
            return $this->error(__('Failed to delete lead', 'boostbot'), 500);
        }

        BoostBot_Logger::info('Lead deleted', ['id' => $id]);

        return $this->success([
            'message' => __('Lead deleted successfully', 'boostbot'),
        ]);
    }

    /**
     * Create a new lead (public endpoint).
     *
     * @since 1.0.0
     * @param WP_REST_Request $request The REST request object.
     * @return WP_REST_Response|WP_Error The response or error.
     */
    public function create_lead($request)
    {
        BoostBot_Logger::debug('Creating lead');

        $conversation_id = sanitize_text_field($request->get_param('conversation_id'));
        $visitor_id = sanitize_text_field($request->get_param('visitor_id'));
        $name = sanitize_text_field($request->get_param('name'));
        $email = sanitize_email($request->get_param('email'));
        $phone = sanitize_text_field($request->get_param('phone'));
        $company = sanitize_text_field($request->get_param('company'));
        $source_page = esc_url_raw($request->get_param('source_page'));
        $referrer = esc_url_raw($request->get_param('referrer'));

        // Validate - need at least email or phone
        if (empty($email) && empty($phone)) {
            return $this->validation_error([
                'email' => __('Email or phone is required', 'boostbot'),
            ]);
        }

        // Validate email format if provided
        if (!empty($email) && !is_email($email)) {
            return $this->validation_error([
                'email' => __('Invalid email format', 'boostbot'),
            ]);
        }

        $model = new BoostBot_Lead_Model();

        // Check for existing lead by email. This endpoint is public/unauthenticated,
        // so blindly updating a lead on an email match lets anyone who knows an
        // email overwrite that lead (IDOR). Only mutate in place when the request
        // is provably tied to the existing lead via a matching conversation_id or
        // visitor_id; otherwise insert a new row (admin can dedupe).
        if (!empty($email)) {
            $existing = $model->get_by_email($email);
            if ($existing) {
                $conversation_matches = !empty($conversation_id)
                    && !empty($existing->conversation_id)
                    && hash_equals((string) $existing->conversation_id, (string) $conversation_id);
                $visitor_matches = !empty($visitor_id)
                    && !empty($existing->visitor_id)
                    && hash_equals((string) $existing->visitor_id, (string) $visitor_id);

                if ($conversation_matches || $visitor_matches) {
                    // Ownership proven — update existing lead.
                    $model->update($existing->id, [
                        'name'            => $name ?: $existing->name,
                        'phone'           => $phone ?: $existing->phone,
                        'company'         => $company ?: $existing->company,
                        'conversation_id' => $conversation_id ?: $existing->conversation_id,
                    ]);

                    return $this->success([
                        'message' => __('Lead updated', 'boostbot'),
                        'lead_id' => $existing->id,
                        'is_new'  => false,
                    ]);
                }
                // No ownership proof — fall through and insert a new lead row.
            }
        }

        $language = BoostBot_Language::with_fallback( $request->get_param( 'language' ) );

        // Create lead data
        $lead_data = [
            'language'        => $language,
            'conversation_id' => $conversation_id ?: '',
            'visitor_id'      => $visitor_id ?: $this->get_visitor_identifier($request),
            'name'            => $name,
            'email'           => $email,
            'phone'           => $phone,
            'company'         => $company,
            'lead_status'     => 'new',
            'source_page'     => $source_page,
            'referrer'        => $referrer,
        ];

        // Calculate lead score
        $lead_data['lead_score'] = $model->calculate_score($lead_data);

        $lead_id = $model->create($lead_data);

        if (!$lead_id) {
            return $this->error(__('Failed to create lead', 'boostbot'), 500);
        }

        BoostBot_Logger::info('Lead created', ['lead_id' => $lead_id]);

        return $this->success([
            'message' => __('Lead created successfully', 'boostbot'),
            'lead_id' => $lead_id,
            'is_new'  => true,
        ], 201);
    }

    /**
     * Get lead statistics.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request The REST request object.
     * @return WP_REST_Response The response with stats.
     */
    public function get_stats($request)
    {
        BoostBot_Logger::debug('Getting lead stats');

        $model = new BoostBot_Lead_Model();

        // Language filter (stats should reflect the current language context)
        $lang_filter = [];
        $language = sanitize_text_field($request->get_param('language'));
        if (!empty($language) && $language !== 'all') {
            $lang_filter['language'] = $language;
        }

        // Get counts by status
        $stats = [
            'total'       => $model->count($lang_filter),
            'by_status'   => [
                'new'       => $model->count(array_merge($lang_filter, ['lead_status' => 'new'])),
                'contacted' => $model->count(array_merge($lang_filter, ['lead_status' => 'contacted'])),
                'qualified' => $model->count(array_merge($lang_filter, ['lead_status' => 'qualified'])),
                'converted' => $model->count(array_merge($lang_filter, ['lead_status' => 'converted'])),
                'lost'      => $model->count(array_merge($lang_filter, ['lead_status' => 'lost'])),
            ],
            'this_month'  => $this->count_this_month($model, $lang_filter),
            'this_week'   => $this->count_this_week($model, $lang_filter),
            'avg_score'   => $this->get_average_score($model, $lang_filter),
        ];

        return $this->success($stats);
    }

    /**
     * Decode JSON-encoded columns on a lead DB row so the REST response
     * returns objects instead of raw JSON strings.
     *
     * @param object|array $lead Raw lead row from the database.
     * @return object|array The lead with decoded JSON fields.
     */
    private function decode_lead_json_fields( $lead ) {
        if ( ! $lead ) {
            return $lead;
        }

        $json_fields = [ 'form_data', 'utm_data', 'metadata' ];

        foreach ( $json_fields as $field ) {
            if ( isset( $lead->$field ) && is_string( $lead->$field ) ) {
                $decoded = json_decode( $lead->$field, true );
                $lead->$field = is_array( $decoded ) ? $decoded : null;
            }
        }

        return $lead;
    }

    /**
     * Enrich a lead with form field Hebrew labels and form/widget name
     * by looking up the original widget definition.
     *
     * Adds `form_field_labels` (name→label map) and `form_name` to the lead.
     *
     * @param object $lead Lead object (must have decoded metadata already).
     * @return object The enriched lead.
     */
    private function enrich_lead_form_info( $lead ) {
        if ( ! $lead ) {
            return $lead;
        }

        $lead->form_field_labels = null;
        $lead->form_name         = null;

        $widget_id = 0;
        if ( ! empty( $lead->metadata ) && is_array( $lead->metadata ) && ! empty( $lead->metadata['widget_id'] ) ) {
            $widget_id = absint( $lead->metadata['widget_id'] );
        }

        if ( $widget_id <= 0 ) {
            return $lead;
        }

        if ( class_exists( 'BoostBot_Widget_Model' ) ) {
            $widget_model = new \BoostBot_Widget_Model();
            $widget       = $widget_model->get( $widget_id );
            if ( $widget && ! empty( $widget->name ) ) {
                $lead->form_name = $widget->name;
            }
        }

        if ( class_exists( 'BoostBot_Form_Field_Model' ) ) {
            $field_model = new \BoostBot_Form_Field_Model();
            $fields      = $field_model->get_by_widget( $widget_id );

            if ( ! empty( $fields ) ) {
                $labels = [];
                foreach ( $fields as $field ) {
                    if ( ! empty( $field->name ) && ! empty( $field->label ) ) {
                        $labels[ $field->name ] = $field->label;
                    }
                }
                if ( ! empty( $labels ) ) {
                    $lead->form_field_labels = $labels;
                }
            }
        }

        return $lead;
    }

    /**
     * Build filters from request parameters.
     *
     * @since 1.0.0
     * @param WP_REST_Request $request The REST request object.
     * @return array Filters array.
     */
    private function build_filters($request)
    {
        $filters = [];

        // Status filter
        $status = sanitize_text_field($request->get_param('status'));
        if (!empty($status)) {
            $filters['lead_status'] = $status;
        }

        // Search filter
        $search = sanitize_text_field($request->get_param('search'));
        if (!empty($search)) {
            $filters['search'] = $search;
        }

        // Language filter
        $language = sanitize_text_field($request->get_param('language'));
        if (!empty($language) && $language !== 'all') {
            $filters['language'] = $language;
        }

        return $filters;
    }

    /**
     * Query leads with LIKE search + column filters.
     *
     * @param string $search     Search term.
     * @param array  $filters    Column-equality filters (status, language, etc.).
     * @param array  $sorting    Sorting params with orderby and order keys.
     * @param array  $pagination Pagination params with per_page and offset keys.
     * @return array
     */
    private function query_leads_with_search( $search, $filters, $sorting, $pagination ) {
        global $wpdb;
        $table = self::sanitize_identifier( $wpdb->prefix . 'boostbot_leads' );

        $where_parts = [];
        $params      = [];

        // LIKE search across contact fields
        $like = '%' . $wpdb->esc_like( $search ) . '%';
        $where_parts[] = '(name LIKE %s OR email LIKE %s OR phone LIKE %s OR company LIKE %s)';
        $params        = array_merge( $params, [ $like, $like, $like, $like ] );

        // Column filters
        if ( ! empty( $filters['lead_status'] ) ) {
            $where_parts[] = 'lead_status = %s';
            $params[]      = $filters['lead_status'];
        }
        if ( ! empty( $filters['language'] ) ) {
            $where_parts[] = 'language = %s';
            $params[]      = $filters['language'];
        }

        $where_sql = implode( ' AND ', $where_parts );

        $allowed_sort = [ 'id', 'name', 'email', 'lead_score', 'lead_status', 'created_at' ];
        $orderby      = in_array( $sorting['orderby'], $allowed_sort, true ) ? $sorting['orderby'] : 'created_at';
        $order        = strtoupper( $sorting['order'] ) === 'ASC' ? 'ASC' : 'DESC';

        $params[] = absint( $pagination['per_page'] );
        $params[] = absint( $pagination['offset'] );

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $table sanitized via sanitize_identifier(); array-args form unpacks $params at runtime; values bind via prepare().
        return $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d",
                $params
            )
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter
    }

    /**
     * Count leads matching a LIKE search + column filters.
     *
     * @param string $search  Search term.
     * @param array  $filters Column-equality filters.
     * @return int
     */
    private function count_leads_with_search( $search, $filters ) {
        global $wpdb;
        $table = self::sanitize_identifier( $wpdb->prefix . 'boostbot_leads' );

        $where_parts = [];
        $params      = [];

        $like = '%' . $wpdb->esc_like( $search ) . '%';
        $where_parts[] = '(name LIKE %s OR email LIKE %s OR phone LIKE %s OR company LIKE %s)';
        $params        = array_merge( $params, [ $like, $like, $like, $like ] );

        if ( ! empty( $filters['lead_status'] ) ) {
            $where_parts[] = 'lead_status = %s';
            $params[]      = $filters['lead_status'];
        }
        if ( ! empty( $filters['language'] ) ) {
            $where_parts[] = 'language = %s';
            $params[]      = $filters['language'];
        }

        $where_sql = implode( ' AND ', $where_parts );

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $table sanitized via sanitize_identifier(); $where_sql contains %s placeholders inside dynamic WHERE builder; values bind via prepare().
        return (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}",
                $params
            )
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, PluginCheck.Security.DirectDB.UnescapedDBParameter
    }

    /**
     * Count leads created this month.
     *
     * @since 1.0.0
     * @param BoostBot_Lead_Model $model The lead model.
     * @return int Count.
     */
    private function count_this_month($model, $lang_filter = [])
    {
        global $wpdb;
        $table = self::sanitize_identifier( $wpdb->prefix . 'boostbot_leads' );

        $where = 'MONTH(created_at) = MONTH(CURRENT_DATE()) AND YEAR(created_at) = YEAR(CURRENT_DATE())';
        if ( ! empty( $lang_filter['language'] ) ) {
            $where .= $wpdb->prepare( ' AND language = %s', $lang_filter['language'] );
        }

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        return (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM `{$table}` WHERE {$where}"
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
    }

    /**
     * Count leads created this week.
     *
     * @since 1.0.0
     * @param BoostBot_Lead_Model $model The lead model.
     * @return int Count.
     */
    private function count_this_week($model, $lang_filter = [])
    {
        global $wpdb;
        $table = self::sanitize_identifier( $wpdb->prefix . 'boostbot_leads' );

        $where = 'YEARWEEK(created_at, 1) = YEARWEEK(CURRENT_DATE(), 1)';
        if ( ! empty( $lang_filter['language'] ) ) {
            $where .= $wpdb->prepare( ' AND language = %s', $lang_filter['language'] );
        }

        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
        return (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM `{$table}` WHERE {$where}"
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
    }

    /**
     * Get average lead score.
     *
     * @since 1.0.0
     * @param BoostBot_Lead_Model $model The lead model.
     * @return float Average score.
     */
    private function get_average_score($model, $lang_filter = [])
    {
        global $wpdb;
        $table = self::sanitize_identifier( $wpdb->prefix . 'boostbot_leads' );

        $where = '1=1';
        if ( ! empty( $lang_filter['language'] ) ) {
            $where = $wpdb->prepare( 'language = %s', $lang_filter['language'] );
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $table sanitized via sanitize_identifier(); language value pre-bound through prepare().
        $avg = $wpdb->get_var( "SELECT AVG(lead_score) FROM {$table} WHERE {$where}" );

        return round((float) $avg, 1);
    }

    /**
     * Restrict a SQL identifier to [A-Za-z0-9_] characters.
     *
     * @param string $identifier Raw identifier.
     * @return string Identifier with disallowed characters removed.
     */
    private static function sanitize_identifier( $identifier ) {
        return preg_replace( '/[^A-Za-z0-9_]/', '', (string) $identifier );
    }
}










