This file is a merged representation of the entire codebase, combined into a single document by Repomix.

<file_summary>
This section contains a summary of this file.

<purpose>
This file contains a packed representation of the entire repository's contents.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.
</purpose>

<file_format>
The content is organized as follows:
1. This summary section
2. Repository information
3. Directory structure
4. Repository files (if enabled)
5. Multiple file entries, each consisting of:
  - File path as an attribute
  - Full contents of the file
</file_format>

<usage_guidelines>
- This file should be treated as read-only. Any changes should be made to the
  original repository files, not this packed version.
- When processing this file, use the file path to distinguish
  between different files in the repository.
- Be aware that this file may contain sensitive information. Handle it with
  the same level of security as you would the original repository.
</usage_guidelines>

<notes>
- Some files may have been excluded based on .gitignore rules and Repomix's configuration
- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files
- Files matching patterns in .gitignore are excluded
- Files matching default ignore patterns are excluded
- Files are sorted by Git change count (files with more changes are at the bottom)
</notes>

</file_summary>

<directory_structure>
.github/
  workflows/
    deploy-to-wordpress-svn.yml
    sync-to-gitlab.yml
assets/
  css/
    admin.css
  images/
    banner-1544x500.jpg
    icon.svg
    logo.svg
  js/
    admin.js
includes/
  Admin/
    Settings.php
  Api/
    Controllers/
      CustomPostTypeController.php
      OrderController.php
      PageController.php
      PostController.php
      ProductController.php
      ProductInitController.php
    Middleware/
      AuthMiddleware.php
      RateLimitMiddleware.php
    Routes.php
  Core/
    Activator.php
    Deactivator.php
    Loader.php
    Plugin.php
  Frontend/
    Widget.php
  Models/
    BaseModel.php
    CustomPostType.php
    Page.php
    Post.php
    Product.php
  Traits/
    SecureOrderHandling.php
  Utils/
    Cache.php
    DateTimeHelper.php
    Formatter.php
    ProductChangeTracker.php
languages/
  index.php
  messages.mo
  muchat-ai-fa_IR.mo
  muchat-ai-fa_IR.po
  muchat-ai.pot
templates/
  admin/
    api-documentation.php
    product-example.php
    settings.php
    widget-settings.php
.distignore
.gitignore
index.php
muchat-ai.php
readme.txt
uninstall.php
</directory_structure>

<files>
This section contains the contents of the repository's files.

<file path="includes/Core/Loader.php">
<?php

namespace Muchat\Api\Core;

class Loader
{
    /**
     * Array of action hooks to register
     *
     * @var array
     */
    protected $actions;

    /**
     * Array of filter hooks to register
     *
     * @var array
     */
    protected $filters;

    /**
     * Initialize collections
     */
    public function __construct()
    {
        $this->actions = [];
        $this->filters = [];
    }

    /**
     * Add a new action
     *
     * @param string $hook          The name of the WordPress action
     * @param object $component     A reference to the instance of the object on which the action is defined
     * @param string $callback      The name of the function definition on the $component
     * @param int    $priority      Optional. The priority at which the function should be fired. Default is 10
     * @param int    $accepted_args Optional. The number of arguments that should be passed to the $callback. Default is 1
     */
    public function add_action($hook, $component, $callback, $priority = 10, $accepted_args = 1)
    {
        $this->actions = $this->add($this->actions, $hook, $component, $callback, $priority, $accepted_args);
    }

    /**
     * Add a new filter
     *
     * @param string $hook          The name of the WordPress filter
     * @param object $component     A reference to the instance of the object on which the filter is defined
     * @param string $callback      The name of the function definition on the $component
     * @param int    $priority      Optional. The priority at which the function should be fired. Default is 10
     * @param int    $accepted_args Optional. The number of arguments that should be passed to the $callback. Default is 1
     */
    public function add_filter($hook, $component, $callback, $priority = 10, $accepted_args = 1)
    {
        $this->filters = $this->add($this->filters, $hook, $component, $callback, $priority, $accepted_args);
    }

    /**
     * Helper function to register the actions and hooks into a single collection
     *
     * @param array  $hooks         The collection of hooks that is being registered
     * @param string $hook          The name of the WordPress filter that is being registered
     * @param object $component     A reference to the instance of the object on which the filter is defined
     * @param string $callback      The name of the function definition on the $component
     * @param int    $priority      The priority at which the function should be fired
     * @param int    $accepted_args The number of arguments that should be passed to the $callback
     * @return array The collection of actions and filters registered with WordPress
     */
    private function add($hooks, $hook, $component, $callback, $priority, $accepted_args)
    {
        $hooks[] = [
            'hook'          => $hook,
            'component'     => $component,
            'callback'      => $callback,
            'priority'      => $priority,
            'accepted_args' => $accepted_args
        ];

        return $hooks;
    }

    /**
     * Register all filters and actions with WordPress
     */
    public function run()
    {
        foreach ($this->filters as $hook) {
            add_filter(
                $hook['hook'],
                [$hook['component'], $hook['callback']],
                $hook['priority'],
                $hook['accepted_args']
            );
        }

        foreach ($this->actions as $hook) {
            add_action(
                $hook['hook'],
                [$hook['component'], $hook['callback']],
                $hook['priority'],
                $hook['accepted_args']
            );
        }
    }
}
</file>

<file path=".gitignore">
.DS_Store
codetree.txt
</file>

<file path="index.php">
<?php 
// Silence is golden.
</file>

<file path=".github/workflows/sync-to-gitlab.yml">
name: Sync Repo to GitLab

on:
  push:
    branches:
      - '**'

jobs:
  sync-to-gitlab:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Push to GitLab
        env:
          GITLAB_URL: ${{ secrets.GITLAB_URL }}
          GITLAB_USERNAME: ${{ secrets.GITLAB_USERNAME }}
          GITLAB_PAT: ${{ secrets.GITLAB_PAT }}
        run: |
          echo "Starting sync to GitLab..."

          gitlab_url_without_protocol=${GITLAB_URL#https://}
          gitlab_repo_url_with_credentials="https://${GITLAB_USERNAME}:${GITLAB_PAT}@${gitlab_url_without_protocol}"

          git remote set-url gitlab "$gitlab_repo_url_with_credentials" || git remote add gitlab "$gitlab_repo_url_with_credentials"

          branch_name="${{ github.ref_name }}"
          
          echo "Pushing branch '$branch_name' to GitLab..."

          # git push --force gitlab "$branch_name"
          git push gitlab "$branch_name"

          echo "Sync completed successfully!"
</file>

<file path="assets/images/icon.svg">
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20" height="20" viewBox="0 0 108 108" version="1.1" xmlns="http://www.w3.org/2000/svg">
    <g fill="currentColor" fill-rule="nonzero">
        <g transform="translate(26.7696, 40.8469)">
            <path
                d="M60.1117297,-2.39899859e-24 C60.0207568,4.63855135 58.9879459,9.01488649 57.1588541,13.0059243 C66.8009189,14.6124 66.8790486,22.464973 66.8790486,26.045027 C66.8790486,29.3639351 66.8790486,39.4234054 53.5135135,39.4234054 L40.1222919,39.4234054 C31.4070811,39.4234054 23.6080216,43.6306378 18.7222378,50.1368108 C15.4825297,47.6805405 13.3655351,43.7868973 13.3655351,39.4234054 L13.3655351,26.045027 C13.3655351,23.3993189 13.8353838,21.3155027 14.5674486,19.5977189 C14.1757297,19.5260108 13.7840108,19.3558378 13.3655351,19.3558378 L0.666778378,19.3558378 C0.23652973,21.4268108 2.39899859e-24,23.6412 2.39899859e-24,26.045027 L2.39899859e-24,39.4234054 C2.39899859e-24,54.200627 11.9677622,66.1801622 26.7439135,66.1801622 C26.7439135,58.7856649 32.7277946,52.8017838 40.1222919,52.8017838 L53.5135135,52.8017838 C66.8790486,52.8017838 80.257427,45.2767135 80.257427,26.045027 C80.257427,9.68166486 70.9557081,2.09772973 60.1117297,-2.39899859e-24 Z" />
        </g>
        <path
            d="M53.5135135,13.3783784 C58.5041838,13.3783784 66.8918919,15.115427 66.8918919,26.7567568 L66.8918919,40.1351351 C66.8918919,44.5125405 64.7748973,48.3986919 61.5351892,50.8421189 C56.6494054,44.3487892 48.8621189,40.1351351 40.1351351,40.1351351 L26.7696,40.1351351 C13.3783784,40.1351351 13.3783784,30.0692432 13.3783784,26.7567568 C13.3783784,22.7592973 13.3783784,13.3783784 26.7696,13.3783784 L53.5135135,13.3783784 M53.5135135,0 L26.7696,0 C13.3783784,0 0,7.10766486 0,26.7567568 C0,45.9884432 13.3783784,53.5135135 26.7696,53.5135135 L40.1351351,53.5135135 C47.5296324,53.5135135 53.5135135,59.5113081 53.5135135,66.8918919 C68.2907351,66.8918919 80.2831135,54.9123568 80.2831135,40.1351351 L80.2831135,26.7567568 C80.2831135,8.28924324 66.8918919,0 53.5135135,0 L53.5135135,0 Z" />
    </g>
</svg>
</file>

<file path="assets/images/logo.svg">
<?xml version="1.0" encoding="UTF-8"?>
<svg width="108px" height="108px" viewBox="0 0 108 108" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>SvgjsG5864</title>
    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="logo-type.b2b1067a-(1)" fill="#155DEE" fill-rule="nonzero">
            <g id="SvgjsG5864" transform="translate(0, 0)">
                <g id="Group" transform="translate(26.7696, 40.8469)">
                    <path d="M60.1117297,-2.39899859e-24 C60.0207568,4.63855135 58.9879459,9.01488649 57.1588541,13.0059243 C66.8009189,14.6124 66.8790486,22.464973 66.8790486,26.045027 C66.8790486,29.3639351 66.8790486,39.4234054 53.5135135,39.4234054 L40.1222919,39.4234054 C31.4070811,39.4234054 23.6080216,43.6306378 18.7222378,50.1368108 C15.4825297,47.6805405 13.3655351,43.7868973 13.3655351,39.4234054 L13.3655351,26.045027 C13.3655351,23.3993189 13.8353838,21.3155027 14.5674486,19.5977189 C14.1757297,19.5260108 13.7840108,19.3558378 13.3655351,19.3558378 L0.666778378,19.3558378 C0.23652973,21.4268108 2.39899859e-24,23.6412 2.39899859e-24,26.045027 L2.39899859e-24,39.4234054 C2.39899859e-24,54.200627 11.9677622,66.1801622 26.7439135,66.1801622 C26.7439135,58.7856649 32.7277946,52.8017838 40.1222919,52.8017838 L53.5135135,52.8017838 C66.8790486,52.8017838 80.257427,45.2767135 80.257427,26.045027 C80.257427,9.68166486 70.9557081,2.09772973 60.1117297,-2.39899859e-24 Z" id="Path"></path>
                </g>
                <path d="M53.5135135,13.3783784 C58.5041838,13.3783784 66.8918919,15.115427 66.8918919,26.7567568 L66.8918919,40.1351351 C66.8918919,44.5125405 64.7748973,48.3986919 61.5351892,50.8421189 C56.6494054,44.3487892 48.8621189,40.1351351 40.1351351,40.1351351 L26.7696,40.1351351 C13.3783784,40.1351351 13.3783784,30.0692432 13.3783784,26.7567568 C13.3783784,22.7592973 13.3783784,13.3783784 26.7696,13.3783784 L53.5135135,13.3783784 M53.5135135,0 L26.7696,0 C13.3783784,0 0,7.10766486 0,26.7567568 C0,45.9884432 13.3783784,53.5135135 26.7696,53.5135135 L40.1351351,53.5135135 C47.5296324,53.5135135 53.5135135,59.5113081 53.5135135,66.8918919 C68.2907351,66.8918919 80.2831135,54.9123568 80.2831135,40.1351351 L80.2831135,26.7567568 C80.2831135,8.28924324 66.8918919,0 53.5135135,0 L53.5135135,0 Z" id="Shape"></path>
            </g>
        </g>
    </g>
</svg>
</file>

<file path="includes/Api/Controllers/CustomPostTypeController.php">
<?php

namespace Muchat\Api\Api\Controllers;

use Muchat\Api\Models\CustomPostType;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

class CustomPostTypeController
{
    /**
     * @var CustomPostType
     */
    private $custom_post_type_model;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->custom_post_type_model = new CustomPostType();
    }

    /**
     * Get custom post type items
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response|WP_Error
     */
    public function get_custom_post_type_items($request)
    {
        try {
            // Get post type from route parameter
            $post_type = $request->get_param('post_type');
            
            if (empty($post_type)) {
                return new WP_Error(
                    'missing_post_type',
                    __('Post type parameter is required', 'muchat-ai'),
                    ['status' => 400]
                );
            }

            // Get required parameters
            $modified_after = $request->get_param('modified_after');
            $order_by = $request->get_param('order_by');
            $order = $request->get_param('order');

            // Get skip and take parameters
            $skip = $request->get_param('skip') !== null ? intval($request->get_param('skip')) : 0;
            $take = $request->get_param('take') !== null ? intval($request->get_param('take')) : 30;

            // Prepare parameters for the model
            $params = [
                'modified_after' => $modified_after,
                'order_by' => $order_by,
                'order' => $order,
                'skip' => $skip,
                'take' => $take
            ];

            // Start output buffering to catch any unexpected output
            ob_start();
            $result = $this->custom_post_type_model->get_custom_post_type_items($params, $post_type);
            $unexpected_output = ob_get_clean();

            // If there was unexpected output, log it for debugging
            if (!empty($unexpected_output) && function_exists('error_log')) {
                error_log('Unexpected output in Custom Post Type API: ' . $unexpected_output);
            }

            // Check if there's an error in the result
            if (isset($result['error'])) {
                return new WP_Error(
                    'invalid_post_type',
                    $result['error'],
                    ['status' => 400]
                );
            }

            // Create proper response
            $response = new WP_REST_Response($result, 200);
            $response->header('Cache-Control', 'no-cache, no-store, must-revalidate');
            $response->header('Pragma', 'no-cache');
            $response->header('Expires', '0');

            return $response;
        } catch (\Exception $e) {
            // Log the error for debugging
            if (function_exists('error_log')) {
                error_log('Error in Custom Post Type API: ' . $e->getMessage());
            }

            return new WP_Error(
                'custom_post_type_fetch_error',
                $e->getMessage(),
                ['status' => 500]
            );
        }
    }
}
</file>

<file path="includes/Api/Controllers/PageController.php">
<?php

namespace Muchat\Api\Api\Controllers;

use Muchat\Api\Models\Page;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

class PageController
{
    /**
     * @var Page
     */
    private $page_model;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->page_model = new Page();
    }

    /**
     * Get pages
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response|WP_Error
     */
    public function get_pages($request)
    {
        try {
            // Get required parameters
            $modified_after = $request->get_param('modified_after');
            $order_by = $request->get_param('order_by');
            $order = $request->get_param('order');

            // Get skip and take parameters
            $skip = $request->get_param('skip') !== null ? intval($request->get_param('skip')) : 0;
            $take = $request->get_param('take') !== null ? intval($request->get_param('take')) : 30;

            // Prepare parameters for the model
            $params = [
                'modified_after' => $modified_after,
                'order_by' => $order_by,
                'order' => $order,
                'skip' => $skip,
                'take' => $take
            ];

            // Start output buffering to catch any unexpected output
            ob_start();
            $result = $this->page_model->get_pages($params);
            $unexpected_output = ob_get_clean();

            // If there was unexpected output, log it for debugging
            if (!empty($unexpected_output) && function_exists('error_log')) {
                error_log('Unexpected output in Pages API: ' . $unexpected_output);
            }

            // Create proper response
            $response = new WP_REST_Response($result, 200);
            $response->header('Cache-Control', 'no-cache, no-store, must-revalidate');
            $response->header('Pragma', 'no-cache');
            $response->header('Expires', '0');

            return $response;
        } catch (\Exception $e) {
            // Log the error for debugging
            if (function_exists('error_log')) {
                error_log('Error in Pages API: ' . $e->getMessage());
            }

            return new WP_Error(
                'pages_fetch_error',
                $e->getMessage(),
                ['status' => 500]
            );
        }
    }
}
</file>

<file path="includes/Api/Controllers/ProductInitController.php">
<?php

namespace Muchat\Api\Api\Controllers;

use Muchat\Api\Utils\ProductChangeTracker;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

/**
 * Controller for initializing muchat_date_modified for existing products
 * This should be run once after activating the feature
 */
class ProductInitController
{
    /**
     * Initialize muchat_date_modified for existing products
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response|WP_Error
     */
    public function initialize_products($request)
    {
        try {
            // Check if WooCommerce is active
            if (!class_exists('WooCommerce')) {
                return new WP_Error(
                    'woocommerce_required',
                    __('WooCommerce is required for this functionality', 'muchat-ai'),
                    ['status' => 500]
                );
            }

            // Get batch size (default 50)
            $batch_size = $request->get_param('batch_size') ?: 50;
            
            // Run initialization
            $result = ProductChangeTracker::initialize_existing_products($batch_size);

            return new WP_REST_Response([
                'success' => true,
                'message' => sprintf(
                    'Initialized %d products. %d remaining.',
                    $result['processed'],
                    $result['remaining']
                ),
                'data' => $result
            ], 200);
        } catch (\Exception $e) {
            return new WP_Error(
                'initialization_error',
                $e->getMessage(),
                ['status' => 500]
            );
        }
    }
}
</file>

<file path="includes/Traits/SecureOrderHandling.php">
<?php

namespace Muchat\Api\Traits;

/**
 * Trait for secure order parameter handling
 * 
 * SECURITY: Provides strict whitelist validation for orderby parameters
 * to prevent SQL injection attacks.
 * 
 * Apply to all model classes that handle user-provided order parameters.
 */
trait SecureOrderHandling
{
    /**
     * SECURITY: Immutable whitelist of allowed orderby fields
     * Using constants prevents runtime modification attacks
     * 
     * These map to WP_Query orderby values, not raw SQL columns
     */
    private const ORDERBY_WHITELIST = [
        'date' => 'date',
        'modified' => 'modified',
        'title' => 'title',
        'ID' => 'ID',
        'id' => 'ID',
        'name' => 'name',
        'author' => 'author',
        'menu_order' => 'menu_order',
    ];

    /**
     * SECURITY: Immutable whitelist of allowed sort directions
     */
    private const DIRECTION_WHITELIST = ['ASC', 'DESC'];

    /**
     * Sanitize and validate orderby parameter
     * 
     * SECURITY: Only allows whitelisted field names to prevent SQL injection
     * 
     * @param string|null $order_by Raw order_by parameter (can be comma-separated)
     * @param string|null $order Raw order direction
     * @return array Safe orderby array for WP_Query
     */
    protected function sanitize_orderby(?string $order_by, ?string $order = 'ASC'): array
    {
        // Immutable validation for direction
        $order = strtoupper(trim($order ?? 'ASC'));
        if (!in_array($order, self::DIRECTION_WHITELIST, true)) {
            $order = 'ASC';
        }

        // Default ordering if no order_by provided
        if (empty($order_by)) {
            return ['modified' => $order, 'ID' => 'ASC'];
        }

        // Parse comma-separated fields
        $fields = array_map('trim', explode(',', $order_by));
        $orderby_array = [];

        foreach ($fields as $field) {
            // Case-insensitive lookup but output uses canonical case
            $field_key = strtolower($field);

            // SECURITY: Strict whitelist check using the immutable constant
            foreach (self::ORDERBY_WHITELIST as $allowed => $canonical) {
                if (strtolower($allowed) === $field_key) {
                    $orderby_array[$canonical] = $order;
                    break;
                }
            }
            // Silently ignore non-whitelisted fields for security
        }

        // Ensure we have at least one valid field
        if (empty($orderby_array)) {
            return ['modified' => $order, 'ID' => 'ASC'];
        }

        // Always add ID as secondary sort for deterministic pagination
        if (!isset($orderby_array['ID'])) {
            $orderby_array['ID'] = 'ASC';
        }

        return $orderby_array;
    }
}
</file>

<file path="includes/Utils/DateTimeHelper.php">
<?php

namespace Muchat\Api\Utils;

/**
 * Centralized date/time handling for consistent timezone management
 * 
 * This class ensures all dates are handled consistently across the plugin:
 * - Input dates are parsed and converted to UTC
 * - Output dates are formatted in ISO 8601 format
 * - Database queries use GMT columns
 *
 * @package Muchat\Api\Utils
 */
class DateTimeHelper
{
    /**
     * Parse a date string and return UTC DateTime
     * 
     * Accepts various formats and always returns UTC
     * 
     * @param string $date_string Input date string
     * @return \DateTime|null DateTime in UTC or null if parsing fails
     */
    public static function parse_to_utc($date_string)
    {
        if (empty($date_string)) {
            return null;
        }

        try {
            // Try parsing as ISO 8601 first (most precise)
            if (preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/', $date_string)) {
                $dt = new \DateTime($date_string);
                $dt->setTimezone(new \DateTimeZone('UTC'));
                return $dt;
            }

            // Try parsing as standard MySQL format
            if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $date_string)) {
                $dt = new \DateTime($date_string, new \DateTimeZone('UTC'));
                return $dt;
            }

            // Try parsing as UTC explicitly
            $dt = new \DateTime($date_string, new \DateTimeZone('UTC'));
            return $dt;
        } catch (\Exception $e) {
            // Fallback to strtotime
            $timestamp = strtotime($date_string);
            if ($timestamp === false) {
                return null;
            }

            $dt = new \DateTime('@' . $timestamp);
            $dt->setTimezone(new \DateTimeZone('UTC'));
            return $dt;
        }
    }

    /**
     * Format a date for API output (always ISO 8601 UTC)
     * 
     * @param \DateTime|string|int $date DateTime object, string, or timestamp
     * @return string|null ISO 8601 formatted date or null
     */
    public static function format_for_api($date)
    {
        if (empty($date)) {
            return null;
        }

        try {
            if ($date instanceof \DateTime) {
                $dt = clone $date;
            } elseif (is_numeric($date)) {
                $dt = new \DateTime('@' . $date);
            } else {
                $dt = new \DateTime($date);
            }

            $dt->setTimezone(new \DateTimeZone('UTC'));

            // ISO 8601 with milliseconds
            return $dt->format('Y-m-d\TH:i:s.v\Z');
        } catch (\Exception $e) {
            return null;
        }
    }

    /**
     * Format for MySQL query (always GMT)
     * 
     * @param \DateTime|string $date Date to format
     * @return string MySQL formatted datetime in GMT
     */
    public static function format_for_mysql_gmt($date)
    {
        $dt = self::parse_to_utc($date);
        if (!$dt) {
            return current_time('mysql', true); // Fallback to current GMT
        }

        return $dt->format('Y-m-d H:i:s');
    }

    /**
     * Get date for database column comparison
     * 
     * @param string $date_string User-provided date string
     * @param string $column Database column ('post_modified' or 'post_modified_gmt')
     * @return array Date query array for WP_Query
     */
    public static function get_date_query_after($date_string, $column = 'post_modified_gmt')
    {
        $dt = self::parse_to_utc($date_string);

        if (!$dt) {
            return [];
        }

        return [
            'column' => $column,
            'after' => $dt->format('Y-m-d H:i:s'),
            'inclusive' => false,
        ];
    }

    /**
     * Format a WooCommerce date object for API output
     * 
     * @param \WC_DateTime|null $date_object WooCommerce DateTime object
     * @return string|null ISO 8601 formatted date or null
     */
    public static function format_wc_date($date_object)
    {
        if (!$date_object) {
            return null;
        }

        try {
            $dt = new \DateTime('@' . $date_object->getTimestamp());
            $dt->setTimezone(new \DateTimeZone('UTC'));
            return $dt->format('Y-m-d\TH:i:s.v\Z');
        } catch (\Exception $e) {
            return null;
        }
    }

    /**
     * Get current UTC time in various formats
     * 
     * @param string $format Output format ('mysql', 'iso8601', 'timestamp')
     * @return string|int Current time in requested format
     */
    public static function now($format = 'mysql')
    {
        $dt = new \DateTime('now', new \DateTimeZone('UTC'));

        switch ($format) {
            case 'mysql':
                return $dt->format('Y-m-d H:i:s');
            case 'iso8601':
                return $dt->format('Y-m-d\TH:i:s.v\Z');
            case 'timestamp':
                return $dt->getTimestamp();
            default:
                return $dt->format($format);
        }
    }
}
</file>

<file path="includes/Utils/Formatter.php">
<?php

namespace Muchat\Api\Utils;

use \DOMDocument;
use \DOMXPath;

class Formatter
{
    /**
     * Lightweight content cleaning for list views
     * 
     * PERFORMANCE: Uses simple string operations instead of DOMDocument.
     * For list views (e.g., 30 products per page), DOMDocument is too heavy.
     * This method is O(n) on string length, not O(n*m) like DOM parsing.
     * 
     * @param string $content The content to clean
     * @param int $word_limit Maximum number of words to return (default: 30)
     * @return string Cleaned, trimmed content
     */
    public function clean_html_content_light($content, $word_limit = 30)
    {
        if (empty($content)) {
            return '';
        }
        
        // Render shortcodes if present (like WPBakery)
        if (has_shortcode($content, 'vc_') || strpos($content, '[vc_') !== false) {
            $content = $this->render_vc_shortcodes($content);
        } else {
            $content = do_shortcode($content);
        }
        
        // Strip all HTML tags
        $content = wp_strip_all_tags($content);
        
        // Decode HTML entities
        $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        
        // Replace special whitespace characters
        $special_chars = [
            '&nbsp;', '&#160;', 
            chr(194) . chr(160), // NO-BREAK SPACE
            chr(226) . chr(128) . chr(168), // LINE SEPARATOR
        ];
        $content = str_replace($special_chars, ' ', $content);
        
        // Normalize whitespace
        $content = preg_replace('/\s+/', ' ', $content);
        $content = trim($content);
        
        // Trim to word limit
        if ($word_limit > 0) {
            $content = wp_trim_words($content, $word_limit, '...');
        }
        
        return $content;
    }
    
    /**
     * Remove empty values from array
     *
     * @param array $array
     * @return array
     */
    public function remove_empty_values($array)
    {
        foreach ($array as $key => $value) {
            if ($key === 'price') {
                continue;
            }

            if (is_array($value)) {
                $array[$key] = $this->remove_empty_values($value);
                if (empty($array[$key])) {
                    unset($array[$key]);
                }
            } elseif ($value === "" || $value === null) {
                unset($array[$key]);
            }
        }
        return $array;
    }

    private function render_vc_shortcodes($content)
    {

        if (!file_exists(WP_PLUGIN_DIR . '/js_composer/js_composer.php')) {
            return do_shortcode($content);
        }

        try {

            if (!class_exists('\WPBMap')) {
                include_once(WP_PLUGIN_DIR . '/js_composer/js_composer.php');
            }


            if (class_exists('\Vc_Manager')) {
                $vc = \Vc_Manager::getInstance();
                $vc->init();
            }


            if (class_exists('\WPBMap')) {
                \WPBMap::addAllMappedShortcodes();
            }


            $content = apply_filters('vc_shortcode_content_filter', $content);
            $content = do_shortcode($content);
        } catch (\Exception $e) {

            $content = do_shortcode($content);
        }

        return $content;
    }

    public function clean_html_content($content)
    {
        if (empty($content)) {
            return '';
        }

        // 1. Render WPBakery shortcodes first
        $content = $this->render_vc_shortcodes($content);

        // 2. Initialize DOMDocument
        $dom = new DOMDocument('1.0', 'UTF-8');
        $dom->encoding = 'UTF-8';
        libxml_use_internal_errors(true);

        // Add XML encoding declaration
        $content = '<?xml encoding="UTF-8">' . $content;

        // Load HTML with proper encoding
        $dom->loadHTML(
            htmlspecialchars_decode(htmlentities($content, ENT_QUOTES, 'UTF-8'), ENT_QUOTES),
            LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD | LIBXML_NOWARNING
        );
        libxml_clear_errors();

        // 3. Create XPath object
        $xpath = new DOMXPath($dom);

        // 4. Remove all attributes except src and href
        $elements = $xpath->query('//*[@*]');
        foreach ($elements as $element) {
            $attributes = $element->attributes;
            $attributesToRemove = [];

            foreach ($attributes as $attribute) {
                if (!in_array($attribute->name, ['src', 'href'])) {
                    $attributesToRemove[] = $attribute->name;
                }
            }

            foreach ($attributesToRemove as $attributeName) {
                $element->removeAttribute($attributeName);
            }
        }

        // 5. Get initial clean HTML
        $cleanHtml = $dom->saveHTML();
        $cleanHtml = htmlspecialchars_decode(htmlentities($cleanHtml, ENT_QUOTES, 'UTF-8'), ENT_QUOTES);

        // 6. Remove DOCTYPE and html/head/body tags
        $cleanHtml = preg_replace('/<!(DOCTYPE|doctype).*?>/', '', $cleanHtml);
        $cleanHtml = preg_replace('/<(html|body|head).*?>|<\/(html|body|head)>/', '', $cleanHtml);

        // 7. Decode HTML entities
        $cleanHtml = html_entity_decode($cleanHtml, ENT_QUOTES | ENT_HTML5, 'UTF-8');

        // 11. Replace special characters
        $special_chars = [
            '&nbsp;',
            '&#160;',
            '&amp;',
            '&quot;',
            '&apos;',
            '&lt;',
            '&gt;',
            chr(194) . chr(160),
            chr(226) . chr(128) . chr(168),
            chr(226) . chr(128) . chr(169),
            chr(226) . chr(128) . chr(170),
            chr(226) . chr(128) . chr(175),
            chr(226) . chr(128) . chr(176),
            chr(226) . chr(128) . chr(177)
        ];
        $cleanHtml = str_replace($special_chars, ' ', $cleanHtml);

        // 8. Comprehensive whitespace and formatting cleanup patterns
        $whitespace_cleanup = [
            // Basic whitespace normalization
            '/[\r\n\t]+/' => ' ',
            '/[ \t]+/' => ' ',
            '/\h+/' => ' ',

            // Spacing around inline elements
            '/<\/a>([^\s])/' => '</a> $1',    // Space after links
            '/<img([^>]*)>([^\s])/' => '<img$1> $2',  // Space after images
            '/([^\s])<(a|img)/' => '$1 <$2',  // Space before links/images

            // Headers formatting
            '/(<h[1-6]>.*?<\/h[1-6]>)/i' => "\n\n$1\n\n",  // Space around headers
            '/(<h[1-6]>)\s*|\s*(<\/h[1-6]>)/i' => '$1$2',  // Clean inside headers

            // List formatting
            '/<ul>(.*?)<\/ul>/is' => "\n<ul>\n$1\n</ul>\n",
            '/<li>(.*?)<\/li>/is' => "• $1\n",

            // Paragraph handling
            '/<p>\s*(.*?)\s*<\/p>/is' => "$1\n\n",  // Convert paragraphs to double line breaks
            '/\n{3,}/' => "\n\n",    // Maximum two consecutive line breaks

            // Clean spacing around HTML elements
            '/>\s+</' => '><',
            '/\s*(<br[^>]*>)\s*/' => "\n",
            '/\s*(<\/?(?:p|div)[^>]*>)\s*/' => "\n",

            // Final cleanup
            '/[ \t]+(\n|\r\n?)/' => '$1',
            '/^[ \t]+/m' => '',
            '/[ \t]+$/' => '',
            '/\s+(\p{P})/' => '$1',
            '/(\p{P})\s+/' => '$1 '
        ];

        // 9. Convert HTML attributes to single quotes
        $cleanHtml = preg_replace_callback(
            '/<([^>]+)>/i',
            function ($matches) {
                return '<' . preg_replace('/="([^"]*)"/', "='$1'", $matches[1]) . '>';
            },
            $cleanHtml
        );

        // 10. Apply whitespace cleanup
        foreach ($whitespace_cleanup as $pattern => $replacement) {
            $cleanHtml = preg_replace($pattern, $replacement, $cleanHtml);
        }

        // 12. Define and apply WordPress security filter
        $allowed_html = [
            'a' => ['href' => []],
            'img' => ['src' => []],
            'h1' => [],
            'h2' => [],
            'h3' => [],
            'ul' => [],
            'li' => []
        ];
        $cleanHtml = wp_kses($cleanHtml, $allowed_html);

        // 13. Clean up links
        $cleanHtml = preg_replace_callback(
            '/<a[^>]*href=[\'"](.*?)[\'"][^>]*>(.*?)<\/a>/i',
            function ($matches) {
                $url = trim($matches[1]);
                $text = trim($matches[2]);

                // If the link content is an img tag, return only the img
                if (preg_match('/<img[^>]+>/i', $text)) {
                    return " {$text} ";
                }

                // If URL or text is empty, return only the text content
                if (empty($url) || empty($text)) return " {$text} ";

                // Check if URL starts with http or https
                if (preg_match('/^https?:\/\//i', $url)) {
                    // Validate URL format
                    if (!filter_var($url, FILTER_VALIDATE_URL)) return " {$text} ";

                    // Check for disallowed protocols
                    $disallowed = ['javascript:', 'data:', 'vbscript:', 'file:', '#'];
                    foreach ($disallowed as $protocol) {
                        if (stripos($url, $protocol) !== false) return " {$text} ";
                    }

                    // Return complete link for valid absolute URLs
                    return sprintf("<a href='%s'>%s</a>", urldecode(esc_url($url)), $text);
                }

                // For relative links, return only the text content
                return " {$text} ";
            },
            $cleanHtml
        );


        // 14. Clean up images
        $cleanHtml = preg_replace_callback(
            '/<img[^>]*>/i',
            function ($matches) {
                if (preg_match('/src=([\'"])((?:(?!\1).)*)\1/', $matches[0], $src_matches)) {
                    $src = $src_matches[2];

                    if (preg_match('/\.svg$/i', $src)) return '';

                    if ((filter_var($src, FILTER_VALIDATE_URL) || strpos($src, '/') === 0) &&
                        !preg_match('/^(javascript:|data:|vbscript:)/i', $src)
                    ) {
                        return sprintf("<img src='%s'>", urldecode(esc_url($src)));
                    }
                }
                return '';
            },
            $cleanHtml
        );

        // 15. Remove unnecessary elements
        $cleanHtml = preg_replace([
            '/<!--(.|\s)*?-->/',
            '/<link[^>]*>/i',
            '/<style\s*>[\s\S]*?<\/style>/im',  // Alternative pattern
            '/<style\s[^>]*>[\s\S]*?<\/style>/im',  // Alternative pattern with attributes
            '/<script\b[^>]*>(.*?)<\/script>/is',
            '/<iframe[^>]*>.*?<\/iframe>/is',
            '/\s+style\s*=\s*"[^"]*"/',
            '/<svg[^>]*>.*?<\/svg>/is',
        ], '', $cleanHtml);


        // 16. Final normalization
        $cleanHtml = preg_replace('/\s+/', ' ', $cleanHtml);          // Normalize spaces
        $cleanHtml = preg_replace('/\n\s+/', "\n", $cleanHtml);       // Clean line starts
        $cleanHtml = preg_replace('/\s+\n/', "\n", $cleanHtml);       // Clean line ends
        $cleanHtml = preg_replace('/\n{3,}/', "\n\n", $cleanHtml);    // Max double line breaks


        return trim($cleanHtml);
    }
}
</file>

<file path="languages/index.php">
<?php
// Silence is golden.
</file>

<file path="languages/muchat-ai-fa_IR.po">
# Muchat AI Chatbot - Persian (Farsi) Translation
# Copyright (C) 2026 Muchat Team
# This file is distributed under the same license as the Muchat AI Chatbot package.
# Muchat Team <support@mu.chat>, 2026.
#
msgid ""
msgstr ""
"Project-Id-Version: Muchat AI Chatbot 2.0.52\n"
"Report-Msgid-Bugs-To: support@mu.chat\n"
"POT-Creation-Date: 2026-02-25 09:34+0330\n"
"PO-Revision-Date: 2026-02-25 09:34+0330\n"
"Last-Translator: Muchat Team <support@mu.chat>\n"
"Language-Team: Persian <support@mu.chat>\n"
"Language: fa_IR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Muchat Translation Tool 1.0\n"

#: includes/Core/Plugin.php:126
msgid "Settings"
msgstr "تنظیمات"

#: includes/Core/Activator.php:39 includes/Core/Activator.php:57
#: includes/Core/Activator.php:68 includes/Admin/Settings.php:884
#: includes/Admin/Settings.php:902 includes/Admin/Settings.php:913
#: includes/Admin/Settings.php:966
msgid "ID"
msgstr "شناسه"

#: includes/Core/Activator.php:40 includes/Admin/Settings.php:885
msgid "Name"
msgstr "نام"

#: includes/Core/Activator.php:41 includes/Core/Activator.php:62
#: includes/Core/Activator.php:73 includes/Admin/Settings.php:886
#: includes/Admin/Settings.php:907 includes/Admin/Settings.php:918
#: includes/Admin/Settings.php:971
msgid "Permalink"
msgstr "پیوند یکتا"

#: includes/Core/Activator.php:42 includes/Admin/Settings.php:887
msgid "Date Modified"
msgstr "تاریخ ویرایش"

#: includes/Core/Activator.php:43 includes/Admin/Settings.php:888
#: templates/admin/api-documentation.php:64
#: templates/admin/api-documentation.php:140
#: templates/admin/api-documentation.php:512
#: templates/admin/api-documentation.php:574
#: templates/admin/api-documentation.php:618
#: templates/admin/api-documentation.php:659
msgid "Description"
msgstr "توضیحات"

#: includes/Core/Activator.php:44 includes/Admin/Settings.php:889
msgid "Short Description"
msgstr "توضیح کوتاه"

#: includes/Core/Activator.php:45 includes/Admin/Settings.php:890
msgid "Currency"
msgstr "واحد پول"

#: includes/Core/Activator.php:46 includes/Admin/Settings.php:891
msgid "Price"
msgstr "قیمت"

#: includes/Core/Activator.php:47 includes/Admin/Settings.php:892
msgid "Regular Price"
msgstr "قیمت عادی"

#: includes/Core/Activator.php:48 includes/Admin/Settings.php:893
msgid "Sale Price"
msgstr "قیمت حراج"

#: includes/Core/Activator.php:49 includes/Admin/Settings.php:894
msgid "Stock Status"
msgstr "وضعیت موجودی"

#: includes/Core/Activator.php:50 includes/Core/Activator.php:63
#: includes/Admin/Settings.php:895 includes/Admin/Settings.php:908
msgid "Categories"
msgstr "دسته‌بندی‌ها"

#: includes/Core/Activator.php:51 includes/Core/Activator.php:64
#: includes/Admin/Settings.php:896 includes/Admin/Settings.php:909
msgid "Tags"
msgstr "برچسب‌ها"

#: includes/Core/Activator.php:52 includes/Admin/Settings.php:897
msgid "Images"
msgstr "تصاویر"

#: includes/Core/Activator.php:53 includes/Admin/Settings.php:898
msgid "Attributes"
msgstr "ویژگی‌ها"

#: includes/Core/Activator.php:54 includes/Admin/Settings.php:899
msgid "Variations"
msgstr "تنوع‌ها"

#: includes/Core/Activator.php:58 includes/Core/Activator.php:69
#: includes/Admin/Settings.php:903 includes/Admin/Settings.php:914
#: includes/Admin/Settings.php:967
msgid "Title"
msgstr "عنوان"

#: includes/Core/Activator.php:59 includes/Core/Activator.php:70
#: includes/Admin/Settings.php:904 includes/Admin/Settings.php:915
#: includes/Admin/Settings.php:968
msgid "Content"
msgstr "محتوا"

#: includes/Core/Activator.php:60 includes/Core/Activator.php:71
#: includes/Admin/Settings.php:905 includes/Admin/Settings.php:916
#: includes/Admin/Settings.php:969
msgid "Excerpt"
msgstr "چکیده"

#: includes/Core/Activator.php:61 includes/Core/Activator.php:72
#: includes/Admin/Settings.php:906 includes/Admin/Settings.php:917
#: includes/Admin/Settings.php:970
msgid "Modified Date"
msgstr "تاریخ ویرایش"

#: includes/Core/Activator.php:65 includes/Core/Activator.php:74
#: includes/Admin/Settings.php:910 includes/Admin/Settings.php:919
#: includes/Admin/Settings.php:972
msgid "Featured Image"
msgstr "تصویر شاخص"

#. translators: %s: Alternative filter name
#: includes/Frontend/Widget.php:780
#, php-format
msgid ""
"The muchat_widget_output filter has been removed for security reasons. Use "
"%s instead for configuration changes."
msgstr ""
"فیلتر muchat_widget_output به دلایل امنیتی حذف شده است. به‌جای آن از "
"%s برای تغییرات پیکربندی استفاده کنید."

#: includes/Admin/Settings.php:177 includes/Admin/Settings.php:184
msgid "Copied!"
msgstr "کپی شد!"

#: includes/Admin/Settings.php:178
msgid "Failed!"
msgstr "ناموفق!"

#: includes/Admin/Settings.php:204
msgid "Muchat Settings"
msgstr "تنظیمات موچت"

#: includes/Admin/Settings.php:205
msgid "Muchat"
msgstr "موچت"

#: includes/Admin/Settings.php:215 includes/Admin/Settings.php:216
msgid "Widget Settings"
msgstr "تنظیمات ویجت"

#: includes/Admin/Settings.php:226 includes/Admin/Settings.php:227
msgid "WooCommerce API"
msgstr "API ووکامرس"

#: includes/Admin/Settings.php:236 includes/Admin/Settings.php:237
#: templates/admin/product-example.php:23
msgid "Product Example"
msgstr "نمونه محصول"

#: includes/Admin/Settings.php:251 includes/Admin/Settings.php:252
#: templates/admin/api-documentation.php:10
msgid "API Documentation"
msgstr "مستندات API"

#: includes/Admin/Settings.php:292
msgid "Security verification failed. Please try again."
msgstr "تأیید امنیتی ناموفق بود. لطفاً دوباره تلاش کنید."

#: includes/Admin/Settings.php:420
msgid "Could not establish a connection to Muchat servers."
msgstr "امکان برقراری ارتباط با سرورهای موچت وجود ندارد."

#. translators: 1: The server IP address, 2: The specific error message from WordPress.
#: includes/Admin/Settings.php:431
#, php-format
msgid "Your server (IP: %1$s) cannot connect to Muchat. Error: %2$s"
msgstr "سرور شما (IP: %1$s) نمی‌تواند به موچت متصل شود. خطا: %2$s"

#. translators: %s: The server IP address.
#: includes/Admin/Settings.php:445
#, php-format
msgid "Connection from your server (IP: %s) to Muchat is successful."
msgstr "اتصال از سرور شما (IP: %s) به موچت با موفقیت برقرار شد."

#. translators: 1: The server IP address, 2: The HTTP status code from the server.
#: includes/Admin/Settings.php:453
#, php-format
msgid ""
"Your server (IP: %1$s) connected to Muchat, but received an error. Status "
"code: %2$d"
msgstr ""
"سرور شما (IP: %1$s) به موچت متصل شد، اما خطایی دریافت کرد. کد وضعیت: %2$d"

#: includes/Admin/Settings.php:489 includes/Admin/Settings.php:863
#: includes/Api/Controllers/OrderController.php:62
#: includes/Api/Controllers/ProductController.php:43
#: includes/Api/Controllers/ProductController.php:119
#: includes/Api/Controllers/ProductController.php:185
#: includes/Api/Controllers/ProductInitController.php:29
msgid "WooCommerce is required for this functionality"
msgstr "برای این قابلیت، ووکامرس الزامی است"

#: includes/Admin/Settings.php:572
msgid "API field settings updated."
msgstr "تنظیمات فیلدهای API به‌روز شد."

#: includes/Admin/Settings.php:609
msgid ""
"Analyzing product meta fields in the background. This may take a few moments "
"for large catalogs."
msgstr ""
"در حال تحلیل فیلدهای متا محصول در پس‌زمینه. این ممکن است برای کاتالوگ‌های بزرگ چند لحظه طول بکشد."

#: includes/Admin/Settings.php:613
msgid "Refresh Page"
msgstr "بارگذاری مجدد صفحه"

#: includes/Admin/Settings.php:634
msgid ""
"Select which product meta fields you want to include in the API response."
msgstr "انتخاب کنید کدام فیلدهای متا محصول در پاسخ API گنجانده شوند."

#: includes/Admin/Settings.php:641 templates/admin/api-documentation.php:572
#: templates/admin/api-documentation.php:616
#: templates/admin/api-documentation.php:657
msgid "Enable"
msgstr "فعال‌سازی"

#: includes/Admin/Settings.php:642
msgid "Meta Field"
msgstr "فیلد متا"

#: includes/Admin/Settings.php:643 templates/admin/api-documentation.php:139
msgid "Type"
msgstr "نوع"

#: includes/Admin/Settings.php:644
msgid "Custom Label"
msgstr "برچسب سفارشی"

#: includes/Admin/Settings.php:645
msgid "Sample Values"
msgstr "مقادیر نمونه"

#. translators: %s: number of products where this meta field is used
#: includes/Admin/Settings.php:674
#, php-format
msgid "Used in %s product"
msgid_plural "Used in %s products"
msgstr[0] "استفاده شده در %s محصول"

#: includes/Admin/Settings.php:701
msgid "Product Attribute"
msgstr "ویژگی محصول"

#: includes/Admin/Settings.php:704
msgid "ACF Field"
msgstr "فیلد ACF"

#: includes/Admin/Settings.php:707
msgid "Variation"
msgstr "تنوع"

#: includes/Admin/Settings.php:710
msgid "Product & Variation"
msgstr "محصول و تنوع"

#: includes/Admin/Settings.php:761
msgid "Connection status has been re-checked."
msgstr "وضعیت اتصال مجدداً بررسی شد."

#: includes/Admin/Settings.php:777
msgid "Product meta fields cache has been cleared successfully."
msgstr "کش فیلدهای متا محصول با موفقیت پاک شد."

#: includes/Admin/Settings.php:827 templates/admin/api-documentation.php:565
msgid "Product Fields"
msgstr "فیلدهای محصول"

#: includes/Admin/Settings.php:835 templates/admin/api-documentation.php:609
msgid "Post Fields"
msgstr "فیلدهای نوشته"

#: includes/Admin/Settings.php:843 templates/admin/api-documentation.php:650
msgid "Page Fields"
msgstr "فیلدهای صفحه"

#: includes/Admin/Settings.php:1267 includes/Admin/Settings.php:1372
msgid "Security check failed"
msgstr "بررسی امنیتی ناموفق بود"

#: includes/Admin/Settings.php:1272 includes/Admin/Settings.php:1377
msgid "Permission denied"
msgstr "دسترسی رد شد"

#: includes/Admin/Settings.php:1281
msgid "Query too short"
msgstr "عبارت جستجو خیلی کوتاه است"

#: includes/Admin/Settings.php:1387
msgid "Invalid content type"
msgstr "نوع محتوای نامعتبر"

#: includes/Admin/Settings.php:1391
msgid "Invalid item ID"
msgstr "شناسه آیتم نامعتبر"

#: includes/Admin/Settings.php:1400
msgid "WooCommerce not active"
msgstr "ووکامرس فعال نیست"

#: includes/Admin/Settings.php:1418
msgid "Content not found"
msgstr "محتوا یافت نشد"

#: includes/Models/CustomPostType.php:23
#, php-format
msgid "Invalid post type: %s"
msgstr "نوع نوشته نامعتبر: %s"

#: includes/Api/Middleware/AuthMiddleware.php:46
msgid "Invalid authentication header"
msgstr "هدر احراز هویت نامعتبر"

#: includes/Api/Middleware/AuthMiddleware.php:99
msgid "Authentication service temporarily unavailable. Please try again later."
msgstr "سرویس احراز هویت موقتاً در دسترس نیست. لطفاً بعداً دوباره تلاش کنید."

#: includes/Api/Middleware/AuthMiddleware.php:123
#: includes/Api/Middleware/AuthMiddleware.php:204
msgid "Invalid token"
msgstr "توکن نامعتبر"

#. translators: %s: error message from the API response
#: includes/Api/Middleware/AuthMiddleware.php:185
#, php-format
msgid "Token validation failed: %s"
msgstr "اعتبارسنجی توکن ناموفق بود: %s"

#. translators: %d: number of seconds to wait before retrying
#: includes/Api/Middleware/RateLimitMiddleware.php:103
#, php-format
msgid "Rate limit exceeded. Please try again in %d seconds."
msgstr "محدودیت نرخ درخواست تجاوز شد. لطفاً %d ثانیه دیگر دوباره تلاش کنید."

#: includes/Api/Routes.php:265
msgid "Number of items to skip."
msgstr "تعداد آیتم‌هایی که باید رد شوند."

#: includes/Api/Routes.php:272
msgid "Number of items to take."
msgstr "تعداد آیتم‌هایی که باید دریافت شوند."

#: includes/Api/Routes.php:280
msgid ""
"Sort collection by attribute. Can be a single field or comma-separated list "
"(e.g., \"modified,ID\")."
msgstr ""
"مرتب‌سازی مجموعه بر اساس ویژگی. می‌تواند یک فیلد یا لیست جداشده با کاما باشد "
"(مثلاً: \"modified,ID\")."

#: includes/Api/Routes.php:286
msgid "Order sort attribute ascending or descending."
msgstr "ترتیب مرتب‌سازی صعودی یا نزولی."

#: includes/Api/Routes.php:292
msgid "Limit results to items modified after specified date."
msgstr "محدود کردن نتایج به آیتم‌های ویرایش‌شده پس از تاریخ مشخص."

#: includes/Api/Routes.php:299
msgid ""
"Limit results to items with muchat_date_modified after specified date (only "
"price/stock changes)."
msgstr ""
"محدود کردن نتایج به آیتم‌هایی که muchat_date_modified آن‌ها بعد از تاریخ مشخص است (فقط تغییرات قیمت/موجودی)."

#: includes/Api/Routes.php:306
msgid "Filter to show only in-stock products (product-specific)."
msgstr "فیلتر برای نمایش فقط محصولات موجود در انبار (مخصوص محصول)."

#: includes/Api/Controllers/OrderController.php:91
msgid "Unable to find order with the provided information"
msgstr "یافتن سفارش با اطلاعات ارائه‌شده امکان‌پذیر نیست"

#: includes/Api/Controllers/OrderController.php:140
msgid "Invalid Order ID format"
msgstr "فرمت شناسه سفارش نامعتبر است"

#: includes/Api/Controllers/OrderController.php:151
msgid "Invalid email format"
msgstr "فرمت ایمیل نامعتبر است"

#: includes/Api/Controllers/OrderController.php:163
msgid "Invalid phone number format"
msgstr "فرمت شماره تلفن نامعتبر است"

#: includes/Api/Controllers/OrderController.php:174
msgid "At least one of Order ID, Email or Phone is required"
msgstr "حداقل یکی از موارد شناسه سفارش، ایمیل یا تلفن الزامی است"

#: includes/Api/Controllers/OrderController.php:372
msgid "Order not found"
msgstr "سفارش یافت نشد"

#: includes/Api/Controllers/OrderController.php:381
msgid "Email does not match with order"
msgstr "ایمیل با سفارش مطابقت ندارد"

#: includes/Api/Controllers/OrderController.php:392
msgid "Phone does not match with order"
msgstr "تلفن با سفارش مطابقت ندارد"

#: includes/Api/Controllers/OrderController.php:406
msgid "Email or phone is required when order ID is not provided"
msgstr "هنگامی که شناسه سفارش ارائه نشده، ایمیل یا تلفن الزامی است"

#: includes/Api/Controllers/OrderController.php:434
msgid "No orders found with the provided criteria"
msgstr "هیچ سفارشی با معیارهای ارائه‌شده یافت نشد"

#: includes/Api/Controllers/OrderController.php:455
msgid "No valid orders found"
msgstr "هیچ سفارش معتبری یافت نشد"

#: includes/Api/Controllers/CustomPostTypeController.php:45
msgid "Post type parameter is required"
msgstr "پارامتر نوع نوشته الزامی است"

#: includes/Api/Controllers/ProductController.php:129
msgid "Invalid product ID"
msgstr "شناسه محصول نامعتبر است"

#: includes/Api/Controllers/ProductController.php:147
msgid "Product not found"
msgstr "محصول یافت نشد"

#: templates/admin/api-documentation.php:18
msgid ""
"This page provides detailed documentation on how to use the Muchat API "
"endpoints."
msgstr ""
"این صفحه مستندات دقیقی درباره نحوه استفاده از نقاط پایانی API موچت ارائه می‌دهد."

#: templates/admin/api-documentation.php:26
msgid "Base URL"
msgstr "آدرس پایه"

#: templates/admin/api-documentation.php:29
msgid "All API endpoints use this base URL:"
msgstr "تمام نقاط پایانی API از این آدرس پایه استفاده می‌کنند:"

#: templates/admin/api-documentation.php:33
#: templates/admin/api-documentation.php:118
#: templates/admin/api-documentation.php:442
#: templates/admin/product-example.php:100
msgid "Copy"
msgstr "کپی"

#: templates/admin/api-documentation.php:42
msgid "Authentication"
msgstr "احراز هویت"

#: templates/admin/api-documentation.php:45
msgid ""
"Most API requests require authentication. Use the Authorization header with "
"a Bearer token."
msgstr ""
"اکثر درخواست‌های API نیاز به احراز هویت دارند. از هدر Authorization با یک توکن Bearer استفاده کنید."

#: templates/admin/api-documentation.php:46
msgid "Note:"
msgstr "توجه:"

#: templates/admin/api-documentation.php:46
msgid ""
"The Custom Post Types endpoint (/custom-post-types/{post_type}) is public "
"and does not require an authentication token."
msgstr ""
"نقطه پایانی انواع نوشته سفارشی (/custom-post-types/{post_type}) عمومی است "
"و نیازی به توکن احراز هویت ندارد."

#: templates/admin/api-documentation.php:47
msgid "Example headers for protected endpoints:"
msgstr "هدرهای نمونه برای نقاط پایانی محافظت‌شده:"

#: templates/admin/api-documentation.php:56
msgid "Available Endpoints"
msgstr "نقاط پایانی موجود"

#: templates/admin/api-documentation.php:62
msgid "Endpoint"
msgstr "نقطه پایانی"

#: templates/admin/api-documentation.php:63
msgid "Method"
msgstr "متد"

#: templates/admin/api-documentation.php:65
msgid "URL"
msgstr "آدرس"

#: templates/admin/api-documentation.php:73
msgid "Retrieve all products with pagination"
msgstr "دریافت تمام محصولات با صفحه‌بندی"

#: templates/admin/api-documentation.php:78
msgid "Retrieve a single product by ID"
msgstr "دریافت یک محصول بر اساس شناسه"

#: templates/admin/api-documentation.php:83
msgid "Retrieve all product IDs"
msgstr "دریافت تمام شناسه‌های محصول"

#: templates/admin/api-documentation.php:88
msgid "Retrieve all posts with pagination"
msgstr "دریافت تمام نوشته‌ها با صفحه‌بندی"

#: templates/admin/api-documentation.php:93
msgid "Retrieve all pages with pagination"
msgstr "دریافت تمام صفحات با صفحه‌بندی"

#: templates/admin/api-documentation.php:98
msgid "Retrieve all items from a custom post type with pagination"
msgstr "دریافت تمام آیتم‌ها از یک نوع نوشته سفارشی با صفحه‌بندی"

#: templates/admin/api-documentation.php:103
msgid "Track an order by order ID and email"
msgstr "پیگیری سفارش بر اساس شناسه سفارش و ایمیل"

#: templates/admin/api-documentation.php:132
msgid "Query Parameters"
msgstr "پارامترهای پرسش"

#: templates/admin/api-documentation.php:138
msgid "Parameter"
msgstr "پارامتر"

#: templates/admin/api-documentation.php:141
msgid "Default"
msgstr "پیش‌فرض"

#: templates/admin/api-documentation.php:150
msgid "Number of items to skip for pagination"
msgstr "تعداد آیتم‌هایی که برای صفحه‌بندی رد می‌شوند"

#: templates/admin/api-documentation.php:156
msgid "Number of items to take (max: 100)"
msgstr "تعداد آیتم‌هایی که دریافت می‌شوند (حداکثر: ۱۰۰)"

#: templates/admin/api-documentation.php:162
msgid "Filter items modified after date (Y-m-d H:i:s)"
msgstr "فیلتر آیتم‌های ویرایش‌شده پس از تاریخ (Y-m-d H:i:s)"

#: templates/admin/api-documentation.php:168
msgid ""
"Sort collection by attribute. Can be a single field or comma-separated list "
"(e.g., \"modified,ID\")"
msgstr ""
"مرتب‌سازی مجموعه بر اساس ویژگی. می‌تواند یک فیلد یا لیست جداشده با کاما باشد "
"(مثلاً: \"modified,ID\")"

#: templates/admin/api-documentation.php:174
msgid "Sort order: ASC or DESC"
msgstr "ترتیب مرتب‌سازی: صعودی (ASC) یا نزولی (DESC)"

#: templates/admin/api-documentation.php:180
msgid "Filter to show only in-stock products"
msgstr "فیلتر برای نمایش فقط محصولات موجود"

#: templates/admin/api-documentation.php:202
msgid "Example Requests"
msgstr "درخواست‌های نمونه"

#: templates/admin/api-documentation.php:212
msgid "Basic Request (cURL)"
msgstr "درخواست پایه (cURL)"

#: templates/admin/api-documentation.php:218
msgid "With Pagination (cURL)"
msgstr "با صفحه‌بندی (cURL)"

#: templates/admin/api-documentation.php:228
msgid "Filter by Modified Date (cURL)"
msgstr "فیلتر بر اساس تاریخ ویرایش (cURL)"

#: templates/admin/api-documentation.php:239
msgid "Basic Request (PHP)"
msgstr "درخواست پایه (PHP)"

#: templates/admin/api-documentation.php:260
msgid "Using cURL in PHP"
msgstr "استفاده از cURL در PHP"

#: templates/admin/api-documentation.php:282
msgid "Using Fetch API (JavaScript)"
msgstr "استفاده از Fetch API (جاوااسکریپت)"

#: templates/admin/api-documentation.php:303
msgid "Using Axios (JavaScript)"
msgstr "استفاده از Axios (جاوااسکریپت)"

#: templates/admin/api-documentation.php:328 templates/admin/settings.php:50
msgid "Product Meta Fields"
msgstr "فیلدهای متا محصول"

#: templates/admin/api-documentation.php:331
msgid ""
"Product meta fields are included in the response when selected in the "
"Product Meta Fields section of the Settings page. Meta fields can contain "
"simple text values or complex structured data."
msgstr ""
"فیلدهای متا محصول هنگامی که در بخش فیلدهای متا محصول صفحه تنظیمات انتخاب شوند، در پاسخ گنجانده می‌شوند. فیلدهای متا می‌توانند حاوی مقادیر متنی ساده یا داده‌های ساختاریافته پیچیده باشند."

#: templates/admin/api-documentation.php:333
msgid "Example Response with Meta Fields"
msgstr "پاسخ نمونه با فیلدهای متا"

#: templates/admin/api-documentation.php:351
msgid "Custom Meta Field Labels"
msgstr "برچسب‌های سفارشی فیلد متا"

#: templates/admin/api-documentation.php:352
msgid ""
"You can customize the display label of each meta field in the API response "
"by setting a Custom Label in the Product Meta Fields section of the Settings "
"page."
msgstr ""
"می‌توانید برچسب نمایشی هر فیلد متا در پاسخ API را با تنظیم یک برچسب سفارشی در بخش فیلدهای متا محصول صفحه تنظیمات سفارشی کنید."

#: templates/admin/api-documentation.php:359
msgid "Response Structure"
msgstr "ساختار پاسخ"

#: templates/admin/api-documentation.php:362
msgid "Pagination Response"
msgstr "پاسخ صفحه‌بندی"

#: templates/admin/api-documentation.php:375
msgid "Single Product Response"
msgstr "پاسخ محصول تکی"

#: templates/admin/api-documentation.php:434
msgid "Custom Post Types"
msgstr "انواع نوشته سفارشی"

#: templates/admin/api-documentation.php:437
msgid ""
"The API supports retrieving data from custom post types. You can access any "
"public custom post type using the following endpoint format:"
msgstr ""
"API از دریافت داده از انواع نوشته سفارشی پشتیبانی می‌کند. می‌توانید به هر نوع نوشته سفارشی عمومی با استفاده از فرمت نقطه پایانی زیر دسترسی داشته باشید:"

#: templates/admin/api-documentation.php:446
msgid "How to Use"
msgstr "نحوه استفاده"

#: templates/admin/api-documentation.php:447
msgid ""
"Replace {post_type} with the name of your custom post type. For example, if "
"you have a custom post type called \"portfolio\", you would use:"
msgstr ""
"عبارت {post_type} را با نام نوع نوشته سفارشی خود جایگزین کنید. برای مثال، اگر یک نوع نوشته سفارشی به نام \"portfolio\" دارید، از این استفاده کنید:"

#: templates/admin/api-documentation.php:450
msgid "Example Request"
msgstr "درخواست نمونه"

#: templates/admin/api-documentation.php:454
#: templates/admin/api-documentation.php:513
msgid "Example Response"
msgstr "پاسخ نمونه"

#: templates/admin/api-documentation.php:476
msgid "Supported Post Types"
msgstr "انواع نوشته پشتیبانی‌شده"

#: templates/admin/api-documentation.php:477
msgid ""
"The API automatically detects and supports all public custom post types "
"registered in WordPress. The following post types are excluded (they have "
"their own dedicated endpoints):"
msgstr ""
"API به‌طور خودکار تمام انواع نوشته سفارشی عمومی ثبت‌شده در وردپرس را شناسایی و پشتیبانی می‌کند. انواع نوشته زیر مستثنی هستند (آن‌ها نقاط پایانی اختصاصی خود را دارند):"

#: templates/admin/api-documentation.php:479
msgid "Use /posts endpoint"
msgstr "از نقطه پایانی /posts استفاده کنید"

#: templates/admin/api-documentation.php:480
msgid "Use /pages endpoint"
msgstr "از نقطه پایانی /pages استفاده کنید"

#: templates/admin/api-documentation.php:481
msgid "Use /products endpoint"
msgstr "از نقطه پایانی /products استفاده کنید"

#: templates/admin/api-documentation.php:482
msgid "Not supported"
msgstr "پشتیبانی نمی‌شود"

#: templates/admin/api-documentation.php:485
msgid "Custom Fields and Taxonomies"
msgstr "فیلدهای سفارشی و دسته‌بندی‌ها"

#: templates/admin/api-documentation.php:486
msgid ""
"Custom post type items can include custom meta fields and taxonomy terms. "
"These are automatically included in the response if they are configured in "
"the plugin settings."
msgstr ""
"آیتم‌های نوع نوشته سفارشی می‌توانند شامل فیلدهای متا سفارشی و عبارات طبقه‌بندی باشند. "
"این موارد در صورتی که در تنظیمات افزونه پیکربندی شده باشند، به‌طور خودکار در پاسخ گنجانده می‌شوند."

#: templates/admin/api-documentation.php:488
msgid "Available Parameters"
msgstr "پارامترهای موجود"

#: templates/admin/api-documentation.php:489
msgid ""
"The custom post types endpoint supports the same parameters as other "
"endpoints:"
msgstr ""
"نقطه پایانی انواع نوشته سفارشی از همان پارامترهای سایر نقاط پایانی پشتیبانی می‌کند:"

#: templates/admin/api-documentation.php:491
msgid "Number of items to skip (default: 0)"
msgstr "تعداد آیتم‌هایی که رد می‌شوند (پیش‌فرض: ۰)"

#: templates/admin/api-documentation.php:492
msgid "Number of items to retrieve (default: 30, max: 100)"
msgstr "تعداد آیتم‌هایی که دریافت می‌شوند (پیش‌فرض: ۳۰، حداکثر: ۱۰۰)"

#: templates/admin/api-documentation.php:493
msgid "Field to sort by (default: modified,ID)"
msgstr "فیلد مرتب‌سازی (پیش‌فرض: modified,ID)"

#: templates/admin/api-documentation.php:494
msgid "Sort order: ASC or DESC (default: ASC)"
msgstr "ترتیب مرتب‌سازی: صعودی (ASC) یا نزولی (DESC) (پیش‌فرض: ASC)"

#: templates/admin/api-documentation.php:495
msgid "Filter items modified after this date (format: YYYY-MM-DD HH:MM:SS)"
msgstr "فیلتر آیتم‌های ویرایش‌شده پس از این تاریخ (فرمت: YYYY-MM-DD HH:MM:SS)"

#: templates/admin/api-documentation.php:503
msgid "Error Handling"
msgstr "مدیریت خطا"

#: templates/admin/api-documentation.php:506
msgid ""
"The API returns standard HTTP status codes along with error messages in case "
"of failure."
msgstr ""
"API کدهای وضعیت HTTP استاندارد را همراه با پیام‌های خطا در صورت بروز مشکل بازمی‌گرداند."

#: templates/admin/api-documentation.php:511
msgid "Status Code"
msgstr "کد وضعیت"

#: templates/admin/api-documentation.php:519
msgid "Unauthorized - Invalid or missing token"
msgstr "غیرمجاز - توکن نامعتبر یا مفقود"

#: templates/admin/api-documentation.php:526
msgid "Not Found - Resource does not exist"
msgstr "یافت نشد - منبع وجود ندارد"

#: templates/admin/api-documentation.php:533
msgid "Bad Request - Invalid parameters"
msgstr "درخواست بد - پارامترهای نامعتبر"

#: templates/admin/api-documentation.php:540
msgid "Server Error - Something went wrong on the server"
msgstr "خطای سرور - مشکلی در سرور رخ داده است"

#: templates/admin/api-documentation.php:553
msgid "Available API Fields"
msgstr "فیلدهای API موجود"

#: templates/admin/api-documentation.php:556
msgid ""
"The following sections show all available fields that can be included in API "
"responses. Select which fields you want to enable."
msgstr ""
"بخش‌های زیر تمام فیلدهای موجودی را نشان می‌دهند که می‌توانند در پاسخ‌های API گنجانده شوند. انتخاب کنید کدام فیلدها را می‌خواهید فعال کنید."

#: templates/admin/api-documentation.php:567
msgid "Select which fields you want to include in the products API response."
msgstr "انتخاب کنید کدام فیلدها در پاسخ API محصولات گنجانده شوند."

#: templates/admin/api-documentation.php:573
#: templates/admin/api-documentation.php:617
#: templates/admin/api-documentation.php:658
msgid "Field"
msgstr "فیلد"

#: templates/admin/api-documentation.php:595
#: templates/admin/api-documentation.php:636
#: templates/admin/api-documentation.php:677
msgid "Enabled"
msgstr "فعال"

#: templates/admin/api-documentation.php:597
#: templates/admin/api-documentation.php:638
#: templates/admin/api-documentation.php:679
msgid "Disabled"
msgstr "غیرفعال"

#: templates/admin/api-documentation.php:611
msgid "Select which fields you want to include in the posts API response."
msgstr "انتخاب کنید کدام فیلدها در پاسخ API نوشته‌ها گنجانده شوند."

#: templates/admin/api-documentation.php:652
msgid "Select which fields you want to include in the pages API response."
msgstr "انتخاب کنید کدام فیلدها در پاسخ API صفحات گنجانده شوند."

#: templates/admin/api-documentation.php:691
msgid "Save Field Settings"
msgstr "ذخیره تنظیمات فیلدها"

#: templates/admin/settings.php:13
msgid "Muchat API Settings"
msgstr "تنظیمات API موچت"

#: templates/admin/settings.php:22
msgid "Muchat Server Connection"
msgstr "اتصال سرور موچت"

#: templates/admin/settings.php:32
msgid "Re-check"
msgstr "بررسی مجدد"

#: templates/admin/settings.php:78
msgid "Save Changes"
msgstr "ذخیره تغییرات"

#: templates/admin/settings.php:85
msgid "Refresh Fields"
msgstr "بارگذاری مجدد فیلدها"

#: templates/admin/widget-settings.php:25
msgid "Muchat Chatbot Settings"
msgstr "تنظیمات چت‌بات موچت"

#: templates/admin/widget-settings.php:32
msgid "Successfully disconnected from Muchat."
msgstr "با موفقیت از موچت قطع شد."

#: templates/admin/widget-settings.php:38
msgid "Connect with Muchat to activate the chatbot on your website."
msgstr "برای فعال‌سازی چت‌بات در وب‌سایت خود به موچت متصل شوید."

#: templates/admin/widget-settings.php:41
msgid "Connect with Muchat"
msgstr "اتصال به موچت"

#: templates/admin/widget-settings.php:47
msgid "Connected with Muchat. You can now use Muchat throughout your website."
msgstr "به موچت متصل شدید. اکنون می‌توانید از موچت در سراسر وب‌سایت خود استفاده کنید."

#: templates/admin/widget-settings.php:49
msgid "Widget is currently disabled. Enable it in settings below."
msgstr "ویجت در حال حاضر غیرفعال است. آن را در تنظیمات زیر فعال کنید."

#: templates/admin/widget-settings.php:56
msgid "Connection Settings"
msgstr "تنظیمات اتصال"

#: templates/admin/widget-settings.php:58
msgid "Agent ID"
msgstr "شناسه Agent"

#: templates/admin/widget-settings.php:62
msgid "Reconfigure"
msgstr "پیکربندی مجدد"

#: templates/admin/widget-settings.php:71
msgid ""
"Are you sure you want to disconnect from Muchat? This will remove your Agent "
"ID."
msgstr ""
"آیا مطمئن هستید که می‌خواهید از موچت قطع شوید؟ این کار شناسه Agent شما را حذف خواهد کرد."

#: templates/admin/widget-settings.php:72
msgid "Disconnect"
msgstr "قطع اتصال"

#: templates/admin/widget-settings.php:79
msgid "Widget Customization"
msgstr "سفارشی‌سازی ویجت"

#: templates/admin/widget-settings.php:80
msgid "Customize your Muchat chatbot widget appearance in the Muchat dashboard"
msgstr "ظاهر ویجت چت‌بات موچت خود را در داشبورد موچت سفارشی کنید"

#: templates/admin/widget-settings.php:89
msgid "Customize Widget in Muchat Panel"
msgstr "سفارشی‌سازی ویجت در پنل موچت"

#: templates/admin/widget-settings.php:108
msgid "Widget Display"
msgstr "نمایش ویجت"

#: templates/admin/widget-settings.php:111
msgid "Control the visibility of the Muchat chatbot widget on your site"
msgstr "کنترل نمایش ویجت چت‌بات موچت در سایت خود"

#: templates/admin/widget-settings.php:114
msgid "Display Status"
msgstr "وضعیت نمایش"

#: templates/admin/widget-settings.php:118
msgid "Enable Muchat Widget"
msgstr "فعال‌سازی ویجت موچت"

#: templates/admin/widget-settings.php:121
msgid ""
"You can temporarily disable the Muchat chatbot widget without losing your "
"settings."
msgstr ""
"می‌توانید ویجت چت‌بات موچت را بدون از دست دادن تنظیمات خود به‌طور موقت غیرفعال کنید."

#: templates/admin/widget-settings.php:132
msgid "Contact Information"
msgstr "اطلاعات تماس"

#: templates/admin/widget-settings.php:135
msgid ""
"User information for personalizing the conversation experience (optional)"
msgstr ""
"اطلاعات کاربر برای شخصی‌سازی تجربه مکالمه (اختیاری)"

#: templates/admin/widget-settings.php:138
msgid "User Info"
msgstr "اطلاعات کاربر"

#: templates/admin/widget-settings.php:142
msgid "Automatically send logged-in user information"
msgstr "ارسال خودکار اطلاعات کاربر وارد‌شده"

#: templates/admin/widget-settings.php:145
msgid ""
"When enabled, the plugin will send the name and email of logged-in users to "
"personalize chat interactions."
msgstr ""
"هنگامی که فعال باشد، افزونه نام و ایمیل کاربران وارد‌شده را برای شخصی‌سازی تعاملات چت ارسال می‌کند."

#: templates/admin/widget-settings.php:156
msgid "Interface Settings"
msgstr "تنظیمات رابط کاربری"

#: templates/admin/widget-settings.php:159
msgid "Customize the appearance and initial messages of the chatbot"
msgstr "ظاهر و پیام‌های اولیه چت‌بات را سفارشی کنید"

#: templates/admin/widget-settings.php:162
msgid "Initial Messages"
msgstr "پیام‌های اولیه"

#: templates/admin/widget-settings.php:167
#: templates/admin/widget-settings.php:190
msgid "Add Message"
msgstr "افزودن پیام"

#: templates/admin/widget-settings.php:171
msgid ""
"Add welcome messages that will be shown when a visitor opens the chat "
"widget. You can include personalization variables such as"
msgstr ""
"پیام‌های خوش‌آمدگویی اضافه کنید که هنگام باز کردن ویجت چت توسط بازدیدکننده نمایش داده می‌شوند. می‌توانید متغیرهای شخصی‌سازی مانند"

#: templates/admin/widget-settings.php:171
#: templates/admin/widget-settings.php:246
msgid "and"
msgstr "و"

#: templates/admin/widget-settings.php:171
msgid "example:"
msgstr "مثال:"

#: templates/admin/widget-settings.php:171
msgid ""
"Add multiple messages to create a conversation flow (each message will "
"appear in sequence). If left empty, default settings from your Muchat "
"dashboard will be used."
msgstr ""
"چندین پیام اضافه کنید تا یک جریان مکالمه ایجاد کنید (هر پیام به ترتیب ظاهر می‌شود). اگر خالی بماند، از تنظیمات پیش‌فرض داشبورد موچت شما استفاده می‌شود."

#: templates/admin/widget-settings.php:176
msgid "Different Messages for Guests"
msgstr "پیام‌های متفاوت برای مهمانان"

#: templates/admin/widget-settings.php:180
msgid "Enable different initial messages for non-logged-in users."
msgstr "فعال‌سازی پیام‌های اولیه متفاوت برای کاربران وارد‌نشده."

#: templates/admin/widget-settings.php:185
msgid "Guest Initial Messages"
msgstr "پیام‌های اولیه مهمانان"

#: templates/admin/widget-settings.php:194
msgid ""
"These messages will be shown to visitors who are not logged in. If left "
"empty, the default messages above will be used."
msgstr ""
"این پیام‌ها به بازدیدکنندگانی که وارد نشده‌اند نمایش داده می‌شوند. اگر خالی بماند، از پیام‌های پیش‌فرض بالا استفاده می‌شود."

#: templates/admin/widget-settings.php:199
msgid "Load Strategy"
msgstr "استراتژی بارگذاری"

#: templates/admin/widget-settings.php:202
msgid "Fast (Recommended)"
msgstr "سریع (توصیه شده)"

#: templates/admin/widget-settings.php:203
msgid "SEO Friendly"
msgstr "سازگار با سئو"

#: templates/admin/widget-settings.php:204
msgid "Complete Load"
msgstr "بارگذاری کامل"

#: templates/admin/widget-settings.php:207
msgid ""
"Controls how the chatbot loads on your website. Fast is recommended for most "
"sites."
msgstr ""
"نحوه بارگذاری چت‌بات در وب‌سایت شما را کنترل می‌کند. برای اکثر سایت‌ها گزینه سریع توصیه می‌شود."

#: templates/admin/widget-settings.php:212
msgid "Script Load Position"
msgstr "موقعیت بارگذاری اسکریپت"

#: templates/admin/widget-settings.php:215
msgid "Footer (Recommended)"
msgstr "فوتر (توصیه شده)"

#: templates/admin/widget-settings.php:216
msgid "Header"
msgstr "هدر"

#: templates/admin/widget-settings.php:217
msgid "Body Opening"
msgstr "ابتدای Body"

#: templates/admin/widget-settings.php:220
msgid "Footer (Recommended):"
msgstr "فوتر (توصیه شده):"

#: templates/admin/widget-settings.php:220
msgid ""
"Best for page speed and mobile compatibility. Loads after the page content."
msgstr ""
"بهترین گزینه برای سرعت صفحه و سازگاری با موبایل. پس از محتوای صفحه بارگذاری می‌شود."

#: templates/admin/widget-settings.php:221
msgid "Header:"
msgstr "هدر:"

#: templates/admin/widget-settings.php:221
msgid ""
"Loads in the <head> section. Use if you need the widget available as early "
"as possible."
msgstr ""
"در بخش <head> بارگذاری می‌شود. در صورتی که نیاز دارید ویجت هر چه زودتر در دسترس باشد استفاده کنید."

#: templates/admin/widget-settings.php:222
msgid "Body Opening:"
msgstr "ابتدای Body:"

#: templates/admin/widget-settings.php:222
msgid ""
"Loads right after <body> tag. Requires WordPress 5.2+ and modern theme "
"support."
msgstr ""
"بلافاصله بعد از تگ <body> بارگذاری می‌شود. نیاز به وردپرس ۵.۲+ و پشتیبانی قالب مدرن دارد."

#: templates/admin/widget-settings.php:233
msgid "Dynamic Prompt"
msgstr "پرامپت پویا"

#: templates/admin/widget-settings.php:236
msgid "Configure the context prompt that will be sent to the AI model"
msgstr "پرامپت متنی که به مدل هوش مصنوعی ارسال می‌شود را پیکربندی کنید"

#: templates/admin/widget-settings.php:239
msgid "Context for Logged-in Users"
msgstr "متن برای کاربران وارد‌شده"

#: templates/admin/widget-settings.php:243
msgid ""
"Provide additional context that will be appended to the Agent system prompt."
msgstr ""
"متن اضافی ارائه دهید که به پرامپت سیستم Agent اضافه می‌شود."

#: templates/admin/widget-settings.php:246
msgid "You can use variables like"
msgstr "می‌توانید از متغیرهایی مانند"

#: templates/admin/widget-settings.php:246
msgid "in the context. Example:"
msgstr "در متن استفاده کنید. مثال:"

#: templates/admin/widget-settings.php:246
msgid ""
"The user you are talking to is $name $lastname (Email: $email, Phone: "
"$phone). They are currently on the page: $page_title. Start by greeting them "
"by name."
msgstr ""
"کاربری که با او صحبت می‌کنید $name $lastname است (ایمیل: $email، تلفن: "
"$phone). در حال حاضر در صفحه: $page_title هستند. با خوش‌آمدگویی به نام شروع کنید."

#: templates/admin/widget-settings.php:251
msgid "Different Context for Guests"
msgstr "متن متفاوت برای مهمانان"

#: templates/admin/widget-settings.php:255
msgid "Enable different context for non-logged-in users."
msgstr "فعال‌سازی متن متفاوت برای کاربران وارد‌نشده."

#: templates/admin/widget-settings.php:260
msgid "Context for Guests"
msgstr "متن برای مهمانان"

#: templates/admin/widget-settings.php:264
msgid ""
"This context will be used for visitors who are not logged in. If left empty, "
"no context will be sent for guests."
msgstr ""
"این متن برای بازدیدکنندگانی که وارد نشده‌اند استفاده می‌شود. اگر خالی بماند، هیچ متنی برای مهمانان ارسال نمی‌شود."

#: templates/admin/widget-settings.php:267
msgid "Example:"
msgstr "مثال:"

#: templates/admin/widget-settings.php:278
msgid "Display Rules"
msgstr "قوانین نمایش"

#: templates/admin/widget-settings.php:281
msgid ""
"Specify which pages the Muchat chatbot should appear on or be hidden from"
msgstr ""
"مشخص کنید چت‌بات موچت در کدام صفحات نمایش داده شود یا پنهان بماند"

#: templates/admin/widget-settings.php:284
msgid "Display Mode"
msgstr "حالت نمایش"

#: templates/admin/widget-settings.php:289
msgid "Show on all pages except those listed below"
msgstr "نمایش در تمام صفحات به جز موارد فهرست‌شده در زیر"

#: templates/admin/widget-settings.php:294
msgid "Only show on pages listed below"
msgstr "فقط در صفحات فهرست‌شده در زیر نمایش داده شود"

#: templates/admin/widget-settings.php:300
msgid "Page List"
msgstr "فهرست صفحات"

#: templates/admin/widget-settings.php:307
msgid "One pattern per line. Use"
msgstr "یک الگو در هر سطر. از"

#: templates/admin/widget-settings.php:307
msgid "for homepage,"
msgstr "برای صفحه اصلی،"

#: templates/admin/widget-settings.php:307
msgid "for wildcards."
msgstr "برای کاراکتر عام استفاده کنید."

#: templates/admin/widget-settings.php:310
msgid "English examples:"
msgstr "مثال‌های انگلیسی:"

#: templates/admin/widget-settings.php:313
msgid "Persian/RTL examples:"
msgstr "مثال‌های فارسی/راست‌به‌چپ:"

#: templates/admin/widget-settings.php:316
msgid ""
"URLs are case-insensitive (/About = /about). Persian/Arabic URLs are fully "
"supported."
msgstr ""
"آدرس‌ها به بزرگی و کوچکی حروف حساس نیستند (/About = /about). آدرس‌های فارسی/عربی کاملاً پشتیبانی می‌شوند."

#: templates/admin/widget-settings.php:326
msgid ""
"If you use a caching plugin and display rules don't work correctly, please "
"clear your site cache."
msgstr ""
"اگر از افزونه کشینگ استفاده می‌کنید و قوانین نمایش به درستی کار نمی‌کنند، لطفاً کش سایت خود را پاک کنید."

#: templates/admin/widget-settings.php:335
msgid "Schedule Settings"
msgstr "تنظیمات زمان‌بندی"

#: templates/admin/widget-settings.php:338
msgid "Configure when the Muchat chatbot should be available on your website"
msgstr "تنظیم کنید که چت‌بات موچت چه زمانی در وب‌سایت شما در دسترس باشد"

#: templates/admin/widget-settings.php:342
msgid "Enable Schedule"
msgstr "فعال‌سازی زمان‌بندی"

#: templates/admin/widget-settings.php:346
msgid "Enable scheduling"
msgstr "فعال کردن زمان‌بندی"

#: templates/admin/widget-settings.php:349
msgid ""
"When enabled, the chatbot will only be available during specified days and "
"times."
msgstr ""
"هنگامی که فعال باشد، چت‌بات فقط در روزها و ساعات مشخص‌شده در دسترس خواهد بود."

#: templates/admin/widget-settings.php:354
msgid "Active Days"
msgstr "روزهای فعال"

#: templates/admin/widget-settings.php:358
msgid "Saturday"
msgstr "شنبه"

#: templates/admin/widget-settings.php:359
msgid "Sunday"
msgstr "یکشنبه"

#: templates/admin/widget-settings.php:360
msgid "Monday"
msgstr "دوشنبه"

#: templates/admin/widget-settings.php:361
msgid "Tuesday"
msgstr "سه‌شنبه"

#: templates/admin/widget-settings.php:362
msgid "Wednesday"
msgstr "چهارشنبه"

#: templates/admin/widget-settings.php:363
msgid "Thursday"
msgstr "پنجشنبه"

#: templates/admin/widget-settings.php:364
msgid "Friday"
msgstr "جمعه"

#: templates/admin/widget-settings.php:383
msgid ""
"Select the days when the chatbot should be available. If no days are "
"selected, the chatbot will be available every day."
msgstr ""
"روزهایی را که چت‌بات باید در دسترس باشد انتخاب کنید. اگر هیچ روزی انتخاب نشود، چت‌بات هر روز در دسترس خواهد بود."

#: templates/admin/widget-settings.php:388
msgid "Active Hours"
msgstr "ساعات فعال"

#: templates/admin/widget-settings.php:391
msgid "to"
msgstr "تا"

#: templates/admin/widget-settings.php:394
msgid ""
"Set the time range during which the chatbot should be available. If no time "
"is set, the chatbot will be available 24/7."
msgstr ""
"بازه زمانی که چت‌بات باید در دسترس باشد را تنظیم کنید. اگر زمانی تنظیم نشود، چت‌بات ۲۴ ساعته در دسترس خواهد بود."

#: templates/admin/widget-settings.php:395
msgid "Times are based on your website's timezone:"
msgstr "زمان‌ها بر اساس منطقه زمانی وب‌سایت شما هستند:"

#: templates/admin/widget-settings.php:405
msgid "Save Settings"
msgstr "ذخیره تنظیمات"

#: templates/admin/product-example.php:35
msgid ""
"This page allows you to view how a specific product will appear in API "
"responses."
msgstr ""
"این صفحه به شما امکان می‌دهد ببینید یک محصول خاص چگونه در پاسخ‌های API نمایش داده می‌شود."

#. translators: %1$s: opening link tag, %2$s: closing link tag
#: templates/admin/product-example.php:39
#, php-format
msgid ""
"To modify which fields are included, visit the %1$sAPI Settings%2$s page."
msgstr ""
"برای تغییر فیلدهای گنجانده‌شده، از صفحه %1$sتنظیمات API%2$s دیدن کنید."

#: templates/admin/product-example.php:49
msgid "Select Product"
msgstr "انتخاب محصول"

#: templates/admin/product-example.php:56
msgid "Select a Product"
msgstr "یک محصول انتخاب کنید"

#: templates/admin/product-example.php:64
msgid "Search for a product..."
msgstr "جستجوی محصول..."

#: templates/admin/product-example.php:80
msgid "Get Example"
msgstr "دریافت نمونه"

#: templates/admin/product-example.php:83
msgid "Search for a product to view its API response."
msgstr "برای مشاهده پاسخ API آن، یک محصول جستجو کنید."

#: templates/admin/product-example.php:93
msgid "API Response Example"
msgstr "نمونه پاسخ API"

#: templates/admin/product-example.php:98
msgid "JSON Response"
msgstr "پاسخ JSON"

#: templates/admin/product-example.php:109
msgid "Product not found or error retrieving data."
msgstr "محصول یافت نشد یا خطایی در دریافت داده رخ داد."
</file>

<file path="languages/muchat-ai.pot">
# Muchat AI Chatbot - Translation Template
# Copyright (C) 2026 Muchat Team
# This file is distributed under the same license as the Muchat AI Chatbot package.
# Muchat Team <support@mu.chat>, 2026.
#
msgid ""
msgstr ""
"Project-Id-Version: Muchat AI Chatbot 2.0.52\n"
"Report-Msgid-Bugs-To: support@mu.chat\n"
"POT-Creation-Date: 2026-02-25 09:34+0330\n"
"PO-Revision-Date: 2026-02-25 09:34+0330\n"
"Last-Translator: Muchat Team <support@mu.chat>\n"
"Language-Team: Muchat Team <support@mu.chat>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#: includes/Core/Plugin.php:126
msgid "Settings"
msgstr ""

#: includes/Core/Activator.php:39 includes/Core/Activator.php:57
#: includes/Core/Activator.php:68 includes/Admin/Settings.php:884
#: includes/Admin/Settings.php:902 includes/Admin/Settings.php:913
#: includes/Admin/Settings.php:966
msgid "ID"
msgstr ""

#: includes/Core/Activator.php:40 includes/Admin/Settings.php:885
msgid "Name"
msgstr ""

#: includes/Core/Activator.php:41 includes/Core/Activator.php:62
#: includes/Core/Activator.php:73 includes/Admin/Settings.php:886
#: includes/Admin/Settings.php:907 includes/Admin/Settings.php:918
#: includes/Admin/Settings.php:971
msgid "Permalink"
msgstr ""

#: includes/Core/Activator.php:42 includes/Admin/Settings.php:887
msgid "Date Modified"
msgstr ""

#: includes/Core/Activator.php:43 includes/Admin/Settings.php:888
#: templates/admin/api-documentation.php:64
#: templates/admin/api-documentation.php:140
#: templates/admin/api-documentation.php:512
#: templates/admin/api-documentation.php:574
#: templates/admin/api-documentation.php:618
#: templates/admin/api-documentation.php:659
msgid "Description"
msgstr ""

#: includes/Core/Activator.php:44 includes/Admin/Settings.php:889
msgid "Short Description"
msgstr ""

#: includes/Core/Activator.php:45 includes/Admin/Settings.php:890
msgid "Currency"
msgstr ""

#: includes/Core/Activator.php:46 includes/Admin/Settings.php:891
msgid "Price"
msgstr ""

#: includes/Core/Activator.php:47 includes/Admin/Settings.php:892
msgid "Regular Price"
msgstr ""

#: includes/Core/Activator.php:48 includes/Admin/Settings.php:893
msgid "Sale Price"
msgstr ""

#: includes/Core/Activator.php:49 includes/Admin/Settings.php:894
msgid "Stock Status"
msgstr ""

#: includes/Core/Activator.php:50 includes/Core/Activator.php:63
#: includes/Admin/Settings.php:895 includes/Admin/Settings.php:908
msgid "Categories"
msgstr ""

#: includes/Core/Activator.php:51 includes/Core/Activator.php:64
#: includes/Admin/Settings.php:896 includes/Admin/Settings.php:909
msgid "Tags"
msgstr ""

#: includes/Core/Activator.php:52 includes/Admin/Settings.php:897
msgid "Images"
msgstr ""

#: includes/Core/Activator.php:53 includes/Admin/Settings.php:898
msgid "Attributes"
msgstr ""

#: includes/Core/Activator.php:54 includes/Admin/Settings.php:899
msgid "Variations"
msgstr ""

#: includes/Core/Activator.php:58 includes/Core/Activator.php:69
#: includes/Admin/Settings.php:903 includes/Admin/Settings.php:914
#: includes/Admin/Settings.php:967
msgid "Title"
msgstr ""

#: includes/Core/Activator.php:59 includes/Core/Activator.php:70
#: includes/Admin/Settings.php:904 includes/Admin/Settings.php:915
#: includes/Admin/Settings.php:968
msgid "Content"
msgstr ""

#: includes/Core/Activator.php:60 includes/Core/Activator.php:71
#: includes/Admin/Settings.php:905 includes/Admin/Settings.php:916
#: includes/Admin/Settings.php:969
msgid "Excerpt"
msgstr ""

#: includes/Core/Activator.php:61 includes/Core/Activator.php:72
#: includes/Admin/Settings.php:906 includes/Admin/Settings.php:917
#: includes/Admin/Settings.php:970
msgid "Modified Date"
msgstr ""

#: includes/Core/Activator.php:65 includes/Core/Activator.php:74
#: includes/Admin/Settings.php:910 includes/Admin/Settings.php:919
#: includes/Admin/Settings.php:972
msgid "Featured Image"
msgstr ""

#. translators: %s: Alternative filter name
#: includes/Frontend/Widget.php:780
#, php-format
msgid ""
"The muchat_widget_output filter has been removed for security reasons. Use "
"%s instead for configuration changes."
msgstr ""

#: includes/Admin/Settings.php:177 includes/Admin/Settings.php:184
msgid "Copied!"
msgstr ""

#: includes/Admin/Settings.php:178
msgid "Failed!"
msgstr ""

#: includes/Admin/Settings.php:204
msgid "Muchat Settings"
msgstr ""

#: includes/Admin/Settings.php:205
msgid "Muchat"
msgstr ""

#: includes/Admin/Settings.php:215 includes/Admin/Settings.php:216
msgid "Widget Settings"
msgstr ""

#: includes/Admin/Settings.php:226 includes/Admin/Settings.php:227
msgid "WooCommerce API"
msgstr ""

#: includes/Admin/Settings.php:236 includes/Admin/Settings.php:237
#: templates/admin/product-example.php:23
msgid "Product Example"
msgstr ""

#: includes/Admin/Settings.php:251 includes/Admin/Settings.php:252
#: templates/admin/api-documentation.php:10
msgid "API Documentation"
msgstr ""

#: includes/Admin/Settings.php:292
msgid "Security verification failed. Please try again."
msgstr ""

#: includes/Admin/Settings.php:420
msgid "Could not establish a connection to Muchat servers."
msgstr ""

#. translators: 1: The server IP address, 2: The specific error message from WordPress.
#: includes/Admin/Settings.php:431
#, php-format
msgid "Your server (IP: %1$s) cannot connect to Muchat. Error: %2$s"
msgstr ""

#. translators: %s: The server IP address.
#: includes/Admin/Settings.php:445
#, php-format
msgid "Connection from your server (IP: %s) to Muchat is successful."
msgstr ""

#. translators: 1: The server IP address, 2: The HTTP status code from the server.
#: includes/Admin/Settings.php:453
#, php-format
msgid ""
"Your server (IP: %1$s) connected to Muchat, but received an error. Status "
"code: %2$d"
msgstr ""

#: includes/Admin/Settings.php:489 includes/Admin/Settings.php:863
#: includes/Api/Controllers/OrderController.php:62
#: includes/Api/Controllers/ProductController.php:43
#: includes/Api/Controllers/ProductController.php:119
#: includes/Api/Controllers/ProductController.php:185
#: includes/Api/Controllers/ProductInitController.php:29
msgid "WooCommerce is required for this functionality"
msgstr ""

#: includes/Admin/Settings.php:572
msgid "API field settings updated."
msgstr ""

#: includes/Admin/Settings.php:609
msgid ""
"Analyzing product meta fields in the background. This may take a few moments "
"for large catalogs."
msgstr ""

#: includes/Admin/Settings.php:613
msgid "Refresh Page"
msgstr ""

#: includes/Admin/Settings.php:634
msgid ""
"Select which product meta fields you want to include in the API response."
msgstr ""

#: includes/Admin/Settings.php:641 templates/admin/api-documentation.php:572
#: templates/admin/api-documentation.php:616
#: templates/admin/api-documentation.php:657
msgid "Enable"
msgstr ""

#: includes/Admin/Settings.php:642
msgid "Meta Field"
msgstr ""

#: includes/Admin/Settings.php:643 templates/admin/api-documentation.php:139
msgid "Type"
msgstr ""

#: includes/Admin/Settings.php:644
msgid "Custom Label"
msgstr ""

#: includes/Admin/Settings.php:645
msgid "Sample Values"
msgstr ""

#. translators: %s: number of products where this meta field is used
#: includes/Admin/Settings.php:674
#, php-format
msgid "Used in %s product"
msgid_plural "Used in %s products"
msgstr[0] ""
msgstr[1] ""

#: includes/Admin/Settings.php:701
msgid "Product Attribute"
msgstr ""

#: includes/Admin/Settings.php:704
msgid "ACF Field"
msgstr ""

#: includes/Admin/Settings.php:707
msgid "Variation"
msgstr ""

#: includes/Admin/Settings.php:710
msgid "Product & Variation"
msgstr ""

#: includes/Admin/Settings.php:761
msgid "Connection status has been re-checked."
msgstr ""

#: includes/Admin/Settings.php:777
msgid "Product meta fields cache has been cleared successfully."
msgstr ""

#: includes/Admin/Settings.php:827 templates/admin/api-documentation.php:565
msgid "Product Fields"
msgstr ""

#: includes/Admin/Settings.php:835 templates/admin/api-documentation.php:609
msgid "Post Fields"
msgstr ""

#: includes/Admin/Settings.php:843 templates/admin/api-documentation.php:650
msgid "Page Fields"
msgstr ""

#: includes/Admin/Settings.php:1267 includes/Admin/Settings.php:1372
msgid "Security check failed"
msgstr ""

#: includes/Admin/Settings.php:1272 includes/Admin/Settings.php:1377
msgid "Permission denied"
msgstr ""

#: includes/Admin/Settings.php:1281
msgid "Query too short"
msgstr ""

#: includes/Admin/Settings.php:1387
msgid "Invalid content type"
msgstr ""

#: includes/Admin/Settings.php:1391
msgid "Invalid item ID"
msgstr ""

#: includes/Admin/Settings.php:1400
msgid "WooCommerce not active"
msgstr ""

#: includes/Admin/Settings.php:1418
msgid "Content not found"
msgstr ""

#: includes/Models/CustomPostType.php:23
#, php-format
msgid "Invalid post type: %s"
msgstr ""

#: includes/Api/Middleware/AuthMiddleware.php:46
msgid "Invalid authentication header"
msgstr ""

#: includes/Api/Middleware/AuthMiddleware.php:99
msgid "Authentication service temporarily unavailable. Please try again later."
msgstr ""

#: includes/Api/Middleware/AuthMiddleware.php:123
#: includes/Api/Middleware/AuthMiddleware.php:204
msgid "Invalid token"
msgstr ""

#. translators: %s: error message from the API response
#: includes/Api/Middleware/AuthMiddleware.php:185
#, php-format
msgid "Token validation failed: %s"
msgstr ""

#. translators: %d: number of seconds to wait before retrying
#: includes/Api/Middleware/RateLimitMiddleware.php:103
#, php-format
msgid "Rate limit exceeded. Please try again in %d seconds."
msgstr ""

#: includes/Api/Routes.php:265
msgid "Number of items to skip."
msgstr ""

#: includes/Api/Routes.php:272
msgid "Number of items to take."
msgstr ""

#: includes/Api/Routes.php:280
msgid ""
"Sort collection by attribute. Can be a single field or comma-separated list "
"(e.g., \"modified,ID\")."
msgstr ""

#: includes/Api/Routes.php:286
msgid "Order sort attribute ascending or descending."
msgstr ""

#: includes/Api/Routes.php:292
msgid "Limit results to items modified after specified date."
msgstr ""

#: includes/Api/Routes.php:299
msgid ""
"Limit results to items with muchat_date_modified after specified date (only "
"price/stock changes)."
msgstr ""

#: includes/Api/Routes.php:306
msgid "Filter to show only in-stock products (product-specific)."
msgstr ""

#: includes/Api/Controllers/OrderController.php:91
msgid "Unable to find order with the provided information"
msgstr ""

#: includes/Api/Controllers/OrderController.php:140
msgid "Invalid Order ID format"
msgstr ""

#: includes/Api/Controllers/OrderController.php:151
msgid "Invalid email format"
msgstr ""

#: includes/Api/Controllers/OrderController.php:163
msgid "Invalid phone number format"
msgstr ""

#: includes/Api/Controllers/OrderController.php:174
msgid "At least one of Order ID, Email or Phone is required"
msgstr ""

#: includes/Api/Controllers/OrderController.php:372
msgid "Order not found"
msgstr ""

#: includes/Api/Controllers/OrderController.php:381
msgid "Email does not match with order"
msgstr ""

#: includes/Api/Controllers/OrderController.php:392
msgid "Phone does not match with order"
msgstr ""

#: includes/Api/Controllers/OrderController.php:406
msgid "Email or phone is required when order ID is not provided"
msgstr ""

#: includes/Api/Controllers/OrderController.php:434
msgid "No orders found with the provided criteria"
msgstr ""

#: includes/Api/Controllers/OrderController.php:455
msgid "No valid orders found"
msgstr ""

#: includes/Api/Controllers/CustomPostTypeController.php:45
msgid "Post type parameter is required"
msgstr ""

#: includes/Api/Controllers/ProductController.php:129
msgid "Invalid product ID"
msgstr ""

#: includes/Api/Controllers/ProductController.php:147
msgid "Product not found"
msgstr ""

#: templates/admin/api-documentation.php:18
msgid ""
"This page provides detailed documentation on how to use the Muchat API "
"endpoints."
msgstr ""

#: templates/admin/api-documentation.php:26
msgid "Base URL"
msgstr ""

#: templates/admin/api-documentation.php:29
msgid "All API endpoints use this base URL:"
msgstr ""

#: templates/admin/api-documentation.php:33
#: templates/admin/api-documentation.php:118
#: templates/admin/api-documentation.php:442
#: templates/admin/product-example.php:100
msgid "Copy"
msgstr ""

#: templates/admin/api-documentation.php:42
msgid "Authentication"
msgstr ""

#: templates/admin/api-documentation.php:45
msgid ""
"Most API requests require authentication. Use the Authorization header with "
"a Bearer token."
msgstr ""

#: templates/admin/api-documentation.php:46
msgid "Note:"
msgstr ""

#: templates/admin/api-documentation.php:46
msgid ""
"The Custom Post Types endpoint (/custom-post-types/{post_type}) is public "
"and does not require an authentication token."
msgstr ""

#: templates/admin/api-documentation.php:47
msgid "Example headers for protected endpoints:"
msgstr ""

#: templates/admin/api-documentation.php:56
msgid "Available Endpoints"
msgstr ""

#: templates/admin/api-documentation.php:62
msgid "Endpoint"
msgstr ""

#: templates/admin/api-documentation.php:63
msgid "Method"
msgstr ""

#: templates/admin/api-documentation.php:65
msgid "URL"
msgstr ""

#: templates/admin/api-documentation.php:73
msgid "Retrieve all products with pagination"
msgstr ""

#: templates/admin/api-documentation.php:78
msgid "Retrieve a single product by ID"
msgstr ""

#: templates/admin/api-documentation.php:83
msgid "Retrieve all product IDs"
msgstr ""

#: templates/admin/api-documentation.php:88
msgid "Retrieve all posts with pagination"
msgstr ""

#: templates/admin/api-documentation.php:93
msgid "Retrieve all pages with pagination"
msgstr ""

#: templates/admin/api-documentation.php:98
msgid "Retrieve all items from a custom post type with pagination"
msgstr ""

#: templates/admin/api-documentation.php:103
msgid "Track an order by order ID and email"
msgstr ""

#: templates/admin/api-documentation.php:132
msgid "Query Parameters"
msgstr ""

#: templates/admin/api-documentation.php:138
msgid "Parameter"
msgstr ""

#: templates/admin/api-documentation.php:141
msgid "Default"
msgstr ""

#: templates/admin/api-documentation.php:150
msgid "Number of items to skip for pagination"
msgstr ""

#: templates/admin/api-documentation.php:156
msgid "Number of items to take (max: 100)"
msgstr ""

#: templates/admin/api-documentation.php:162
msgid "Filter items modified after date (Y-m-d H:i:s)"
msgstr ""

#: templates/admin/api-documentation.php:168
msgid ""
"Sort collection by attribute. Can be a single field or comma-separated list "
"(e.g., \"modified,ID\")"
msgstr ""

#: templates/admin/api-documentation.php:174
msgid "Sort order: ASC or DESC"
msgstr ""

#: templates/admin/api-documentation.php:180
msgid "Filter to show only in-stock products"
msgstr ""

#: templates/admin/api-documentation.php:202
msgid "Example Requests"
msgstr ""

#: templates/admin/api-documentation.php:212
msgid "Basic Request (cURL)"
msgstr ""

#: templates/admin/api-documentation.php:218
msgid "With Pagination (cURL)"
msgstr ""

#: templates/admin/api-documentation.php:228
msgid "Filter by Modified Date (cURL)"
msgstr ""

#: templates/admin/api-documentation.php:239
msgid "Basic Request (PHP)"
msgstr ""

#: templates/admin/api-documentation.php:260
msgid "Using cURL in PHP"
msgstr ""

#: templates/admin/api-documentation.php:282
msgid "Using Fetch API (JavaScript)"
msgstr ""

#: templates/admin/api-documentation.php:303
msgid "Using Axios (JavaScript)"
msgstr ""

#: templates/admin/api-documentation.php:328 templates/admin/settings.php:50
msgid "Product Meta Fields"
msgstr ""

#: templates/admin/api-documentation.php:331
msgid ""
"Product meta fields are included in the response when selected in the "
"Product Meta Fields section of the Settings page. Meta fields can contain "
"simple text values or complex structured data."
msgstr ""

#: templates/admin/api-documentation.php:333
msgid "Example Response with Meta Fields"
msgstr ""

#: templates/admin/api-documentation.php:351
msgid "Custom Meta Field Labels"
msgstr ""

#: templates/admin/api-documentation.php:352
msgid ""
"You can customize the display label of each meta field in the API response "
"by setting a Custom Label in the Product Meta Fields section of the Settings "
"page."
msgstr ""

#: templates/admin/api-documentation.php:359
msgid "Response Structure"
msgstr ""

#: templates/admin/api-documentation.php:362
msgid "Pagination Response"
msgstr ""

#: templates/admin/api-documentation.php:375
msgid "Single Product Response"
msgstr ""

#: templates/admin/api-documentation.php:434
msgid "Custom Post Types"
msgstr ""

#: templates/admin/api-documentation.php:437
msgid ""
"The API supports retrieving data from custom post types. You can access any "
"public custom post type using the following endpoint format:"
msgstr ""

#: templates/admin/api-documentation.php:446
msgid "How to Use"
msgstr ""

#: templates/admin/api-documentation.php:447
msgid ""
"Replace {post_type} with the name of your custom post type. For example, if "
"you have a custom post type called \"portfolio\", you would use:"
msgstr ""

#: templates/admin/api-documentation.php:450
msgid "Example Request"
msgstr ""

#: templates/admin/api-documentation.php:454
#: templates/admin/api-documentation.php:513
msgid "Example Response"
msgstr ""

#: templates/admin/api-documentation.php:476
msgid "Supported Post Types"
msgstr ""

#: templates/admin/api-documentation.php:477
msgid ""
"The API automatically detects and supports all public custom post types "
"registered in WordPress. The following post types are excluded (they have "
"their own dedicated endpoints):"
msgstr ""

#: templates/admin/api-documentation.php:479
msgid "Use /posts endpoint"
msgstr ""

#: templates/admin/api-documentation.php:480
msgid "Use /pages endpoint"
msgstr ""

#: templates/admin/api-documentation.php:481
msgid "Use /products endpoint"
msgstr ""

#: templates/admin/api-documentation.php:482
msgid "Not supported"
msgstr ""

#: templates/admin/api-documentation.php:485
msgid "Custom Fields and Taxonomies"
msgstr ""

#: templates/admin/api-documentation.php:486
msgid ""
"Custom post type items can include custom meta fields and taxonomy terms. "
"These are automatically included in the response if they are configured in "
"the plugin settings."
msgstr ""

#: templates/admin/api-documentation.php:488
msgid "Available Parameters"
msgstr ""

#: templates/admin/api-documentation.php:489
msgid ""
"The custom post types endpoint supports the same parameters as other "
"endpoints:"
msgstr ""

#: templates/admin/api-documentation.php:491
msgid "Number of items to skip (default: 0)"
msgstr ""

#: templates/admin/api-documentation.php:492
msgid "Number of items to retrieve (default: 30, max: 100)"
msgstr ""

#: templates/admin/api-documentation.php:493
msgid "Field to sort by (default: modified,ID)"
msgstr ""

#: templates/admin/api-documentation.php:494
msgid "Sort order: ASC or DESC (default: ASC)"
msgstr ""

#: templates/admin/api-documentation.php:495
msgid "Filter items modified after this date (format: YYYY-MM-DD HH:MM:SS)"
msgstr ""

#: templates/admin/api-documentation.php:503
msgid "Error Handling"
msgstr ""

#: templates/admin/api-documentation.php:506
msgid ""
"The API returns standard HTTP status codes along with error messages in case "
"of failure."
msgstr ""

#: templates/admin/api-documentation.php:511
msgid "Status Code"
msgstr ""

#: templates/admin/api-documentation.php:519
msgid "Unauthorized - Invalid or missing token"
msgstr ""

#: templates/admin/api-documentation.php:526
msgid "Not Found - Resource does not exist"
msgstr ""

#: templates/admin/api-documentation.php:533
msgid "Bad Request - Invalid parameters"
msgstr ""

#: templates/admin/api-documentation.php:540
msgid "Server Error - Something went wrong on the server"
msgstr ""

#: templates/admin/api-documentation.php:553
msgid "Available API Fields"
msgstr ""

#: templates/admin/api-documentation.php:556
msgid ""
"The following sections show all available fields that can be included in API "
"responses. Select which fields you want to enable."
msgstr ""

#: templates/admin/api-documentation.php:567
msgid "Select which fields you want to include in the products API response."
msgstr ""

#: templates/admin/api-documentation.php:573
#: templates/admin/api-documentation.php:617
#: templates/admin/api-documentation.php:658
msgid "Field"
msgstr ""

#: templates/admin/api-documentation.php:595
#: templates/admin/api-documentation.php:636
#: templates/admin/api-documentation.php:677
msgid "Enabled"
msgstr ""

#: templates/admin/api-documentation.php:597
#: templates/admin/api-documentation.php:638
#: templates/admin/api-documentation.php:679
msgid "Disabled"
msgstr ""

#: templates/admin/api-documentation.php:611
msgid "Select which fields you want to include in the posts API response."
msgstr ""

#: templates/admin/api-documentation.php:652
msgid "Select which fields you want to include in the pages API response."
msgstr ""

#: templates/admin/api-documentation.php:691
msgid "Save Field Settings"
msgstr ""

#: templates/admin/settings.php:13
msgid "Muchat API Settings"
msgstr ""

#: templates/admin/settings.php:22
msgid "Muchat Server Connection"
msgstr ""

#: templates/admin/settings.php:32
msgid "Re-check"
msgstr ""

#: templates/admin/settings.php:78
msgid "Save Changes"
msgstr ""

#: templates/admin/settings.php:85
msgid "Refresh Fields"
msgstr ""

#: templates/admin/widget-settings.php:25
msgid "Muchat Chatbot Settings"
msgstr ""

#: templates/admin/widget-settings.php:32
msgid "Successfully disconnected from Muchat."
msgstr ""

#: templates/admin/widget-settings.php:38
msgid "Connect with Muchat to activate the chatbot on your website."
msgstr ""

#: templates/admin/widget-settings.php:41
msgid "Connect with Muchat"
msgstr ""

#: templates/admin/widget-settings.php:47
msgid "Connected with Muchat. You can now use Muchat throughout your website."
msgstr ""

#: templates/admin/widget-settings.php:49
msgid "Widget is currently disabled. Enable it in settings below."
msgstr ""

#: templates/admin/widget-settings.php:56
msgid "Connection Settings"
msgstr ""

#: templates/admin/widget-settings.php:58
msgid "Agent ID"
msgstr ""

#: templates/admin/widget-settings.php:62
msgid "Reconfigure"
msgstr ""

#: templates/admin/widget-settings.php:71
msgid ""
"Are you sure you want to disconnect from Muchat? This will remove your Agent "
"ID."
msgstr ""

#: templates/admin/widget-settings.php:72
msgid "Disconnect"
msgstr ""

#: templates/admin/widget-settings.php:79
msgid "Widget Customization"
msgstr ""

#: templates/admin/widget-settings.php:80
msgid "Customize your Muchat chatbot widget appearance in the Muchat dashboard"
msgstr ""

#: templates/admin/widget-settings.php:89
msgid "Customize Widget in Muchat Panel"
msgstr ""

#: templates/admin/widget-settings.php:108
msgid "Widget Display"
msgstr ""

#: templates/admin/widget-settings.php:111
msgid "Control the visibility of the Muchat chatbot widget on your site"
msgstr ""

#: templates/admin/widget-settings.php:114
msgid "Display Status"
msgstr ""

#: templates/admin/widget-settings.php:118
msgid "Enable Muchat Widget"
msgstr ""

#: templates/admin/widget-settings.php:121
msgid ""
"You can temporarily disable the Muchat chatbot widget without losing your "
"settings."
msgstr ""

#: templates/admin/widget-settings.php:132
msgid "Contact Information"
msgstr ""

#: templates/admin/widget-settings.php:135
msgid ""
"User information for personalizing the conversation experience (optional)"
msgstr ""

#: templates/admin/widget-settings.php:138
msgid "User Info"
msgstr ""

#: templates/admin/widget-settings.php:142
msgid "Automatically send logged-in user information"
msgstr ""

#: templates/admin/widget-settings.php:145
msgid ""
"When enabled, the plugin will send the name and email of logged-in users to "
"personalize chat interactions."
msgstr ""

#: templates/admin/widget-settings.php:156
msgid "Interface Settings"
msgstr ""

#: templates/admin/widget-settings.php:159
msgid "Customize the appearance and initial messages of the chatbot"
msgstr ""

#: templates/admin/widget-settings.php:162
msgid "Initial Messages"
msgstr ""

#: templates/admin/widget-settings.php:167
#: templates/admin/widget-settings.php:190
msgid "Add Message"
msgstr ""

#: templates/admin/widget-settings.php:171
msgid ""
"Add welcome messages that will be shown when a visitor opens the chat "
"widget. You can include personalization variables such as"
msgstr ""

#: templates/admin/widget-settings.php:171
#: templates/admin/widget-settings.php:246
msgid "and"
msgstr ""

#: templates/admin/widget-settings.php:171
msgid "example:"
msgstr ""

#: templates/admin/widget-settings.php:171
msgid ""
"Add multiple messages to create a conversation flow (each message will "
"appear in sequence). If left empty, default settings from your Muchat "
"dashboard will be used."
msgstr ""

#: templates/admin/widget-settings.php:176
msgid "Different Messages for Guests"
msgstr ""

#: templates/admin/widget-settings.php:180
msgid "Enable different initial messages for non-logged-in users."
msgstr ""

#: templates/admin/widget-settings.php:185
msgid "Guest Initial Messages"
msgstr ""

#: templates/admin/widget-settings.php:194
msgid ""
"These messages will be shown to visitors who are not logged in. If left "
"empty, the default messages above will be used."
msgstr ""

#: templates/admin/widget-settings.php:199
msgid "Load Strategy"
msgstr ""

#: templates/admin/widget-settings.php:202
msgid "Fast (Recommended)"
msgstr ""

#: templates/admin/widget-settings.php:203
msgid "SEO Friendly"
msgstr ""

#: templates/admin/widget-settings.php:204
msgid "Complete Load"
msgstr ""

#: templates/admin/widget-settings.php:207
msgid ""
"Controls how the chatbot loads on your website. Fast is recommended for most "
"sites."
msgstr ""

#: templates/admin/widget-settings.php:212
msgid "Script Load Position"
msgstr ""

#: templates/admin/widget-settings.php:215
msgid "Footer (Recommended)"
msgstr ""

#: templates/admin/widget-settings.php:216
msgid "Header"
msgstr ""

#: templates/admin/widget-settings.php:217
msgid "Body Opening"
msgstr ""

#: templates/admin/widget-settings.php:220
msgid "Footer (Recommended):"
msgstr ""

#: templates/admin/widget-settings.php:220
msgid ""
"Best for page speed and mobile compatibility. Loads after the page content."
msgstr ""

#: templates/admin/widget-settings.php:221
msgid "Header:"
msgstr ""

#: templates/admin/widget-settings.php:221
msgid ""
"Loads in the <head> section. Use if you need the widget available as early "
"as possible."
msgstr ""

#: templates/admin/widget-settings.php:222
msgid "Body Opening:"
msgstr ""

#: templates/admin/widget-settings.php:222
msgid ""
"Loads right after <body> tag. Requires WordPress 5.2+ and modern theme "
"support."
msgstr ""

#: templates/admin/widget-settings.php:233
msgid "Dynamic Prompt"
msgstr ""

#: templates/admin/widget-settings.php:236
msgid "Configure the context prompt that will be sent to the AI model"
msgstr ""

#: templates/admin/widget-settings.php:239
msgid "Context for Logged-in Users"
msgstr ""

#: templates/admin/widget-settings.php:243
msgid ""
"Provide additional context that will be appended to the Agent system prompt."
msgstr ""

#: templates/admin/widget-settings.php:246
msgid "You can use variables like"
msgstr ""

#: templates/admin/widget-settings.php:246
msgid "in the context. Example:"
msgstr ""

#: templates/admin/widget-settings.php:246
msgid ""
"The user you are talking to is $name $lastname (Email: $email, Phone: "
"$phone). They are currently on the page: $page_title. Start by greeting them "
"by name."
msgstr ""

#: templates/admin/widget-settings.php:251
msgid "Different Context for Guests"
msgstr ""

#: templates/admin/widget-settings.php:255
msgid "Enable different context for non-logged-in users."
msgstr ""

#: templates/admin/widget-settings.php:260
msgid "Context for Guests"
msgstr ""

#: templates/admin/widget-settings.php:264
msgid ""
"This context will be used for visitors who are not logged in. If left empty, "
"no context will be sent for guests."
msgstr ""

#: templates/admin/widget-settings.php:267
msgid "Example:"
msgstr ""

#: templates/admin/widget-settings.php:278
msgid "Display Rules"
msgstr ""

#: templates/admin/widget-settings.php:281
msgid ""
"Specify which pages the Muchat chatbot should appear on or be hidden from"
msgstr ""

#: templates/admin/widget-settings.php:284
msgid "Display Mode"
msgstr ""

#: templates/admin/widget-settings.php:289
msgid "Show on all pages except those listed below"
msgstr ""

#: templates/admin/widget-settings.php:294
msgid "Only show on pages listed below"
msgstr ""

#: templates/admin/widget-settings.php:300
msgid "Page List"
msgstr ""

#: templates/admin/widget-settings.php:307
msgid "One pattern per line. Use"
msgstr ""

#: templates/admin/widget-settings.php:307
msgid "for homepage,"
msgstr ""

#: templates/admin/widget-settings.php:307
msgid "for wildcards."
msgstr ""

#: templates/admin/widget-settings.php:310
msgid "English examples:"
msgstr ""

#: templates/admin/widget-settings.php:313
msgid "Persian/RTL examples:"
msgstr ""

#: templates/admin/widget-settings.php:316
msgid ""
"URLs are case-insensitive (/About = /about). Persian/Arabic URLs are fully "
"supported."
msgstr ""

#: templates/admin/widget-settings.php:326
msgid ""
"If you use a caching plugin and display rules don't work correctly, please "
"clear your site cache."
msgstr ""

#: templates/admin/widget-settings.php:335
msgid "Schedule Settings"
msgstr ""

#: templates/admin/widget-settings.php:338
msgid "Configure when the Muchat chatbot should be available on your website"
msgstr ""

#: templates/admin/widget-settings.php:342
msgid "Enable Schedule"
msgstr ""

#: templates/admin/widget-settings.php:346
msgid "Enable scheduling"
msgstr ""

#: templates/admin/widget-settings.php:349
msgid ""
"When enabled, the chatbot will only be available during specified days and "
"times."
msgstr ""

#: templates/admin/widget-settings.php:354
msgid "Active Days"
msgstr ""

#: templates/admin/widget-settings.php:358
msgid "Saturday"
msgstr ""

#: templates/admin/widget-settings.php:359
msgid "Sunday"
msgstr ""

#: templates/admin/widget-settings.php:360
msgid "Monday"
msgstr ""

#: templates/admin/widget-settings.php:361
msgid "Tuesday"
msgstr ""

#: templates/admin/widget-settings.php:362
msgid "Wednesday"
msgstr ""

#: templates/admin/widget-settings.php:363
msgid "Thursday"
msgstr ""

#: templates/admin/widget-settings.php:364
msgid "Friday"
msgstr ""

#: templates/admin/widget-settings.php:383
msgid ""
"Select the days when the chatbot should be available. If no days are "
"selected, the chatbot will be available every day."
msgstr ""

#: templates/admin/widget-settings.php:388
msgid "Active Hours"
msgstr ""

#: templates/admin/widget-settings.php:391
msgid "to"
msgstr ""

#: templates/admin/widget-settings.php:394
msgid ""
"Set the time range during which the chatbot should be available. If no time "
"is set, the chatbot will be available 24/7."
msgstr ""

#: templates/admin/widget-settings.php:395
msgid "Times are based on your website's timezone:"
msgstr ""

#: templates/admin/widget-settings.php:405
msgid "Save Settings"
msgstr ""

#: templates/admin/product-example.php:35
msgid ""
"This page allows you to view how a specific product will appear in API "
"responses."
msgstr ""

#. translators: %1$s: opening link tag, %2$s: closing link tag
#: templates/admin/product-example.php:39
#, php-format
msgid ""
"To modify which fields are included, visit the %1$sAPI Settings%2$s page."
msgstr ""

#: templates/admin/product-example.php:49
msgid "Select Product"
msgstr ""

#: templates/admin/product-example.php:56
msgid "Select a Product"
msgstr ""

#: templates/admin/product-example.php:64
msgid "Search for a product..."
msgstr ""

#: templates/admin/product-example.php:80
msgid "Get Example"
msgstr ""

#: templates/admin/product-example.php:83
msgid "Search for a product to view its API response."
msgstr ""

#: templates/admin/product-example.php:93
msgid "API Response Example"
msgstr ""

#: templates/admin/product-example.php:98
msgid "JSON Response"
msgstr ""

#: templates/admin/product-example.php:109
msgid "Product not found or error retrieving data."
msgstr ""
</file>

<file path="includes/Api/Middleware/RateLimitMiddleware.php">
<?php

namespace Muchat\Api\Api\Middleware;

/**
 * Rate Limiting Middleware for API endpoints
 * 
 * SECURITY: Prevents abuse through:
 * - Denial of Service attacks
 * - Brute force attempts
 * - Data scraping
 * - API abuse
 * 
 * Uses a sliding window algorithm with transient-based storage.
 * Compatible with object cache (Redis/Memcached) for distributed rate limiting.
 *
 * @package Muchat\Api\Api\Middleware
 */
class RateLimitMiddleware
{
    /**
     * Default rate limit: requests per window
     */
    const DEFAULT_RATE_LIMIT = 100;
    
    /**
     * Default time window in seconds
     */
    const DEFAULT_RATE_WINDOW = 60;
    
    /**
     * Stricter limit for unauthenticated requests
     */
    const UNAUTHENTICATED_RATE_LIMIT = 30;

    /**
     * Higher limit for authenticated Muchat requests (us)
     * 2000 requests per minute allows for fast data synchronization
     */
    const AUTHENTICATED_RATE_LIMIT = 2000;
    
    /**
     * Rate limit for sensitive endpoints
     */
    const SENSITIVE_RATE_LIMIT = 20;
    
    /**
     * Transient prefix for rate limit data
     */
    const TRANSIENT_PREFIX = 'muchat_rate_';
    
    /**
     * Check rate limit for a request
     * 
     * @param \WP_REST_Request $request The incoming request
     * @param string $endpoint_type Type of endpoint: 'default', 'sensitive', 'public'
     * @return bool|\WP_Error True if allowed, WP_Error if rate limited
     */
    public function check_rate_limit($request, $endpoint_type = 'default')
    {
        // Get rate limit based on endpoint type and authentication status
        $limit = $this->get_rate_limit($endpoint_type, $request);
        $window = self::DEFAULT_RATE_WINDOW;
        
        // Get client identifier
        $identifier = $this->get_client_identifier($request);
        $cache_key = self::TRANSIENT_PREFIX . $endpoint_type . '_' . $identifier;
        
        // Get current request count
        $rate_data = get_transient($cache_key);
        
        if ($rate_data === false) {
            // First request in window
            $rate_data = [
                'count' => 1,
                'window_start' => time(),
            ];
            set_transient($cache_key, $rate_data, $window);
            
            return true;
        }
        
        // Check if within rate limit
        if ($rate_data['count'] >= $limit) {
            $retry_after = $window - (time() - $rate_data['window_start']);
            $retry_after = max(1, $retry_after);
            
            // Log rate limit hit for security monitoring
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log(sprintf(
                    'Muchat Rate Limit: Client %s exceeded %d requests on %s endpoint (Auth: %s)',
                    $identifier,
                    $limit,
                    $endpoint_type,
                    $this->is_authenticated($request) ? 'Yes' : 'No'
                ));
            }
            
            return new \WP_Error(
                'rate_limit_exceeded',
                sprintf(
                    /* translators: %d: number of seconds to wait before retrying */
                    __('Rate limit exceeded. Please try again in %d seconds.', 'muchat-ai'),
                    $retry_after
                ),
                [
                    'status' => 429,
                    'headers' => [
                        'Retry-After' => $retry_after,
                        'X-RateLimit-Limit' => $limit,
                        'X-RateLimit-Remaining' => 0,
                        'X-RateLimit-Reset' => $rate_data['window_start'] + $window,
                    ]
                ]
            );
        }
        
        // Increment counter
        $rate_data['count']++;
        $remaining_ttl = $window - (time() - $rate_data['window_start']);
        set_transient($cache_key, $rate_data, max(1, $remaining_ttl));
        
        return true;
    }
    
    /**
     * Get rate limit for endpoint type
     * 
     * @param string $endpoint_type Type of endpoint
     * @param \WP_REST_Request|null $request The request object
     * @return int Rate limit
     */
    private function get_rate_limit($endpoint_type, $request = null)
    {
        // If authenticated (MuChat), use the high limit to avoid throttling ourselves
        if ($request && $this->is_authenticated($request)) {
            return self::AUTHENTICATED_RATE_LIMIT;
        }

        $limits = [
            'default' => self::DEFAULT_RATE_LIMIT,
            'sensitive' => self::SENSITIVE_RATE_LIMIT,
            'public' => self::UNAUTHENTICATED_RATE_LIMIT,
        ];
        
        /**
         * Filter to customize rate limits
         * 
         * @param array $limits Array of limits by endpoint type
         * @return array Modified limits
         */
        $limits = apply_filters('muchat_rate_limits', $limits);
        
        return $limits[$endpoint_type] ?? self::DEFAULT_RATE_LIMIT;
    }
    
    /**
     * Check if request is authenticated with a Bearer token
     * 
     * @param \WP_REST_Request $request
     * @return bool
     */
    private function is_authenticated($request)
    {
        $auth_header = $request->get_header('Authorization');
        return $auth_header && preg_match('/^Bearer\s+(.*)$/i', $auth_header);
    }
    
    /**
     * Get unique identifier for the client
     * 
     * Uses token hash if authenticated, otherwise IP address.
     * This prevents rate limit bypass by using multiple tokens.
     * 
     * @param \WP_REST_Request $request The incoming request
     * @return string Hashed client identifier
     */
    private function get_client_identifier($request)
    {
        // Use token if authenticated (prevents rate limit per-token bypass)
        $auth_header = $request->get_header('Authorization');
        if ($auth_header && preg_match('/^Bearer\s+(.*)$/i', $auth_header, $matches)) {
            // Hash the token for privacy
            return hash('sha256', 'token_' . $matches[1]);
        }
        
        // Fall back to IP address for unauthenticated requests
        $ip = $this->get_client_ip();
        
        // Hash the IP for privacy (in case logs are exposed)
        return hash('sha256', 'ip_' . $ip);
    }
    
    /**
     * Get client IP address
     * 
     * Handles proxies and load balancers safely.
     * 
     * @return string IP address
     */
    private function get_client_ip()
    {
        // Check for proxy headers (in order of trust)
        $headers = [
            'HTTP_CF_CONNECTING_IP',     // Cloudflare
            'HTTP_X_REAL_IP',            // Nginx proxy
            'HTTP_X_FORWARDED_FOR',      // Standard proxy header
            'REMOTE_ADDR',               // Direct connection
        ];
        
        foreach ($headers as $header) {
            if (!empty($_SERVER[$header])) {
                $ip = $_SERVER[$header];
                
                // X-Forwarded-For can contain multiple IPs, take the first one
                if ($header === 'HTTP_X_FORWARDED_FOR') {
                    $ips = explode(',', $ip);
                    $ip = trim($ips[0]);
                }
                
                // Validate IP format
                if (filter_var($ip, FILTER_VALIDATE_IP)) {
                    return $ip;
                }
            }
        }
        
        return '0.0.0.0'; // Fallback
    }
    
    /**
     * Add rate limit headers to response
     * 
     * @param \WP_REST_Response $response The response object
     * @param \WP_REST_Request $request The request object
     * @param string $endpoint_type Type of endpoint
     * @return \WP_REST_Response Modified response
     */
    public function add_rate_limit_headers($response, $request, $endpoint_type = 'default')
    {
        $limit = $this->get_rate_limit($endpoint_type, $request);
        $identifier = $this->get_client_identifier($request);
        $cache_key = self::TRANSIENT_PREFIX . $endpoint_type . '_' . $identifier;
        
        $rate_data = get_transient($cache_key);
        
        if ($rate_data !== false) {
            $remaining = max(0, $limit - $rate_data['count']);
            $reset = $rate_data['window_start'] + self::DEFAULT_RATE_WINDOW;
            
            $response->header('X-RateLimit-Limit', $limit);
            $response->header('X-RateLimit-Remaining', $remaining);
            $response->header('X-RateLimit-Reset', $reset);
        }
        
        return $response;
    }
    
    /**
     * Clear rate limit for a client (for admin use)
     * 
     * @param string $identifier Client identifier or IP
     */
    public static function clear_rate_limit($identifier)
    {
        $hashed = hash('sha256', 'ip_' . $identifier);
        
        foreach (['default', 'sensitive', 'public'] as $type) {
            delete_transient(self::TRANSIENT_PREFIX . $type . '_' . $hashed);
        }
    }
}
</file>

<file path="includes/Core/Deactivator.php">
<?php

namespace Muchat\Api\Core;

class Deactivator
{
    /**
     * Fired during plugin deactivation
     *
     * @return void
     */
    public function deactivate()
    {
        // Clear any cached data
        delete_transient('muchat_ai_chatbot_plugin_cache');

        // Flush rewrite rules
        flush_rewrite_rules();
    }
}
</file>

<file path="includes/Models/CustomPostType.php">
<?php

namespace Muchat\Api\Models;

use Muchat\Api\Traits\SecureOrderHandling;

class CustomPostType extends BaseModel
{
    use SecureOrderHandling;

    /**
     * Get custom post type items
     *
     * @param array $params
     * @param string $post_type
     * @return array
     */
    public function get_custom_post_type_items($params, $post_type)
    {
        // Validate post type
        if (!$this->is_valid_post_type($post_type)) {
            return [
                'error' => sprintf(__('Invalid post type: %s', 'muchat-ai'), $post_type)
            ];
        }

        // Base query args
        $args = [
            'post_type' => $post_type,
            'post_status' => 'publish',
            'fields' => 'ids',
        ];

        // SECURITY: Use validated orderby from whitelist
        $args['orderby'] = $this->sanitize_orderby(
            $params['order_by'] ?? null,
            $params['order'] ?? 'ASC'
        );

        // Note: Do NOT set $args['order'] when using array-based orderby
        // as it will override the individual sort orders in the array

        // Add date filters if provided
        if (!empty($params['modified_after'])) {
            $modified_after_timestamp = strtotime($params['modified_after']);
            if ($modified_after_timestamp) {
                $args['date_query'][] = [
                    'column' => 'post_modified_gmt',
                    'after' => gmdate('Y-m-d H:i:s', $modified_after_timestamp),
                    'inclusive' => false
                ];
            }
        }

        // Set pagination parameters with boundary validation
        $requested_offset = max(0, isset($params['skip']) ? (int) $params['skip'] : 0);
        $requested_limit = min(max(1, isset($params['take']) ? (int) $params['take'] : 10), 100);

        $args['posts_per_page'] = $requested_limit;
        $args['offset'] = $requested_offset;
        $args['no_found_rows'] = false; // We need found_posts for total_count

        // Execute the final query with pagination
        $query = new \WP_Query($args);

        // Get total count from the same query
        $total_count = (int) $query->found_posts;

        // Fallback: If found_posts is 0 but we have results, some optimization plugin
        // may have set no_found_rows to true. Run a separate COUNT query.
        if ($total_count === 0 && !empty($query->posts)) {
            $count_args = $args;
            $count_args['posts_per_page'] = -1;
            $count_args['offset'] = 0;
            $count_args['fields'] = 'ids';
            $count_args['no_found_rows'] = true; // We only need the count, not pagination info
            $count_query = new \WP_Query($count_args);
            $total_count = $count_query->post_count;
        }

        // Prime post cache to avoid N+1 queries
        if (!empty($query->posts)) {
            _prime_post_caches($query->posts, true, true);
        }

        // Format the items
        $items = array_map(function ($item_id) use ($post_type) {
            return $this->format_custom_post_type_item(get_post($item_id), $post_type, 'list');
        }, $query->posts);

        $plugin = new \Muchat\Api\Core\Plugin();

        return [
            'plugin_version' => $plugin->get_version(),
            'offset' => $requested_offset,
            'limit' => $requested_limit,
            'total_count' => $total_count,
            'has_more' => ($requested_offset + count($items) < $total_count),
            'items' => array_values(array_filter($items))
        ];
    }

    /**
     * SECURITY: List of sensitive custom post types that should never be exposed
     * These may contain PII, financial data, or internal business information.
     * 
     * @var array
     */
    private const SENSITIVE_POST_TYPES = [
        // WooCommerce sensitive CPTs
        'shop_order',
        'shop_order_refund', 
        'shop_subscription',
        'shop_coupon',
        'scheduled-action',
        
        // Easy Digital Downloads sensitive CPTs
        'edd_payment',
        'edd_discount',
        'edd_log',
        'edd_license',
        'edd_license_log',
        
        // User/Customer data CPTs
        'user_request',           // GDPR requests
        'wpcf7_contact_form',     // Contact Form 7 submissions
        'flamingo_contact',       // Flamingo (CF7 addon)
        'flamingo_inbound',
        'wpforms',                // WPForms
        'wpforms_entry',
        'gf_entry',               // Gravity Forms
        'ninja_forms_entry',      // Ninja Forms
        
        // Internal WordPress CPTs
        'customize_changeset',
        'custom_css',
        'wp_global_styles',
        'wp_navigation',
        'wp_template',
        'wp_template_part',
        'wp_block',
        'oembed_cache',
        'wp_font_family',
        'wp_font_face',
        
        // ACF internal CPTs
        'acf-field-group',
        'acf-field',
        'acf-post-type',
        'acf-taxonomy',
        
        // Cart/Wishlist CPTs (contain user shopping behavior)
        'abandoned_cart',
        'ywalt_cart',             // YITH Abandoned Cart
        'customer_wishlist',
        'yith_wishlist',
        
        // Notification/Email CPTs
        'notification',
        'email_log',
        'wpml_notification',
        
        // Security/Audit CPTs
        'audit_log',
        'activity_log',
        'login_attempt',
        
        // Internal plugin data
        'wc_webhook',
        'wc_order_stats',
        'wc_reserved_stock',
        'action-group',
    ];
    
    /**
     * Validate if post type exists and is safe to expose
     * 
     * SECURITY: This method implements defense-in-depth by:
     * 1. Excluding built-in WordPress post types
     * 2. Excluding known sensitive post types from popular plugins
     * 3. Only allowing public post types or those with REST API access
     * 4. Providing a filter for site admins to customize the exclusion list
     *
     * @param string $post_type
     * @return bool
     */
    private function is_valid_post_type($post_type)
    {
        // Exclude built-in post types that have their own endpoints
        $excluded_types = ['post', 'page', 'product', 'attachment', 'revision', 'nav_menu_item'];
        
        if (in_array($post_type, $excluded_types, true)) {
            return false;
        }
        
        // SECURITY: Exclude sensitive post types that may contain PII or financial data
        $sensitive_types = self::SENSITIVE_POST_TYPES;
        
        /**
         * Filter to add additional sensitive post types to block
         * 
         * @param array $sensitive_types Array of post type slugs to block
         * @return array Modified array of post types to block
         */
        $sensitive_types = apply_filters('muchat_sensitive_post_types', $sensitive_types);
        
        if (in_array($post_type, $sensitive_types, true)) {
            // Log attempt for security monitoring
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log(sprintf(
                    'Muchat Security: Blocked access to sensitive post type: %s',
                    $post_type
                ));
            }
            return false;
        }

        // Check if post type exists
        $post_type_object = get_post_type_object($post_type);
        
        if (!$post_type_object) {
            return false;
        }

        // Only allow public post types or those explicitly set to show in REST API
        if (!$post_type_object->public && !$post_type_object->show_in_rest) {
            return false;
        }
        
        // SECURITY: Additional check - if the post type has restricted capabilities,
        // ensure they're not bypassing permission requirements
        if (!empty($post_type_object->cap->read_private_posts)) {
            // This CPT has special read capabilities - be extra cautious
            $required_cap = $post_type_object->cap->read_private_posts;
            
            // Check if this is a sensitive capability pattern
            $sensitive_caps = ['read_private_', 'manage_', 'edit_others_'];
            foreach ($sensitive_caps as $pattern) {
                if (strpos($required_cap, $pattern) === 0) {
                    // Log and allow - the CPT is public but has private post capability
                    // The is_valid_post_type check happens before any specific post access
                    break;
                }
            }
        }

        return true;
    }

    /**
     * Format custom post type item data
     *
     * @param \WP_Post $item
     * @param string $post_type
     * @param string $context Either 'list' or 'single' - determines formatting depth
     * @return array|null
     */
    protected function format_custom_post_type_item($item, $post_type, $context = 'list')
    {
        $data = [];
        
        // Get enabled fields for this custom post type
        $enabled_fields = $this->get_enabled_fields($post_type);

        // If no specific fields are configured, use default fields
        if (empty($enabled_fields)) {
            $enabled_fields = $this->get_default_fields_for_post_type($post_type);
        }

        foreach ($enabled_fields as $field) {
            switch ($field) {
                case 'id':
                    $data['id'] = $item->ID;
                    break;
                case 'title':
                    $data['title'] = get_the_title($item);
                    break;
                case 'content':
                    if ($context === 'single') {
                        $data['content'] = $this->formatter->clean_html_content(apply_filters('the_content', $item->post_content));
                    } else {
                        $data['content'] = $this->formatter->clean_html_content_light(apply_filters('the_content', $item->post_content), 50);
                    }
                    break;
                case 'excerpt':
                    if ($context === 'single') {
                        $data['excerpt'] = $this->formatter->clean_html_content(get_the_excerpt($item));
                    } else {
                        $data['excerpt'] = $this->formatter->clean_html_content_light(get_the_excerpt($item), 30);
                    }
                    break;
                case 'modified_date':
                    $post_data = get_post($item->ID);
                    if (isset($post_data->post_modified_gmt) && $post_data->post_modified_gmt !== '0000-00-00 00:00:00') {
                        try {
                            // Create a DateTime object from the GMT/UTC time provided by WordPress
                            $datetime = new \DateTime($post_data->post_modified_gmt, new \DateTimeZone('UTC'));
                            // Format it to the exact ISO 8601 format with milliseconds and 'Z'
                            $data['date_modified'] = $datetime->format('Y-m-d\TH:i:s.v\Z');
                        } catch (\Exception $e) {
                            // In case of an error, fallback to null
                            $data['date_modified'] = null;
                        }
                    } else {
                        $data['date_modified'] = null;
                    }
                    break;
                case 'permalink':
                    $data['url'] = urldecode(get_permalink($item->ID));
                    break;
                case 'featured_image':
                    $data['featured_image'] = $this->get_featured_image($item);
                    break;
                default:
                    // Handle custom fields (meta fields)
                    if (strpos($field, 'meta_') === 0) {
                        $meta_key = substr($field, 5); // Remove 'meta_' prefix
                        $meta_value = get_post_meta($item->ID, $meta_key, true);
                        if ($meta_value !== false && $meta_value !== '') {
                            $data[$meta_key] = $meta_value;
                        }
                    }
                    // Handle taxonomy fields
                    elseif (strpos($field, 'tax_') === 0) {
                        $taxonomy = substr($field, 4); // Remove 'tax_' prefix
                        $data[$taxonomy] = $this->get_taxonomy_terms($item, $taxonomy);
                    }
                    break;
            }
        }

        // Get custom meta fields if configured
        $options = $this->get_options();
        if (isset($options["{$post_type}_meta_fields"]) && is_array($options["{$post_type}_meta_fields"])) {
            foreach ($options["{$post_type}_meta_fields"] as $meta_key) {
                $meta_value = get_post_meta($item->ID, $meta_key, true);
                if ($meta_value !== false && $meta_value !== '') {
                    // Use custom label if available
                    $label = $this->get_meta_field_label($meta_key, $post_type);
                    $data[$label] = $meta_value;
                }
            }
        }

        $cleaned_data = $this->formatter->remove_empty_values($data);

        // Ensure the ID is always present to prevent the item from being filtered out
        if (!isset($cleaned_data['id'])) {
            $cleaned_data['id'] = $item->ID;
        }

        return $cleaned_data;
    }

    /**
     * Get default fields for a post type
     *
     * @param string $post_type
     * @return array
     */
    private function get_default_fields_for_post_type($post_type)
    {
        return ['id', 'title', 'content', 'excerpt', 'modified_date', 'permalink', 'featured_image'];
    }

    /**
     * Get featured image
     *
     * @param \WP_Post $item
     * @return string|null
     */
    protected function get_featured_image($item)
    {
        if (!has_post_thumbnail($item)) {
            return null;
        }

        $image_id = get_post_thumbnail_id($item);
        $image_url = wp_get_attachment_url($image_id);

        return $image_url ?: null;
    }

    /**
     * Get taxonomy terms
     *
     * @param \WP_Post $item
     * @param string $taxonomy
     * @return array
     */
    protected function get_taxonomy_terms($item, $taxonomy)
    {
        $terms = get_the_terms($item->ID, $taxonomy);
        $term_data = [];

        if ($terms && !is_wp_error($terms)) {
            foreach ($terms as $term) {
                $term_data[urldecode($term->name)] = urldecode(get_term_link($term, $taxonomy));
            }
        }

        return $term_data;
    }

    /**
     * Get meta field label
     *
     * @param string $meta_key
     * @param string $post_type
     * @return string
     */
    private function get_meta_field_label($meta_key, $post_type)
    {
        $options = $this->get_options();
        if (isset($options['meta_labels'][$meta_key])) {
            return $options['meta_labels'][$meta_key];
        }
        return $meta_key;
    }
}
</file>

<file path="includes/Utils/ProductChangeTracker.php">
<?php

namespace Muchat\Api\Utils;

/**
 * Track product changes for prices and stock status
 * Updates _muchat_date_modified only when significant changes occur
 */
class ProductChangeTracker
{
    /**
     * Meta key for muchat modified date
     */
    const META_KEY = '_muchat_date_modified';

    /**
     * Track if we're currently processing to prevent infinite loops
     * NOTE: This is process-local; database locking handles cross-process concurrency
     */
    private static $processing_products = [];

    /**
     * Lock timeout in seconds for each database-level lock attempt
     */
    const LOCK_TIMEOUT = 2;
    
    /**
     * Maximum number of retry attempts for acquiring a lock
     */
    const LOCK_MAX_RETRIES = 3;
    
    /**
     * Backoff delay in microseconds between retry attempts
     */
    const LOCK_RETRY_DELAY_US = 100000; // 100ms
    
    /**
     * Cache for GET_LOCK availability check
     * 
     * @var bool|null
     */
    private static $get_lock_available = null;
    
    /**
     * Transient-based lock prefix for fallback
     */
    const TRANSIENT_LOCK_PREFIX = 'muchat_plock_';

    /**
     * Get the parent product ID if the given ID is for a variation.
     *
     * @param int $product_or_variation_id The ID of the product or variation.
     * @return int The ID of the main product.
     */
    private function get_main_product_id($product_or_variation_id)
    {
        $product = wc_get_product($product_or_variation_id);
        if ($product && $product->is_type('variation')) {
            return $product->get_parent_id();
        }
        return $product_or_variation_id;
    }

    /**
     * Generate a unique lock name for a product
     * 
     * SECURITY: Uses a hash of plugin version to avoid collisions with other plugins
     * that might use similar lock naming patterns.
     *
     * @param int $product_id The product ID
     * @return string Lock name (truncated to MySQL's 64-character limit)
     */
    private function get_lock_name($product_id)
    {
        // Use a unique prefix combining plugin identifier and version hash
        // This prevents lock name collisions with other plugins
        $version_hash = substr(wp_hash('muchat_product_lock_v1'), 0, 8);
        $lock_name = 'mcp_' . $version_hash . '_' . intval($product_id);
        
        // MySQL lock names are limited to 64 characters
        return substr($lock_name, 0, 64);
    }
    
    /**
     * Check if MySQL GET_LOCK is available on this server
     * 
     * COMPATIBILITY: Some shared hosting providers disable GET_LOCK
     * or restrict its usage. We test once and cache the result.
     *
     * @return bool True if GET_LOCK is available
     */
    private function is_get_lock_available()
    {
        if (self::$get_lock_available !== null) {
            return self::$get_lock_available;
        }
        
        // Check transient cache first
        $cached = get_transient('muchat_get_lock_status');
        if ($cached !== false) {
            self::$get_lock_available = ($cached === 'yes');
            return self::$get_lock_available;
        }
        
        global $wpdb;
        
        // Test if GET_LOCK works
        $test_lock = 'muchat_test_' . wp_generate_password(8, false);
        
        // Suppress errors during test
        $suppress = $wpdb->suppress_errors(true);
        
        try {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $result = $wpdb->get_var(
                $wpdb->prepare("SELECT GET_LOCK(%s, %d)", $test_lock, 0)
            );
            
            $available = ($result === '1' || $result === 1);
            
            if ($available) {
                // Release the test lock
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $wpdb->query($wpdb->prepare("SELECT RELEASE_LOCK(%s)", $test_lock));
            }
            
            // Check for SQL errors that indicate GET_LOCK is not available
            if (!empty($wpdb->last_error)) {
                $available = false;
            }
            
        } catch (\Exception $e) {
            $available = false;
        }
        
        $wpdb->suppress_errors($suppress);
        
        self::$get_lock_available = $available;
        
        // Cache the result for 1 hour
        set_transient('muchat_get_lock_status', $available ? 'yes' : 'no', HOUR_IN_SECONDS);
        
        if (!$available && defined('WP_DEBUG') && WP_DEBUG) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log('Muchat: GET_LOCK not available, using transient-based fallback');
        }
        
        return $available;
    }
    
    /**
     * Acquire a database-level lock for a product
     * 
     * CONCURRENCY: Uses MySQL GET_LOCK which works across PHP-FPM workers.
     * This prevents race conditions in multi-process environments.
     * 
     * COMPATIBILITY: Falls back to transient-based locking on shared hosting
     * where GET_LOCK may be disabled or restricted.
     * 
     * IMPROVEMENTS:
     * - Uses unique prefix to avoid collisions with other plugins
     * - Implements retry logic with exponential backoff
     * - Shorter timeout per attempt for faster failure detection
     * - Logs failures for debugging in WP_DEBUG mode
     * - Falls back to transient locks when GET_LOCK unavailable
     *
     * @param int $product_id The product ID to lock
     * @return bool True if lock acquired, false otherwise
     */
    private function acquire_product_lock($product_id)
    {
        // Check if GET_LOCK is available, use fallback if not
        if (!$this->is_get_lock_available()) {
            return $this->acquire_transient_lock($product_id);
        }
        
        global $wpdb;

        $lock_name = $this->get_lock_name($product_id);
        $attempt = 0;
        $delay = self::LOCK_RETRY_DELAY_US;

        while ($attempt < self::LOCK_MAX_RETRIES) {
            // GET_LOCK returns 1 if lock acquired, 0 if timeout, NULL on error
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $result = $wpdb->get_var(
                $wpdb->prepare("SELECT GET_LOCK(%s, %d)", $lock_name, self::LOCK_TIMEOUT)
            );

            if ($result === '1' || $result === 1) {
                return true;
            }
            
            // If result is NULL, GET_LOCK had an error - fall back
            if ($result === null) {
                self::$get_lock_available = false;
                set_transient('muchat_get_lock_status', 'no', HOUR_IN_SECONDS);
                return $this->acquire_transient_lock($product_id);
            }
            
            $attempt++;
            
            if ($attempt < self::LOCK_MAX_RETRIES) {
                // Exponential backoff: 100ms, 200ms, 400ms...
                usleep($delay);
                $delay *= 2;
            }
        }
        
        // Log failed lock acquisition for debugging
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log(sprintf(
                'Muchat: Failed to acquire lock for product #%d after %d attempts',
                $product_id,
                self::LOCK_MAX_RETRIES
            ));
        }

        return false;
    }
    
    /**
     * Acquire an option-based lock (fallback for shared hosting)
     * 
     * COMPATIBILITY: This is a fallback when MySQL GET_LOCK is not available.
     * 
     * CONCURRENCY FIX: Uses add_option() instead of get_transient()/set_transient().
     * add_option() is atomic - if the option already exists, it returns false.
     * This eliminates the race condition between checking and setting.
     * 
     * Transient-based locks had a race condition:
     * 1. Process A calls get_transient() -> returns false
     * 2. Process B calls get_transient() -> returns false (race!)
     * 3. Both processes call set_transient() and think they have the lock
     *
     * @param int $product_id The product ID to lock
     * @return bool True if lock acquired, false otherwise
     */
    private function acquire_transient_lock($product_id)
    {
        $lock_key = self::TRANSIENT_LOCK_PREFIX . intval($product_id);
        $lock_timeout = self::LOCK_TIMEOUT * self::LOCK_MAX_RETRIES; // Total timeout
        
        // Generate a unique lock value with process ID and timestamp
        $lock_value = wp_generate_password(16, false) . '_' . getmypid() . '_' . microtime(true);
        
        $attempt = 0;
        $delay = self::LOCK_RETRY_DELAY_US;
        
        // PERFORMANCE: Use object cache for locking when available (Redis/Memcached)
        // wp_cache_add is atomic and faster than database operations
        $use_object_cache = wp_using_ext_object_cache();
        
        while ($attempt < self::LOCK_MAX_RETRIES) {
            $acquired = false;
            
            if ($use_object_cache) {
                // ATOMIC OPERATION: wp_cache_add returns false if key already exists
                // This is faster than add_option for high-traffic sites with Redis/Memcached
                $acquired = wp_cache_add($lock_key, $lock_value, 'muchat_locks', $lock_timeout);
            } else {
                // ATOMIC OPERATION: add_option returns false if the option already exists
                // autoload = 'no' for performance (locks are temporary)
                $acquired = add_option($lock_key, $lock_value, '', 'no');
            }
            
            if ($acquired) {
                // Lock successfully acquired
                // Store lock info for verification and cleanup
                $this->current_lock_values[$product_id] = [
                    'value' => $lock_value,
                    'use_object_cache' => $use_object_cache
                ];
                return true;
            }
            
            // Lock exists - check if it's stale (older than lock_timeout)
            // This prevents deadlocks if a process crashes without releasing the lock
            $existing_lock = $use_object_cache 
                ? wp_cache_get($lock_key, 'muchat_locks') 
                : get_option($lock_key);
                
            if ($existing_lock !== false) {
                // Extract timestamp from lock value (format: random_pid_timestamp)
                $lock_str = is_array($existing_lock) ? '' : (string) $existing_lock;
                $parts = explode('_', $lock_str);
                if (count($parts) >= 3) {
                    $lock_timestamp = (float) end($parts);
                    $lock_age = microtime(true) - $lock_timestamp;
                    
                    // If lock is stale (older than timeout), force remove and retry
                    if ($lock_age > $lock_timeout) {
                        if ($use_object_cache) {
                            wp_cache_delete($lock_key, 'muchat_locks');
                        } else {
                            delete_option($lock_key);
                        }
                        // Don't count this as an attempt - retry immediately
                        continue;
                    }
                }
            }
            
            $attempt++;
            
            if ($attempt < self::LOCK_MAX_RETRIES) {
                usleep($delay);
                $delay *= 2; // Exponential backoff
            }
        }
        
        return false;
    }
    
    /**
     * Storage for current lock values to verify ownership
     * 
     * @var array
     */
    private $current_lock_values = [];

    /**
     * Release a database-level lock for a product
     *
     * @param int $product_id The product ID to unlock
     */
    private function release_product_lock($product_id)
    {
        // Release both types of locks to be safe
        $this->release_transient_lock($product_id);
        
        if (!$this->is_get_lock_available()) {
            return;
        }
        
        global $wpdb;

        $lock_name = $this->get_lock_name($product_id);

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->query($wpdb->prepare("SELECT RELEASE_LOCK(%s)", $lock_name));
    }
    
    /**
     * Release an option-based lock
     * 
     * SECURITY: Only releases the lock if we own it (matching lock value).
     * This prevents one process from accidentally releasing another's lock.
     *
     * @param int $product_id The product ID to unlock
     */
    private function release_transient_lock($product_id)
    {
        $lock_key = self::TRANSIENT_LOCK_PREFIX . intval($product_id);
        
        // Only release if we own this lock
        if (isset($this->current_lock_values[$product_id])) {
            $lock_info = $this->current_lock_values[$product_id];
            $lock_value = is_array($lock_info) ? $lock_info['value'] : $lock_info;
            $use_object_cache = is_array($lock_info) && !empty($lock_info['use_object_cache']);
            
            // Get current lock value to verify ownership
            $current_value = $use_object_cache 
                ? wp_cache_get($lock_key, 'muchat_locks') 
                : get_option($lock_key);
            
            // Verify we still own the lock before deleting
            if ($current_value === $lock_value) {
                if ($use_object_cache) {
                    wp_cache_delete($lock_key, 'muchat_locks');
                } else {
                    delete_option($lock_key);
                }
            }
            
            unset($this->current_lock_values[$product_id]);
        } else {
            // Fallback: delete from both caches if we don't have tracking info
            // (for backwards compatibility with existing locks)
            if (wp_using_ext_object_cache()) {
                wp_cache_delete($lock_key, 'muchat_locks');
            }
            delete_option($lock_key);
        }
    }

    /**
     * Initialize hooks
     */
    public function __construct()
    {
        // Hook into product save - multiple hooks to catch all scenarios
        add_action('woocommerce_update_product', [$this, 'track_product_changes'], 20, 1);
        add_action('woocommerce_new_product', [$this, 'track_product_changes'], 20, 1);
        
        // Hook into product object save (modern WooCommerce)
        add_action('woocommerce_after_product_object_save', [$this, 'track_product_object_changes'], 20, 1);
        
        // Hook into variation save
        add_action('woocommerce_update_product_variation', [$this, 'track_product_changes'], 20, 1);
        add_action('woocommerce_new_product_variation', [$this, 'track_product_changes'], 20, 1);
        
        // Hook into stock changes (for external stock management plugins)
        add_action('woocommerce_product_set_stock', [$this, 'on_stock_change'], 20, 1);
        add_action('woocommerce_variation_set_stock', [$this, 'on_stock_change'], 20, 1);
        
        // Hook into bulk edit
        add_action('woocommerce_product_bulk_edit_save', [$this, 'on_bulk_edit'], 20, 1);
        
        // WordPress post save hook as fallback
        add_action('save_post_product', [$this, 'on_post_save'], 20, 3);
    }

    /**
     * Track product changes
     * 
     * CONCURRENCY: Uses database-level locking to prevent race conditions
     * in multi-process environments (PHP-FPM with multiple workers).
     *
     * @param int $product_id
     */
    public function track_product_changes($product_id)
    {
        // Always work with the main product ID, even if a variation ID is passed.
        $main_product_id = $this->get_main_product_id($product_id);

        // Process-local check to prevent infinite loops within the same request
        if (isset(self::$processing_products[$main_product_id])) {
            return;
        }

        // CONCURRENCY: Acquire database-level lock to prevent race conditions
        // across multiple PHP processes
        if (!$this->acquire_product_lock($main_product_id)) {
            // Another process is handling this product, skip
            return;
        }

        try {
            self::$processing_products[$main_product_id] = true;

            $product = wc_get_product($main_product_id);

            if (!$product) {
                return;
            }

            // For variations, any save is considered significant enough to update the parent.
            // For other types, we check for actual price/stock changes.
            $is_variation_update = ($product_id != $main_product_id);
            if ($is_variation_update || $this->has_significant_change($main_product_id)) {
                $this->update_muchat_modified_date($main_product_id);
            }
        } finally {
            // Always release lock and clean up, even if an exception occurs
            unset(self::$processing_products[$main_product_id]);
            $this->release_product_lock($main_product_id);
        }
    }

    /**
     * Track product object changes (modern WooCommerce)
     *
     * @param \WC_Product $product
     */
    public function track_product_object_changes($product)
    {
        if (!$product || !is_a($product, 'WC_Product')) {
            return;
        }
        
        $this->track_product_changes($product->get_id());
    }

    /**
     * Handle WordPress post save
     *
     * @param int $post_id
     * @param \WP_Post $post
     * @param bool $update
     */
    public function on_post_save($post_id, $post, $update)
    {
        // Only proceed if this is an update (not a new post)
        if (!$update) {
            return;
        }
        
        // Skip autosaves and revisions
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
            return;
        }
        
        if (wp_is_post_revision($post_id)) {
            return;
        }
        
        $this->track_product_changes($post_id);
    }

    /**
     * Handle stock changes
     *
     * @param \WC_Product $product
     */
    public function on_stock_change($product)
    {
        if (!$product) {
            return;
        }
        
        // Always update the main product
        $main_product_id = $this->get_main_product_id($product->get_id());
        $this->update_muchat_modified_date($main_product_id);
    }

    /**
     * Handle bulk edit
     *
     * @param \WC_Product $product
     */
    public function on_bulk_edit($product)
    {
        if (!$product) {
            return;
        }
        
        $product_id = is_numeric($product) ? $product : $product->get_id();
        $this->track_product_changes($product_id);
    }

    /**
     * Check if product has significant changes (price or stock)
     *
     * @param int $product_id
     * @return bool
     */
    private function has_significant_change($product_id)
    {
        $product = wc_get_product($product_id);
        
        if (!$product) {
            return false;
        }

        // Get previous values from our snapshot
        $previous_data = get_post_meta($product_id, '_muchat_previous_data', true);
        
        // Current values
        $current_data = [
            'price' => $product->get_price(),
            'regular_price' => $product->get_regular_price(),
            'sale_price' => $product->get_sale_price(),
            'stock_status' => $product->get_stock_status(),
            'stock_quantity' => $product->get_stock_quantity(),
        ];

        // If no previous data exists, this is first time - set it
        if (empty($previous_data)) {
            update_post_meta($product_id, '_muchat_previous_data', $current_data);
            $this->update_muchat_modified_date($product_id);
            return true;
        }

        // Compare values
        $has_change = false;
        
        // Check price changes
        if ($previous_data['price'] !== $current_data['price']) {
            $has_change = true;
        }
        
        if ($previous_data['regular_price'] !== $current_data['regular_price']) {
            $has_change = true;
        }
        
        if ($previous_data['sale_price'] !== $current_data['sale_price']) {
            $has_change = true;
        }
        
        // Check stock changes
        if ($previous_data['stock_status'] !== $current_data['stock_status']) {
            $has_change = true;
        }
        
        if ($previous_data['stock_quantity'] !== $current_data['stock_quantity']) {
            $has_change = true;
        }

        // Update snapshot if there's a change
        if ($has_change) {
            update_post_meta($product_id, '_muchat_previous_data', $current_data);
        }

        return $has_change;
    }

    /**
     * Update muchat modified date
     *
     * @param int $product_id
     */
    private function update_muchat_modified_date($product_id)
    {
        $current_time = current_time('mysql', true); // GMT time
        update_post_meta($product_id, self::META_KEY, $current_time);
        
        // Log for debugging (optional)
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log(sprintf(
                'Muchat: Updated %s for product #%d to %s',
                self::META_KEY,
                $product_id,
                $current_time
            ));
        }
    }

    /**
     * Get muchat modified date for a product
     *
     * @param int $product_id
     * @return string|null
     */
    public static function get_muchat_modified_date($product_id)
    {
        $date = get_post_meta($product_id, self::META_KEY, true);
        return $date ?: null;
    }

    /**
     * Threshold for large catalog - above this, use estimation
     */
    const LARGE_CATALOG_THRESHOLD = 10000;

    /**
     * Get count of uninitialized products using efficient LEFT JOIN
     * 
     * PERFORMANCE: Uses LEFT JOIN instead of NOT EXISTS for better query performance
     * on large databases with millions of postmeta rows.
     * 
     * For catalogs with 10k+ products, uses cached estimation to prevent
     * query timeouts on shared hosting.
     *
     * @param bool $force_exact Force exact count even for large catalogs
     * @return int Count of products without muchat tracking meta (-1 if calculating)
     */
    public static function get_uninitialized_count($force_exact = false)
    {
        global $wpdb;

        // First, get total product count (fast indexed query)
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $total_products = (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM {$wpdb->posts} 
             WHERE post_type = 'product' AND post_status = 'publish'"
        );

        // For large catalogs, use estimation unless forced
        if (!$force_exact && $total_products > self::LARGE_CATALOG_THRESHOLD) {
            return self::get_uninitialized_count_estimated($total_products);
        }

        // PERFORMANCE: LEFT JOIN is much faster than NOT EXISTS for this pattern
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        return (int) $wpdb->get_var($wpdb->prepare("
            SELECT COUNT(p.ID)
            FROM {$wpdb->posts} p
            LEFT JOIN {$wpdb->postmeta} pm 
                ON p.ID = pm.post_id 
                AND pm.meta_key = %s
            WHERE p.post_type = 'product'
                AND p.post_status = 'publish'
                AND pm.meta_id IS NULL
        ", self::META_KEY));
    }

    /**
     * Get estimated uninitialized count for large catalogs
     * 
     * Uses cached count and schedules background recalculation.
     * Prevents query timeouts on shared hosting with 500k+ products.
     *
     * @param int $total_products Total product count
     * @return int Estimated count (-1 if calculating)
     */
    private static function get_uninitialized_count_estimated($total_products)
    {
        $cache_key = 'muchat_uninitialized_count';
        $cached = get_transient($cache_key);

        if ($cached !== false) {
            return (int) $cached;
        }

        // Get initialized count (faster query - just counts meta rows)
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $initialized_count = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key = %s",
            self::META_KEY
        ));

        $estimated = max(0, $total_products - $initialized_count);

        // Cache the estimate for 5 minutes
        set_transient($cache_key, $estimated, 5 * MINUTE_IN_SECONDS);

        // Schedule background exact count calculation if Action Scheduler available
        if (function_exists('as_schedule_single_action') && 
            !as_has_scheduled_action('muchat_calculate_uninitialized_count', [], 'muchat')) {
            as_schedule_single_action(time() + 30, 'muchat_calculate_uninitialized_count', [], 'muchat');
        }

        return $estimated;
    }

    /**
     * Calculate exact uninitialized count in background
     * Called by Action Scheduler to update cached estimate
     */
    public static function calculate_exact_uninitialized_count()
    {
        // Force exact calculation
        $exact_count = self::get_uninitialized_count(true);

        // Cache for 30 minutes since this was exact
        set_transient('muchat_uninitialized_count', $exact_count, 30 * MINUTE_IN_SECONDS);
    }

    /**
     * Initialize existing products with efficient batch processing
     * 
     * PERFORMANCE: Uses direct SQL with LEFT JOIN instead of WP_Query with NOT EXISTS
     * This is O(n) with small constant vs O(n*m) for the meta_query approach.
     *
     * @param int $batch_size Number of products to process per batch
     * @return array Results with count of processed products
     */
    public static function initialize_existing_products($batch_size = 100)
    {
        global $wpdb;

        $current_time = current_time('mysql', true);

        // PERFORMANCE: Use efficient LEFT JOIN instead of NOT EXISTS meta query
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $product_ids = $wpdb->get_col($wpdb->prepare("
            SELECT p.ID
            FROM {$wpdb->posts} p
            LEFT JOIN {$wpdb->postmeta} pm 
                ON p.ID = pm.post_id 
                AND pm.meta_key = %s
            WHERE p.post_type = 'product'
                AND p.post_status = 'publish'
                AND pm.meta_id IS NULL
            LIMIT %d
        ", self::META_KEY, $batch_size));

        if (empty($product_ids)) {
            return [
                'processed' => 0,
                'remaining' => 0,
                'total_found' => 0
            ];
        }

        $processed = 0;

        // Process products efficiently
        foreach ($product_ids as $product_id) {
            $product = wc_get_product($product_id);

            if ($product) {
                // Set initial data snapshot
                $initial_data = [
                    'price' => $product->get_price(),
                    'regular_price' => $product->get_regular_price(),
                    'sale_price' => $product->get_sale_price(),
                    'stock_status' => $product->get_stock_status(),
                    'stock_quantity' => $product->get_stock_quantity(),
                ];

                update_post_meta($product_id, '_muchat_previous_data', $initial_data);
                update_post_meta($product_id, self::META_KEY, $current_time);
                $processed++;
            }
        }

        // Get remaining count
        $remaining = self::get_uninitialized_count();

        return [
            'processed' => $processed,
            'remaining' => $remaining,
            'total_found' => $processed + $remaining
        ];
    }

    /**
     * Schedule background initialization using Action Scheduler
     * 
     * PERFORMANCE: For large catalogs, this schedules batches via Action Scheduler
     * (included with WooCommerce) to prevent timeouts and server overload.
     *
     * @return array Status information
     */
    public static function schedule_background_initialization()
    {
        // Check if Action Scheduler is available (bundled with WooCommerce)
        if (!function_exists('as_schedule_single_action')) {
            return [
                'success' => false,
                'message' => 'Action Scheduler not available. Please ensure WooCommerce is active.',
            ];
        }

        $pending = self::get_uninitialized_count();

        if ($pending === 0) {
            return [
                'success' => true,
                'status' => 'complete',
                'message' => 'All products already initialized',
            ];
        }

        // Check if already scheduled
        if (as_has_scheduled_action('muchat_init_products_batch', [], 'muchat_product_init')) {
            return [
                'success' => true,
                'status' => 'running',
                'pending' => $pending,
                'message' => 'Initialization already in progress',
            ];
        }

        // Schedule first batch
        as_schedule_single_action(
            time(),
            'muchat_init_products_batch',
            [],
            'muchat_product_init'
        );

        return [
            'success' => true,
            'status' => 'scheduled',
            'pending' => $pending,
            'message' => 'Background initialization started',
        ];
    }

    /**
     * Process a single batch (called by Action Scheduler)
     */
    public static function process_scheduled_batch()
    {
        $result = self::initialize_existing_products(100);

        // If there are more products, schedule next batch
        if ($result['remaining'] > 0) {
            as_schedule_single_action(
                time() + 5, // 5 second delay between batches
                'muchat_init_products_batch',
                [],
                'muchat_product_init'
            );
        }

        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log(sprintf(
                'Muchat: Processed batch of %d products, %d remaining',
                $result['processed'],
                $result['remaining']
            ));
        }
    }

    /**
     * Get initialization progress
     *
     * @return array Progress information
     */
    public static function get_initialization_progress()
    {
        global $wpdb;

        // Get total published products
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $total = (int) $wpdb->get_var("
            SELECT COUNT(*) FROM {$wpdb->posts} 
            WHERE post_type = 'product' AND post_status = 'publish'
        ");

        // Get initialized count
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $initialized = (int) $wpdb->get_var($wpdb->prepare("
            SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta}
            WHERE meta_key = %s
        ", self::META_KEY));

        $pending = max(0, $total - $initialized);

        // Check if background job is running
        $is_running = false;
        if (function_exists('as_has_scheduled_action')) {
            $is_running = as_has_scheduled_action('muchat_init_products_batch', [], 'muchat_product_init');
        }

        return [
            'total' => $total,
            'initialized' => $initialized,
            'pending' => $pending,
            'percent_complete' => $total > 0 ? round(($initialized / $total) * 100, 1) : 100,
            'is_running' => $is_running,
        ];
    }
}

// Register the Action Scheduler hooks
add_action('muchat_init_products_batch', [ProductChangeTracker::class, 'process_scheduled_batch']);
add_action('muchat_calculate_uninitialized_count', [ProductChangeTracker::class, 'calculate_exact_uninitialized_count']);
</file>

<file path="includes/Api/Controllers/PostController.php">
<?php

namespace Muchat\Api\Api\Controllers;

use Muchat\Api\Models\Post;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

class PostController
{
    /**
     * @var Post
     */
    private $post_model;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->post_model = new Post();
    }

    /**
     * Get posts
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response|WP_Error
     */
    public function get_posts($request)
    {
        try {
            // Get required parameters
            $modified_after = $request->get_param('modified_after');
            $order_by = $request->get_param('order_by');
            $order = $request->get_param('order');

            // Get skip and take parameters
            $skip = $request->get_param('skip') !== null ? intval($request->get_param('skip')) : 0;
            $take = $request->get_param('take') !== null ? intval($request->get_param('take')) : 30;

            // Prepare parameters for the model
            $params = [
                'modified_after' => $modified_after,
                'order_by' => $order_by,
                'order' => $order,
                'skip' => $skip,
                'take' => $take
            ];

            // Start output buffering to catch any unexpected output
            ob_start();
            $result = $this->post_model->get_posts($params);
            $unexpected_output = ob_get_clean();

            // If there was unexpected output, log it for debugging
            if (!empty($unexpected_output) && function_exists('error_log')) {
                error_log('Unexpected output in Posts API: ' . $unexpected_output);
            }

            // Create proper response
            $response = new WP_REST_Response($result, 200);
            $response->header('Cache-Control', 'no-cache, no-store, must-revalidate');
            $response->header('Pragma', 'no-cache');
            $response->header('Expires', '0');

            return $response;
        } catch (\Exception $e) {
            // Log the error for debugging
            if (function_exists('error_log')) {
                error_log('Error in Posts API: ' . $e->getMessage());
            }

            return new WP_Error(
                'posts_fetch_error',
                $e->getMessage(),
                ['status' => 500]
            );
        }
    }
}
</file>

<file path="includes/Models/BaseModel.php">
<?php

namespace Muchat\Api\Models;

use Muchat\Api\Utils\Formatter;

abstract class BaseModel
{
    /**
     * @var Formatter
     */
    protected $formatter;

    /**
     * @var array|null
     */
    protected $options = null;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->formatter = new Formatter();
    }

    /**
     * Get plugin options (lazy loading)
     *
     * @return array
     */
    protected function get_options()
    {
        if ($this->options === null) {
            $this->options = get_option('muchat_ai_chatbot_plugin_options', []);
        }
        return $this->options;
    }

    /**
     * Get enabled fields for content type
     *
     * @param string $content_type
     * @return array
     */
    protected function get_enabled_fields($content_type)
    {
        $options = $this->get_options();
        return isset($options["{$content_type}_fields"])
            ? $options["{$content_type}_fields"]
            : [];
    }

    /**
     * Check if field is enabled
     *
     * @param string $field
     * @param string $content_type
     * @return bool
     */
    protected function is_field_enabled($field, $content_type)
    {
        $enabled_fields = $this->get_enabled_fields($content_type);
        return in_array($field, $enabled_fields);
    }

    /**
     * Build WP_Query args
     *
     * @param array $params
     * @return array
     */
    protected function build_query_args($params)
    {
        $args = [
            'post_status' => 'publish',
            'posts_per_page' => isset($params['per_page']) ? $params['per_page'] : 10,
            'paged' => isset($params['page']) ? $params['page'] : 1,
            'orderby' => isset($params['orderby']) ? $params['orderby'] : 'modified',
            'order' => isset($params['order']) ? $params['order'] : 'ASC',
            'fields' => 'ids'
        ];

        if (!empty($params['modified_after'])) {
            $modified_after_timestamp = strtotime($params['modified_after']);
            if ($modified_after_timestamp) {
                $args['date_query'] = [
                    [
                        'column' => 'post_modified_gmt',
                        'after' => gmdate('Y-m-d H:i:s', $modified_after_timestamp),
                        'inclusive' => false
                    ]
                ];
            }
        }

        return $args;
    }

    /**
     * Format response data
     *
     * @param \WP_Query $query
     * @param array $items
     * @return array
     */
    protected function format_response($query, $items)
    {
        return [
            'page_number' => (int) $query->get('paged'),
            'page_size' => (int) $query->get('posts_per_page'),
            'total_count' => (int) $query->found_posts,
            'total_pages' => (int) $query->max_num_pages,
            'items' => array_values(array_filter($items))
        ];
    }
}
</file>

<file path="includes/Utils/Cache.php">
<?php

namespace Muchat\Api\Utils;

/**
 * Cache utility class with performance-optimized methods
 * 
 * PERFORMANCE: Uses Cache Salt/Versioning system instead of tracking individual keys.
 * This prevents wp_options table bloat and eliminates database write overhead.
 *
 * @package Muchat\Api\Utils
 */
class Cache
{
    /**
     * Cache group for WordPress object cache
     */
    const CACHE_GROUP = 'muchat';

    /**
     * Prefix for all transients
     */
    const TRANSIENT_PREFIX = 'muchat_';

    /**
     * Default cache duration (1 hour)
     */
    const DEFAULT_EXPIRATION = HOUR_IN_SECONDS;

    /**
     * Option key for cache salt (versioning)
     */
    const CACHE_SALT_OPTION = 'muchat_cache_salt';

    /**
     * Known cache keys for targeted clearing
     */
    private static $known_keys = [
        'widget_config',
        'widget_script',
        'connection_status',
        'product_meta_fields_cache',
    ];

    /**
     * In-memory cache for salt to avoid repeated DB queries
     * 
     * @var string|null
     */
    private static $salt_cache = null;

    /**
     * Get the current cache salt for versioning
     * 
     * PERFORMANCE: Uses in-memory caching to avoid repeated database queries.
     * The salt is used to version cache keys - changing it invalidates all caches.
     * 
     * @return string The cache salt
     */
    public static function get_cache_salt()
    {
        // Return cached value if available
        if (self::$salt_cache !== null) {
            return self::$salt_cache;
        }

        $salt = get_option(self::CACHE_SALT_OPTION);
        
        if (!$salt) {
            $salt = (string) time();
            // PERFORMANCE: autoload = true so it loads from alloptions cache
            // This prevents an extra query on every frontend widget request
            update_option(self::CACHE_SALT_OPTION, $salt, true);
        }

        self::$salt_cache = $salt;
        return $salt;
    }

    /**
     * Invalidate all widget caches by updating the salt
     * 
     * PERFORMANCE: Instead of tracking and deleting individual cache keys,
     * we simply change the salt. All old cache keys become orphaned and will
     * be automatically cleaned up by WordPress's transient garbage collection.
     * 
     * This is O(1) operation vs O(n) for tracking individual keys.
     */
    public static function invalidate_cache_salt()
    {
        $new_salt = (string) time();
        // PERFORMANCE: autoload = true for consistent caching behavior
        update_option(self::CACHE_SALT_OPTION, $new_salt, true);
        self::$salt_cache = $new_salt;

        // Also clear object cache if available
        if (wp_using_ext_object_cache()) {
            if (function_exists('wp_cache_flush_group')) {
                wp_cache_flush_group(self::CACHE_GROUP);
            }
        }

        // Clear alloptions cache
        wp_cache_delete('alloptions', 'options');
    }

    /**
     * Build a salted cache key for widget-related caches
     * 
     * @param string $key The base key
     * @return string The salted key
     */
    private static function get_salted_key($key)
    {
        // Only salt widget keys for now
        if (strpos($key, 'widget_') === 0) {
            return $key . '_' . self::get_cache_salt();
        }
        return $key;
    }

    /**
     * Set a cached value
     * 
     * PERFORMANCE: Uses salted keys for widget caches to enable O(1) invalidation.
     * No more tracking individual keys in the database.
     * 
     * OPTIMIZATION: Trust the Transients API to handle caching correctly.
     * When wp_using_ext_object_cache() is true (Redis/Memcached), WordPress 
     * automatically routes set_transient() to the object cache.
     * Manually calling both wp_cache_set() AND set_transient() causes double writes.
     * 
     * @param string $key Cache key (without prefix)
     * @param mixed $value Value to cache
     * @param int $expiration Expiration in seconds (0 = no expiration)
     * @return bool Success
     */
    public static function set($key, $value, $expiration = self::DEFAULT_EXPIRATION)
    {
        $salted_key = self::get_salted_key($key);
        $prefixed_key = self::TRANSIENT_PREFIX . sanitize_key($salted_key);

        // Trust WordPress Transients API:
        // - With object cache: automatically uses wp_cache_set()
        // - Without object cache: writes to database
        return set_transient($prefixed_key, $value, $expiration);
    }

    /**
     * Get a cached value
     * 
     * OPTIMIZATION: Trust WordPress Transients API to handle caching correctly.
     * When object cache is available, get_transient() automatically reads from it.
     * 
     * @param string $key Cache key (without prefix)
     * @return mixed|false Cached value or false if not found
     */
    public static function get($key)
    {
        $salted_key = self::get_salted_key($key);
        $prefixed_key = self::TRANSIENT_PREFIX . sanitize_key($salted_key);

        // Trust WordPress Transients API:
        // - With object cache: automatically uses wp_cache_get()
        // - Without object cache: reads from database
        return get_transient($prefixed_key);
    }

    /**
     * Delete a specific cache key
     * 
     * OPTIMIZATION: Trust WordPress Transients API to handle deletion correctly.
     * When object cache is available, delete_transient() automatically deletes from it.
     * 
     * @param string $key Cache key (without prefix)
     * @return bool Success
     */
    public static function delete($key)
    {
        $salted_key = self::get_salted_key($key);
        $prefixed_key = self::TRANSIENT_PREFIX . sanitize_key($salted_key);

        // Trust WordPress Transients API:
        // - With object cache: automatically uses wp_cache_delete()
        // - Without object cache: deletes from database
        return delete_transient($prefixed_key);
    }

    /**
     * Clear widget cache
     * 
     * PERFORMANCE: Uses cache salt invalidation - O(1) operation.
     * Old transients will be garbage collected by WordPress automatically.
     * 
     * For object cache (Redis/Memcached), we flush the cache group.
     */
    public static function clear_widget_cache()
    {
        // Invalidate the salt - this makes all old widget caches invalid
        self::invalidate_cache_salt();

        // Also clear known static keys without salt (for backwards compatibility)
        foreach (self::$known_keys as $key) {
            if (strpos($key, 'widget') !== false) {
                $full_key = self::TRANSIENT_PREFIX . $key;
                delete_transient($full_key);

                if (wp_using_ext_object_cache()) {
                    wp_cache_delete($full_key, 'transient');
                    wp_cache_delete($full_key, self::CACHE_GROUP);
                }
            }
        }

        // Clear object cache groups if supported
        if (wp_using_ext_object_cache()) {
            if (function_exists('wp_cache_flush_group')) {
                wp_cache_flush_group(self::CACHE_GROUP);
                wp_cache_flush_group('transient');
            }

            if (function_exists('wp_cache_flush_runtime')) {
                wp_cache_flush_runtime();
            }
        }

        wp_cache_delete('alloptions', 'options');
    }

    /**
     * Clear a specific type of cache
     * 
     * @param string $type Type of cache to clear: 'widget', 'token', 'connection', 'meta'
     */
    public static function clear_cache_by_type($type)
    {
        if ($type === 'widget') {
            self::clear_widget_cache();
            return;
        }

        global $wpdb;

        $prefixes = [
            'token' => 'muchat_token_',
            'connection' => 'muchat_connection_',
            'meta' => 'muchat_product_meta_',
        ];

        if (!isset($prefixes[$type])) {
            return;
        }

        $prefix = $prefixes[$type];

        // SECURITY: Use $wpdb->esc_like for LIKE patterns
        $escaped_transient_prefix = $wpdb->esc_like('_transient_' . $prefix);
        $escaped_timeout_prefix = $wpdb->esc_like('_transient_timeout_' . $prefix);

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM {$wpdb->options} 
                WHERE option_name LIKE %s 
                OR option_name LIKE %s",
                $escaped_transient_prefix . '%',
                $escaped_timeout_prefix . '%'
            )
        );

        wp_cache_delete('alloptions', 'options');
    }

    /**
     * Clear all plugin caches
     * 
     * Use sparingly - prefer targeted cache clearing
     */
    public static function clear_all()
    {
        global $wpdb;

        // Invalidate cache salt first
        self::invalidate_cache_salt();

        // SECURITY: Use $wpdb->esc_like for LIKE patterns
        $escaped_transient_prefix = $wpdb->esc_like('_transient_' . self::TRANSIENT_PREFIX);
        $escaped_timeout_prefix = $wpdb->esc_like('_transient_timeout_' . self::TRANSIENT_PREFIX);

        // Delete all plugin transients
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM {$wpdb->options} 
                WHERE option_name LIKE %s 
                OR option_name LIKE %s",
                $escaped_transient_prefix . '%',
                $escaped_timeout_prefix . '%'
            )
        );

        // Clear object cache group
        if (wp_using_ext_object_cache() && function_exists('wp_cache_flush_group')) {
            wp_cache_flush_group(self::CACHE_GROUP);
        }

        wp_cache_delete('alloptions', 'options');
    }

    /**
     * Clear token validation cache for a specific token
     * 
     * @param string $token The token to invalidate
     */
    public static function invalidate_token($token)
    {
        $token_hash = hash('sha256', $token);
        $key = 'token_' . substr($token_hash, 0, 32);
        self::delete($key);
    }

    /**
     * Get or set a cached value (cache-aside pattern)
     * 
     * @param string $key Cache key
     * @param callable $callback Function to generate value if not cached
     * @param int $expiration Cache expiration
     * @return mixed Cached or generated value
     */
    public static function remember($key, callable $callback, $expiration = self::DEFAULT_EXPIRATION)
    {
        $value = self::get($key);

        if ($value !== false) {
            return $value;
        }

        $value = $callback();

        if ($value !== null && $value !== false) {
            self::set($key, $value, $expiration);
        }

        return $value;
    }

    /**
     * Cleanup old orphaned widget cache transients
     * 
     * MAINTENANCE: This can be called periodically via WP-Cron to clean up
     * orphaned transients from old cache salts. Not critical since WordPress
     * handles transient garbage collection, but helps keep the database clean.
     * 
     * @param int $max_age Maximum age in seconds for orphaned transients (default: 1 week)
     */
    public static function cleanup_orphaned_transients($max_age = WEEK_IN_SECONDS)
    {
        global $wpdb;

        // SECURITY: Use $wpdb->esc_like for LIKE patterns
        $escaped_prefix = $wpdb->esc_like('_transient_muchat_widget_');

        // Delete expired transients (WordPress core does this, but we can be proactive)
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->query(
            $wpdb->prepare(
                "DELETE a, b FROM {$wpdb->options} a
                LEFT JOIN {$wpdb->options} b ON b.option_name = CONCAT('_transient_timeout_', SUBSTRING(a.option_name, 12))
                WHERE a.option_name LIKE %s
                AND b.option_value < %d",
                $escaped_prefix . '%',
                time() - $max_age
            )
        );

        wp_cache_delete('alloptions', 'options');
    }
}
</file>

<file path=".distignore">
/.wordpress-org
/.git
/.github
/node_modules
/.DS_Store

.distignore
.gitignore
.gitattributes
composer.json
composer.lock
package.json
package-lock.json
webpack.config.js
gulpfile.js
Gruntfile.js
phpunit.xml
phpcs.xml
.phpcs.xml.dist
tests/
bin/
.travis.yml
.circleci/
.gitlab-ci.yml
README.md
CHANGELOG.md
DEPLOYMENT.md
</file>

<file path="uninstall.php">
<?php
/**
 * Muchat AI Plugin Uninstall Handler
 * 
 * This file is executed when the plugin is deleted (not deactivated).
 * It removes all plugin data to ensure a clean uninstall (Leave No Trace principle).
 * 
 * PERFORMANCE: Uses batch deletion to prevent table locks on large databases.
 * For sites with 100k+ products, direct DELETE queries can lock the postmeta table.
 * 
 * @package Muchat
 */

// Security: Exit if not called by WordPress uninstall process
if (!defined('WP_UNINSTALL_PLUGIN')) {
    exit;
}

/**
 * Remove all plugin data from the database
 * 
 * DATA HYGIENE: A well-behaved plugin should remove all traces when deleted.
 * This includes options, transients, post meta, and cached data.
 */

global $wpdb;

// 1. Delete all known plugin options
$options_to_delete = [
    // Main plugin options
    'muchat_ai_chatbot_plugin_options',
    'muchat_ai_chatbot_agent_id',
    'muchat_ai_chatbot_verify',
    'muchat_ai_chatbot_context',
    'muchat_ai_chatbot_interface_initial_messages',
    
    // Guest messaging options
    'muchat_ai_chatbot_enable_guest_messages',
    'muchat_ai_chatbot_guest_initial_messages',
    'muchat_ai_chatbot_enable_guest_context',
    'muchat_ai_chatbot_guest_context',
    
    // Performance options
    'muchat_ai_chatbot_load_strategy',
    'muchat_ai_chatbot_script_position',
    'muchat_ai_chatbot_use_logged_in_user_info',
    
    // Visibility options
    'muchat_ai_chatbot_visibility_mode',
    'muchat_ai_chatbot_visibility_pages',
    'muchat_ai_chatbot_widget_enabled',
    
    // Schedule options
    'muchat_ai_chatbot_schedule_enabled',
    'muchat_ai_chatbot_schedule_days',
    'muchat_ai_chatbot_schedule_start_time',
    'muchat_ai_chatbot_schedule_end_time',
    
    // Internal/system options
    'muchat_ai_chatbot_onboarding',
    'muchat_cache_salt',
    'muchat_connection_status',
    'muchat_get_lock_status',
];

foreach ($options_to_delete as $option) {
    delete_option($option);
}

// 2. Delete all plugin transients (using LIKE pattern for dynamic keys)
// This catches: muchat_token_*, muchat_rate_*, muchat_plock_*, muchat_widget_*, etc.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query(
    $wpdb->prepare(
        "DELETE FROM {$wpdb->options} 
         WHERE option_name LIKE %s 
         OR option_name LIKE %s",
        $wpdb->esc_like('_transient_muchat_') . '%',
        $wpdb->esc_like('_transient_timeout_muchat_') . '%'
    )
);

// 3. Delete option-based locks (from atomic locking system)
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query(
    $wpdb->prepare(
        "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
        $wpdb->esc_like('muchat_plock_') . '%'
    )
);

// 4. Delete product meta data added by the plugin
// PERFORMANCE: Use batch deletion to prevent table locks on large databases
// For sites with 100k+ products, a single DELETE can lock the table for minutes

/**
 * Batch delete meta data to prevent table locks
 * 
 * @param string $meta_key The meta key to delete
 * @param int $batch_size Number of rows to delete per batch
 * @param int $max_batches Maximum number of batches (safety limit)
 */
function muchat_batch_delete_postmeta($meta_key, $batch_size = 1000, $max_batches = 100) {
    global $wpdb;
    
    $batch_count = 0;
    
    do {
        // Delete in batches using LIMIT to prevent table lock
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $deleted = $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM {$wpdb->postmeta} 
                 WHERE meta_key = %s 
                 LIMIT %d",
                $meta_key,
                $batch_size
            )
        );
        
        $batch_count++;
        
        // Small delay between batches to reduce database load
        if ($deleted > 0 && $batch_count < $max_batches) {
            usleep(10000); // 10ms delay
        }
        
    } while ($deleted > 0 && $batch_count < $max_batches);
    
    return $batch_count;
}

// Delete each meta key in batches
muchat_batch_delete_postmeta('_muchat_date_modified');
muchat_batch_delete_postmeta('_muchat_previous_data');

// 5. Clear scheduled actions (if Action Scheduler is available)
if (function_exists('as_unschedule_all_actions')) {
    as_unschedule_all_actions('muchat_init_products_batch');
    as_unschedule_all_actions('muchat_calculate_uninitialized_count');
    as_unschedule_all_actions('muchat_analyze_meta_fields');
}

// 6. Clear object cache to remove any cached plugin data
// This is critical for sites using Redis/Memcached
wp_cache_flush();

// 7. Clear alloptions cache to ensure deleted options are not served from cache
wp_cache_delete('alloptions', 'options');
</file>

<file path=".github/workflows/deploy-to-wordpress-svn.yml">
name: Deploy to WordPress.org Repository

on:
  release:
    types: [released]

permissions:
  contents: write

jobs:
  deploy:
    name: Deploy to WordPress.org
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Create WordPress.org assets directory
        run: |
          mkdir -p .wordpress-org
          cp assets/images/logo.svg .wordpress-org/icon.svg
          cp assets/images/banner-1544x500.* .wordpress-org/ 2>/dev/null || true

      - name: WordPress Plugin Deploy
        id: deploy
        uses: 10up/action-wordpress-plugin-deploy@stable
        with:
          generate-zip: true
        env:
          SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
          SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
          SLUG: muchat-ai

      - name: Upload release asset
        uses: softprops/action-gh-release@v2
        with:
          files: ${{ steps.deploy.outputs.zip-path }}
</file>

<file path="assets/css/admin.css">
/* General WordPress Admin Styles */
.wrap h1 {
    margin-bottom: 20px;
}

/* Settings Wrapper and Cards */
.muchat-api-settings-wrapper {
    max-width: 100%;
    margin-top: 20px;
}

.muchat-api-section {
    background: #fff;
    padding: 20px;
    margin-bottom: 20px;
    border: 1px solid #c3c4c7;
    border-radius: 0;
    box-shadow: 0 1px 1px rgba(0, 0, 0, .04);
}

.muchat-api-section h2 {
    margin-top: 0;
    padding-bottom: 12px;
    margin-bottom: 15px;
    border-bottom: 1px solid #eee;
    font-size: 1.3em;
    font-weight: 600;
}

.muchat-api-section .description {
    margin-bottom: 15px;
    color: #50575e;
    font-style: normal;
}

/* Product Meta Fields */
.muchat-api-meta-fields-wrapper {
    margin-top: 20px;
}

.muchat-api-meta-field {
    background: #fff;
    border: 1px solid #c3c4c7;
    border-radius: 2px;
    padding: 15px;
    transition: all 0.2s ease;
}

.muchat-api-meta-field:hover {
    box-shadow: 0 1px 4px rgba(0,0,0,0.1);
}

.meta-field-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 8px;
}

.meta-key {
    font-weight: 600;
    color: #1d2327;
}

.meta-usage {
    font-size: 0.85em;
    color: #50575e;
    display: block;
    margin-top: 4px;
}

.meta-samples {
    font-size: 0.9em;
    color: #50575e;
    margin-top: 8px;
    padding: 8px;
    background: #f6f7f7;
    border-radius: 3px;
    word-break: break-word;
    border: 1px solid #dcdcde;
}

.meta-field-label-input {
    margin-top: 8px;
    width: 100%;
    padding: 6px 8px;
    border: 1px solid #8c8f94;
    border-radius: 3px;
    box-shadow: 0 0 0 transparent;
}

.meta-field-label-wrapper {
    margin-top: 12px;
}

.meta-field-label-wrapper label {
    display: block;
    margin-bottom: 4px;
    font-size: 13px;
    color: #1d2327;
    font-weight: 500;
}

/* Meta Fields Documentation */
.meta-field-example {
    margin: 15px 0;
    padding: 12px;
    background: #f6f7f7;
    border-left: 4px solid #2271b1;
}

/* Submit Button Enhancement */
.wrap .submit {
    margin-top: 20px;
    padding-top: 15px;
    border-top: 1px solid #eee;
}

/* WordPress Core UI Elements */
#wpbody-content .wrap {
    margin: 10px 20px 0 2px;
}

.notice {
    margin: 20px 0 15px;
}

/* Settings Page Styles */
/* Fix checkbox alignment */
input[type=checkbox] {
    margin-top: 1px;
}

/* Improve table styling */
.muchat-api-meta-fields-wrapper table {
    border-spacing: 0;
}

.muchat-api-meta-fields-wrapper th {
    font-weight: 600;
    text-align: left;
    padding: 10px;
}

.muchat-api-meta-fields-wrapper td {
    padding: 12px 10px;
    vertical-align: middle;
}

/* Fix for long text overflow */
.muchat-api-meta-fields-wrapper .sample-values {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Fix regular-text width */
.muchat-api-meta-fields-wrapper input.regular-text {
    width: 100%;
}

/* Widget Settings Page Styles */
.action-buttons {
    margin: 15px 0;
}

.action-buttons .button {
    margin-right: 10px;
}

.customize-section {
    margin-top: 15px;
}

.initial-message-row {
    display: flex;
    margin-bottom: 10px;
    align-items: center;
}

.initial-message-row input {
    flex-grow: 1;
    margin-right: 5px;
}

#muchat-add-initial-message {
    margin-bottom: 15px;
}

/* Product Example Page Styles */
/* Ensure Select2 is properly styled */
.select2-container--default .select2-selection--single {
    border-color: #8c8f94;
    min-height: 30px;
}

/* Fix select2 dropdown width */
.select2-container {
    width: 100% !important;
}
</file>

<file path="includes/Core/Activator.php">
<?php

namespace Muchat\Api\Core;

class Activator
{
    /**
     * Fired during plugin activation
     *
     * @return void
     */
    public function activate()
    {
        // Get default fields
        $default_fields = $this->get_default_fields();

        // Initialize default options if they don't exist
        if (!get_option('muchat_ai_chatbot_plugin_options')) {
            update_option('muchat_ai_chatbot_plugin_options', [
                'product_fields' => array_keys($default_fields['product']),
                'post_fields'    => array_keys($default_fields['post']),
                'page_fields'    => array_keys($default_fields['page'])
            ]);
        }

        // Flush rewrite rules
        flush_rewrite_rules();
    }

    /**
     * Get default fields for all content types
     *
     * @return array
     */
    private function get_default_fields()
    {
        return [
            'product' => [
                'id'                => __('ID', 'muchat-ai'),
                'name'              => __('Name', 'muchat-ai'),
                'permalink'         => __('Permalink', 'muchat-ai'),
                'date_modified'     => __('Date Modified', 'muchat-ai'),
                'description'       => __('Description', 'muchat-ai'),
                'short_description' => __('Short Description', 'muchat-ai'),
                'currency' => __('Currency', 'muchat-ai'),
                'price'            => __('Price', 'muchat-ai'),
                'regular_price'     => __('Regular Price', 'muchat-ai'),
                'sale_price'       => __('Sale Price', 'muchat-ai'),
                'stock_status'     => __('Stock Status', 'muchat-ai'),
                'categories'       => __('Categories', 'muchat-ai'),
                'tags'             => __('Tags', 'muchat-ai'),
                'images'           => __('Images', 'muchat-ai'),
                'attributes'       => __('Attributes', 'muchat-ai'),
                'variations'       => __('Variations', 'muchat-ai')
            ],
            'post' => [
                'id'             => __('ID', 'muchat-ai'),
                'title'          => __('Title', 'muchat-ai'),
                'content'        => __('Content', 'muchat-ai'),
                'excerpt'        => __('Excerpt', 'muchat-ai'),
                'modified_date'  => __('Modified Date', 'muchat-ai'),
                'permalink'      => __('Permalink', 'muchat-ai'),
                'categories'     => __('Categories', 'muchat-ai'),
                'tags'           => __('Tags', 'muchat-ai'),
                'featured_image' => __('Featured Image', 'muchat-ai')
            ],
            'page' => [
                'id'             => __('ID', 'muchat-ai'),
                'title'          => __('Title', 'muchat-ai'),
                'content'        => __('Content', 'muchat-ai'),
                'excerpt'        => __('Excerpt', 'muchat-ai'),
                'modified_date'  => __('Modified Date', 'muchat-ai'),
                'permalink'      => __('Permalink', 'muchat-ai'),
                'featured_image' => __('Featured Image', 'muchat-ai')
            ]
        ];
    }
}
</file>

<file path="assets/js/admin.js">
(function ($) {
  'use strict';

  $(document).ready(function () {
    // Handle copy buttons - Uses modern Clipboard API with fallback
    $('.copy-button').on('click', function (e) {
      e.preventDefault();
      const button = $(this);
      const text = button.data('clipboard-text');
      const originalText = button.text();

      /**
       * Copy text to clipboard using modern Clipboard API
       * with fallback to legacy execCommand for older browsers
       */
      async function copyToClipboard(text) {
        // Modern Clipboard API (requires secure context: HTTPS or localhost)
        if (navigator.clipboard && window.isSecureContext) {
          try {
            await navigator.clipboard.writeText(text);
            return true;
          } catch (err) {
            console.warn('Clipboard API failed, trying fallback:', err);
          }
        }

        // Fallback for older browsers or non-HTTPS
        const textarea = document.createElement('textarea');
        textarea.value = text;
        textarea.style.position = 'fixed';
        textarea.style.opacity = '0';
        textarea.style.left = '-9999px';
        textarea.setAttribute('readonly', '');
        document.body.appendChild(textarea);

        try {
          textarea.select();
          textarea.setSelectionRange(0, textarea.value.length); // For mobile compatibility
          const success = document.execCommand('copy');
          document.body.removeChild(textarea);
          return success;
        } catch (err) {
          document.body.removeChild(textarea);
          throw err;
        }
      }

      // TRANSLATION: Use localized strings from WordPress
      const copiedText = (typeof muchatPluginAdmin !== 'undefined' && muchatPluginAdmin.copiedText) 
        ? muchatPluginAdmin.copiedText 
        : 'Copied!';
      const failedText = (typeof muchatPluginAdmin !== 'undefined' && muchatPluginAdmin.failedText) 
        ? muchatPluginAdmin.failedText 
        : 'Failed!';

      copyToClipboard(text)
        .then(() => {
          // Success feedback
          button.text(copiedText).addClass('copied');
          setTimeout(() => {
            button.text(originalText).removeClass('copied');
          }, 2000);
        })
        .catch((err) => {
          console.error('Copy failed:', err);
          button.text(failedText).addClass('error');
          setTimeout(() => {
            button.text(originalText).removeClass('error');
          }, 2000);
        });
    });

    // Show/hide label field based on checkbox state
    $('.muchat-api-meta-field input[type="checkbox"]').on('change', function () {
      const labelWrapper = $(this)
        .closest('.muchat-api-meta-field')
        .find('.meta-field-label-wrapper');

      if (this.checked) {
        labelWrapper.slideDown(200);
      } else {
        labelWrapper.slideUp(200);
      }
    });

    // Set initial state
    $('.muchat-api-meta-field input[type="checkbox"]').each(function () {
      const labelWrapper = $(this)
        .closest('.muchat-api-meta-field')
        .find('.meta-field-label-wrapper');

      if (!this.checked) {
        labelWrapper.hide();
      }
    });

    // Common admin functionality for all pages
    // Remove handlediv elements that control collapsing
    $('.postbox .handlediv').remove();

    // Remove closed class to ensure boxes are always open
    $('.postbox').removeClass('closed');

    // Make notices dismissible
    $('.notice.is-dismissible').each(function() {
        const $notice = $(this);
        if (!$notice.find('.notice-dismiss').length) {
            const $button = $('<button type="button" class="notice-dismiss"><span class="screen-reader-text">Dismiss this notice.</span></button>');

            $notice.append($button);

            $button.on('click', function() {
                $notice.fadeOut(100, function() {
                    $notice.remove();
                });
            });
        }
    });

    // Tab functionality using WordPress filter links
    $('.nav-tab').on('click', function(e) {
        e.preventDefault();

        // Get the tab ID
        var tabId = $(this).data('tab');

        // Update active state on tabs
        $('.nav-tab').removeClass('nav-tab-active');
        $(this).addClass('nav-tab-active');

        // Show/hide tab panels
        $('.tab-panel').hide();
        $('#' + tabId).show();
    });

    // Copy JSON functionality for product example page - Uses modern Clipboard API
    $('.copy-json').on('click', async function() {
        const jsonText = $('#json-example').text();
        const $button = $(this);
        const originalText = $button.text();

        /**
         * Copy text using modern Clipboard API with fallback
         */
        async function copyText(text) {
            // Modern Clipboard API (requires secure context)
            if (navigator.clipboard && window.isSecureContext) {
                try {
                    await navigator.clipboard.writeText(text);
                    return true;
                } catch (err) {
                    console.warn('Clipboard API failed, using fallback:', err);
                }
            }

            // Fallback for older browsers or non-HTTPS
            const textarea = document.createElement('textarea');
            textarea.value = text;
            textarea.setAttribute('readonly', '');
            textarea.style.position = 'absolute';
            textarea.style.left = '-9999px';
            document.body.appendChild(textarea);

            try {
                textarea.select();
                textarea.setSelectionRange(0, textarea.value.length);
                const success = document.execCommand('copy');
                document.body.removeChild(textarea);
                return success;
            } catch (err) {
                document.body.removeChild(textarea);
                throw err;
            }
        }

        // TRANSLATION: Use localized strings from WordPress
        const copiedText = (typeof muchatPluginAdmin !== 'undefined' && muchatPluginAdmin.copiedText) 
            ? muchatPluginAdmin.copiedText 
            : 'Copied!';
        const failedText = (typeof muchatPluginAdmin !== 'undefined' && muchatPluginAdmin.failedText) 
            ? muchatPluginAdmin.failedText 
            : 'Failed!';

        try {
            await copyText(jsonText);
            $button.text(copiedText);
            $button.addClass('button-primary');
        } catch (err) {
            console.error('Copy failed:', err);
            $button.text(failedText);
            $button.addClass('error');
        }

        setTimeout(function() {
            $button.text(originalText);
            $button.removeClass('button-primary error');
        }, 2000);
    });
  });
})(jQuery);

// Widget Settings Page Functionality
(function($) {
    'use strict';

    $(document).ready(function() {
        // --- Reusable function for Initial Messages Management ---
        function initializeMessageList(containerId, buttonId, inputId) {
            var container = document.getElementById(containerId);
            var addBtn = document.getElementById(buttonId);
            var hiddenInput = document.getElementById(inputId);

            if (!container || !addBtn || !hiddenInput) {
                return; // Exit if elements are not found
            }

            var messages = [];

            // Parse initial messages with error handling
            try {
                if (hiddenInput.value && hiddenInput.value.trim() !== '') {
                    try {
                        messages = JSON.parse(hiddenInput.value);
                        if (!Array.isArray(messages)) {
                            console.warn('Initial messages not in expected array format, converting...');
                            messages = hiddenInput.value.split(',').map(function(x) {
                                return x.trim();
                            });
                        }
                    } catch (e) {
                        console.warn('Failed to parse JSON, falling back to comma separation for ' + inputId, e);
                        messages = hiddenInput.value.split(',').map(function(x) {
                            return x.trim();
                        });
                    }
                }
            } catch (e) {
                console.error('Error processing initial messages for ' + inputId + ':', e);
                messages = [];
            }

            function render() {
                container.innerHTML = '';
                messages.forEach(function(msg, idx) {
                    var div = document.createElement('div');
                    div.className = 'initial-message-row';
                    var input = document.createElement('input');
                    input.type = 'text';
                    input.className = 'regular-text';
                    input.value = msg || '';
                    input.placeholder = 'Message...';
                    input.oninput = function() {
                        messages[idx] = input.value;
                        updateHidden();
                    };
                    var remove = document.createElement('button');
                    remove.type = 'button';
                    remove.className = 'button';
                    remove.innerHTML = '<span class="dashicons dashicons-trash"></span>';
                    remove.title = 'Remove message';
                    remove.onclick = function() {
                        messages.splice(idx, 1);
                        render();
                        updateHidden();
                    };
                    div.appendChild(input);
                    div.appendChild(remove);
                    container.appendChild(div);
                });
                updateHidden();
            }

            function updateHidden() {
                try {
                    var filtered = messages.map(function(x) {
                        return (x || '').trim();
                    }).filter(Boolean);
                    hiddenInput.value = JSON.stringify(filtered);
                } catch (e) {
                    console.error('Error updating hidden input for ' + inputId + ':', e);
                }
            }

            addBtn.onclick = function() {
                messages.push('');
                render();
            };

            render();
        }
        
        // Initialize both message lists
        initializeMessageList('muchat-initial-messages-list', 'muchat-add-initial-message', 'muchat_ai_chatbot_interface_initial_messages');
        initializeMessageList('muchat-guest-initial-messages-list', 'muchat-add-guest-initial-message', 'muchat_ai_chatbot_guest_initial_messages');

        // --- Toggle for Guest Initial Messages ---
        var guestToggleCheckbox = document.getElementById('muchat_ai_chatbot_enable_guest_messages');
        var guestMessagesRow = document.getElementById('muchat-guest-initial-messages-row');

        if (guestToggleCheckbox && guestMessagesRow) {
            function toggleGuestMessages() {
                guestMessagesRow.style.display = guestToggleCheckbox.checked ? 'table-row' : 'none';
            }
            
            // Set initial state on page load
            toggleGuestMessages();

            // Add event listener for changes
            guestToggleCheckbox.addEventListener('change', toggleGuestMessages);
        }
        
        // --- Toggle for Guest Context ---
        var guestContextCheckbox = document.getElementById('muchat_ai_chatbot_enable_guest_context');
        var guestContextRow = document.getElementById('muchat-guest-context-row');

        if (guestContextCheckbox && guestContextRow) {
            function toggleGuestContext() {
                guestContextRow.style.display = guestContextCheckbox.checked ? 'table-row' : 'none';
            }
            
            // Set initial state on page load
            toggleGuestContext();

            // Add event listener for changes
            guestContextCheckbox.addEventListener('change', toggleGuestContext);
        }


        // Schedule Settings Toggle
        var scheduleEnabled = document.getElementById('muchat_ai_chatbot_schedule_enabled');
        var scheduleOptions = document.querySelectorAll('.schedule-options');

        if (scheduleEnabled && scheduleOptions.length > 0) {
            function toggleScheduleOptions() {
                scheduleOptions.forEach(function(element) {
                    element.style.display = scheduleEnabled.checked ? 'table-row' : 'none';
                });
            }

            scheduleEnabled.addEventListener('change', toggleScheduleOptions);
        }
    });
})(jQuery);

// API Tester functionality
(function($) {
  'use strict';

  let searchTimeout;
  let currentPage = 1;
  let isLoading = false;
  let lastQuery = '';

  const contentSearch = $('#content-search');
  const searchResults = $('#search-results');
  const searchStats = $('#search-stats');
  const responseContent = $('#response-content');

  // Direct ID input handling
  contentSearch.on('input', function() {
      clearTimeout(searchTimeout);
      const query = $(this).val();
      
      // Check for direct ID input (e.g., "product/123")
      const directMatch = query.match(/^(product|post|page)\/(\d+)$/i);
      if (directMatch) {
          loadPreview(directMatch[1], directMatch[2]);
          return;
      }

      // Normal search
      if (query.length >= 2) {
          searchTimeout = setTimeout(() => performSearch(query), 300);
      } else {
          searchResults.empty();
          searchStats.hide();
      }
  });

  function performSearch(query, page = 1) {
      if (isLoading) return;
      
      isLoading = true;
      lastQuery = query;
      
      if (page === 1) {
          searchResults.empty();
      }

      contentSearch.addClass('loading');

      $.ajax({
          url: ajaxurl,
          type: 'POST',
          data: {
              action: 'muchat_api_search_content',
              query: query,
              page: page,
              nonce: muchatApiAdmin.nonce
          },
          success: function(response) {
              if (response.success) {
                  if (page === 1) {
                      searchResults.empty();
                      searchStats.text(`Found ${response.data.total} items`).show();
                  }
                  
                  response.data.items.forEach(item => {
                      const element = $(`
                          <div class="result-item" data-id="${item.id}" data-type="${item.type}">
                              <span class="result-type ${item.type}">${item.type}</span>
                              <span class="result-title">${item.title}</span>
                          </div>
                      `);
                      searchResults.append(element);
                  });

                  if (response.data.more) {
                      currentPage = page + 1;
                      // Load more when scrolling near bottom
                      initInfiniteScroll();
                  }
              }
          },
          complete: function() {
              isLoading = false;
              contentSearch.removeClass('loading');
          }
      });
  }

  function initInfiniteScroll() {
      searchResults.off('scroll').on('scroll', function() {
          if (!isLoading && 
              ($(this).scrollTop() + $(this).innerHeight() >= $(this)[0].scrollHeight - 50)) {
              performSearch(lastQuery, currentPage);
          }
      });
  }

  // Handle result selection
  searchResults.on('click', '.result-item', function() {
      searchResults.find('.result-item').removeClass('selected');
      $(this).addClass('selected');
      
      const id = $(this).data('id');
      const type = $(this).data('type');
      loadPreview(type, id);
  });

  function loadPreview(type, id) {
    responseContent.empty().addClass('loading');

    $.ajax({
        url: ajaxurl,
        type: 'POST',
        data: {
            action: 'muchat_api_preview',
            content_type: type,
            item_id: id,
            nonce: muchatApiAdmin.nonce
        },
        success: function(response) {
            if (response.success && response.data) {
                responseContent.html(formatJSON(response.data));
            } else {
                const errorMessage = response.data?.message || 'Error loading preview';
                responseContent.html(`
                    <div class="api-error">
                        <h3>Error</h3>
                        <p>${errorMessage}</p>
                    </div>
                `);
            }
        },
        error: function(jqXHR, textStatus, errorThrown) {
            responseContent.html(`
                <div class="api-error">
                    <h3>Error</h3>
                    <p>Failed to load preview: ${textStatus}</p>
                </div>
            `);
        },
        complete: function() {
            responseContent.removeClass('loading');
        }
    });
}

  function formatJSON(data) {
      return JSON.stringify(data, null, 2)
          .replace(/&/g, '&amp;')
          .replace(/</g, '&lt;')
          .replace(/>/g, '&gt;')
          .replace(/("(\\u[a-zA-Z0-9]{4}|\$$^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function(match) {
              let cls = 'number';
              if (/^"/.test(match)) {
                  if (/:$/.test(match)) {
                      cls = 'key';
                  } else {
                      cls = 'string';
                  }
              } else if (/true|false/.test(match)) {
                  cls = 'boolean';
              } else if (/null/.test(match)) {
                  cls = 'null';
              }
              return '<span class="json-' + cls + '">' + match + '</span>';
          });
  }

})(jQuery);
</file>

<file path="includes/Api/Controllers/OrderController.php">
<?php

namespace Muchat\Api\Api\Controllers;

use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

/**
 * Order Controller Class
 * Handles order tracking functionality with multiple search methods
 */
class OrderController
{
    /**
     * Logger instance
     * @var \WC_Logger
     */
    private $logger;

    /**
     * Constructor - initialize logger
     */
    public function __construct()
    {
        // Check if WooCommerce is active
        if (!function_exists('\\wc_get_logger')) {
            // Create a basic logger using error_log if WooCommerce is not available
            $this->logger = new class {
                public function error($message, $context = [])
                {
                    // error_log("Muchat API Error: " . $message);
                }
            };
            return;
        }

        $this->logger = \wc_get_logger();
    }

    /**
     * Track order endpoint handler
     * 
     * SECURITY: Implements additional protections against enumeration attacks:
     * - Random delay to prevent timing attacks
     * - Generic error messages to prevent information leakage
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response|WP_Error
     */
    public function track_order($request)
    {
        try {
            // SECURITY: Add artificial delay to prevent timing attacks (100-300ms)
            // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_mt_rand
            usleep(wp_rand(100000, 300000));
            
            // Check if WooCommerce is active
            if (!class_exists('WooCommerce')) {
                return new WP_Error(
                    'woocommerce_required',
                    __('WooCommerce is required for this functionality', 'muchat-ai'),
                    ['status' => 500]
                );
            }

            // Validate incoming parameters
            $params = $this->validate_parameters($request);
            if (is_wp_error($params)) {
                $this->log_error('Parameter validation failed', [
                    'error' => $params->get_error_message(),
                    'request_params' => $this->mask_sensitive_params($request->get_params())
                ]);
                return $params;
            }

            // Get order(s) based on parameters
            $result = $this->get_order($params);
            if (is_wp_error($result)) {
                $error_code = $result->get_error_code();
                
                // Log detailed error for debugging (with masked params)
                $this->log_error('Order retrieval failed', [
                    'error_code' => $error_code,
                    'params' => $this->mask_sensitive_params($params)
                ]);
                
                // SECURITY: Return generic error message to prevent enumeration
                return new WP_Error(
                    'order_not_found',
                    __('Unable to find order with the provided information', 'muchat-ai'),
                    ['status' => 404]
                );
            }

            // Always return array format
            $formatted_result = [];

            if (is_array($result)) {
                $formatted_result = $result;
            } else {
                // Single order case - wrap in array
                $formatted_result = [$this->format_order_data($result)];
            }

            return new WP_REST_Response($formatted_result, 200);
        } catch (\Exception $e) {
            $this->log_error('Unexpected error in track_order', [
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString()
            ]);
            return new WP_Error(
                'order_tracking_error',
                $e->getMessage(),
                ['status' => 500]
            );
        }
    }


    /**
     * Validate and sanitize request parameters
     * Performs thorough validation and sanitization of all input parameters
     *
     * @param WP_REST_Request $request
     * @return array|WP_Error
     */
    private function validate_parameters($request)
    {
        // Get and sanitize parameters
        $order_id = $this->sanitize_order_id($request->get_param('order_id'));
        $email = $this->sanitize_email($request->get_param('email'));
        $phone = $this->sanitize_phone($request->get_param('phone'));

        // Validate Order ID
        if ($order_id !== null) {
            if (!is_numeric($order_id) || $order_id <= 0) {
                return new WP_Error(
                    'invalid_order_id',
                    __('Invalid Order ID format', 'muchat-ai'),
                    ['status' => 400]
                );
            }
        }

        // Validate Email
        if ($email !== null) {
            if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
                return new WP_Error(
                    'invalid_email',
                    __('Invalid email format', 'muchat-ai'),
                    ['status' => 400]
                );
            }
        }

        // Validate Phone
        if ($phone !== null) {
            $normalized_phone = $this->normalize_phone_number($phone);
            if (empty($normalized_phone)) {
                return new WP_Error(
                    'invalid_phone',
                    __('Invalid phone number format', 'muchat-ai'),
                    ['status' => 400]
                );
            }
            $phone = $normalized_phone;
        }

        // Check if at least one valid parameter is provided
        if (!$order_id && !$email && !$phone) {
            return new WP_Error(
                'missing_parameters',
                __('At least one of Order ID, Email or Phone is required', 'muchat-ai'),
                ['status' => 400]
            );
        }

        return [
            'order_id' => $order_id,
            'email' => $email,
            'phone' => $phone
        ];
    }

    /**
     * Sanitize order ID
     * @param mixed $order_id
     * @return int|null
     */
    private function sanitize_order_id($order_id)
    {
        if ($order_id === null || $order_id === '') {
            return null;
        }
        return (int) sanitize_text_field($order_id);
    }

    /**
     * Sanitize email
     * @param mixed $email
     * @return string|null
     */
    private function sanitize_email($email)
    {
        if ($email === null || $email === '') {
            return null;
        }
        return sanitize_email($email);
    }

    /**
     * Sanitize phone number
     * @param mixed $phone
     * @return string|null
     */
    private function sanitize_phone($phone)
    {
        if ($phone === null || $phone === '') {
            return null;
        }
        return sanitize_text_field($phone);
    }

    /**
     * Normalize phone number to E.164 format
     * 
     * Handles international formats while maintaining backward compatibility
     * with Iranian number validation for stores primarily serving Iran.
     * 
     * @param string $phone Raw phone number
     * @param bool $strict_iranian If true, only validate Iranian numbers (default: false)
     * @return string Normalized phone number or empty string if invalid
     */
    private function normalize_phone_number($phone, $strict_iranian = false)
    {
        if (empty($phone)) {
            return '';
        }

        // Convert to string and trim
        $phone = trim((string) $phone);

        // Convert Persian/Arabic digits to Latin
        $persian_arabic = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹',
                           '٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'];
        $latin = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                  '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
        $phone = str_replace($persian_arabic, $latin, $phone);

        // Check if has leading + before stripping non-numeric
        $has_plus = (substr($phone, 0, 1) === '+');

        // Remove all non-numeric characters except leading +
        $phone = preg_replace('/[^0-9]/', '', $phone);

        if (empty($phone)) {
            return '';
        }

        // Handle Iranian formats first (backward compatibility)
        if ($this->is_iranian_phone($phone, $has_plus)) {
            return $this->normalize_iranian_phone($phone);
        }

        // If strict Iranian mode, return empty for non-Iranian numbers
        if ($strict_iranian) {
            return '';
        }

        // For international numbers, try to normalize to E.164
        if ($has_plus) {
            // Already has country code, just validate length
            $digits_only = $phone;
            if (strlen($digits_only) >= 7 && strlen($digits_only) <= 15) {
                return '+' . $digits_only;
            }
        }

        // Number without country code - store as-is if reasonable length
        if (strlen($phone) >= 7 && strlen($phone) <= 15) {
            return $phone;
        }

        return '';
    }

    /**
     * Check if phone number appears to be Iranian
     * 
     * @param string $digits Digits-only phone number
     * @param bool $had_plus Whether the original number had a + prefix
     * @return bool
     */
    private function is_iranian_phone($digits, $had_plus = false)
    {
        // Iranian mobile: starts with 09 or 989 or +989
        // Iranian landline: starts with 0 + 2-digit area code

        // Check for +98 or 98 prefix (international format)
        if ($had_plus && substr($digits, 0, 2) === '98' && strlen($digits) >= 12) {
            return true;
        }

        // Check for 98 prefix without plus
        if (substr($digits, 0, 2) === '98' && strlen($digits) >= 12 && strlen($digits) <= 13) {
            return true;
        }

        // Check for 09x mobile format
        if (substr($digits, 0, 2) === '09' && strlen($digits) === 11) {
            return true;
        }

        // Check for landline (0xx format)
        if (substr($digits, 0, 1) === '0' && strlen($digits) >= 10 && strlen($digits) <= 11) {
            // Exclude numbers that look like international (too long)
            if (strlen($digits) <= 11) {
                return true;
            }
        }

        return false;
    }

    /**
     * Normalize Iranian phone to standard 11-digit format (09xxxxxxxxx)
     * 
     * @param string $digits Digits-only phone number
     * @return string Normalized Iranian phone number
     */
    private function normalize_iranian_phone($digits)
    {
        // Remove +98 or 98 prefix
        if (substr($digits, 0, 2) === '98' && strlen($digits) >= 12) {
            $digits = '0' . substr($digits, 2);
        }

        // Add leading 0 if missing (9xxxxxxxxx format)
        if (substr($digits, 0, 1) === '9' && strlen($digits) === 10) {
            $digits = '0' . $digits;
        }

        // Validate final format for mobile (09xxxxxxxxx)
        if (strlen($digits) === 11 && substr($digits, 0, 2) === '09') {
            return $digits;
        }

        // Also allow landlines (0xx-xxxxxxx format = 10-11 digits)
        if (strlen($digits) >= 10 && strlen($digits) <= 11 && substr($digits, 0, 1) === '0') {
            return $digits;
        }

        return '';
    }

    /**
     * Get order(s) based on provided parameters
     * Handles both single order lookup and multiple order search
     *
     * @param array $params
     * @return \WC_Order|array|WP_Error
     */
    private function get_order($params)
    {
        // Single order lookup if order ID is provided
        if (!empty($params['order_id'])) {
            $order = wc_get_order($params['order_id']);
            if (!$order) {
                return new WP_Error(
                    'order_not_found',
                    __('Order not found', 'muchat-ai'),
                    ['status' => 404]
                );
            }

            // Validate email if provided
            if (!empty($params['email']) && $order->get_billing_email() !== $params['email']) {
                return new WP_Error(
                    'invalid_email',
                    __('Email does not match with order', 'muchat-ai'),
                    ['status' => 404]
                );
            }

            // Validate phone if provided
            if (!empty($params['phone'])) {
                $order_phone = $this->normalize_phone_number($order->get_billing_phone());
                if ($order_phone !== $params['phone']) {
                    return new WP_Error(
                        'invalid_phone',
                        __('Phone does not match with order', 'muchat-ai'),
                        ['status' => 404]
                    );
                }
            }

            // Return formatted order data directly
            return [$this->format_order_data($order)];
        }

        // For multiple order search, at least email or phone must be provided
        if (empty($params['email']) && empty($params['phone'])) {
            return new WP_Error(
                'missing_parameters',
                __('Email or phone is required when order ID is not provided', 'muchat-ai'),
                ['status' => 400]
            );
        }

        // Search parameters for multiple orders
        $args = [
            'return' => 'ids',
            'limit'  => 5,
            'orderby' => 'date',
            'order' => 'DESC',
        ];

        // Add search filters
        if (!empty($params['email'])) {
            $args['billing_email'] = $params['email'];
        }

        if (!empty($params['phone'])) {
            $args['billing_phone'] = $params['phone'];
        }

        // Get order IDs using the WooCommerce function
        $order_ids = wc_get_orders($args);

        if (empty($order_ids)) {
            return new WP_Error(
                'order_not_found',
                __('No orders found with the provided criteria', 'muchat-ai'),
                ['status' => 404]
            );
        }

        // Format orders for response
        $formatted_orders = [];
        foreach ($order_ids as $order_id) {
            $order = wc_get_order($order_id);
            if ($order && !is_a($order, 'WC_Order_Refund')) {
                try {
                    $formatted_orders[] = $this->format_order_data($order);
                } catch (\Exception $e) {
                    continue; // Skip orders with formatting issues
                }
            }
        }

        if (empty($formatted_orders)) {
            return new WP_Error(
                'order_not_found',
                __('No valid orders found', 'muchat-ai'),
                ['status' => 404]
            );
        }

        return $formatted_orders;
    }

    /**
     * Format order data for API response
     * Prepares comprehensive order information
     *
     * @param \WC_Order $order
     * @return array
     */
    private function format_order_data($order)
    {
        // Format date helper function
        $format_date = function ($date_object) {
            if (!$date_object) {
                return null;
            }

            if (function_exists('parsidate')) {
                return parsidate('Y-m-d H:i:s', $date_object->getTimestamp(), 'eng');
            }

            return $date_object->format('Y-m-d H:i:s');
        };

        // Get and format public meta data
        $public_meta_data = array_filter($order->get_meta_data(), function ($meta) {
            return !str_starts_with($meta->key, '_');
        });

        $formatted_meta = [];
        foreach ($public_meta_data as $meta) {
            $formatted_meta[$meta->key] = $this->format_meta_value($meta);
        }

        // Get customer notes
        $customer_notes = wc_get_order_notes([
            'order_id' => $order->get_id(),
            'type' => 'customer'
        ]);

        // Prepare response data
        return [
            'status' => $order->get_status(),
            'status_label' => wc_get_order_status_name($order->get_status()),
            'date_created' => $format_date($order->get_date_created()),
            'currency' => html_entity_decode(get_woocommerce_currency_symbol($order->get_currency())),

            'customer' => [
                'first_name' => $order->get_billing_first_name(),
                'last_name' => $order->get_billing_last_name(),
                'email' => $order->get_billing_email(),
                'phone' => $this->normalize_phone_number($order->get_billing_phone())
            ],

            'items' => array_values(array_map(function ($item) {
                return [
                    'name' => $item->get_name(),
                    'quantity' => $item->get_quantity(),
                    'total' => wc_format_decimal($item->get_total())
                ];
            }, $order->get_items())),

            'totals' => [
                'subtotal' => wc_format_decimal($order->get_subtotal()),
                'shipping_total' => wc_format_decimal($order->get_shipping_total()),
                'tax_total' => wc_format_decimal($order->get_total_tax()),
                'discount_total' => wc_format_decimal($order->get_total_discount()),
                'total' => wc_format_decimal($order->get_total())
            ],

            'payment' => [
                'method_title' => $order->get_payment_method_title()
            ],

            'shipping' => [
                'method' => $order->get_shipping_method()
            ],

            'customer_notes' => array_values(array_map(function ($note) {
                return $note->content;
            }, $customer_notes)),

            'meta_data' => $formatted_meta

        ];
    }

    /**
     * Format meta value based on type
     * 
     * @param \WC_Meta_Data $meta
     * @return mixed
     */
    private function format_meta_value($meta)
    {
        if (strpos($meta->key, 'date') !== false || strpos($meta->key, '_time') !== false) {
            $timestamp = is_numeric($meta->value) ? $meta->value : strtotime($meta->value);
            if ($timestamp) {
                if (function_exists('parsidate')) {
                    return parsidate('Y-m-d H:i:s', $timestamp, 'eng');
                }
                return gmdate('Y-m-d H:i:s', $timestamp);
            }
        }
        return $meta->value;
    }

    /**
     * Log error messages
     * 
     * @param string $message
     * @param array $context
     * @return void
     */
    private function log_error($message, $context = [])
    {
        $this->logger->error(
            $message . ': ' . wp_json_encode($context),
            ['source' => 'muchat-api-order-tracking']
        );
    }
    
    /**
     * Mask sensitive parameters for logging
     * 
     * SECURITY: Prevents sensitive data from being logged
     * 
     * @param array $params Parameters to mask
     * @return array Masked parameters
     */
    private function mask_sensitive_params($params)
    {
        $masked = [];
        foreach ($params as $key => $value) {
            if (is_string($value) && strlen($value) > 3) {
                $masked[$key] = substr($value, 0, 3) . '***';
            } else {
                $masked[$key] = '***';
            }
        }
        return $masked;
    }
}
</file>

<file path="includes/Api/Controllers/ProductController.php">
<?php

namespace Muchat\Api\Api\Controllers;

use Muchat\Api\Models\Product;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

class ProductController
{
    /**
     * @var Product
     */
    private $product_model;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->product_model = new Product();
    }

    /**
     * Get products
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response|WP_Error
     */
    public function get_products($request)
    {
        try {
            // Check if WooCommerce is active
            if (!class_exists('WooCommerce')) {
                return new WP_Error(
                    'woocommerce_required',
                    __('WooCommerce is required for this functionality', 'muchat-ai'),
                    ['status' => 500]
                );
            }

            // Get required parameters
            $modified_after = $request->get_param('modified_after');
            $muchat_modified_after = $request->get_param('muchat_modified_after');
            $order_by = $request->get_param('order_by');
            $order = $request->get_param('order');
            $in_stock = $request->get_param('in_stock');

            // Get skip and take parameters
            $skip = $request->get_param('skip');
            $take = $request->get_param('take');

            // Prepare parameters for the model
            $params = [
                'modified_after' => $modified_after,
                'muchat_modified_after' => $muchat_modified_after,
                'order_by' => $order_by,
                'order' => $order,
                'in_stock' => $in_stock,
                'skip' => $skip,
                'take' => $take
            ];

            // Start output buffering to catch any unexpected output
            ob_start();
            $result = $this->product_model->get_products($params);
            $unexpected_output = ob_get_clean();

            // If there was unexpected output, log it for debugging
            if (!empty($unexpected_output) && function_exists('error_log')) {
                error_log('Unexpected output in Products API: ' . $unexpected_output);
            }

            // Create proper response
            $response = new WP_REST_Response($result, 200);
            $response->header('Cache-Control', 'no-cache, no-store, must-revalidate');
            $response->header('Pragma', 'no-cache');
            $response->header('Expires', '0');

            return $response;
        } catch (\Exception $e) {
            // Log the error for debugging
            if (function_exists('error_log')) {
                error_log('Error in Products API: ' . $e->getMessage());
            }

            return new WP_Error(
                'products_fetch_error',
                $e->getMessage(),
                ['status' => 500]
            );
        }
    }

    /**
     * Get single product
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response|WP_Error
     */
    public function get_single_product($request)
    {
        try {
            // Check if WooCommerce is active
            if (!class_exists('WooCommerce')) {
                return new WP_Error(
                    'woocommerce_required',
                    __('WooCommerce is required for this functionality', 'muchat-ai'),
                    ['status' => 500]
                );
            }

            $product_id = (int) $request['id'];

            if (!$product_id) {
                return new WP_Error(
                    'invalid_product_id',
                    __('Invalid product ID', 'muchat-ai'),
                    ['status' => 400]
                );
            }

            // Start output buffering to catch any unexpected output
            ob_start();
            $product = $this->product_model->get_product($product_id);
            $unexpected_output = ob_get_clean();

            // If there was unexpected output, log it for debugging
            if (!empty($unexpected_output) && function_exists('error_log')) {
                error_log('Unexpected output in Product API (single): ' . $unexpected_output);
            }

            if (!$product) {
                return new WP_Error(
                    'product_not_found',
                    __('Product not found', 'muchat-ai'),
                    ['status' => 404]
                );
            }

            return new WP_REST_Response($product, 200);
        } catch (\Exception $e) {
            // Log the error for debugging
            if (function_exists('error_log')) {
                error_log('Error in Product API (single): ' . $e->getMessage());
            }

            return new WP_Error(
                'product_fetch_error',
                $e->getMessage(),
                ['status' => 500]
            );
        }
    }

    /**
     * Get product IDs
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response|WP_Error
     */
    public function get_product_ids($request)
    {
        try {
            // Check if WooCommerce is active
            if (!class_exists('WooCommerce')) {
                return new WP_Error(
                    'woocommerce_required',
                    __('WooCommerce is required for this functionality', 'muchat-ai'),
                    ['status' => 500]
                );
            }

            $modified_after = $request->get_param('modified_after');
            $muchat_modified_after = $request->get_param('muchat_modified_after');

            // Start output buffering to catch any unexpected output
            ob_start();
            $result = $this->product_model->get_product_ids($modified_after, $muchat_modified_after);
            $unexpected_output = ob_get_clean();

            // If there was unexpected output, log it for debugging
            if (!empty($unexpected_output) && function_exists('error_log')) {
                error_log('Unexpected output in Product IDs API: ' . $unexpected_output);
            }

            return new WP_REST_Response($result, 200);
        } catch (\Exception $e) {
            // Log the error for debugging
            if (function_exists('error_log')) {
                error_log('Error in Product IDs API: ' . $e->getMessage());
            }

            return new WP_Error(
                'product_ids_fetch_error',
                $e->getMessage(),
                ['status' => 500]
            );
        }
    }
}
</file>

<file path="includes/Api/Middleware/AuthMiddleware.php">
<?php

namespace Muchat\Api\Api\Middleware;

class AuthMiddleware
{
    /**
     * SECURITY: Short cache duration (5 minutes) to balance performance with security
     * This ensures revoked tokens are invalidated quickly
     */
    const TOKEN_CACHE_DURATION = 5 * MINUTE_IN_SECONDS;

    /**
     * SECURITY: Cache duration for failed validation attempts (1 minute)
     * Helps prevent brute force attacks while allowing quick retry
     */
    const FAILED_CACHE_DURATION = MINUTE_IN_SECONDS;
    
    /**
     * RELIABILITY: Circuit breaker failure threshold
     */
    const CIRCUIT_BREAKER_THRESHOLD = 5;
    
    /**
     * RELIABILITY: Circuit breaker timeout in seconds
     */
    const CIRCUIT_BREAKER_TIMEOUT = 30;

    /**
     * Verify API token
     *
     * @param \WP_REST_Request $request
     * @return bool|\WP_Error
     */
    public function verify_token($request)
    {
        if (defined('MUCHAT_DEV_MODE') && MUCHAT_DEV_MODE === true) {
            return true;
        }

        // Production mode implementation:
        $auth_header = $request->get_header('Authorization');

        if (!$auth_header || !preg_match('/^Bearer\s+(.*)$/i', $auth_header, $matches)) {
            return new \WP_Error(
                'invalid_auth',
                __('Invalid authentication header', 'muchat-ai'),
                ['status' => 401]
            );
        }

        $token = trim($matches[1]);

        // Validate the token
        $validation_result = $this->validate_token($token, $request);

        if (is_wp_error($validation_result)) {
            return $validation_result;
        }

        return true;
    }

    /**
     * Makes an API request to validate the token
     * 
     * SECURITY: Implements tiered caching with short TTL for security balance
     * - Uses SHA-256 instead of MD5 for token hashing
     * - Reduces cache duration to 5 minutes
     * - Supports force-revalidation header from Muchat service
     * - Caches failed attempts briefly to prevent brute force
     * 
     * RELIABILITY: Implements circuit breaker pattern to prevent cascading failures
     * - Opens circuit after 5 consecutive failures
     * - Auto-closes after 30 seconds
     * - Uses cached valid tokens during outage
     * 
     * @param string $token The token to validate
     * @param \WP_REST_Request|null $request Optional request object for checking headers
     * @return bool|\WP_Error True on success, WP_Error on failure
     */
    private function validate_token($token, $request = null)
    {
        // SECURITY: Use SHA-256 instead of MD5 for better hash quality
        $token_hash = hash('sha256', $token);
        $cache_key = 'token_' . substr($token_hash, 0, 32);

        // RELIABILITY: Check circuit breaker state
        $circuit_state = \Muchat\Api\Utils\Cache::get('circuit_breaker');
        if ($circuit_state === 'open') {
            // Circuit is open - fail fast
            // Check if we have a cached valid token during outage
            $cached_result = \Muchat\Api\Utils\Cache::get($cache_key);
            if ($cached_result !== false && isset($cached_result['valid']) && $cached_result['valid']) {
                return true; // Use cached valid token during outage
            }
            
            return new \WP_Error(
                'service_unavailable',
                __('Authentication service temporarily unavailable. Please try again later.', 'muchat-ai'),
                ['status' => 503]
            );
        }

        // SECURITY: Check for force-revalidation header from Muchat service
        $force_revalidate = false;
        if ($request && method_exists($request, 'get_header')) {
            $revalidate_header = $request->get_header('X-Muchat-Revalidate');
            $force_revalidate = ($revalidate_header === 'true');
        }

        // Check cache first (unless force revalidation requested)
        if (!$force_revalidate) {
            $cached_result = \Muchat\Api\Utils\Cache::get($cache_key);
            if ($cached_result !== false) {
                // SECURITY: Cached result includes validation status
                if (is_array($cached_result) && isset($cached_result['valid'])) {
                    if ($cached_result['valid'] === true) {
                        return true;
                    }
                    // Invalid token was cached - return error
                    return new \WP_Error(
                        'invalid_token',
                        __('Invalid token', 'muchat-ai'),
                        ['status' => 401]
                    );
                }
                // Legacy cache format (just true) - accept but will be updated on next validation
                if ($cached_result === true) {
                    return true;
                }
            }
        }

        global $wp_filter;

        // Save the current filter state
        $saved_http_request_filter = isset($wp_filter['http_request_args']) ? $wp_filter['http_request_args'] : null;
        $saved_pre_http_request_filter = isset($wp_filter['pre_http_request']) ? $wp_filter['pre_http_request'] : null;

        // Temporary solution - Remove potentially interfering filters for this specific request
        if (isset($wp_filter['http_request_args'])) {
            unset($wp_filter['http_request_args']);
        }
        if (isset($wp_filter['pre_http_request'])) {
            unset($wp_filter['pre_http_request']);
        }

        try {
            // MAINTAINABILITY: Using centralized constant for App URL
            $app_url = defined('MUCHAT_APP_URL') ? MUCHAT_APP_URL : 'https://app.mu.chat';
            
            // RELIABILITY: Allow endpoint override via filter for testing/staging
            $auth_endpoint = apply_filters('muchat_auth_endpoint', $app_url . '/api/validate-token');
            
            // Make the request with reduced timeout
            $response = wp_remote_post($auth_endpoint, [
                'headers' => [
                    'Authorization' => "Bearer {$token}",
                    'Accept' => 'application/json',
                    'User-Agent' => 'WordPress/' . get_bloginfo('version') . '; ' . get_bloginfo('url')
                ],
                'timeout' => 5, // RELIABILITY: Reduced from 15 for faster fail-fast
                'sslverify' => true, // SECURITY: Enable SSL verification in production
                'blocking' => true,
                'redirection' => 5,
                'httpversion' => '1.1'
            ]);

            if (is_wp_error($response)) {
                $error_message = $response->get_error_message();

                // RELIABILITY: Increment failure counter for circuit breaker
                $failures = (int) \Muchat\Api\Utils\Cache::get('auth_failures') + 1;
                \Muchat\Api\Utils\Cache::set('auth_failures', $failures, 5 * MINUTE_IN_SECONDS);
                
                // Open circuit after threshold failures
                if ($failures >= self::CIRCUIT_BREAKER_THRESHOLD) {
                    \Muchat\Api\Utils\Cache::set('circuit_breaker', 'open', self::CIRCUIT_BREAKER_TIMEOUT);
                    \Muchat\Api\Utils\Cache::delete('auth_failures');
                }

                return new \WP_Error(
                    'token_validation_failed',
                    // translators: %s: error message from the API response
                    sprintf(__('Token validation failed: %s', 'muchat-ai'), $error_message),
                    ['status' => 401]
                );
            }

            // RELIABILITY: Reset failure counter on successful connection
            \Muchat\Api\Utils\Cache::delete('auth_failures');

            $response_code = wp_remote_retrieve_response_code($response);

            if ($response_code !== 200) {
                // SECURITY: Cache invalid tokens briefly to prevent brute force
                \Muchat\Api\Utils\Cache::set($cache_key, [
                    'valid' => false,
                    'time' => time(),
                ], self::FAILED_CACHE_DURATION);

                return new \WP_Error(
                    'invalid_token',
                    __('Invalid token', 'muchat-ai'),
                    ['status' => 401]
                );
            }

            // SECURITY: Cache successful validation with metadata and short TTL
            \Muchat\Api\Utils\Cache::set($cache_key, [
                'valid' => true,
                'time' => time(),
            ], self::TOKEN_CACHE_DURATION);

            return true;
        } finally {
            // Restore the original filters
            if ($saved_http_request_filter) {
                $wp_filter['http_request_args'] = $saved_http_request_filter;
            }
            if ($saved_pre_http_request_filter) {
                $wp_filter['pre_http_request'] = $saved_pre_http_request_filter;
            }
        }
    }

    /**
     * Clear token cache (for use when receiving revocation webhook)
     * 
     * SECURITY: Allows immediate invalidation of tokens when revoked on Muchat server
     * 
     * @param string $token The token to invalidate
     */
    public static function invalidate_token_cache($token)
    {
        \Muchat\Api\Utils\Cache::invalidate_token($token);
    }

    /**
     * Verify webhook signature from Muchat service
     * 
     * SECURITY: Validates HMAC-SHA256 signature to ensure webhook is from Muchat
     * 
     * @param \WP_REST_Request $request The incoming request
     * @return bool True if signature is valid
     */
    public function verify_webhook_signature($request)
    {
        $signature = $request->get_header('X-Muchat-Signature');
        $payload = $request->get_body();
        $secret = get_option('muchat_webhook_secret');

        // If no secret configured, deny all webhooks
        if (empty($signature) || empty($secret)) {
            return false;
        }

        // Compute expected signature
        $expected = hash_hmac('sha256', $payload, $secret);

        // Use timing-safe comparison
        return hash_equals($expected, $signature);
    }

    /**
     * Handle token revocation webhook from Muchat service
     * 
     * SECURITY: Immediately invalidates cached tokens when revoked on Muchat server
     * This eliminates the 5-minute cache window for compromised tokens.
     * 
     * @param \WP_REST_Request $request The incoming request
     * @return \WP_REST_Response The response
     */
    public function handle_token_revocation($request)
    {
        $body = json_decode($request->get_body(), true);

        if (isset($body['token_hash'])) {
            // Clear specific token from cache using the same key format as validate_token()
            $cache_key = 'token_' . substr($body['token_hash'], 0, 32);
            \Muchat\Api\Utils\Cache::delete($cache_key);

            return new \WP_REST_Response([
                'success' => true,
                'message' => 'Token cache invalidated'
            ], 200);
        }

        // If 'clear_all' is specified, clear all token caches
        if (isset($body['clear_all']) && $body['clear_all'] === true) {
            \Muchat\Api\Utils\Cache::clear_cache_by_type('token');

            return new \WP_REST_Response([
                'success' => true,
                'message' => 'All token caches cleared'
            ], 200);
        }

        return new \WP_REST_Response([
            'success' => false,
            'message' => 'No action taken - missing token_hash or clear_all parameter'
        ], 400);
    }
}
</file>

<file path="includes/Api/Routes.php">
<?php

namespace Muchat\Api\Api;

use Muchat\Api\Api\Controllers\ProductController;
use Muchat\Api\Api\Controllers\ProductInitController;
use Muchat\Api\Api\Controllers\PostController;
use Muchat\Api\Api\Controllers\PageController;
use Muchat\Api\Api\Controllers\OrderController;
use Muchat\Api\Api\Controllers\CustomPostTypeController;
use Muchat\Api\Api\Middleware\AuthMiddleware;
use Muchat\Api\Api\Middleware\RateLimitMiddleware;

class Routes
{
    /**
     * @var string
     */
    private $namespace = 'muchat-api/v1';

    /**
     * @var ProductController
     */
    private $product_controller;

    /**
     * @var ProductInitController
     */
    private $product_init_controller;

    /**
     * @var PostController
     */
    private $post_controller;

    /**
     * @var PageController
     */
    private $page_controller;

    /**
     * @var OrderController|null
     */
    private $order_controller;

    /**
     * @var CustomPostTypeController
     */
    private $custom_post_type_controller;

    /**
     * @var AuthMiddleware
     */
    private $auth_middleware;
    
    /**
     * @var RateLimitMiddleware
     */
    private $rate_limit_middleware;

    /**
     * Constructor
     * 
     * DEPENDENCY: Controllers are injected but WooCommerce-dependent controllers
     * should gracefully handle WooCommerce not being active.
     *
     * @param ProductController $product_controller
     * @param PostController $post_controller
     * @param PageController $page_controller
     * @param OrderController|null $order_controller Optional if WooCommerce not active
     */
    public function __construct(
        ProductController $product_controller,
        PostController $post_controller,
        PageController $page_controller,
        ?OrderController $order_controller = null
    ) {
        $this->product_controller = $product_controller;
        $this->product_init_controller = new ProductInitController();
        $this->post_controller = $post_controller;
        $this->page_controller = $page_controller;
        
        // DEPENDENCY: Only set order controller if provided and WooCommerce is active
        // This prevents issues if WooCommerce is deactivated after plugin is loaded
        $this->order_controller = $order_controller;
        
        $this->custom_post_type_controller = new CustomPostTypeController();
        $this->auth_middleware = new AuthMiddleware();
        $this->rate_limit_middleware = new RateLimitMiddleware();
    }

    /**
     * Register REST API routes
     *
     * @return void
     */
    public function register_routes()
    {
        $routes = [];

        // WooCommerce-dependent routes - only register if WooCommerce is active
        if (class_exists('WooCommerce')) {
            $woocommerce_routes = [
                [
                    'path' => '/products',
                    'callback' => [$this->product_controller, 'get_products'],
                    'methods' => \WP_REST_Server::CREATABLE
                ],
                [
                    'path' => '/products/(?P<id>[\d]+)',
                    'callback' => [$this->product_controller, 'get_single_product'],
                    'methods' => \WP_REST_Server::CREATABLE
                ],
                [
                    'path' => '/product-ids',
                    'callback' => [$this->product_controller, 'get_product_ids'],
                    'methods' => \WP_REST_Server::CREATABLE
                ],
                [
                    'path' => '/products/initialize-muchat-tracking',
                    'callback' => [$this->product_init_controller, 'initialize_products'],
                    'methods' => \WP_REST_Server::CREATABLE
                ],
                [
                    'path' => '/orders/track',
                    'callback' => [$this->order_controller, 'track_order'],
                    'methods' => \WP_REST_Server::CREATABLE,
                    'rate_limit_type' => 'sensitive', // SECURITY: 20 requests per minute to prevent enumeration
                ]
            ];

            $routes = array_merge($routes, $woocommerce_routes);
        }

        // Always available routes
        $standard_routes = [
            [
                'path' => '/posts',
                'callback' => [$this->post_controller, 'get_posts'],
                'methods' => \WP_REST_Server::CREATABLE
            ],
            [
                'path' => '/pages',
                'callback' => [$this->page_controller, 'get_pages'],
                'methods' => \WP_REST_Server::CREATABLE
            ],
            [
                // SECURITY: Custom post types now require authentication to prevent IDOR
                // This prevents exposure of potentially sensitive CPTs like internal_docs,
                // employee_profiles, abandoned_carts, customer_wishlists, etc.
                'path' => '/custom-post-types/(?P<post_type>[a-zA-Z0-9_-]+)',
                'callback' => [$this->custom_post_type_controller, 'get_custom_post_type_items'],
                'methods' => \WP_REST_Server::CREATABLE, // Changed to POST with auth
                'public' => false, // SECURITY: Now requires authentication
            ]
        ];

        $routes = array_merge($routes, $standard_routes);

        // Register webhook endpoint for token revocation
        // SECURITY: This allows Muchat to immediately invalidate tokens when revoked
        register_rest_route($this->namespace, '/webhook/token-revoked', [
            'methods' => \WP_REST_Server::CREATABLE,
            'callback' => [$this->auth_middleware, 'handle_token_revocation'],
            'permission_callback' => [$this->auth_middleware, 'verify_webhook_signature'],
        ]);

        foreach ($routes as $route) {
            // Determine endpoint type for rate limiting
            $endpoint_type = !empty($route['public']) ? 'public' : 'default';
            if (isset($route['rate_limit_type'])) {
                $endpoint_type = $route['rate_limit_type'];
            }
            
            $route_args = [
                [
                    'methods' => $route['methods'],
                    'callback' => $this->wrap_callback_with_rate_limit(
                        $route['callback'],
                        $endpoint_type
                    ),
                    'permission_callback' => !empty($route['public']) 
                        ? '__return_true' 
                        : [$this, 'check_auth_and_rate_limit'],
                    'args' => $this->get_collection_params(),
                    // Add headers to prevent caching
                    'headers' => [
                        'Cache-Control' => 'no-cache, no-store, must-revalidate',
                        'Pragma' => 'no-cache',
                        'Expires' => '0'
                    ]
                ]
            ];

            register_rest_route($this->namespace, $route['path'], $route_args);
        }
    }
    
    /**
     * Combined permission callback for auth and rate limiting
     * 
     * SECURITY: Checks rate limit BEFORE authentication to prevent
     * brute force attacks from consuming server resources.
     * 
     * @param \WP_REST_Request $request
     * @return bool|\WP_Error
     */
    public function check_auth_and_rate_limit($request)
    {
        // Check rate limit first (cheap operation)
        $rate_check = $this->rate_limit_middleware->check_rate_limit($request, 'default');
        if (is_wp_error($rate_check)) {
            return $rate_check;
        }
        
        // Then check authentication
        return $this->auth_middleware->verify_token($request);
    }
    
    /**
     * Wrap a callback to add rate limit headers to response
     * 
     * @param callable $callback Original callback
     * @param string $endpoint_type Endpoint type for rate limiting
     * @return callable Wrapped callback
     */
    private function wrap_callback_with_rate_limit($callback, $endpoint_type)
    {
        $rate_middleware = $this->rate_limit_middleware;
        
        return function($request) use ($callback, $endpoint_type, $rate_middleware) {
            // For public endpoints, check rate limit here
            if ($endpoint_type === 'public') {
                $rate_check = $rate_middleware->check_rate_limit($request, 'public');
                if (is_wp_error($rate_check)) {
                    return $rate_check;
                }
            }
            
            // Call original callback
            $response = call_user_func($callback, $request);
            
            // Add rate limit headers if response is valid
            if ($response instanceof \WP_REST_Response) {
                $response = $rate_middleware->add_rate_limit_headers(
                    $response, 
                    $request, 
                    $endpoint_type
                );
            }
            
            return $response;
        };
    }

    /**
     * Get collection parameters
     *
     * @return array
     */
    private function get_collection_params()
    {
        return [
            'skip' => [
                'description' => __('Number of items to skip.', 'muchat-ai'),
                'type' => 'integer',
                'default' => 0,
                'minimum' => 0,
                'sanitize_callback' => 'absint',
            ],
            'take' => [
                'description' => __('Number of items to take.', 'muchat-ai'),
                'type' => 'integer',
                'default' => 30,
                'minimum' => 1,
                'maximum' => 100,
                'sanitize_callback' => 'absint',
            ],
            'order_by' => [
                'description' => __('Sort collection by attribute. Can be a single field or comma-separated list (e.g., "modified,ID").', 'muchat-ai'),
                'type' => 'string',
                'default' => 'modified,ID',
                // 'enum' => ['date', 'id', 'modified', 'title'],
            ],
            'order' => [
                'description' => __('Order sort attribute ascending or descending.', 'muchat-ai'),
                'type' => 'string',
                'default' => 'ASC',
                'enum' => ['ASC', 'DESC'],
            ],
            'modified_after' => [
                'description' => __('Limit results to items modified after specified date.', 'muchat-ai'),
                'type' => 'string',
                'validate_callback' => function ($param) {
                    return strtotime($param) !== false;
                }
            ],
            'muchat_modified_after' => [
                'description' => __('Limit results to items with muchat_date_modified after specified date (only price/stock changes).', 'muchat-ai'),
                'type' => 'string',
                'validate_callback' => function ($param) {
                    return strtotime($param) !== false;
                }
            ],
            'in_stock' => [
                'description' => __('Filter to show only in-stock products (product-specific).', 'muchat-ai'),
                'type' => 'string',
                'enum' => ['true', 'false'],
                'default' => 'false'
            ]
        ];
    }
}
</file>

<file path="includes/Core/Plugin.php">
<?php

namespace Muchat\Api\Core;

class Plugin
{
    /**
     * Loader instance
     *
     * @var Loader
     */
    protected $loader;

    /**
     * Plugin version
     *
     * @var string
     */
    protected $version;

    /**
     * Settings class instance
     *
     * @var \Muchat\Api\Admin\Settings
     */
    private $settings_instance;

    /**
     * Initialize the plugin
     */
    public function __construct()
    {
        $this->version = MUCHAT_AI_CHATBOT_PLUGIN_VERSION;

        $this->load_dependencies();
        $this->load_textdomain();

        // Only run admin code in admin environment
        if (is_admin()) {
            $this->setup_admin();
        } else {
            // Initialize frontend widget
            new \Muchat\Api\Frontend\Widget();
        }

        // Register the plugin action links filter
        $this->loader->add_filter('plugin_action_links_' . plugin_basename(MUCHAT_AI_CHATBOT_PLUGIN_FILE), $this, 'add_plugin_action_links');

        // Instead of directly executing setup_api, just register dependencies
        // so that API code is only executed when the API is called
        $this->loader->add_action('rest_api_init', $this, 'register_api_routes');
        $this->loader->add_filter('rest_pre_serve_request', $this, 'add_cors_and_cache_headers', 10, 4);

        // Invalidate the pages-to-exclude cache when WooCommerce page settings change
        add_action('woocommerce_update_options', ['\Muchat\Api\Models\Page', 'invalidate_pages_to_exclude_cache']);
    }

    /**
     * Load plugin translations
     *
     * @return void
     */
    private function load_textdomain()
    {
        load_plugin_textdomain(
            'muchat-ai',
            false,
            dirname(plugin_basename(MUCHAT_AI_CHATBOT_PLUGIN_FILE)) . '/languages/'
        );
    }

    /**
     * Load required dependencies
     *
     * @return void
     */
    private function load_dependencies()
    {
        $this->loader = new Loader();
    }

    /**
     * Get (and instantiate if necessary) the settings admin class
     *
     * @return \Muchat\Api\Admin\Settings
     */
    public function get_settings_instance()
    {
        if (null === $this->settings_instance) {
            $this->settings_instance = new \Muchat\Api\Admin\Settings();
        }
        return $this->settings_instance;
    }

    /**
     * Register admin hooks
     *
     * @return void
     */
    private function setup_admin()
    {
        $this->loader->add_action('admin_menu', $this, 'add_admin_menu');
        $this->loader->add_action('admin_init', $this, 'register_admin_settings');
        $this->loader->add_action('admin_enqueue_scripts', $this, 'enqueue_admin_assets');

    }

    
    /**
     * Wrapper for adding the admin menu
     */
    public function add_admin_menu()
    {
        $this->get_settings_instance()->add_menu_page();
    }

    /**
     * Wrapper for registering settings
     */
    public function register_admin_settings()
    {
        $this->get_settings_instance()->register_settings();
    }

    /**
     * Wrapper for enqueuing admin assets
     *
     * @param string $hook The current admin page hook.
     */
    public function enqueue_admin_assets($hook)
    {
        $this->get_settings_instance()->enqueue_styles($hook);
        $this->get_settings_instance()->enqueue_scripts($hook);
    }

    /**
     * Add settings link to plugin action links
     *
     * @param array $links Existing plugin action links
     * @return array Modified plugin action links
     */
    public function add_plugin_action_links($links)
    {
        $settings_link = '<a href="' . admin_url('admin.php?page=muchat-settings') . '">' . __('Settings', 'muchat-ai') . '</a>';
        array_unshift($links, $settings_link);
        return $links;
    }

    /**
     * Register API routes and controllers
     * This function is only executed when the rest_api_init hook is called
     *
     * @return void
     */
    public function register_api_routes()
    {
        // Disable object caching for our endpoints - This is only executed here, not on all pages
        add_filter('rest_cache_skip', function ($skip, $request) {
            if (strpos($request->get_route(), 'muchat-api/v1') !== false) {
                return true;
            }
            return $skip;
        }, 10, 2);

        // Set cache headers EARLY in the request lifecycle
        // This catches cases where caching happens at the filter level
        add_filter('rest_pre_dispatch', function ($result, $server, $request) {
            if (strpos($request->get_route(), 'muchat-api/v1') !== false) {
                // Set headers immediately for cache busting
                if (!headers_sent()) {
                    header('Cache-Control: private, no-cache, no-store, must-revalidate, max-age=0');
                    header('Vary: Authorization, Cookie');
                }
                // Disable WordPress object caching for this request
                wp_suspend_cache_addition(true);
            }
            return $result;
        }, 1, 3);

        // Prevent WordPress from running unwanted scripts on our API endpoints
        add_filter('script_loader_tag', function ($tag, $handle, $src) {
            if (
                defined('REST_REQUEST') && REST_REQUEST && isset($_SERVER['REQUEST_URI'])
                && strpos($_SERVER['REQUEST_URI'], 'muchat-api/v1') !== false
            ) {
                return '';
            }
            return $tag;
        }, 10, 3);



        // Controllers are only created when the API is called
        $routes = new \Muchat\Api\Api\Routes(
            new \Muchat\Api\Api\Controllers\ProductController(),
            new \Muchat\Api\Api\Controllers\PostController(),
            new \Muchat\Api\Api\Controllers\PageController(),
            new \Muchat\Api\Api\Controllers\OrderController()
        );

        $routes->register_routes();
    }

    /**
     * Add CORS and cache prevention headers for API responses
     * 
     * PERFORMANCE: Includes headers for various caching layers:
     * - Browser cache (Cache-Control, Pragma, Expires)
     * - Varnish (X-Accel-Expires)
     * - CDNs like Fastly/CloudFlare (Surrogate-Control)
     * - Vary header for proper cache keying by Authorization
     *
     * @param bool $served Whether the request has already been served
     * @param WP_REST_Response $result Response to send
     * @param WP_REST_Request $request Request used to generate response
     * @param WP_REST_Server $server Server instance
     * @return bool
     */
    public function add_cors_and_cache_headers($served, $result, $request, $server)
    {
        // Check if this is our API endpoint
        if (strpos($request->get_route(), 'muchat-api/v1') !== false) {
            // Standard cache prevention headers
            header('Cache-Control: private, no-cache, no-store, must-revalidate, max-age=0');
            header('Pragma: no-cache');
            header('Expires: Thu, 01 Jan 1970 00:00:00 GMT');

            // Varnish-specific header
            header('X-Accel-Expires: 0');

            // Surrogate-Control for CDNs like Fastly/CloudFlare
            header('Surrogate-Control: no-store');

            // Prevent caching by authenticated user
            // Cookie is included because widget responses include user-specific data
            header('Vary: Authorization, Accept-Encoding, Cookie');

            // CORS headers for our endpoints
            header('Access-Control-Allow-Headers: Authorization, Content-Type, X-Requested-With, X-Muchat-Revalidate');
            header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
            header('Access-Control-Allow-Credentials: true');

            // Security headers
            header('X-Content-Type-Options: nosniff');
            header('X-Frame-Options: DENY');
        }
        return $served;
    }

    /**
     * Run the plugin
     *
     * @return void
     */
    public function run()
    {
        $this->loader->run();
    }

    /**
     * Get loader instance
     *
     * @return Loader
     */
    public function get_loader()
    {
        return $this->loader;
    }

    /**
     * Get plugin version
     *
     * @return string
     */
    public function get_version()
    {
        return $this->version;
    }
}
</file>

<file path="templates/admin/product-example.php">
<?php

/**
 * Admin Product Example Template
 *
 * @package Muchat\Api
 */

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

// Get screen object
$screen = get_current_screen();

// Enqueue WooCommerce admin scripts
wp_enqueue_script('wc-enhanced-select');
wp_enqueue_style('woocommerce_admin_styles');
?>

<div class="wrap">
    <h1 class="wp-heading-inline"><?php echo esc_html__('Product Example', 'muchat-ai'); ?></h1>

    <hr class="wp-header-end">

    <?php if (!empty($error_message)): ?>
        <div class="notice notice-error is-dismissible">
            <p><?php echo esc_html($error_message); ?></p>
        </div>
    <?php endif; ?>

    <div class="notice notice-info is-dismissible">
        <p>
            <?php echo esc_html__('This page allows you to view how a specific product will appear in API responses.', 'muchat-ai'); ?>
            <?php
            echo sprintf(
                // translators: %1$s: opening link tag, %2$s: closing link tag
                esc_html__('To modify which fields are included, visit the %1$sAPI Settings%2$s page.', 'muchat-ai'),
                '<a href="' . esc_url(admin_url('admin.php?page=muchat-api-settings')) . '">',
                '</a>'
            ); ?>
        </p>
    </div>

    <div class="metabox-holder" style="margin-top: 20px;">
        <div id="product-search" class="postbox">
            <div class="postbox-header">
                <h2 class="hndle"><?php echo esc_html__('Select Product', 'muchat-ai'); ?></h2>
            </div>
            <div class="inside">
                <form method="post" action="" style="padding: 12px;">
                    <?php wp_nonce_field('muchat_ai_chatbot_product_example_action', 'muchat_ai_chatbot_product_example_nonce'); ?>
                    <div style="margin-bottom: 8px;">
                        <label for="product_id" style="display: block; margin-bottom: 8px; font-weight: 600;">
                            <?php echo esc_html__('Select a Product', 'muchat-ai'); ?>
                        </label>
                        <div style="display: flex; align-items: center; gap: 10px;">
                            <div style="flex: 1;">
                                <select
                                    class="wc-product-search"
                                    id="product_id"
                                    name="product_id"
                                    data-placeholder="<?php esc_attr_e('Search for a product...', 'muchat-ai'); ?>"
                                    data-allow_clear="true"
                                    data-type="variable"
                                    data-exclude_type="variation"
                                    style="width: 100%;">
                                    <?php
                                    // If we have a product ID, pre-populate the select with that product
                                    if (!empty($product_id)) {
                                        $product = wc_get_product($product_id);
                                        if ($product) {
                                            echo '<option value="' . esc_attr($product_id) . '" selected>' . esc_html($product->get_name()) . ' (#' . esc_html($product_id) . ')</option>';
                                        }
                                    }
                                    ?>
                                </select>
                            </div>
                            <?php submit_button(__('Get Example', 'muchat-ai'), 'primary', 'submit', false); ?>
                        </div>
                        <p class="description" style="margin-top: 8px;">
                            <?php echo esc_html__('Search for a product to view its API response.', 'muchat-ai'); ?>
                        </p>
                    </div>
                </form>
            </div>
        </div>

        <?php if (!empty($example_data)): ?>
            <div id="api-response" class="postbox" style="margin-top: 20px;">
                <div class="postbox-header">
                    <h2 class="hndle"><?php echo esc_html__('API Response Example', 'muchat-ai'); ?></h2>
                </div>
                <div class="inside" style="padding: 12px;">
                    <div style="position: relative; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 4px; margin-bottom: 10px; overflow: hidden;">
                        <div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #f5f5f5; border-bottom: 1px solid #dcdcde;">
                            <span style="font-weight: 600; font-size: 13px;"><?php echo esc_html__('JSON Response', 'muchat-ai'); ?></span>
                            <button type="button" class="button button-secondary copy-json">
                                <?php echo esc_html__('Copy', 'muchat-ai'); ?>
                            </button>
                        </div>
                        <pre id="json-example" style="padding: 16px; margin: 0; overflow: auto; max-height: 500px; white-space: pre-wrap; font-family: Consolas, Monaco, monospace; font-size: 13px; line-height: 1.6; color: #333;"><?php echo esc_html(json_encode($example_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); ?></pre>
                    </div>
                </div>
            </div>
        <?php elseif ($product_id > 0): ?>
            <div class="notice notice-error is-dismissible" style="margin-top: 20px;">
                <p><?php echo esc_html__('Product not found or error retrieving data.', 'muchat-ai'); ?></p>
            </div>
        <?php endif; ?>
    </div>
</div>
</file>

<file path="includes/Models/Page.php">
<?php

namespace Muchat\Api\Models;

use Muchat\Api\Traits\SecureOrderHandling;

class Page extends BaseModel
{
    use SecureOrderHandling;

    /**
     * Get pages
     *
     * @param array $params
     * @return array
     */
    public function get_pages($params)
    {
        // Base query args
        $args = [
            'post_type' => 'page',
            'post_status' => 'publish',
            'fields' => 'ids',
        ];

        // SECURITY: Use validated orderby from whitelist
        $args['orderby'] = $this->sanitize_orderby(
            $params['order_by'] ?? null,
            $params['order'] ?? 'ASC'
        );

        // Note: Do NOT set $args['order'] when using array-based orderby
        // as it will override the individual sort orders in the array

        // Add date filters if provided
        if (!empty($params['modified_after'])) {
            $modified_after_timestamp = strtotime($params['modified_after']);
            if ($modified_after_timestamp) {
                $args['date_query'][] = [
                    'column' => 'post_modified_gmt',
                    'after' => gmdate('Y-m-d H:i:s', $modified_after_timestamp),
                    'inclusive' => false
                ];
            }
        }

        // Get pages to exclude
        $pages_to_exclude = $this->get_pages_to_exclude();
        if (!empty($pages_to_exclude)) {
            $args['post__not_in'] = $pages_to_exclude;
        }

        // Set pagination parameters with boundary validation
        $requested_offset = max(0, isset($params['skip']) ? (int) $params['skip'] : 0);
        $requested_limit = min(max(1, isset($params['take']) ? (int) $params['take'] : 10), 100);

        $args['posts_per_page'] = $requested_limit;
        $args['offset'] = $requested_offset;
        $args['no_found_rows'] = false; // We need found_posts for total_count

        // Execute the final query with pagination
        $query = new \WP_Query($args);

        // Get total count from the same query
        $total_count = (int) $query->found_posts;

        // Fallback: If found_posts is 0 but we have results, some optimization plugin
        // may have set no_found_rows to true. Run a separate COUNT query.
        if ($total_count === 0 && !empty($query->posts)) {
            $count_args = $args;
            $count_args['posts_per_page'] = -1;
            $count_args['offset'] = 0;
            $count_args['fields'] = 'ids';
            $count_args['no_found_rows'] = true; // We only need the count, not pagination info
            $count_query = new \WP_Query($count_args);
            $total_count = $count_query->post_count;
        }

        // Prime post cache to avoid N+1 queries
        if (!empty($query->posts)) {
            _prime_post_caches($query->posts, true, true);
        }

        // Format the pages
        $pages = array_map(function ($page_id) {
            return $this->format_page(get_post($page_id), 'list');
        }, $query->posts);

        $plugin = new \Muchat\Api\Core\Plugin();

        return [
            'plugin_version' => $plugin->get_version(),
            'offset' => $requested_offset,
            'limit' => $requested_limit,
            'total_count' => $total_count,
            'has_more' => ($requested_offset + count($pages) < $total_count),
            'items' => array_values(array_filter($pages))
        ];
    }

    /**
     * Get single page by ID
     *
     * @param int $page_id
     * @return array|null
     */
    public function get_page($page_id)
    {
        $page = get_post($page_id);

        if (!$page || $page->post_type !== 'page' || $page->post_status !== 'publish') {
            return null;
        }

        return $this->format_page($page, 'single');
    }

    /**
     * Get an array of page IDs to exclude
     *
     * Cached for 1 hour to avoid repeated DB lookups on every page request.
     * Cache is invalidated when WooCommerce page settings change.
     *
     * @return array
     */
    private function get_pages_to_exclude()
    {
        $cache_key = 'muchat_pages_to_exclude';
        $cached = get_transient($cache_key);

        if ($cached !== false) {
            return $cached;
        }

        $woocommerce_pages = [];
        // Only check WooCommerce pages if WooCommerce is active
        if (class_exists('WooCommerce')) {
            $woocommerce_pages = array_filter([
                wc_get_page_id('cart'),
                wc_get_page_id('checkout'),
                wc_get_page_id('myaccount'),
                wc_get_page_id('shop'),
                wc_get_page_id('terms'),
            ]);
        }
        // Additional pages to exclude regardless of WooCommerce
        $additional_pages = array_filter([
            $this->get_page_id_by_slug('wishlist'),
            $this->get_page_id_by_slug('compare'),
            $this->get_page_id_by_slug('wish-list'),
            $this->get_page_id_by_slug('compare-list'),
            $this->get_page_id_by_slug('my-wishlist'),
            $this->get_page_id_by_slug('my-compare'),
            $this->get_page_id_by_title('Wishlist'),
            $this->get_page_id_by_title('Compare'),
            $this->get_page_id_by_title('Wish List'),
            $this->get_page_id_by_title('Compare List'),
            $this->get_page_id_by_title('My Wishlist'),
            $this->get_page_id_by_title('My Compare'),
            $this->get_page_id_by_title('علاقه‌مندی‌ها'), // For Persian sites
            $this->get_page_id_by_title('مقایسه') // For Persian sites
        ]);

        $result = array_unique(array_merge($woocommerce_pages, $additional_pages));

        set_transient($cache_key, $result, HOUR_IN_SECONDS);

        return $result;
    }

    /**
     * Invalidate the pages-to-exclude cache when WooCommerce page settings change
     */
    public static function invalidate_pages_to_exclude_cache()
    {
        delete_transient('muchat_pages_to_exclude');
    }


    /**
     * Get page ID by slug
     */
    private function get_page_id_by_slug($slug)
    {
        $page = get_page_by_path($slug);
        return $page ? $page->ID : null;
    }

    /**
     * Get page ID by title
     */
    private function get_page_id_by_title($title)
    {
        // Use WP_Query instead of deprecated get_page_by_title
        $query = new \WP_Query([
            'post_type' => 'page',
            'title' => $title,
            'posts_per_page' => 1,
            'post_status' => 'publish',
        ]);

        if ($query->have_posts()) {
            return $query->posts[0]->ID;
        }

        return null;
    }


    /**
     * Format page data
     *
     * @param \WP_Post $page
     * @param string $context Either 'list' or 'single' - determines formatting depth
     * @return array|null
     */
    protected function format_page($page, $context = 'list')
    {
        $data = [];
        $enabled_fields = $this->get_enabled_fields('page');

        foreach ($enabled_fields as $field) {
            switch ($field) {
                case 'id':
                    $data['id'] = $page->ID;
                    break;
                case 'title':
                    $data['title'] = get_the_title($page);
                    break;
                case 'content':
                    if ($context === 'single') {
                        $data['content'] = $this->formatter->clean_html_content(apply_filters('the_content', $page->post_content));
                    } else {
                        $data['content'] = $this->formatter->clean_html_content_light(apply_filters('the_content', $page->post_content), 50);
                    }
                    break;
                case 'excerpt':
                    if ($context === 'single') {
                        $data['excerpt'] = $this->formatter->clean_html_content(get_the_excerpt($page));
                    } else {
                        $data['excerpt'] = $this->formatter->clean_html_content_light(get_the_excerpt($page), 30);
                    }
                    break;
                case 'modified_date':
                    $data['date_modified'] = \Muchat\Api\Utils\DateTimeHelper::format_for_api($page->post_modified_gmt ?? null);
                    break;
                case 'permalink':
                    $data['url'] = urldecode(get_permalink($page->ID));
                    break;
                case 'featured_image':
                    $data['featured_image'] = $this->get_featured_image($page);
                    break;
            }
        }

        $cleaned_data = $this->formatter->remove_empty_values($data);

        // Ensure the ID is always present to prevent the page from being filtered out
        if (!isset($cleaned_data['id'])) {
            $cleaned_data['id'] = $page->ID;
        }

        return $cleaned_data;
    }

    /**
     * Get featured image
     *
     * @param \WP_Post $page
     * @return string|null
     */
    protected function get_featured_image($page)
    {
        if (!has_post_thumbnail($page)) {
            return null;
        }

        $image_id = get_post_thumbnail_id($page);
        $image_url = wp_get_attachment_url($image_id);

        return $image_url ?: null;
    }
}
</file>

<file path="includes/Models/Post.php">
<?php

namespace Muchat\Api\Models;

use Muchat\Api\Traits\SecureOrderHandling;

class Post extends BaseModel
{
    use SecureOrderHandling;

    /**
     * Get posts
     *
     * @param array $params
     * @return array
     */
    public function get_posts($params)
    {
        // Base query args
        $args = [
            'post_type' => 'post',
            'post_status' => 'publish',
            'fields' => 'ids',
        ];

        // SECURITY: Use validated orderby from whitelist
        $args['orderby'] = $this->sanitize_orderby(
            $params['order_by'] ?? null,
            $params['order'] ?? 'ASC'
        );

        // Note: Do NOT set $args['order'] when using array-based orderby
        // as it will override the individual sort orders in the array

        // Add date filters if provided
        if (!empty($params['modified_after'])) {
            $modified_after_timestamp = strtotime($params['modified_after']);
            if ($modified_after_timestamp) {
                $args['date_query'][] = [
                    'column' => 'post_modified_gmt',
                    'after' => gmdate('Y-m-d H:i:s', $modified_after_timestamp),
                    'inclusive' => false
                ];
            }
        }

        // Set pagination parameters with boundary validation
        $requested_offset = max(0, isset($params['skip']) ? (int) $params['skip'] : 0);
        $requested_limit = min(max(1, isset($params['take']) ? (int) $params['take'] : 10), 100);

        // Update the query with pagination parameters
        $args['posts_per_page'] = $requested_limit;
        $args['offset'] = $requested_offset;
        $args['no_found_rows'] = false; // We need found_posts for total_count

        // Execute the final query with pagination
        $query = new \WP_Query($args);

        // Get total count from the same query
        $total_count = (int) $query->found_posts;

        // Fallback: If found_posts is 0 but we have results, some optimization plugin
        // may have set no_found_rows to true. Run a separate COUNT query.
        if ($total_count === 0 && !empty($query->posts)) {
            $count_args = $args;
            $count_args['posts_per_page'] = -1;
            $count_args['offset'] = 0;
            $count_args['fields'] = 'ids';
            $count_args['no_found_rows'] = true; // We only need the count, not pagination info
            $count_query = new \WP_Query($count_args);
            $total_count = $count_query->post_count;
        }

        // Prime post cache to avoid N+1 queries
        if (!empty($query->posts)) {
            _prime_post_caches($query->posts, true, true);
        }

        // Format the posts
        $posts = array_map(function ($post_id) {
            return $this->format_post(get_post($post_id), 'list');
        }, $query->posts);


        $plugin = new \Muchat\Api\Core\Plugin();

        return [
            'plugin_version' => $plugin->get_version(),
            'offset' => $requested_offset,
            'limit' => $requested_limit,
            'total_count' => $total_count,
            'has_more' => ($requested_offset + count($posts) < $total_count),
            'items' => array_values(array_filter($posts))
        ];
    }

    /**
     * Get single post by ID
     *
     * @param int $post_id
     * @return array|null
     */
    public function get_post($post_id)
    {
        $post = get_post($post_id);

        if (!$post || $post->post_type !== 'post' || $post->post_status !== 'publish') {
            return null;
        }

        return $this->format_post($post, 'single');
    }

    /**
     * Check if a post is valid (has content)
     * 
     * @param \WP_Post $post
     * @return bool
     */
    private function is_valid_post($post)
    {
        if (empty($post) || empty($post->post_content)) {
            return false;
        }

        // Check that the content is not empty and not just whitespace characters
        $content = trim(wp_strip_all_tags($post->post_content));
        return !empty($content);
    }

    /**
     * Format post data
     *
     * @param \WP_Post $post
     * @param string $context Either 'list' or 'single' - determines formatting depth
     * @return array|null
     */
    protected function format_post($post, $context = 'list')
    {
        $data = [];
        $enabled_fields = $this->get_enabled_fields('post');

        foreach ($enabled_fields as $field) {
            switch ($field) {
                case 'id':
                    $data['id'] = $post->ID;
                    break;
                case 'title':
                    $data['title'] = get_the_title($post);
                    break;
                case 'content':
                    if ($context === 'single') {
                        $data['content'] = $this->formatter->clean_html_content(apply_filters('the_content', $post->post_content));
                    } else {
                        $data['content'] = $this->formatter->clean_html_content_light(apply_filters('the_content', $post->post_content), 50);
                    }
                    break;
                case 'excerpt':
                    if ($context === 'single') {
                        $data['excerpt'] = $this->formatter->clean_html_content(get_the_excerpt($post));
                    } else {
                        $data['excerpt'] = $this->formatter->clean_html_content_light(get_the_excerpt($post), 30);
                    }
                    break;
                case 'modified_date':
                    $data['date_modified'] = \Muchat\Api\Utils\DateTimeHelper::format_for_api($post->post_modified_gmt ?? null);
                    break;
                case 'permalink':
                    $data['url'] = urldecode(get_permalink($post->ID));
                    break;
                case 'featured_image':
                    $data['featured_image'] = $this->get_featured_image($post);
                    break;
                case 'categories':
                    $data['categories'] = $this->get_taxonomy_terms($post, 'category');
                    break;
                case 'tags':
                    $data['tags'] = $this->get_taxonomy_terms($post, 'post_tag');
                    break;
            }
        }

        $cleaned_data = $this->formatter->remove_empty_values($data);

        // Ensure the ID is always present to prevent the post from being filtered out
        if (!isset($cleaned_data['id'])) {
            $cleaned_data['id'] = $post->ID;
        }

        return $cleaned_data;
    }

    /**
     * Get featured image
     *
     * @param \WP_Post $post
     * @return string|null
     */
    protected function get_featured_image($post)
    {
        if (!has_post_thumbnail($post)) {
            return null;
        }

        $image_id = get_post_thumbnail_id($post);
        $image_url = wp_get_attachment_url($image_id);

        return $image_url ?: null;
    }

    /**
     * Get taxonomy terms
     *
     * @param \WP_Post $post
     * @param string $taxonomy
     * @return array
     */
    protected function get_taxonomy_terms($post, $taxonomy)
    {
        $terms = get_the_terms($post->ID, $taxonomy);
        $term_data = [];

        if ($terms && !is_wp_error($terms)) {
            foreach ($terms as $term) {
                $term_data[urldecode($term->name)] = urldecode(get_term_link($term, $taxonomy));
            }
        }

        return $term_data;
    }
}
</file>

<file path="templates/admin/api-documentation.php">
<?php
if (!defined('ABSPATH')) {
    exit;
}

// Get screen object
$screen = get_current_screen();
?>
<div class="wrap">
    <h1 class="wp-heading-inline"><?php echo esc_html__('API Documentation', 'muchat-ai'); ?></h1>

    <hr class="wp-header-end">

    <?php settings_errors('muchat_ai_chatbot_plugin_messages'); ?>

    <div class="notice notice-info">
        <p>
            <?php echo esc_html__('This page provides detailed documentation on how to use the Muchat API endpoints.', 'muchat-ai'); ?>
        </p>
    </div>

    <div class="metabox-holder">
        <!-- Base URL Section -->
        <div id="base-url" class="postbox">
            <div class="postbox-header">
                <h2 class="hndle"><?php echo esc_html__('Base URL', 'muchat-ai'); ?></h2>
            </div>
            <div class="inside">
                <p class="description"><?php echo esc_html__('All API endpoints use this base URL:', 'muchat-ai'); ?></p>
                <div class="wp-clearfix" style="margin: 15px 0;">
                    <code style="display: inline-block; margin-right: 10px; padding: 8px; background: #f0f0f1; border-radius: 3px;"><?php echo esc_html(rest_url('muchat-api/v1')); ?></code>
                    <button type="button" class="button copy-button" data-clipboard-text="<?php echo esc_url(rest_url('muchat-api/v1')); ?>">
                        <?php echo esc_html__('Copy', 'muchat-ai'); ?>
                    </button>
                </div>
            </div>
        </div>

        <!-- Authentication Section -->
        <div id="authentication" class="postbox">
            <div class="postbox-header">
                <h2 class="hndle"><?php echo esc_html__('Authentication', 'muchat-ai'); ?></h2>
            </div>
            <div class="inside">
                <p><?php echo esc_html__('Most API requests require authentication. Use the Authorization header with a Bearer token.', 'muchat-ai'); ?></p>
                <p><strong><?php echo esc_html__('Note:', 'muchat-ai'); ?></strong> <?php echo esc_html__('The Custom Post Types endpoint (/custom-post-types/{post_type}) is public and does not require an authentication token.', 'muchat-ai'); ?></p>
                <p class="description"><?php echo esc_html__('Example headers for protected endpoints:', 'muchat-ai'); ?></p>
                <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace;">Authorization: Bearer YOUR_API_TOKEN
Content-Type: application/json</pre>
            </div>
        </div>

        <!-- Available Endpoints Section -->
        <div id="endpoints" class="postbox">
            <div class="postbox-header">
                <h2 class="hndle"><?php echo esc_html__('Available Endpoints', 'muchat-ai'); ?></h2>
            </div>
            <div class="inside">
                <table class="wp-list-table widefat fixed striped">
                    <thead>
                        <tr>
                            <th scope="col" width="15%"><?php echo esc_html__('Endpoint', 'muchat-ai'); ?></th>
                            <th scope="col" width="10%"><?php echo esc_html__('Method', 'muchat-ai'); ?></th>
                            <th scope="col" width="25%"><?php echo esc_html__('Description', 'muchat-ai'); ?></th>
                            <th scope="col" width="50%"><?php echo esc_html__('URL', 'muchat-ai'); ?></th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php
                        $endpoints = [
                            [
                                'path' => '/products',
                                'description' => __('Retrieve all products with pagination', 'muchat-ai'),
                                'url' => rest_url('muchat-api/v1/products')
                            ],
                            [
                                'path' => '/products/{id}',
                                'description' => __('Retrieve a single product by ID', 'muchat-ai'),
                                'url' => rest_url('muchat-api/v1/products/{id}')
                            ],
                            [
                                'path' => '/product-ids',
                                'description' => __('Retrieve all product IDs', 'muchat-ai'),
                                'url' => rest_url('muchat-api/v1/product-ids')
                            ],
                            [
                                'path' => '/posts',
                                'description' => __('Retrieve all posts with pagination', 'muchat-ai'),
                                'url' => rest_url('muchat-api/v1/posts')
                            ],
                            [
                                'path' => '/pages',
                                'description' => __('Retrieve all pages with pagination', 'muchat-ai'),
                                'url' => rest_url('muchat-api/v1/pages')
                            ],
                            [
                                'path' => '/custom-post-types/{post_type}',
                                'description' => __('Retrieve all items from a custom post type with pagination', 'muchat-ai'),
                                'url' => rest_url('muchat-api/v1/custom-post-types/{post_type}')
                            ],
                            [
                                'path' => '/orders/track',
                                'description' => __('Track an order by order ID and email', 'muchat-ai'),
                                'url' => rest_url('muchat-api/v1/orders/track')
                            ]
                        ];

                        foreach ($endpoints as $endpoint):
                        ?>
                            <tr>
                                <td><code style="word-break: keep-all;"><?php echo esc_html($endpoint['path']); ?></code></td>
                                <td><span class="badge" style="background-color: #0073aa; color: white; padding: 3px 8px; border-radius: 3px; display: inline-block;">GET</span></td>
                                <td><?php echo esc_html($endpoint['description']); ?></td>
                                <td>
                                    <div class="wp-clearfix">
                                        <code style="display: inline-block; margin-right: 10px; padding: 4px 8px; background: #f0f0f1; border-radius: 3px; font-size: 12px; word-break: break-all; max-width: 80%;"><?php echo esc_html($endpoint['url']); ?></code>
                                        <button type="button" class="button copy-button" data-clipboard-text="<?php echo esc_url($endpoint['url']); ?>">
                                            <?php echo esc_html__('Copy', 'muchat-ai'); ?>
                                        </button>
                                    </div>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
        </div>

        <!-- Query Parameters Section -->
        <div id="parameters" class="postbox">
            <div class="postbox-header">
                <h2 class="hndle"><?php echo esc_html__('Query Parameters', 'muchat-ai'); ?></h2>
            </div>
            <div class="inside">
                <table class="wp-list-table widefat fixed striped">
                    <thead>
                        <tr>
                            <th scope="col" width="15%"><?php echo esc_html__('Parameter', 'muchat-ai'); ?></th>
                            <th scope="col" width="15%"><?php echo esc_html__('Type', 'muchat-ai'); ?></th>
                            <th scope="col" width="55%"><?php echo esc_html__('Description', 'muchat-ai'); ?></th>
                            <th scope="col" width="15%"><?php echo esc_html__('Default', 'muchat-ai'); ?></th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php
                        $parameters = [
                            [
                                'name' => 'skip',
                                'type' => 'integer',
                                'description' => __('Number of items to skip for pagination', 'muchat-ai'),
                                'default' => '0'
                            ],
                            [
                                'name' => 'take',
                                'type' => 'integer',
                                'description' => __('Number of items to take (max: 100)', 'muchat-ai'),
                                'default' => '30'
                            ],
                            [
                                'name' => 'modified_after',
                                'type' => 'string',
                                'description' => __('Filter items modified after date (Y-m-d H:i:s)', 'muchat-ai'),
                                'default' => 'null'
                            ],
                            [
                                'name' => 'order_by',
                                'type' => 'string',
                                'description' => __('Sort collection by attribute. Can be a single field or comma-separated list (e.g., "modified,ID")', 'muchat-ai'),
                                'default' => 'modified,ID'
                            ],
                            [
                                'name' => 'order',
                                'type' => 'string',
                                'description' => __('Sort order: ASC or DESC', 'muchat-ai'),
                                'default' => 'ASC'
                            ],
                            [
                                'name' => 'in_stock',
                                'type' => 'boolean',
                                'description' => __('Filter to show only in-stock products', 'muchat-ai'),
                                'default' => 'false'
                            ]
                        ];

                        foreach ($parameters as $param):
                        ?>
                            <tr>
                                <td><code><?php echo esc_html($param['name']); ?></code></td>
                                <td><span class="badge" style="background-color: #f0f0f1; color: #50575e; padding: 2px 6px; border-radius: 3px; font-size: 12px; display: inline-block;"><?php echo esc_html($param['type']); ?></span></td>
                                <td><?php echo esc_html($param['description']); ?></td>
                                <td><code><?php echo esc_html($param['default']); ?></code></td>
                            </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
        </div>

        <!-- Example Requests Section -->
        <div id="examples" class="postbox">
            <div class="postbox-header">
                <h2 class="hndle"><?php echo esc_html__('Example Requests', 'muchat-ai'); ?></h2>
            </div>
            <div class="inside">
                <div class="nav-tab-wrapper wp-clearfix">
                    <a href="#curl-tab" class="nav-tab nav-tab-active" data-tab="curl-tab">cURL</a>
                    <a href="#php-tab" class="nav-tab" data-tab="php-tab">PHP</a>
                    <a href="#js-tab" class="nav-tab" data-tab="js-tab">JavaScript</a>
                </div>

                <div class="tab-panel" id="curl-tab" style="display: block; margin-top: 15px;">
                    <h3><?php echo esc_html__('Basic Request (cURL)', 'muchat-ai'); ?></h3>
                    <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;">curl -X POST \
  <?php echo esc_html(rest_url('muchat-api/v1/products')); ?> \
  -H 'Authorization: Bearer YOUR_API_TOKEN' \
  -H 'Content-Type: application/json'</pre>

                    <h3><?php echo esc_html__('With Pagination (cURL)', 'muchat-ai'); ?></h3>
                    <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;">curl -X POST \
  <?php echo esc_html(rest_url('muchat-api/v1/products')); ?> \
  -H 'Authorization: Bearer YOUR_API_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
  "skip": 30,
  "take": 30
}'</pre>

                    <h3><?php echo esc_html__('Filter by Modified Date (cURL)', 'muchat-ai'); ?></h3>
                    <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;">curl -X POST \
  <?php echo esc_html(rest_url('muchat-api/v1/products')); ?> \
  -H 'Authorization: Bearer YOUR_API_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
  "modified_after": "2023-01-01 00:00:00"
}'</pre>
                </div>

                <div class="tab-panel" id="php-tab" style="display: none; margin-top: 15px;">
                    <h3><?php echo esc_html__('Basic Request (PHP)', 'muchat-ai'); ?></h3>
                    <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;">$url = '<?php echo esc_html(rest_url('muchat-api/v1/products')); ?>';

$options = [
    'http' => [
        'header'  => "Content-type: application/json\r\n" .
                     "Authorization: Bearer YOUR_API_TOKEN\r\n",
        'method'  => 'POST'
    ]
];

$context  = stream_context_create($options);
$response = file_get_contents($url, false, $context);

if ($response === false) {
    // Handle error
} else {
    $products = json_decode($response, true);
    // Process the products
}</pre>

                    <h3><?php echo esc_html__('Using cURL in PHP', 'muchat-ai'); ?></h3>
                    <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;">$url = '<?php echo esc_html(rest_url('muchat-api/v1/products')); ?>';
$data = [
    'skip' => 0,
    'take' => 30
];

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data, JSON_UNESCAPED_UNICODE));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer YOUR_API_TOKEN'
]);

$response = curl_exec($ch);
curl_close($ch);

$products = json_decode($response, true);</pre>
                </div>

                <div class="tab-panel" id="js-tab" style="display: none; margin-top: 15px;">
                    <h3><?php echo esc_html__('Using Fetch API (JavaScript)', 'muchat-ai'); ?></h3>
                    <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;">fetch('<?php echo esc_html(rest_url('muchat-api/v1/products')); ?>', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer YOUR_API_TOKEN'
    },
    body: JSON.stringify({
        skip: 0,
        take: 30
    })
})
.then(response => response.json())
.then(data => {
    console.log(data);
    // Process the products
})
.catch(error => {
    console.error('Error:', error);
});</pre>

                    <h3><?php echo esc_html__('Using Axios (JavaScript)', 'muchat-ai'); ?></h3>
                    <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;">// Requires axios library
axios.post('<?php echo esc_html(rest_url('muchat-api/v1/products')); ?>', {
    in_stock: 'true',
    order_by: 'title',
    order: 'DESC'
}, {
    headers: {
        'Authorization': 'Bearer YOUR_API_TOKEN'
    }
})
.then(function (response) {
    const products = response.data;
    // Process the products
})
.catch(function (error) {
    console.error('Error:', error);
});</pre>
                </div>
            </div>
        </div>

        <!-- Product Meta Fields Section -->
        <div id="meta-fields" class="postbox">
            <div class="postbox-header">
                <h2 class="hndle"><?php echo esc_html__('Product Meta Fields', 'muchat-ai'); ?></h2>
            </div>
            <div class="inside">
                <p><?php echo esc_html__('Product meta fields are included in the response when selected in the Product Meta Fields section of the Settings page. Meta fields can contain simple text values or complex structured data.', 'muchat-ai'); ?></p>

                <h3><?php echo esc_html__('Example Response with Meta Fields', 'muchat-ai'); ?></h3>
                <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;">{
  "id": 123,
  "name": "Product Name",
  // other product fields...
  "meta_data": [
    {
      "Brand": "Apple",
      "Model": "iPhone 15",
      "Technical Specs": {
        "Processor": "A16 Bionic",
        "RAM": "8GB",
        "Storage": ["128GB", "256GB", "512GB"]
      }
    }
  ]
}</pre>

                <h3><?php echo esc_html__('Custom Meta Field Labels', 'muchat-ai'); ?></h3>
                <p><?php echo esc_html__('You can customize the display label of each meta field in the API response by setting a Custom Label in the Product Meta Fields section of the Settings page.', 'muchat-ai'); ?></p>
            </div>
        </div>

        <!-- Response Structure Section -->
        <div id="response-structure" class="postbox">
            <div class="postbox-header">
                <h2 class="hndle"><?php echo esc_html__('Response Structure', 'muchat-ai'); ?></h2>
            </div>
            <div class="inside">
                <h3><?php echo esc_html__('Pagination Response', 'muchat-ai'); ?></h3>
                <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;">{
  "offset": 0,        // Current offset (skip value)
  "limit": 30,        // Number of items per page (take value)
  "total_count": 150, // Total number of items
  "has_more": true,   // Whether there are more items after this page
  "items": [          // Array of items for this page
    { /* item 1 */ },
    { /* item 2 */ },
    // ...
  ]
}</pre>

                <h3><?php echo esc_html__('Single Product Response', 'muchat-ai'); ?></h3>
                <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;">{
  "id": 123,
  "name": "Product Name",
  "url": "https://example.com/product/product-name",
  "date_modified": "2023-01-15 14:30:45",
  "description": "Product description...",
  "short_description": "Short description...",
  "price": "199.99",
  "regular_price": "249.99",
  "sale_price": "199.99",
  "currency": "$",
  "stock_status": "instock",
  "categories": {
    "Category 1": "https://example.com/category/category-1",
    "Category 2": "https://example.com/category/category-2"
  },
  "tags": {
    "Tag 1": "https://example.com/tag/tag-1"
  },
  "images": [
    "https://example.com/wp-content/uploads/product-image-1.jpg",
    "https://example.com/wp-content/uploads/product-image-2.jpg"
  ],
  "attributes": {
    "Color": "Red",
    "Size": ["S", "M", "L"]
  },
  "variations": [
    {
      "title": "Product Name - Red, Small",
      "price": "199.99",
      "regular_price": "249.99",
      "sale_price": "199.99",
      "stock_status": "instock",
      "attributes": {
        "Color": "Red",
        "Size": "S"
      },
      "image": "https://example.com/wp-content/uploads/product-red-small.jpg"
    }
    // More variations...
  ],
  "meta_data": [
    {
      "Custom Field 1": "Value 1",
      "Custom Field 2": {
        "Nested Key 1": "Nested Value 1",
        "Nested Key 2": "Nested Value 2"
      }
    }
  ]
}</pre>
            </div>
        </div>

        <!-- Custom Post Types Section -->
        <div id="custom-post-types" class="postbox">
            <div class="postbox-header">
                <h2 class="hndle"><?php echo esc_html__('Custom Post Types', 'muchat-ai'); ?></h2>
            </div>
            <div class="inside">
                <p><?php echo esc_html__('The API supports retrieving data from custom post types. You can access any public custom post type using the following endpoint format:', 'muchat-ai'); ?></p>
                
                <div class="wp-clearfix" style="margin: 15px 0;">
                    <code style="display: inline-block; margin-right: 10px; padding: 8px; background: #f0f0f1; border-radius: 3px;"><?php echo esc_html(rest_url('muchat-api/v1/custom-post-types/{post_type}')); ?></code>
                    <button type="button" class="button copy-button" data-clipboard-text="<?php echo esc_url(rest_url('muchat-api/v1/custom-post-types/{post_type}')); ?>">
                        <?php echo esc_html__('Copy', 'muchat-ai'); ?>
                    </button>
                </div>

                <h3><?php echo esc_html__('How to Use', 'muchat-ai'); ?></h3>
                <p><?php echo esc_html__('Replace {post_type} with the name of your custom post type. For example, if you have a custom post type called "portfolio", you would use:', 'muchat-ai'); ?></p>
                <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;"><?php echo esc_html(rest_url('muchat-api/v1/custom-post-types/portfolio')); ?></pre>

                <h3><?php echo esc_html__('Example Request', 'muchat-ai'); ?></h3>
                <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;">curl -X GET \
  '<?php echo esc_html(rest_url('muchat-api/v1/custom-post-types/portfolio')); ?>?take=10&order_by=title&order=ASC'</pre>

                <h3><?php echo esc_html__('Example Response', 'muchat-ai'); ?></h3>
                <pre style="margin: 15px 0; padding: 15px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; line-height: 1.5;">{
  "plugin_version": "1.0.0",
  "post_type": "portfolio",
  "offset": 0,
  "limit": 30,
  "total_count": 50,
  "has_more": true,
  "items": [
    {
      "id": 123,
      "title": "Portfolio Item Title",
      "content": "Portfolio item content...",
      "excerpt": "Portfolio item excerpt...",
      "date_modified": "2023-01-15 14:30:45",
      "url": "https://example.com/portfolio/item-slug",
      "featured_image": "https://example.com/wp-content/uploads/image.jpg"
    }
    // More items...
  ]
}</pre>

                <h3><?php echo esc_html__('Supported Post Types', 'muchat-ai'); ?></h3>
                <p><?php echo esc_html__('The API automatically detects and supports all public custom post types registered in WordPress. The following post types are excluded (they have their own dedicated endpoints):', 'muchat-ai'); ?></p>
                <ul style="margin-left: 20px;">
                    <li><code>post</code> - <?php echo esc_html__('Use /posts endpoint', 'muchat-ai'); ?></li>
                    <li><code>page</code> - <?php echo esc_html__('Use /pages endpoint', 'muchat-ai'); ?></li>
                    <li><code>product</code> - <?php echo esc_html__('Use /products endpoint', 'muchat-ai'); ?></li>
                    <li><code>attachment</code>, <code>revision</code>, <code>nav_menu_item</code> - <?php echo esc_html__('Not supported', 'muchat-ai'); ?></li>
                </ul>

                <h3><?php echo esc_html__('Custom Fields and Taxonomies', 'muchat-ai'); ?></h3>
                <p><?php echo esc_html__('Custom post type items can include custom meta fields and taxonomy terms. These are automatically included in the response if they are configured in the plugin settings.', 'muchat-ai'); ?></p>

                <h3><?php echo esc_html__('Available Parameters', 'muchat-ai'); ?></h3>
                <p><?php echo esc_html__('The custom post types endpoint supports the same parameters as other endpoints:', 'muchat-ai'); ?></p>
                <ul style="margin-left: 20px;">
                    <li><code>skip</code> - <?php echo esc_html__('Number of items to skip (default: 0)', 'muchat-ai'); ?></li>
                    <li><code>take</code> - <?php echo esc_html__('Number of items to retrieve (default: 30, max: 100)', 'muchat-ai'); ?></li>
                    <li><code>order_by</code> - <?php echo esc_html__('Field to sort by (default: modified,ID)', 'muchat-ai'); ?></li>
                    <li><code>order</code> - <?php echo esc_html__('Sort order: ASC or DESC (default: ASC)', 'muchat-ai'); ?></li>
                    <li><code>modified_after</code> - <?php echo esc_html__('Filter items modified after this date (format: YYYY-MM-DD HH:MM:SS)', 'muchat-ai'); ?></li>
                </ul>
            </div>
        </div>

        <!-- Error Handling Section -->
        <div id="error-handling" class="postbox">
            <div class="postbox-header">
                <h2 class="hndle"><?php echo esc_html__('Error Handling', 'muchat-ai'); ?></h2>
            </div>
            <div class="inside">
                <p><?php echo esc_html__('The API returns standard HTTP status codes along with error messages in case of failure.', 'muchat-ai'); ?></p>

                <table class="wp-list-table widefat fixed striped">
                    <thead>
                        <tr>
                            <th scope="col" width="15%"><?php echo esc_html__('Status Code', 'muchat-ai'); ?></th>
                            <th scope="col" width="35%"><?php echo esc_html__('Description', 'muchat-ai'); ?></th>
                            <th scope="col" width="50%"><?php echo esc_html__('Example Response', 'muchat-ai'); ?></th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td><code>401</code></td>
                            <td><?php echo esc_html__('Unauthorized - Invalid or missing token', 'muchat-ai'); ?></td>
                            <td>
                                <pre style="margin: 5px 0; padding: 10px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; font-size: 12px;">{"code":"rest_forbidden","message":"Sorry, you are not allowed to do that."}</pre>
                            </td>
                        </tr>
                        <tr>
                            <td><code>404</code></td>
                            <td><?php echo esc_html__('Not Found - Resource does not exist', 'muchat-ai'); ?></td>
                            <td>
                                <pre style="margin: 5px 0; padding: 10px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; font-size: 12px;">{"code":"rest_no_route","message":"No route was found matching the URL and request method."}</pre>
                            </td>
                        </tr>
                        <tr>
                            <td><code>400</code></td>
                            <td><?php echo esc_html__('Bad Request - Invalid parameters', 'muchat-ai'); ?></td>
                            <td>
                                <pre style="margin: 5px 0; padding: 10px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; font-size: 12px;">{"code":"rest_invalid_param","message":"Invalid parameter(s): per_page"}</pre>
                            </td>
                        </tr>
                        <tr>
                            <td><code>500</code></td>
                            <td><?php echo esc_html__('Server Error - Something went wrong on the server', 'muchat-ai'); ?></td>
                            <td>
                                <pre style="margin: 5px 0; padding: 10px; background: #f0f0f1; border: 1px solid #dcdcde; border-radius: 3px; overflow-x: auto; font-family: Consolas, Monaco, monospace; font-size: 12px;">{"code":"internal_server_error","message":"Internal server error."}</pre>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>

        <!-- Available API Fields Section -->
        <div id="available-fields" class="postbox">
            <div class="postbox-header">
                <h2 class="hndle"><?php echo esc_html__('Available API Fields', 'muchat-ai'); ?></h2>
            </div>
            <div class="inside">
                <p><?php echo esc_html__('The following sections show all available fields that can be included in API responses. Select which fields you want to enable.', 'muchat-ai'); ?></p>

                <form method="post" action="options.php">
                    <?php
                    settings_fields('muchat_ai_chatbot_plugin_settings');
                    ?>

                    <!-- Product Fields Section -->
                    <div class="muchat-api-section">
                        <h3><?php echo esc_html__('Product Fields', 'muchat-ai'); ?></h3>
                        <p class="description">
                            <?php echo esc_html__('Select which fields you want to include in the products API response.', 'muchat-ai'); ?>
                        </p>
                        <table class="wp-list-table widefat fixed striped">
                            <thead>
                                <tr>
                                    <th scope="col" width="10%"><?php echo esc_html__('Enable', 'muchat-ai'); ?></th>
                                    <th scope="col" width="20%"><?php echo esc_html__('Field', 'muchat-ai'); ?></th>
                                    <th scope="col" width="70%"><?php echo esc_html__('Description', 'muchat-ai'); ?></th>
                                </tr>
                            </thead>
                            <tbody>
                                <?php
                                $available_fields = (new \Muchat\Api\Admin\Settings())->get_available_fields();
                                $options = get_option('muchat_ai_chatbot_plugin_options', []);

                                foreach ($available_fields['product'] as $field => $label):
                                    $is_enabled = in_array($field, isset($options['product_fields']) ? $options['product_fields'] : []);
                                ?>
                                    <tr>
                                        <td>
                                            <input type="checkbox"
                                                name="muchat_ai_chatbot_plugin_options[product_fields][]"
                                                value="<?php echo esc_attr($field); ?>"
                                                <?php checked($is_enabled); ?>>
                                        </td>
                                        <td>
                                            <code><?php echo esc_html($field); ?></code>
                                            <?php if ($is_enabled): ?>
                                                <span class="badge" style="background-color: #00a32a; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; margin-left: 5px;"><?php echo esc_html__('Enabled', 'muchat-ai'); ?></span>
                                            <?php else: ?>
                                                <span class="badge" style="background-color: #ccc; color: #333; padding: 2px 6px; border-radius: 3px; font-size: 10px; margin-left: 5px;"><?php echo esc_html__('Disabled', 'muchat-ai'); ?></span>
                                            <?php endif; ?>
                                        </td>
                                        <td><?php echo esc_html($label); ?></td>
                                    </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>

                    <!-- Post Fields Section -->
                    <div class="muchat-api-section" style="margin-top: 30px;">
                        <h3><?php echo esc_html__('Post Fields', 'muchat-ai'); ?></h3>
                        <p class="description">
                            <?php echo esc_html__('Select which fields you want to include in the posts API response.', 'muchat-ai'); ?>
                        </p>
                        <table class="wp-list-table widefat fixed striped">
                            <thead>
                                <tr>
                                    <th scope="col" width="10%"><?php echo esc_html__('Enable', 'muchat-ai'); ?></th>
                                    <th scope="col" width="20%"><?php echo esc_html__('Field', 'muchat-ai'); ?></th>
                                    <th scope="col" width="70%"><?php echo esc_html__('Description', 'muchat-ai'); ?></th>
                                </tr>
                            </thead>
                            <tbody>
                                <?php
                                foreach ($available_fields['post'] as $field => $label):
                                    $is_enabled = in_array($field, isset($options['post_fields']) ? $options['post_fields'] : []);
                                ?>
                                    <tr>
                                        <td>
                                            <input type="checkbox"
                                                name="muchat_ai_chatbot_plugin_options[post_fields][]"
                                                value="<?php echo esc_attr($field); ?>"
                                                <?php checked($is_enabled); ?>>
                                        </td>
                                        <td>
                                            <code><?php echo esc_html($field); ?></code>
                                            <?php if ($is_enabled): ?>
                                                <span class="badge" style="background-color: #00a32a; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; margin-left: 5px;"><?php echo esc_html__('Enabled', 'muchat-ai'); ?></span>
                                            <?php else: ?>
                                                <span class="badge" style="background-color: #ccc; color: #333; padding: 2px 6px; border-radius: 3px; font-size: 10px; margin-left: 5px;"><?php echo esc_html__('Disabled', 'muchat-ai'); ?></span>
                                            <?php endif; ?>
                                        </td>
                                        <td><?php echo esc_html($label); ?></td>
                                    </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>

                    <!-- Page Fields Section -->
                    <div class="muchat-api-section" style="margin-top: 30px;">
                        <h3><?php echo esc_html__('Page Fields', 'muchat-ai'); ?></h3>
                        <p class="description">
                            <?php echo esc_html__('Select which fields you want to include in the pages API response.', 'muchat-ai'); ?>
                        </p>
                        <table class="wp-list-table widefat fixed striped">
                            <thead>
                                <tr>
                                    <th scope="col" width="10%"><?php echo esc_html__('Enable', 'muchat-ai'); ?></th>
                                    <th scope="col" width="20%"><?php echo esc_html__('Field', 'muchat-ai'); ?></th>
                                    <th scope="col" width="70%"><?php echo esc_html__('Description', 'muchat-ai'); ?></th>
                                </tr>
                            </thead>
                            <tbody>
                                <?php
                                foreach ($available_fields['page'] as $field => $label):
                                    $is_enabled = in_array($field, isset($options['page_fields']) ? $options['page_fields'] : []);
                                ?>
                                    <tr>
                                        <td>
                                            <input type="checkbox"
                                                name="muchat_ai_chatbot_plugin_options[page_fields][]"
                                                value="<?php echo esc_attr($field); ?>"
                                                <?php checked($is_enabled); ?>>
                                        </td>
                                        <td>
                                            <code><?php echo esc_html($field); ?></code>
                                            <?php if ($is_enabled): ?>
                                                <span class="badge" style="background-color: #00a32a; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; margin-left: 5px;"><?php echo esc_html__('Enabled', 'muchat-ai'); ?></span>
                                            <?php else: ?>
                                                <span class="badge" style="background-color: #ccc; color: #333; padding: 2px 6px; border-radius: 3px; font-size: 10px; margin-left: 5px;"><?php echo esc_html__('Disabled', 'muchat-ai'); ?></span>
                                            <?php endif; ?>
                                        </td>
                                        <td><?php echo esc_html($label); ?></td>
                                    </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>

                    <!-- Submit Button -->
                    <div style="margin-top: 20px;">
                        <?php submit_button(__('Save Field Settings', 'muchat-ai')); ?>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>
</file>

<file path="includes/Frontend/Widget.php">
<?php

namespace Muchat\Api\Frontend;

class Widget
{
    /**
     * Script position setting
     *
     * @var string
     */
    private $script_position;

    /**
     * WordPress hook used for script injection
     *
     * @var string
     */
    private $script_hook;

    /**
     * Priority for the hook
     *
     * @var int
     */
    private $script_priority;

    /**
     * Initialize the class
     */
    public function __construct()
    {
        // Get script position setting with 'footer' as default for better performance
        $position = get_option('muchat_ai_chatbot_script_position', 'footer');
        
        // Validate the position value
        $valid_positions = ['head', 'footer', 'body_open'];
        if (!in_array($position, $valid_positions, true)) {
            $position = 'footer';
        }
        
        $hook = 'wp_footer'; // Default hook
        $priority = 100; // Default priority - load after most scripts

        if ($position === 'head') {
            $hook = 'wp_head';
            $priority = 100; // Still use late priority for better compatibility
        } elseif ($position === 'footer') {
            $hook = 'wp_footer';
            $priority = 100; // Load after most footer scripts
        } elseif ($position === 'body_open') {
            // wp_body_open was introduced in WP 5.2.
            $hook = 'wp_body_open';
            $priority = 20; // Use earlier priority for body_open
        }
        
        add_action($hook, [$this, 'render_widget'], $priority);
        $this->register_cache_clearing_hooks();
        
        // Store for potential debugging
        $this->script_position = $position;
        $this->script_hook = $hook;
        $this->script_priority = $priority;
    }

    /**
     * Register hooks for clearing widget cache when settings change
     */
    private function register_cache_clearing_hooks()
    {
        // Core widget settings
        add_action('update_option_muchat_ai_chatbot_agent_id', [$this, 'clear_widget_cache']);
        add_action('update_option_muchat_ai_chatbot_interface_initial_messages', [$this, 'clear_widget_cache']);
        add_action('update_option_muchat_ai_chatbot_use_logged_in_user_info', [$this, 'clear_widget_cache']);
        add_action('update_option_muchat_ai_chatbot_load_strategy', [$this, 'clear_widget_cache']);
        add_action('update_option_muchat_ai_chatbot_script_position', [$this, 'clear_widget_cache']);
        add_action('update_option_muchat_ai_chatbot_widget_enabled', [$this, 'clear_widget_cache']);
        
        // Display Rules settings - IMPORTANT for visibility to work correctly!
        add_action('update_option_muchat_ai_chatbot_visibility_mode', [$this, 'clear_all_caches']);
        add_action('update_option_muchat_ai_chatbot_visibility_pages', [$this, 'clear_all_caches']);
        
        // Schedule settings
        add_action('update_option_muchat_ai_chatbot_schedule_enabled', [$this, 'clear_all_caches']);
        add_action('update_option_muchat_ai_chatbot_schedule_days', [$this, 'clear_all_caches']);
        add_action('update_option_muchat_ai_chatbot_schedule_start_time', [$this, 'clear_all_caches']);
        add_action('update_option_muchat_ai_chatbot_schedule_end_time', [$this, 'clear_all_caches']);
    }

    /**
     * Clear widget cache
     */
    public function clear_widget_cache()
    {
        \Muchat\Api\Utils\Cache::clear_widget_cache();
    }

    /**
     * Clear internal caches when display rules or schedule settings change.
     * Note: We only clear our own internal caches, not third-party cache plugins.
     * Users should manually clear their site cache if display rules don't work correctly.
     */
    public function clear_all_caches()
    {
        \Muchat\Api\Utils\Cache::clear_widget_cache();
    }

    /**
     * Render the widget on the frontend
     */
    public function render_widget()
    {
        // Add debug comment for admins
        if (current_user_can('manage_options')) {
            echo "\n<!-- Muchat Widget | Position: " . esc_html($this->script_position ?? 'unknown') . 
                 " | Hook: " . esc_html($this->script_hook ?? 'unknown') . 
                 " | Priority: " . esc_html($this->script_priority ?? 'unknown') . " -->\n";
        }
        
        // Check for agent ID
        $agent_id = get_option('muchat_ai_chatbot_agent_id');

        // Debug mode check - SECURITY: Fixed logic bug where second line was overwriting first
        $debug = false;
        if (isset($_GET['muchat_ai_chatbot_debug']) && function_exists('current_user_can') && current_user_can('manage_options')) {
            $debug = true;
        } elseif (isset($_GET['muchat_ai_chatbot_nonce']) && wp_verify_nonce(sanitize_key($_GET['muchat_ai_chatbot_nonce']), 'muchat_ai_chatbot_debug')) {
            $debug = true;
        }

        if ($debug) {
            echo "<!-- Muchat Debug: Starting hook_head function -->\n";
            echo "<!-- Muchat Debug: Agent ID: " . ($agent_id ? esc_html($agent_id) : 'NOT SET') . " -->\n";
            echo "<!-- Muchat Debug: Load Strategy: " . esc_html(get_option('muchat_ai_chatbot_load_strategy', 'FAST')) . " -->\n";
            echo "<!-- Muchat Debug: Use logged in user info: " . esc_html(get_option('muchat_ai_chatbot_use_logged_in_user_info', '')) . " -->\n";
        }

        // Exit if no agent ID
        if (empty($agent_id)) {
            if ($debug) echo "<!-- Muchat Debug: Agent ID is empty, widget will not be displayed -->\n";
            return;
        }

        // Check visibility rules
        if (!$this->should_display()) {
            if ($debug) echo "<!-- Muchat Debug: Page doesn't match visibility rules, widget will not be displayed -->\n";
            return;
        }

        // Generate config for the chatbot
        $config = $this->generate_widget_config();

        if ($debug) {
            echo "<!-- Muchat Debug: Generated Config: " . esc_html(json_encode($config, JSON_UNESCAPED_UNICODE)) . " -->\n";
        }

        // Output the widget script directly to avoid wp_kses_post stripping it
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Script tag must be output directly as wp_kses would strip required JS
        echo $this->generate_widget_script($config);
    }

    /**
     * Determines if the chatbot should be displayed on the current page
     * based on visibility settings. This is the primary logic gate.
     */
    private function should_display()
    {
        // 1. Check if widget is globally enabled
        if (get_option('muchat_ai_chatbot_widget_enabled', '1') !== '1') {
            return false;
        }

        // 2. Check scheduling if enabled
        if (get_option('muchat_ai_chatbot_schedule_enabled', '0') === '1') {
            if (!$this->is_within_schedule()) {
                return false;
            }
        }

        // 3. Check page visibility rules
        $visibility_mode = get_option('muchat_ai_chatbot_visibility_mode', 'all');
        $visibility_pages = get_option('muchat_ai_chatbot_visibility_pages', '');

        // Rule: 'Disabled Everywhere'
        if ($visibility_mode === 'none') {
            return false;
        }

        // Rule: 'Show on all pages' (and no exceptions are listed)
        if ($visibility_mode === 'all' && empty(trim($visibility_pages))) {
            return true;
        }

        // Rule: 'Only show on listed pages' (and the list is empty)
        if ($visibility_mode === 'specific' && empty(trim($visibility_pages))) {
            return false;
        }

        // The logic now depends on matching the current page against the list.
        $matches = $this->is_path_match($visibility_pages);

        // Rule: 'Show on all pages EXCEPT those listed'
        if ($visibility_mode === 'all') {
            return !$matches;
        }

        // Rule: 'Only show on pages listed'
        if ($visibility_mode === 'specific') {
            return $matches;
        }

        // Default fallback, should ideally not be reached.
        return false;
    }

    /**
     * Checks if the current request path matches any of the given patterns.
     * Handles exact, wildcard, UTF-8 (Persian, Arabic, etc.), percent-encoded, and <front> patterns.
     *
     * @param string $patterns_string A newline-separated string of URL patterns.
     * @return bool True if the path matches one of the patterns, false otherwise.
     */
    private function is_path_match($patterns_string)
    {
        // 1. Get and normalize the current page's path.
        // We get the raw URI, strip any query parameters, and then decode it.
        $request_uri = isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : '';
        $path_only = strtok($request_uri, '?');
        $current_path = $this->normalize_path(urldecode($path_only));

        // Treat an empty path (which can be the homepage) as '/'.
        if (empty($current_path)) {
            $current_path = '/';
        }

        // 2. Use WordPress's reliable function to determine if it's the front page.
        $is_front = is_front_page();

        // 3. Handle the global wildcard match immediately.
        if (trim($patterns_string) === '*') {
            return true;
        }

        // 4. Process the list of patterns.
        $patterns = explode("\n", $patterns_string);

        foreach ($patterns as $pattern) {
            $pattern = trim($pattern);
            if (empty($pattern)) {
                continue;
            }

            // Also decode the user-provided pattern in case it's percent-encoded.
            $decoded_pattern = urldecode($pattern);

            // A. Check for the special '<front>' tag.
            if (strtolower($decoded_pattern) === '<front>') {
                if ($is_front) {
                    return true; // Match found.
                }
                continue; // Not the front page, so check next pattern.
            }

            // B. Normalize the pattern for comparison.
            $normalized_pattern = $this->normalize_path($decoded_pattern);
            if (empty($normalized_pattern)) {
                $normalized_pattern = '/';
            }

            // C. Check for wildcard matches.
            if (strpos($normalized_pattern, '*') !== false) {
                // SECURITY: ReDoS Protection
                // Limit pattern complexity to prevent Regular Expression Denial of Service
                // - Max pattern length: 500 chars (reasonable for URL patterns)
                // - Max wildcards: 5 (prevents exponential backtracking)
                if (strlen($normalized_pattern) > 500) {
                    continue; // Skip overly long patterns
                }
                
                $wildcard_count = substr_count($normalized_pattern, '*');
                if ($wildcard_count > 5) {
                    continue; // Skip patterns with too many wildcards
                }
                
                // Escape regex characters, then replace our wildcard `*` with `.*`.
                // Using non-greedy `.*?` to reduce backtracking
                // The 'u' modifier is crucial for correct UTF-8 pattern matching.
                // The 'i' modifier makes it case-insensitive for Latin characters.
                $regex = '@^' . str_replace('\*', '.*?', preg_quote($normalized_pattern, '@')) . '$@ui';
                
                // Set a reasonable time limit for regex execution (PHP 7.3+)
                if (preg_match($regex, $current_path)) {
                    return true; // Match found.
                }
            }
            // D. Check for exact matches (only if no wildcard).
            else {
                // Case-insensitive comparison for Latin characters, exact for non-Latin (Persian, etc.)
                if ($this->paths_match($normalized_pattern, $current_path)) {
                    return true; // Match found.
                }
                // Also check if the pattern is for the homepage and it is the homepage
                if ($normalized_pattern === '/' && $is_front) {
                    return true;
                }
            }
        }

        // No patterns matched the current path.
        return false;
    }

    /**
     * Normalize a URL path for consistent comparison.
     * Handles trailing slashes, Unicode normalization (NFC), and encoding issues.
     *
     * @param string|null $path The path to normalize.
     * @return string The normalized path.
     */
    private function normalize_path($path)
    {
        // PHP 8.1+ COMPAT: Ensure path is always a string
        if ($path === null) {
            return '';
        }
        $path = (string) $path;
        
        // Remove trailing slash (but keep leading slash)
        $path = rtrim($path, '/');
        
        // Ensure path starts with /
        if (!empty($path) && $path[0] !== '/') {
            $path = '/' . $path;
        }
        
        // Normalize Unicode characters to NFC form (Canonical Composition)
        // This ensures that characters like Persian/Arabic are consistently represented.
        // For example: "ک" (Arabic Kaf U+0643) vs "ک" (Farsi Keh U+06A9) 
        // or combining characters like "ی" + diacritic vs precomposed form
        if (function_exists('normalizer_normalize')) {
            $normalized = normalizer_normalize($path, \Normalizer::FORM_C);
            if ($normalized !== false) {
                $path = $normalized;
            }
        }
        
        // Handle double URL encoding that some servers/browsers might cause
        // For example: %25D9%2585 (double-encoded Persian) -> %D9%85 -> م
        $prev_path = '';
        $max_iterations = 3; // Prevent infinite loops
        $iteration = 0;
        while ($prev_path !== $path && $iteration < $max_iterations) {
            $prev_path = $path;
            $decoded = urldecode($path);
            // Only update if decoding actually changed something and result is valid UTF-8
            if ($decoded !== $path && mb_check_encoding($decoded, 'UTF-8')) {
                $path = $decoded;
            } else {
                break;
            }
            $iteration++;
        }
        
        return $path;
    }

    /**
     * Compare two paths for equality.
     * Case-insensitive for ASCII/Latin characters, exact match for non-Latin (Persian, Arabic, etc.)
     *
     * @param string $pattern The pattern path.
     * @param string $path The current path.
     * @return bool True if paths match.
     */
    private function paths_match($pattern, $path)
    {
        // First try exact match (fastest)
        if ($pattern === $path) {
            return true;
        }
        
        // Try case-insensitive match for paths with ASCII characters
        // This handles /About vs /about for English URLs
        // mb_strtolower with UTF-8 encoding preserves non-Latin characters correctly
        $pattern_lower = mb_strtolower($pattern, 'UTF-8');
        $path_lower = mb_strtolower($path, 'UTF-8');
        
        if ($pattern_lower === $path_lower) {
            return true;
        }
        
        return false;
    }

    /**
     * Check if current time is within scheduled hours
     */
    private function is_within_schedule()
    {
        $current_day = strtolower(current_time('l')); // Get current day in lowercase
        $current_time = current_time('H:i'); // Get current time in 24-hour format

        $schedule_days = get_option('muchat_ai_chatbot_schedule_days', array());
        if (!is_array($schedule_days)) {
            $schedule_days = array();
        }

        $start_time = get_option('muchat_ai_chatbot_schedule_start_time', '');
        $end_time = get_option('muchat_ai_chatbot_schedule_end_time', '');

        // If no days are selected, consider all days as active
        $is_day_active = empty($schedule_days) || in_array($current_day, $schedule_days);

        // If no time is set, consider all times as active
        $is_time_active = true;
        if (!empty($start_time) && !empty($end_time)) {
            $is_time_active = ($current_time >= $start_time && $current_time <= $end_time);
        }

        return $is_day_active && $is_time_active;
    }

    /**
     * Generate widget configuration
     */
    private function generate_widget_config()
    {
        $config = [];

        // Basic config
        $config['agentId'] = sanitize_text_field(get_option('muchat_ai_chatbot_agent_id'));
        $config['loadStrategy'] = sanitize_text_field(get_option('muchat_ai_chatbot_load_strategy', 'FAST'));

        // Contact information
        $use_logged_in_user_info = get_option('muchat_ai_chatbot_use_logged_in_user_info', '') === '1';
        if ($use_logged_in_user_info && is_user_logged_in()) {
            $current_user = wp_get_current_user();
            $mobile = get_user_meta($current_user->ID, 'billing_phone', true)
                ?: get_user_meta($current_user->ID, 'digits_phone', true)
                ?: get_user_meta($current_user->ID, 'phone', true);

            $config['contact'] = array_filter([
                'firstName' => $current_user->user_firstname,
                'lastName' => $current_user->user_lastname,
                'email' => $current_user->user_email,
                'phoneNumber' => $mobile,
                'userId' => $current_user->ID,
            ]);
        }
        
        // Context with variable replacement
        $context = '';
        $enable_guest_context = get_option('muchat_ai_chatbot_enable_guest_context', '0');
        $is_guest = !is_user_logged_in();

        if ($is_guest && '1' === $enable_guest_context) {
            $context = get_option('muchat_ai_chatbot_guest_context', '');
        } else if (!$is_guest) {
            // Only load the main context for logged-in users
            $context = get_option('muchat_ai_chatbot_context', '');
        }
        
        if (!empty($context)) {
            $config['context'] = $this->process_context($context);
        }

        // Interface settings
        $initial_messages = $this->get_initial_messages();
        if (!empty($initial_messages)) {
            // Process variables in each message
            $processed_messages = array_map([$this, 'replace_chat_variables'], $initial_messages);

            $config['interface'] = [
                'initialMessages' => $processed_messages,
            ];
        }

        // Allow for filter modifications
        return apply_filters('muchat_widget_config', $config);
    }

    /**
     * Process context with variable replacement
     * 
     * @param string|null $context The context string to process
     * @return string Processed context
     */
    private function process_context($context)
    {
        // PHP 8.1+ COMPAT: Ensure context is always a string
        if ($context === null || $context === '') {
            return '';
        }
        $context = (string) $context;
        
        // First, do the standard variable replacements.
        $context = $this->replace_chat_variables($context);

        // The rest of this function handles the more complex, legacy format:
        // "The user you are talking to is $name $lastname (Email: $email, Phone: $phone)..."
        // We need to rebuild this carefully to avoid empty parentheses or extra commas.

        // We can reuse the same user data logic from the variable replacer
        $user_data = $this->get_user_data();
        $firstName = $user_data['firstName'];
        $lastName = $user_data['lastName'];
        $email = $user_data['email'];
        $phone = $user_data['phone'];
        $page_title = $user_data['page_title'];

        // --- Rebuild the complex sentence structure ---
        // This logic is specific to the old format and should only run if it's detected.
        if (preg_match('/The user you are talking to is/i', $context)) {
            $namePart = trim($firstName . ' ' . $lastName);

            $contactParts = [];
            if (!empty($email)) {
                $contactParts[] = "Email: $email";
            }
            if (!empty($phone)) {
                $contactParts[] = "Phone: $phone";
            }

            $contactInfo = '';
            if (!empty($contactParts)) {
                $contactInfo = ' (' . implode(', ', $contactParts) . ')';
            }
            
            // Reconstruct the sentence, ensuring no empty parts are included.
            $final_context = "The user you are talking to is {$namePart}{$contactInfo}.";
            $final_context .= " They are currently on the page: {$page_title}.";
            $final_context .= " Start by greeting them by name.";
            
            // Clean up any double spaces that might have occurred.
            return preg_replace('/\s+/', ' ', $final_context);
        }

        // If the special sentence isn't used, return the context with simple variables replaced.
        return $context;
    }

    /**
     * Replace variables in a string with user data.
     *
     * @param string|null $text The text containing variables like $name, $lastname.
     * @return string The text with variables replaced.
     */
    private function replace_chat_variables($text)
    {
        // PHP 8.1+ COMPAT: Ensure text is always a string
        if ($text === null || $text === '') {
            return '';
        }
        $text = (string) $text;
        
        $user_data = $this->get_user_data();

        $replacements = [
            '$name' => $user_data['firstName'],
            '$lastname' => $user_data['lastName'],
            '$email' => $user_data['email'],
            '$phone' => $user_data['phone'],
            '$page_title' => $user_data['page_title'],
        ];

        return str_replace(array_keys($replacements), array_values($replacements), $text);
    }

    /**
     * Get user data for logged-in users with XSS sanitization
     * 
     * SECURITY: All user-provided data is sanitized to prevent XSS attacks
     * when injected into JavaScript context.
     *
     * @return array Sanitized user data safe for JavaScript injection
     */
    private function get_user_data()
    {
        $data = [
            'firstName' => '',
            'lastName' => '',
            'email' => '',
            'phone' => '',
            'page_title' => ''
        ];

        // SECURITY: Sanitize page title - remove any HTML/script content
        $page_title = wp_title('', false);
        $data['page_title'] = $this->sanitize_for_js_context($page_title);

        if (is_user_logged_in()) {
            $current_user = wp_get_current_user();

            // SECURITY: Sanitize all user-provided data to prevent XSS
            $data['firstName'] = $this->sanitize_for_js_context($current_user->user_firstname);
            $data['lastName'] = $this->sanitize_for_js_context($current_user->user_lastname);
            $data['email'] = $this->sanitize_for_js_context($current_user->user_email);

            $phone = get_user_meta($current_user->ID, 'billing_phone', true)
                ?: get_user_meta($current_user->ID, 'digits_phone', true)
                ?: get_user_meta($current_user->ID, 'phone', true)
                ?: '';
            $data['phone'] = $this->sanitize_for_js_context($phone);
        }

        return $data;
    }

    /**
     * Sanitize a string for safe use in JavaScript context
     * 
     * SECURITY: This provides defense-in-depth beyond JSON encoding
     * - Decodes HTML entities that might hide malicious content
     * - Strips all HTML tags
     * - Removes dangerous characters for JS context (more aggressive)
     * - Limits string length to prevent DoS
     * - Encodes problematic unicode characters
     * 
     * @param string $value The value to sanitize
     * @return string Sanitized value
     */
    private function sanitize_for_js_context($value)
    {
        if (empty($value) || !is_string($value)) {
            return '';
        }

        // Step 1: Decode any HTML entities that might hide malicious content
        // Do this FIRST so we can properly sanitize the decoded content
        $value = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');

        // Step 2: Strip all HTML tags
        $value = wp_strip_all_tags($value);

        // Step 3: Remove dangerous characters for JS context
        // Removes anything that could break JS strings or enable script injection.
        // Single quote (') is intentionally excluded to preserve names like O'Brien.
        $value = preg_replace('/[<>"`\\\\\x00-\x1f\x7f]/u', '', $value);

        // Step 4: Limit length to prevent DoS
        $value = mb_substr($value, 0, 255, 'UTF-8');

        // Step 5: Additional WordPress sanitization
        $value = sanitize_text_field($value);

        // Step 6: Encode problematic unicode characters that might cause issues
        // This handles edge cases where unicode could be used to bypass filters
        $value = preg_replace_callback('/[\x{80}-\x{FFFF}]/u', function ($match) {
            $ord = mb_ord($match[0], 'UTF-8');
            // Only encode potentially problematic characters
            // Keep common accented letters and CJK for usability
            if ($ord !== false && ($ord < 0x100 || $ord > 0xFFFF)) {
                return sprintf('\\u%04x', $ord);
            }
            return $match[0];
        }, $value);

        return $value;
    }

    /**
     * Get initial messages based on user login status.
     * It first checks for specific guest messages if applicable.
     * If not, it falls back to the default initial messages.
     * Returns an array of messages, or an empty array if none are configured.
     *
     * @return array
     */
    private function get_initial_messages()
    {
        // A helper function to robustly parse the message string, which can be
        // JSON, a legacy comma-separated string, or empty.
        $parse_message_string = function ($json_string) {
            // If the input is empty or just an empty JSON array, return empty.
            if (empty($json_string) || trim($json_string) === '[]') {
                return [];
            }

            // First, try to decode as JSON. This is the modern format.
            try {
                $decoded = json_decode($json_string, true, 512, JSON_THROW_ON_ERROR);
                // If it decodes to a valid array, return it.
                if (is_array($decoded)) {
                    return $decoded;
                }
            } catch (\Exception $e) {
                // The string is not valid JSON. We assume it's the legacy format.
            }

            // Fallback to parsing the legacy comma-separated string format.
            $messages = array_map('trim', explode(',', $json_string));

            // Filter out any resultant empty strings from the list.
            return array_filter($messages);
        };

        // --- Step 1: Check for Guest-specific Messages ---
        $enable_guest_messages = get_option('muchat_ai_chatbot_enable_guest_messages', '0');
        $use_guest_logic = ('1' === $enable_guest_messages && !is_user_logged_in());
        
        if ($use_guest_logic) {
            $guest_messages_json = get_option('muchat_ai_chatbot_guest_initial_messages', '');
            // If guest messages are enabled but empty, we don't fall back to default.
            // We just return the (empty) result for the guest-specific setting.
             return $parse_message_string($guest_messages_json);
        }

        // --- Step 2: Fallback to Default Messages ---
        // This runs if guest messages are not enabled or the user is logged in.
        $initial_messages_json = get_option('muchat_ai_chatbot_interface_initial_messages', '');
        return $parse_message_string($initial_messages_json);
    }

    /**
     * Generate the script tag for the widget
     * 
     * SECURITY: Uses wp_json_encode with security flags to prevent XSS:
     * - JSON_HEX_TAG: Converts < and > to \u003C and \u003E
     * - JSON_HEX_AMP: Converts & to \u0026  
     * - JSON_HEX_APOS: Converts ' to \u0027
     * - JSON_HEX_QUOT: Converts " to \u0022
     * - JSON_UNESCAPED_UNICODE: Keeps Unicode characters readable
     */
    private function generate_widget_script($config)
    {
        // SECURITY: Sanitize all config values recursively before encoding
        $config = $this->sanitize_config_recursive($config);
        
        $json_config = [];

        // SECURITY FLAGS: Prevent XSS by encoding special HTML characters
        $json_flags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE;

        // Convert PHP array to JS object notation with secure encoding
        foreach ($config as $key => $value) {
            // Sanitize key to only allow alphanumeric and underscore
            $safe_key = preg_replace('/[^a-zA-Z0-9_]/', '', $key);
            
            // Use wp_json_encode with security flags
            $encoded_value = wp_json_encode($value, $json_flags);
            
            // Skip if encoding failed (returns false on failure)
            if ($encoded_value === false) {
                continue;
            }
            
            $json_config[] = $safe_key . ": " . $encoded_value;
        }

        $config_string = implode(",\n        ", $json_config);

        // SECURITY: Escape plugin version for use in URL
        $safe_version = rawurlencode(MUCHAT_AI_CHATBOT_PLUGIN_VERSION);

        // Build the script content
        // MAINTAINABILITY: Using centralized constant for CDN URL
        $cdn_url = defined('MUCHAT_CDN_URL') ? MUCHAT_CDN_URL : 'https://cdn.mu.chat';
        $script_content = "import Chatbox from '" . esc_url($cdn_url) . "/embeds/dist/chatbox/index.js?v=" . $safe_version . "';\n";
        $script_content .= "const widget = await Chatbox.initBubble({\n";
        $script_content .= "    " . $config_string . "\n";
        $script_content .= "});";

        /**
         * SECURITY: The muchat_widget_output filter has been completely removed.
         * 
         * This filter was deprecated in 2.1.0 and is now removed for security reasons.
         * Allowing external code to modify raw script output poses XSS risks.
         * 
         * If you need to customize the widget, use the 'muchat_widget_config' filter instead,
         * which only allows safe configuration changes.
         * 
         * @since 2.1.0 Deprecated with warning
         * @since 2.2.0 Completely removed - filter is no longer applied
         */
        if (has_filter('muchat_widget_output')) {
            _doing_it_wrong(
                'muchat_widget_output',
                sprintf(
                    /* translators: %s: Alternative filter name */
                    esc_html__('The muchat_widget_output filter has been removed for security reasons. Use %s instead for configuration changes.', 'muchat-ai'),
                    "'muchat_widget_config'"
                ),
                '2.2.0'
            );
            
            // Log the usage for security audit - but do NOT execute the filter
            if (defined('WP_DEBUG') && WP_DEBUG) {
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
                error_log('Muchat Security: muchat_widget_output filter detected but NOT executed. This filter is removed. Use muchat_widget_config instead.');
            }
            
            // IMPORTANT: We intentionally do NOT apply the filter anymore
            // The old code would have called: apply_filters('muchat_widget_output', $script_content)
            // This is now blocked for security
        }
        
        // SECURITY: Use WordPress's built-in function for safe inline script output
        // wp_get_inline_script_tag handles proper escaping and is available since WP 5.7
        if (function_exists('wp_get_inline_script_tag')) {
            return wp_get_inline_script_tag($script_content, ['type' => 'module']);
        }
        
        // Fallback for WP < 5.7 - manually build script tag with proper escaping
        // Use CDATA comment to prevent any HTML parsing issues
        return "\n<script type='module'>\n" . $script_content . "\n</script>\n";
    }
    
    /**
     * Validate widget output from filters to prevent XSS injection
     * 
     * SECURITY: This method provides defense-in-depth against malicious
     * themes or plugins that might try to inject scripts via the filter.
     *
     * @param string $filtered_output The output after filter processing
     * @param string $original_output The original unfiltered output
     * @return bool True if output is safe, false otherwise
     */
    private function validate_widget_output($filtered_output, $original_output)
    {
        // If filter didn't modify anything, it's safe
        if ($filtered_output === $original_output) {
            return true;
        }
        
        // If filter returned empty or non-string, reject
        if (!is_string($filtered_output) || empty($filtered_output)) {
            return false;
        }
        
        // Check for malicious patterns that shouldn't appear in legitimate modifications
        $dangerous_patterns = [
            '/<script[^>]*>[^<]*(?!Chatbox\.initBubble)[^<]*<\/script>/is', // Additional script tags
            '/javascript\s*:/i',           // javascript: protocol
            '/on\w+\s*=/i',                 // Event handlers (onclick, onerror, etc.)
            '/data\s*:/i',                  // data: protocol
            '/vbscript\s*:/i',              // vbscript: protocol
            '/<iframe/i',                   // iframes
            '/<object/i',                   // objects
            '/<embed/i',                    // embeds
            '/<form/i',                     // forms
            '/document\.(cookie|domain|write)/i', // Document manipulation
            '/window\.(location|open)/i',  // Window manipulation
            '/eval\s*\(/i',                // eval()
            '/Function\s*\(/i',            // Function constructor
            '/setTimeout\s*\(/i',          // setTimeout with string
            '/setInterval\s*\(/i',         // setInterval with string
            '/innerHTML\s*=/i',            // innerHTML assignment
            '/outerHTML\s*=/i',            // outerHTML assignment
        ];
        
        foreach ($dangerous_patterns as $pattern) {
            // Skip the pattern check for our legitimate script tag
            if (preg_match($pattern, $filtered_output)) {
                // Allow our own script pattern
                if ($pattern === '/<script[^>]*>[^<]*(?!Chatbox\.initBubble)[^<]*<\/script>/is') {
                    // Count script tags - should only be one
                    preg_match_all('/<script/i', $filtered_output, $matches);
                    if (count($matches[0]) > 1) {
                        return false;
                    }
                    continue;
                }
                return false;
            }
        }
        
        // Ensure the script still contains our expected structure
        if (strpos($filtered_output, 'Chatbox.initBubble') === false) {
            return false;
        }
        
        // Ensure script tag structure is preserved
        if (strpos($filtered_output, "<script type='module'>") !== 0) {
            // Check for alternative valid opening
            if (strpos($filtered_output, "\n<script type='module'>") !== 0) {
                return false;
            }
        }
        
        return true;
    }

    /**
     * Recursively sanitize config values
     * 
     * @param mixed $data Data to sanitize
     * @return mixed Sanitized data
     */
    private function sanitize_config_recursive($data)
    {
        if (is_array($data)) {
            $sanitized = [];
            foreach ($data as $key => $value) {
                $safe_key = is_string($key) ? sanitize_text_field($key) : $key;
                $sanitized[$safe_key] = $this->sanitize_config_recursive($value);
            }
            return $sanitized;
        } elseif (is_string($data)) {
            // Sanitize string values - remove any potential script injections
            return sanitize_text_field($data);
        } elseif (is_int($data) || is_float($data) || is_bool($data) || is_null($data)) {
            return $data;
        }
        
        return '';
    }
}
</file>

<file path="templates/admin/settings.php">
<?php
if (!defined('ABSPATH')) {
    exit;
}

// Get screen object
$screen = get_current_screen();

// Ensure dashicons are available
wp_enqueue_style('dashicons');
?>
<div class="wrap">
    <h1 class="wp-heading-inline"><?php echo esc_html__('Muchat API Settings', 'muchat-ai'); ?></h1>

    <hr class="wp-header-end">

    <!-- Connection Status Indicator -->
    <div class="notice" style="border-left-color: <?php echo $connection_status['success'] ? '#4CAF50' : '#F44336'; ?>; display: flex; align-items: center; justify-content: space-between; padding: 10px 15px;">
        <div style="display: flex; align-items: center;">
            <span class="dashicons <?php echo $connection_status['success'] ? 'dashicons-yes-alt' : 'dashicons-warning'; ?>" style="font-size: 24px; margin-right: 10px; color: <?php echo $connection_status['success'] ? '#4CAF50' : '#F44336'; ?>;"></span>
            <div>
                <strong><?php esc_html_e('Muchat Server Connection', 'muchat-ai'); ?></strong><br>
                <small><?php echo esc_html($connection_status['message']); ?></small>
            </div>
        </div>
        <form method="get">
            <input type="hidden" name="page" value="muchat-woocommerce-api">
            <input type="hidden" name="action" value="muchat_check_connection">
            <?php wp_nonce_field('muchat_check_connection_action'); ?>
            <button type="submit" class="button button-secondary">
                <span class="dashicons dashicons-update" style="vertical-align: text-top; margin-right: 3px;"></span>
                <?php esc_html_e('Re-check', 'muchat-ai'); ?>
            </button>
        </form>
    </div>


    <?php settings_errors('muchat_ai_chatbot_plugin_messages'); ?>

    <form method="post" action="options.php" style="margin-top: 20px;">
        <?php
        settings_fields('muchat_ai_chatbot_plugin_settings');
        $available_fields = $this->get_available_fields();
        ?>

        <div class="metabox-holder">
            <!-- Product Meta Section -->
            <div id="product-meta-fields" class="postbox">
                <div class="postbox-header">
                    <h2 class="hndle"><?php echo esc_html__('Product Meta Fields', 'muchat-ai'); ?></h2>
                </div>
                <div class="inside" style="padding: 15px 20px;">
                    <?php do_settings_sections('muchat-meta-settings'); ?>
                </div>
            </div>

            <!-- Hidden fields to preserve other settings -->
            <?php if (isset($options['product_fields']) && is_array($options['product_fields'])): ?>
                <?php foreach ($options['product_fields'] as $field): ?>
                    <input type="hidden" name="muchat_ai_chatbot_plugin_options[product_fields][]" value="<?php echo esc_attr($field); ?>">
                <?php endforeach; ?>
            <?php endif; ?>

            <?php if (isset($options['post_fields']) && is_array($options['post_fields'])): ?>
                <?php foreach ($options['post_fields'] as $field): ?>
                    <input type="hidden" name="muchat_ai_chatbot_plugin_options[post_fields][]" value="<?php echo esc_attr($field); ?>">
                <?php endforeach; ?>
            <?php endif; ?>

            <?php if (isset($options['page_fields']) && is_array($options['page_fields'])): ?>
                <?php foreach ($options['page_fields'] as $field): ?>
                    <input type="hidden" name="muchat_ai_chatbot_plugin_options[page_fields][]" value="<?php echo esc_attr($field); ?>">
                <?php endforeach; ?>
            <?php endif; ?>

        </div>

        <?php submit_button(__('Save Changes', 'muchat-ai'), 'primary', 'submit', false, ['style' => 'margin-top: 20px;']); ?>
    </form>

    <form method="get" style="display: inline-block; margin-left: 10px; margin-top: 20px;">
        <input type="hidden" name="page" value="muchat-woocommerce-api">
        <input type="hidden" name="action" value="muchat_refresh_meta">
        <?php wp_nonce_field('muchat_refresh_meta_action'); ?>
        <?php submit_button(__('Refresh Fields', 'muchat-ai'), 'secondary', 'refresh_meta', false); ?>
    </form>
</div>
</file>

<file path="includes/Models/Product.php">
<?php

namespace Muchat\Api\Models;

class Product extends BaseModel
{
    /**
     * SECURITY: Immutable whitelist of allowed orderby columns
     * Using constants prevents runtime modification
     */
    private const ALLOWED_ORDERBY_COLUMNS = [
        'modified' => 'post_modified',
        'date' => 'post_date',
        'title' => 'post_title',
        'ID' => 'ID',
        'id' => 'ID',
    ];

    /**
     * SECURITY: Immutable whitelist of allowed sort directions
     */
    private const ALLOWED_DIRECTIONS = ['ASC', 'DESC'];

    /**
     * Get products
     *
     * @param array $params {
     *     Optional. Array of parameters.
     *
     *     @type int    $skip                  Number of items to skip
     *     @type int    $take                  Number of items to take
     *     @type string $modified_after        Only include items modified after this date
     *     @type string $muchat_modified_after Only include items with muchat modified after this date
     *     @type string $order_by              Field to order by. If comma-separated, only the first field is used (e.g., "modified" from "modified,ID")
     *     @type string $order                 Sort order (ASC or DESC)
     *     @type string $in_stock              Whether to include only in-stock products (product-specific)
     * }
     * @return array
     */
    public function get_products($params)
    {
        // Base query args
        $args = [
            'post_type' => 'product',
            'post_status' => 'publish',
            'fields' => 'ids',
        ];

        // Handle ordering - SECURITY: Validate BEFORE passing to filter
        // PHP 8.1+ COMPAT: Check for empty/null values to prevent deprecation warnings
        // 
        // BUG FIX: sanitize_key() strips commas, breaking multi-column sorting.
        // If order_by is "modified,ID", sanitize_key returns "modifiedid" which fails whitelist.
        // Solution: Extract the first field before sanitizing to support comma-separated input.
        $raw_order_by = !empty($params['order_by']) ? (string) $params['order_by'] : 'modified';
        
        // Extract the primary sort field (first item before comma)
        // This gracefully handles both "modified" and "modified,ID" inputs
        $parts = explode(',', $raw_order_by);
        $primary_sort = trim($parts[0]);
        $order_by_field = sanitize_key($primary_sort);
        
        $order_direction = !empty($params['order']) ? strtoupper(sanitize_key((string) $params['order'])) : 'ASC';

        // SECURITY: Strict whitelist validation for order direction
        if (!in_array($order_direction, self::ALLOWED_DIRECTIONS, true)) {
            $order_direction = 'ASC';
        }

        // SECURITY: Strict whitelist validation for orderby field using array_key_exists
        if (!array_key_exists($order_by_field, self::ALLOWED_ORDERBY_COLUMNS)) {
            $order_by_field = 'modified';
        }

        // Get the actual column name from validated key
        $sql_column = self::ALLOWED_ORDERBY_COLUMNS[$order_by_field];

        // SECURITY: Pass validated data through query vars instead of closure capture
        // This prevents race conditions and memory manipulation attacks
        $args['muchat_orderby_column'] = $sql_column;
        $args['muchat_orderby_direction'] = $order_direction;

        // Add the orderby filter with re-validation inside callback (defense in depth)
        add_filter('posts_orderby', [$this, 'apply_custom_orderby'], 10, 2);

        // Add date filters if provided
        if (!empty($params['modified_after'])) {
            try {
                // Parse the date as UTC to ensure consistency
                $date = new \DateTime($params['modified_after'], new \DateTimeZone('UTC'));
                $args['date_query'][] = [
                    'column' => 'post_modified_gmt',
                    'after' => $date->format('Y-m-d H:i:s'),
                    'inclusive' => false
                ];
            } catch (\Exception $e) {
                // If date parsing fails, try with strtotime as fallback
                $modified_after_timestamp = strtotime($params['modified_after']);
                if ($modified_after_timestamp) {
                    $args['date_query'][] = [
                        'column' => 'post_modified_gmt',
                        'after' => gmdate('Y-m-d H:i:s', $modified_after_timestamp),
                        'inclusive' => false
                    ];
                }
            }
        }

        // Add muchat_modified_after filter if provided
        if (!empty($params['muchat_modified_after'])) {
            try {
                // Parse the date as UTC to ensure consistency
                $date = new \DateTime($params['muchat_modified_after'], new \DateTimeZone('UTC'));
                $formatted_date = $date->format('Y-m-d H:i:s');
                
                if (!isset($args['meta_query'])) {
                    $args['meta_query'] = [];
                }
                
                $args['meta_query'][] = [
                    'key' => '_muchat_date_modified',
                    'value' => $formatted_date,
                    'compare' => '>',
                    'type' => 'DATETIME'
                ];
            } catch (\Exception $e) {
                // If date parsing fails, try with strtotime as fallback
                $muchat_modified_timestamp = strtotime($params['muchat_modified_after']);
                if ($muchat_modified_timestamp) {
                    if (!isset($args['meta_query'])) {
                        $args['meta_query'] = [];
                    }
                    
                    $args['meta_query'][] = [
                        'key' => '_muchat_date_modified',
                        'value' => gmdate('Y-m-d H:i:s', $muchat_modified_timestamp),
                        'compare' => '>',
                        'type' => 'DATETIME'
                    ];
                }
            }
        }

        // Product-specific: Add in-stock filter if requested
        if (!empty($params['in_stock']) && $params['in_stock'] === 'true') {
            $args['meta_query'][] = [
                'key' => '_stock_status',
                'value' => 'instock',
                'compare' => '='
            ];
        }

        // Set pagination parameters with boundary validation
        $requested_offset = max(0, isset($params['skip']) ? (int) $params['skip'] : 0);
        $requested_limit = min(max(1, isset($params['take']) ? (int) $params['take'] : 30), 100);

        // Update the query with pagination parameters
        $args['posts_per_page'] = $requested_limit;
        $args['offset'] = $requested_offset;
        
        // Disable caching for this query to ensure fresh results
        $args['cache_results'] = false;
        $args['update_post_meta_cache'] = false;
        $args['update_post_term_cache'] = false;
        $args['no_found_rows'] = false; // We need found_posts for total_count

        // Execute the final query with pagination
        $query = new \WP_Query($args);

        // Remove the custom orderby filter to avoid affecting other queries
        remove_filter('posts_orderby', [$this, 'apply_custom_orderby'], 10);

        // Get total count from the same query
        $total_count = (int) $query->found_posts;

        // Fallback: If found_posts is 0 but we have results, some optimization plugin
        // may have set no_found_rows to true. Run a separate COUNT query.
        if ($total_count === 0 && !empty($query->posts)) {
            $count_args = $args;
            $count_args['posts_per_page'] = -1;
            $count_args['offset'] = 0;
            $count_args['fields'] = 'ids';
            $count_args['no_found_rows'] = true; // We only need the count, not pagination info
            $count_query = new \WP_Query($count_args);
            $total_count = $count_query->post_count;
        }

        // Format the products
        $products = array_map(function ($product_id) {
            return $this->format_product(wc_get_product($product_id));
        }, $query->posts);

        // Use common response format consistent with other endpoints
        $plugin = new \Muchat\Api\Core\Plugin();

        return [
            'plugin_version' => $plugin->get_version(),
            'offset' => $requested_offset,
            'limit' => $requested_limit,
            'total_count' => $total_count,
            'has_more' => ($requested_offset + count($products) < $total_count),
            'items' => array_values(array_filter($products))
        ];
    }

    /**
     * Apply custom orderby to WP_Query
     * 
     * SECURITY: This method re-validates the orderby values inside the callback
     * as defense-in-depth against memory manipulation attacks.
     *
     * @param string $orderby Current orderby SQL
     * @param \WP_Query $query The query object
     * @return string Modified orderby SQL
     */
    public function apply_custom_orderby($orderby, $query)
    {
        // Check if this is our query by looking for our custom query vars
        if (empty($query->query_vars['muchat_orderby_column'])) {
            return $orderby;
        }

        global $wpdb;

        $column = $query->query_vars['muchat_orderby_column'];
        $direction = $query->query_vars['muchat_orderby_direction'] ?? 'ASC';

        // SECURITY: Re-validate inside callback (defense in depth)
        // Use hardcoded arrays to prevent any possibility of manipulation
        $valid_columns = ['post_modified', 'post_date', 'post_title', 'ID'];
        $valid_directions = ['ASC', 'DESC'];

        if (!in_array($column, $valid_columns, true)) {
            return $orderby;
        }
        if (!in_array($direction, $valid_directions, true)) {
            $direction = 'ASC';
        }

        // SECURITY: Extra validation - only allow alphanumeric and underscore
        // This is belt-and-suspenders in case whitelist values are ever modified
        $column = preg_replace('/[^a-zA-Z_]/', '', $column);
        $direction = preg_replace('/[^A-Z]/', '', $direction);

        // Final sanity check after regex
        if (empty($column) || !in_array($direction, ['ASC', 'DESC'], true)) {
            return $orderby;
        }

        // Safe - values are triple-validated (whitelist, re-validation, regex)
        return "{$wpdb->posts}.{$column} {$direction}, {$wpdb->posts}.ID ASC";
    }

    /**
     * Get single product
     * 
     * PERFORMANCE: Uses 'single' context for full HTML formatting.
     * Single product requests need complete description formatting.
     *
     * @param int $product_id
     * @return array|null
     */
    public function get_product($product_id)
    {
        $product = wc_get_product($product_id);

        if (!$product || $product->get_status() !== 'publish') {
            return null;
        }

        // Use 'single' context for full HTML formatting
        return $this->format_product($product, 'single');
    }

    /**
     * Get product IDs
     *
     * @param string|null $modified_after
     * @param string|null $muchat_modified_after
     * @return array
     */
    public function get_product_ids($modified_after = null, $muchat_modified_after = null)
    {
        $args = [
            'post_type' => 'product',
            'post_status' => 'publish',
            'posts_per_page' => -1,
            'orderby' => ['modified' => 'ASC', 'ID' => 'ASC'],
        ];

        if ($modified_after) {
            try {
                // Parse the date as UTC to ensure consistency
                $date = new \DateTime($modified_after, new \DateTimeZone('UTC'));
                $args['date_query'] = [
                    [
                        'column' => 'post_modified_gmt',
                        'after' => $date->format('Y-m-d H:i:s'),
                        'inclusive' => false
                    ]
                ];
            } catch (\Exception $e) {
                // If date parsing fails, try with strtotime as fallback
                $modified_after_timestamp = strtotime($modified_after);
                if ($modified_after_timestamp) {
                    $args['date_query'] = [
                        [
                            'column' => 'post_modified_gmt',
                            'after' => gmdate('Y-m-d H:i:s', $modified_after_timestamp),
                            'inclusive' => false
                        ]
                    ];
                }
            }
        }

        // Add muchat_modified_after filter
        if ($muchat_modified_after) {
            try {
                $date = new \DateTime($muchat_modified_after, new \DateTimeZone('UTC'));
                $formatted_date = $date->format('Y-m-d H:i:s');
                
                $args['meta_query'] = [
                    [
                        'key' => '_muchat_date_modified',
                        'value' => $formatted_date,
                        'compare' => '>',
                        'type' => 'DATETIME'
                    ]
                ];
            } catch (\Exception $e) {
                $muchat_modified_timestamp = strtotime($muchat_modified_after);
                if ($muchat_modified_timestamp) {
                    $args['meta_query'] = [
                        [
                            'key' => '_muchat_date_modified',
                            'value' => gmdate('Y-m-d H:i:s', $muchat_modified_timestamp),
                            'compare' => '>',
                            'type' => 'DATETIME'
                        ]
                    ];
                }
            }
        }

        // Filter to select only ID and post_modified_gmt for performance
        $fields_filter = function ($fields) {
            global $wpdb;
            return "{$wpdb->posts}.ID, {$wpdb->posts}.post_modified_gmt";
        };
        add_filter('posts_fields', $fields_filter);

        // Ensure we get found_posts for total_count
        $args['no_found_rows'] = false;

        $query = new \WP_Query($args);

        // Clean up the filter
        remove_filter('posts_fields', $fields_filter);

        // Get total count from the same query
        $total_count = (int) $query->found_posts;

        // Fallback: If found_posts is 0 but we have results, some optimization plugin
        // may have set no_found_rows to true. Use post_count instead.
        if ($total_count === 0 && !empty($query->posts)) {
            $total_count = $query->post_count;
        }

        $products = array_map(function ($post) {
            $muchat_modified = \Muchat\Api\Utils\ProductChangeTracker::get_muchat_modified_date($post->ID);
            return [
                'id' => $post->ID,
                'date_modified' => \Muchat\Api\Utils\DateTimeHelper::format_for_api($post->post_modified_gmt),
                'muchat_date_modified' => \Muchat\Api\Utils\DateTimeHelper::format_for_api($muchat_modified)
            ];
        }, $query->posts);

        return [
            'total_count' => $total_count,
            'items' => $products
        ];
    }

    /**
     * Format product data
     * 
     * PERFORMANCE: Uses context to determine formatting level.
     * - 'list' context: Uses lightweight HTML cleaning (no DOMDocument)
     * - 'single' context: Uses full HTML cleaning with DOMDocument
     * 
     * This prevents CPU overhead when loading 30+ products in a list view.
     *
     * @param \WC_Product $product
     * @param string $context Either 'list' or 'single' - determines formatting depth
     * @return array|null
     */
    protected function format_product($product, $context = 'list')
    {
        $data = [];
        $enabled_fields = $this->get_enabled_fields('product');

        foreach ($enabled_fields as $field) {
            switch ($field) {
                case 'id':
                    $data['id'] = $product->get_id();
                    break;
                case 'name':
                    $data['name'] = $product->get_name();
                    break;
                case 'permalink':
                    $data['url'] = urldecode($product->get_permalink());
                    break;
                case 'date_modified':
                    $post_data = get_post($product->get_id());
                    $data['date_modified'] = \Muchat\Api\Utils\DateTimeHelper::format_for_api($post_data->post_modified_gmt ?? null);
                    
                    // Add muchat_date_modified (tracks only price/stock changes)
                    $muchat_modified = \Muchat\Api\Utils\ProductChangeTracker::get_muchat_modified_date($product->get_id());
                    $data['muchat_date_modified'] = \Muchat\Api\Utils\DateTimeHelper::format_for_api($muchat_modified);
                    break;
                case 'description':
                    // PERFORMANCE: Use lightweight formatter for list views
                    if ($context === 'single') {
                        $data['description'] = $this->formatter->clean_html_content($product->get_description());
                    } else {
                        // List view: use lightweight version without DOMDocument
                        $data['description'] = $this->formatter->clean_html_content_light($product->get_description(), 50);
                    }
                    break;
                case 'short_description':
                    // PERFORMANCE: Use lightweight formatter for list views
                    if ($context === 'single') {
                        $data['short_description'] = $this->formatter->clean_html_content($product->get_short_description());
                    } else {
                        // List view: use lightweight version without DOMDocument
                        $data['short_description'] = $this->formatter->clean_html_content_light($product->get_short_description(), 30);
                    }
                    break;
                case 'price':
                    $price = $product->get_price();
                    $data['price'] = $price === '' ? null : $price;
                    break;
                case 'regular_price':
                    $data['regular_price'] = $product->get_regular_price();
                    break;
                case 'sale_price':
                    $data['sale_price'] = $product->get_sale_price();
                    break;
                case 'currency':
                    $data['currency'] = html_entity_decode(get_woocommerce_currency_symbol(), ENT_QUOTES | ENT_HTML5, 'UTF-8');
                    break;
                case 'stock_status':
                    $data['stock_status'] = $product->get_stock_status();
                    break;
                case 'categories':
                    $data['categories'] = $this->get_taxonomy_terms($product, 'product_cat');
                    break;
                case 'tags':
                    $data['tags'] = $this->get_taxonomy_terms($product, 'product_tag');
                    break;
                case 'images':
                    $data['images'] = $this->get_product_images($product);
                    break;
                case 'attributes':
                    $data['attributes'] = $this->get_product_attributes($product);
                    break;
                case 'weight':
                    $data['weight'] = $product->get_weight();
                    break;
                case 'dimensions':
                    $data['dimensions'] = [
                        'length' => $product->get_length(),
                        'width' => $product->get_width(),
                        'height' => $product->get_height()
                    ];
                    break;
                case 'variations':
                    $data['variations'] = $product->is_type('variable') ? $this->get_product_variations($product) : [];
                    break;
            }
        }

        // Add meta fields if enabled
        $options = $this->get_options();
        $selected_meta_keys = isset($options['product_meta_fields']) ?
            $options['product_meta_fields'] : [];

        if (!empty($selected_meta_keys)) {
            $meta_data = $this->format_meta_data($product, $selected_meta_keys);
            if (!empty($meta_data)) {
                $data['meta_data'] = $meta_data;
            }
        }

        // Clean up empty values
        $formatted_data = $this->formatter->remove_empty_values($data);

        // Ensure the ID is always present to prevent the product from being filtered out
        if (!isset($formatted_data['id'])) {
            $formatted_data['id'] = $product->get_id();
        }

        /**
         * Filter the formatted product data before returning
         * 
         * EXTENSIBILITY: Allows other plugins/themes to modify the API output.
         * This enables developers to add custom fields (e.g., brand, custom attributes)
         * without modifying the core plugin code.
         * 
         * @since 2.1.0
         * 
         * @param array       $formatted_data The formatted product data array
         * @param \WC_Product $product        The WooCommerce product object
         * @param string      $context        The formatting context ('list' or 'single')
         * 
         * @return array Modified product data
         * 
         * @example
         * // Add a custom "brand" field to product API output
         * add_filter('muchat_ai_formatted_product_data', function($data, $product, $context) {
         *     $data['brand'] = get_post_meta($product->get_id(), '_brand', true);
         *     return $data;
         * }, 10, 3);
         */
        return apply_filters('muchat_ai_formatted_product_data', $formatted_data, $product, $context);
    }

    /**
     * Get taxonomy terms
     *
     * @param \WC_Product $product
     * @param string $taxonomy
     * @return array
     */
    protected function get_taxonomy_terms($product, $taxonomy)
    {
        $terms = get_the_terms($product->get_id(), $taxonomy);
        $term_data = [];

        if ($terms && !is_wp_error($terms)) {
            foreach ($terms as $term) {
                $term_data[urldecode($term->name)] = urldecode(get_term_link($term, $taxonomy));
            }
        }

        return $term_data;
    }

    /**
     * Get product images
     *
     * @param \WC_Product $product
     * @return array
     */
    protected function get_product_images($product)
    {
        $images = [];

        // Add featured image
        if ($product->get_image_id()) {
            $image_url = wp_get_attachment_url($product->get_image_id());
            if ($image_url) {
                $images[] = $image_url;
            }
        }

        // Add gallery images
        $gallery_images = $product->get_gallery_image_ids();
        foreach ($gallery_images as $image_id) {
            $image_url = wp_get_attachment_url($image_id);
            if ($image_url) {
                $images[] = $image_url;
                if (count($images) >= 4) {
                    break;
                }
            }
        }

        return $images;
    }

    /**
     * Get product attributes
     *
     * @param \WC_Product $product
     * @return array
     */
    protected function get_product_attributes($product)
    {
        $attributes = [];

        // Get product attributes for all product types
        $product_attributes = $product->get_attributes();

        foreach ($product_attributes as $attribute_name => $attribute) {
            $name = wc_attribute_label($attribute_name);
            $decoded_attribute_name = urldecode($attribute_name);

            if ($attribute->is_taxonomy()) {
                $terms = wp_get_post_terms($product->get_id(), $attribute->get_name(), ['fields' => 'names']);
                $attributes[$decoded_attribute_name] = count($terms) === 1 ? $terms[0] : $terms;
            } else {
                $attribute_options = $attribute->get_options();
                $attributes[$decoded_attribute_name] = count($attribute_options) === 1 ? $attribute_options[0] : $attribute_options;
            }
        }

        return $attributes;
    }

    /**
     * Get product variations
     * 
     * PERFORMANCE: Uses direct SQL with LIMIT instead of loading all children.
     * Uses batch loading to reduce N+1 query problem.
     * Instead of loading each variation individually (50+ queries per product),
     * this loads all variation data in 2-3 batch queries.
     *
     * @param \WC_Product_Variable $product
     * @return array
     */
    protected function get_product_variations($product)
    {
        global $wpdb;
        
        $parent_id = $product->get_id();
        $max_variations = 100;
        
        // PERFORMANCE: Get only the IDs we need with LIMIT, not all children
        // This prevents memory issues on products with 1000+ variations
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $variation_ids = $wpdb->get_col($wpdb->prepare(
            "SELECT ID FROM {$wpdb->posts}
             WHERE post_parent = %d
             AND post_type = 'product_variation'
             AND post_status = 'publish'
             ORDER BY menu_order ASC, ID ASC
             LIMIT %d",
            $parent_id,
            $max_variations
        ));
        
        if (empty($variation_ids)) {
            return [];
        }
        
        // SECURITY: Sanitize all IDs as integers
        $sanitized_ids = array_map('absint', $variation_ids);
        $sanitized_ids = array_filter($sanitized_ids);
        
        if (empty($sanitized_ids)) {
            return [];
        }
        
        // SECURITY: Use proper prepared statement with placeholders for IN clause
        // Even though IDs are sanitized, this is the WordPress best practice
        $placeholders = implode(',', array_fill(0, count($sanitized_ids), '%d'));
        
        // Batch load all post data in one query
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $posts_data = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT ID, post_title, post_parent, post_status
                FROM {$wpdb->posts}
                WHERE ID IN ({$placeholders})
                AND post_status = 'publish'",
                ...$sanitized_ids
            ),
            OBJECT_K
        );
        
        if (empty($posts_data)) {
            return [];
        }
        
        // Get IDs of published variations only
        $published_ids = array_keys($posts_data);
        
        if (empty($published_ids)) {
            return [];
        }
        
        // Batch load all meta data for variations
        $meta_keys_needed = [
            '_price', '_regular_price', '_sale_price', 
            '_stock_status', '_stock', '_thumbnail_id'
        ];
        
        // Get selected meta keys for variations
        $options = $this->get_options();
        $selected_meta_keys = isset($options['product_meta_fields']) ?
            $options['product_meta_fields'] : [];
        
        // Add custom meta keys to the batch load
        $all_meta_keys = array_merge($meta_keys_needed, $selected_meta_keys);
        
        // SECURITY: Sanitize all IDs as integers for the meta query
        $sanitized_published_ids = array_map('absint', $published_ids);
        $sanitized_published_ids = array_filter($sanitized_published_ids);
        
        if (empty($sanitized_published_ids)) {
            return [];
        }
        
        // SECURITY: Use proper prepared statement with placeholders for IN clause
        $id_placeholders = implode(',', array_fill(0, count($sanitized_published_ids), '%d'));
        
        // Build safe IN clause for meta keys
        $meta_key_placeholders = implode(',', array_fill(0, count($all_meta_keys), '%s'));
        
        // Combine all parameters for prepare: IDs first, then LIKE pattern, then meta keys
        $prepare_params = array_merge(
            $sanitized_published_ids,
            ['attribute_%'],
            $all_meta_keys
        );
        
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $all_meta = $wpdb->get_results($wpdb->prepare(
            "SELECT post_id, meta_key, meta_value
            FROM {$wpdb->postmeta}
            WHERE post_id IN ({$id_placeholders})
            AND (meta_key LIKE %s OR meta_key IN ({$meta_key_placeholders}))",
            ...$prepare_params
        ));
        
        // Index meta by post_id for fast lookup
        $meta_by_post = [];
        foreach ($all_meta as $meta) {
            if (!isset($meta_by_post[$meta->post_id])) {
                $meta_by_post[$meta->post_id] = [];
            }
            $meta_by_post[$meta->post_id][$meta->meta_key] = $meta->meta_value;
        }
        
        // Build variation data without additional queries
        $variations = [];
        foreach ($published_ids as $vid) {
            if (!isset($posts_data[$vid])) {
                continue;
            }
            
            $post = $posts_data[$vid];
            $meta = $meta_by_post[$vid] ?? [];
            
            $variation_data = [
                'title' => $post->post_title,
                'price' => $meta['_price'] ?? null,
                'regular_price' => $meta['_regular_price'] ?? null,
                'sale_price' => $meta['_sale_price'] ?? null,
                'stock_status' => $meta['_stock_status'] ?? 'instock',
                'attributes' => $this->get_variation_attributes_from_meta($meta),
                'image' => $this->get_variation_image_from_meta($meta['_thumbnail_id'] ?? null)
            ];
            
            // Add custom meta data for variations
            if (!empty($selected_meta_keys)) {
                $custom_meta = [];
                foreach ($selected_meta_keys as $key) {
                    if (isset($meta[$key]) && $meta[$key] !== '') {
                        $custom_meta[$key] = $meta[$key];
                    }
                }
                if (!empty($custom_meta)) {
                    $variation_data['meta_data'] = [$custom_meta];
                }
            }
            
            $variations[] = $variation_data;
        }
        
        return $variations;
    }
    
    /**
     * Get variation attributes from pre-loaded meta data
     * 
     * PERFORMANCE: Avoids additional database queries by using pre-loaded meta
     *
     * @param array $meta Pre-loaded meta data
     * @return array
     */
    protected function get_variation_attributes_from_meta($meta)
    {
        $attributes = [];
        
        foreach ($meta as $key => $value) {
            if (strpos($key, 'attribute_') !== 0) {
                continue;
            }
            
            $taxonomy = str_replace('attribute_', '', $key);
            $attribute_label = urldecode(wc_attribute_label($taxonomy));
            
            // Try to get term name if it's a taxonomy attribute
            if (taxonomy_exists($taxonomy) && !empty($value)) {
                $term = get_term_by('slug', $value, $taxonomy);
                $attribute_value = $term ? urldecode($term->name) : urldecode($value);
            } else {
                $attribute_value = urldecode($value);
            }
            
            $attributes[$attribute_label] = $attribute_value;
        }
        
        return $attributes;
    }
    
    /**
     * Get variation image from pre-loaded thumbnail ID
     * 
     * @param int|null $thumbnail_id Pre-loaded thumbnail ID
     * @return string|null
     */
    protected function get_variation_image_from_meta($thumbnail_id)
    {
        if (empty($thumbnail_id)) {
            return null;
        }
        
        $image_url = wp_get_attachment_url((int)$thumbnail_id);
        return $image_url ?: null;
    }

    /**
     * Get variation attributes
     *
     * @param \WC_Product_Variation $variation
     * @return array
     */
    protected function get_variation_attributes($variation)
    {
        $attributes = [];
        foreach ($variation->get_variation_attributes() as $attribute_name => $attribute_value) {
            $taxonomy = str_replace('attribute_', '', $attribute_name);
            $term = get_term_by('slug', $attribute_value, $taxonomy);
            $attribute_label = urldecode(wc_attribute_label($taxonomy, $variation));
            $attribute_value = $term ?
                urldecode($term->name) :
                urldecode($attribute_value);

            $attributes[$attribute_label] = $attribute_value;
        }
        return $attributes;
    }

    /**
     * Get variation image
     *
     * @param \WC_Product_Variation $variation
     * @return string|null
     */
    protected function get_variation_image($variation)
    {
        $variation_image_id = get_post_thumbnail_id($variation->get_id());

        if (empty($variation_image_id)) {
            return null;
        }

        $image_url = wp_get_attachment_url($variation_image_id);
        return $image_url ?: null;
    }

    /**
     * Get meta field label
     */
    public function get_meta_field_label($meta_key)
    {
        static $field_labels = null;

        // Initialize labels cache if not already done
        if ($field_labels === null) {
            $field_labels = array();

            // 1. Try to get from WooCommerce Product Options
            add_action('woocommerce_product_options_general_product_data', function () use (&$field_labels) {
                $original_function = 'woocommerce_wp_text_input';

                if (function_exists($original_function)) {
                    function woocommerce_wp_text_input($field)
                    {
                        global $product_field_labels;
                        if (!empty($field['id']) && !empty($field['label'])) {
                            // Remove array notation if exists
                            $clean_id = preg_replace('/$$\d*$$$/', '', $field['id']);
                            $product_field_labels[$clean_id] = $field['label'];
                        }
                    }
                }

                // Capture fields
                global $product_field_labels;
                $product_field_labels = array();

                // Trigger the action to collect fields
                do_action('woocommerce_product_options_general_product_data');

                if (!empty($product_field_labels)) {
                    $field_labels = array_merge($field_labels, $product_field_labels);
                }
            });

            // 2. Try to get from Variations
            add_action('woocommerce_product_after_variable_attributes', function ($loop, $variation_data, $variation) use (&$field_labels) {
                $original_function = 'woocommerce_wp_text_input';

                if (function_exists($original_function)) {
                    function woocommerce_wp_text_input($field)
                    {
                        global $variation_field_labels;
                        if (!empty($field['id']) && !empty($field['label'])) {
                            // Remove array notation if exists
                            $clean_id = preg_replace('/$$\d*$$$/', '', $field['id']);
                            $variation_field_labels[$clean_id] = $field['label'];
                        }
                    }
                }

                // Capture fields
                global $variation_field_labels;
                $variation_field_labels = array();

                // Trigger the action to collect fields
                do_action('woocommerce_product_after_variable_attributes', 0, array(), null);

                if (!empty($variation_field_labels)) {
                    $field_labels = array_merge($field_labels, $variation_field_labels);
                }
            }, 10, 3);

            // 3. Try ACF Fields
            if (function_exists('acf_get_field_groups')) {
                $groups = acf_get_field_groups(['post_type' => 'product']);
                foreach ($groups as $group) {
                    $fields = acf_get_fields($group);
                    if ($fields) {
                        foreach ($fields as $field) {
                            $field_labels[$field['name']] = $field['label'];
                        }
                    }
                }
            }

            // Check if it's an ACF field
            if (function_exists('get_field_object')) {
                $acf_field = get_field_object($meta_key);
                if ($acf_field && !empty($acf_field['label'])) {
                    return $acf_field['label'];
                }
            }

            // 4. Try WooCommerce Attributes
            if (function_exists('wc_get_attribute_taxonomies')) {
                $attributes = wc_get_attribute_taxonomies();
                foreach ($attributes as $attribute) {
                    $field_labels['pa_' . $attribute->attribute_name] = $attribute->attribute_label;
                }
            }

            // 5. Allow other plugins to add their field labels
            $field_labels = apply_filters('woocommerce_product_meta_field_labels', $field_labels);
        }

        // Return label if exists
        if (isset($field_labels[$meta_key])) {
            return $field_labels[$meta_key];
        }

        // Try WooCommerce dynamic label
        $dynamic_label = apply_filters('woocommerce_product_meta_field_label', $meta_key);
        if ($dynamic_label !== $meta_key) {
            return $dynamic_label;
        }

        // Try attribute label
        if (strpos($meta_key, 'pa_') === 0) {
            $taxonomy = wc_attribute_taxonomy_name(substr($meta_key, 3));
            $attribute = wc_get_attribute(wc_attribute_taxonomy_id_by_name($taxonomy));
            if ($attribute) {
                return $attribute->label;
            }
        }

        // Return original key if no label found
        return $meta_key;
    }

    /**
     * Format product meta data
     */
    protected function format_meta_data($product, $selected_meta_keys)
    {
        $meta_data = array();
        $result = array(); // A single object
        $options = $this->get_options();
        $meta_labels = isset($options['meta_labels']) ? $options['meta_labels'] : [];

        foreach ($selected_meta_keys as $meta_key) {
            $meta_value = $product->get_meta($meta_key);

            if ($meta_value !== '') {
                // First get the default label from get_meta_field_label method
                $default_label = $this->get_meta_field_label($meta_key);

                // If a custom label exists, use it; otherwise use the default label
                $label = isset($meta_labels[$meta_key]) && !empty($meta_labels[$meta_key])
                    ? $meta_labels[$meta_key]
                    : $default_label;

                // Ensure label is decoded
                $label = urldecode($label);

                // Handle serialized and JSON data
                if (is_serialized($meta_value)) {
                    $unserialized_value = maybe_unserialize($meta_value);
                    // Convert complex data to strings
                    if (is_array($unserialized_value) || is_object($unserialized_value)) {
                        $meta_value = $this->convert_complex_data_to_displayable($unserialized_value);
                    } else {
                        $meta_value = $unserialized_value;
                    }
                } elseif (is_array($meta_value)) {
                    // If meta_value is already an array, just use it directly
                    $meta_value = $this->convert_complex_data_to_displayable($meta_value);
                } elseif ($this->is_json($meta_value)) {
                    $json_value = json_decode($meta_value, true);
                    // Convert complex data to strings
                    if (is_array($json_value) || is_object($json_value)) {
                        $meta_value = $this->convert_complex_data_to_displayable($json_value);
                    } else {
                        $meta_value = $json_value;
                    }
                }

                // Add to single object
                $result[$label] = $meta_value;
            }
        }

        // Wrap in array if not empty
        if (!empty($result)) {
            $meta_data[] = $result;
        }

        return $meta_data;
    }

    /**
     * Convert complex data (arrays/objects) to displayable format
     *
     * @param mixed $data
     * @return mixed
     */
    protected function convert_complex_data_to_displayable($data)
    {
        if (is_array($data)) {
            $result = array();
            foreach ($data as $key => $value) {
                // Decode the key
                $decoded_key = urldecode($key);

                if (is_array($value) || is_object($value)) {
                    $result[$decoded_key] = $this->convert_complex_data_to_displayable($value);
                } else {
                    $result[$decoded_key] = $value;
                }
            }
            return $result;
        } elseif (is_object($data)) {
            return $this->convert_complex_data_to_displayable((array)$data);
        } else {
            return $data;
        }
    }

    /**
     * Get all product meta fields with their information
     * 
     * PERFORMANCE OPTIMIZATION:
     * - Uses a two-step query approach to avoid filesort on large tables
     * - Step 1: Get sample product IDs using indexed query
     * - Step 2: Get meta keys only from the sample (uses postmeta index)
     * - Cached for 12 hours to reduce database load
     * - Safe for catalogs with 100k+ products
     *
     * @return array
     */
    public function get_product_meta_fields()
    {
        $cache_key = 'muchat_product_meta_fields_cache';
        $cached_fields = get_transient($cache_key);

        if (false !== $cached_fields) {
            return $cached_fields;
        }

        global $wpdb;

        $meta_fields = [];
        $added_fields = []; // Track added fields to prevent duplication

        // PERFORMANCE: Two-step approach to avoid heavy table scans
        // Step 1: Get sample product IDs efficiently (uses post_type index)
        // This query is fast because it only reads from wp_posts with an index
        $sample_size = 200; // Smaller sample, but more efficient
        
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $sample_product_ids = $wpdb->get_col($wpdb->prepare("
            SELECT ID FROM {$wpdb->posts}
            WHERE post_type = 'product'
            AND post_status = 'publish'
            ORDER BY ID DESC
            LIMIT %d
        ", $sample_size));
        
        // Also get some variation IDs
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $sample_variation_ids = $wpdb->get_col($wpdb->prepare("
            SELECT ID FROM {$wpdb->posts}
            WHERE post_type = 'product_variation'
            AND post_status = 'publish'
            ORDER BY ID DESC
            LIMIT %d
        ", $sample_size));
        
        $all_sample_ids = array_merge($sample_product_ids, $sample_variation_ids);
        
        if (empty($all_sample_ids)) {
            return [];
        }
        
        // SECURITY: Sanitize all IDs as integers to prevent SQL injection
        $sanitized_ids = array_map('absint', $all_sample_ids);
        $sanitized_ids = array_filter($sanitized_ids); // Remove zeros
        
        if (empty($sanitized_ids)) {
            return [];
        }
        
        // SECURITY: Use proper prepared statement with %d placeholders for IN clause
        $placeholders = implode(',', array_fill(0, count($sanitized_ids), '%d'));
        
        // Step 2: Get meta keys from sample products only (uses post_id index on postmeta)
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $regular_fields = $wpdb->get_results($wpdb->prepare(
            "SELECT 
                pm.meta_key,
                COUNT(DISTINCT pm.post_id) as usage_count,
                MAX(CASE WHEN p.post_type = 'product' THEN 1 ELSE 0 END) as is_product,
                MAX(CASE WHEN p.post_type = 'product_variation' THEN 1 ELSE 0 END) as is_variation
            FROM {$wpdb->postmeta} pm
            INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
            WHERE pm.post_id IN ({$placeholders})
            AND pm.meta_key NOT LIKE %s
            AND pm.meta_key NOT LIKE %s
            AND pm.meta_value != ''
            GROUP BY pm.meta_key
            HAVING usage_count >= 2
            ORDER BY usage_count DESC
            LIMIT 100",
            ...array_merge($sanitized_ids, ['\_%', 'wp_%'])
        ));

        // 2. Process regular fields
        foreach ($regular_fields as $result) {
            // Skip if this field is an ACF field
            if (function_exists('acf_get_field_groups')) {
                $is_acf_field = false;
                $groups = acf_get_field_groups(['post_type' => 'product']);
                foreach ($groups as $group) {
                    $fields = acf_get_fields($group);
                    if ($fields) {
                        foreach ($fields as $field) {
                            if ($field['name'] === $result->meta_key) {
                                $is_acf_field = true;
                                break 2;
                            }
                        }
                    }
                }
                if ($is_acf_field) {
                    continue;
                }
            }

            // Get sample values
            $sample_values = $this->get_meta_sample_values($result->meta_key);

            // Skip if no valid sample values
            if (empty($sample_values)) {
                continue;
            }

            // Determine field type
            $type = ($result->is_product && $result->is_variation) ? 'both' : ($result->is_product ? 'product' : 'variation');

            $meta_fields[] = [
                'key' => $result->meta_key,
                'display_name' => $this->get_meta_display_name($result->meta_key),
                'type' => $type,
                'usage_count' => (int)$result->usage_count,
                'sample_values' => $sample_values
            ];

            $added_fields[] = $result->meta_key;
        }

        // 3. Get ACF fields
        if (function_exists('acf_get_field_groups')) {
            $groups = acf_get_field_groups(['post_type' => 'product']);

            foreach ($groups as $group) {
                $fields = acf_get_fields($group);
                if ($fields) {
                    foreach ($fields as $field) {
                        // Skip if field was already added as a regular field
                        if (in_array($field['name'], $added_fields)) {
                            continue;
                        }

                        // Get usage count for ACF field
                        $usage_count = $wpdb->get_var($wpdb->prepare("
                            SELECT COUNT(DISTINCT post_id) 
                            FROM {$wpdb->postmeta} 
                            WHERE meta_key = %s
                            AND post_id IN (
                                SELECT ID FROM {$wpdb->posts}
                                WHERE post_type IN ('product', 'product_variation')
                            )
                        ", $field['name']));

                        if ($usage_count > 0) {
                            // Get sample values
                            $sample_values = $this->get_meta_sample_values($field['name']);

                            $meta_fields[] = [
                                'key' => $field['name'],
                                'display_name' => $field['label'],
                                'type' => 'acf',
                                'usage_count' => (int)$usage_count,
                                'sample_values' => $sample_values
                            ];

                            $added_fields[] = $field['name'];
                        }
                    }
                }
            }
        }

        // 4. Sort all fields by usage count
        usort($meta_fields, function ($a, $b) {
            return $b['usage_count'] - $a['usage_count'];
        });

        // Combine standard meta fields and ACF fields
        $all_fields = array_values($meta_fields);

        // PERFORMANCE: Store the result in cache for 12 hours
        // Shorter cache than before to balance freshness vs performance
        set_transient($cache_key, $all_fields, 12 * HOUR_IN_SECONDS);
        
        // Clear the "analyzing" flag
        delete_transient('muchat_meta_analysis_running');

        return $all_fields;
    }
    
    /**
     * Schedule background analysis of meta fields
     * 
     * PERFORMANCE: For large catalogs, this prevents timeout on admin page load.
     * Uses Action Scheduler (bundled with WooCommerce) for reliable background processing.
     *
     * @return array Status information
     */
    public static function schedule_meta_analysis()
    {
        // Check if Action Scheduler is available (bundled with WooCommerce)
        if (!function_exists('as_schedule_single_action')) {
            return [
                'success' => false,
                'message' => 'Action Scheduler not available',
            ];
        }
        
        // Check if already scheduled or running
        if (as_has_scheduled_action('muchat_analyze_meta_fields', [], 'muchat')) {
            return [
                'success' => true,
                'status' => 'scheduled',
                'message' => 'Analysis already scheduled',
            ];
        }
        
        // Check if we have cached results
        $cached = get_transient('muchat_product_meta_fields_cache');
        if ($cached !== false) {
            return [
                'success' => true,
                'status' => 'cached',
                'message' => 'Using cached analysis',
            ];
        }
        
        // Set "analyzing" flag
        set_transient('muchat_meta_analysis_running', true, 10 * MINUTE_IN_SECONDS);
        
        // Schedule background analysis
        as_schedule_single_action(
            time(),
            'muchat_analyze_meta_fields',
            [],
            'muchat'
        );
        
        return [
            'success' => true,
            'status' => 'scheduled',
            'message' => 'Background analysis scheduled',
        ];
    }
    
    /**
     * Generate meta fields in background
     * 
     * Called by Action Scheduler to perform the heavy analysis.
     */
    public static function generate_meta_fields_background()
    {
        $product = new self();
        $product->get_product_meta_fields();
    }
    
    /**
     * Check if meta analysis is currently running
     *
     * @return bool
     */
    public static function is_meta_analysis_running()
    {
        return get_transient('muchat_meta_analysis_running') !== false;
    }
    
    /**
     * Get meta analysis status
     *
     * @return array
     */
    public static function get_meta_analysis_status()
    {
        $cached = get_transient('muchat_product_meta_fields_cache');
        $is_running = self::is_meta_analysis_running();
        
        if ($cached !== false) {
            return [
                'status' => 'ready',
                'count' => count($cached),
            ];
        }
        
        if ($is_running) {
            return [
                'status' => 'analyzing',
            ];
        }
        
        return [
            'status' => 'not_started',
        ];
    }

    /**
     * Get sample values for a meta key from the database
     *
     * @param string $meta_key
     * @return array
     */
    private function get_meta_sample_values($meta_key)
    {
        global $wpdb;

        $values = $wpdb->get_col($wpdb->prepare("
        SELECT DISTINCT meta_value
        FROM {$wpdb->postmeta}
        WHERE meta_key = %s
        AND meta_value != ''
        LIMIT 5
    ", $meta_key));

        return array_filter(array_map(function ($value) {
            if (empty($value)) {
                return null;
            }

            // Handle serialized data
            if (is_serialized($value)) {
                return '[Serialized Data]';
            }

            // Handle arrays directly
            if (is_array($value)) {
                return '[Array Data]';
            }

            // Handle JSON
            if ($this->is_json($value)) {
                return '[JSON Data]';
            }

            // Handle long values
            if (strlen($value) > 50) {
                return substr($value, 0, 47) . '...';
            }

            return $value;
        }, $values));
    }

    /**
     * Get display name for meta key
     *
     * @param string $meta_key
     * @return string
     */
    private function get_meta_display_name($meta_key)
    {
        // Handle attribute taxonomies
        if (strpos($meta_key, 'pa_') === 0) {
            $taxonomy = get_taxonomy(wc_attribute_taxonomy_name(substr($meta_key, 3)));
            return $taxonomy ? $taxonomy->labels->singular_name : $meta_key;
        }

        // Handle attribute variations
        if (strpos($meta_key, 'attribute_') === 0) {
            return str_replace('attribute_', '', $meta_key);
        }

        return $meta_key;
    }

    /**
     * Check if a string is valid JSON
     *
     * @param string $string
     * @return boolean
     */
    private function is_json($string)
    {
        if (!is_string($string)) {
            return false;
        }

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

        json_decode($string);
        return (json_last_error() === JSON_ERROR_NONE);
    }
}
</file>

<file path="templates/admin/widget-settings.php">
<?php
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Safely get option value as string
 * 
 * SECURITY: Ensures option values are always returned as strings,
 * preventing potential issues with array values being passed to escaping functions.
 * 
 * @param string $option_name The option name to retrieve
 * @param string $default Default value if option doesn't exist
 * @return string The option value as a string
 */
function muchat_get_option_string($option_name, $default = '') {
    $value = get_option($option_name, $default);
    if (is_array($value)) {
        return $default;
    }
    return (string) $value;
}
?>
<div class="wrap">
    <h1><?php esc_html_e('Muchat Chatbot Settings', 'muchat-ai'); ?></h1>

    <?php
    // Show disconnection success notice
    if ($disconnected_notice):
    ?>
        <div class="notice notice-success is-dismissible">
            <p><?php esc_html_e('Successfully disconnected from Muchat.', 'muchat-ai'); ?></p>
        </div>
    <?php endif; ?>

    <?php if (!$is_muchat_working): ?>
        <div class="notice notice-info is-dismissible">
            <p><?php esc_html_e('Connect with Muchat to activate the chatbot on your website.', 'muchat-ai'); ?></p>
            <p>
                <a class="button button-primary" href="<?php echo esc_url($secure_link_display); ?>">
                    <?php esc_html_e('Connect with Muchat', 'muchat-ai'); ?>
                </a>
            </p>
        </div>
    <?php else: ?>
        <div class="notice notice-success is-dismissible">
            <p><span class="dashicons dashicons-yes-alt" style="vertical-align: text-bottom;"></span> <?php esc_html_e('Connected with Muchat. You can now use Muchat throughout your website.', 'muchat-ai'); ?></p>
            <?php if ($widget_enabled !== '1'): ?>
                <p><span class="dashicons dashicons-warning" style="vertical-align: text-bottom; color: #d63638;"></span> <?php esc_html_e('Widget is currently disabled. Enable it in settings below.', 'muchat-ai'); ?></p>
            <?php endif; ?>
        </div>
    <?php endif; ?>

    <?php if ($is_muchat_working): ?>
        <div class="card" style="width: 100%; max-width: 100%; margin-left: 0; box-sizing: border-box;">
            <h2><?php esc_html_e('Connection Settings', 'muchat-ai'); ?></h2>
            <div class="inside">
                <p><strong><?php esc_html_e('Agent ID', 'muchat-ai'); ?>:</strong> <code><?php echo esc_html(is_array($agent_id) ? '' : $agent_id); ?></code></p>

                <div class="action-buttons">
                    <a class="button" href="<?php echo esc_url($secure_link_display); ?>">
                        <?php esc_html_e('Reconfigure', 'muchat-ai'); ?>
                    </a>
                    <?php
                    $disconnect_url = wp_nonce_url(
                        admin_url('admin.php?page=muchat-settings&muchat_action=disconnect'),
                        'muchat_disconnect_action',
                        '_wpnonce'
                    );
                    ?>
                    <a class="button" href="<?php echo esc_url($disconnect_url); ?>" onclick="return confirm('<?php echo esc_js(__('Are you sure you want to disconnect from Muchat? This will remove your Agent ID.', 'muchat-ai')); ?>');">
                        <span style="color: #d63638;"><?php esc_html_e('Disconnect', 'muchat-ai'); ?></span>
                    </a>
                </div>

                <hr>

                <div class="customize-section">
                    <h3><?php esc_html_e('Widget Customization', 'muchat-ai'); ?></h3>
                    <p><?php esc_html_e('Customize your Muchat chatbot widget appearance in the Muchat dashboard', 'muchat-ai'); ?></p>
                    <p>
                        <?php 
                        // MAINTAINABILITY: Using centralized constant for App URL
                        $muchat_app_url = defined('MUCHAT_APP_URL') ? MUCHAT_APP_URL : 'https://app.mu.chat';
                        $customize_url = $muchat_app_url . '/agents/' . esc_attr(is_array($agent_id) ? '' : $agent_id) . '?tab=customize';
                        ?>
                        <a href="<?php echo esc_url($customize_url); ?>" class="button button-primary" target="_blank">
                            <span class="dashicons dashicons-admin-appearance" style="vertical-align: text-bottom;"></span>
                            <?php esc_html_e('Customize Widget in Muchat Panel', 'muchat-ai'); ?>
                        </a>
                    </p>
                </div>
            </div>
        </div>

        <form method="post" action="options.php">
            <?php settings_fields('muchat-settings-group'); ?>
            <?php do_settings_sections('muchat-settings-group'); ?>
            <?php wp_nonce_field('muchat_ai_chatbot_settings_action', 'muchat_ai_chatbot_settings_nonce'); ?>

            <!-- Add hidden input for agent_id -->
            <input type="hidden" name="muchat_ai_chatbot_agent_id" value="<?php echo esc_attr(is_array($agent_id) ? '' : $agent_id); ?>" />

            <div class="metabox-holder">
                <!-- Widget Display Settings -->
                <div class="postbox">
                    <div class="postbox-header">
                        <h2 class="hndle"><?php esc_html_e('Widget Display', 'muchat-ai'); ?></h2>
                    </div>
                    <div class="inside">
                        <p><?php esc_html_e('Control the visibility of the Muchat chatbot widget on your site', 'muchat-ai'); ?></p>
                        <table class="form-table">
                            <tr valign="top">
                                <th scope="row"><?php esc_html_e('Display Status', 'muchat-ai'); ?></th>
                                <td>
                                    <label>
                                        <input type="checkbox" name="muchat_ai_chatbot_widget_enabled" value="1" <?php checked($widget_enabled, '1'); ?> />
                                        <?php esc_html_e('Enable Muchat Widget', 'muchat-ai'); ?>
                                    </label>
                                    <p class="description">
                                        <?php esc_html_e('You can temporarily disable the Muchat chatbot widget without losing your settings.', 'muchat-ai'); ?>
                                    </p>
                                </td>
                            </tr>
                        </table>
                    </div>
                </div>

                <!-- Contact Information Settings -->
                <div class="postbox">
                    <div class="postbox-header">
                        <h2 class="hndle"><?php esc_html_e('Contact Information', 'muchat-ai'); ?></h2>
                    </div>
                    <div class="inside">
                        <p><?php esc_html_e('User information for personalizing the conversation experience (optional)', 'muchat-ai'); ?></p>
                        <table class="form-table">
                            <tr valign="top">
                                <th scope="row"><?php esc_html_e('User Info', 'muchat-ai'); ?></th>
                                <td>
                                    <label>
                                        <input type="checkbox" id="muchat_ai_chatbot_use_logged_in_user_info" name="muchat_ai_chatbot_use_logged_in_user_info" value="1" <?php checked($use_logged_in_user_info, '1'); ?> />
                                        <?php esc_html_e('Automatically send logged-in user information', 'muchat-ai'); ?>
                                    </label>
                                    <p class="description">
                                        <?php esc_html_e('When enabled, the plugin will send the name and email of logged-in users to personalize chat interactions.', 'muchat-ai'); ?>
                                    </p>
                                </td>
                            </tr>
                        </table>
                    </div>
                </div>

                <!-- Interface Settings -->
                <div class="postbox">
                    <div class="postbox-header">
                        <h2 class="hndle"><?php esc_html_e('Interface Settings', 'muchat-ai'); ?></h2>
                    </div>
                    <div class="inside">
                        <p><?php esc_html_e('Customize the appearance and initial messages of the chatbot', 'muchat-ai'); ?></p>
                        <table class="form-table">
                            <tr valign="top">
                                <th scope="row"><?php esc_html_e('Initial Messages', 'muchat-ai'); ?></th>
                                <td>
                                    <div id="muchat-initial-messages-list"></div>
                                    <button type="button" class="button" id="muchat-add-initial-message">
                                        <span class="dashicons dashicons-plus-alt2" style="vertical-align: text-bottom;"></span>
                                        <?php esc_html_e('Add Message', 'muchat-ai'); ?>
                                    </button>
                                    <input type="hidden" id="muchat_ai_chatbot_interface_initial_messages" name="muchat_ai_chatbot_interface_initial_messages" value="<?php echo esc_attr(is_array($interface_initial_messages) ? wp_json_encode($interface_initial_messages) : $interface_initial_messages); ?>" />
                                    <p class="description">
                                        <?php esc_html_e('Add welcome messages that will be shown when a visitor opens the chat widget. You can include personalization variables such as', 'muchat-ai'); ?> <code>$name</code> <?php esc_html_e('and', 'muchat-ai'); ?> <code>$lastname</code> (<?php esc_html_e('example:', 'muchat-ai'); ?> <code>"Hello $name, welcome to our site!"</code>). <?php esc_html_e('Add multiple messages to create a conversation flow (each message will appear in sequence). If left empty, default settings from your Muchat dashboard will be used.', 'muchat-ai'); ?>
                                    </p>
                                </td>
                            </tr>
                             <tr valign="top">
                                <th scope="row"><?php esc_html_e('Different Messages for Guests', 'muchat-ai'); ?></th>
                                <td>
                                    <label>
                                        <input type="checkbox" id="muchat_ai_chatbot_enable_guest_messages" name="muchat_ai_chatbot_enable_guest_messages" value="1" <?php checked($enable_guest_messages, '1'); ?> />
                                        <?php esc_html_e('Enable different initial messages for non-logged-in users.', 'muchat-ai'); ?>
                                    </label>
                                </td>
                            </tr>
                            <tr valign="top" id="muchat-guest-initial-messages-row" style="display: none;">
                                <th scope="row"><?php esc_html_e('Guest Initial Messages', 'muchat-ai'); ?></th>
                                <td>
                                    <div id="muchat-guest-initial-messages-list"></div>
                                    <button type="button" class="button" id="muchat-add-guest-initial-message">
                                        <span class="dashicons dashicons-plus-alt2" style="vertical-align: text-bottom;"></span>
                                        <?php esc_html_e('Add Message', 'muchat-ai'); ?>
                                    </button>
                                    <input type="hidden" id="muchat_ai_chatbot_guest_initial_messages" name="muchat_ai_chatbot_guest_initial_messages" value="<?php echo esc_attr(is_array($guest_initial_messages) ? wp_json_encode($guest_initial_messages) : $guest_initial_messages); ?>" />
                                    <p class="description">
                                        <?php esc_html_e('These messages will be shown to visitors who are not logged in. If left empty, the default messages above will be used.', 'muchat-ai'); ?>
                                    </p>
                                </td>
                            </tr>
                            <tr valign="top">
                                <th scope="row"><?php esc_html_e('Load Strategy', 'muchat-ai'); ?></th>
                                <td>
                                    <select name="muchat_ai_chatbot_load_strategy" class="regular-text">
                                        <option value="FAST" <?php selected($load_strategy, 'FAST'); ?>><?php esc_html_e('Fast (Recommended)', 'muchat-ai'); ?></option>
                                        <option value="SEO_FRIENDLY" <?php selected($load_strategy, 'SEO_FRIENDLY'); ?>><?php esc_html_e('SEO Friendly', 'muchat-ai'); ?></option>
                                        <option value="COMPLETE_LOAD" <?php selected($load_strategy, 'COMPLETE_LOAD'); ?>><?php esc_html_e('Complete Load', 'muchat-ai'); ?></option>
                                    </select>
                                    <p class="description">
                                        <?php esc_html_e('Controls how the chatbot loads on your website. Fast is recommended for most sites.', 'muchat-ai'); ?>
                                    </p>
                                </td>
                            </tr>
                            <tr valign="top">
                                <th scope="row"><?php esc_html_e('Script Load Position', 'muchat-ai'); ?></th>
                                <td>
                                    <select name="muchat_ai_chatbot_script_position" class="regular-text" id="muchat_ai_chatbot_script_position">
                                        <option value="footer" <?php selected($script_position, 'footer'); ?>><?php esc_html_e('Footer (Recommended)', 'muchat-ai'); ?></option>
                                        <option value="head" <?php selected($script_position, 'head'); ?>><?php esc_html_e('Header', 'muchat-ai'); ?></option>
                                        <option value="body_open" <?php selected($script_position, 'body_open'); ?>><?php esc_html_e('Body Opening', 'muchat-ai'); ?></option>
                                    </select>
                                    <p class="description">
                                        <strong><?php esc_html_e('Footer (Recommended):', 'muchat-ai'); ?></strong> <?php esc_html_e('Best for page speed and mobile compatibility. Loads after the page content.', 'muchat-ai'); ?><br>
                                        <strong><?php esc_html_e('Header:', 'muchat-ai'); ?></strong> <?php esc_html_e('Loads in the <head> section. Use if you need the widget available as early as possible.', 'muchat-ai'); ?><br>
                                        <strong><?php esc_html_e('Body Opening:', 'muchat-ai'); ?></strong> <?php esc_html_e('Loads right after <body> tag. Requires WordPress 5.2+ and modern theme support.', 'muchat-ai'); ?>
                                    </p>
                                </td>
                            </tr>
                        </table>
                    </div>
                </div>

                <!-- Dynamic Prompt Settings -->
                <div class="postbox">
                    <div class="postbox-header">
                        <h2 class="hndle"><?php esc_html_e('Dynamic Prompt', 'muchat-ai'); ?></h2>
                    </div>
                    <div class="inside">
                        <p><?php esc_html_e('Configure the context prompt that will be sent to the AI model', 'muchat-ai'); ?></p>
                        <table class="form-table">
                            <tr valign="top">
                                <th scope="row"><?php esc_html_e('Context for Logged-in Users', 'muchat-ai'); ?></th>
                                <td>
                                    <textarea name="muchat_ai_chatbot_context" rows="3" cols="50" class="large-text code"><?php echo esc_textarea(is_array($context) ? '' : $context); ?></textarea>
                                    <p class="description">
                                        <?php esc_html_e('Provide additional context that will be appended to the Agent system prompt.', 'muchat-ai'); ?>
                                    </p>
                                    <p class="description">
                                        <?php esc_html_e('You can use variables like', 'muchat-ai'); ?> <code>$name</code>, <code>$lastname</code>, <code>$email</code>, <code>$phone</code> <?php esc_html_e('and', 'muchat-ai'); ?> <code>$page_title</code> <?php esc_html_e('in the context. Example:', 'muchat-ai'); ?> <code><?php esc_html_e('The user you are talking to is $name $lastname (Email: $email, Phone: $phone). They are currently on the page: $page_title. Start by greeting them by name.', 'muchat-ai'); ?></code>
                                    </p>
                                </td>
                            </tr>
                            <tr valign="top">
                                <th scope="row"><?php esc_html_e('Different Context for Guests', 'muchat-ai'); ?></th>
                                <td>
                                    <label>
                                        <input type="checkbox" id="muchat_ai_chatbot_enable_guest_context" name="muchat_ai_chatbot_enable_guest_context" value="1" <?php checked($enable_guest_context, '1'); ?> />
                                        <?php esc_html_e('Enable different context for non-logged-in users.', 'muchat-ai'); ?>
                                    </label>
                                </td>
                            </tr>
                            <tr valign="top" id="muchat-guest-context-row" style="display: none;">
                                <th scope="row"><?php esc_html_e('Context for Guests', 'muchat-ai'); ?></th>
                                <td>
                                    <textarea name="muchat_ai_chatbot_guest_context" rows="3" cols="50" class="large-text code"><?php echo esc_textarea(is_array($guest_context) ? '' : $guest_context); ?></textarea>
                                    <p class="description">
                                        <?php esc_html_e('This context will be used for visitors who are not logged in. If left empty, no context will be sent for guests.', 'muchat-ai'); ?>
                                    </p>
                                    <p class="description">
                                        <?php esc_html_e('Example:', 'muchat-ai'); ?> <code>A visitor is on the page: $page_title. Offer them a 10% discount with the code WELCOME10.</code>
                                    </p>
                                </td>
                            </tr>
                        </table>
                    </div>
                </div>

                <!-- Display Rules Settings -->
                <div class="postbox">
                    <div class="postbox-header">
                        <h2 class="hndle"><?php esc_html_e('Display Rules', 'muchat-ai'); ?></h2>
                    </div>
                    <div class="inside">
                        <p><?php esc_html_e('Specify which pages the Muchat chatbot should appear on or be hidden from', 'muchat-ai'); ?></p>
                        <table class="form-table">
                            <tr valign="top">
                                <th scope="row"><?php esc_html_e('Display Mode', 'muchat-ai'); ?></th>
                                <td>
                                    <fieldset>
                                        <label>
                                            <input type="radio" name="muchat_ai_chatbot_visibility_mode" value="all" <?php checked(get_option('muchat_ai_chatbot_visibility_mode', 'all'), 'all'); ?> />
                                            <?php esc_html_e('Show on all pages except those listed below', 'muchat-ai'); ?>
                                        </label>
                                        <br />
                                        <label>
                                            <input type="radio" name="muchat_ai_chatbot_visibility_mode" value="specific" <?php checked(get_option('muchat_ai_chatbot_visibility_mode', 'all'), 'specific'); ?> />
                                            <?php esc_html_e('Only show on pages listed below', 'muchat-ai'); ?>
                                        </label>
                                    </fieldset>
                                </td>
                            </tr>
                            <tr valign="top">
                                <th scope="row"><?php esc_html_e('Page List', 'muchat-ai'); ?></th>
                                <td>
                                    <textarea name="muchat_ai_chatbot_visibility_pages" rows="6" cols="50" class="large-text code" dir="auto"><?php
                                                                                                                                    $visibility_pages = get_option('muchat_ai_chatbot_visibility_pages', '');
                                                                                                                                    echo esc_textarea(is_array($visibility_pages) ? '' : htmlspecialchars_decode($visibility_pages));
                                                                                                                                    ?></textarea>
                                    <p class="description">
                                        <?php esc_html_e('One pattern per line. Use', 'muchat-ai'); ?> <code>&lt;front&gt;</code> <?php esc_html_e('for homepage,', 'muchat-ai'); ?> <code>*</code> <?php esc_html_e('for wildcards.', 'muchat-ai'); ?>
                                    </p>
                                    <p class="description">
                                        <?php esc_html_e('English examples:', 'muchat-ai'); ?> <code>/about</code>, <code>/blog/*</code>, <code>/product/shoes</code>
                                    </p>
                                    <p class="description">
                                        <?php esc_html_e('Persian/RTL examples:', 'muchat-ai'); ?> <code>/محصول/کفش</code>, <code>/دسته-بندی/*</code>, <code>/تماس-با-ما</code>
                                    </p>
                                    <p class="description">
                                        <small><?php esc_html_e('URLs are case-insensitive (/About = /about). Persian/Arabic URLs are fully supported.', 'muchat-ai'); ?></small>
                                    </p>
                                </td>
                            </tr>
                        </table>
                        
                        <!-- Cache Notice -->
                        <div class="notice notice-info inline" style="margin: 15px 0 0; padding: 10px 15px;">
                            <p style="margin: 0;">
                                <span class="dashicons dashicons-info" style="margin-right: 5px;"></span>
                                <?php esc_html_e('If you use a caching plugin and display rules don\'t work correctly, please clear your site cache.', 'muchat-ai'); ?>
                            </p>
                        </div>
                    </div>
                </div>

                <!-- Schedule Settings -->
                <div class="postbox">
                    <div class="postbox-header">
                        <h2 class="hndle"><?php esc_html_e('Schedule Settings', 'muchat-ai'); ?></h2>
                    </div>
                    <div class="inside">
                        <p><?php esc_html_e('Configure when the Muchat chatbot should be available on your website', 'muchat-ai'); ?></p>

                        <table class="form-table">
                            <tr valign="top">
                                <th scope="row"><?php esc_html_e('Enable Schedule', 'muchat-ai'); ?></th>
                                <td>
                                    <label>
                                        <input type="checkbox" id="muchat_ai_chatbot_schedule_enabled" name="muchat_ai_chatbot_schedule_enabled" value="1" <?php checked($schedule_enabled, '1'); ?> />
                                        <?php esc_html_e('Enable scheduling', 'muchat-ai'); ?>
                                    </label>
                                    <p class="description">
                                        <?php esc_html_e('When enabled, the chatbot will only be available during specified days and times.', 'muchat-ai'); ?>
                                    </p>
                                </td>
                            </tr>
                            <tr valign="top" class="schedule-options" style="display: <?php echo $schedule_enabled === '1' ? 'table-row' : 'none'; ?>;">
                                <th scope="row"><?php esc_html_e('Active Days', 'muchat-ai'); ?></th>
                                <td>
                                    <?php
                                    $days = array(
                                        'saturday' => esc_html__('Saturday', 'muchat-ai'),
                                        'sunday' => esc_html__('Sunday', 'muchat-ai'),
                                        'monday' => esc_html__('Monday', 'muchat-ai'),
                                        'tuesday' => esc_html__('Tuesday', 'muchat-ai'),
                                        'wednesday' => esc_html__('Wednesday', 'muchat-ai'),
                                        'thursday' => esc_html__('Thursday', 'muchat-ai'),
                                        'friday' => esc_html__('Friday', 'muchat-ai')
                                    );

                                    // Ensure schedule_days is an array
                                    $schedule_days = get_option('muchat_ai_chatbot_schedule_days', array());
                                    if (!is_array($schedule_days)) {
                                        $schedule_days = array();
                                    }

                                    foreach ($days as $day => $label) {
                                    ?>
                                        <label style="display: inline-block; margin-right: 15px;">
                                            <input type="checkbox" name="muchat_ai_chatbot_schedule_days[]" value="<?php echo esc_attr($day); ?>" <?php checked(in_array($day, $schedule_days)); ?> />
                                            <?php echo esc_html($label); ?>
                                        </label>
                                    <?php
                                    }
                                    ?>
                                    <p class="description">
                                        <?php esc_html_e('Select the days when the chatbot should be available. If no days are selected, the chatbot will be available every day.', 'muchat-ai'); ?>
                                    </p>
                                </td>
                            </tr>
                            <tr valign="top" class="schedule-options" style="display: <?php echo $schedule_enabled === '1' ? 'table-row' : 'none'; ?>;">
                                <th scope="row"><?php esc_html_e('Active Hours', 'muchat-ai'); ?></th>
                                <td>
                                    <input type="time" name="muchat_ai_chatbot_schedule_start_time" value="<?php echo esc_attr(is_array($schedule_start_time) ? '' : $schedule_start_time); ?>" />
                                    <?php esc_html_e('to', 'muchat-ai'); ?>
                                    <input type="time" name="muchat_ai_chatbot_schedule_end_time" value="<?php echo esc_attr(is_array($schedule_end_time) ? '' : $schedule_end_time); ?>" />
                                    <p class="description">
                                        <?php esc_html_e('Set the time range during which the chatbot should be available. If no time is set, the chatbot will be available 24/7.', 'muchat-ai'); ?>
                                        <?php esc_html_e('Times are based on your website\'s timezone:', 'muchat-ai'); ?> <strong><?php echo esc_html(is_array($current_timezone) ? '' : $current_timezone); ?></strong>
                                    </p>
                                </td>
                            </tr>
                        </table>
                    </div>
                </div>
            </div>

            <p class="submit">
                <?php submit_button(esc_html__('Save Settings', 'muchat-ai'), 'primary', 'submit', false); ?>
            </p>
        </form>
    <?php endif; ?>
</div>
</file>

<file path="includes/Admin/Settings.php">
<?php

namespace Muchat\Api\Admin;

class Settings
{
    /**
     * Product model instance
     * 
     * @var \Muchat\Api\Models\Product
     */
    protected $product_model;
    
    /**
     * Verify request nonce with multiple possible sources
     * 
     * SECURITY: Consolidates nonce verification logic for consistency
     * 
     * @param string $action The nonce action to verify
     * @param array $nonce_keys Array of possible GET/POST keys containing the nonce
     * @return bool True if valid nonce found
     */
    private function verify_request_nonce($action, $nonce_keys = ['_wpnonce', 'muchat_nonce'])
    {
        foreach ($nonce_keys as $key) {
            // Check GET parameters
            if (isset($_GET[$key])) {
                $nonce = sanitize_key(wp_unslash($_GET[$key]));
                if (wp_verify_nonce($nonce, $action)) {
                    return true;
                }
            }
            // Check POST parameters
            if (isset($_POST[$key])) {
                $nonce = sanitize_key(wp_unslash($_POST[$key]));
                if (wp_verify_nonce($nonce, $action)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * PERFORMANCE: Pre-encoded SVG icon to avoid Disk I/O on every page load
     * This is the base64-encoded version of assets/images/icon.svg
     * 
     * The SVG contains only safe elements (path, g) with no scripts or external references.
     * Re-generate this constant if the icon file changes using:
     * cat assets/images/icon.svg | base64 | tr -d '\n'
     */
    private const MENU_ICON_BASE64 = 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMDggMTA4IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8ZyBmaWxsPSJjdXJyZW50Q29sb3IiIGZpbGwtcnVsZT0ibm9uemVybyI+CiAgICAgICAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjYuNzY5NiwgNDAuODQ2OSkiPgogICAgICAgICAgICA8cGF0aAogICAgICAgICAgICAgICAgZD0iTTYwLjExMTcyOTcsLTIuMzk4OTk4NTllLTI0IEM2MC4wMjA3NTY4LDQuNjM4NTUxMzUgNTguOTg3OTQ1OSw5LjAxNDg4NjQ5IDU3LjE1ODg1NDEsMTMuMDA1OTI0MyBDNjYuODAwOTE4OSwxNC42MTI0IDY2Ljg3OTA0ODYsMjIuNDY0OTczIDY2Ljg3OTA0ODYsMjYuMDQ1MDI3IEM2Ni44NzkwNDg2LDI5LjM2MzkzNTEgNjYuODc5MDQ4NiwzOS40MjM0MDU0IDUzLjUxMzUxMzUsMzkuNDIzNDA1NCBMNDAuMTIyMjkxOSwzOS40MjM0MDU0IEMzMS40MDcwODExLDM5LjQyMzQwNTQgMjMuNjA4MDIxNiw0My42MzA2Mzc4IDE4LjcyMjIzNzgsNTAuMTM2ODEwOCBDMTUuNDgyNTI5Nyw0Ny42ODA1NDA1IDEzLjM2NTUzNTEsNDMuNzg2ODk3MyAxMy4zNjU1MzUxLDM5LjQyMzQwNTQgTDEzLjM2NTUzNTEsMjYuMDQ1MDI3IEMxMy4zNjU1MzUxLDIzLjM5OTMxODkgMTMuODM1MzgzOCwyMS4zMTU1MDI3IDE0LjU2NzQ0ODYsMTkuNTk3NzE4OSBDMTQuMTc1NzI5NywxOS41MjYwMTA4IDEzLjc4NDAxMDgsMTkuMzU1ODM3OCAxMy4zNjU1MzUxLDE5LjM1NTgzNzggTDAuNjY2Nzc4Mzc4LDE5LjM1NTgzNzggQzAuMjM2NTI5NzMsMjEuNDI2ODEwOCAyLjM5ODk5ODU5ZS0yNCwyMy42NDEyIDIuMzk4OTk4NTllLTI0LDI2LjA0NTAyNyBMMi4zOTg5OTg1OWUtMjQsMzkuNDIzNDA1NCBDMi4zOTg5OTg1OWUtMjQsNTQuMjAwNjI3IDExLjk2Nzc2MjIsNjYuMTgwMTYyMiAyNi43NDM5MTM1LDY2LjE4MDE2MjIgQzI2Ljc0MzkxMzUsNTguNzg1NjY0OSAzMi43Mjc3OTQ2LDUyLjgwMTc4MzggNDAuMTIyMjkxOSw1Mi44MDE3ODM4IEw1My41MTM1MTM1LDUyLjgwMTc4MzggQzY2Ljg3OTA0ODYsNTIuODAxNzgzOCA4MC4yNTc0MjcsNDUuMjc2NzEzNSA4MC4yNTc0MjcsMjYuMDQ1MDI3IEM4MC4yNTc0MjcsOS42ODE2NjQ4NiA3MC45NTU3MDgxLDIuMDk3NzI5NzMgNjAuMTExNzI5NywtMi4zOTg5OTg1OWUtMjQgWiIgLz4KICAgICAgICA8L2c+CiAgICAgICAgPHBhdGgKICAgICAgICAgICAgZD0iTTUzLjUxMzUxMzUsMTMuMzc4Mzc4NCBDNTguNTA0MTgzOCwxMy4zNzgzNzg0IDY2Ljg5MTg5MTksMTUuMTE1NDI3IDY2Ljg5MTg5MTksMjYuNzU2NzU2OCBMNjYuODkxODkxOSw0MC4xMzUxMzUxIEM2Ni44OTE4OTE5LDQ0LjUxMjU0MDUgNjQuNzc0ODk3Myw0OC4zOTg2OTE5IDYxLjUzNTE4OTIsNTAuODQyMTE4OSBDNTYuNjQ5NDA1NCw0NC4zNDg3ODkyIDQ4Ljg2MjExODksNDAuMTM1MTM1MSA0MC4xMzUxMzUxLDQwLjEzNTEzNTEgTDI2Ljc2OTYsNDAuMTM1MTM1MSBDMTMuMzc4Mzc4NCw0MC4xMzUxMzUxIDEzLjM3ODM3ODQsMzAuMDY5MjQzMiAxMy4zNzgzNzg0LDI2Ljc1Njc1NjggQzEzLjM3ODM3ODQsMjIuNzU5Mjk3MyAxMy4zNzgzNzg0LDEzLjM3ODM3ODQgMjYuNzY5NiwxMy4zNzgzNzg0IEw1My41MTM1MTM1LDEzLjM3ODM3ODQgTTUzLjUxMzUxMzUsMCBMMjYuNzY5NiwwIEMxMy4zNzgzNzg0LDAgMCw3LjEwNzY2NDg2IDAsMjYuNzU2NzU2OCBDMCw0NS45ODg0NDMyIDEzLjM3ODM3ODQsNTMuNTEzNTEzNSAyNi43Njk2LDUzLjUxMzUxMzUgTDQwLjEzNTEzNTEsNTMuNTEzNTEzNSBDNDcuNTI5NjMyNCw1My41MTM1MTM1IDUzLjUxMzUxMzUsNTkuNTExMzA4MSA1My41MTM1MTM1LDY2Ljg5MTg5MTkgQzY4LjI5MDczNTEsNjYuODkxODkxOSA4MC4yODMxMTM1LDU0LjkxMjM1NjggODAuMjgzMTEzNSw0MC4xMzUxMzUxIEw4MC4yODMxMTM1LDI2Ljc1Njc1NjggQzgwLjI4MzExMzUsOC4yODkyNDMyNCA2Ni44OTE4OTE5LDAgNTMuNTEzNTEzNSwwIEw1My41MTM1MTM1LDAgWiIgLz4KICAgIDwvZz4KPC9zdmc+';

    /**
     * Initialize the class
     */
    public function __construct()
    {
        $this->product_model = new \Muchat\Api\Models\Product();
        add_action('admin_enqueue_scripts', [$this, 'enqueue_styles']);
        add_action('admin_enqueue_scripts', [$this, 'enqueue_scripts']);
        
        // Register AJAX handlers for API tester functionality
        add_action('wp_ajax_muchat_api_search_content', [$this, 'ajax_search_content']);
        add_action('wp_ajax_muchat_api_preview', [$this, 'ajax_preview_content']);
        
        // Hide API Documentation menu item
        add_action('admin_head', [$this, 'hide_api_documentation_menu_css']);
    }

    /**
     * Hide API Documentation menu item via CSS
     */
    public function hide_api_documentation_menu_css()
    {
        echo '<style>
            #adminmenu a[href="admin.php?page=muchat-api-documentation"],
            #adminmenu a[href="admin.php?page=muchat-api-documentation"] + ul,
            #adminmenu .wp-submenu a[href="admin.php?page=muchat-api-documentation"] {
                display: none !important;
            }
        </style>';
    }

    /**
     * Get the SVG icon for the menu
     * 
     * PERFORMANCE: Uses pre-encoded base64 constant to avoid disk I/O
     * SECURITY: SVG content has been validated to contain only safe elements
     * 
     * @return string Base64-encoded SVG data URI
     */
    private function muchat_ai_chatbot_get_menu_icon()
    {
        // Use pre-encoded constant for performance (no disk I/O)
        return 'data:image/svg+xml;base64,' . self::MENU_ICON_BASE64;
    }

    public function get_meta_field_label($meta_key)
    {
        return $this->product_model->get_meta_field_label($meta_key);
    }

    /**
     * Register the stylesheets for the admin area
     *
     * @return void
     */
    public function enqueue_styles($hook)
    {
        if (
            'toplevel_page_muchat-settings' !== $hook &&
            'muchat_page_muchat-woocommerce-api' !== $hook &&
            'muchat_page_muchat-product-example' !== $hook &&
            'muchat_page_muchat-documentation' !== $hook &&
            'muchat_page_muchat-api-documentation' !== $hook
        ) {
            return;
        }

        // PERFORMANCE: Use file modification time for better cache busting
        $css_file = MUCHAT_AI_CHATBOT_PLUGIN_PATH . 'assets/css/admin.css';
        $version = MUCHAT_AI_CHATBOT_PLUGIN_VERSION;
        if (file_exists($css_file)) {
            $version .= '.' . filemtime($css_file);
        }

        // Main plugin admin style
        wp_enqueue_style(
            'muchat-admin',
            MUCHAT_AI_CHATBOT_PLUGIN_URL . 'assets/css/admin.css',
            [],
            $version
        );

        // For API Documentation page, ensure WP core admin styles are properly loaded
        if ('muchat_page_muchat-documentation' === $hook || 'muchat_page_muchat-api-documentation' === $hook) {
            wp_enqueue_style('wp-admin');
            wp_enqueue_style('dashboard');
            wp_enqueue_style('list-tables');
        }
    }

    /**
     * Register the JavaScript for the admin area
     *
     * @return void
     */
    public function enqueue_scripts($hook)
    {
        if (
            'toplevel_page_muchat-settings' !== $hook &&
            'muchat_page_muchat-woocommerce-api' !== $hook &&
            'muchat_page_muchat-product-example' !== $hook &&
            'muchat_page_muchat-documentation' !== $hook &&
            'muchat_page_muchat-api-documentation' !== $hook
        ) {
            return;
        }

        // PERFORMANCE: Use file modification time for better cache busting
        $js_file = MUCHAT_AI_CHATBOT_PLUGIN_PATH . 'assets/js/admin.js';
        $js_version = MUCHAT_AI_CHATBOT_PLUGIN_VERSION;
        if (file_exists($js_file)) {
            $js_version .= '.' . filemtime($js_file);
        }

        wp_enqueue_script(
            'muchat-admin',
            MUCHAT_AI_CHATBOT_PLUGIN_URL . 'assets/js/admin.js',
            ['jquery'],
            $js_version,
            true
        );

        wp_localize_script('muchat-admin', 'muchatPluginAdmin', [
            'copiedText' => __('Copied!', 'muchat-ai'),
            'failedText' => __('Failed!', 'muchat-ai')
        ]);

        // Also localize for API tester functionality
        wp_localize_script('muchat-admin', 'muchatApiAdmin', [
            'nonce' => wp_create_nonce('muchat_api_nonce'),
            'copiedText' => __('Copied!', 'muchat-ai')
        ]);

        // For API Documentation page, ensure WP core admin scripts are properly loaded
        if ('muchat_page_muchat-documentation' === $hook || 'muchat_page_muchat-api-documentation' === $hook) {
            wp_enqueue_script('common');
            wp_enqueue_script('postbox');
        }
    }


    /**
     * Add menu page and submenus
     *
     * @return void
     */
    public function add_menu_page()
    {
        // Main Menu Item
        add_menu_page(
            __('Muchat Settings', 'muchat-ai'),
            __('Muchat', 'muchat-ai'),
            'manage_options',
            'muchat-settings',
            [$this, 'render_widget_settings_page'],
            $this->muchat_ai_chatbot_get_menu_icon()
        );

        // Widget Settings Submenu (First item)
        add_submenu_page(
            'muchat-settings',
            __('Widget Settings', 'muchat-ai'),
            __('Widget Settings', 'muchat-ai'),
            'manage_options',
            'muchat-settings',
            [$this, 'render_widget_settings_page']
        );

        // WooCommerce API Settings Submenu (Only if WooCommerce is active)
        if (class_exists('WooCommerce')) {
            add_submenu_page(
                'muchat-settings',
                __('WooCommerce API', 'muchat-ai'),
                __('WooCommerce API', 'muchat-ai'),
                'manage_options',
                'muchat-woocommerce-api',
                [$this, 'render_settings_page']
            );

            // Product Example Submenu (Only if WooCommerce is active)
            add_submenu_page(
                'muchat-settings',
                __('Product Example', 'muchat-ai'),
                __('Product Example', 'muchat-ai'),
                'manage_options',
                'muchat-product-example',
                [$this, 'render_product_example_page']
            );
        }


        // API Documentation page
        // We use 'muchat-settings' as parent but hide it via CSS
        // This ensures the page is registered correctly and accessible via direct link
        // without causing permission issues.
        add_submenu_page(
            'muchat-settings',
            __('API Documentation', 'muchat-ai'),
            __('API Documentation', 'muchat-ai'),
            'manage_options',
            'muchat-api-documentation',
            [$this, 'render_api_documentation_page']
        );
    }

    /**
     * Clear widget cache
     */
    private function muchat_ai_chatbot_clear_widget_cache()
    {
        \Muchat\Api\Utils\Cache::clear_widget_cache();
    }

    public function render_widget_settings_page()
    {
        if (!current_user_can('manage_options')) {
            return;
        }

        // SECURITY: Handle URL parameters with nonce verification for agent ID changes
        // The agentId can come from Muchat callback, so we need to verify the muchat_nonce
        if (isset($_GET["agentId"]) && !empty($_GET["agentId"])) {
            // Use unified nonce verification method
            if ($this->verify_request_nonce('muchat_connect_action') || 
                $this->verify_request_nonce('muchat_set_agent_id')) {
                $sanitized_agent_id = sanitize_text_field(wp_unslash($_GET["agentId"]));
                update_option("muchat_ai_chatbot_agent_id", $sanitized_agent_id);

                // Clear widget cache when agent ID changes
                $this->muchat_ai_chatbot_clear_widget_cache();
            } else {
                // Log potential CSRF attempt
                muchat_log('Blocked potential CSRF attempt to change agent ID', 'warning');
                
                // Show admin notice
                add_settings_error(
                    'muchat_settings',
                    'nonce_failed',
                    __('Security verification failed. Please try again.', 'muchat-ai'),
                    'error'
                );
            }
        }

        // SECURITY: Handle verification parameter with nonce check
        $verify_param = null;
        if (isset($_GET["muchat_verify"]) && !empty($_GET["muchat_verify"])) {
            if ($this->verify_request_nonce('muchat_connect_action') || 
                $this->verify_request_nonce('muchat_set_verify')) {
                $verify_param = sanitize_text_field(wp_unslash($_GET["muchat_verify"]));
                update_option("muchat_ai_chatbot_verify", $verify_param);
            }
        } elseif (isset($_GET["muchat_ai_chatbot_verify"]) && !empty($_GET["muchat_ai_chatbot_verify"])) {
            // Legacy parameter - also requires nonce
            if ($this->verify_request_nonce('muchat_connect_action')) {
                $verify_param = sanitize_text_field(wp_unslash($_GET["muchat_ai_chatbot_verify"]));
                update_option("muchat_ai_chatbot_verify", $verify_param);
            }
        }

        // Handle disconnect action
        if (isset($_GET["muchat_action"]) && $_GET["muchat_action"] === "disconnect") {
            if (isset($_GET['_wpnonce'])) {
                $nonce = sanitize_text_field(wp_unslash($_GET['_wpnonce']));
                if (wp_verify_nonce($nonce, 'muchat_disconnect_action')) {
                    delete_option("muchat_ai_chatbot_agent_id");
                    delete_option("muchat_ai_chatbot_verify");
                    $this->muchat_ai_chatbot_clear_widget_cache();

                    // Redirect to the settings page to show the disconnected state
                    wp_safe_redirect(admin_url('admin.php?page=muchat-settings&disconnected=1'));
                    exit;
                }
            }
        }

        // Show a notice when disconnected
        $disconnected_notice = isset($_GET['disconnected']) ? true : false;

        // Security check for connections
        if (isset($_GET["connect"]) && $_GET["connect"] == "1") {
            check_admin_referer('muchat_connect_action', 'muchat_nonce');
        }

        // Get settings
        $agent_id = get_option('muchat_ai_chatbot_agent_id');
        $context = get_option('muchat_ai_chatbot_context', '');
        $interface_initial_messages = get_option('muchat_ai_chatbot_interface_initial_messages', '');
        $enable_guest_messages = get_option('muchat_ai_chatbot_enable_guest_messages', '0');
        $guest_initial_messages = get_option('muchat_ai_chatbot_guest_initial_messages', '');
        $enable_guest_context = get_option('muchat_ai_chatbot_enable_guest_context', '0');
        $guest_context = get_option('muchat_ai_chatbot_guest_context', '');
        $load_strategy = get_option('muchat_ai_chatbot_load_strategy', 'FAST');
        $script_position = get_option('muchat_ai_chatbot_script_position', 'footer');
        $use_logged_in_user_info = get_option('muchat_ai_chatbot_use_logged_in_user_info', '');
        $widget_enabled = get_option('muchat_ai_chatbot_widget_enabled', '1');

        $is_muchat_working = isset($agent_id) && !empty($agent_id);

        // Build callback URL
        $is_secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on';
        $protocol = $is_secure ? "https://" : "http://";
        $host = isset($_SERVER['HTTP_HOST']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'])) : '';
        $request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
        $http_callback = esc_url($protocol . $host . $request_uri);

        // Build muchat configuration URL
        // MAINTAINABILITY: Using centralized constant for App URL
        $base_url = defined('MUCHAT_APP_URL') ? MUCHAT_APP_URL : 'https://app.mu.chat';
        $site_url = get_option('siteurl');

        // Ensure callback URL is correctly formatted for admin page
        $admin_page = 'admin.php?page=muchat-settings';
        $correct_callback = admin_url($admin_page);

        $add_to_muchat_link = "{$base_url}/integrations/wordpress/config?callback={$correct_callback}&siteurl={$site_url}&agentId={$agent_id}";

        // Add security nonce
        $connect_url = add_query_arg('connect', '1', $add_to_muchat_link);
        $secure_link = wp_nonce_url($connect_url, 'muchat_connect_action', 'muchat_nonce');
        $secure_link_display = htmlspecialchars_decode($secure_link);

        // Enqueue WordPress admin styles
        wp_enqueue_style('wp-admin');
        wp_enqueue_style('dashboard');
        wp_enqueue_style('muchat-style', plugins_url('assets/admin.css', __FILE__));

        // Get scheduling settings
        $schedule_enabled = get_option('muchat_ai_chatbot_schedule_enabled', '0');
        $schedule_days = get_option('muchat_ai_chatbot_schedule_days', array());
        $schedule_start_time = get_option('muchat_ai_chatbot_schedule_start_time', '09:00');
        $schedule_end_time = get_option('muchat_ai_chatbot_schedule_end_time', '17:00');

        // Get current time and timezone info
        $current_timezone = wp_timezone_string();
        $current_time = current_time('H:i');
        $current_day = strtolower(current_time('l'));

        // Include the template
        require_once MUCHAT_AI_CHATBOT_PLUGIN_PATH . 'templates/admin/widget-settings.php';
    }

    /**
     * Checks the connection status to Muchat servers.
     *
     * Performs a lightweight HEAD request to check for connectivity. The result is
     * cached for 5 minutes in a transient to avoid excessive requests on page load.
     *
     * @return array An array containing 'success' (bool) and 'message' (string).
     */
    public function check_muchat_connection_status()
    {
        $cache_key = 'connection_status';

        // Return cached status if available.
        $cached_status = \Muchat\Api\Utils\Cache::get($cache_key);
        if (false !== $cached_status) {
            return $cached_status;
        }

        // Get server IP for debugging purposes.
        $server_ip = isset($_SERVER['SERVER_ADDR']) ? sanitize_text_field($_SERVER['SERVER_ADDR']) : 'IP not detectable';

        // Default status assumes failure.
        $status = [
            'success' => false,
            'message' => __('Could not establish a connection to Muchat servers.', 'muchat-ai'),
        ];

        // MAINTAINABILITY: Using centralized constant for App URL
        $app_url = defined('MUCHAT_APP_URL') ? MUCHAT_APP_URL : 'https://app.mu.chat';
        $response = wp_remote_head($app_url, ['timeout' => 10]);

        if (is_wp_error($response)) {
            // WordPress-level error (e.g., cURL error, DNS issue).
            $status['message'] = sprintf(
                // translators: 1: The server IP address, 2: The specific error message from WordPress.
                __('Your server (IP: %1$s) cannot connect to Muchat. Error: %2$s', 'muchat-ai'),
                $server_ip,
                $response->get_error_message()
            );
        } else {
            // We have a response, check the HTTP status code.
            $response_code = wp_remote_retrieve_response_code($response);

            if ($response_code >= 200 && $response_code < 400) {
                // Success (2xx) or Redirect (3xx) are considered successful connections.
                $status = [
                    'success' => true,
                    'message' => sprintf(
                        // translators: %s: The server IP address.
                        __('Connection from your server (IP: %s) to Muchat is successful.', 'muchat-ai'),
                        $server_ip
                    ),
                ];
            } else {
                // Server responded with an error code (4xx, 5xx).
                $status['message'] = sprintf(
                    // translators: 1: The server IP address, 2: The HTTP status code from the server.
                    __('Your server (IP: %1$s) connected to Muchat, but received an error. Status code: %2$d', 'muchat-ai'),
                    $server_ip,
                    $response_code
                );
            }
        }

        // Cache the result for 5 minutes.
        \Muchat\Api\Utils\Cache::set($cache_key, $status, 5 * MINUTE_IN_SECONDS);

        return $status;
    }

    /**
     * Remove API Documentation from admin menu
     * while preserving direct URL access
     */
    public function remove_api_documentation_menu()
    {
        remove_submenu_page('muchat-settings', 'muchat-documentation');
    }

    /**
     * Render product example page
     *
     * @return void
     */
    public function render_product_example_page()
    {
        if (!current_user_can('manage_options')) {
            return;
        }

        // Check if WooCommerce is active
        if (!class_exists('WooCommerce')) {
            echo '<div class="wrap"><div class="notice notice-error"><p>' .
                esc_html__('WooCommerce is required for this functionality', 'muchat-ai') .
                '</p></div></div>';
            return;
        }

        $product_id = 0;
        $example_data = [];
        $error_message = '';

        // Verify nonce before processing form data
        if (isset($_POST['product_id']) && isset($_POST['muchat_ai_chatbot_product_example_nonce']) && wp_verify_nonce(sanitize_key($_POST['muchat_ai_chatbot_product_example_nonce']), 'muchat_ai_chatbot_product_example_action')) {
            $product_id = intval($_POST['product_id']);
        }

        if ($product_id > 0) {
            try {
                // Method 1: Using the product model directly
                $product_model = new \Muchat\Api\Models\Product();
                $example_data = $product_model->get_product($product_id);

                if (empty($example_data)) {
                    // Method 2: Using the product controller
                    $product_controller = new \Muchat\Api\Api\Controllers\ProductController();
                    $request = new \WP_REST_Request('POST', '/muchat-api/v1/products/' . $product_id);
                    $response = $product_controller->get_single_product($request);
                    if (!is_wp_error($response)) {
                        $example_data = $response->get_data();
                    } else {
                        $error_message = $response->get_error_message();
                        $error_code = $response->get_error_code();
                    }
                }

                // We don't need to change the data, we display it as is

            } catch (\Exception $e) {
                $error_message = $e->getMessage();
            }
        }

        require_once MUCHAT_AI_CHATBOT_PLUGIN_PATH . 'templates/admin/product-example.php';
    }

    /**
     * Render API documentation page
     *
     * @return void
     */
    public function render_api_documentation_page()
    {
        if (!current_user_can('manage_options')) {
            return;
        }

        // Get options and available fields for the field tables
        $options = get_option('muchat_ai_chatbot_plugin_options', []);

        // Since this is a settings page in WordPress admin
        // We can simply check if the 'settings-updated' parameter exists
        // and only show the success message in that case

        // We don't need to validate the nonce again as WordPress core already did this
        // during the form submission to options.php before redirecting here

        // The code sniffer is concerned about nonce verification specifically when 
        // processing form data for actions, but here we're just displaying a message
        // based on query parameters set by WordPress itself

        // These variables are intentionally unverified via nonce because:
        // 1. They don't trigger any sensitive actions
        // 2. They're just used for conditional messaging
        // 3. WordPress core already verified nonce during form processing
        // phpcs:disable WordPress.Security.NonceVerification.Recommended

        if (
            isset($_GET['settings-updated']) &&
            $_GET['settings-updated'] === 'true' &&
            current_user_can('manage_options')
        ) {

            add_settings_error(
                'muchat_ai_chatbot_plugin_messages',
                'muchat_ai_chatbot_plugin_message',
                __('API field settings updated.', 'muchat-ai'),
                'updated'
            );
        }
        // phpcs:enable WordPress.Security.NonceVerification.Recommended

        // Display any settings errors and success messages
        settings_errors('muchat_ai_chatbot_plugin_messages');

        require_once MUCHAT_AI_CHATBOT_PLUGIN_PATH . 'templates/admin/api-documentation.php';
    }

    /**
     * Render product meta fields section
     * 
     * PERFORMANCE: Uses cached data if available, otherwise schedules background analysis
     * to prevent page load timeouts on large catalogs.
     */
    public function render_product_meta_section()
    {
        // Check analysis status
        $status = \Muchat\Api\Models\Product::get_meta_analysis_status();
        
        // Get plugin options
        $options = get_option('muchat_ai_chatbot_plugin_options', []);
        $selected_meta_keys = isset($options['product_meta_fields']) ? $options['product_meta_fields'] : [];
        
        // If analysis is not ready, schedule it and show loading message
        if ($status['status'] !== 'ready') {
            // Try to schedule background analysis
            \Muchat\Api\Models\Product::schedule_meta_analysis();
            
            ?>
            <div class="muchat-api-meta-fields-wrapper">
                <div class="notice notice-info inline" style="margin: 15px 0;">
                    <p>
                        <span class="dashicons dashicons-update" style="animation: rotation 2s infinite linear;"></span>
                        <?php esc_html_e('Analyzing product meta fields in the background. This may take a few moments for large catalogs.', 'muchat-ai'); ?>
                    </p>
                    <p>
                        <button type="button" class="button" onclick="location.reload();">
                            <?php esc_html_e('Refresh Page', 'muchat-ai'); ?>
                        </button>
                    </p>
                </div>
                <style>
                    @keyframes rotation {
                        from { transform: rotate(0deg); }
                        to { transform: rotate(360deg); }
                    }
                </style>
            </div>
            <?php
            return;
        }
        
        // Get cached meta fields
        $product = new \Muchat\Api\Models\Product();
        $meta_fields = $product->get_product_meta_fields();
?>
        <div class="muchat-api-meta-fields-wrapper">
            <p class="description">
                <?php esc_html_e('Select which product meta fields you want to include in the API response.', 'muchat-ai'); ?>
            </p>

            <!-- Table of fields -->
            <table class="wp-list-table widefat fixed striped" id="meta-fields-table" style="border-collapse: collapse; margin-top: 15px;">
                <thead>
                    <tr>
                        <th scope="col" style="width: 5%; padding: 10px;"><?php echo esc_html__('Enable', 'muchat-ai'); ?></th>
                        <th scope="col" style="width: 30%; padding: 10px;"><?php echo esc_html__('Meta Field', 'muchat-ai'); ?></th>
                        <th scope="col" style="width: 15%; padding: 10px;"><?php echo esc_html__('Type', 'muchat-ai'); ?></th>
                        <th scope="col" style="width: 20%; padding: 10px;"><?php echo esc_html__('Custom Label', 'muchat-ai'); ?></th>
                        <th scope="col" style="width: 30%; padding: 10px;"><?php echo esc_html__('Sample Values', 'muchat-ai'); ?></th>
                    </tr>
                </thead>
                <tbody>
                    <?php
                    // Sort meta fields alphabetically by display name
                    usort($meta_fields, function ($a, $b) {
                        return strcasecmp(urldecode($a['display_name']), urldecode($b['display_name']));
                    });

                    foreach ($meta_fields as $meta_info):
                    ?>
                        <tr>
                            <td style="padding: 12px 10px; vertical-align: middle; text-align: center;">
                                <input type="checkbox"
                                    name="muchat_ai_chatbot_plugin_options[product_meta_fields][]"
                                    id="meta_field_<?php echo esc_attr($meta_info['key']); ?>"
                                    value="<?php echo esc_attr($meta_info['key']); ?>"
                                    <?php checked(in_array($meta_info['key'], $selected_meta_keys)); ?>>
                            </td>
                            <td style="padding: 12px 10px; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
                                <label for="meta_field_<?php echo esc_attr($meta_info['key']); ?>" style="font-weight: 500; cursor: pointer; display: inline-block; max-width: 100%;">
                                    <code style="font-size: 12px; background: #f0f0f1; padding: 3px 5px; border-radius: 3px;"><?php echo esc_html(urldecode($meta_info['display_name'])); ?></code>
                                </label>
                                <?php if (!empty($meta_info['usage_count'])): ?>
                                    <div class="meta-usage" style="font-size: 11px; color: #666; margin-top: 5px;">
                                        <?php
                                        echo sprintf(
                                            // translators: %s: number of products where this meta field is used
                                            esc_html(_n('Used in %s product', 'Used in %s products', $meta_info['usage_count'], 'muchat-ai')),
                                            '<strong>' . number_format($meta_info['usage_count']) . '</strong>'
                                        ); ?>
                                    </div>
                                <?php endif; ?>
                            </td>
                            <td style="padding: 12px 10px; vertical-align: middle;">
                                <span class="badge" style="background-color: <?php
                                                                                // Different background colors based on type
                                                                                if ($meta_info['type'] === 'attribute') {
                                                                                    echo '#e9f5e9'; // Light green for attributes
                                                                                    $text_color = '#1e7e34'; // Darker green for text
                                                                                } elseif ($meta_info['type'] === 'acf') {
                                                                                    echo '#e6f3ff'; // Light blue for ACF
                                                                                    $text_color = '#0066cc'; // Darker blue for text
                                                                                } elseif ($meta_info['type'] === 'variation') {
                                                                                    echo '#ffeeba'; // Light yellow for variations
                                                                                    $text_color = '#856404'; // Darker yellow for text
                                                                                } else {
                                                                                    echo '#f0f0f1'; // Default WP gray
                                                                                    $text_color = '#50575e'; // Default WP text color
                                                                                }
                                                                                ?>; color: <?php echo esc_attr($text_color); ?>; padding: 3px 8px; border-radius: 3px; font-size: 12px; display: inline-block; font-weight: 500;">
                                    <?php
                                    // Show a more user-friendly label for the type
                                    if ($meta_info['type'] === 'attribute') {
                                        echo '<span class="dashicons dashicons-tag" style="font-size: 14px; width: 14px; height: 14px; margin-right: 2px; vertical-align: text-bottom;"></span> ';
                                        echo esc_html__('Product Attribute', 'muchat-ai');
                                    } elseif ($meta_info['type'] === 'acf') {
                                        echo '<span class="dashicons dashicons-admin-customizer" style="font-size: 14px; width: 14px; height: 14px; margin-right: 2px; vertical-align: text-bottom;"></span> ';
                                        echo esc_html__('ACF Field', 'muchat-ai');
                                    } elseif ($meta_info['type'] === 'variation') {
                                        echo '<span class="dashicons dashicons-randomize" style="font-size: 14px; width: 14px; height: 14px; margin-right: 2px; vertical-align: text-bottom;"></span> ';
                                        echo esc_html__('Variation', 'muchat-ai');
                                    } elseif ($meta_info['type'] === 'both') {
                                        echo '<span class="dashicons dashicons-image-filter" style="font-size: 14px; width: 14px; height: 14px; margin-right: 2px; vertical-align: text-bottom;"></span> ';
                                        echo esc_html__('Product & Variation', 'muchat-ai');
                                    } else {
                                        echo '<span class="dashicons dashicons-admin-generic" style="font-size: 14px; width: 14px; height: 14px; margin-right: 2px; vertical-align: text-bottom;"></span> ';
                                        echo esc_html($meta_info['type']);
                                    }
                                    ?>
                                </span>
                            </td>
                            <td style="padding: 12px 10px; vertical-align: middle;">
                                <?php $default_label = $this->get_meta_field_label($meta_info['key']); ?>
                                <input type="text"
                                    id="meta_label_<?php echo esc_attr($meta_info['key']); ?>"
                                    name="muchat_ai_chatbot_plugin_options[meta_labels][<?php echo esc_attr($meta_info['key']); ?>]"
                                    value="<?php echo esc_attr(isset($options['meta_labels'][$meta_info['key']]) ? $options['meta_labels'][$meta_info['key']] : ''); ?>"
                                    class="regular-text"
                                    style="width: 100%;"
                                    placeholder="<?php echo esc_attr($default_label); ?>">
                            </td>
                            <td style="padding: 12px 10px; vertical-align: middle;">
                                <?php if (!empty($meta_info['sample_values'])): ?>
                                    <div class="sample-values tooltip-trigger" style="max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: #50575e; cursor: help;" title="<?php echo esc_attr(implode(' | ', array_map('urldecode', $meta_info['sample_values']))); ?>">
                                        <?php echo esc_html(implode(' | ', array_map('urldecode', $meta_info['sample_values']))); ?>
                                    </div>
                                <?php endif; ?>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
        </div>
<?php
    }

    /**
     * Register settings
     *
     * @return void
     */
    public function register_settings()
    {
        // Handle cache clearing for connection status check
        if (
            isset($_GET['action']) &&
            $_GET['action'] === 'muchat_check_connection' &&
            isset($_GET['_wpnonce']) &&
            wp_verify_nonce(sanitize_key($_GET['_wpnonce']), 'muchat_check_connection_action')
        ) {
            \Muchat\Api\Utils\Cache::delete('connection_status');
            add_settings_error(
                'muchat_ai_chatbot_plugin_messages',
                'connection_rechecked',
                __('Connection status has been re-checked.', 'muchat-ai'),
                'info'
            );
        }

        // Handle cache clearing action
        if (
            isset($_GET['action']) &&
            $_GET['action'] === 'muchat_refresh_meta' &&
            isset($_GET['_wpnonce']) &&
            wp_verify_nonce(sanitize_key($_GET['_wpnonce']), 'muchat_refresh_meta_action')
        ) {
            delete_transient('muchat_product_meta_fields_cache');
            add_settings_error(
                'muchat_ai_chatbot_plugin_messages',
                'meta_cache_cleared',
                __('Product meta fields cache has been cleared successfully.', 'muchat-ai'),
                'updated'
            );
        }

        // Register plugin settings
        $this->register_plugin_settings();

        // Register plugin onboarding
        $this->register_plugin_onboarding();

        // Register the main settings group for the plugin
        register_setting(
            'muchat_ai_chatbot_plugin_settings',
            'muchat_ai_chatbot_plugin_options',
            array(
                'sanitize_callback' => function ($input) {
                    // If input is not an array, return an empty array
                    if (!is_array($input)) {
                        return array();
                    }

                    // Sanitize meta field selections (array of values)
                    if (isset($input['product_meta_fields']) && is_array($input['product_meta_fields'])) {
                        $input['product_meta_fields'] = array_map('sanitize_text_field', $input['product_meta_fields']);
                    }

                    // Sanitize meta labels (associative array)
                    if (isset($input['meta_labels']) && is_array($input['meta_labels'])) {
                        foreach ($input['meta_labels'] as $key => $value) {
                            $input['meta_labels'][$key] = sanitize_text_field($value);
                        }
                    }

                    return $input;
                }
            )
        );

        // Product Meta Section (in a separate page section)
        add_settings_section(
            'muchat_ai_chatbot_plugin_product_meta_section',
            '', // Empty title as we're using custom HTML
            [$this, 'render_product_meta_section'],
            'muchat-meta-settings'  // Meta settings section identifier
        );

        // Products Section
        add_settings_section(
            'muchat_ai_chatbot_plugin_products_section',
            esc_html__('Product Fields', 'muchat-ai'),
            null,
            'muchat-settings'
        );

        // Posts Section
        add_settings_section(
            'muchat_ai_chatbot_plugin_posts_section',
            esc_html__('Post Fields', 'muchat-ai'),
            null,
            'muchat-settings'
        );

        // Pages Section
        add_settings_section(
            'muchat_ai_chatbot_plugin_pages_section',
            esc_html__('Page Fields', 'muchat-ai'),
            null,
            'muchat-settings'
        );
    }

    /**
     * Render settings page
     *
     * @return void
     */
    public function render_settings_page()
    {
        if (!current_user_can('manage_options')) {
            return;
        }

        // Check if WooCommerce is active
        if (!class_exists('WooCommerce')) {
            echo '<div class="wrap"><div class="notice notice-error"><p>' .
                esc_html__('WooCommerce is required for this functionality', 'muchat-ai') .
                '</p></div></div>';
            return;
        }

        // Get connection status
        $connection_status = $this->check_muchat_connection_status();

        $options = get_option('muchat_ai_chatbot_plugin_options', []);
        require_once MUCHAT_AI_CHATBOT_PLUGIN_PATH . 'templates/admin/settings.php';
    }

    /**
     * Get available fields
     *
     * @return array
     */
    public function get_available_fields()
    {
        return [
            'product' => [
                'id' => __('ID', 'muchat-ai'),
                'name' => __('Name', 'muchat-ai'),
                'permalink' => __('Permalink', 'muchat-ai'),
                'date_modified' => __('Date Modified', 'muchat-ai'),
                'description' => __('Description', 'muchat-ai'),
                'short_description' => __('Short Description', 'muchat-ai'),
                'currency' => __('Currency', 'muchat-ai'),
                'price' => __('Price', 'muchat-ai'),
                'regular_price' => __('Regular Price', 'muchat-ai'),
                'sale_price' => __('Sale Price', 'muchat-ai'),
                'stock_status' => __('Stock Status', 'muchat-ai'),
                'categories' => __('Categories', 'muchat-ai'),
                'tags' => __('Tags', 'muchat-ai'),
                'images' => __('Images', 'muchat-ai'),
                'attributes' => __('Attributes', 'muchat-ai'),
                'variations' => __('Variations', 'muchat-ai'),
            ],
            'post' => [
                'id' => __('ID', 'muchat-ai'),
                'title' => __('Title', 'muchat-ai'),
                'content' => __('Content', 'muchat-ai'),
                'excerpt' => __('Excerpt', 'muchat-ai'),
                'modified_date' => __('Modified Date', 'muchat-ai'),
                'permalink' => __('Permalink', 'muchat-ai'),
                'categories' => __('Categories', 'muchat-ai'),
                'tags' => __('Tags', 'muchat-ai'),
                'featured_image' => __('Featured Image', 'muchat-ai'),
            ],
            'page' => [
                'id' => __('ID', 'muchat-ai'),
                'title' => __('Title', 'muchat-ai'),
                'content' => __('Content', 'muchat-ai'),
                'excerpt' => __('Excerpt', 'muchat-ai'),
                'modified_date' => __('Modified Date', 'muchat-ai'),
                'permalink' => __('Permalink', 'muchat-ai'),
                'featured_image' => __('Featured Image', 'muchat-ai'),
            ]
        ];
    }

    /**
     * Get available custom post types
     *
     * @return array
     */
    public function get_available_custom_post_types()
    {
        $excluded_types = ['post', 'page', 'product', 'attachment', 'revision', 'nav_menu_item'];
        $post_types = get_post_types(['public' => true], 'objects');
        $custom_post_types = [];

        foreach ($post_types as $post_type) {
            // Skip excluded types
            if (in_array($post_type->name, $excluded_types)) {
                continue;
            }

            // Only include public post types or those explicitly set to show in REST API
            if (!$post_type->public && !$post_type->show_in_rest) {
                continue;
            }

            $custom_post_types[$post_type->name] = [
                'name' => $post_type->name,
                'label' => $post_type->label,
                'singular_label' => $post_type->labels->singular_name ?? $post_type->label,
                'description' => $post_type->description ?? '',
            ];
        }

        return $custom_post_types;
    }

    /**
     * Get default fields for custom post type
     *
     * @param string $post_type
     * @return array
     */
    public function get_default_fields_for_custom_post_type($post_type)
    {
        return [
            'id' => __('ID', 'muchat-ai'),
            'title' => __('Title', 'muchat-ai'),
            'content' => __('Content', 'muchat-ai'),
            'excerpt' => __('Excerpt', 'muchat-ai'),
            'modified_date' => __('Modified Date', 'muchat-ai'),
            'permalink' => __('Permalink', 'muchat-ai'),
            'featured_image' => __('Featured Image', 'muchat-ai'),
        ];
    }

    /**
     * Register plugin settings
     */
    public function register_plugin_settings()
    {
        // Register all settings
        register_setting('muchat-settings-group', 'muchat_ai_chatbot_agent_id', array(
            'type' => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'default' => ''
        ));
        register_setting('muchat-settings-group', 'muchat_ai_chatbot_verify', array(
            'type' => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'default' => ''
        ));
        register_setting('muchat-settings-group', 'muchat_ai_chatbot_context', array(
            'type' => 'string',
            'sanitize_callback' => 'sanitize_textarea_field',
            'default' => ''
        ));
        register_setting('muchat-settings-group', 'muchat_ai_chatbot_interface_initial_messages', array(
            'type' => 'string',
            'sanitize_callback' => 'sanitize_textarea_field',
            'default' => ''
        ));
        register_setting('muchat-settings-group', 'muchat_ai_chatbot_enable_guest_messages', array(
            'type' => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'default' => '0'
        ));
        register_setting('muchat-settings-group', 'muchat_ai_chatbot_guest_initial_messages', array(
            'type' => 'string',
            'sanitize_callback' => 'sanitize_textarea_field',
            'default' => ''
        ));
        register_setting('muchat-settings-group', 'muchat_ai_chatbot_enable_guest_context', array(
            'type' => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'default' => '0'
        ));
        register_setting('muchat-settings-group', 'muchat_ai_chatbot_guest_context', array(
            'type' => 'string',
            'sanitize_callback' => 'sanitize_textarea_field',
            'default' => ''
        ));
        register_setting('muchat-settings-group', 'muchat_ai_chatbot_load_strategy', array(
            'type' => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'default' => 'FAST'
        ));
        register_setting('muchat-settings-group', 'muchat_ai_chatbot_script_position', array(
            'type' => 'string',
            'sanitize_callback' => function($input) {
                $valid_positions = ['head', 'footer', 'body_open'];
                return in_array($input, $valid_positions, true) ? $input : 'footer';
            },
            'default' => 'footer'
        ));
        register_setting('muchat-settings-group', 'muchat_ai_chatbot_use_logged_in_user_info', array(
            'type' => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'default' => ''
        ));
        register_setting('muchat-settings-group', 'muchat_ai_chatbot_visibility_mode', array(
            'type' => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'default' => 'all'
        ));
        register_setting('muchat-settings-group', 'muchat_ai_chatbot_visibility_pages', array(
            'type' => 'string',
            'sanitize_callback' => array($this, 'sanitize_visibility_pages'),
            'default' => ''
        ));
        register_setting('muchat-settings-group', 'muchat_ai_chatbot_widget_enabled', array(
            'type' => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'default' => '1'
        ));

        // Scheduling settings
        register_setting('muchat-settings-group', 'muchat_ai_chatbot_schedule_enabled', array(
            'type' => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'default' => '0'
        ));
        register_setting('muchat-settings-group', 'muchat_ai_chatbot_schedule_days', array(
            'type' => 'array',
            'sanitize_callback' => function ($input) {
                if (!is_array($input)) {
                    return array();
                }
                return array_map('sanitize_text_field', $input);
            },
            'default' => array()
        ));
        register_setting('muchat-settings-group', 'muchat_ai_chatbot_schedule_start_time', array(
            'type' => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'default' => '09:00'
        ));
        register_setting('muchat-settings-group', 'muchat_ai_chatbot_schedule_end_time', array(
            'type' => 'string',
            'sanitize_callback' => 'sanitize_text_field',
            'default' => '17:00'
        ));

        // Default settings
        $this->initialize_default_settings();
    }

    /**
     * Initialize default settings
     * 
     * PERFORMANCE: Options are categorized by autoload behavior:
     * - autoload = 'yes' (default): Options needed on every frontend page (widget rendering)
     * - autoload = 'no': Options only needed in admin or specific conditions
     * 
     * This prevents loading unnecessary data into alloptions on every page load.
     */
    private function initialize_default_settings()
    {
        // =====================================================================
        // ADMIN-ONLY OPTIONS (autoload = 'no')
        // These are only needed in WordPress admin, not on frontend pages
        // =====================================================================
        
        // Onboarding state - only checked in admin
        add_option('muchat_ai_chatbot_onboarding', false, '', 'no');
        
        // =====================================================================
        // LARGE TEXT OPTIONS (autoload = 'no')
        // These can contain substantial text and shouldn't bloat alloptions
        // They are loaded on-demand only when widget is rendered
        // =====================================================================
        
        // Main context - can be several KB of text
        add_option('muchat_ai_chatbot_context', '', '', 'no');
        
        // Initial messages - can contain multiple messages
        add_option('muchat_ai_chatbot_interface_initial_messages', '', '', 'no');
        
        // Guest message content - loaded on-demand when needed
        add_option('muchat_ai_chatbot_guest_initial_messages', '', '', 'no');
        add_option('muchat_ai_chatbot_guest_context', '', '', 'no');
        
        // Visibility pages list - can be large, loaded on-demand
        add_option('muchat_ai_chatbot_visibility_pages', '', '', 'no');
        
        // Schedule days array - only needed for schedule check
        add_option('muchat_ai_chatbot_schedule_days', array(), '', 'no');
        
        // =====================================================================
        // FRONTEND OPTIONS (autoload = 'yes' - default)
        // These are needed on every frontend page for widget rendering
        // =====================================================================
        
        // Guest messaging toggles - checked on every widget load
        add_option('muchat_ai_chatbot_enable_guest_messages', '0');
        add_option('muchat_ai_chatbot_enable_guest_context', '0');

        // Load strategy affects script loading on every page
        add_option('muchat_ai_chatbot_load_strategy', 'FAST');
        add_option('muchat_ai_chatbot_script_position', 'footer');

        // Visibility mode - checked on every page to decide if widget shows
        add_option('muchat_ai_chatbot_visibility_mode', 'all');

        // Widget enabled state - checked on every page
        add_option('muchat_ai_chatbot_widget_enabled', '1');

        // Schedule enabled toggle - checked on every page
        add_option('muchat_ai_chatbot_schedule_enabled', '0');
        add_option('muchat_ai_chatbot_schedule_start_time', '09:00');
        add_option('muchat_ai_chatbot_schedule_end_time', '17:00');
    }

    /**
     * Register plugin onboarding
     */
    public function register_plugin_onboarding()
    {
        $onboarding = get_option('muchat_ai_chatbot_onboarding');
        $agent_id = get_option('muchat_ai_chatbot_agent_id');

        if (empty($agent_id) && (empty($onboarding) || !$onboarding)) {
            update_option("muchat_ai_chatbot_onboarding", true);
            wp_safe_redirect(admin_url('admin.php?page=muchat-settings'));
            exit;
        }
    }

    /**
     * Sanitize visibility pages input.
     * Preserves Unicode characters (Persian, Arabic, etc.), percent-encoded URLs,
     * wildcards (*), and the special <front> tag.
     *
     * @param string $input The raw input from the textarea.
     * @return string The sanitized input.
     */
    public function sanitize_visibility_pages($input)
    {
        if (empty($input)) {
            return '';
        }

        // Split input into lines
        $lines = explode("\n", $input);
        $sanitized_lines = array();

        foreach ($lines as $line) {
            $line = trim($line);
            
            // Skip empty lines
            if (empty($line)) {
                continue;
            }

            // Check for special <front> tag (case-insensitive)
            if (strtolower($line) === '<front>') {
                $sanitized_lines[] = '<front>';
                continue;
            }

            // Check for global wildcard
            if ($line === '*') {
                $sanitized_lines[] = '*';
                continue;
            }

            // Decode percent-encoded URLs to UTF-8
            // This handles URLs like %d8%aa%d8%b3%d8%aa (تست)
            $decoded_line = $line;
            
            // Check if the line contains percent-encoded characters
            if (preg_match('/%[0-9A-Fa-f]{2}/', $line)) {
                $decoded = urldecode($line);
                // Only use decoded version if it's valid UTF-8
                if (mb_check_encoding($decoded, 'UTF-8')) {
                    $decoded_line = $decoded;
                }
            }

            // Normalize the path:
            // 1. Remove any HTML tags (except for security, we don't want scripts)
            $decoded_line = wp_strip_all_tags($decoded_line);
            
            // 2. Ensure path starts with /
            if (!empty($decoded_line) && $decoded_line[0] !== '/' && $decoded_line[0] !== '*') {
                $decoded_line = '/' . $decoded_line;
            }

            // 3. Remove trailing slash (unless it's just /)
            if (strlen($decoded_line) > 1) {
                $decoded_line = rtrim($decoded_line, '/');
            }

            // 4. Normalize Unicode to NFC form for consistent storage
            if (function_exists('normalizer_normalize')) {
                $normalized = normalizer_normalize($decoded_line, \Normalizer::FORM_C);
                if ($normalized !== false) {
                    $decoded_line = $normalized;
                }
            }

            // 5. Only allow safe characters:
            // - Unicode letters and numbers (including Persian/Arabic)
            // - Forward slash, hyphen, underscore, dot, asterisk (wildcard)
            // - Percent sign (for any remaining encoded chars)
            // - Hash (#) for anchor links
            // - Question mark (?) and equals (=) and ampersand (&) for query strings
            // Using preg_replace with 'u' modifier for UTF-8 support
            $decoded_line = preg_replace('/[^\p{L}\p{N}\/\-\_\.\*\%\#\?\=\&]/u', '', $decoded_line);

            if (!empty($decoded_line)) {
                $sanitized_lines[] = $decoded_line;
            }
        }

        return implode("\n", $sanitized_lines);
    }

    /**
     * AJAX handler for searching content (products, posts, pages)
     * 
     * SECURITY: Implements nonce verification and capability check
     */
    public function ajax_search_content()
    {
        // SECURITY: Verify nonce
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key($_POST['nonce']), 'muchat_api_nonce')) {
            wp_send_json_error(['message' => __('Security check failed', 'muchat-ai')], 403);
        }

        // SECURITY: Check user capability
        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Permission denied', 'muchat-ai')], 403);
        }

        // Sanitize input
        $query = isset($_POST['query']) ? sanitize_text_field(wp_unslash($_POST['query'])) : '';
        $page = isset($_POST['page']) ? absint($_POST['page']) : 1;
        $per_page = 20;

        if (strlen($query) < 2) {
            wp_send_json_error(['message' => __('Query too short', 'muchat-ai')], 400);
        }

        $results = [];
        $total = 0;

        // Search products (if WooCommerce is active)
        if (class_exists('WooCommerce')) {
            $product_args = [
                'post_type' => 'product',
                'post_status' => 'publish',
                's' => $query,
                'posts_per_page' => $per_page,
                'paged' => $page,
            ];
            
            $product_query = new \WP_Query($product_args);
            
            foreach ($product_query->posts as $post) {
                $results[] = [
                    'id' => $post->ID,
                    'title' => esc_html($post->post_title),
                    'type' => 'product',
                ];
            }
            $total += $product_query->found_posts;
        }

        // Search posts
        $post_args = [
            'post_type' => 'post',
            'post_status' => 'publish',
            's' => $query,
            'posts_per_page' => $per_page,
            'paged' => $page,
        ];
        
        $post_query = new \WP_Query($post_args);
        
        foreach ($post_query->posts as $post) {
            $results[] = [
                'id' => $post->ID,
                'title' => esc_html($post->post_title),
                'type' => 'post',
            ];
        }
        $total += $post_query->found_posts;

        // Search pages
        $page_args = [
            'post_type' => 'page',
            'post_status' => 'publish',
            's' => $query,
            'posts_per_page' => $per_page,
            'paged' => $page,
        ];
        
        $page_query = new \WP_Query($page_args);
        
        foreach ($page_query->posts as $post) {
            $results[] = [
                'id' => $post->ID,
                'title' => esc_html($post->post_title),
                'type' => 'page',
            ];
        }
        $total += $page_query->found_posts;

        // Sort results by relevance (title match priority)
        usort($results, function($a, $b) use ($query) {
            $a_match = stripos($a['title'], $query) !== false ? 0 : 1;
            $b_match = stripos($b['title'], $query) !== false ? 0 : 1;
            return $a_match - $b_match;
        });

        wp_send_json_success([
            'items' => array_slice($results, 0, $per_page),
            'total' => $total,
            'more' => ($page * $per_page) < $total,
        ]);
    }

    /**
     * AJAX handler for previewing content via API
     * 
     * SECURITY: Implements nonce verification and capability check
     */
    public function ajax_preview_content()
    {
        // SECURITY: Verify nonce
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_key($_POST['nonce']), 'muchat_api_nonce')) {
            wp_send_json_error(['message' => __('Security check failed', 'muchat-ai')], 403);
        }

        // SECURITY: Check user capability
        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Permission denied', 'muchat-ai')], 403);
        }

        // Sanitize input
        $content_type = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : '';
        $item_id = isset($_POST['item_id']) ? absint($_POST['item_id']) : 0;

        // Validate content type
        $allowed_types = ['product', 'post', 'page'];
        if (!in_array($content_type, $allowed_types, true)) {
            wp_send_json_error(['message' => __('Invalid content type', 'muchat-ai')], 400);
        }

        if ($item_id <= 0) {
            wp_send_json_error(['message' => __('Invalid item ID', 'muchat-ai')], 400);
        }

        $data = null;

        try {
            switch ($content_type) {
                case 'product':
                    if (!class_exists('WooCommerce')) {
                        wp_send_json_error(['message' => __('WooCommerce not active', 'muchat-ai')], 400);
                    }
                    $product_model = new \Muchat\Api\Models\Product();
                    $data = $product_model->get_product($item_id);
                    break;

                case 'post':
                    $post_model = new \Muchat\Api\Models\Post();
                    $data = $post_model->get_post($item_id);
                    break;

                case 'page':
                    $page_model = new \Muchat\Api\Models\Page();
                    $data = $page_model->get_page($item_id);
                    break;
            }

            if (empty($data)) {
                wp_send_json_error(['message' => __('Content not found', 'muchat-ai')], 404);
            }

            wp_send_json_success($data);

        } catch (\Exception $e) {
            wp_send_json_error(['message' => $e->getMessage()], 500);
        }
    }
}
</file>

<file path="muchat-ai.php">
<?php

/**
 * Plugin Name: Muchat - AI Chatbot (with Autosync)
 * Plugin URI: https://mu.chat
 * Description: Muchat, a powerful tool for customer support using artificial intelligence
 * Version: 2.0.52
 * Author: Muchat
 * Text Domain: muchat-ai
 * Domain Path: /languages
 * Requires at least: 5.0
 * Requires PHP: 7.3
 * Author: Muchat Team
 * Author URI: https://mu.chat/about-us
 * License: GPL-2.0+
 * License URI: http://www.gnu.org/licenses/gpl-2.0.txt
 */

if (!defined('ABSPATH')) {
    exit;
}

// If this file is called directly, abort.
if (!defined('WPINC')) {
    die;
}

// Define plugin constants with unique prefix
define('MUCHAT_AI_CHATBOT_PLUGIN_VERSION', '2.0.52');
// define('MUCHAT_AI_CHATBOT_CACHE_DURATION', HOUR_IN_SECONDS);
define('MUCHAT_AI_CHATBOT_PLUGIN_FILE', __FILE__);
define('MUCHAT_AI_CHATBOT_PLUGIN_PATH', plugin_dir_path(__FILE__));
define('MUCHAT_AI_CHATBOT_PLUGIN_URL', plugin_dir_url(__FILE__));

// Centralized external service URLs
// MAINTAINABILITY: All external Muchat URLs in one place for easy updates
define('MUCHAT_CDN_URL', 'https://cdn.mu.chat');
define('MUCHAT_APP_URL', 'https://app.mu.chat');

/**
 * Log message with proper debug check
 * 
 * BEST PRACTICE: Only logs when WP_DEBUG is enabled
 * Prevents unnecessary logging in production environments
 * 
 * @param string $message The message to log
 * @param string $level Log level: 'debug', 'info', 'warning', 'error'
 */
function muchat_log($message, $level = 'debug')
{
    // Only log if WP_DEBUG is enabled
    if (!defined('WP_DEBUG') || !WP_DEBUG) {
        return;
    }
    
    // For production-like environments, only log warnings and errors
    if (!defined('WP_DEBUG_LOG') || !WP_DEBUG_LOG) {
        if (in_array($level, ['debug', 'info'], true)) {
            return;
        }
    }
    
    $prefix = strtoupper($level);
    // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
    error_log("[Muchat {$prefix}] {$message}");
}

/**
 * Setup API isolation to prevent PHP error output from corrupting JSON responses
 * 
 * BEST PRACTICE: Uses output buffering instead of overriding error handlers.
 * This approach:
 * - Does NOT interfere with monitoring tools (Sentry, New Relic, etc.)
 * - Does NOT mask errors from other plugins or WordPress core
 * - Only cleans error output for API responses to return valid JSON
 */
function muchat_ai_setup_api_isolation()
{
    // Only apply to our API endpoints
    if (!isset($_SERVER['REQUEST_URI']) ||
        strpos($_SERVER['REQUEST_URI'], '/wp-json/muchat-api/') === false) {
        return;
    }

    $plugin_path = MUCHAT_AI_CHATBOT_PLUGIN_PATH;

    // Use output buffering to catch and clean PHP error output
    // This does NOT override error handlers, so monitoring tools still work
    ob_start(function($buffer) use ($plugin_path) {
        // If there's an error message in the buffer, clean it for JSON
        if (!headers_sent() && !empty($buffer)) {
            // SAFETY CHECK: If the buffer already contains valid JSON response,
            // don't interfere with it. This prevents issues with wp_send_json_*
            $trimmed = ltrim($buffer);
            if (strpos($trimmed, '{') === 0 || strpos($trimmed, '[') === 0) {
                // Already a JSON response, pass through unchanged
                return $buffer;
            }
            
            // Check if Content-Type header is already set to application/json
            $headers = headers_list();
            foreach ($headers as $header) {
                if (stripos($header, 'Content-Type:') === 0 && 
                    stripos($header, 'application/json') !== false) {
                    // JSON content type already set, pass through
                    return $buffer;
                }
            }
            
            // Check if buffer contains PHP error output
            if (preg_match('/<b>(?:Fatal|Warning|Notice|Parse) error<\/b>/i', $buffer) ||
                preg_match('/^(Fatal|Parse) error:/i', $buffer)) {
                
                // Log the original error
                error_log('Muchat API caught PHP error in output: ' . substr($buffer, 0, 500));
                
                // Return clean JSON error
                http_response_code(500);
                header('Content-Type: application/json', true, 500);
                return wp_json_encode([
                    'code' => 'internal_error',
                    'message' => 'An internal error occurred',
                    'data' => ['status' => 500]
                ]);
            }
        }
        return $buffer;
    });

    // Register shutdown function for fatal errors
    // This runs AFTER monitoring tools have already seen the error
    register_shutdown_function(function() use ($plugin_path) {
        $error = error_get_last();
        if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
            // Fire action so monitoring tools can capture this
            // They should have already captured it, but this is a safety net
            do_action('muchat_fatal_error', $error);
            
            // Check if this is our API request
            if (isset($_SERVER['REQUEST_URI']) &&
                strpos($_SERVER['REQUEST_URI'], '/wp-json/muchat-api/') !== false) {
                
                // Clean any partial output
                while (ob_get_level()) {
                    ob_end_clean();
                }
                
                if (!headers_sent()) {
                    http_response_code(500);
                    header('Content-Type: application/json');
                    echo wp_json_encode([
                        'code' => 'fatal_error',
                        'message' => 'A fatal error occurred',
                        'data' => ['status' => 500]
                    ]);
                }
            }
        }
    });

    // Disable error display for API responses only (not logging)
    if (php_sapi_name() !== 'cli') {
        @ini_set('display_errors', 0);
    }
}

// Simple Autoloader
spl_autoload_register(function ($class) {
    // Project-specific namespace prefix
    $prefix = 'Muchat\\Api\\';
    $base_dir = MUCHAT_AI_CHATBOT_PLUGIN_PATH . 'includes/';

    // Check if the class uses the namespace prefix
    $len = strlen($prefix);
    if (strncmp($prefix, $class, $len) !== 0) {
        return;
    }

    // Get the relative class name
    $relative_class = substr($class, $len);

    // Replace namespace separators with directory separators
    $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';

    // If the file exists, require it
    if (file_exists($file)) {
        require_once $file;
    }
});

/**
 * This function is hooked into 'plugins_loaded' to ensure all necessary WordPress
 * functionality is available.
 */
function muchat_ai_chatbot_run() {
    $plugin = new \Muchat\Api\Core\Plugin();
    $plugin->run();
}

// Hook the run function to the 'plugins_loaded' action
add_action('plugins_loaded', 'muchat_ai_chatbot_run');

// Initialize product change tracker for WooCommerce on plugins_loaded hook
function muchat_ai_chatbot_initialize_woocommerce()
{
    // Initialize product change tracker for WooCommerce
    if (class_exists('WooCommerce')) {
        new \Muchat\Api\Utils\ProductChangeTracker();
    }
}
add_action('plugins_loaded', 'muchat_ai_chatbot_initialize_woocommerce');

// Action Scheduler hook for background meta fields analysis
add_action('muchat_analyze_meta_fields', function () {
    if (class_exists('\Muchat\Api\Models\Product')) {
        \Muchat\Api\Models\Product::generate_meta_fields_background();
    }
});

// Hook for plugin activation
register_activation_hook(__FILE__, function () {
    $activator = new Muchat\Api\Core\Activator();
    $activator->activate();
});

// Hook for plugin deactivation 
register_deactivation_hook(__FILE__, function () {
    $deactivator = new Muchat\Api\Core\Deactivator();
    $deactivator->deactivate();
});

// Declare WooCommerce HPOS compatibility
add_action('before_woocommerce_init', function () {
    if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) {
        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true);
    }
});

// Isolate our API from other plugin errors
if (isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], '/wp-json/muchat-api/') !== false) {
    // Set up error isolation for our API
    muchat_ai_setup_api_isolation();
}

// Run the plugin - This direct call is removed and handled by the hook now.
// muchat_ai_chatbot_run_plugin();
</file>

<file path="readme.txt">
=== Muchat - AI Chatbot (with Autosync) ===
Contributors: muchatai
Tags: chat, woocommerce, customer support, AI chatbot, chatbot
Requires at least: 5.0
Tested up to: 6.8
Requires PHP: 7.3
Stable tag: 2.0.52
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Integrate MuChat: AI Chatbot for WordPress/WooCommerce, with auto-sync for enhanced customer support

== Description ==

Muchat seamlessly integrates a powerful AI-driven chat solution into your WordPress and WooCommerce websites. It offers an intelligent blend of automated AI responses and live chat capabilities, designed to elevate your customer support, foster real-time engagement, and streamline communication.
By leveraging features like WooCommerce product data synchronization and visitor tracking, Muchat ensures your interactions are timely, relevant, and efficient. This powerful combination not only enhances your customer support operations but also boosts user engagement, improves customer satisfaction, and can help drive conversions.

**Benefits of Muchat:**
- Provide instant responses and support, improving customer satisfaction.
- Engage visitors in real-time, guiding them through your site or products.
- Streamline product inquiries with direct WooCommerce integration.
- Enhance operational efficiency with smart chat tools.
- Customize the chat experience to perfectly match your brand.
- Increase sales opportunities by being readily available to assist.
- Offer support across various pages with precise visibility controls.

**Key Features:**
- AI-powered chatbot for automated assistance & live chat for human interaction.
- **Deep WooCommerce Integration:**
    - Product information auto-sync with the chat system.
    - Share product details directly in chat.
    - View product metadata for informed responses.
- **Advanced Customization:**
    - Custom color schemes to match your website branding.
    - Adjustable chat widget position (e.g., bottom-right, bottom-left).
    - Configurable initial greeting messages.
- **Smart Controls:**
    - Scheduling Options: Define chat availability based on your business hours or staff presence.
    - Multi-Page Visibility: Precisely control on which pages the chat widget appears or is hidden.
    - Visitor Information Tracking: Understand your visitors better.
- Mobile-Friendly & Responsive Design: Ensures a great user experience on all devices.
- Easy Setup: Quickly connect with your Muchat Agent ID.

Learn more at https://mu.chat

== Installation ==

1.  Upload the plugin files to the `/wp-content/plugins/muchat-ai` directory, or install the plugin through the WordPress plugins screen directly.
2.  Activate the plugin through the 'Plugins' screen in WordPress.
3.  Navigate to Settings->Muchat to configure the plugin and enter your Muchat Agent ID to connect with the chat service.
4.  Customize your chat widget appearance, scheduling, and page visibility as needed.
5.  Start engaging with your website visitors!

== Frequently Asked Questions ==

= Does this plugin require WooCommerce? =
While Muchat enhances WooCommerce sites significantly (e.g., product sharing, metadata access), it functions as a robust chat solution even without WooCommerce installed. Core features like AI chat, live chat, scheduling, and page visibility are available for all WordPress sites.

= Do I need a Muchat Agent ID? =
Yes, a Muchat Agent ID is required to connect the plugin to the Muchat service, enabling all chat functionalities.

= Can I customize when and where the chat widget appears? =
Absolutely. You can set specific pages for widget visibility (show/hide) and define a schedule (days/times) for when the chat should be active and available to your visitors.

= Is the chat widget mobile-friendly? =
Yes, the Muchat widget is fully responsive and designed to work seamlessly across all device sizes, including desktops, tablets, and smartphones.

== Screenshots ==

1.  Chat widget on the frontend engaging a visitor.
2.  Admin settings page for Muchat configuration.
3.  WooCommerce integration settings showcasing product sync.
4.  Schedule configuration interface for setting chat availability.
5.  Page visibility controls for the chat widget.

== Changelog ==

= 2.0.52 =
* Feat: Added optional `post_type` parameter to `/posts` endpoint to include custom post types alongside regular posts.
* Enhancement: Proper pagination and count handling when combining regular posts with custom post types.

= 2.0.51 =
* Feat: New Custom Post Types API endpoint (`/custom-post-types/{post_type}`) - public access, no authentication required.
* Feat: Full UTF-8/Persian/Arabic URL support in Display Rules for RTL websites.
* Feat: Added API content search and preview functionality in admin panel.
* Feat: Added single post/page retrieval methods for API preview.
* Security: Enhanced SQL injection prevention with strict whitelist approach for order parameters.
* Fix: Improved API error isolation to prevent other plugin conflicts from affecting API responses.
* Docs: Added comprehensive Custom Post Types documentation to API Documentation page.

= 2.0.50 =
* Feat: Add `muchat_date_modified` field to products for precise tracking of price/stock changes.
* Feat: Add `muchat_modified_after` filter to products API endpoint.
* Fix: Ensure variation changes update the parent product's `muchat_date_modified`.
* Fix: Plugin initialization now uses `plugins_loaded` hook for better compatibility.

= 2.0.49 =
* Fix: Complete rewrite of ordering logic using custom SQL approach to ensure ASC/DESC ordering works correctly across all WordPress environments.
* Fix: Resolved issue where order parameter was being ignored, causing identical results regardless of sort direction.
* Fix: Disabled query caching to ensure fresh results are always returned.
* Enhancement: Added proper SQL field mapping for all order_by options (modified, date, title, ID).
* Fix: Improved API response reliability by cleaning output buffers before processing.

= 2.0.48 =
* Fix: Add `strtoupper` to order param to fix ordering issue in some environments.

= 2.0.47 =
* Fix: Improved Script Load Position implementation with proper priority handling for better mobile compatibility.
* Fix: Resolved critical issue where scripts were loading too early (priority 1) causing conflicts and mobile display issues.
* Fix: Corrected default value inconsistency - now consistently defaults to 'footer' for optimal performance.
* Tweak: Added validation for script position values to prevent invalid configurations.
* Feature: Added debug HTML comment for administrators to verify script load position (visible in page source).
* Tweak: Enhanced admin UI with clearer descriptions for each Script Load Position option.
* Tweak: Removed misleading theme support warning that appeared even when themes properly supported the feature.

= 2.0.46 =
* Feature: Return `date_modified` along with `id` in the `product-ids` endpoint.
* Fix: Corrected sorting for the `product-ids` endpoint to use `date_modified` and then `id`.
* Tweak: Optimized the performance of the `product-ids` endpoint for a large number of products.

= 2.0.45 =
* Compatibility: Added PHP 7.3 compatibility - replaced null coalescing operators with isset/ternary for better backward compatibility.
* Performance: Implemented error isolation system to prevent other plugin errors from affecting API endpoints.


= 2.0.44 =
* Feature: Added a "Script Load Position" setting (Header, Footer, Body Opening) to provide control over widget loading and improve site performance.
* Tweak: The default script load position for new installations is now "Footer" for better performance, while maintaining "Header" for existing sites to ensure backward compatibility.

= 2.0.43 =
* New: Added the ability to define a separate context for non-logged-in (guest) users.
* Fix: Personalization variables (e.g., `$name`, `$page_title`) now work correctly within the "Initial Messages" fields.
* Fix: Resolved an issue where empty "Initial Messages" would incorrectly override the settings from the main Muchat dashboard.
* Tweak: Removed obsolete and unused code from the admin settings page, including the "Primary Color" setting and non-functional manual contact fields.
* Tweak: Refactored duplicated code for fetching user data into a single, reusable function.

= 2.0.42 =
* Bugfix: Fix plugin version

= 2.0.41 =
* Bugfix: Fix initialMessages issue

= 2.0.40 =
* Bugfix: Fix pages api empty value

= 2.0.39 =
* Feature: Added check app status

= 2.0.38 =
- Feature: Added an option for different initial messages for guest (non-logged-in) users.
- Performance: Major enhancements to API and admin pages.
- Performance: Optimized database queries for products, posts, and pages to reduce memory usage on large sites.
- Performance: Implemented caching for API token validation to prevent server slowdowns.
- Performance: Cached heavy queries on the plugin's settings page to improve load times.
- Bugfix: Corrected an "undefined variable" warning on the widget settings page.

= 2.0.37 =
* Fixed issue with `<front>` tag being removed when saving display rules

= 2.0.36 =
* Added banner to WordPress plugin

= 2.0.35 =
* Added settings link to the plugin in WordPress plugins list page for easier access

= 2.0.34 =
* Security improvements and bug fixes

= 2.0.33 =
* Minor bug fixes and performance optimizations.

= 2.0.32 =
* Added scheduling feature for chat availability.
* Implemented multi-page visibility control for the chat widget.
* Improved WooCommerce integration for product data handling.
* Enhanced chat widget performance and responsiveness.
* Expanded customization options for the chat interface.
* General performance improvements and bug fixes.

== Upgrade Notice ==

= 2.0.31 =
Major update! This version introduces powerful scheduling features, precise multi-page visibility controls, and significant WooCommerce integration enhancements. It also includes important performance boosts and critical bug fixes. Recommended for all users.

== Privacy Policy ==

Muchat collects and processes user data as necessary to provide its chat functionality and improve service. This may include:

*   Chat messages exchanged.
*   User contact information (e.g., name, email) if voluntarily provided by the user.
*   Information about pages visited on your site where Muchat is active.
*   WooCommerce product interaction data (if WooCommerce is active and integrated).

We are committed to user privacy. For more detailed information about how we collect, use, and protect data, please visit our full privacy policy page https://docs.mu.chat/privacy/privacy-policy.

== Troubleshooting ==

= "Token validation failed: User has blocked requests through HTTP" error =

This error indicates that your WordPress installation is configured to block external HTTP requests, which Muchat needs to communicate with its core service.

To resolve this:

1.  Access your WordPress installation files (usually via FTP or your hosting control panel).
2.  Locate and edit the `wp-config.php` file in the root of your WordPress directory.
3.  Find if `define('WP_HTTP_BLOCK_EXTERNAL', true);` exists.
4.  You have two options:
    *   **Option A (Allow all external requests - less secure):**
        Change it to `define('WP_HTTP_BLOCK_EXTERNAL', false);`
    *   **Option B (Recommended - Allow specific hosts):**
        Add `app.mu.chat` to the list of allowed hosts. If `WP_ACCESSIBLE_HOSTS` is already defined, add `app.mu.chat` to the comma-separated list. Otherwise, add a new line:
        `define('WP_ACCESSIBLE_HOSTS', 'app.mu.chat,*.wordpress.org');` (Ensure `*.wordpress.org` is included if it was there or if you need updates for WordPress core/plugins/themes via HTTP).

**Important:** Use the exact domain `app.mu.chat`. Wildcard patterns like `*.mu.chat` are generally not supported by `WP_ACCESSIBLE_HOSTS`.
</file>

</files>
