<?php
namespace App\Repositories;

use App\Database\Connection;
use PDO;

class KpiRepository
{
    private $db;
    private $uiOracleCache = [];
    private static $activityTypeBackfillAttempted = false;

    public function __construct(Connection $db)
    {
        $this->db = $db;
    }

    /**
     * 1. فروش
     * تعداد معاملاتی که در بازه زمانی ساخته شده‌اند
     * مجموع مبلغ قابل پرداخت این معاملات
     * مجموع کل پرداخت‌های ثبت شده برای این معاملات (بدون محدودیت تاریخی)
     */
    public function getSales(array $filters): array
    {
        if ($this->getKpiPaymentDateBasis($filters, 'sales') === 'ui_payment_date') {
            return $this->getSalesUiOracle($filters);
        }
        $pdo = $this->db->getPdo();
        $parts = [];
        $params = [];
        $statusCsv = $this->normalizeCsvList($filters['deal_statuses_csv'] ?? '');
        $leadTypeCsv = $this->normalizeCsvList($filters['deal_lead_types_csv'] ?? '');

        $legacyClause = $this->buildLegacyDealFilterClause($filters, 'd');
        if ($legacyClause !== '') {
            $parts[] = $legacyClause;
        }

        // فروش کل بر اساس تاریخ پرداخت (نه تاریخ ثبت معامله)
        if (!empty($filters['date_from'])) {
            $parts[] = "pay.payment_date >= ?";
            $params[] = $filters['date_from'];
        }
        if (!empty($filters['date_to'])) {
            $parts[] = "pay.payment_date <= ?";
            $params[] = $filters['date_to'];
        }

        if (!empty($filters['owner_didar_ids_csv'])) {
            $parts[] = "FIND_IN_SET(d.owner_didar_id, ?)";
            $params[] = $filters['owner_didar_ids_csv'];
        } elseif (!empty($filters['owner_didar_id']) && trim((string)$filters['owner_didar_id']) !== '') {
            $parts[] = "d.owner_didar_id = ?";
            $params[] = trim((string)$filters['owner_didar_id']);
        }

        if (!empty($filters['product_ids_csv'])) {
            $parts[] = "EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = d.id AND FIND_IN_SET(dp.product_id, ?))";
            $params[] = $filters['product_ids_csv'];
        } elseif (!empty($filters['product_id'])) {
            $parts[] = "EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = d.id AND dp.product_id = ?)";
            $params[] = $filters['product_id'];
        }

        if ($statusCsv !== '' && !$this->csvHasAll($statusCsv, ['pending', 'won', 'lost'])) {
            $parts[] = "(
                (FIND_IN_SET('pending', ?) > 0 AND d.status NOT IN ('Won', 'Lost'))
                OR (FIND_IN_SET('won', ?) > 0 AND d.status = 'Won')
                OR (FIND_IN_SET('lost', ?) > 0 AND d.status = 'Lost')
            )";
            $params[] = $statusCsv;
            $params[] = $statusCsv;
            $params[] = $statusCsv;
        }

        if ($leadTypeCsv !== '' && !$this->csvHasAll($leadTypeCsv, ['new', 'old'])) {
            $parts[] = "(
                (FIND_IN_SET('new', ?) > 0 AND EXISTS (
                    SELECT 1 FROM persons pflt
                    WHERE pflt.didar_contact_id = d.contact_didar_id
                      AND DATE(COALESCE(pflt.register_time, pflt.created_at)) = DATE(COALESCE(d.register_time, d.created_at))
                ))
                OR (FIND_IN_SET('old', ?) > 0 AND EXISTS (
                    SELECT 1 FROM persons pflt
                    WHERE pflt.didar_contact_id = d.contact_didar_id
                      AND DATE(COALESCE(pflt.register_time, pflt.created_at)) < DATE(COALESCE(d.register_time, d.created_at))
                ))
            )";
            $params[] = $leadTypeCsv;
            $params[] = $leadTypeCsv;
        }

        $where = !empty($parts) ? implode(' AND ', $parts) : '1=1';
        $salesQualifierSql = $this->buildKpiFirstPaymentDayByDealSql($filters, 'sales');
        $salesAllTimeTotalsSql = $this->buildKpiAllTimePaymentTotalsByDealSql($filters, 'sales');

        $sql = "
            SELECT
                COUNT(*) as deal_count,
                SUM(s.payable_amount) as total_contract_amount,
                SUM(s.collected_amount) as total_collected_amount
            FROM (
                SELECT
                    d.id,
                    COALESCE(d.payable_amount, 0) as payable_amount,
                    SUM(COALESCE(ap.total_paid, 0)) as collected_amount
                FROM deals d
                INNER JOIN ({$salesQualifierSql}) pay ON pay.deal_id = d.id
                LEFT JOIN ({$salesAllTimeTotalsSql}) ap ON ap.deal_id = d.id
                WHERE {$where}
                  AND pay.payment_date >= DATE(COALESCE(d.register_time, d.created_at))
                GROUP BY d.id, d.payable_amount
            ) s
        ";

        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    /**
     * 2. تسویه حساب کامل
     * تعداد هر معامله که حداقل یک پرداخت ثبت شده دارد
     * و مجموع مبلغ پرداخت های ثبت شده آن با مبلغ قابل پرداخت آن معامله (name="payable_amount") برابر باشد
     * و مجموع مبلغ تسویه شده این معاملات تسویه شده
     */
    public function getSettledDeals(array $filters): array
    {
        $pdo = $this->db->getPdo();
        $whereClause = $this->buildWhereClauseForDeals($filters, 'd');
        $params = $this->buildParamsForDeals($filters);
        $paymentsByDealSql = $this->buildKpiAllTimePaymentTotalsByDealSql($filters, 'payment');

        $sql = "
            SELECT
                COUNT(DISTINCT d.id) as deal_count,
                SUM(d.payable_amount) as total_amount,
                SUM(COALESCE(tp.total_paid, 0)) as total_collected_amount
            FROM deals d
            LEFT JOIN ({$paymentsByDealSql}) tp ON tp.deal_id = d.id
            WHERE d.payable_amount IS NOT NULL
              AND d.payable_amount > 0
              AND COALESCE(tp.total_paid, 0) >= d.payable_amount
              AND {$whereClause}
        ";

        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    /**
     * 3. مانده حساب
     * تعداد معاملاتی که حداقل یک پرداخت ثبت شده دارد
     * و مجموع رقم پرداخت های ثبت شده آن با مبلغ قابل پرداخت آن معامله برابر نباشد
     * و مجموع مبلغ پرداخت شده این معاملات
     * و مجموع مبلغ پرداخت نشده این معاملات
     * بر اساس تفریق مبلغ قابل پرداخت از مجموع پرداخت های ثبت شده
     */
    public function getOutstandingBalance(array $filters): array
    {
        $pdo = $this->db->getPdo();
        $where = $this->buildWhereClauseForDeals($filters, 'd');
        $params = $this->buildParamsForDeals($filters);
        $paymentsByDealSql = $this->buildKpiAllTimePaymentTotalsByDealSql($filters, 'payment');

        $sql = "
            SELECT
                COUNT(DISTINCT d.id) as deal_count,
                SUM(d.payable_amount) as total_contract_amount,
                SUM(COALESCE(tp.total_paid, 0)) as total_collected,
                SUM(d.payable_amount - COALESCE(tp.total_paid, 0)) as outstanding_amount
            FROM deals d
            LEFT JOIN ({$paymentsByDealSql}) tp ON tp.deal_id = d.id
            WHERE d.payable_amount IS NOT NULL
              AND d.payable_amount > 0
              AND COALESCE(tp.total_paid, 0) > 0
              AND COALESCE(tp.total_paid, 0) < d.payable_amount
              AND {$where}
        ";

        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    /**
     * 4. فروش ناموفق
     * تعداد معاملاتی که در وضعیت ناموفق قرار دارند
     * و مجموع مبلغ قابل پرداخت معاملات ناموفق
     * همچنین دلایل ناموفق شدن معاملات باید تعداد هر دلیل مشخص باشه
     * قابل تفکیک باشد
     */
    public function getLostSales(array $filters): array
    {
        $pdo = $this->db->getPdo();
        $where = $this->buildWhereClauseForLostSales($filters);
        $params = $this->buildParamsForLostSales($filters);

        $sql = "
            SELECT
                COUNT(*) as total_lost,
                s.failure_reason_code,
                s.failure_reason_text,
                SUM(s.payable_amount) as total_amount
            FROM (
                SELECT
                    d.id,
                    d.payable_amount,
                    d.failure_reason_code,
                    COALESCE(
                        NULLIF(TRIM(d.failure_reason_text), ''),
                        NULLIF(TRIM(d.failure_reason_description), ''),
                        CASE TRIM(d.failure_reason_code)
                            WHEN 'no_financial_ability' THEN 'عدم توان مالی'
                            WHEN 'no_purchase_intent' THEN 'فعلا قصد خرید ندارد'
                            WHEN 'competitor_purchase' THEN 'خرید از رقبا'
                            WHEN 'wrong_number' THEN 'شماره اشتباه'
                            WHEN 'irrelevant_lead' THEN 'لید نامرتبط'
                            WHEN 'other' THEN 'سایر'
                            ELSE NULL
                        END,
                        NULLIF(TRIM(d.failure_reason_code), ''),
                        'نامشخص'
                    ) as failure_reason_text
                FROM deals d
                WHERE d.status = 'Lost'
                  AND {$where}
            ) s
            GROUP BY s.failure_reason_code, s.failure_reason_text
        ";

        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * 5. فروش جاری
     * تعداد و مجموع مبلغ قابل پرداخت همه معاملاتی که در وضعیت جاری قرار دارند
     */
    public function getOpenPipeline(array $filters): array
    {
        $pdo = $this->db->getPdo();
        $where = $this->buildWhereClauseForDeals($filters, 'd');
        $params = $this->buildParamsForDeals($filters);

        $sql = "
            SELECT
                COUNT(DISTINCT d.id) as deal_count,
                SUM(d.payable_amount) as total_potential_amount
            FROM deals d
            WHERE d.status NOT IN ('Won', 'Lost')
              AND {$where}
        ";

        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    /**
     * 5.5. معاملات بدون پرداخت
     * تعداد معاملاتی که هیچ transaction ندارند
     * شامل معاملات با payable_amount > 0 و همچنین معاملات با payable_amount = 0 یا NULL
     */
    public function getDealsWithoutPayments(array $filters): array
    {
        $pdo = $this->db->getPdo();
        $where = $this->buildWhereClauseForDeals($filters, 'd');
        $params = $this->buildParamsForDeals($filters);
        $paymentsByDealSql = $this->buildAbsolutePaymentPresenceByDealSql();

        $sql = "
            SELECT
                COUNT(DISTINCT d.id) as deal_count,
                SUM(COALESCE(d.payable_amount, 0)) as total_potential_amount
            FROM deals d
            LEFT JOIN ({$paymentsByDealSql}) tp ON tp.deal_id = d.id
            WHERE d.status NOT IN ('Won', 'Lost')
              AND (tp.total_paid IS NULL OR tp.total_paid = 0)
              AND {$where}
        ";

        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    /**
     * تعداد فروش محصولاتی که حداقل یک پرداخت در بازه زمانی دارند
     */
    public function getProductSalesCounts(array $filters): array
    {
        $pdo = $this->db->getPdo();
        $parts = [];
        $params = [];
        $dealProductHintExpr = $this->buildDealProductHintExpr('d');
        $resolvedProductExpr = $this->buildResolvedProductNameExpr($dealProductHintExpr, 'p');
        $primaryDealProductSql = $this->buildPrimaryDealProductSql();

        $legacyClause = $this->buildLegacyDealFilterClause($filters, 'd');
        if ($legacyClause !== '') {
            $parts[] = $legacyClause;
        }
        if (!empty($filters['owner_didar_ids_csv'])) {
            $parts[] = "FIND_IN_SET(d.owner_didar_id, ?)";
            $params[] = $filters['owner_didar_ids_csv'];
        } elseif (!empty($filters['owner_didar_id']) && trim($filters['owner_didar_id']) !== '') {
            $parts[] = "d.owner_didar_id = ?";
            $params[] = trim($filters['owner_didar_id']);
        }
        if (!empty($filters['product_ids_csv'])) {
            $parts[] = "FIND_IN_SET(dp.product_id, ?)";
            $params[] = $filters['product_ids_csv'];
        } elseif (!empty($filters['product_id'])) {
            $parts[] = "dp.product_id = ?";
            $params[] = $filters['product_id'];
        }

        $paymentsByDealDateSql = $this->buildKpiPaymentsWindowByDealDateSql($filters, 'sales');
        $paymentDateWhere = [];
        $paymentParams = [];
        if (!empty($filters['date_from'])) {
            $paymentDateWhere[] = "pbd.payment_date >= ?";
            $paymentParams[] = $filters['date_from'];
        }
        if (!empty($filters['date_to'])) {
            $paymentDateWhere[] = "pbd.payment_date <= ?";
            $paymentParams[] = $filters['date_to'];
        }
        $paymentDateClause = !empty($paymentDateWhere) ? implode(' AND ', $paymentDateWhere) : '1=1';
        $where = !empty($parts) ? implode(' AND ', $parts) : '1=1';

        $sql = "
            SELECT
                MIN(dp.product_id) AS product_id,
                {$resolvedProductExpr} AS product_name,
                COUNT(DISTINCT d.id) AS deal_count
            FROM deals d
            INNER JOIN (
                SELECT DISTINCT pbd.deal_id
                FROM ({$paymentsByDealDateSql}) pbd
                WHERE {$paymentDateClause}
            ) pay ON pay.deal_id = d.id
            INNER JOIN ({$primaryDealProductSql}) dp ON dp.deal_id = d.id
            LEFT JOIN products p ON p.id = dp.product_id
            WHERE {$where}
            GROUP BY {$resolvedProductExpr}
            ORDER BY deal_count DESC, product_name ASC
        ";

        $stmt = $pdo->prepare($sql);
        $stmt->execute(array_merge($paymentParams, $params));
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * 6. نرخ تبدیل و نرخ عدم تبدیل لید جدید
     * تعداد معامله ثبت لید های جدید (یعنی لیدی که در آن بازه تاریخی برای اولین بار در دیتابیس ثبت شده)
     * برای هر کاربر
     * به همراه نرخ عدم تبدیل برای همین معاملات و لیدها بر اساس معاملاتی که هنوز براشون پرداختی ثبت نشده
     * 
     * SIMPLIFIED VERSION: Uses person.register_time as basis for "new lead" definition
     */
    public function getDealConversionRate(array $filters): array
    {
        if ($this->getKpiPaymentDateBasis($filters, 'sales') === 'ui_payment_date') {
            return $this->getDealConversionRateUiOracle($filters);
        }
        $pdo = $this->db->getPdo();
        $where = $this->buildWhereClauseForDeals($filters, 'd');
        $params = $this->buildParamsForDeals($filters);
        $paymentsByDealSql = $this->buildKpiPaymentsByDealSql($filters, 'sales');
        $firstPaymentAllTimeFilters = $filters;
        unset($firstPaymentAllTimeFilters['date_from'], $firstPaymentAllTimeFilters['date_to']);
        $firstPaymentAllTimeFilters['kpi_sales_count_rule'] = 'first_payment_only';
        $firstPaymentAllTimeSql = $this->buildKpiPaymentsByDealSql($firstPaymentAllTimeFilters, 'sales', 'first_payment_only');

        $sql = "
            SELECT
                COUNT(DISTINCT d.id) as total_deals,
                COUNT(DISTINCT CASE
                    WHEN COALESCE(tp.total_paid, 0) > 0
                     AND (
                        fpa.first_payment_date IS NULL
                        OR fpa.first_payment_date >= DATE(COALESCE(d.register_time, d.created_at))
                     )
                    THEN d.id
                END) as converted_deals
            FROM deals d
            LEFT JOIN ({$paymentsByDealSql}) tp ON tp.deal_id = d.id
            LEFT JOIN ({$firstPaymentAllTimeSql}) fpa ON fpa.deal_id = d.id
            WHERE {$where}
              AND (
                fpa.first_payment_date IS NULL
                OR fpa.first_payment_date >= DATE(COALESCE(d.register_time, d.created_at))
              )
        ";

        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        $row = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];

        $totalDeals = (int)($row['total_deals'] ?? 0);
        $convertedDeals = (int)($row['converted_deals'] ?? 0);
        $conversionRate = $totalDeals > 0 ? round(($convertedDeals / $totalDeals) * 100, 2) : 0;
        $nonConversionRate = $totalDeals > 0 ? round((($totalDeals - $convertedDeals) / $totalDeals) * 100, 2) : 0;

        return [
            'total_deals' => $totalDeals,
            'converted_deals' => $convertedDeals,
            'conversion_rate' => $conversionRate,
            'non_conversion_rate' => $nonConversionRate
        ];
    }

    /**
     * نرخ تبدیل لید جدید (persons)
     * لید جدید: persons.register_time در بازه
     * تبدیل: حداقل یک معامله با پرداخت confirmed (بدون محدودیت تاریخ پرداخت)
     */
    public function getLeadConversionRate(array $filters): array
    {
        $pdo = $this->db->getPdo();
        $paymentsByDealSql = $this->buildKpiPaymentsByDealSql($filters, 'sales');

        $dateFrom = $filters['date_from'] ?? null;
        $dateTo = $filters['date_to'] ?? null;

        $personWhereParts = [];
        $personParams = [];

        if (!empty($dateFrom)) {
            $personWhereParts[] = "DATE(p.register_time) >= ?";
            $personParams[] = $dateFrom;
        }
        if (!empty($dateTo)) {
            $personWhereParts[] = "DATE(p.register_time) <= ?";
            $personParams[] = $dateTo;
        }
        if (!empty($filters['owner_didar_ids_csv'])) {
            $personWhereParts[] = "FIND_IN_SET(p.owner_didar_id, ?)";
            $personParams[] = $filters['owner_didar_ids_csv'];
        } elseif (!empty($filters['owner_didar_id'])) {
            $personWhereParts[] = "p.owner_didar_id = ?";
            $personParams[] = $filters['owner_didar_id'];
        }

        $personWhere = !empty($personWhereParts) ? implode(' AND ', $personWhereParts) : '1=1';

        $leadsSql = "
            SELECT COUNT(DISTINCT p.didar_contact_id) as total_new_leads
            FROM persons p
            WHERE {$personWhere}
        ";
        $stmt = $pdo->prepare($leadsSql);
        $stmt->execute($personParams);
        $totalNewLeads = (int) $stmt->fetchColumn();

        $convertedWhereParts = $personWhereParts;
        $convertedParams = $personParams;

        if (!empty($filters['owner_didar_ids_csv'])) {
            $convertedWhereParts[] = "FIND_IN_SET(d.owner_didar_id, ?)";
            $convertedParams[] = $filters['owner_didar_ids_csv'];
        } elseif (!empty($filters['owner_didar_id'])) {
            $convertedWhereParts[] = "d.owner_didar_id = ?";
            $convertedParams[] = $filters['owner_didar_id'];
        }
        if (!empty($filters['product_ids_csv'])) {
            $convertedWhereParts[] = "EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = d.id AND FIND_IN_SET(dp.product_id, ?))";
            $convertedParams[] = $filters['product_ids_csv'];
        } elseif (!empty($filters['product_id'])) {
            $convertedWhereParts[] = "EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = d.id AND dp.product_id = ?)";
            $convertedParams[] = $filters['product_id'];
        }

        $convertedWhere = !empty($convertedWhereParts) ? implode(' AND ', $convertedWhereParts) : '1=1';

        $convertedSql = "
            SELECT COUNT(DISTINCT p.didar_contact_id) as converted_leads
            FROM persons p
            INNER JOIN deals d ON d.contact_didar_id = p.didar_contact_id
            INNER JOIN ({$paymentsByDealSql}) tp ON tp.deal_id = d.id
            WHERE COALESCE(tp.total_paid, 0) > 0
              AND {$convertedWhere}
        ";
        $stmt = $pdo->prepare($convertedSql);
        $stmt->execute($convertedParams);
        $convertedLeads = (int) $stmt->fetchColumn();

        $conversionRate = $totalNewLeads > 0 ? round(($convertedLeads / $totalNewLeads) * 100, 2) : 0;
        $nonConversionRate = $totalNewLeads > 0 ? round((($totalNewLeads - $convertedLeads) / $totalNewLeads) * 100, 2) : 0;

        return [
            'total_new_leads' => $totalNewLeads,
            'converted_leads' => $convertedLeads,
            'conversion_rate' => $conversionRate,
            'non_conversion_rate' => $nonConversionRate
        ];
    }

    /**
     * Backward compatibility
     */
    public function getNewLeadConversionRate(array $filters): array
    {
        return $this->getLeadConversionRate($filters);
    }

    /**
     * 7. نرخ زمان تبدیل
     * نرخ مدت زمانی که طول کشیده از زمان ثبت یک معامله تا زمان تبدیل شدن آن معامله
     * یعنی از created_at تا اولین پرداخت
     * 
     * FIXED: Now calculates time from FIRST PAYMENT DATE to FIRST PAYMENT DATE (always 0)
     * This correctly represents that conversion happens at registration time (when first payment is received)
     */
    public function getTimeToConversion(array $filters): array
    {
        if ($this->getKpiPaymentDateBasis($filters, 'sales') === 'ui_payment_date') {
            $oracle = $this->getUiOracleKpiData($filters);
            $deals = (array)($oracle['state']['deals'] ?? []);
            $convCohort = (array)($oracle['sets']['conversion_cohort'] ?? []);
            $convConverted = (array)($oracle['sets']['conversion_converted'] ?? []);
            $salesFirstAll = (array)($oracle['aggs']['sales_first_all'] ?? []);

            $sumDays = 0.0;
            $countDays = 0;
            foreach ($convCohort as $dealId => $_) {
                $dealId = (string)$dealId;
                if (!isset($convConverted[$dealId])) {
                    continue;
                }
                $deal = $deals[$dealId] ?? null;
                if (!$deal) {
                    continue;
                }
                $dealDate = (string)($deal['deal_date'] ?? '');
                $firstDate = (string)($salesFirstAll[$dealId]['first'] ?? '');
                if ($dealDate === '' || $firstDate === '') {
                    continue;
                }
                try {
                    $diffDays = (int)((new \DateTime($firstDate))->diff(new \DateTime($dealDate)))->format('%r%a');
                } catch (\Throwable $e) {
                    continue;
                }
                if ($diffDays < 0) {
                    continue;
                }
                $sumDays += $diffDays;
                $countDays++;
            }

            return [
                'deal_count' => $countDays,
                'avg_conversion_days' => $countDays > 0 ? round($sumDays / $countDays, 2) : 0.0,
            ];
        }

        $pdo = $this->db->getPdo();
        $where = $this->buildWhereClauseForDeals($filters, 'd');
        $params = $this->buildParamsForDeals($filters);
        $firstPaymentAllTimeFilters = $filters;
        unset($firstPaymentAllTimeFilters['date_from'], $firstPaymentAllTimeFilters['date_to']);
        $firstPaymentAllTimeFilters['kpi_sales_count_rule'] = 'first_payment_only';
        $firstPaymentAllTimeSql = $this->buildKpiPaymentsByDealSql($firstPaymentAllTimeFilters, 'sales', 'first_payment_only');

        $sql = "
            SELECT
                COUNT(*) as deal_count,
                AVG(DATEDIFF(first_payment_date, deal_date)) as avg_conversion_days
            FROM (
                SELECT
                    d.id,
                    DATE(COALESCE(d.register_time, d.created_at)) as deal_date,
                    pay.first_payment_date
                FROM deals d
                INNER JOIN ({$firstPaymentAllTimeSql}) pay ON pay.deal_id = d.id
                WHERE COALESCE(pay.total_paid, 0) > 0
                  AND {$where}
                GROUP BY d.id
            ) s
            WHERE s.first_payment_date IS NOT NULL
              AND s.first_payment_date >= s.deal_date
        ";

        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    /**
     * فروش روزانه در ماه جاری
     */
    public function getDailySales(array $filters): array
    {
        $pdo = $this->db->getPdo();
        $parts = [];
        $params = [];

        $legacyClause = $this->buildLegacyDealFilterClause($filters, 'd');
        if ($legacyClause !== '') {
            $parts[] = $legacyClause;
        }
        if (!empty($filters['date_from'])) {
            $parts[] = "pay.payment_date >= ?";
            $params[] = $filters['date_from'];
        }
        if (!empty($filters['date_to'])) {
            $parts[] = "pay.payment_date <= ?";
            $params[] = $filters['date_to'];
        }
        if (!empty($filters['owner_didar_ids_csv'])) {
            $parts[] = "FIND_IN_SET(d.owner_didar_id, ?)";
            $params[] = $filters['owner_didar_ids_csv'];
        } elseif (!empty($filters['owner_didar_id']) && trim($filters['owner_didar_id']) !== '') {
            $parts[] = "d.owner_didar_id = ?";
            $params[] = trim($filters['owner_didar_id']);
        }
        if (!empty($filters['product_ids_csv'])) {
            $parts[] = "EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = d.id AND FIND_IN_SET(dp.product_id, ?))";
            $params[] = $filters['product_ids_csv'];
        } elseif (!empty($filters['product_id'])) {
            $parts[] = "EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = d.id AND dp.product_id = ?)";
            $params[] = $filters['product_id'];
        }

        $where = !empty($parts) ? implode(' AND ', $parts) : '1=1';
        $paymentsByDealDateSql = $this->buildKpiPaymentsWindowByDealDateSql($filters, 'sales');

        $sql = "
            SELECT
                pay.payment_date as date,
                COUNT(DISTINCT d.id) as deal_count,
                SUM(d.payable_amount) as total_amount,
                SUM(pay.amount) as collected_amount
            FROM deals d
            INNER JOIN ({$paymentsByDealDateSql}) pay ON pay.deal_id = d.id
            WHERE {$where}
            GROUP BY pay.payment_date
            ORDER BY pay.payment_date
        ";

        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * فعالیت روزانه به تفکیک نوع فعالیت (برای نمودار Stacked)
     */
    private function buildResolvedActivityTypeExpr(string $alias = 'a'): string
    {
        return "
            COALESCE(
                NULLIF(
                    TRIM(
                        CASE
                            WHEN TRIM(COALESCE({$alias}.activity_type_title, '')) = 'فعالیت' THEN ''
                            ELSE COALESCE({$alias}.activity_type_title, '')
                        END
                    ),
                    ''
                ),
                NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT({$alias}.raw_json, '$.ActivityTypeTitle'))), ''),
                NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT({$alias}.raw_json, '$.title'))), ''),
                NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT({$alias}.raw_json, '$.title_alt'))), ''),
                NULLIF(TRIM(COALESCE({$alias}.stage, '')), ''),
                'نامشخص'
            )
        ";
    }

    private function buildCanonicalActivityTypeExpr(string $alias = 'a'): string
    {
        $resolvedExpr = $this->buildResolvedActivityTypeExpr($alias);
        return "
            CASE
                WHEN LOWER({$resolvedExpr}) IN ('contact_no_answer', 'no_answer', 'contact_not_answered') THEN 'تماس عدم پاسخ'
                WHEN LOWER({$resolvedExpr}) IN ('contact_followup', 'followup', 'contact_new', 'new_contact') THEN 'تماس پیگیری'
                WHEN LOWER({$resolvedExpr}) IN ('payment_link', 'send_payment_link') THEN 'ارسال لینک پرداخت'
                WHEN LOWER({$resolvedExpr}) IN ('outstanding_followup', 'outstanding_balance_followup', 'payment_followup') THEN 'پیگیری مانده حساب'
                WHEN LOWER({$resolvedExpr}) IN ('payment', 'paid', 'successful_payment') THEN 'پرداخت'
                WHEN LOWER({$resolvedExpr}) IN ('course_delivered') THEN 'برگزاری دوره'
                WHEN LOWER({$resolvedExpr}) IN ('without_purchase') THEN 'ناموفق'
                WHEN ({$resolvedExpr}) LIKE '%عدم پاسخ%' OR ({$resolvedExpr}) LIKE '%بی پاسخ%' THEN 'تماس عدم پاسخ'
                WHEN ({$resolvedExpr}) LIKE '%پیگیری مانده%' THEN 'پیگیری مانده حساب'
                WHEN ({$resolvedExpr}) LIKE '%ارسال لینک%' THEN 'ارسال لینک پرداخت'
                WHEN ({$resolvedExpr}) LIKE '%برگزاری%' OR ({$resolvedExpr}) LIKE '%دوره%' THEN 'برگزاری دوره'
                WHEN ({$resolvedExpr}) LIKE '%پشتیبانی%' THEN 'پشتیبانی'
                WHEN ({$resolvedExpr}) LIKE '%ارجاع%' OR LOWER({$resolvedExpr}) LIKE '%crm%' THEN 'ارجاع به CRM'
                WHEN ({$resolvedExpr}) LIKE '%ناموفق%' THEN 'ناموفق'
                WHEN ({$resolvedExpr}) LIKE '%پرداخت%' OR ({$resolvedExpr}) LIKE '%واریز%' THEN 'پرداخت'
                WHEN ({$resolvedExpr}) LIKE '%پیگیری%' OR ({$resolvedExpr}) LIKE '%تماس%' OR ({$resolvedExpr}) LIKE '%خروجی%' THEN 'تماس پیگیری'
                ELSE {$resolvedExpr}
            END
        ";
    }

    private function decodeActivityTypeTitleFromRawJson(string $rawJson): string
    {
        $rawJson = trim($rawJson);
        if ($rawJson === '') {
            return '';
        }
        $decoded = json_decode($rawJson, true);
        if (!is_array($decoded)) {
            return '';
        }
        $title = trim((string)($decoded['ActivityTypeTitle'] ?? ''));
        if ($title !== '') {
            return $title;
        }
        $title = trim((string)($decoded['title'] ?? ''));
        if ($title !== '' && $title !== 'فعالیت') {
            return $title;
        }
        $title = trim((string)($decoded['title_alt'] ?? ''));
        if ($title !== '' && $title !== 'فعالیت') {
            return $title;
        }
        if (isset($decoded['Activity']) && is_array($decoded['Activity'])) {
            $activityTitle = trim((string)($decoded['Activity']['ActivityTypeTitle'] ?? ''));
            if ($activityTitle !== '') {
                return $activityTitle;
            }
        }
        return '';
    }

    private function normalizeActivityTypeLabelPhp(string $value): string
    {
        $value = trim($value);
        $code = strtolower(str_replace([' ', '-', '/'], '_', $value));
        if ($value === '' || $value === 'فعالیت') {
            return 'نامشخص';
        }
        if (in_array($code, ['contact_no_answer', 'no_answer', 'contact_not_answered'], true)) {
            return 'تماس عدم پاسخ';
        }
        if (in_array($code, ['contact_followup', 'followup', 'contact_new', 'new_contact'], true)) {
            return 'تماس پیگیری';
        }
        if (in_array($code, ['payment_link', 'send_payment_link'], true)) {
            return 'ارسال لینک پرداخت';
        }
        if (in_array($code, ['outstanding_followup', 'outstanding_balance_followup', 'payment_followup'], true)) {
            return 'پیگیری مانده حساب';
        }
        if (in_array($code, ['payment', 'paid', 'successful_payment'], true)) {
            return 'پرداخت';
        }
        if (in_array($code, ['course_delivered'], true)) {
            return 'برگزاری دوره';
        }
        if (in_array($code, ['without_purchase'], true)) {
            return 'ناموفق';
        }
        if (mb_stripos($value, 'عدم پاسخ') !== false || mb_stripos($value, 'بی پاسخ') !== false) {
            return 'تماس عدم پاسخ';
        }
        if (mb_stripos($value, 'پیگیری مانده') !== false) {
            return 'پیگیری مانده حساب';
        }
        if (mb_stripos($value, 'ارسال لینک') !== false) {
            return 'ارسال لینک پرداخت';
        }
        if (mb_stripos($value, 'برگزاری') !== false || mb_stripos($value, 'دوره') !== false) {
            return 'برگزاری دوره';
        }
        if (mb_stripos($value, 'پشتیبانی') !== false) {
            return 'پشتیبانی';
        }
        if (mb_stripos($value, 'ارجاع') !== false || mb_stripos($value, 'crm') !== false) {
            return 'ارجاع به CRM';
        }
        if (mb_stripos($value, 'ناموفق') !== false) {
            return 'ناموفق';
        }
        if (mb_stripos($value, 'پرداخت') !== false || mb_stripos($value, 'واریز') !== false) {
            return 'پرداخت';
        }
        if (mb_stripos($value, 'پیگیری') !== false || mb_stripos($value, 'تماس') !== false || mb_stripos($value, 'خروجی') !== false) {
            return 'تماس پیگیری';
        }
        return $value;
    }

    public function ensureActivityTypeBackfill(): void
    {
        if (self::$activityTypeBackfillAttempted) {
            return;
        }
        self::$activityTypeBackfillAttempted = true;

        $pdo = $this->db->getPdo();
        $rows = $this->runSelectAll("
            SELECT
                a.id,
                COALESCE(a.activity_type_id, '') as activity_type_id,
                COALESCE(a.activity_type_title, '') as activity_type_title,
                COALESCE(a.raw_json, '') as raw_json,
                COALESCE(a.stage, '') as stage
            FROM activities a
            WHERE TRIM(COALESCE(a.activity_type_title, '')) = ''
               OR TRIM(COALESCE(a.activity_type_title, '')) = 'فعالیت'
               OR TRIM(COALESCE(a.activity_type_title, '')) = 'نامشخص'
            LIMIT 10000
        ");
        if (empty($rows)) {
            return;
        }

        $lookupTitles = [];
        foreach ($this->runSelectAll("
            SELECT COALESCE(li.value, '') as lookup_value, COALESCE(li.title, '') as lookup_title
            FROM lookup_items li
            INNER JOIN lookup_groups lg ON lg.id = li.group_id
            WHERE lg.code = 'activity_types_fallback'
        ") as $lookupRow) {
            $lookupValue = trim((string)($lookupRow['lookup_value'] ?? ''));
            if ($lookupValue !== '' && !isset($lookupTitles[$lookupValue])) {
                $lookupTitles[$lookupValue] = trim((string)($lookupRow['lookup_title'] ?? ''));
            }
        }

        $stmt = $pdo->prepare("UPDATE activities SET activity_type_title = ?, last_sync = NOW() WHERE id = ?");
        foreach ($rows as $row) {
            $rawTitle = $this->decodeActivityTypeTitleFromRawJson((string)($row['raw_json'] ?? ''));
            $lookupTitle = $lookupTitles[trim((string)($row['activity_type_id'] ?? ''))] ?? '';
            $title = trim((string)($row['activity_type_title'] ?? ''));
            if ($title === '' || $title === 'فعالیت' || $title === 'نامشخص') {
                $title = $rawTitle !== '' ? $rawTitle : $lookupTitle;
            }
            if ($title === '' || $title === 'فعالیت' || $title === 'نامشخص') {
                $title = $this->normalizeActivityTypeLabelPhp((string)($row['stage'] ?? ''));
            }
            $title = trim($title);
            if ($title === '' || $title === 'فعالیت') {
                $title = 'نامشخص';
            }
            $stmt->execute([$title, (int)$row['id']]);
        }
    }

    public function getDailyActivitiesByType(array $filters): array
    {
        $pdo = $this->db->getPdo();
        $parts = [];
        $params = [];

        if (!empty($filters['date_from'])) {
            $parts[] = "DATE(COALESCE(a.register_date, a.created_at)) >= ?";
            $params[] = $filters['date_from'];
        }
        if (!empty($filters['date_to'])) {
            $parts[] = "DATE(COALESCE(a.register_date, a.created_at)) <= ?";
            $params[] = $filters['date_to'];
        }
        if (!empty($filters['owner_didar_ids_csv'])) {
            $parts[] = "FIND_IN_SET(a.owner_didar_id, ?)";
            $params[] = $filters['owner_didar_ids_csv'];
        } elseif (!empty($filters['owner_didar_id']) && trim($filters['owner_didar_id']) !== '') {
            $parts[] = "a.owner_didar_id = ?";
            $params[] = trim($filters['owner_didar_id']);
        }

        $where = !empty($parts) ? implode(' AND ', $parts) : '1=1';
        $activityTypeExpr = $this->buildCanonicalActivityTypeExpr('a');

        $sql = "
            SELECT
                DATE(COALESCE(a.register_date, a.created_at)) as date,
                {$activityTypeExpr} as activity_type,
                COUNT(*) as activities_count
            FROM activities a
            WHERE {$where}
            GROUP BY DATE(COALESCE(a.register_date, a.created_at)), {$activityTypeExpr}
            ORDER BY date ASC, activity_type ASC
        ";

        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * KPI های عملیاتی روزانه (کلی + تفکیک کارشناس)
     */
    public function getDailyOperationalKpis(array $filters): array
    {
        $pdo = $this->db->getPdo();
        $paymentsByDealSql = $this->buildUnifiedPaymentsByDealSql();
        $dateFrom = !empty($filters['date_from']) ? $filters['date_from'] : date('Y-m-01');
        $dateTo = !empty($filters['date_to']) ? $filters['date_to'] : date('Y-m-t');
        $ownerFilterCsv = !empty($filters['owner_didar_ids_csv'])
            ? trim((string)$filters['owner_didar_ids_csv'])
            : (!empty($filters['owner_didar_id']) && trim((string)$filters['owner_didar_id']) !== '' ? trim((string)$filters['owner_didar_id']) : null);
        $productFilterCsv = !empty($filters['product_ids_csv'])
            ? trim((string)$filters['product_ids_csv'])
            : (!empty($filters['product_id']) ? (string)$filters['product_id'] : null);

        $overall = [];
        $byExpert = [];

        $initOverall = function (string $date) use (&$overall) {
            if (!isset($overall[$date])) {
                $overall[$date] = [
                    'date' => $date,
                    'new_leads_count' => 0,
                    'new_deals_for_existing_leads_count' => 0,
                    'activities_count' => 0,
                    'new_leads_with_activity_count' => 0,
                    'leads_created_with_payment_count' => 0,
                    'deals_created_with_payment_count' => 0,
                    'leads_without_activity_count' => 0,
                    'deals_without_activity_count' => 0
                ];
            }
        };

        $initExpert = function (string $date, string $ownerDidarId, string $ownerName) use (&$byExpert) {
            $key = $date . '|' . $ownerDidarId;
            if (!isset($byExpert[$key])) {
                $byExpert[$key] = [
                    'date' => $date,
                    'owner_didar_id' => $ownerDidarId,
                    'owner_name' => $ownerName !== '' ? $ownerName : 'بدون مسئول',
                    'new_leads_count' => 0,
                    'new_deals_for_existing_leads_count' => 0,
                    'activities_count' => 0,
                    'new_leads_with_activity_count' => 0,
                    'leads_created_with_payment_count' => 0,
                    'deals_created_with_payment_count' => 0,
                    'leads_without_activity_count' => 0,
                    'deals_without_activity_count' => 0
                ];
            }
            return $key;
        };

        // 1) لید جدید
        $params = [$dateFrom, $dateTo];
        $whereOwner = '';
        if ($ownerFilterCsv !== null) {
            $whereOwner = ' AND FIND_IN_SET(p.owner_didar_id, ?)';
            $params[] = $ownerFilterCsv;
        }
        $sql = "
            SELECT
                DATE(p.register_time) as report_date,
                COALESCE(p.owner_didar_id, '') as owner_didar_id,
                COALESCE(u.display_name, '') as owner_name,
                COUNT(*) as total_count
            FROM persons p
            LEFT JOIN users u ON u.didar_user_id = p.owner_didar_id
            WHERE DATE(p.register_time) BETWEEN ? AND ?
              {$whereOwner}
            GROUP BY DATE(p.register_time), COALESCE(p.owner_didar_id, ''), COALESCE(u.display_name, '')
        ";
        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $date = $row['report_date'];
            $count = (int)($row['total_count'] ?? 0);
            $initOverall($date);
            $overall[$date]['new_leads_count'] += $count;
            $expertKey = $initExpert($date, (string)$row['owner_didar_id'], (string)$row['owner_name']);
            $byExpert[$expertKey]['new_leads_count'] += $count;
        }

        // 2) معامله جدید برای لیدهای قبلی
        $params = [$dateFrom, $dateTo];
        $whereOwner = '';
        if ($ownerFilterCsv !== null) {
            $whereOwner = ' AND FIND_IN_SET(d.owner_didar_id, ?)';
            $params[] = $ownerFilterCsv;
        }
        $whereProduct = '';
        if ($productFilterCsv !== null && $productFilterCsv !== '') {
            $whereProduct = ' AND EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = d.id AND FIND_IN_SET(dp.product_id, ?))';
            $params[] = $productFilterCsv;
        }
        $sql = "
            SELECT
                DATE(COALESCE(d.register_time, d.created_at)) as report_date,
                COALESCE(d.owner_didar_id, '') as owner_didar_id,
                COALESCE(u.display_name, '') as owner_name,
                COUNT(*) as total_count
            FROM deals d
            INNER JOIN persons p ON p.didar_contact_id = d.contact_didar_id
            LEFT JOIN users u ON u.didar_user_id = d.owner_didar_id
            WHERE COALESCE(d.is_legacy_didar, 0) = 0
              AND DATE(COALESCE(d.register_time, d.created_at)) BETWEEN ? AND ?
              AND DATE(COALESCE(p.register_time, p.created_at)) < DATE(COALESCE(d.register_time, d.created_at))
              {$whereOwner}
              {$whereProduct}
            GROUP BY DATE(COALESCE(d.register_time, d.created_at)), COALESCE(d.owner_didar_id, ''), COALESCE(u.display_name, '')
        ";
        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $date = $row['report_date'];
            $count = (int)($row['total_count'] ?? 0);
            $initOverall($date);
            $overall[$date]['new_deals_for_existing_leads_count'] += $count;
            $expertKey = $initExpert($date, (string)$row['owner_didar_id'], (string)$row['owner_name']);
            $byExpert[$expertKey]['new_deals_for_existing_leads_count'] += $count;
        }

        // 3) تعداد فعالیت روزانه
        $params = [$dateFrom, $dateTo];
        $whereOwner = '';
        if ($ownerFilterCsv !== null) {
            $whereOwner = ' AND FIND_IN_SET(a.owner_didar_id, ?)';
            $params[] = $ownerFilterCsv;
        }
        $sql = "
            SELECT
                DATE(COALESCE(a.register_date, a.created_at)) as report_date,
                COALESCE(a.owner_didar_id, '') as owner_didar_id,
                COALESCE(u.display_name, '') as owner_name,
                COUNT(*) as total_count
            FROM activities a
            LEFT JOIN users u ON u.didar_user_id = a.owner_didar_id
            WHERE DATE(COALESCE(a.register_date, a.created_at)) BETWEEN ? AND ?
              {$whereOwner}
            GROUP BY DATE(COALESCE(a.register_date, a.created_at)), COALESCE(a.owner_didar_id, ''), COALESCE(u.display_name, '')
        ";
        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $date = $row['report_date'];
            $count = (int)($row['total_count'] ?? 0);
            $initOverall($date);
            $overall[$date]['activities_count'] += $count;
            $expertKey = $initExpert($date, (string)$row['owner_didar_id'], (string)$row['owner_name']);
            $byExpert[$expertKey]['activities_count'] += $count;
        }

        // 4) چند لید جدید همان روز فعالیت داشته‌اند
        $params = [$dateFrom, $dateTo];
        $whereOwner = '';
        if ($ownerFilterCsv !== null) {
            $whereOwner = ' AND FIND_IN_SET(p.owner_didar_id, ?)';
            $params[] = $ownerFilterCsv;
        }
        $sql = "
            SELECT
                DATE(p.register_time) as report_date,
                COALESCE(p.owner_didar_id, '') as owner_didar_id,
                COALESCE(u.display_name, '') as owner_name,
                COUNT(DISTINCT p.didar_contact_id) as total_count
            FROM persons p
            LEFT JOIN users u ON u.didar_user_id = p.owner_didar_id
            INNER JOIN activities a
                ON a.contact_didar_id = p.didar_contact_id
               AND DATE(COALESCE(a.register_date, a.created_at)) = DATE(p.register_time)
            WHERE DATE(p.register_time) BETWEEN ? AND ?
              {$whereOwner}
            GROUP BY DATE(p.register_time), COALESCE(p.owner_didar_id, ''), COALESCE(u.display_name, '')
        ";
        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $date = $row['report_date'];
            $count = (int)($row['total_count'] ?? 0);
            $initOverall($date);
            $overall[$date]['new_leads_with_activity_count'] += $count;
            $expertKey = $initExpert($date, (string)$row['owner_didar_id'], (string)$row['owner_name']);
            $byExpert[$expertKey]['new_leads_with_activity_count'] += $count;
        }

        // 5) لیدهای ثبت‌شده روز که هر زمان پرداخت داشته‌اند
        $params = [$dateFrom, $dateTo];
        $whereOwner = '';
        if ($ownerFilterCsv !== null) {
            $whereOwner = ' AND FIND_IN_SET(p.owner_didar_id, ?)';
            $params[] = $ownerFilterCsv;
        }
        $whereProductSub = '';
        if ($productFilterCsv !== null && $productFilterCsv !== '') {
            $whereProductSub = ' AND EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = d2.id AND FIND_IN_SET(dp.product_id, ?))';
            $params[] = $productFilterCsv;
        }
        $sql = "
            SELECT
                DATE(p.register_time) as report_date,
                COALESCE(p.owner_didar_id, '') as owner_didar_id,
                COALESCE(u.display_name, '') as owner_name,
                COUNT(DISTINCT CASE WHEN paid.contact_didar_id IS NOT NULL THEN p.didar_contact_id END) as total_count
            FROM persons p
            LEFT JOIN users u ON u.didar_user_id = p.owner_didar_id
            LEFT JOIN (
                SELECT DISTINCT d2.contact_didar_id
                FROM deals d2
                INNER JOIN ({$paymentsByDealSql}) p2 ON p2.deal_id = d2.id
                WHERE COALESCE(d2.is_legacy_didar, 0) = 0
                  {$whereProductSub}
            ) paid ON paid.contact_didar_id = p.didar_contact_id
            WHERE DATE(p.register_time) BETWEEN ? AND ?
              {$whereOwner}
            GROUP BY DATE(p.register_time), COALESCE(p.owner_didar_id, ''), COALESCE(u.display_name, '')
        ";
        // برای ساب‌کوئری product، پارامتر قبل از بازه می‌آید
        if ($productFilterCsv !== null && $productFilterCsv !== '') {
            $stmt = $pdo->prepare($sql);
            $execParams = [$productFilterCsv, $dateFrom, $dateTo];
            if ($ownerFilterCsv !== null) {
                $execParams[] = $ownerFilterCsv;
            }
            $stmt->execute($execParams);
        } else {
            $stmt = $pdo->prepare($sql);
            $stmt->execute($params);
        }
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $date = $row['report_date'];
            $count = (int)($row['total_count'] ?? 0);
            $initOverall($date);
            $overall[$date]['leads_created_with_payment_count'] += $count;
            $expertKey = $initExpert($date, (string)$row['owner_didar_id'], (string)$row['owner_name']);
            $byExpert[$expertKey]['leads_created_with_payment_count'] += $count;
        }

        // 6) معاملات ثبت‌شده روز که هر زمان پرداخت داشته‌اند
        $params = [$dateFrom, $dateTo];
        $whereOwner = '';
        if ($ownerFilterCsv !== null) {
            $whereOwner = ' AND FIND_IN_SET(d.owner_didar_id, ?)';
            $params[] = $ownerFilterCsv;
        }
        $whereProduct = '';
        if ($productFilterCsv !== null && $productFilterCsv !== '') {
            $whereProduct = ' AND EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = d.id AND FIND_IN_SET(dp.product_id, ?))';
            $params[] = $productFilterCsv;
        }
        $sql = "
            SELECT
                DATE(COALESCE(d.register_time, d.created_at)) as report_date,
                COALESCE(d.owner_didar_id, '') as owner_didar_id,
                COALESCE(u.display_name, '') as owner_name,
                COUNT(DISTINCT CASE WHEN paid.deal_id IS NOT NULL THEN d.id END) as total_count
            FROM deals d
            LEFT JOIN users u ON u.didar_user_id = d.owner_didar_id
            LEFT JOIN (
                SELECT DISTINCT deal_id
                FROM ({$paymentsByDealSql}) p3
            ) paid ON paid.deal_id = d.id
            WHERE COALESCE(d.is_legacy_didar, 0) = 0
              AND DATE(COALESCE(d.register_time, d.created_at)) BETWEEN ? AND ?
              {$whereOwner}
              {$whereProduct}
            GROUP BY DATE(COALESCE(d.register_time, d.created_at)), COALESCE(d.owner_didar_id, ''), COALESCE(u.display_name, '')
        ";
        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $date = $row['report_date'];
            $count = (int)($row['total_count'] ?? 0);
            $initOverall($date);
            $overall[$date]['deals_created_with_payment_count'] += $count;
            $expertKey = $initExpert($date, (string)$row['owner_didar_id'], (string)$row['owner_name']);
            $byExpert[$expertKey]['deals_created_with_payment_count'] += $count;
        }

        // 7) لیدهای ثبت‌شده روز که هیچ فعالیتی ندارند
        $params = [$dateFrom, $dateTo];
        $whereOwner = '';
        if ($ownerFilterCsv !== null) {
            $whereOwner = ' AND FIND_IN_SET(p.owner_didar_id, ?)';
            $params[] = $ownerFilterCsv;
        }
        $sql = "
            SELECT
                DATE(p.register_time) as report_date,
                COALESCE(p.owner_didar_id, '') as owner_didar_id,
                COALESCE(u.display_name, '') as owner_name,
                COUNT(DISTINCT p.didar_contact_id) as total_count
            FROM persons p
            LEFT JOIN users u ON u.didar_user_id = p.owner_didar_id
            LEFT JOIN activities a ON a.contact_didar_id = p.didar_contact_id
            WHERE DATE(p.register_time) BETWEEN ? AND ?
              {$whereOwner}
              AND a.id IS NULL
            GROUP BY DATE(p.register_time), COALESCE(p.owner_didar_id, ''), COALESCE(u.display_name, '')
        ";
        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $date = $row['report_date'];
            $count = (int)($row['total_count'] ?? 0);
            $initOverall($date);
            $overall[$date]['leads_without_activity_count'] += $count;
            $expertKey = $initExpert($date, (string)$row['owner_didar_id'], (string)$row['owner_name']);
            $byExpert[$expertKey]['leads_without_activity_count'] += $count;
        }

        // 8) معاملات ثبت‌شده روز که هیچ فعالیتی ندارند
        $params = [$dateFrom, $dateTo];
        $whereOwner = '';
        if ($ownerFilterCsv !== null) {
            $whereOwner = ' AND FIND_IN_SET(d.owner_didar_id, ?)';
            $params[] = $ownerFilterCsv;
        }
        $whereProduct = '';
        if ($productFilterCsv !== null && $productFilterCsv !== '') {
            $whereProduct = ' AND EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = d.id AND FIND_IN_SET(dp.product_id, ?))';
            $params[] = $productFilterCsv;
        }
        $sql = "
            SELECT
                DATE(COALESCE(d.register_time, d.created_at)) as report_date,
                COALESCE(d.owner_didar_id, '') as owner_didar_id,
                COALESCE(u.display_name, '') as owner_name,
                COUNT(DISTINCT d.id) as total_count
            FROM deals d
            LEFT JOIN users u ON u.didar_user_id = d.owner_didar_id
            LEFT JOIN activities a ON a.deal_didar_id = d.didar_deal_id
            WHERE COALESCE(d.is_legacy_didar, 0) = 0
              AND DATE(COALESCE(d.register_time, d.created_at)) BETWEEN ? AND ?
              {$whereOwner}
              {$whereProduct}
              AND a.id IS NULL
            GROUP BY DATE(COALESCE(d.register_time, d.created_at)), COALESCE(d.owner_didar_id, ''), COALESCE(u.display_name, '')
        ";
        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $date = $row['report_date'];
            $count = (int)($row['total_count'] ?? 0);
            $initOverall($date);
            $overall[$date]['deals_without_activity_count'] += $count;
            $expertKey = $initExpert($date, (string)$row['owner_didar_id'], (string)$row['owner_name']);
            $byExpert[$expertKey]['deals_without_activity_count'] += $count;
        }

        // 9) تفکیک نوع فعالیت روزانه به ازای کارشناس
        $params = [$dateFrom, $dateTo];
        $whereOwner = '';
        if ($ownerFilterCsv !== null) {
            $whereOwner = ' AND FIND_IN_SET(a.owner_didar_id, ?)';
            $params[] = $ownerFilterCsv;
        }
        $activityTypeExpr = $this->buildCanonicalActivityTypeExpr('a');
        $sql = "
            SELECT
                DATE(COALESCE(a.register_date, a.created_at)) as report_date,
                COALESCE(a.owner_didar_id, '') as owner_didar_id,
                COALESCE(u.display_name, '') as owner_name,
                {$activityTypeExpr} as activity_type,
                COUNT(*) as total_count
            FROM activities a
            LEFT JOIN users u ON u.didar_user_id = a.owner_didar_id
            WHERE DATE(COALESCE(a.register_date, a.created_at)) BETWEEN ? AND ?
              {$whereOwner}
            GROUP BY DATE(COALESCE(a.register_date, a.created_at)), COALESCE(a.owner_didar_id, ''), COALESCE(u.display_name, ''), {$activityTypeExpr}
            ORDER BY report_date ASC, owner_name ASC, activity_type ASC
        ";
        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        $activityTypeByExpertDaily = $stmt->fetchAll(PDO::FETCH_ASSOC);

        // 10) لیدهای جدید همان روز + چه فعالیتی + توسط کدام کارشناس
        $params = [$dateFrom, $dateTo];
        $whereOwner = '';
        if ($ownerFilterCsv !== null) {
            $whereOwner = ' AND FIND_IN_SET(p.owner_didar_id, ?)';
            $params[] = $ownerFilterCsv;
        }
        $activityTypeExpr = $this->buildCanonicalActivityTypeExpr('a');
        $sql = "
            SELECT
                DATE(p.register_time) as report_date,
                COALESCE(p.owner_didar_id, '') as lead_owner_didar_id,
                COALESCE(lu.display_name, '') as lead_owner_name,
                COALESCE(a.owner_didar_id, '') as activity_owner_didar_id,
                COALESCE(au.display_name, '') as activity_owner_name,
                {$activityTypeExpr} as activity_type,
                COUNT(*) as activity_count,
                COUNT(DISTINCT p.didar_contact_id) as leads_count
            FROM persons p
            INNER JOIN activities a
                ON a.contact_didar_id = p.didar_contact_id
               AND DATE(COALESCE(a.register_date, a.created_at)) = DATE(p.register_time)
            LEFT JOIN users lu ON lu.didar_user_id = p.owner_didar_id
            LEFT JOIN users au ON au.didar_user_id = a.owner_didar_id
            WHERE DATE(p.register_time) BETWEEN ? AND ?
              {$whereOwner}
            GROUP BY
                DATE(p.register_time),
                COALESCE(p.owner_didar_id, ''),
                COALESCE(lu.display_name, ''),
                COALESCE(a.owner_didar_id, ''),
                COALESCE(au.display_name, ''),
                {$activityTypeExpr}
            ORDER BY report_date ASC, lead_owner_name ASC, activity_owner_name ASC, activity_type ASC
        ";
        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        $newLeadsActivityByExpertDaily = $stmt->fetchAll(PDO::FETCH_ASSOC);

        ksort($overall);
        usort($byExpert, function ($a, $b) {
            if ($a['date'] === $b['date']) {
                return strcmp((string)$a['owner_name'], (string)$b['owner_name']);
            }
            return strcmp((string)$a['date'], (string)$b['date']);
        });

        return [
            'overall_daily' => array_values($overall),
            'by_expert_daily' => array_values($byExpert),
            'activity_type_by_expert_daily' => $activityTypeByExpertDaily,
            'new_leads_activity_by_expert_daily' => $newLeadsActivityByExpertDaily
        ];
    }

    /**
     * جدول‌های پیشرفته KPI (روزانه + تفکیک کارشناس)
     */
    public function getAdvancedKpiTables(array $filters): array
    {
        $skipActivityTables = !empty($filters['_perf_skip_activity_tables']);
        $whereDeals = $this->buildWhereClauseForDeals($filters, 'd');
        $paramsDeals = $this->buildParamsForDeals($filters);
        $statusCsv = $this->normalizeCsvList($filters['deal_statuses_csv'] ?? '');
        $leadTypeCsv = $this->normalizeCsvList($filters['deal_lead_types_csv'] ?? '');
        $paymentsByDealSql = $this->buildUnifiedPaymentsByDealSql();
        $legacyOpenClause = $this->buildLegacyDealFilterClause($filters, 'd2');
        $legacyOpenClause = $legacyOpenClause !== '' ? (' AND ' . $legacyOpenClause) : '';
        $legacyDealsClauseForD = $this->buildLegacyDealFilterClause($filters, 'd');
        $legacyDealsClauseForD = $legacyDealsClauseForD !== '' ? (' AND ' . $legacyDealsClauseForD) : '';
        $legacyDealsClauseForD4 = $this->buildLegacyDealFilterClause($filters, 'd4');
        $legacyDealsClauseForD4 = $legacyDealsClauseForD4 !== '' ? (' AND ' . $legacyDealsClauseForD4) : '';

        $dealsBaseSql = "
            SELECT
                d.id as deal_id,
                DATE(COALESCE(d.register_time, d.created_at)) as deal_date,
                COALESCE(d.owner_didar_id, '') as owner_didar_id,
                COALESCE(u.display_name, 'بدون مسئول') as owner_name,
                COALESCE(d.contact_didar_id, '') as contact_didar_id,
                {$this->buildDealProductHintExpr('d')} as deal_product_hint,
                CASE
                    WHEN DATE(COALESCE(p.register_time, p.created_at)) = DATE(COALESCE(d.register_time, d.created_at)) THEN 'new'
                    ELSE 'old'
                END as lead_type,
                CASE
                    WHEN d.status = 'Won' THEN 'won'
                    WHEN d.status = 'Lost' THEN 'lost'
                    ELSE 'pending'
                END as deal_status_group,
                COALESCE(d.payable_amount, 0) as payable_amount,
                COALESCE(pay.total_paid, 0) as total_paid,
                pay.first_payment_date,
                DATE(d.lost_time) as lost_date,
                CASE WHEN COALESCE(pay.total_paid, 0) > 0 THEN 1 ELSE 0 END as has_payment,
                CASE WHEN COALESCE(od.open_count, 0) > 0 THEN 1 ELSE 0 END as has_open_deal,
                COALESCE(
                    NULLIF(TRIM(d.failure_reason_text), ''),
                    NULLIF(TRIM(d.failure_reason_description), ''),
                    CASE TRIM(d.failure_reason_code)
                        WHEN 'no_financial_ability' THEN 'عدم توان مالی'
                        WHEN 'no_purchase_intent' THEN 'فعلا قصد خرید ندارد'
                        WHEN 'competitor_purchase' THEN 'خرید از رقبا'
                        WHEN 'wrong_number' THEN 'شماره اشتباه'
                        WHEN 'irrelevant_lead' THEN 'لید نامرتبط'
                        WHEN 'other' THEN 'سایر'
                        ELSE NULL
                    END,
                    NULLIF(TRIM(d.failure_reason_code), ''),
                    'نامشخص'
                ) as failure_reason
            FROM deals d
            LEFT JOIN users u ON u.didar_user_id = d.owner_didar_id
            LEFT JOIN persons p ON p.didar_contact_id = d.contact_didar_id
            LEFT JOIN ({$paymentsByDealSql}) pay ON pay.deal_id = d.id
            LEFT JOIN (
                SELECT
                    d2.contact_didar_id,
                    COUNT(*) as open_count
                FROM deals d2
                WHERE d2.status NOT IN ('Won', 'Lost')
                  {$legacyOpenClause}
                GROUP BY d2.contact_didar_id
            ) od ON od.contact_didar_id = d.contact_didar_id
            WHERE {$whereDeals}
        ";

        $tableDealStatus = $this->runSelectAll("
            SELECT
                b.deal_date as date,
                b.owner_didar_id,
                b.owner_name,
                COUNT(*) as deal_total,
                SUM(CASE WHEN b.lead_type = 'new' THEN 1 ELSE 0 END) as deal_new_total,
                SUM(CASE WHEN b.lead_type = 'old' THEN 1 ELSE 0 END) as deal_old_total,
                SUM(CASE WHEN b.deal_status_group = 'pending' THEN 1 ELSE 0 END) as deal_pending_total,
                SUM(CASE WHEN b.deal_status_group = 'pending' AND b.lead_type = 'new' THEN 1 ELSE 0 END) as deal_pending_new,
                SUM(CASE WHEN b.deal_status_group = 'pending' AND b.lead_type = 'old' THEN 1 ELSE 0 END) as deal_pending_old,
                SUM(CASE WHEN b.has_payment = 1 THEN 1 ELSE 0 END) as deal_with_sales_total,
                SUM(CASE WHEN b.has_payment = 1 AND b.lead_type = 'new' THEN 1 ELSE 0 END) as deal_with_sales_new,
                SUM(CASE WHEN b.has_payment = 1 AND b.lead_type = 'old' THEN 1 ELSE 0 END) as deal_with_sales_old,
                SUM(CASE WHEN b.has_payment = 0 AND b.deal_status_group = 'pending' THEN 1 ELSE 0 END) as deal_without_sales_total,
                SUM(CASE WHEN b.has_payment = 0 AND b.deal_status_group = 'pending' AND b.lead_type = 'new' THEN 1 ELSE 0 END) as deal_without_sales_new,
                SUM(CASE WHEN b.has_payment = 0 AND b.deal_status_group = 'pending' AND b.lead_type = 'old' THEN 1 ELSE 0 END) as deal_without_sales_old,
                SUM(CASE WHEN b.deal_status_group = 'won' THEN 1 ELSE 0 END) as deal_won_total,
                SUM(CASE WHEN b.deal_status_group = 'won' AND b.lead_type = 'new' THEN 1 ELSE 0 END) as deal_won_new,
                SUM(CASE WHEN b.deal_status_group = 'won' AND b.lead_type = 'old' THEN 1 ELSE 0 END) as deal_won_old,
                SUM(CASE WHEN b.deal_status_group = 'lost' THEN 1 ELSE 0 END) as deal_lost_total,
                SUM(CASE WHEN b.deal_status_group = 'lost' AND b.lead_type = 'new' THEN 1 ELSE 0 END) as deal_lost_new,
                SUM(CASE WHEN b.deal_status_group = 'lost' AND b.lead_type = 'old' THEN 1 ELSE 0 END) as deal_lost_old
            FROM ({$dealsBaseSql}) b
            GROUP BY b.deal_date, b.owner_didar_id, b.owner_name
            ORDER BY b.deal_date ASC, b.owner_name ASC
        ", $paramsDeals);

        $dateFrom = !empty($filters['date_from']) ? (string)$filters['date_from'] : date('Y-m-01');
        $dateTo = !empty($filters['date_to']) ? (string)$filters['date_to'] : date('Y-m-t');
        $leadParams = [$dateFrom, $dateTo];
        $leadOwnerWhere = '';
        if (!empty($filters['owner_didar_ids_csv'])) {
            $leadOwnerWhere = ' AND FIND_IN_SET(p.owner_didar_id, ?)';
            $leadParams[] = $filters['owner_didar_ids_csv'];
        } elseif (!empty($filters['owner_didar_id']) && trim((string)$filters['owner_didar_id']) !== '') {
            $leadOwnerWhere = ' AND p.owner_didar_id = ?';
            $leadParams[] = trim((string)$filters['owner_didar_id']);
        }

        $leadDealExistsClause = '';
        $leadDealFilterParts = [];
        $leadDealFilterParams = [];
        $applyLeadDealFilters =
            !empty($filters['product_ids_csv']) ||
            !empty($filters['product_id']) ||
            ($statusCsv !== '' && !$this->csvHasAll($statusCsv, ['pending', 'won', 'lost'])) ||
            ($leadTypeCsv !== '' && !$this->csvHasAll($leadTypeCsv, ['new', 'old']));

        if ($applyLeadDealFilters) {
            if ($this->db->hasColumn('deals', 'is_legacy_didar')) {
                $leadDealFilterParts[] = "COALESCE(d3.is_legacy_didar, 0) = 0";
            }
            $leadDealFilterParts[] = "DATE(COALESCE(d3.register_time, d3.created_at)) BETWEEN ? AND ?";
            $leadDealFilterParams[] = $dateFrom;
            $leadDealFilterParams[] = $dateTo;

            if (!empty($filters['product_ids_csv'])) {
                $leadDealFilterParts[] = "EXISTS (SELECT 1 FROM deal_products dp3 WHERE dp3.deal_id = d3.id AND FIND_IN_SET(dp3.product_id, ?))";
                $leadDealFilterParams[] = $filters['product_ids_csv'];
            } elseif (!empty($filters['product_id'])) {
                $leadDealFilterParts[] = "EXISTS (SELECT 1 FROM deal_products dp3 WHERE dp3.deal_id = d3.id AND dp3.product_id = ?)";
                $leadDealFilterParams[] = $filters['product_id'];
            }

            if ($statusCsv !== '' && !$this->csvHasAll($statusCsv, ['pending', 'won', 'lost'])) {
                $leadDealFilterParts[] = "(
                    (FIND_IN_SET('pending', ?) > 0 AND d3.status NOT IN ('Won', 'Lost'))
                    OR (FIND_IN_SET('won', ?) > 0 AND d3.status = 'Won')
                    OR (FIND_IN_SET('lost', ?) > 0 AND d3.status = 'Lost')
                )";
                $leadDealFilterParams[] = $statusCsv;
                $leadDealFilterParams[] = $statusCsv;
                $leadDealFilterParams[] = $statusCsv;
            }

            if ($leadTypeCsv !== '' && !$this->csvHasAll($leadTypeCsv, ['new', 'old'])) {
                $leadDealFilterParts[] = "(
                    (FIND_IN_SET('new', ?) > 0 AND DATE(COALESCE(p.register_time, p.created_at)) = DATE(COALESCE(d3.register_time, d3.created_at)))
                    OR (FIND_IN_SET('old', ?) > 0 AND DATE(COALESCE(p.register_time, p.created_at)) < DATE(COALESCE(d3.register_time, d3.created_at)))
                )";
                $leadDealFilterParams[] = $leadTypeCsv;
                $leadDealFilterParams[] = $leadTypeCsv;
            }

            $leadDealExistsClause = "
                AND EXISTS (
                    SELECT 1
                    FROM deals d3
                    WHERE d3.contact_didar_id = p.didar_contact_id
                      AND " . implode(' AND ', $leadDealFilterParts) . "
                )
            ";
            $leadParams = array_merge($leadParams, $leadDealFilterParams);
        }

        $leadsBaseSql = "
            SELECT
                DATE(COALESCE(p.register_time, p.created_at)) as lead_date,
                COALESCE(p.owner_didar_id, '') as owner_didar_id,
                COALESCE(u.display_name, 'بدون مسئول') as owner_name,
                COALESCE(p.sale_status, 0) as sale_status,
                CASE WHEN COALESCE(od.open_count, 0) > 0 THEN 1 ELSE 0 END as has_open_deal,
                cp.first_payment_date,
                CASE WHEN cp.first_payment_date IS NOT NULL THEN 1 ELSE 0 END as has_sale_payment
            FROM persons p
            LEFT JOIN users u ON u.didar_user_id = p.owner_didar_id
            LEFT JOIN (
                SELECT
                    d2.contact_didar_id,
                    COUNT(*) as open_count
                FROM deals d2
                WHERE d2.status NOT IN ('Won', 'Lost')
                  {$legacyOpenClause}
                GROUP BY d2.contact_didar_id
            ) od ON od.contact_didar_id = p.didar_contact_id
            LEFT JOIN (
                SELECT
                    d4.contact_didar_id,
                    MIN(DATE(pay4.first_payment_date)) as first_payment_date
                FROM deals d4
                LEFT JOIN ({$paymentsByDealSql}) pay4 ON pay4.deal_id = d4.id
                WHERE pay4.first_payment_date IS NOT NULL
                  {$legacyDealsClauseForD4}
                GROUP BY d4.contact_didar_id
            ) cp ON cp.contact_didar_id = p.didar_contact_id
            WHERE DATE(COALESCE(p.register_time, p.created_at)) BETWEEN ? AND ?
              {$leadOwnerWhere}
              {$leadDealExistsClause}
        ";

        $tableLeadStatus = $this->runSelectAll("
            SELECT
                l.lead_date as date,
                l.owner_didar_id,
                l.owner_name,
                COUNT(*) as total_leads,
                SUM(CASE WHEN l.has_sale_payment = 1 THEN 1 ELSE 0 END) as leads_with_sales,
                SUM(CASE WHEN l.has_sale_payment = 1 AND l.first_payment_date = l.lead_date THEN 1 ELSE 0 END) as leads_with_new_sales,
                SUM(CASE WHEN l.has_sale_payment = 1 AND l.first_payment_date < l.lead_date THEN 1 ELSE 0 END) as leads_with_previous_sales,
                SUM(CASE WHEN l.has_sale_payment = 0 THEN 1 ELSE 0 END) as leads_without_sales,
                SUM(CASE WHEN l.has_sale_payment = 0 AND l.has_open_deal = 1 THEN 1 ELSE 0 END) as leads_without_sales_with_current_deal,
                SUM(CASE WHEN l.has_sale_payment = 0 AND l.has_open_deal = 0 THEN 1 ELSE 0 END) as leads_without_sales_without_current_deal,
                COUNT(*) as new_leads_count
            FROM ({$leadsBaseSql}) l
            GROUP BY l.lead_date, l.owner_didar_id, l.owner_name
            ORDER BY l.lead_date ASC, l.owner_name ASC
        ", $leadParams);

        $tablePaymentMix = $this->runSelectAll("
            SELECT
                b.deal_date as date,
                b.owner_didar_id,
                b.owner_name,
                COUNT(DISTINCT CASE WHEN b.lead_type = 'new' AND b.has_payment = 1 THEN b.contact_didar_id END) as new_leads_with_payment,
                COUNT(DISTINCT CASE WHEN b.lead_type = 'new' AND b.has_payment = 0 AND b.deal_status_group <> 'lost' THEN b.contact_didar_id END) as new_leads_without_payment,
                COUNT(DISTINCT CASE WHEN b.lead_type = 'new' AND b.deal_status_group = 'lost' THEN b.contact_didar_id END) as new_leads_failed,
                COUNT(DISTINCT CASE WHEN b.lead_type = 'old' AND b.has_payment = 1 THEN b.contact_didar_id END) as old_leads_with_payment,
                COUNT(DISTINCT CASE WHEN b.lead_type = 'old' AND b.has_payment = 0 AND b.deal_status_group <> 'lost' THEN b.contact_didar_id END) as old_leads_without_payment,
                COUNT(DISTINCT CASE WHEN b.lead_type = 'old' AND b.deal_status_group = 'lost' THEN b.contact_didar_id END) as old_leads_failed
            FROM ({$dealsBaseSql}) b
            GROUP BY b.deal_date, b.owner_didar_id, b.owner_name
            ORDER BY b.deal_date ASC, b.owner_name ASC
        ", $paramsDeals);

        $tablePaymentStatus = $this->runSelectAll("
            SELECT
                b.deal_date as date,
                b.owner_didar_id,
                b.owner_name,
                SUM(CASE WHEN b.has_payment = 1 AND b.payable_amount > 0 AND b.total_paid < b.payable_amount THEN 1 ELSE 0 END) as outstanding_deals_count,
                SUM(CASE WHEN b.has_payment = 1 AND b.payable_amount > 0 AND b.total_paid >= b.payable_amount THEN 1 ELSE 0 END) as settled_deals_count,
                SUM(CASE WHEN b.has_payment = 1 AND b.payable_amount > 0 AND b.total_paid < b.payable_amount THEN (b.payable_amount - b.total_paid) ELSE 0 END) as outstanding_amount_sum,
                SUM(CASE WHEN b.has_payment = 1 AND b.payable_amount > 0 AND b.total_paid >= b.payable_amount THEN b.payable_amount ELSE 0 END) as settled_contract_amount_sum,
                SUM(CASE WHEN b.has_payment = 1 THEN b.total_paid ELSE 0 END) as payments_sum
            FROM ({$dealsBaseSql}) b
            GROUP BY b.deal_date, b.owner_didar_id, b.owner_name
            ORDER BY b.deal_date ASC, b.owner_name ASC
        ", $paramsDeals);

        $tableConversionStatus = $this->runSelectAll("
            SELECT
                b.deal_date as date,
                b.owner_didar_id,
                b.owner_name,
                ROUND((SUM(CASE WHEN b.has_payment = 1 THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0)) * 100, 2) as conversion_rate_total,
                ROUND((SUM(CASE WHEN b.has_payment = 0 THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0)) * 100, 2) as non_conversion_rate_total,
                ROUND((SUM(CASE WHEN b.lead_type = 'new' AND b.has_payment = 1 THEN 1 ELSE 0 END) / NULLIF(SUM(CASE WHEN b.lead_type = 'new' THEN 1 ELSE 0 END), 0)) * 100, 2) as conversion_rate_new,
                ROUND((SUM(CASE WHEN b.lead_type = 'new' AND b.has_payment = 0 THEN 1 ELSE 0 END) / NULLIF(SUM(CASE WHEN b.lead_type = 'new' THEN 1 ELSE 0 END), 0)) * 100, 2) as non_conversion_rate_new,
                ROUND((SUM(CASE WHEN b.lead_type = 'old' AND b.has_payment = 1 THEN 1 ELSE 0 END) / NULLIF(SUM(CASE WHEN b.lead_type = 'old' THEN 1 ELSE 0 END), 0)) * 100, 2) as conversion_rate_old,
                ROUND((SUM(CASE WHEN b.lead_type = 'old' AND b.has_payment = 0 THEN 1 ELSE 0 END) / NULLIF(SUM(CASE WHEN b.lead_type = 'old' THEN 1 ELSE 0 END), 0)) * 100, 2) as non_conversion_rate_old,
                SUM(CASE WHEN b.has_payment = 1 THEN 1 ELSE 0 END) as sales_total_count,
                SUM(CASE WHEN b.lead_type = 'new' AND b.has_payment = 1 THEN 1 ELSE 0 END) as sales_new_count,
                SUM(CASE WHEN b.lead_type = 'old' AND b.has_payment = 1 THEN 1 ELSE 0 END) as sales_old_count,
                COUNT(*) as total_deals_count,
                SUM(CASE WHEN b.lead_type = 'new' THEN 1 ELSE 0 END) as total_new_deals_count,
                SUM(CASE WHEN b.lead_type = 'old' THEN 1 ELSE 0 END) as total_old_deals_count,
                SUM(CASE WHEN b.has_payment = 1 THEN b.total_paid ELSE 0 END) as total_collected_amount,
                SUM(CASE WHEN b.has_payment = 1 AND b.payable_amount > 0 AND b.total_paid < b.payable_amount THEN (b.payable_amount - b.total_paid) ELSE 0 END) as outstanding_amount_sum
            FROM ({$dealsBaseSql}) b
            GROUP BY b.deal_date, b.owner_didar_id, b.owner_name
            ORDER BY b.deal_date ASC, b.owner_name ASC
        ", $paramsDeals);

        $tableConversionTime = $this->runSelectAll("
            SELECT
                b.deal_date as date,
                b.owner_didar_id,
                b.owner_name,
                ROUND(AVG(CASE WHEN b.has_payment = 1 AND b.first_payment_date IS NOT NULL THEN DATEDIFF(b.first_payment_date, b.deal_date) END), 2) as avg_conversion_days_total,
                SUM(CASE WHEN b.has_payment = 1 AND b.first_payment_date IS NOT NULL THEN DATEDIFF(b.first_payment_date, b.deal_date) ELSE 0 END) as sum_conversion_days_total,
                SUM(CASE WHEN b.has_payment = 1 AND b.first_payment_date IS NOT NULL THEN 1 ELSE 0 END) as count_conversion_days_total,

                ROUND(AVG(CASE WHEN b.lead_type = 'new' AND b.has_payment = 1 AND b.first_payment_date IS NOT NULL THEN DATEDIFF(b.first_payment_date, b.deal_date) END), 2) as avg_conversion_days_new,
                SUM(CASE WHEN b.lead_type = 'new' AND b.has_payment = 1 AND b.first_payment_date IS NOT NULL THEN DATEDIFF(b.first_payment_date, b.deal_date) ELSE 0 END) as sum_conversion_days_new,
                SUM(CASE WHEN b.lead_type = 'new' AND b.has_payment = 1 AND b.first_payment_date IS NOT NULL THEN 1 ELSE 0 END) as count_conversion_days_new,

                ROUND(AVG(CASE WHEN b.lead_type = 'old' AND b.has_payment = 1 AND b.first_payment_date IS NOT NULL THEN DATEDIFF(b.first_payment_date, b.deal_date) END), 2) as avg_conversion_days_old,
                SUM(CASE WHEN b.lead_type = 'old' AND b.has_payment = 1 AND b.first_payment_date IS NOT NULL THEN DATEDIFF(b.first_payment_date, b.deal_date) ELSE 0 END) as sum_conversion_days_old,
                SUM(CASE WHEN b.lead_type = 'old' AND b.has_payment = 1 AND b.first_payment_date IS NOT NULL THEN 1 ELSE 0 END) as count_conversion_days_old,

                ROUND(AVG(CASE WHEN b.deal_status_group = 'lost' AND b.lost_date IS NOT NULL THEN DATEDIFF(b.lost_date, b.deal_date) END), 2) as avg_failure_days_total,
                SUM(CASE WHEN b.deal_status_group = 'lost' AND b.lost_date IS NOT NULL THEN DATEDIFF(b.lost_date, b.deal_date) ELSE 0 END) as sum_failure_days_total,
                SUM(CASE WHEN b.deal_status_group = 'lost' AND b.lost_date IS NOT NULL THEN 1 ELSE 0 END) as count_failure_days_total,

                ROUND(AVG(CASE WHEN b.lead_type = 'new' AND b.deal_status_group = 'lost' AND b.lost_date IS NOT NULL THEN DATEDIFF(b.lost_date, b.deal_date) END), 2) as avg_failure_days_new,
                SUM(CASE WHEN b.lead_type = 'new' AND b.deal_status_group = 'lost' AND b.lost_date IS NOT NULL THEN DATEDIFF(b.lost_date, b.deal_date) ELSE 0 END) as sum_failure_days_new,
                SUM(CASE WHEN b.lead_type = 'new' AND b.deal_status_group = 'lost' AND b.lost_date IS NOT NULL THEN 1 ELSE 0 END) as count_failure_days_new,

                ROUND(AVG(CASE WHEN b.lead_type = 'old' AND b.deal_status_group = 'lost' AND b.lost_date IS NOT NULL THEN DATEDIFF(b.lost_date, b.deal_date) END), 2) as avg_failure_days_old,
                SUM(CASE WHEN b.lead_type = 'old' AND b.deal_status_group = 'lost' AND b.lost_date IS NOT NULL THEN DATEDIFF(b.lost_date, b.deal_date) ELSE 0 END) as sum_failure_days_old,
                SUM(CASE WHEN b.lead_type = 'old' AND b.deal_status_group = 'lost' AND b.lost_date IS NOT NULL THEN 1 ELSE 0 END) as count_failure_days_old,
                SUM(CASE WHEN b.has_payment = 1 AND b.first_payment_date IS NOT NULL AND DATEDIFF(b.first_payment_date, b.deal_date) > 0 THEN 1 ELSE 0 END) as payments_after_deal_count,
                SUM(CASE WHEN b.has_payment = 1 AND b.first_payment_date IS NOT NULL AND DATEDIFF(b.first_payment_date, b.deal_date) < 0 THEN 1 ELSE 0 END) as payments_before_deal_count,
                SUM(CASE WHEN b.has_payment = 1 AND b.first_payment_date IS NOT NULL AND DATEDIFF(b.first_payment_date, b.deal_date) = 0 THEN 1 ELSE 0 END) as payments_same_day_count
            FROM ({$dealsBaseSql}) b
            GROUP BY b.deal_date, b.owner_didar_id, b.owner_name
            ORDER BY b.deal_date ASC, b.owner_name ASC
        ", $paramsDeals);

        $tableFailureReasons = $this->runSelectAll("
            SELECT
                b.deal_date as date,
                b.owner_didar_id,
                b.owner_name,
                b.failure_reason,
                SUM(CASE WHEN b.deal_status_group = 'lost' AND b.has_open_deal = 0 THEN 1 ELSE 0 END) as failed_without_current_count,
                SUM(CASE WHEN b.deal_status_group = 'lost' AND b.lead_type = 'new' THEN 1 ELSE 0 END) as failed_new_count,
                SUM(CASE WHEN b.deal_status_group = 'lost' AND b.lead_type = 'old' THEN 1 ELSE 0 END) as failed_old_count
            FROM ({$dealsBaseSql}) b
            WHERE b.deal_status_group = 'lost'
            GROUP BY b.deal_date, b.owner_didar_id, b.owner_name, b.failure_reason
            ORDER BY b.deal_date ASC, b.owner_name ASC, b.failure_reason ASC
        ", $paramsDeals);

        $resolvedProductExpr = $this->buildResolvedProductNameExpr('b.deal_product_hint', 'pr');
        $primaryDealProductSql = $this->buildPrimaryDealProductSql();

        $tableProductStatus = $this->runSelectAll("
            SELECT
                b.deal_date as date,
                b.owner_didar_id,
                b.owner_name,
                {$resolvedProductExpr} as product_name,
                COUNT(DISTINCT dp.deal_id) as sales_total_count,
                COUNT(DISTINCT CASE WHEN b.lead_type = 'new' THEN dp.deal_id END) as sales_new_count,
                COUNT(DISTINCT CASE WHEN b.lead_type = 'old' THEN dp.deal_id END) as sales_old_count
            FROM ({$dealsBaseSql}) b
            INNER JOIN ({$primaryDealProductSql}) dp ON dp.deal_id = b.deal_id
            LEFT JOIN products pr ON pr.id = dp.product_id
            WHERE b.has_payment = 1
            GROUP BY b.deal_date, b.owner_didar_id, b.owner_name, {$resolvedProductExpr}
            ORDER BY b.deal_date ASC, b.owner_name ASC, product_name ASC
        ", $paramsDeals);

        $tableProductSoldByOwnerDay = $this->runSelectAll("
            SELECT
                b.deal_date as date,
                b.owner_didar_id,
                b.owner_name,
                {$resolvedProductExpr} as product_name,
                COUNT(DISTINCT CASE WHEN b.lead_type = 'new' THEN b.deal_id END) as sold_new_count,
                COUNT(DISTINCT CASE WHEN b.lead_type = 'old' THEN b.deal_id END) as sold_old_count,
                COUNT(DISTINCT b.deal_id) as sold_total_count
            FROM ({$dealsBaseSql}) b
            INNER JOIN ({$primaryDealProductSql}) dp ON dp.deal_id = b.deal_id
            LEFT JOIN products pr ON pr.id = dp.product_id
            WHERE b.has_payment = 1
            GROUP BY b.deal_date, b.owner_didar_id, b.owner_name, {$resolvedProductExpr}
            ORDER BY b.deal_date ASC, b.owner_name ASC, product_name ASC
        ", $paramsDeals);

        $tableProductRequestsByOwnerDay = $this->runSelectAll("
            SELECT
                b.deal_date as date,
                b.owner_didar_id,
                b.owner_name,
                {$resolvedProductExpr} as product_name,
                COUNT(DISTINCT CASE WHEN b.lead_type = 'new' THEN b.deal_id END) as requested_new_count,
                COUNT(DISTINCT CASE WHEN b.lead_type = 'old' THEN b.deal_id END) as requested_old_count,
                COUNT(DISTINCT b.deal_id) as requested_total_count,
                COUNT(DISTINCT CASE WHEN b.has_payment = 1 THEN b.deal_id END) as sold_total_count,
                COUNT(DISTINCT CASE WHEN b.has_payment = 0 THEN b.deal_id END) as unsold_total_count
            FROM ({$dealsBaseSql}) b
            INNER JOIN ({$primaryDealProductSql}) dp ON dp.deal_id = b.deal_id
            LEFT JOIN products pr ON pr.id = dp.product_id
            GROUP BY b.deal_date, b.owner_didar_id, b.owner_name, {$resolvedProductExpr}
            ORDER BY b.deal_date ASC, b.owner_name ASC, product_name ASC
        ", $paramsDeals);

        $tableProductFailedByOwnerDay = $this->runSelectAll("
            SELECT
                b.deal_date as date,
                b.owner_didar_id,
                b.owner_name,
                {$resolvedProductExpr} as product_name,
                COUNT(DISTINCT CASE WHEN b.lead_type = 'new' THEN b.deal_id END) as failed_new_count,
                COUNT(DISTINCT CASE WHEN b.lead_type = 'old' THEN b.deal_id END) as failed_old_count,
                COUNT(DISTINCT b.deal_id) as failed_total_count
            FROM ({$dealsBaseSql}) b
            INNER JOIN ({$primaryDealProductSql}) dp ON dp.deal_id = b.deal_id
            LEFT JOIN products pr ON pr.id = dp.product_id
            WHERE b.has_payment = 0
            GROUP BY b.deal_date, b.owner_didar_id, b.owner_name, {$resolvedProductExpr}
            ORDER BY b.deal_date ASC, b.owner_name ASC, product_name ASC
        ", $paramsDeals);

        $tableActivitySummary = [];
        $tableActivityTypeDetails = [];
        if (!$skipActivityTables) {
            $activityTypeExpr = $this->buildCanonicalActivityTypeExpr('a');
            $activityOwnerWhereAct = '';
            $activityOwnerWherePersons = '';
            $activityOwnerWhereDeals = '';
            $activityOwnerParamsAct = [];
            $activityOwnerParamsPersons = [];
            $activityOwnerParamsDeals = [];

            if (!empty($filters['owner_didar_ids_csv'])) {
                $activityOwnerWhereAct = ' AND FIND_IN_SET(a.owner_didar_id, ?)';
                $activityOwnerWherePersons = ' AND FIND_IN_SET(p.owner_didar_id, ?)';
                $activityOwnerWhereDeals = ' AND FIND_IN_SET(d.owner_didar_id, ?)';
                $activityOwnerParamsAct = [$filters['owner_didar_ids_csv']];
                $activityOwnerParamsPersons = [$filters['owner_didar_ids_csv']];
                $activityOwnerParamsDeals = [$filters['owner_didar_ids_csv']];
            } elseif (!empty($filters['owner_didar_id']) && trim((string)$filters['owner_didar_id']) !== '') {
                $ownerDidarId = trim((string)$filters['owner_didar_id']);
                $activityOwnerWhereAct = ' AND a.owner_didar_id = ?';
                $activityOwnerWherePersons = ' AND p.owner_didar_id = ?';
                $activityOwnerWhereDeals = ' AND d.owner_didar_id = ?';
                $activityOwnerParamsAct = [$ownerDidarId];
                $activityOwnerParamsPersons = [$ownerDidarId];
                $activityOwnerParamsDeals = [$ownerDidarId];
            }

            $activitySummaryParams = array_merge(
                [$dateFrom, $dateTo],
                $activityOwnerParamsAct,
                [$dateFrom, $dateTo],
                $activityOwnerParamsPersons,
                [$dateFrom, $dateTo],
                $activityOwnerParamsDeals
            );

            $tableActivitySummary = $this->runSelectAll("
            SELECT
                b.activity_date as date,
                b.owner_didar_id,
                b.owner_name,
                b.total_activities,
                b.total_duration_minutes,
                b.avg_duration_minutes,
                b.activities_new_lead,
                COALESCE(n.new_leads_without_activity, 0) as new_leads_without_activity,
                b.activities_old_lead,
                COALESCE(o.old_leads_without_activity, 0) as old_leads_without_activity,
                b.no_response_new,
                b.no_response_old,
                (b.no_response_new + b.no_response_old) as no_response_total
            FROM (
                SELECT
                    DATE(COALESCE(a.register_date, a.created_at)) as activity_date,
                    COALESCE(a.owner_didar_id, '') as owner_didar_id,
                    COALESCE(u.display_name, 'بدون مسئول') as owner_name,
                    COUNT(*) as total_activities,
                    SUM(COALESCE(a.duration, 0)) as total_duration_minutes,
                    ROUND(AVG(CASE WHEN COALESCE(a.duration, 0) > 0 THEN a.duration END), 2) as avg_duration_minutes,
                    SUM(CASE WHEN DATE(COALESCE(p.register_time, p.created_at)) = DATE(COALESCE(a.register_date, a.created_at)) THEN 1 ELSE 0 END) as activities_new_lead,
                    SUM(CASE WHEN DATE(COALESCE(p.register_time, p.created_at)) < DATE(COALESCE(a.register_date, a.created_at)) THEN 1 ELSE 0 END) as activities_old_lead,
                    SUM(CASE
                        WHEN (
                            {$activityTypeExpr} = 'تماس عدم پاسخ'
                        )
                        AND DATE(COALESCE(p.register_time, p.created_at)) = DATE(COALESCE(a.register_date, a.created_at))
                        THEN 1 ELSE 0 END
                    ) as no_response_new,
                    SUM(CASE
                        WHEN (
                            {$activityTypeExpr} = 'تماس عدم پاسخ'
                        )
                        AND DATE(COALESCE(p.register_time, p.created_at)) < DATE(COALESCE(a.register_date, a.created_at))
                        THEN 1 ELSE 0 END
                    ) as no_response_old
                FROM activities a
                LEFT JOIN persons p ON p.didar_contact_id = a.contact_didar_id
                LEFT JOIN users u ON u.didar_user_id = a.owner_didar_id
                WHERE DATE(COALESCE(a.register_date, a.created_at)) BETWEEN ? AND ?
                  {$activityOwnerWhereAct}
                GROUP BY DATE(COALESCE(a.register_date, a.created_at)), COALESCE(a.owner_didar_id, ''), COALESCE(u.display_name, 'بدون مسئول')
            ) b
            LEFT JOIN (
                SELECT
                    DATE(COALESCE(p.register_time, p.created_at)) as activity_date,
                    COALESCE(p.owner_didar_id, '') as owner_didar_id,
                    COUNT(DISTINCT p.didar_contact_id) as new_leads_without_activity
                FROM persons p
                LEFT JOIN activities a
                    ON a.contact_didar_id = p.didar_contact_id
                   AND DATE(COALESCE(a.register_date, a.created_at)) = DATE(COALESCE(p.register_time, p.created_at))
                WHERE DATE(COALESCE(p.register_time, p.created_at)) BETWEEN ? AND ?
                  {$activityOwnerWherePersons}
                  AND a.id IS NULL
                GROUP BY DATE(COALESCE(p.register_time, p.created_at)), COALESCE(p.owner_didar_id, '')
            ) n ON n.activity_date = b.activity_date AND n.owner_didar_id = b.owner_didar_id
            LEFT JOIN (
                SELECT
                    DATE(COALESCE(d.register_time, d.created_at)) as activity_date,
                    COALESCE(d.owner_didar_id, '') as owner_didar_id,
                    COUNT(DISTINCT p.didar_contact_id) as old_leads_without_activity
                FROM deals d
                INNER JOIN persons p ON p.didar_contact_id = d.contact_didar_id
                LEFT JOIN activities a
                    ON a.contact_didar_id = p.didar_contact_id
                   AND DATE(COALESCE(a.register_date, a.created_at)) = DATE(COALESCE(d.register_time, d.created_at))
                WHERE DATE(COALESCE(d.register_time, d.created_at)) BETWEEN ? AND ?
                  AND DATE(COALESCE(p.register_time, p.created_at)) < DATE(COALESCE(d.register_time, d.created_at))
                  {$activityOwnerWhereDeals}
                  AND a.id IS NULL
                  {$legacyDealsClauseForD}
                GROUP BY DATE(COALESCE(d.register_time, d.created_at)), COALESCE(d.owner_didar_id, '')
            ) o ON o.activity_date = b.activity_date AND o.owner_didar_id = b.owner_didar_id
            ORDER BY b.activity_date ASC, b.owner_name ASC
        ", $activitySummaryParams);

            $tableActivityTypeDetails = $this->runSelectAll("
            SELECT
                t.activity_date as date,
                t.owner_didar_id,
                COALESCE(u.display_name, 'بدون مسئول') as owner_name,
                t.activity_type,
                t.activity_type_new_count,
                t.activity_type_old_count,
                t.activity_count_total,
                t.activity_type_duration_minutes,
                ROUND((t.activity_type_duration_minutes / NULLIF(t.activity_count_total, 0)), 2) as avg_activity_type_duration_minutes,
                ROUND((t.activity_count_total / NULLIF(d.total_activities, 0)) * 100, 2) as activity_share_percent
            FROM (
                SELECT
                    DATE(COALESCE(a.register_date, a.created_at)) as activity_date,
                    COALESCE(a.owner_didar_id, '') as owner_didar_id,
                    {$activityTypeExpr} as activity_type,
                    SUM(CASE WHEN DATE(COALESCE(p.register_time, p.created_at)) = DATE(COALESCE(a.register_date, a.created_at)) THEN 1 ELSE 0 END) as activity_type_new_count,
                    SUM(CASE WHEN DATE(COALESCE(p.register_time, p.created_at)) < DATE(COALESCE(a.register_date, a.created_at)) THEN 1 ELSE 0 END) as activity_type_old_count,
                    COUNT(*) as activity_count_total,
                    SUM(COALESCE(a.duration, 0)) as activity_type_duration_minutes
                FROM activities a
                LEFT JOIN persons p ON p.didar_contact_id = a.contact_didar_id
                WHERE DATE(COALESCE(a.register_date, a.created_at)) BETWEEN ? AND ?
                  {$activityOwnerWhereAct}
                GROUP BY DATE(COALESCE(a.register_date, a.created_at)), COALESCE(a.owner_didar_id, ''), {$activityTypeExpr}
            ) t
            LEFT JOIN users u ON u.didar_user_id = t.owner_didar_id
            INNER JOIN (
                SELECT
                    DATE(COALESCE(a.register_date, a.created_at)) as activity_date,
                    COALESCE(a.owner_didar_id, '') as owner_didar_id,
                    COUNT(*) as total_activities
                FROM activities a
                WHERE DATE(COALESCE(a.register_date, a.created_at)) BETWEEN ? AND ?
                  {$activityOwnerWhereAct}
                GROUP BY DATE(COALESCE(a.register_date, a.created_at)), COALESCE(a.owner_didar_id, '')
            ) d ON d.activity_date = t.activity_date AND d.owner_didar_id = t.owner_didar_id
            ORDER BY t.activity_date ASC, owner_name ASC, t.activity_type ASC
        ", array_merge(
                [$dateFrom, $dateTo],
                $activityOwnerParamsAct,
                [$dateFrom, $dateTo],
                $activityOwnerParamsAct
            ));
        }

        $tableSummaryByUser = $this->runSelectAll("
            SELECT
                b.owner_didar_id,
                b.owner_name,
                COUNT(*) as total_deals_count,
                SUM(CASE WHEN b.has_payment = 1 THEN 1 ELSE 0 END) as sales_total_count,
                ROUND((SUM(CASE WHEN b.has_payment = 1 THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0)) * 100, 2) as conversion_rate_total,
                SUM(CASE WHEN b.lead_type = 'new' THEN 1 ELSE 0 END) as total_new_deals_count,
                SUM(CASE WHEN b.lead_type = 'new' AND b.has_payment = 1 THEN 1 ELSE 0 END) as sales_new_count,
                ROUND((SUM(CASE WHEN b.lead_type = 'new' AND b.has_payment = 1 THEN 1 ELSE 0 END) / NULLIF(SUM(CASE WHEN b.lead_type = 'new' THEN 1 ELSE 0 END), 0)) * 100, 2) as conversion_rate_new,
                SUM(CASE WHEN b.has_payment = 1 THEN b.total_paid ELSE 0 END) as total_collected_amount,
                SUM(CASE WHEN b.has_payment = 1 AND b.payable_amount > 0 AND b.total_paid < b.payable_amount THEN (b.payable_amount - b.total_paid) ELSE 0 END) as outstanding_amount_sum
            FROM ({$dealsBaseSql}) b
            GROUP BY b.owner_didar_id, b.owner_name
            ORDER BY conversion_rate_total DESC, sales_total_count DESC
        ", $paramsDeals);

        $result = [
            'deal_status' => $tableDealStatus,
            'lead_status' => $tableLeadStatus,
            'payment_mix' => $tablePaymentMix,
            'payment_status' => $tablePaymentStatus,
            'summary_by_user' => $tableSummaryByUser,
            'conversion_status' => $tableConversionStatus,
            'conversion_time' => $tableConversionTime,
            'failure_reasons' => $tableFailureReasons,
            'product_status' => $tableProductStatus,
            'product_sold_by_owner_day' => $tableProductSoldByOwnerDay,
            'product_requests_by_owner_day' => $tableProductRequestsByOwnerDay,
            'product_failed_by_owner_day' => $tableProductFailedByOwnerDay,
            'activity_summary' => $tableActivitySummary,
            'activity_type_details' => $tableActivityTypeDetails
        ];

        if ($this->getKpiPaymentDateBasis($filters, 'sales') === 'ui_payment_date') {
            $result = $this->applyUiOracleAdvancedKpiOverrides($filters, $result);
        }

        return $result;
    }

    /**
     * داشبورد عملکرد (خروجی نرمال‌شده برای UI اختصاصی عملکرد)
     */
    public function getPerformanceDashboardData(array $filters): array
    {
        $this->ensureActivityTypeBackfill();
        $skipCharts = !empty($filters['_perf_skip_charts']);
        $skipTabs = !empty($filters['_perf_skip_tabs']);
        $skipRaw = !empty($filters['_perf_skip_raw']);

        $sales = $this->getSales($filters);
        $settled = $this->getSettledDeals($filters);
        $outstanding = $this->getOutstandingBalance($filters);
        $salesWindowClassified = $this->getPaymentWindowDealClassification($filters);
        $lost = $this->getLostSales($filters);
        $openPipeline = $this->getOpenPipeline($filters);
        $withoutPayments = $this->getDealsWithoutPayments($filters);
        $productSales = (!$skipCharts || !$skipRaw) ? $this->getProductSalesCounts($filters) : [];
        $dealConversion = $this->getDealConversionRate($filters);
        $leadConversion = $this->getLeadConversionRate($filters);
        $dailySales = $skipCharts ? [] : $this->getDailySales($filters);
        $dailyActivitiesByType = $skipCharts ? [] : $this->getDailyActivitiesByType($filters);
        $dailyOperational = ($skipCharts && $skipRaw)
            ? ['overall_daily' => [], 'by_expert_daily' => []]
            : $this->getDailyOperationalKpis($filters);
        $advanced = $skipTabs ? [
            'deal_status' => [],
            'lead_status' => [],
            'payment_mix' => [],
            'payment_status' => [],
            'summary_by_user' => [],
            'conversion_status' => [],
            'conversion_time' => [],
            'failure_reasons' => [],
            'product_status' => [],
            'product_sold_by_owner_day' => [],
            'product_requests_by_owner_day' => [],
            'product_failed_by_owner_day' => [],
            'activity_summary' => [],
            'activity_type_details' => [],
        ] : $this->getAdvancedKpiTables($filters);
        $uiOracleForPerformance = null;
        if (
            $this->getKpiPaymentDateBasis($filters, 'sales') === 'ui_payment_date'
            || $this->getKpiPaymentDateBasis($filters, 'payment') === 'ui_payment_date'
        ) {
            $uiOracleForPerformance = $this->getUiOracleKpiData($filters);
        }
        $activityPerformance = $this->buildActivityPerformanceSnapshot($filters);
        if (!empty($uiOracleForPerformance)) {
            $lostReasonBreakdown = $this->buildLostReasonBreakdownFromOracle($uiOracleForPerformance);
        } else {
            $lostReasonBreakdown = array_values(array_filter(array_map(static function ($row) {
                $count = (int)($row['total_lost'] ?? 0);
                $reason = trim((string)($row['failure_reason_text'] ?? ''));
                if ($count <= 0 || $reason === '') {
                    return null;
                }
                return [
                    'reason' => $reason,
                    'count' => $count,
                    'total_amount' => (float)($row['total_amount'] ?? 0),
                ];
            }, $lost)));
            usort($lostReasonBreakdown, static function ($a, $b) {
                return ((int)($b['count'] ?? 0)) <=> ((int)($a['count'] ?? 0));
            });
        }

        $activityDaily = [];
        if (!$skipCharts) {
            foreach (($dailyOperational['overall_daily'] ?? []) as $row) {
                $activityDaily[] = [
                    'date' => $row['date'] ?? '',
                    'activities_count' => (int)($row['activities_count'] ?? 0)
                ];
            }
        }

        $dealStatusBreakdown = [];
        if (empty($filters['_perf_disable_chart_deal_status'])) {
            $dealStatusBreakdown = [
                ['status' => 'جاری', 'count' => 0],
                ['status' => 'دارای فروش', 'count' => 0],
                ['status' => 'بدون فروش', 'count' => 0],
                ['status' => 'ناموفق', 'count' => 0],
            ];
            foreach (($advanced['deal_status'] ?? []) as $row) {
                $dealStatusBreakdown[0]['count'] += (int)($row['deal_pending_total'] ?? 0);
                $dealStatusBreakdown[1]['count'] += (int)($row['deal_with_sales_total'] ?? 0);
                $dealStatusBreakdown[2]['count'] += (int)($row['deal_without_sales_total'] ?? 0);
                $dealStatusBreakdown[3]['count'] += (int)($row['deal_lost_total'] ?? 0);
            }
        }

        $productSalesShare = [];
        if (!$skipCharts && !empty($advanced['product_status'])) {
            $productCounts = [];
            foreach (($advanced['product_status'] ?? []) as $row) {
                $name = trim((string)($row['product_name'] ?? ''));
                if ($name === '') {
                    continue;
                }
                $productCounts[$name] = ($productCounts[$name] ?? 0) + (int)($row['sales_total_count'] ?? 0);
            }
            arsort($productCounts);
            foreach ($productCounts as $name => $count) {
                $productSalesShare[] = ['product_name' => $name, 'deal_count' => (int)$count];
            }
        } elseif (!$skipCharts) {
            $productSalesShare = array_map(static function ($row) {
                return [
                    'product_name' => $row['product_name'] ?? 'نامشخص',
                    'deal_count' => (int)($row['deal_count'] ?? 0)
                ];
            }, $productSales);
        }

        return [
            'summary_cards' => [
                'total_sales_amount' => (float)($sales['total_collected_amount'] ?? 0),
                'total_contract_amount' => (float)($sales['total_contract_amount'] ?? ($salesWindowClassified['paid_deals_contract_sum'] ?? 0)),
                'with_sales_deals_count' => (int)($sales['deal_count'] ?? ($salesWindowClassified['paid_deals_count'] ?? 0)),
                'settled_count' => (int)($uiOracleForPerformance['financial']['settled_count'] ?? ($salesWindowClassified['settled_count'] ?? ($settled['deal_count'] ?? 0))),
                'settled_amount' => (float)($uiOracleForPerformance['financial']['settled_contract_sum'] ?? ($salesWindowClassified['settled_contract_sum'] ?? ($settled['total_amount'] ?? 0))),
                'settled_collected_amount' => (float)($uiOracleForPerformance['financial']['settled_collected_sum'] ?? ($salesWindowClassified['settled_collected_sum'] ?? ($settled['total_collected_amount'] ?? 0))),
                'outstanding_count' => (int)($uiOracleForPerformance['financial']['outstanding_count'] ?? ($salesWindowClassified['outstanding_count'] ?? ($outstanding['deal_count'] ?? 0))),
                'outstanding_amount' => (float)($uiOracleForPerformance['financial']['outstanding_amount_sum'] ?? ($salesWindowClassified['outstanding_amount_sum'] ?? ($outstanding['outstanding_amount'] ?? 0))),
                'outstanding_contract_amount' => (float)($uiOracleForPerformance['financial']['outstanding_contract_sum'] ?? ($salesWindowClassified['outstanding_contract_sum'] ?? ($outstanding['total_contract_amount'] ?? 0))),
                'outstanding_collected_amount' => (float)($uiOracleForPerformance['financial']['outstanding_collected_sum'] ?? ($salesWindowClassified['outstanding_collected_sum'] ?? ($outstanding['total_collected'] ?? 0))),
                'without_payment_count' => (int)($uiOracleForPerformance['conversion_partition']['without_payment_count'] ?? ($withoutPayments['deal_count'] ?? 0)),
                'without_payment_amount' => (float)($uiOracleForPerformance['conversion_partition']['without_payment_amount'] ?? ($withoutPayments['total_potential_amount'] ?? 0)),
                'open_pipeline_count' => (int)($uiOracleForPerformance['conversion_partition']['open_valid_count'] ?? ($openPipeline['deal_count'] ?? 0)),
                'open_pipeline_amount' => (float)($uiOracleForPerformance['conversion_partition']['open_valid_amount'] ?? ($openPipeline['total_potential_amount'] ?? 0)),
                'won_count' => (int)($uiOracleForPerformance['conversion_partition']['won_count'] ?? 0),
                'won_amount' => (float)($uiOracleForPerformance['conversion_partition']['won_amount'] ?? 0),
                'deal_conversion_rate' => (float)($dealConversion['conversion_rate'] ?? 0),
                'deal_non_conversion_rate' => (float)($dealConversion['non_conversion_rate'] ?? 0),
                'deal_conversion_total_deals' => (int)($dealConversion['total_deals'] ?? 0),
                'lead_conversion_rate' => (float)($leadConversion['conversion_rate'] ?? 0),
                'lead_non_conversion_rate' => (float)($leadConversion['non_conversion_rate'] ?? 0),
                'lead_total_new' => (int)($leadConversion['total_new_leads'] ?? 0),
                'lead_converted' => (int)($leadConversion['converted_leads'] ?? 0),
                'avg_conversion_days' => (float)($this->getTimeToConversion($filters)['avg_conversion_days'] ?? 0),
                'lost_count' => (int)($uiOracleForPerformance['conversion_partition']['lost_count'] ?? array_sum(array_map(static function ($r) { return (int)($r['total_lost'] ?? 0); }, $lost))),
                'lost_amount' => (float)($uiOracleForPerformance['conversion_partition']['lost_amount'] ?? array_sum(array_map(static function ($r) { return (float)($r['total_amount'] ?? 0); }, $lost))),
                'lost_reason_breakdown' => $lostReasonBreakdown,
                'activity_performance' => $activityPerformance,
            ],
            'charts' => $skipCharts ? [] : [
                'daily_sales' => $dailySales,
                'daily_activities' => $activityDaily,
                'daily_activities_by_type' => $dailyActivitiesByType,
                'deal_status_breakdown' => $dealStatusBreakdown,
                'product_sales_share' => $productSalesShare
            ],
            'tabs' => $skipTabs ? [] : [
                'activity' => [
                    'activity_summary' => $advanced['activity_summary'] ?? [],
                    'activity_type_details' => $advanced['activity_type_details'] ?? []
                ],
                'sales_deals' => [
                    'deal_status' => $advanced['deal_status'] ?? [],
                    'lead_status' => $advanced['lead_status'] ?? [],
                    'conversion_status' => $advanced['conversion_status'] ?? []
                ],
                'payments' => [
                    'payment_mix' => $advanced['payment_mix'] ?? [],
                    'payment_status' => $advanced['payment_status'] ?? []
                ],
                'products' => [
                    'product_status' => $advanced['product_status'] ?? [],
                    'product_sold_by_owner_day' => $advanced['product_sold_by_owner_day'] ?? [],
                    'product_requests_by_owner_day' => $advanced['product_requests_by_owner_day'] ?? [],
                    'product_failed_by_owner_day' => $advanced['product_failed_by_owner_day'] ?? []
                ],
                'failure_reasons' => [
                    'failure_reasons' => $advanced['failure_reasons'] ?? []
                ]
            ],
            'raw' => $skipRaw ? [] : [
                'daily_operational' => $dailyOperational,
                'advanced_tables' => $advanced,
                'product_sales' => $productSales,
                'lost_sales' => $lost,
                'activity_performance' => $activityPerformance,
            ]
        ];
    }

    /**
     * طبقه‌بندی معاملات دارای پرداخت در بازه «تاریخ پرداخت»:
     * - مبنای انتخاب معامله: حداقل یک پرداخت در بازه
     * - سپس وضعیت تسویه/بدهکاری بر اساس total_paid همه‌زمان + payable_amount همان معامله
     */
    private function getPaymentWindowDealClassification(array $filters): array
    {
        if ($this->getKpiPaymentDateBasis($filters, 'payment') === 'ui_payment_date') {
            return $this->getPaymentWindowDealClassificationUiOracle($filters);
        }
        $pdo = $this->db->getPdo();
        $paymentsByDealDateSql = $this->buildKpiPaymentsWindowByDealDateSql($filters, 'sales');
        $paymentsByDealSql = $this->buildKpiAllTimePaymentTotalsByDealSql($filters, 'payment');
        $whereDealsNoDate = $this->buildWhereClauseForDealsNoDate($filters, 'd');
        $paramsDealsNoDate = $this->buildParamsForDealsNoDate($filters);

        $paymentDateWhere = [];
        $paymentDateParams = [];
        if (!empty($filters['date_from'])) {
            $paymentDateWhere[] = "pbd.payment_date >= ?";
            $paymentDateParams[] = $filters['date_from'];
        }
        if (!empty($filters['date_to'])) {
            $paymentDateWhere[] = "pbd.payment_date <= ?";
            $paymentDateParams[] = $filters['date_to'];
        }
        $paymentDateClause = !empty($paymentDateWhere) ? implode(' AND ', $paymentDateWhere) : '1=1';

        $sql = "
            SELECT
                COUNT(DISTINCT d.id) as paid_deals_count,
                SUM(COALESCE(d.payable_amount, 0)) as paid_deals_contract_sum,
                SUM(COALESCE(tp.total_paid, 0)) as paid_deals_collected_sum,

                SUM(CASE
                    WHEN COALESCE(d.payable_amount, 0) > 0 AND COALESCE(tp.total_paid, 0) < COALESCE(d.payable_amount, 0) THEN 1
                    ELSE 0
                END) as outstanding_count,
                SUM(CASE
                    WHEN COALESCE(d.payable_amount, 0) > 0 AND COALESCE(tp.total_paid, 0) < COALESCE(d.payable_amount, 0)
                    THEN (COALESCE(d.payable_amount, 0) - COALESCE(tp.total_paid, 0))
                    ELSE 0
                END) as outstanding_amount_sum,
                SUM(CASE
                    WHEN COALESCE(d.payable_amount, 0) > 0 AND COALESCE(tp.total_paid, 0) < COALESCE(d.payable_amount, 0)
                    THEN COALESCE(d.payable_amount, 0)
                    ELSE 0
                END) as outstanding_contract_sum,
                SUM(CASE
                    WHEN COALESCE(d.payable_amount, 0) > 0 AND COALESCE(tp.total_paid, 0) < COALESCE(d.payable_amount, 0)
                    THEN COALESCE(tp.total_paid, 0)
                    ELSE 0
                END) as outstanding_collected_sum,

                SUM(CASE
                    WHEN (COALESCE(d.payable_amount, 0) <= 0 AND COALESCE(tp.total_paid, 0) > 0)
                         OR (COALESCE(d.payable_amount, 0) > 0 AND COALESCE(tp.total_paid, 0) >= COALESCE(d.payable_amount, 0))
                    THEN 1
                    ELSE 0
                END) as settled_count,
                SUM(CASE
                    WHEN (COALESCE(d.payable_amount, 0) <= 0 AND COALESCE(tp.total_paid, 0) > 0)
                         OR (COALESCE(d.payable_amount, 0) > 0 AND COALESCE(tp.total_paid, 0) >= COALESCE(d.payable_amount, 0))
                    THEN COALESCE(d.payable_amount, 0)
                    ELSE 0
                END) as settled_contract_sum,
                SUM(CASE
                    WHEN (COALESCE(d.payable_amount, 0) <= 0 AND COALESCE(tp.total_paid, 0) > 0)
                         OR (COALESCE(d.payable_amount, 0) > 0 AND COALESCE(tp.total_paid, 0) >= COALESCE(d.payable_amount, 0))
                    THEN COALESCE(tp.total_paid, 0)
                    ELSE 0
                END) as settled_collected_sum
            FROM deals d
            INNER JOIN (
                SELECT DISTINCT pbd.deal_id
                FROM ({$paymentsByDealDateSql}) pbd
                WHERE {$paymentDateClause}
            ) pw ON pw.deal_id = d.id
            LEFT JOIN ({$paymentsByDealSql}) tp ON tp.deal_id = d.id
            WHERE {$whereDealsNoDate}
        ";

        $stmt = $pdo->prepare($sql);
        $stmt->execute(array_merge($paymentDateParams, $paramsDealsNoDate));
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
    }

    /**
     * هدف KPI برای کاربر
     */
    public function getKpiTarget(int $userId, int $year, int $month): ?array
    {
        $pdo = $this->db->getPdo();
        $stmt = $pdo->prepare("
            SELECT * FROM kpi_targets
            WHERE user_id = ? AND year = ? AND month = ? AND is_active = 1
        ");
        $stmt->execute([$userId, $year, $month]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        return $result ?: null;
    }

    public function upsertKpiTarget(int $userId, int $year, int $month, float $targetAmount, int $targetDealCount = 0): bool
    {
        $pdo = $this->db->getPdo();
        $stmt = $pdo->prepare("
            INSERT INTO kpi_targets (user_id, year, month, target_amount, target_deal_count)
            VALUES (?, ?, ?, ?, ?)
            ON DUPLICATE KEY UPDATE
                target_amount = VALUES(target_amount),
                target_deal_count = VALUES(target_deal_count),
                updated_at = CURRENT_TIMESTAMP
        ");

        return $stmt->execute([$userId, $year, $month, $targetAmount, $targetDealCount]);
    }

    public function getAllKpiTargets(): array
    {
        $pdo = $this->db->getPdo();
        $stmt = $pdo->prepare("
            SELECT
                kt.*,
                u.first_name,
                u.last_name,
                u.display_name,
                u.didar_user_id,
                u.role
            FROM kpi_targets kt
            INNER JOIN users u ON kt.user_id = u.id
            WHERE kt.is_active = 1
            ORDER BY kt.year DESC, kt.month DESC, u.first_name ASC
        ");
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * لیست معاملات برای جدول KPI
     */
    public function getKpiDealDetails(array $filters): array
    {
        $pdo = $this->db->getPdo();
        $where = $this->buildWhereClauseForDealDetails($filters);
        $params = $this->buildParamsForDeals($filters);
        $paymentsByDealSql = $this->buildUnifiedPaymentsByDealSql();

        $sql = "
            SELECT
                d.id,
                d.didar_deal_id,
                d.title,
                d.status,
                d.payable_amount,
                d.register_time,
                d.won_time,
                d.lost_time,
                u.display_name as owner_name,
                u.didar_user_id as owner_didar_id,
                COALESCE(pay.total_paid, 0) as collected_amount,
                pay.first_payment_date as first_payment_date,
                CASE WHEN COALESCE(pay.total_paid, 0) > 0 THEN 1 ELSE 0 END as payment_count
            FROM deals d
            LEFT JOIN users u ON d.owner_didar_id = u.didar_user_id
            LEFT JOIN ({$paymentsByDealSql}) pay ON pay.deal_id = d.id
            WHERE {$where}
            ORDER BY d.register_time DESC
            LIMIT 500
        ";

        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * Drill-down: رکوردهای پشت یک سلول KPI (لید + معامله + فعالیت)
     */
    public function getKpiDrilldown(array $filters, string $tableId, string $metricKey, array $rowContext): array
    {
        $tableId = trim($tableId);
        $metricKey = trim($metricKey);
        $rowDate = trim((string)($rowContext['date'] ?? ''));
        $rowOwnerDidarId = trim((string)($rowContext['owner_didar_id'] ?? ''));
        $rowProductName = trim((string)($rowContext['product_name'] ?? ''));
        $rowFailureReason = trim((string)($rowContext['failure_reason'] ?? ''));
        $rowActivityType = trim((string)($rowContext['activity_type'] ?? ''));

        if ($tableId === '' || $metricKey === '') {
            return ['leads' => [], 'deals' => [], 'activities' => [], 'payments' => [], 'meta' => ['total_leads' => 0, 'total_deals' => 0, 'total_activities' => 0, 'total_payments' => 0]];
        }

        if ($tableId === 'activity_status' || $tableId === 'activity_summary' || $tableId === 'activity_type_details') {
            return $this->getActivityStatusDrilldown($filters, $metricKey, $rowDate, $rowOwnerDidarId, $rowActivityType);
        }

        if ($tableId === 'lead_status') {
            return $this->getLeadStatusDrilldown($filters, $metricKey, $rowDate, $rowOwnerDidarId);
        }

        if ($tableId === 'performance_cards') {
            return $this->getPerformanceCardDrilldown($filters, $metricKey, $rowOwnerDidarId);
        }
        if ($tableId === 'performance_charts') {
            return $this->getPerformanceChartDrilldown($filters, $metricKey, $rowContext);
        }

        return $this->getDealBasedDrilldown(
            $filters,
            $tableId,
            $metricKey,
            $rowDate,
            $rowOwnerDidarId,
            $rowProductName,
            $rowFailureReason
        );
    }

    private function getPerformanceChartDrilldown(array $filters, string $metricKey, array $rowContext): array
    {
        $rowDate = trim((string)($rowContext['date'] ?? ''));
        $rowProductName = trim((string)($rowContext['product_name'] ?? ''));
        $rowStage = trim((string)($rowContext['stage'] ?? ''));

        if ($metricKey === 'daily_sales_point') {
            return $this->getDailySalesChartPointDrilldown($filters, $rowDate);
        }
        if ($metricKey === 'product_sales_share_item') {
            return $this->getProductSalesShareChartDrilldown($filters, $rowProductName);
        }
        if ($metricKey === 'conversion_funnel_stage') {
            return $this->getConversionFunnelChartDrilldown($filters, $rowStage);
        }

        return ['leads' => [], 'deals' => [], 'activities' => [], 'payments' => [], 'meta' => ['total_leads' => 0, 'total_deals' => 0, 'total_activities' => 0, 'total_payments' => 0]];
    }

    private function getPerformanceCardDrilldown(array $filters, string $metricKey, string $rowOwnerDidarId): array
    {
        if ($metricKey === 'sales_total') {
            return $this->getSalesTotalCardDrilldown($filters, $rowOwnerDidarId);
        }
        if ($metricKey === 'deal_conversion_rate') {
            return $this->getDealConversionAnalysisDrilldown($filters, $rowOwnerDidarId);
        }
        if ($metricKey === 'lead_conversion_rate') {
            return $this->getLeadConversionLeadAnalysisDrilldown($filters, $rowOwnerDidarId);
        }
        if ($metricKey === 'settled_count') {
            return $this->getFinancialPerformanceCardDrilldownUiOracle($filters, 'settled', $rowOwnerDidarId);
        }
        if ($metricKey === 'outstanding_amount_sum') {
            return $this->getFinancialPerformanceCardDrilldownUiOracle($filters, 'outstanding', $rowOwnerDidarId);
        }
        if ($metricKey === 'without_payment_count') {
            return $this->getWithoutPaymentCardDrilldown($filters, $rowOwnerDidarId);
        }
        if ($metricKey === 'open_pipeline_count') {
            return $this->getPerformancePartitionCardDrilldown($filters, 'open_valid', $rowOwnerDidarId);
        }
        if ($metricKey === 'won_count') {
            return $this->getPerformancePartitionCardDrilldown($filters, 'won_valid', $rowOwnerDidarId);
        }
        if ($metricKey === 'lost_count') {
            return $this->getPerformancePartitionCardDrilldown($filters, 'lost_valid', $rowOwnerDidarId);
        }
        if (strpos($metricKey, 'activity_perf_') === 0) {
            return $this->getActivityPerformanceCardDrilldown($filters, $metricKey, $rowOwnerDidarId);
        }

        return ['leads' => [], 'deals' => [], 'activities' => [], 'payments' => [], 'meta' => ['total_leads' => 0, 'total_deals' => 0, 'total_activities' => 0, 'total_payments' => 0]];
    }

    private function buildActivityPerformanceSnapshot(array $filters): array
    {
        $state = $this->buildActivityPerformanceState($filters);
        $card = static function (array $base, array $metrics, string $stageMetricKey = ''): array {
            if ($stageMetricKey !== '') {
                $base['stage_count'] = (int)($metrics[$stageMetricKey]['count'] ?? 0);
                $base['stage_metric_key'] = $stageMetricKey;
            }
            return $base;
        };
        return [
            'past_deals' => [
                $card([
                    'key' => 'activity_perf_past_no_answer',
                    'title' => 'تماس بی پاسخ',
                    'count' => count($state['drilldown']['activity_perf_past_no_answer']['activities'] ?? []),
                    'duration_minutes' => (int)($state['metrics']['activity_perf_past_no_answer']['duration_minutes'] ?? 0),
                ], $state['metrics'], 'activity_perf_past_no_answer_stage'),
                $card([
                    'key' => 'activity_perf_past_outstanding_followup',
                    'title' => 'پیگیری مانده حساب',
                    'count' => count($state['drilldown']['activity_perf_past_outstanding_followup']['activities'] ?? []),
                    'duration_minutes' => (int)($state['metrics']['activity_perf_past_outstanding_followup']['duration_minutes'] ?? 0),
                ], $state['metrics'], 'activity_perf_past_outstanding_followup_stage'),
                $card([
                    'key' => 'activity_perf_past_payment_link',
                    'title' => 'ارسال لینک پرداخت',
                    'count' => count($state['drilldown']['activity_perf_past_payment_link']['activities'] ?? []),
                    'duration_minutes' => (int)($state['metrics']['activity_perf_past_payment_link']['duration_minutes'] ?? 0),
                ], $state['metrics'], 'activity_perf_past_payment_link_stage'),
                [
                    'key' => 'activity_perf_past_outstanding_deals',
                    'title' => 'تعداد واریزی مانده حساب',
                    'count' => (int)($state['metrics']['activity_perf_past_outstanding_deals']['paid_today_count'] ?? 0),
                    'sub_count' => (int)($state['metrics']['activity_perf_past_outstanding_deals']['count'] ?? count($state['drilldown']['activity_perf_past_outstanding_deals']['deal_ids'] ?? [])),
                    'sub_count_label' => 'تعداد کل معاملات دارای مانده حساب',
                ],
                [
                    'key' => 'activity_perf_past_outstanding_amount',
                    'title' => 'واریزی مانده حساب',
                    'amount' => (float)($state['metrics']['activity_perf_past_outstanding_amount']['paid_today_amount'] ?? 0),
                    'sub_amount' => (float)($state['metrics']['activity_perf_past_outstanding_amount']['amount'] ?? 0),
                    'sub_amount_label' => 'مبلغ کل مانده حساب',
                    'primary_value_type' => 'amount',
                ],
                [
                    'key' => 'activity_perf_past_no_contact',
                    'title' => 'عدم تماس',
                    'count' => count($state['drilldown']['activity_perf_past_no_contact']['deal_ids'] ?? []),
                ],
                $card([
                    'key' => 'activity_perf_past_followup_call',
                    'title' => 'تماس پیگیری',
                    'count' => count($state['drilldown']['activity_perf_past_followup_call']['activities'] ?? []),
                    'duration_minutes' => (int)($state['metrics']['activity_perf_past_followup_call']['duration_minutes'] ?? 0),
                ], $state['metrics'], 'activity_perf_past_followup_call_stage'),
            ],
            'daily' => [
                [
                    'key' => 'activity_perf_current_registered_deals',
                    'title' => 'تعداد کل معاملات ثبت‌شده',
                    'count' => count($state['drilldown']['activity_perf_current_registered_deals']['deal_ids'] ?? []),
                ],
                $card([
                    'key' => 'activity_perf_current_no_answer',
                    'title' => 'تماس بی پاسخ',
                    'count' => count($state['drilldown']['activity_perf_current_no_answer']['activities'] ?? []),
                    'duration_minutes' => (int)($state['metrics']['activity_perf_current_no_answer']['duration_minutes'] ?? 0),
                ], $state['metrics'], 'activity_perf_current_no_answer_stage'),
                $card([
                    'key' => 'activity_perf_current_payment_link',
                    'title' => 'ارسال لینک پرداخت',
                    'count' => count($state['drilldown']['activity_perf_current_payment_link']['activities'] ?? []),
                    'duration_minutes' => (int)($state['metrics']['activity_perf_current_payment_link']['duration_minutes'] ?? 0),
                ], $state['metrics'], 'activity_perf_current_payment_link_stage'),
                $card([
                    'key' => 'activity_perf_current_followup_call',
                    'title' => 'تماس پیگیری',
                    'count' => count($state['drilldown']['activity_perf_current_followup_call']['activities'] ?? []),
                    'duration_minutes' => (int)($state['metrics']['activity_perf_current_followup_call']['duration_minutes'] ?? 0),
                ], $state['metrics'], 'activity_perf_current_followup_call_stage'),
                [
                    'key' => 'activity_perf_current_no_contact',
                    'title' => 'عدم تماس',
                    'count' => count($state['drilldown']['activity_perf_current_no_contact']['deal_ids'] ?? []),
                ],
            ],
        ];
    }

    private function getActivityPerformanceCohortDeals(array $filters, string $mode): array
    {
        $whereDeals = $this->buildWhereClauseForDealsNoDate($filters, 'd');
        $paramsDeals = $this->buildParamsForDealsNoDate($filters);
        $dateFrom = (string)($filters['date_from'] ?? date('Y-m-01'));
        $dateTo = (string)($filters['date_to'] ?? date('Y-m-t'));
        $dateClause = $mode === 'past'
            ? "DATE(COALESCE(d.register_time, d.created_at)) < ?"
            : "DATE(COALESCE(d.register_time, d.created_at)) BETWEEN ? AND ?";
        $params = $mode === 'past'
            ? array_merge([$dateFrom], $paramsDeals)
            : array_merge([$dateFrom, $dateTo], $paramsDeals);

        return $this->runSelectAll("
            SELECT
                d.id,
                COALESCE(d.didar_deal_id, '') as didar_deal_id,
                COALESCE(d.payable_amount, 0) as payable_amount
            FROM deals d
            WHERE {$dateClause}
              AND {$whereDeals}
        ", $params);
    }

    private function buildActivityPerformanceState(array $filters, string $rowOwnerDidarId = ''): array
    {
        if ($rowOwnerDidarId !== '') {
            $filters['owner_didar_id'] = $rowOwnerDidarId;
            $filters['owner_didar_ids_csv'] = $rowOwnerDidarId;
            $filters['owner_didar_ids'] = [$rowOwnerDidarId];
        }

        $dateFrom = (string)($filters['date_from'] ?? date('Y-m-01'));
        $dateTo = (string)($filters['date_to'] ?? date('Y-m-t'));
        $activityTypeExpr = $this->buildCanonicalActivityTypeExpr('a');
        $oracleState = $this->buildUiOracleState($filters);
        $oracleKpi = $this->getUiOracleKpiData($filters);
        $salesFirstAllAgg = $oracleKpi['aggs']['sales_first_all'] ?? [];

        $activityOwnerWhere = '';
        $activityOwnerParams = [];
        if (!empty($filters['owner_didar_ids_csv'])) {
            $activityOwnerWhere = " AND FIND_IN_SET(a.owner_didar_id, ?)";
            $activityOwnerParams[] = $filters['owner_didar_ids_csv'];
        } elseif (!empty($filters['owner_didar_id'])) {
            $activityOwnerWhere = " AND a.owner_didar_id = ?";
            $activityOwnerParams[] = $filters['owner_didar_id'];
        }

        $pastDeals = [];
        $dayDeals = [];
        foreach (($oracleState['deals'] ?? []) as $dealId => $deal) {
            $dealDate = (string)($deal['deal_date'] ?? '');
            $effectiveDealDate = $this->resolveActivityPerformanceDealDate($dealId, $deal, $salesFirstAllAgg);
            if ($effectiveDealDate === '') {
                continue;
            }
            $deal['activity_effective_date'] = $effectiveDealDate;
            if ($effectiveDealDate < $dateFrom) {
                if ($this->uiOracleDealPassNoDate($deal, $oracleState, $filters)) {
                    $pastDeals[] = $deal;
                }
                continue;
            }
            if ($this->uiOracleInRange($effectiveDealDate, $filters) && $this->uiOracleDealPassNoDate($deal, $oracleState, $filters)) {
                $dayDeals[] = $deal;
            }
        }

        $allDeals = array_merge($pastDeals, $dayDeals);
        $effectiveDealDateById = [];
        foreach ($allDeals as $dealRow) {
            $dealId = (int)($dealRow['id'] ?? 0);
            $effectiveDate = trim((string)($dealRow['activity_effective_date'] ?? $dealRow['deal_date'] ?? ''));
            if ($dealId > 0 && $effectiveDate !== '') {
                $effectiveDealDateById[$dealId] = $effectiveDate;
            }
        }
        $allDealIds = array_values(array_unique(array_map(static function ($row) {
            return (int)($row['id'] ?? 0);
        }, $allDeals)));
        $allDealDidarIds = array_values(array_filter(array_unique(array_map(static function ($row) {
            return trim((string)($row['didar_deal_id'] ?? ''));
        }, $allDeals))));

        $activityPresence = [];
        $activityRows = [];
        if (!empty($allDealIds)) {
            $dealPlaceholders = implode(',', array_fill(0, count($allDealIds), '?'));
            $activityRows = $this->runSelectAll("
                SELECT
                    a.id as activity_id,
                    DATE(COALESCE(a.register_date, a.created_at)) as activity_date,
                    {$activityTypeExpr} as activity_type,
                    COALESCE(a.duration, 0) as activity_duration_minutes,
                    COALESCE(a.owner_didar_id, '') as activity_owner_didar_id,
                    COALESCE(au.display_name, 'بدون مسئول') as owner_name,
                    d.id as deal_id,
                    COALESCE(d.didar_deal_id, '') as didar_deal_id,
                    COALESCE(d.title, '') as deal_title,
                    DATE(COALESCE(d.register_time, d.created_at)) as deal_date,
                    COALESCE(d.pipeline_stage, '') as pipeline_stage,
                    COALESCE(d.contact_didar_id, '') as contact_didar_id,
                    TRIM(CONCAT(COALESCE(p.first_name, ''), ' ', COALESCE(p.last_name, ''))) as lead_name,
                    COALESCE(p.mobile_phone, '') as mobile_phone
                FROM activities a
                INNER JOIN deals d ON d.didar_deal_id = a.deal_didar_id
                LEFT JOIN users au ON au.didar_user_id = a.owner_didar_id
                LEFT JOIN persons p ON p.didar_contact_id = d.contact_didar_id
                WHERE DATE(COALESCE(a.register_date, a.created_at)) BETWEEN ? AND ?
                  AND d.id IN ({$dealPlaceholders})
                  {$activityOwnerWhere}
                ORDER BY activity_date DESC, a.id DESC
            ", array_merge([$dateFrom, $dateTo], $allDealIds, $activityOwnerParams));

            if (!empty($allDealDidarIds)) {
                foreach ($this->runSelectAll("
                    SELECT DISTINCT COALESCE(a.deal_didar_id, '') as deal_didar_id
                    FROM activities a
                    WHERE a.deal_didar_id IN (" . implode(',', array_fill(0, count($allDealDidarIds), '?')) . ")
                ", $allDealDidarIds) as $presenceRow) {
                    $dealDidarId = trim((string)($presenceRow['deal_didar_id'] ?? ''));
                    if ($dealDidarId !== '') {
                        $activityPresence[$dealDidarId] = true;
                    }
                }
            }
        }

        $paymentTotals = [];
        if (!empty($allDealIds)) {
            $paymentTotalsSql = $this->buildKpiAllTimePaymentTotalsByDealSql($filters, 'payment');
            $paymentPlaceholders = implode(',', array_fill(0, count($allDealIds), '?'));
            foreach ($this->runSelectAll("
                SELECT p.deal_id, COALESCE(p.total_paid, 0) as total_paid
                FROM ({$paymentTotalsSql}) p
                WHERE p.deal_id IN ({$paymentPlaceholders})
            ", $allDealIds) as $paymentRow) {
                $paymentTotals[(int)($paymentRow['deal_id'] ?? 0)] = (float)($paymentRow['total_paid'] ?? 0);
            }
        }

        $pastOutstandingDealIds = [];
        $pastOutstandingAmount = 0.0;
        $pastOutstandingPaidTodayDealIds = [];
        $pastNoContactDealIds = [];
        foreach ($pastDeals as $dealRow) {
            $dealId = (int)($dealRow['id'] ?? 0);
            $payable = (float)($dealRow['payable_amount'] ?? 0);
            $totalPaid = (float)($paymentTotals[$dealId] ?? 0);
            if ($payable > 0 && $totalPaid > 0 && $totalPaid < $payable) {
                $pastOutstandingDealIds[] = $dealId;
                $pastOutstandingAmount += ($payable - $totalPaid);
            }
            $pipelineStage = trim((string)($dealRow['pipeline_stage'] ?? ''));
            if ($dealId > 0 && $pipelineStage === 'معامله جدید') {
                $pastNoContactDealIds[] = $dealId;
            }
        }

        $dayDealIds = array_values(array_filter(array_map(static function ($row) {
            return (int)($row['id'] ?? 0);
        }, $dayDeals)));
        $dayNoContactDealIds = [];
        foreach ($dayDeals as $dealRow) {
            $dealId = (int)($dealRow['id'] ?? 0);
            $pipelineStage = trim((string)($dealRow['pipeline_stage'] ?? ''));
            if ($dealId > 0 && $pipelineStage === 'معامله جدید') {
                $dayNoContactDealIds[] = $dealId;
            }
        }

        $statusScope = $this->getKpiPaymentStatusScope($filters);
        $pastPaymentRows = !empty($pastDeals) ? $this->getPaymentsByDealIds(array_map(static function ($row) {
            return (int)($row['id'] ?? 0);
        }, $pastDeals)) : [];
        $firstPaymentDateByDeal = [];
        foreach ($pastPaymentRows as $paymentRow) {
            if ($statusScope === 'confirmed_only' && ($paymentRow['payment_source'] ?? '') === 'transactions') {
                $status = strtolower(trim((string)($paymentRow['payment_status'] ?? '')));
                if ($status !== 'confirmed') {
                    continue;
                }
            }
            $dealId = (int)($paymentRow['deal_id'] ?? 0);
            $paymentDate = trim((string)($paymentRow['payment_date'] ?? ''));
            if ($dealId <= 0 || $paymentDate === '') {
                continue;
            }
            if (!isset($firstPaymentDateByDeal[$dealId]) || $paymentDate < $firstPaymentDateByDeal[$dealId]) {
                $firstPaymentDateByDeal[$dealId] = $paymentDate;
            }
        }

        $oldOutstandingPaymentRows = [];
        $oldOutstandingPaymentAmount = 0.0;
        foreach ($pastPaymentRows as $paymentRow) {
            if ($statusScope === 'confirmed_only' && ($paymentRow['payment_source'] ?? '') === 'transactions') {
                $status = strtolower(trim((string)($paymentRow['payment_status'] ?? '')));
                if ($status !== 'confirmed') {
                    continue;
                }
            }
            $dealId = (int)($paymentRow['deal_id'] ?? 0);
            $paymentDate = trim((string)($paymentRow['payment_date'] ?? ''));
            $firstDate = trim((string)($firstPaymentDateByDeal[$dealId] ?? ''));
            if ($dealId <= 0 || $paymentDate === '' || $firstDate === '') {
                continue;
            }
            if ($paymentDate < $dateFrom || $paymentDate > $dateTo) {
                continue;
            }
            if ($paymentDate <= $firstDate) {
                continue;
            }
            $oldOutstandingPaymentRows[] = $paymentRow;
            $oldOutstandingPaymentAmount += (float)($paymentRow['amount'] ?? 0);
            if ($dealId > 0) {
                $pastOutstandingPaidTodayDealIds[$dealId] = $dealId;
            }
        }

        $stageMetricMap = [
            'activity_perf_past_no_answer_stage' => ['deals' => $pastDeals, 'title' => 'تماس عدم پاسخ'],
            'activity_perf_past_outstanding_followup_stage' => ['deals' => $pastDeals, 'title' => 'پیگیری مانده حساب'],
            'activity_perf_past_payment_link_stage' => ['deals' => $pastDeals, 'title' => 'ارسال لینک پرداخت'],
            'activity_perf_past_followup_call_stage' => ['deals' => $pastDeals, 'title' => 'تماس پیگیری'],
            'activity_perf_current_no_answer_stage' => ['deals' => $dayDeals, 'title' => 'تماس عدم پاسخ'],
            'activity_perf_current_payment_link_stage' => ['deals' => $dayDeals, 'title' => 'ارسال لینک پرداخت'],
            'activity_perf_current_followup_call_stage' => ['deals' => $dayDeals, 'title' => 'تماس پیگیری'],
        ];

        $metricActivityMap = [
            'activity_perf_past_no_answer' => 'تماس عدم پاسخ',
            'activity_perf_past_outstanding_followup' => 'پیگیری مانده حساب',
            'activity_perf_past_payment_link' => 'ارسال لینک پرداخت',
            'activity_perf_past_followup_call' => 'تماس پیگیری',
            'activity_perf_current_no_answer' => 'تماس عدم پاسخ',
            'activity_perf_current_payment_link' => 'ارسال لینک پرداخت',
            'activity_perf_current_followup_call' => 'تماس پیگیری',
        ];
        $metrics = [];
        $drilldown = [];
        foreach (array_keys($metricActivityMap) as $metricKey) {
            $metrics[$metricKey] = ['duration_minutes' => 0];
            $drilldown[$metricKey] = ['activities' => [], 'deal_ids' => []];
        }
        foreach ($stageMetricMap as $metricKey => $_cfg) {
            $metrics[$metricKey] = ['count' => 0];
            $drilldown[$metricKey] = ['deal_ids' => []];
        }
        $drilldown['activity_perf_past_outstanding_deals'] = [
            'deal_ids' => $pastOutstandingDealIds,
            'payments' => $oldOutstandingPaymentRows,
        ];
        $drilldown['activity_perf_past_no_contact'] = ['deal_ids' => $pastNoContactDealIds];
        $drilldown['activity_perf_current_registered_deals'] = ['deal_ids' => $dayDealIds];
        $drilldown['activity_perf_current_no_contact'] = ['deal_ids' => $dayNoContactDealIds];
        $drilldown['activity_perf_past_outstanding_amount'] = [
            'deal_ids' => $pastOutstandingDealIds,
            'payments' => $oldOutstandingPaymentRows,
        ];
        $metrics['activity_perf_past_outstanding_deals'] = [
            'amount' => $pastOutstandingAmount,
            'count' => count($pastOutstandingDealIds),
            'paid_today_count' => count($pastOutstandingPaidTodayDealIds),
        ];
        $metrics['activity_perf_past_outstanding_amount'] = [
            'amount' => $pastOutstandingAmount,
            'paid_today_amount' => $oldOutstandingPaymentAmount,
        ];

        foreach ($stageMetricMap as $metricKey => $cfg) {
            $targetTitle = $cfg['title'];
            foreach (($cfg['deals'] ?? []) as $dealRow) {
                $dealId = (int)($dealRow['id'] ?? 0);
                if ($dealId <= 0) {
                    continue;
                }
                $stageTitle = $this->normalizeActivityTypeLabelPhp((string)($dealRow['pipeline_stage'] ?? ''));
                if ($stageTitle !== $targetTitle) {
                    continue;
                }
                $drilldown[$metricKey]['deal_ids'][$dealId] = $dealId;
            }
            $drilldown[$metricKey]['deal_ids'] = array_values(array_unique(array_map('intval', $drilldown[$metricKey]['deal_ids'] ?? [])));
            $metrics[$metricKey]['count'] = count($drilldown[$metricKey]['deal_ids']);
        }

        foreach ($activityRows as $activityRow) {
            $dealId = (int)($activityRow['deal_id'] ?? 0);
            $dealDate = trim((string)($effectiveDealDateById[$dealId] ?? $activityRow['deal_date'] ?? ''));
            $activityType = trim((string)($activityRow['activity_type'] ?? ''));
            $duration = (int)($activityRow['activity_duration_minutes'] ?? 0);
            $isPast = ($dealDate !== '' && $dealDate < $dateFrom);
            $isCurrent = ($dealDate !== '' && $dealDate >= $dateFrom && $dealDate <= $dateTo);
            if (!$isPast && !$isCurrent) {
                continue;
            }

            $metricKey = null;
            if ($activityType === 'تماس عدم پاسخ') {
                $metricKey = $isPast ? 'activity_perf_past_no_answer' : 'activity_perf_current_no_answer';
            } elseif ($activityType === 'پیگیری مانده حساب' && $isPast) {
                $metricKey = 'activity_perf_past_outstanding_followup';
            } elseif ($activityType === 'ارسال لینک پرداخت') {
                $metricKey = $isPast ? 'activity_perf_past_payment_link' : 'activity_perf_current_payment_link';
            } elseif ($activityType === 'تماس پیگیری') {
                $metricKey = $isPast ? 'activity_perf_past_followup_call' : 'activity_perf_current_followup_call';
            }

            if ($metricKey === null) {
                continue;
            }
            $metrics[$metricKey]['duration_minutes'] = (int)($metrics[$metricKey]['duration_minutes'] ?? 0) + max(0, $duration);
            $drilldown[$metricKey]['activities'][] = $activityRow;
            $dealId = (int)($activityRow['deal_id'] ?? 0);
            if ($dealId > 0) {
                $drilldown[$metricKey]['deal_ids'][$dealId] = $dealId;
            }
        }

        foreach ($drilldown as $metricKey => $payload) {
            if (isset($payload['deal_ids']) && is_array($payload['deal_ids'])) {
                $drilldown[$metricKey]['deal_ids'] = array_values(array_unique(array_map('intval', $payload['deal_ids'])));
            }
        }

        return [
            'metrics' => $metrics,
            'drilldown' => $drilldown,
        ];
    }

    private function getActivityPerformanceCardDrilldown(array $filters, string $metricKey, string $rowOwnerDidarId = ''): array
    {
        $state = $this->buildActivityPerformanceState($filters, $rowOwnerDidarId);
        $payload = $state['drilldown'][$metricKey] ?? null;
        if (!is_array($payload)) {
            return ['leads' => [], 'deals' => [], 'activities' => [], 'payments' => [], 'meta' => ['total_leads' => 0, 'total_deals' => 0, 'total_activities' => 0, 'total_payments' => 0]];
        }

        $dealIds = array_values(array_unique(array_map('intval', $payload['deal_ids'] ?? [])));
        if (empty($dealIds) && !empty($payload['payments'])) {
            $dealIds = array_values(array_unique(array_map(static function ($row) {
                return (int)($row['deal_id'] ?? 0);
            }, $payload['payments'])));
        }
        $base = $this->buildSimpleDealLeadDrilldownByIds($dealIds, $filters);
        if (!empty($payload['payments'])) {
            $base['payments'] = array_values($payload['payments']);
            $base['meta']['total_payments'] = count($base['payments']);
        }
        if (!empty($payload['activities'])) {
            $base['activities'] = array_values($payload['activities']);
            $base['meta']['total_activities'] = count($base['activities']);
        }
        return $base;
    }

    private function getPerformancePartitionCardDrilldown(array $filters, string $mode, string $rowOwnerDidarId = ''): array
    {
        if ($rowOwnerDidarId !== '') {
            $filters['owner_didar_id'] = $rowOwnerDidarId;
            $filters['owner_didar_ids_csv'] = $rowOwnerDidarId;
            $filters['owner_didar_ids'] = [$rowOwnerDidarId];
        }
        if ($this->getKpiPaymentDateBasis($filters, 'sales') !== 'ui_payment_date') {
            if ($mode === 'open_valid') {
                return $this->getDealBasedDrilldown($filters, 'deal_status', 'deal_pending_total', '', $rowOwnerDidarId, '', '');
            }
            if ($mode === 'won_valid') {
                return $this->getDealBasedDrilldown($filters, 'deal_status', 'deal_won_total', '', $rowOwnerDidarId, '', '');
            }
            if ($mode === 'lost_valid') {
                return $this->getDealBasedDrilldown($filters, 'deal_status', 'deal_lost_total', '', $rowOwnerDidarId, '', '');
            }
            return ['leads' => [], 'deals' => [], 'activities' => [], 'payments' => [], 'meta' => ['total_leads' => 0, 'total_deals' => 0, 'total_activities' => 0, 'total_payments' => 0]];
        }

        $oracle = $this->getUiOracleKpiData($filters);
        $setKey = 'partition_open_valid';
        if ($mode === 'lost_valid') {
            $setKey = 'partition_lost_valid';
        } elseif ($mode === 'won_valid') {
            $setKey = 'partition_won_valid';
        }
        $ids = array_map('intval', array_keys($oracle['sets'][$setKey] ?? []));
        return $this->buildSimpleDealLeadDrilldownByIds($ids, $filters);
    }

    private function getDealConversionAnalysisDrilldown(array $filters, string $rowOwnerDidarId = ''): array
    {
        if ($rowOwnerDidarId !== '') {
            $filters['owner_didar_id'] = $rowOwnerDidarId;
            $filters['owner_didar_ids_csv'] = $rowOwnerDidarId;
            $filters['owner_didar_ids'] = [$rowOwnerDidarId];
        }

        $state = $this->buildUiOracleState($filters);
        $cohortIds = $this->uiOracleDealCohortIds($state, $filters);
        $salesBasis = $this->getKpiPaymentDateBasis($filters, 'sales');
        $salesRule = $this->getKpiPaymentCountRule($filters, 'sales');
        $statusScope = $this->getKpiPaymentStatusScope($filters);

        $salesWindowAgg = $this->uiOracleAggregateByDeal(
            $this->uiOraclePaymentEvents($state, $salesBasis, $statusScope, true, $filters),
            $salesRule
        );
        $firstAllAgg = $this->uiOracleAggregateByDeal(
            $this->uiOraclePaymentEvents($state, $salesBasis, $statusScope, false, $filters),
            'first_payment_only'
        );

        $validIds = [];
        $excludedRows = [];
        foreach ($cohortIds as $dealId => $_) {
            $deal = $state['deals'][$dealId] ?? null;
            if (!$deal) {
                continue;
            }
            $dealDate = (string)($deal['deal_date'] ?? '');
            $firstPaymentDate = (string)($firstAllAgg[$dealId]['first'] ?? '');
            if ($firstPaymentDate !== '' && $dealDate !== '' && $firstPaymentDate < $dealDate) {
                $firstInRange = $this->uiOracleInRange($firstPaymentDate, $filters);
                if ($firstInRange) {
                    $validIds[(string)$dealId] = true;
                    continue;
                }
                $excludedRows[] = [
                    'deal_id' => (int)$dealId,
                    'didar_deal_id' => (string)($deal['didar_deal_id'] ?? ''),
                    'title' => (string)($deal['title'] ?? ('#' . $dealId)),
                    'deal_date' => $dealDate,
                    'first_payment_date' => $firstPaymentDate,
                    'deal_status_group' => ((string)($deal['status'] ?? '') === 'Won' ? 'won' : (((string)($deal['status'] ?? '') === 'Lost') ? 'lost' : 'pending')),
                    'payable_amount' => (float)($deal['payable_amount'] ?? 0),
                    'total_paid' => (float)($salesWindowAgg[$dealId]['total'] ?? 0),
                    'owner_didar_id' => (string)($deal['owner_didar_id'] ?? ''),
                    'exclude_reason_key' => 'first_payment_before_deal_date',
                    'exclude_reason_label' => 'اولین واریزی قبل از تاریخ ثبت معامله است',
                ];
                continue;
            }
            $validIds[(string)$dealId] = true;
        }

        $convertedIds = [];
        $includedRows = [];
        $notConvertedRows = [];
        foreach ($validIds as $dealId => $_) {
            $deal = $state['deals'][$dealId] ?? null;
            if (!$deal) {
                continue;
            }
            $windowPaid = (float)($salesWindowAgg[$dealId]['total'] ?? 0);
            $allFirstDate = (string)($firstAllAgg[$dealId]['first'] ?? '');
            $row = [
                'deal_id' => (int)$dealId,
                'didar_deal_id' => (string)($deal['didar_deal_id'] ?? ''),
                'title' => (string)($deal['title'] ?? ('#' . $dealId)),
                'deal_date' => (string)($deal['deal_date'] ?? ''),
                'first_payment_date' => $allFirstDate,
                'deal_status_group' => ((string)($deal['status'] ?? '') === 'Won' ? 'won' : (((string)($deal['status'] ?? '') === 'Lost') ? 'lost' : 'pending')),
                'payable_amount' => (float)($deal['payable_amount'] ?? 0),
                'total_paid' => $windowPaid,
                'owner_didar_id' => (string)($deal['owner_didar_id'] ?? ''),
                'lead_type' => $this->uiOracleLeadType($deal, $state),
            ];
            if ($windowPaid > 0) {
                $convertedIds[(string)$dealId] = true;
                $row['is_converted'] = 1;
                $row['conversion_reason_label'] = 'طبق معیار فروش فعال، در بازه فروش/واریزی دارد';
                $includedRows[] = $row;
            } else {
                $row['is_converted'] = 0;
                $row['exclude_reason_key'] = 'no_sale_in_selected_window';
                $row['exclude_reason_label'] = 'در بازه و با معیار فروش فعال، واریزی/فروش ندارد';
                $notConvertedRows[] = $row;
            }
        }

        $oracle = $this->getUiOracleKpiData($filters);
        $referenceIds = array_map('intval', array_keys($oracle['sets']['conversion_reference'] ?? []));
        $convertedReferenceIds = array_map('intval', array_keys($oracle['sets']['sales_card_qualified'] ?? []));
        $convertedReferenceLookup = array_fill_keys($convertedReferenceIds, true);
        $referenceRows = [];
        foreach ($referenceIds as $dealId) {
            $deal = $state['deals'][(string)$dealId] ?? null;
            if (!$deal) {
                continue;
            }
            $windowPaid = (float)($salesWindowAgg[(string)$dealId]['total'] ?? 0);
            $allFirstDate = (string)($firstAllAgg[(string)$dealId]['first'] ?? '');
            $referenceRows[] = [
                'deal_id' => (int)$dealId,
                'didar_deal_id' => (string)($deal['didar_deal_id'] ?? ''),
                'title' => (string)($deal['title'] ?? ('#' . $dealId)),
                'deal_date' => (string)($deal['deal_date'] ?? ''),
                'first_payment_date' => $allFirstDate,
                'deal_status_group' => ((string)($deal['status'] ?? '') === 'Won' ? 'won' : (((string)($deal['status'] ?? '') === 'Lost') ? 'lost' : 'pending')),
                'payable_amount' => (float)($deal['payable_amount'] ?? 0),
                'total_paid' => $windowPaid,
                'owner_didar_id' => (string)($deal['owner_didar_id'] ?? ''),
                'lead_type' => $this->uiOracleLeadType($deal, $state),
                'is_converted' => isset($convertedReferenceLookup[(int)$dealId]) ? 1 : 0,
                'conversion_reason_label' => isset($convertedReferenceLookup[(int)$dealId])
                    ? 'طبق معیار فروش فعال، در بازه فروش/واریزی دارد'
                    : 'در مرجع نرخ تبدیل قرار دارد اما در بازه و با معیار فروش فعال، تبدیل نشده است',
            ];
        }
        $includedDealIds = array_map(static function ($id) { return (int)$id; }, $referenceIds);
        $includedDrilldown = $this->buildSimpleDealLeadDrilldownByIds($includedDealIds, $filters);

        $reasonBuckets = [];
        foreach ($excludedRows as $row) {
            $k = (string)($row['exclude_reason_key'] ?? 'unknown');
            if (!isset($reasonBuckets[$k])) {
                $reasonBuckets[$k] = ['key' => $k, 'label' => (string)($row['exclude_reason_label'] ?? 'نامشخص'), 'count' => 0];
            }
            $reasonBuckets[$k]['count']++;
        }
        foreach ($notConvertedRows as $row) {
            $k = (string)($row['exclude_reason_key'] ?? 'unknown');
            if (!isset($reasonBuckets[$k])) {
                $reasonBuckets[$k] = ['key' => $k, 'label' => (string)($row['exclude_reason_label'] ?? 'نامشخص'), 'count' => 0];
            }
            $reasonBuckets[$k]['count']++;
        }
        $reasonBuckets = array_values($reasonBuckets);
        usort($reasonBuckets, static function ($a, $b) {
            return ((int)$b['count'] <=> (int)$a['count']);
        });

        $validCount = count($validIds);
        $convertedCount = count($convertedIds);
        $conversionRate = $validCount > 0 ? round(($convertedCount / $validCount) * 100, 2) : 0.0;
        $nonConversionRate = $validCount > 0 ? round((($validCount - $convertedCount) / $validCount) * 100, 2) : 0.0;
        $crossDateAddedIds = array_map('intval', array_keys($oracle['sets']['conversion_cross_date_sales_added'] ?? []));

        return [
            'leads' => $includedDrilldown['leads'] ?? [],
            'deals' => $includedDrilldown['deals'] ?? [],
            'activities' => [],
            'payments' => [],
            'meta' => [
                'drilldown_kind' => 'conversion_analysis',
                'conversion_type' => 'deal',
                'title' => 'جزئیات نرخ تبدیل معاملات',
                'total_leads' => (int)count($includedDrilldown['leads'] ?? []),
                'total_deals' => (int)count($includedDrilldown['deals'] ?? []),
                'total_activities' => 0,
                'total_payments' => 0,
            ],
            'conversion_analysis' => [
                'type' => 'deal',
                'formula_title' => 'فرمول نرخ تبدیل معاملات',
                'formula_text' => 'نرخ تبدیل = تعداد معاملات تبدیل‌شده ÷ تعداد معاملات معتبرِ ثبت‌شده در بازه × ۱۰۰',
                'criteria' => $this->buildConversionCriteriaSummary($filters),
                'summary' => [
                    'raw_cohort_count' => count($cohortIds),
                    'excluded_count' => count($excludedRows),
                    'valid_cohort_count' => (int)count($referenceIds),
                    'converted_count' => (int)count($convertedReferenceIds),
                    'not_converted_count' => max(0, count($referenceIds) - count($convertedReferenceIds)),
                    'conversion_rate' => (count($referenceIds) > 0 ? round((count($convertedReferenceIds) / count($referenceIds)) * 100, 2) : 0.0),
                    'non_conversion_rate' => (count($referenceIds) > 0 ? round(((count($referenceIds) - count($convertedReferenceIds)) / count($referenceIds)) * 100, 2) : 0.0),
                ],
                'option_a_partition' => [
                    'formula_text' => 'مرجع نرخ تبدیل (Option A) = تعداد یکتای معاملات در اجتماع {فروش کل، بدون پرداخت، ناموفق، موفق (Won)}',
                    'sales_count' => (int)($oracle['sales_card']['deal_count'] ?? 0),
                    'without_payment_count' => (int)($oracle['conversion_partition']['without_payment_count'] ?? 0),
                    'lost_count' => (int)($oracle['conversion_partition']['lost_count'] ?? 0),
                    'won_count' => (int)($oracle['conversion_partition']['won_count'] ?? 0),
                    'reference_total' => (int)($oracle['deal_conversion']['total_deals'] ?? 0),
                    'sales_won_overlap_count' => (int)count($oracle['sets']['conversion_sales_won_overlap'] ?? []),
                    'sales_won_overlap_ids' => array_slice(array_map('intval', array_keys($oracle['sets']['conversion_sales_won_overlap'] ?? [])), 0, 200),
                    'cross_date_sales_added_count' => count($crossDateAddedIds),
                    'cross_date_sales_added_ids' => array_slice($crossDateAddedIds, 0, 200),
                ],
                'excluded_reasons_summary' => $reasonBuckets,
                'included_deals' => $referenceRows,
                'excluded_deals' => array_merge($excludedRows, $notConvertedRows),
                'debug' => [
                    'sales_date_basis' => $salesBasis,
                    'sales_count_rule' => $salesRule,
                    'payment_status_scope' => $statusScope,
                ],
            ],
        ];
    }

    private function getLeadConversionLeadAnalysisDrilldown(array $filters, string $rowOwnerDidarId = ''): array
    {
        if ($rowOwnerDidarId !== '') {
            $filters['owner_didar_id'] = $rowOwnerDidarId;
            $filters['owner_didar_ids_csv'] = $rowOwnerDidarId;
            $filters['owner_didar_ids'] = [$rowOwnerDidarId];
        }
        $pdo = $this->db->getPdo();
        $paymentsByDealSql = $this->buildKpiPaymentsByDealSql($filters, 'sales');
        $dateFrom = $filters['date_from'] ?? null;
        $dateTo = $filters['date_to'] ?? null;

        $personWhereParts = [];
        $personParams = [];
        if (!empty($dateFrom)) { $personWhereParts[] = "DATE(p.register_time) >= ?"; $personParams[] = $dateFrom; }
        if (!empty($dateTo)) { $personWhereParts[] = "DATE(p.register_time) <= ?"; $personParams[] = $dateTo; }
        if (!empty($filters['owner_didar_ids_csv'])) { $personWhereParts[] = "FIND_IN_SET(p.owner_didar_id, ?)"; $personParams[] = $filters['owner_didar_ids_csv']; }
        elseif (!empty($filters['owner_didar_id'])) { $personWhereParts[] = "p.owner_didar_id = ?"; $personParams[] = $filters['owner_didar_id']; }
        $personWhere = !empty($personWhereParts) ? implode(' AND ', $personWhereParts) : '1=1';

        $leadsSql = "
            SELECT
                p.id as person_id,
                p.didar_contact_id as contact_id,
                TRIM(CONCAT(COALESCE(p.first_name, ''), ' ', COALESCE(p.last_name, ''))) as full_name,
                p.mobile_phone,
                p.secondary_mobile_phone,
                DATE(COALESCE(p.register_time, p.created_at)) as lead_date,
                p.owner_didar_id,
                COALESCE(u.display_name, 'بدون مسئول') as owner_name
            FROM persons p
            LEFT JOIN users u ON u.didar_user_id = p.owner_didar_id
            WHERE {$personWhere}
            ORDER BY lead_date DESC, p.id DESC
            LIMIT 5000
        ";
        $allLeads = $this->runSelectAll($leadsSql, $personParams);
        if (empty($allLeads)) {
            return [
                'leads' => [], 'deals' => [], 'activities' => [], 'payments' => [],
                'meta' => ['drilldown_kind' => 'conversion_analysis', 'conversion_type' => 'lead', 'title' => 'جزئیات نرخ تبدیل لید جدید', 'total_leads' => 0, 'total_deals' => 0, 'total_activities' => 0, 'total_payments' => 0],
                'conversion_analysis' => [
                    'type' => 'lead',
                    'formula_title' => 'فرمول نرخ تبدیل لید جدید',
                    'formula_text' => 'نرخ تبدیل لید جدید = لیدهای جدیدِ تبدیل‌شده ÷ کل لیدهای جدید × ۱۰۰',
                    'criteria' => $this->buildConversionCriteriaSummary($filters),
                    'summary' => ['raw_cohort_count' => 0, 'excluded_count' => 0, 'valid_cohort_count' => 0, 'converted_count' => 0, 'not_converted_count' => 0, 'conversion_rate' => 0, 'non_conversion_rate' => 0],
                    'excluded_reasons_summary' => [],
                    'included_leads' => [],
                    'excluded_leads' => [],
                ]
            ];
        }

        $contactIds = array_values(array_filter(array_map(static function ($r) { return trim((string)($r['contact_id'] ?? '')); }, $allLeads)));
        $dealsByContact = [];
        $convertedLeadIds = [];
        if (!empty($contactIds)) {
            $ph = implode(',', array_fill(0, count($contactIds), '?'));
            $dealsJoinSql = "
                SELECT
                    d.id as deal_id,
                    d.didar_deal_id,
                    d.contact_didar_id as contact_id,
                    d.title,
                    DATE(COALESCE(d.register_time, d.created_at)) as deal_date,
                    d.owner_didar_id,
                    COALESCE(u.display_name, 'بدون مسئول') as owner_name,
                    COALESCE(d.payable_amount, 0) as payable_amount,
                    COALESCE(tp.total_paid, 0) as total_paid
                FROM deals d
                LEFT JOIN users u ON u.didar_user_id = d.owner_didar_id
                LEFT JOIN ({$paymentsByDealSql}) tp ON tp.deal_id = d.id
                WHERE d.contact_didar_id IN ({$ph})
                ORDER BY d.id DESC
            ";
            $dealRows = $this->runSelectAll($dealsJoinSql, $contactIds);
            foreach ($dealRows as $row) {
                $cid = (string)($row['contact_id'] ?? '');
                if ($cid === '') continue;
                $dealsByContact[$cid][] = $row;
                if ((float)($row['total_paid'] ?? 0) > 0) {
                    $convertedLeadIds[$cid] = true;
                }
            }
        }

        $includedLeads = [];
        $excludedLeads = [];
        $reasonBuckets = [];
        foreach ($allLeads as $lead) {
            $cid = (string)($lead['contact_id'] ?? '');
            $leadDeals = $dealsByContact[$cid] ?? [];
            $hasAnyDeal = !empty($leadDeals);
            $hasPaidDeal = isset($convertedLeadIds[$cid]);
            $row = [
                'contact_id' => $cid,
                'full_name' => (string)($lead['full_name'] ?? ''),
                'mobile_phone' => (string)($lead['mobile_phone'] ?? ''),
                'secondary_mobile_phone' => (string)($lead['secondary_mobile_phone'] ?? ''),
                'lead_date' => (string)($lead['lead_date'] ?? ''),
                'owner_name' => (string)($lead['owner_name'] ?? ''),
                'owner_didar_id' => (string)($lead['owner_didar_id'] ?? ''),
                'deal_count' => count($leadDeals),
                'converted_deal_count' => 0,
                'first_deal_date' => '',
                'first_payment_date' => '',
            ];
            if ($hasAnyDeal) {
                $firstDealDate = '';
                $firstPaymentDate = '';
                $convertedDealCount = 0;
                foreach ($leadDeals as $dealRow) {
                    $dd = (string)($dealRow['deal_date'] ?? '');
                    if ($dd !== '' && ($firstDealDate === '' || $dd < $firstDealDate)) $firstDealDate = $dd;
                    if ((float)($dealRow['total_paid'] ?? 0) > 0) {
                        $convertedDealCount++;
                    }
                }
                $row['converted_deal_count'] = $convertedDealCount;
                $row['first_deal_date'] = $firstDealDate;
                // approximate first payment date from tx/json is not readily available per lead; keep empty in phase 1
                $row['first_payment_date'] = $firstPaymentDate;
            }

            if ($hasPaidDeal) {
                $row['is_converted'] = 1;
                $row['conversion_reason_label'] = 'حداقل یک معامله با پرداخت (طبق معیار فروش فعال) دارد';
                $includedLeads[] = $row;
            } else {
                $row['is_converted'] = 0;
                if (!$hasAnyDeal) {
                    $row['exclude_reason_key'] = 'no_deal';
                    $row['exclude_reason_label'] = 'هیچ معامله‌ای برای لید ثبت نشده است';
                } else {
                    $row['exclude_reason_key'] = 'deals_without_paid';
                    $row['exclude_reason_label'] = 'معامله دارد اما هیچ معامله‌ای با پرداختِ منطبق با معیار فعال ندارد';
                }
                $excludedLeads[] = $row;
                $k = (string)$row['exclude_reason_key'];
                if (!isset($reasonBuckets[$k])) {
                    $reasonBuckets[$k] = ['key' => $k, 'label' => (string)$row['exclude_reason_label'], 'count' => 0];
                }
                $reasonBuckets[$k]['count']++;
            }
        }
        $reasonBuckets = array_values($reasonBuckets);
        usort($reasonBuckets, static function ($a, $b) { return ((int)$b['count'] <=> (int)$a['count']); });

        $includedContactIds = array_values(array_map(static function ($r) { return (string)$r['contact_id']; }, $includedLeads));
        $includedLeadRows = [];
        if (!empty($includedContactIds)) {
            $ph = implode(',', array_fill(0, count($includedContactIds), '?'));
            $includedLeadRows = $this->runSelectAll("
                SELECT
                    p.didar_contact_id as contact_id,
                    TRIM(CONCAT(COALESCE(p.first_name, ''), ' ', COALESCE(p.last_name, ''))) as full_name,
                    p.mobile_phone,
                    p.secondary_mobile_phone,
                    DATE(COALESCE(p.register_time, p.created_at)) as lead_date,
                    COALESCE(u.display_name, 'بدون مسئول') as owner_name
                FROM persons p
                LEFT JOIN users u ON u.didar_user_id = p.owner_didar_id
                WHERE p.didar_contact_id IN ({$ph})
            ", $includedContactIds);
        }

        $includedDeals = [];
        foreach ($includedContactIds as $cid) {
            foreach (($dealsByContact[$cid] ?? []) as $dealRow) {
                if ((float)($dealRow['total_paid'] ?? 0) > 0) {
                    $includedDeals[] = [
                        'deal_id' => (int)($dealRow['deal_id'] ?? 0),
                        'didar_deal_id' => (string)($dealRow['didar_deal_id'] ?? ''),
                        'title' => (string)($dealRow['title'] ?? ''),
                        'deal_date' => (string)($dealRow['deal_date'] ?? ''),
                        'owner_name' => (string)($dealRow['owner_name'] ?? ''),
                        'owner_didar_id' => (string)($dealRow['owner_didar_id'] ?? ''),
                        'contact_didar_id' => (string)($dealRow['contact_id'] ?? ''),
                        'deal_status_group' => 'pending',
                        'payable_amount' => (float)($dealRow['payable_amount'] ?? 0),
                        'total_paid' => (float)($dealRow['total_paid'] ?? 0),
                        'has_payment' => ((float)($dealRow['total_paid'] ?? 0) > 0 ? 1 : 0),
                    ];
                }
            }
        }

        $totalNewLeads = count($allLeads);
        $convertedLeads = count($includedLeads);
        $conversionRate = $totalNewLeads > 0 ? round(($convertedLeads / $totalNewLeads) * 100, 2) : 0.0;
        $nonConversionRate = $totalNewLeads > 0 ? round((($totalNewLeads - $convertedLeads) / $totalNewLeads) * 100, 2) : 0.0;

        return [
            'leads' => $includedLeadRows,
            'deals' => $includedDeals,
            'activities' => [],
            'payments' => [],
            'meta' => [
                'drilldown_kind' => 'conversion_analysis',
                'conversion_type' => 'lead',
                'title' => 'جزئیات نرخ تبدیل لید جدید',
                'total_leads' => count($includedLeadRows),
                'total_deals' => count($includedDeals),
                'total_activities' => 0,
                'total_payments' => 0,
            ],
            'conversion_analysis' => [
                'type' => 'lead',
                'formula_title' => 'فرمول نرخ تبدیل لید جدید',
                'formula_text' => 'نرخ تبدیل لید جدید = تعداد لیدهای جدیدِ تبدیل‌شده ÷ کل لیدهای جدید در بازه × ۱۰۰',
                'criteria' => $this->buildConversionCriteriaSummary($filters),
                'summary' => [
                    'raw_cohort_count' => $totalNewLeads,
                    'excluded_count' => count($excludedLeads),
                    'valid_cohort_count' => $totalNewLeads,
                    'converted_count' => $convertedLeads,
                    'not_converted_count' => max(0, $totalNewLeads - $convertedLeads),
                    'conversion_rate' => $conversionRate,
                    'non_conversion_rate' => $nonConversionRate,
                ],
                'excluded_reasons_summary' => $reasonBuckets,
                'included_leads' => $includedLeads,
                'excluded_leads' => $excludedLeads,
            ],
        ];
    }

    private function buildConversionCriteriaSummary(array $filters): array
    {
        return [
            'date_from' => (string)($filters['date_from'] ?? ''),
            'date_to' => (string)($filters['date_to'] ?? ''),
            'sales_date_basis' => (string)$this->getKpiPaymentDateBasis($filters, 'sales'),
            'sales_count_rule' => (string)$this->getKpiPaymentCountRule($filters, 'sales'),
            'payment_date_basis' => (string)$this->getKpiPaymentDateBasis($filters, 'payment'),
            'payment_count_rule' => (string)$this->getKpiPaymentCountRule($filters, 'payment'),
            'payment_status_scope' => (string)$this->getKpiPaymentStatusScope($filters),
            'legacy_deal_scope' => (string)($filters['legacy_deal_scope'] ?? 'exclude'),
            'owner_didar_ids_csv' => (string)($filters['owner_didar_ids_csv'] ?? ''),
            'owner_didar_id' => (string)($filters['owner_didar_id'] ?? ''),
        ];
    }

    private function getSalesTotalCardDrilldown(array $filters, string $rowOwnerDidarId): array
    {
        if ($this->getKpiPaymentDateBasis($filters, 'sales') === 'ui_payment_date') {
            return $this->getSalesTotalCardDrilldownUiOracle($filters, $rowOwnerDidarId);
        }
        $whereDeals = $this->buildWhereClauseForDealsNoDate($filters, 'd');
        $paramsDeals = $this->buildParamsForDealsNoDate($filters);
        if ($rowOwnerDidarId !== '') {
            $whereDeals .= " AND d.owner_didar_id = ?";
            $paramsDeals[] = $rowOwnerDidarId;
        }

        $salesQualifierSql = $this->buildKpiFirstPaymentDayByDealSql($filters, 'sales');
        $salesAllTimeTotalsSql = $this->buildKpiAllTimePaymentTotalsByDealSql($filters, 'sales');
        $legacyOpenClause = $this->buildLegacyDealFilterClause($filters, 'd2');
        $legacyOpenClause = $legacyOpenClause !== '' ? (' AND ' . $legacyOpenClause) : '';
        $failureReasonExpr = $this->buildFailureReasonExpr('d');

        $paymentDateWhere = [];
        $paymentDateParams = [];
        if (!empty($filters['date_from'])) {
            $paymentDateWhere[] = "pbd.payment_date >= ?";
            $paymentDateParams[] = $filters['date_from'];
        }
        if (!empty($filters['date_to'])) {
            $paymentDateWhere[] = "pbd.payment_date <= ?";
            $paymentDateParams[] = $filters['date_to'];
        }
        $paymentDateClause = !empty($paymentDateWhere) ? implode(' AND ', $paymentDateWhere) : '1=1';
        $paymentDateClauseQ = str_replace('pbd.', 'q.', $paymentDateClause);

        $dealsBaseSql = "
            SELECT
                d.id as deal_id,
                d.didar_deal_id,
                d.title,
                DATE(COALESCE(d.register_time, d.created_at)) as deal_date,
                COALESCE(d.owner_didar_id, '') as owner_didar_id,
                COALESCE(u.display_name, 'بدون مسئول') as owner_name,
                COALESCE(d.contact_didar_id, '') as contact_didar_id,
                {$this->buildDealProductHintExpr('d')} as deal_product_hint,
                CASE
                    WHEN DATE(COALESCE(p.register_time, p.created_at)) = DATE(COALESCE(d.register_time, d.created_at)) THEN 'new'
                    ELSE 'old'
                END as lead_type,
                CASE
                    WHEN d.status = 'Won' THEN 'won'
                    WHEN d.status = 'Lost' THEN 'lost'
                    ELSE 'pending'
                END as deal_status_group,
                COALESCE(d.payable_amount, 0) as payable_amount,
                COALESCE(pay.total_paid, 0) as total_paid,
                pay.first_payment_date,
                DATE(COALESCE(d.lost_time, d.created_at)) as lost_date,
                CASE WHEN COALESCE(pay.total_paid, 0) > 0 THEN 1 ELSE 0 END as has_payment,
                CASE WHEN COALESCE(od.open_count, 0) > 0 THEN 1 ELSE 0 END as has_open_deal,
                {$failureReasonExpr} as failure_reason
            FROM deals d
            INNER JOIN (
                SELECT DISTINCT q.deal_id, q.payment_date as first_payment_date
                FROM ({$salesQualifierSql}) q
                WHERE {$paymentDateClauseQ}
            ) pw ON pw.deal_id = d.id
            LEFT JOIN users u ON u.didar_user_id = d.owner_didar_id
            LEFT JOIN persons p ON p.didar_contact_id = d.contact_didar_id
            LEFT JOIN ({$salesAllTimeTotalsSql}) pay ON pay.deal_id = d.id
            LEFT JOIN (
                SELECT
                    d2.contact_didar_id,
                    COUNT(*) as open_count
                FROM deals d2
                WHERE d2.status NOT IN ('Won', 'Lost')
                  {$legacyOpenClause}
                GROUP BY d2.contact_didar_id
            ) od ON od.contact_didar_id = d.contact_didar_id
            WHERE {$whereDeals}
              AND pw.first_payment_date >= DATE(COALESCE(d.register_time, d.created_at))
        ";

        $params = array_merge($paymentDateParams, $paramsDeals);

        $dealsSql = "
            SELECT DISTINCT
                b.deal_id,
                b.didar_deal_id,
                b.title,
                b.deal_date,
                b.owner_name,
                b.owner_didar_id,
                b.contact_didar_id,
                b.deal_status_group,
                b.payable_amount,
                b.total_paid,
                b.has_payment
            FROM ({$dealsBaseSql}) b
            ORDER BY b.deal_date DESC, b.deal_id DESC
            LIMIT 500
        ";
        $deals = $this->runSelectAll($dealsSql, $params);

        $leadsSql = "
            SELECT DISTINCT
                p.didar_contact_id as contact_id,
                TRIM(CONCAT(COALESCE(p.first_name, ''), ' ', COALESCE(p.last_name, ''))) as full_name,
                p.mobile_phone,
                p.secondary_mobile_phone,
                DATE(COALESCE(p.register_time, p.created_at)) as lead_date,
                COALESCE(u.display_name, 'بدون مسئول') as owner_name
            FROM persons p
            LEFT JOIN users u ON u.didar_user_id = p.owner_didar_id
            INNER JOIN (
                SELECT DISTINCT contact_didar_id
                FROM ({$dealsBaseSql}) m
                WHERE COALESCE(m.contact_didar_id, '') <> ''
            ) md ON md.contact_didar_id = p.didar_contact_id
            ORDER BY lead_date DESC, full_name ASC
            LIMIT 500
        ";
        $leads = $this->runSelectAll($leadsSql, $params);

        return [
            'leads' => $leads,
            'deals' => $deals,
            'activities' => [],
            'payments' => [],
            'meta' => [
                'total_leads' => count($leads),
                'total_deals' => count($deals),
                'total_activities' => 0,
                'total_payments' => 0
            ]
        ];
    }

    private function getDailySalesChartPointDrilldown(array $filters, string $rowDate): array
    {
        if ($rowDate === '') {
            return ['leads' => [], 'deals' => [], 'activities' => [], 'payments' => [], 'meta' => ['total_leads' => 0, 'total_deals' => 0, 'total_activities' => 0, 'total_payments' => 0]];
        }

        $dailyFilters = $filters;
        $dailyFilters['date_from'] = $rowDate;
        $dailyFilters['date_to'] = $rowDate;
        $dailyFilters['person_date_from'] = $rowDate;
        $dailyFilters['person_date_to'] = $rowDate;

        $pdo = $this->db->getPdo();
        $paymentsByDealDateSql = $this->buildKpiPaymentsWindowByDealDateSql($dailyFilters, 'sales');
        $whereDeals = $this->buildWhereClauseForDealsNoDate($dailyFilters, 'd');
        $paramsDeals = $this->buildParamsForDealsNoDate($dailyFilters);

        $sql = "
            SELECT
                d.id as deal_id,
                SUM(pay.amount) as day_paid
            FROM deals d
            INNER JOIN ({$paymentsByDealDateSql}) pay ON pay.deal_id = d.id
            WHERE {$whereDeals}
              AND pay.payment_date = ?
            GROUP BY d.id
        ";
        $rows = $this->runSelectAll($sql, array_merge($paramsDeals, [$rowDate]));
        $dealIds = [];
        $dayPaidMap = [];
        foreach ($rows as $row) {
            $id = (int)($row['deal_id'] ?? 0);
            if ($id <= 0) {
                continue;
            }
            $dealIds[] = $id;
            $dayPaidMap[(string)$id] = (float)($row['day_paid'] ?? 0);
        }
        if (empty($dealIds)) {
            return ['leads' => [], 'deals' => [], 'activities' => [], 'payments' => [], 'meta' => ['total_leads' => 0, 'total_deals' => 0, 'total_activities' => 0, 'total_payments' => 0]];
        }

        $result = $this->buildSimpleDealLeadDrilldownByIds($dealIds, $dailyFilters);
        foreach ($result['deals'] as &$deal) {
            $did = (string)($deal['deal_id'] ?? '');
            $deal['total_paid'] = (float)($dayPaidMap[$did] ?? 0);
        }
        unset($deal);
        $payments = [];
        foreach ($dayPaidMap as $dealId => $amount) {
            $payments[] = [
                'deal_id' => (int)$dealId,
                'amount' => (float)$amount,
                'payment_date' => $rowDate
            ];
        }
        $result['payments'] = $payments;
        $result['meta']['total_payments'] = count($payments);
        return $result;
    }

    private function getSalesUiOracle(array $filters): array
    {
        $oracle = $this->getUiOracleKpiData($filters);
        $row = $oracle['sales_card'] ?? [];
        return [
            'deal_count' => (int)($row['deal_count'] ?? 0),
            'total_contract_amount' => (float)($row['total_contract_amount'] ?? 0),
            'total_collected_amount' => (float)($row['total_collected_amount'] ?? 0),
        ];
    }

    private function getSalesTotalCardDrilldownUiOracle(array $filters, string $rowOwnerDidarId = ''): array
    {
        $oracle = $this->getUiOracleKpiData($filters);
        $ids = array_map('intval', array_keys($oracle['sets']['sales_card_qualified'] ?? []));
        if ($rowOwnerDidarId !== '') {
            $ids = array_values(array_filter($ids, function ($dealId) use ($oracle, $rowOwnerDidarId) {
                $deal = $oracle['state']['deals'][(string)$dealId] ?? null;
                return $deal && (string)($deal['owner_didar_id'] ?? '') === $rowOwnerDidarId;
            }));
        }
        return $this->buildSimpleDealLeadDrilldownByIds($ids, $filters, 'sales');
    }

    private function getProductSalesShareChartDrilldown(array $filters, string $rowProductName): array
    {
        if ($rowProductName === '') {
            return ['leads' => [], 'deals' => [], 'activities' => [], 'payments' => [], 'meta' => ['total_leads' => 0, 'total_deals' => 0, 'total_activities' => 0, 'total_payments' => 0]];
        }
        if ($this->getKpiPaymentDateBasis($filters, 'sales') === 'ui_payment_date') {
            $oracle = $this->getUiOracleKpiData($filters);
            $targetName = $this->normalizeProductName($rowProductName);
            $ids = [];
            foreach (($oracle['sets']['sales_card_qualified'] ?? []) as $dealId => $_) {
                $deal = $oracle['state']['deals'][(string)$dealId] ?? null;
                if (!$deal) {
                    continue;
                }
                $pid = $this->uiOraclePrimaryProductIdForDeal($deal, $oracle['state']['deal_products'] ?? []);
                $resolved = $this->uiOracleResolvedProductName($deal, $pid, $oracle['state']['product_names'] ?? []);
                if ($resolved !== '' && $this->normalizeProductName($resolved) === $targetName) {
                    $ids[] = (int)$dealId;
                }
            }
            return $this->buildSimpleDealLeadDrilldownByIds($ids, $filters, 'sales');
        }
        $pdo = $this->db->getPdo();
        $dealProductHintExpr = $this->buildDealProductHintExpr('d');
        $resolvedProductExpr = $this->buildResolvedProductNameExpr($dealProductHintExpr, 'p');
        $primaryDealProductSql = $this->buildPrimaryDealProductSql();
        $paymentsByDealDateSql = $this->buildKpiPaymentsWindowByDealDateSql($filters, 'sales');
        $whereDeals = $this->buildWhereClauseForDealsNoDate($filters, 'd');
        $paramsDeals = $this->buildParamsForDealsNoDate($filters);

        $paymentDateWhere = [];
        $paymentParams = [];
        if (!empty($filters['date_from'])) {
            $paymentDateWhere[] = "pbd.payment_date >= ?";
            $paymentParams[] = $filters['date_from'];
        }
        if (!empty($filters['date_to'])) {
            $paymentDateWhere[] = "pbd.payment_date <= ?";
            $paymentParams[] = $filters['date_to'];
        }
        $paymentDateClause = !empty($paymentDateWhere) ? implode(' AND ', $paymentDateWhere) : '1=1';

        $sql = "
            SELECT DISTINCT d.id as deal_id
            FROM deals d
            INNER JOIN (
                SELECT DISTINCT pbd.deal_id
                FROM ({$paymentsByDealDateSql}) pbd
                WHERE {$paymentDateClause}
            ) pay ON pay.deal_id = d.id
            INNER JOIN ({$primaryDealProductSql}) dp ON dp.deal_id = d.id
            LEFT JOIN products p ON p.id = dp.product_id
            WHERE {$whereDeals}
              AND {$resolvedProductExpr} = ?
        ";
        $rows = $this->runSelectAll($sql, array_merge($paymentParams, $paramsDeals, [$rowProductName]));
        $dealIds = array_map('intval', array_column($rows, 'deal_id'));
        return $this->buildSimpleDealLeadDrilldownByIds($dealIds, $filters);
    }

    private function getConversionFunnelChartDrilldown(array $filters, string $stage): array
    {
        $stage = trim($stage);
        if ($stage === 'لید جدید') {
            return $this->getLeadStatusDrilldown($filters, 'lead_new_count', '', '');
        }
        if ($stage === 'معامله') {
            if ($this->getKpiPaymentDateBasis($filters, 'sales') === 'ui_payment_date') {
                $oracle = $this->getUiOracleKpiData($filters);
                $ids = array_map('intval', array_keys($oracle['sets']['conversion_cohort'] ?? []));
                return $this->buildSimpleDealLeadDrilldownByIds($ids, $filters);
            }
            return $this->getDealBasedDrilldown($filters, 'deal_status', 'deal_total', '', '', '', '');
        }
        if ($stage === 'پرداخت') {
            return $this->getSalesTotalCardDrilldown($filters, '');
        }
        if ($stage === 'تسویه کامل') {
            if ($this->getKpiPaymentDateBasis($filters, 'payment') === 'ui_payment_date') {
                return $this->getFinancialPerformanceCardDrilldownUiOracle($filters, 'settled', '');
            }
            return $this->getDealBasedDrilldown($filters, 'payment_status', 'settled_deals_count', '', '', '', '');
        }
        return ['leads' => [], 'deals' => [], 'activities' => [], 'payments' => [], 'meta' => ['total_leads' => 0, 'total_deals' => 0, 'total_activities' => 0, 'total_payments' => 0]];
    }

    private function getDealBasedDrilldown(
        array $filters,
        string $tableId,
        string $metricKey,
        string $rowDate,
        string $rowOwnerDidarId,
        string $rowProductName,
        string $rowFailureReason
    ): array {
        if (
            $tableId === 'payment_status'
            && $rowDate === ''
            && in_array($metricKey, ['settled_deals_count', 'outstanding_amount_sum'], true)
            && $this->getKpiPaymentDateBasis($filters, 'payment') === 'ui_payment_date'
        ) {
            return $this->getFinancialPerformanceCardDrilldownUiOracle(
                $filters,
                $metricKey === 'settled_deals_count' ? 'settled' : 'outstanding',
                $rowOwnerDidarId
            );
        }
        $pdo = $this->db->getPdo();
        $condition = '1=1';
        $conditionParams = [];

        switch ($tableId) {
            case 'deal_status':
                $map = [
                    'deal_total' => '1=1',
                    'deal_new_total' => "b.lead_type = 'new'",
                    'deal_old_total' => "b.lead_type = 'old'",
                    'deal_pending_total' => "b.deal_status_group = 'pending'",
                    'deal_pending_new' => "b.deal_status_group = 'pending' AND b.lead_type = 'new'",
                    'deal_pending_old' => "b.deal_status_group = 'pending' AND b.lead_type = 'old'",
                    'deal_with_sales_total' => "b.has_payment = 1",
                    'deal_with_sales_new' => "b.has_payment = 1 AND b.lead_type = 'new'",
                    'deal_with_sales_old' => "b.has_payment = 1 AND b.lead_type = 'old'",
                    'deal_without_sales_total' => "b.has_payment = 0 AND b.deal_status_group = 'pending'",
                    'deal_without_sales_new' => "b.has_payment = 0 AND b.deal_status_group = 'pending' AND b.lead_type = 'new'",
                    'deal_without_sales_old' => "b.has_payment = 0 AND b.deal_status_group = 'pending' AND b.lead_type = 'old'",
                    'deal_won_total' => "b.deal_status_group = 'won'",
                    'deal_won_new' => "b.deal_status_group = 'won' AND b.lead_type = 'new'",
                    'deal_won_old' => "b.deal_status_group = 'won' AND b.lead_type = 'old'",
                    'deal_lost_total' => "b.deal_status_group = 'lost'",
                    'deal_lost_new' => "b.deal_status_group = 'lost' AND b.lead_type = 'new'",
                    'deal_lost_old' => "b.deal_status_group = 'lost' AND b.lead_type = 'old'",
                ];
                $condition = $map[$metricKey] ?? '1=1';
                break;

            case 'payment_mix':
                $map = [
                    'new_leads_with_payment' => "b.lead_type = 'new' AND b.has_payment = 1",
                    'new_leads_without_payment' => "b.lead_type = 'new' AND b.has_payment = 0 AND b.deal_status_group <> 'lost'",
                    'new_leads_failed' => "b.lead_type = 'new' AND b.deal_status_group = 'lost'",
                    'old_leads_with_payment' => "b.lead_type = 'old' AND b.has_payment = 1",
                    'old_leads_without_payment' => "b.lead_type = 'old' AND b.has_payment = 0 AND b.deal_status_group <> 'lost'",
                    'old_leads_failed' => "b.lead_type = 'old' AND b.deal_status_group = 'lost'",
                ];
                $condition = $map[$metricKey] ?? '1=1';
                break;

            case 'payment_status':
                $map = [
                    'outstanding_deals_count' => "b.has_payment = 1 AND b.payable_amount > 0 AND b.total_paid < b.payable_amount",
                    'settled_deals_count' => "b.has_payment = 1 AND b.payable_amount > 0 AND b.total_paid >= b.payable_amount",
                    'outstanding_amount_sum' => "b.has_payment = 1 AND b.payable_amount > 0 AND b.total_paid < b.payable_amount",
                    'settled_contract_amount_sum' => "b.has_payment = 1 AND b.payable_amount > 0 AND b.total_paid >= b.payable_amount",
                    'payments_sum' => "b.has_payment = 1",
                ];
                $condition = $map[$metricKey] ?? '1=1';
                break;

            case 'conversion_status':
                $map = [
                    'conversion_rate_total' => '1=1',
                    'non_conversion_rate_total' => '1=1',
                    'conversion_rate_new' => "b.lead_type = 'new'",
                    'non_conversion_rate_new' => "b.lead_type = 'new'",
                    'conversion_rate_old' => "b.lead_type = 'old'",
                    'non_conversion_rate_old' => "b.lead_type = 'old'",
                    'sales_total_count' => "b.has_payment = 1",
                    'sales_new_count' => "b.has_payment = 1 AND b.lead_type = 'new'",
                    'sales_old_count' => "b.has_payment = 1 AND b.lead_type = 'old'",
                ];
                $condition = $map[$metricKey] ?? '1=1';
                break;

            case 'conversion_time':
                $map = [
                    'avg_conversion_days_total' => "b.has_payment = 1 AND b.first_payment_date IS NOT NULL",
                    'avg_conversion_days_new' => "b.has_payment = 1 AND b.first_payment_date IS NOT NULL AND b.lead_type = 'new'",
                    'avg_conversion_days_old' => "b.has_payment = 1 AND b.first_payment_date IS NOT NULL AND b.lead_type = 'old'",
                    'avg_failure_days_total' => "b.deal_status_group = 'lost' AND b.lost_date IS NOT NULL",
                    'avg_failure_days_new' => "b.deal_status_group = 'lost' AND b.lost_date IS NOT NULL AND b.lead_type = 'new'",
                    'avg_failure_days_old' => "b.deal_status_group = 'lost' AND b.lost_date IS NOT NULL AND b.lead_type = 'old'",
                ];
                $condition = $map[$metricKey] ?? '1=1';
                break;

            case 'failure_reasons':
                $condition = "b.deal_status_group = 'lost'";
                if ($rowFailureReason !== '') {
                    $condition .= " AND b.failure_reason = ?";
                    $conditionParams[] = $rowFailureReason;
                }
                if ($metricKey === 'failed_without_current_count') {
                    $condition .= " AND b.has_open_deal = 0";
                } elseif ($metricKey === 'failed_new_count') {
                    $condition .= " AND b.lead_type = 'new'";
                } elseif ($metricKey === 'failed_old_count') {
                    $condition .= " AND b.lead_type = 'old'";
                }
                break;

            case 'product_status':
                $condition = "b.has_payment = 1";
                if ($metricKey === 'sales_new_count') {
                    $condition .= " AND b.lead_type = 'new'";
                } elseif ($metricKey === 'sales_old_count') {
                    $condition .= " AND b.lead_type = 'old'";
                }
                break;

            case 'product_sold_by_owner_day':
                $condition = "b.has_payment = 1";
                if ($metricKey === 'sold_new_count') {
                    $condition .= " AND b.lead_type = 'new'";
                } elseif ($metricKey === 'sold_old_count') {
                    $condition .= " AND b.lead_type = 'old'";
                }
                break;

            case 'product_requests_by_owner_day':
                $condition = '1=1';
                if ($metricKey === 'requested_new_count') {
                    $condition .= " AND b.lead_type = 'new'";
                } elseif ($metricKey === 'requested_old_count') {
                    $condition .= " AND b.lead_type = 'old'";
                } elseif ($metricKey === 'sold_total_count') {
                    $condition .= " AND b.has_payment = 1";
                } elseif ($metricKey === 'unsold_total_count') {
                    $condition .= " AND b.has_payment = 0";
                }
                break;

            case 'product_failed_by_owner_day':
                $condition = "b.has_payment = 0";
                if ($metricKey === 'failed_new_count') {
                    $condition .= " AND b.lead_type = 'new'";
                } elseif ($metricKey === 'failed_old_count') {
                    $condition .= " AND b.lead_type = 'old'";
                }
                break;

            case 'summary_by_user':
                $map = [
                    'total_deals_count' => '1=1',
                    'sales_total_count' => 'b.has_payment = 1',
                    'conversion_rate_total' => '1=1',
                    'total_new_deals_count' => "b.lead_type = 'new'",
                    'sales_new_count' => "b.lead_type = 'new' AND b.has_payment = 1",
                    'conversion_rate_new' => "b.lead_type = 'new'",
                    'total_collected_amount' => 'b.has_payment = 1',
                    'outstanding_amount_sum' => "b.has_payment = 1 AND b.payable_amount > 0 AND b.total_paid < b.payable_amount",
                ];
                $condition = $map[$metricKey] ?? '1=1';
                break;
        }

        // For payment_status table, use payment date filtering instead of register time
        // This ensures consistency with the summary card calculation
        $usePaymentDateFilter = ($tableId === 'payment_status');
        $useLostDateFilter = (
            $tableId === 'deal_status'
            && $rowDate === ''
            && in_array($metricKey, ['deal_lost_total', 'deal_lost_new', 'deal_lost_old'], true)
        );

        $baseParams = [];
        if ($usePaymentDateFilter) {
            $dealsBaseSql = $this->buildDrilldownDealsByPaymentDateSql($filters, $rowOwnerDidarId, $baseParams);
        } elseif ($useLostDateFilter) {
            $dealsBaseSql = $this->buildDrilldownDealsByLostDateSql($filters, $rowOwnerDidarId, $baseParams);
        } else {
            $dealsBaseSql = $this->buildDrilldownDealsBaseSql($filters, $rowDate, $rowOwnerDidarId, $baseParams, $tableId !== 'summary_by_user');
        }

        $joinProduct = in_array($tableId, ['product_status', 'product_sold_by_owner_day', 'product_requests_by_owner_day', 'product_failed_by_owner_day'], true);
        $productWhere = '';
        $productParams = [];
        $resolvedProductExpr = $this->buildResolvedProductNameExpr('b.deal_product_hint', 'pr');
        $primaryDealProductSql = $this->buildPrimaryDealProductSql();
        if ($joinProduct && $rowProductName !== '') {
            $productWhere = " AND {$resolvedProductExpr} = ?";
            $productParams[] = $rowProductName;
        }

        $matchedDealsSql = "
            SELECT DISTINCT b.*
            FROM ({$dealsBaseSql}) b
            " . ($joinProduct ? "INNER JOIN ({$primaryDealProductSql}) dp ON dp.deal_id = b.deal_id LEFT JOIN products pr ON pr.id = dp.product_id" : "") . "
            WHERE {$condition}
            {$productWhere}
        ";
        $matchedParams = array_merge($baseParams, $conditionParams, $productParams);

        $dealsSql = "
            SELECT
                b.deal_id,
                b.didar_deal_id,
                b.title,
                b.deal_date,
                b.owner_name,
                b.owner_didar_id,
                b.contact_didar_id,
                b.deal_status_group,
                b.payable_amount,
                b.total_paid,
                b.has_payment
            FROM ({$matchedDealsSql}) b
            ORDER BY b.deal_date DESC, b.deal_id DESC
            LIMIT 500
        ";
        $deals = $this->runSelectAll($dealsSql, $matchedParams);

        $leadsSql = "
            SELECT DISTINCT
                p.didar_contact_id as contact_id,
                TRIM(CONCAT(COALESCE(p.first_name, ''), ' ', COALESCE(p.last_name, ''))) as full_name,
                p.mobile_phone,
                p.secondary_mobile_phone,
                DATE(COALESCE(p.register_time, p.created_at)) as lead_date,
                COALESCE(u.display_name, 'بدون مسئول') as owner_name
            FROM persons p
            LEFT JOIN users u ON u.didar_user_id = p.owner_didar_id
            INNER JOIN (
                SELECT DISTINCT contact_didar_id
                FROM ({$matchedDealsSql}) m
                WHERE COALESCE(m.contact_didar_id, '') <> ''
            ) md ON md.contact_didar_id = p.didar_contact_id
            ORDER BY lead_date DESC, full_name ASC
            LIMIT 500
        ";
        $leads = $this->runSelectAll($leadsSql, $matchedParams);

        $dealIds = array_values(array_filter(array_map(static function ($row) {
            return (int)($row['deal_id'] ?? 0);
        }, $deals)));
        $payments = $this->getPaymentsByDealIds($dealIds);

        return [
            'leads' => $leads,
            'deals' => $deals,
            'activities' => [],
            'payments' => $payments,
            'meta' => [
                'total_leads' => count($leads),
                'total_deals' => count($deals),
                'total_activities' => 0,
                'total_payments' => count($payments)
            ]
        ];
    }

    private function getLeadStatusDrilldown(array $filters, string $metricKey, string $rowDate, string $rowOwnerDidarId): array
    {
        $pdo = $this->db->getPdo();
        $paymentsByDealSql = $this->buildUnifiedPaymentsByDealSql();
        $params = [];
        $whereParts = ["DATE(COALESCE(p.register_time, p.created_at)) BETWEEN ? AND ?"];
        $params[] = !empty($filters['date_from']) ? (string)$filters['date_from'] : date('Y-m-01');
        $params[] = !empty($filters['date_to']) ? (string)$filters['date_to'] : date('Y-m-t');

        if ($rowDate !== '') {
            $whereParts[] = "DATE(COALESCE(p.register_time, p.created_at)) = ?";
            $params[] = $rowDate;
        }
        if ($rowOwnerDidarId !== '') {
            $whereParts[] = "p.owner_didar_id = ?";
            $params[] = $rowOwnerDidarId;
        } elseif (!empty($filters['owner_didar_ids_csv'])) {
            $whereParts[] = "FIND_IN_SET(p.owner_didar_id, ?)";
            $params[] = $filters['owner_didar_ids_csv'];
        } elseif (!empty($filters['owner_didar_id'])) {
            $whereParts[] = "p.owner_didar_id = ?";
            $params[] = $filters['owner_didar_id'];
        }

        $condition = '1=1';
        if ($metricKey === 'leads_with_sales') {
            $condition = "x.has_sale_payment = 1";
        } elseif ($metricKey === 'leads_with_new_sales') {
            $condition = "x.has_sale_payment = 1 AND x.first_payment_date = x.lead_date";
        } elseif ($metricKey === 'leads_with_previous_sales') {
            $condition = "x.has_sale_payment = 1 AND x.first_payment_date < x.lead_date";
        } elseif ($metricKey === 'leads_without_sales') {
            $condition = "x.has_sale_payment = 0";
        } elseif ($metricKey === 'leads_without_sales_with_current_deal') {
            $condition = "x.has_sale_payment = 0 AND x.has_open_deal = 1";
        } elseif ($metricKey === 'leads_without_sales_without_current_deal') {
            $condition = "x.has_sale_payment = 0 AND x.has_open_deal = 0";
        }

        $legacyOpenClause = $this->buildLegacyDealFilterClause($filters, 'd2');
        $legacyOpenClause = $legacyOpenClause !== '' ? (' AND ' . $legacyOpenClause) : '';
        $legacyDealsClauseForD4 = $this->buildLegacyDealFilterClause($filters, 'd4');
        $legacyDealsClauseForD4 = $legacyDealsClauseForD4 !== '' ? (' AND ' . $legacyDealsClauseForD4) : '';

        $leadsSql = "
            SELECT
                x.contact_id,
                x.full_name,
                x.mobile_phone,
                x.secondary_mobile_phone,
                x.lead_date,
                x.owner_name
            FROM (
                SELECT
                    p.didar_contact_id as contact_id,
                    TRIM(CONCAT(COALESCE(p.first_name, ''), ' ', COALESCE(p.last_name, ''))) as full_name,
                    p.mobile_phone,
                    p.secondary_mobile_phone,
                    DATE(COALESCE(p.register_time, p.created_at)) as lead_date,
                    COALESCE(u.display_name, 'بدون مسئول') as owner_name,
                    CASE WHEN COALESCE(od.open_count, 0) > 0 THEN 1 ELSE 0 END as has_open_deal,
                    cp.first_payment_date,
                    CASE WHEN cp.first_payment_date IS NOT NULL THEN 1 ELSE 0 END as has_sale_payment
                FROM persons p
                LEFT JOIN users u ON u.didar_user_id = p.owner_didar_id
                LEFT JOIN (
                    SELECT
                        d2.contact_didar_id,
                        COUNT(*) as open_count
                    FROM deals d2
                    WHERE d2.status NOT IN ('Won', 'Lost')
                      {$legacyOpenClause}
                    GROUP BY d2.contact_didar_id
                ) od ON od.contact_didar_id = p.didar_contact_id
                LEFT JOIN (
                    SELECT
                        d4.contact_didar_id,
                        MIN(DATE(pay4.first_payment_date)) as first_payment_date
                    FROM deals d4
                    LEFT JOIN ({$paymentsByDealSql}) pay4 ON pay4.deal_id = d4.id
                    WHERE pay4.first_payment_date IS NOT NULL
                      {$legacyDealsClauseForD4}
                    GROUP BY d4.contact_didar_id
                ) cp ON cp.contact_didar_id = p.didar_contact_id
                WHERE " . implode(' AND ', $whereParts) . "
            ) x
            WHERE {$condition}
            ORDER BY x.lead_date DESC, x.full_name ASC
            LIMIT 500
        ";
        $leads = $this->runSelectAll($leadsSql, $params);

        $leadIds = array_values(array_filter(array_map(static function ($row) {
            return $row['contact_id'] ?? '';
        }, $leads)));

        $deals = [];
        $payments = [];
        if (!empty($leadIds)) {
            $placeholders = implode(',', array_fill(0, count($leadIds), '?'));
            $dealsWhere = $this->buildWhereClauseForDeals($filters, 'd');
            $dealParams = $this->buildParamsForDeals($filters);
            $dealParams = array_merge($dealParams, $leadIds);
            $paymentsByDealSql2 = $this->buildUnifiedPaymentsByDealSql();
            $dealsSql = "
                SELECT
                    d.id as deal_id,
                    d.didar_deal_id,
                    d.title,
                    DATE(COALESCE(d.register_time, d.created_at)) as deal_date,
                    COALESCE(u.display_name, 'بدون مسئول') as owner_name,
                    d.owner_didar_id,
                    d.contact_didar_id,
                    CASE WHEN d.status = 'Won' THEN 'won' WHEN d.status = 'Lost' THEN 'lost' ELSE 'pending' END as deal_status_group,
                    COALESCE(d.payable_amount, 0) as payable_amount,
                    COALESCE(pay.total_paid, 0) as total_paid,
                    CASE WHEN COALESCE(pay.total_paid, 0) > 0 THEN 1 ELSE 0 END as has_payment
                FROM deals d
                LEFT JOIN users u ON u.didar_user_id = d.owner_didar_id
                LEFT JOIN ({$paymentsByDealSql2}) pay ON pay.deal_id = d.id
                WHERE {$dealsWhere}
                  AND d.contact_didar_id IN ({$placeholders})
                ORDER BY DATE(COALESCE(d.register_time, d.created_at)) DESC, d.id DESC
                LIMIT 500
            ";
            $deals = $this->runSelectAll($dealsSql, $dealParams);

            $dealIds = array_values(array_filter(array_map(static function ($row) {
                return (int)($row['deal_id'] ?? 0);
            }, $deals)));
            $payments = $this->getPaymentsByDealIds($dealIds);
        }

        return [
            'leads' => $leads,
            'deals' => $deals,
            'activities' => [],
            'payments' => $payments,
            'meta' => [
                'total_leads' => count($leads),
                'total_deals' => count($deals),
                'total_activities' => 0,
                'total_payments' => count($payments)
            ]
        ];
    }

    private function getActivityStatusDrilldown(
        array $filters,
        string $metricKey,
        string $rowDate,
        string $rowOwnerDidarId,
        string $rowActivityType
    ): array {
        $dateFrom = !empty($filters['date_from']) ? (string)$filters['date_from'] : date('Y-m-01');
        $dateTo = !empty($filters['date_to']) ? (string)$filters['date_to'] : date('Y-m-t');

        $activityWhere = ["DATE(COALESCE(a.register_date, a.created_at)) BETWEEN ? AND ?"];
        $activityParams = [$dateFrom, $dateTo];

        if ($rowDate !== '') {
            $activityWhere[] = "DATE(COALESCE(a.register_date, a.created_at)) = ?";
            $activityParams[] = $rowDate;
        }
        if ($rowOwnerDidarId !== '') {
            $activityWhere[] = "a.owner_didar_id = ?";
            $activityParams[] = $rowOwnerDidarId;
        } elseif (!empty($filters['owner_didar_ids_csv'])) {
            $activityWhere[] = "FIND_IN_SET(a.owner_didar_id, ?)";
            $activityParams[] = $filters['owner_didar_ids_csv'];
        } elseif (!empty($filters['owner_didar_id'])) {
            $activityWhere[] = "a.owner_didar_id = ?";
            $activityParams[] = $filters['owner_didar_id'];
        }

        $activityTypeExpr = $this->buildCanonicalActivityTypeExpr('a');
        $metricCondition = '1=1';
        if ($metricKey === 'activities_new_lead') {
            $metricCondition = "DATE(COALESCE(p.register_time, p.created_at)) = DATE(COALESCE(a.register_date, a.created_at))";
        } elseif ($metricKey === 'activities_old_lead') {
            $metricCondition = "DATE(COALESCE(p.register_time, p.created_at)) < DATE(COALESCE(a.register_date, a.created_at))";
        } elseif ($metricKey === 'no_response_new') {
            $metricCondition = "(
                {$activityTypeExpr} = 'تماس عدم پاسخ'
                AND DATE(COALESCE(p.register_time, p.created_at)) = DATE(COALESCE(a.register_date, a.created_at))
            )";
        } elseif ($metricKey === 'no_response_old') {
            $metricCondition = "(
                {$activityTypeExpr} = 'تماس عدم پاسخ'
                AND DATE(COALESCE(p.register_time, p.created_at)) < DATE(COALESCE(a.register_date, a.created_at))
            )";
        } elseif ($metricKey === 'no_response_total') {
            $metricCondition = "(
                {$activityTypeExpr} = 'تماس عدم پاسخ'
            )";
        } elseif ($metricKey === 'activity_type_new_count') {
            $metricCondition = "DATE(COALESCE(p.register_time, p.created_at)) = DATE(COALESCE(a.register_date, a.created_at))";
            if ($rowActivityType !== '') {
                $metricCondition .= " AND {$activityTypeExpr} = ?";
                $activityParams[] = $rowActivityType;
            }
        } elseif ($metricKey === 'activity_type_old_count') {
            $metricCondition = "DATE(COALESCE(p.register_time, p.created_at)) < DATE(COALESCE(a.register_date, a.created_at))";
            if ($rowActivityType !== '') {
                $metricCondition .= " AND {$activityTypeExpr} = ?";
                $activityParams[] = $rowActivityType;
            }
        } elseif ($metricKey === 'activity_type' && $rowActivityType !== '') {
            $metricCondition = "{$activityTypeExpr} = ?";
            $activityParams[] = $rowActivityType;
        } elseif (in_array($metricKey, ['activity_count_total', 'activity_type_duration_minutes', 'avg_activity_type_duration_minutes', 'activity_share_percent'], true) && $rowActivityType !== '') {
            $metricCondition = "{$activityTypeExpr} = ?";
            $activityParams[] = $rowActivityType;
        }

        $activitiesSql = "
            SELECT
                a.id as activity_id,
                DATE(COALESCE(a.register_date, a.created_at)) as activity_date,
                COALESCE(u.display_name, 'بدون مسئول') as owner_name,
                {$activityTypeExpr} as activity_type,
                COALESCE(a.duration, 0) as activity_duration_minutes,
                a.contact_didar_id,
                TRIM(CONCAT(COALESCE(p.first_name, ''), ' ', COALESCE(p.last_name, ''))) as lead_name,
                p.mobile_phone
            FROM activities a
            LEFT JOIN persons p ON p.didar_contact_id = a.contact_didar_id
            LEFT JOIN users u ON u.didar_user_id = a.owner_didar_id
            WHERE " . implode(' AND ', $activityWhere) . "
              AND {$metricCondition}
            ORDER BY activity_date DESC, a.id DESC
            LIMIT 500
        ";
        $activities = $this->runSelectAll($activitiesSql, $activityParams);

        $contactIds = array_values(array_filter(array_unique(array_map(static function ($row) {
            return $row['contact_didar_id'] ?? '';
        }, $activities))));

        $leads = [];
        $deals = [];
        $payments = [];
        if (!empty($contactIds)) {
            $placeholders = implode(',', array_fill(0, count($contactIds), '?'));

            $leadSql = "
                SELECT
                    p.didar_contact_id as contact_id,
                    TRIM(CONCAT(COALESCE(p.first_name, ''), ' ', COALESCE(p.last_name, ''))) as full_name,
                    p.mobile_phone,
                    p.secondary_mobile_phone,
                    DATE(COALESCE(p.register_time, p.created_at)) as lead_date,
                    COALESCE(u.display_name, 'بدون مسئول') as owner_name
                FROM persons p
                LEFT JOIN users u ON u.didar_user_id = p.owner_didar_id
                WHERE p.didar_contact_id IN ({$placeholders})
                ORDER BY lead_date DESC, full_name ASC
                LIMIT 500
            ";
            $leads = $this->runSelectAll($leadSql, $contactIds);

            $paymentsByDealSql = $this->buildUnifiedPaymentsByDealSql();
            $dealsWhere = $this->buildWhereClauseForDeals($filters, 'd');
            $dealParams = $this->buildParamsForDeals($filters);
            $dealParams = array_merge($dealParams, $contactIds);
            $dealsSql = "
                SELECT
                    d.id as deal_id,
                    d.didar_deal_id,
                    d.title,
                    DATE(COALESCE(d.register_time, d.created_at)) as deal_date,
                    COALESCE(u.display_name, 'بدون مسئول') as owner_name,
                    d.owner_didar_id,
                    d.contact_didar_id,
                    CASE WHEN d.status = 'Won' THEN 'won' WHEN d.status = 'Lost' THEN 'lost' ELSE 'pending' END as deal_status_group,
                    COALESCE(d.payable_amount, 0) as payable_amount,
                    COALESCE(pay.total_paid, 0) as total_paid,
                    CASE WHEN COALESCE(pay.total_paid, 0) > 0 THEN 1 ELSE 0 END as has_payment
                FROM deals d
                LEFT JOIN users u ON u.didar_user_id = d.owner_didar_id
                LEFT JOIN ({$paymentsByDealSql}) pay ON pay.deal_id = d.id
                WHERE {$dealsWhere}
                  AND d.contact_didar_id IN ({$placeholders})
                ORDER BY deal_date DESC, d.id DESC
                LIMIT 500
            ";
            $deals = $this->runSelectAll($dealsSql, $dealParams);

            $dealIds = array_values(array_filter(array_map(static function ($row) {
                return (int)($row['deal_id'] ?? 0);
            }, $deals)));
            $payments = $this->getPaymentsByDealIds($dealIds);
        }

        return [
            'leads' => $leads,
            'deals' => $deals,
            'activities' => $activities,
            'payments' => $payments,
            'meta' => [
                'total_leads' => count($leads),
                'total_deals' => count($deals),
                'total_activities' => count($activities),
                'total_payments' => count($payments)
            ]
        ];
    }

    private function getPaymentsByDealIds(array $dealIds): array
    {
        if (empty($dealIds)) {
            return [];
        }

        $dealIds = array_values(array_unique(array_map('intval', $dealIds)));
        $placeholders = implode(',', array_fill(0, count($dealIds), '?'));
        $txColumns = [];
        $pickTxExpr = function (string $column, string $fallback = "''") use (&$txColumns): string {
            if (!array_key_exists($column, $txColumns)) {
                $txColumns[$column] = $this->db->hasColumn('transactions', $column);
            }
            return $txColumns[$column] ? "t.{$column}" : $fallback;
        };
        $paymentDateExpr = $pickTxExpr('payment_date');
        $amountExpr = $pickTxExpr('amount', '0');
        $hasDealInvoiceCode = $this->db->hasColumn('deals', 'invoice_code');
        $dealInvoiceExpr = $hasDealInvoiceCode ? "COALESCE(d.invoice_code, '')" : "''";

        // 1) Primary source: transactions (detailed fields)
        $txSql = "
            SELECT
                t.id as transaction_id,
                t.deal_id,
                DATE(CASE WHEN LEFT(COALESCE({$paymentDateExpr}, ''), 10) = '0000-00-00' THEN NULL ELSE {$paymentDateExpr} END) as payment_date,
                COALESCE({$amountExpr}, 0) as amount,
                COALESCE({$pickTxExpr('status')}, '') as payment_status,
                COALESCE({$pickTxExpr('method')}, '') as payment_method,
                COALESCE({$pickTxExpr('method')}, '') as payment_type,
                COALESCE({$pickTxExpr('card_last4')}, '') as card_last4,
                COALESCE({$pickTxExpr('destination_bank')}, '') as destination_bank,
                COALESCE({$pickTxExpr('tracking_code_last4')}, '') as tracking_code_last4,
                COALESCE({$pickTxExpr('destination_card_number')}, '') as destination_card_number,
                COALESCE({$pickTxExpr('payment_time')}, '') as payment_time,
                COALESCE({$pickTxExpr('account_owner_name')}, '') as account_owner_name,
                COALESCE({$pickTxExpr('card_reader_device')}, '') as card_reader_device,
                COALESCE({$pickTxExpr('currency')}, '') as currency,
                COALESCE({$pickTxExpr('currency_other')}, '') as currency_other,
                COALESCE({$pickTxExpr('currency_amount', '0')}, 0) as currency_amount,
                COALESCE({$pickTxExpr('check_bank_name')}, '') as check_bank_name,
                COALESCE({$pickTxExpr('check_date')}, '') as check_date,
                COALESCE({$pickTxExpr('check_number')}, '') as check_number,
                COALESCE({$pickTxExpr('check_owner_name')}, '') as check_owner_name,
                COALESCE({$pickTxExpr('description')}, '') as description,
                COALESCE({$pickTxExpr('created_at')}, '') as created_at,
                COALESCE({$pickTxExpr('invoice_code', $dealInvoiceExpr)}, {$dealInvoiceExpr}) as invoice_code,
                d.didar_deal_id,
                d.title as deal_title,
                COALESCE(p.customer_code, '') as customer_code,
                COALESCE(d.owner_didar_id, '') as owner_didar_id,
                COALESCE(u.display_name, 'بدون مسئول') as owner_name
            FROM transactions t
            LEFT JOIN deals d ON d.id = t.deal_id
            LEFT JOIN persons p ON p.didar_contact_id = d.contact_didar_id
            LEFT JOIN users u ON u.didar_user_id = d.owner_didar_id
            WHERE t.deal_id IN ({$placeholders})
              AND COALESCE({$amountExpr}, 0) > 0
            ORDER BY payment_date DESC, t.id DESC
            LIMIT 2000
        ";
        $payments = $this->runSelectAll($txSql, $dealIds);

        $hasTxByDeal = [];
        foreach ($payments as &$row) {
            $dealId = (int)($row['deal_id'] ?? 0);
            if ($dealId > 0) {
                $hasTxByDeal[$dealId] = true;
            }
            $row['payment_source'] = 'transactions';
        }
        unset($row);

        // 2) Fallback source: deals.payments JSON for deals without transaction
        $missingDealIds = array_values(array_filter($dealIds, static function ($id) use ($hasTxByDeal) {
            return !isset($hasTxByDeal[(int)$id]);
        }));
        if (!empty($missingDealIds)) {
            $jsonPlaceholders = implode(',', array_fill(0, count($missingDealIds), '?'));
            $dealsSql = "
                SELECT
                    d.id as deal_id,
                    d.didar_deal_id,
                    d.title as deal_title,
                    COALESCE(d.owner_didar_id, '') as owner_didar_id,
                    COALESCE(u.display_name, 'بدون مسئول') as owner_name,
                    {$dealInvoiceExpr} as invoice_code,
                    d.payments as payments_json
                FROM deals d
                LEFT JOIN users u ON u.didar_user_id = d.owner_didar_id
                WHERE d.id IN ({$jsonPlaceholders})
            ";
            $dealRows = $this->runSelectAll($dealsSql, $missingDealIds);
            foreach ($dealRows as $dealRow) {
                $decoded = json_decode((string)($dealRow['payments_json'] ?? ''), true);
                if (!is_array($decoded)) {
                    continue;
                }
                foreach ($decoded as $item) {
                    if (!is_array($item)) {
                        continue;
                    }
                    $amount = (float)($item['amount'] ?? 0);
                    if ($amount <= 0) {
                        continue;
                    }
                    $paymentDate = $this->uiOracleNormalizeDate((string)($item['date'] ?? ''));
                    if ($paymentDate === '') {
                        $paymentDate = $this->uiOracleNormalizeDate((string)($item['payment_date'] ?? ''));
                    }
                    if ($paymentDate === '') {
                        $paymentDate = $this->uiOracleJalaliToGregorianString((string)($item['date_persian'] ?? ''));
                    }
                    if ($paymentDate === '') {
                        continue;
                    }
                    $payments[] = [
                        'transaction_id' => '',
                        'deal_id' => (int)($dealRow['deal_id'] ?? 0),
                        'payment_date' => $paymentDate,
                        'amount' => $amount,
                        'payment_status' => (string)($item['status'] ?? ''),
                        'payment_method' => (string)(($item['method'] ?? '') ?: ($item['payment_method'] ?? '')),
                        'payment_type' => (string)(($item['method'] ?? '') ?: ($item['payment_method'] ?? '')),
                        'card_last4' => (string)(($item['card_last4'] ?? '') ?: ($item['card_last_4'] ?? '')),
                        'destination_bank' => (string)(($item['destination_bank'] ?? '') ?: ($item['bank_destination'] ?? '')),
                        'tracking_code_last4' => (string)(($item['tracking_code_last4'] ?? '') ?: ($item['tracking_code'] ?? '')),
                        'destination_card_number' => (string)($item['destination_card_number'] ?? ''),
                        'payment_time' => (string)($item['payment_time'] ?? ''),
                        'account_owner_name' => (string)($item['account_owner_name'] ?? ''),
                        'card_reader_device' => (string)($item['card_reader_device'] ?? ''),
                        'currency' => (string)($item['payment_currency'] ?? $item['currency'] ?? ''),
                        'currency_other' => (string)($item['payment_currency_other'] ?? $item['currency_other'] ?? ''),
                        'currency_amount' => (float)($item['payment_currency_amount'] ?? $item['currency_amount'] ?? 0),
                        'check_bank_name' => (string)($item['check_bank_name'] ?? ''),
                        'check_date' => (string)($item['check_date'] ?? ''),
                        'check_number' => (string)($item['check_number'] ?? ''),
                        'check_owner_name' => (string)($item['check_owner_name'] ?? ''),
                        'description' => (string)($item['description'] ?? ''),
                        'created_at' => '',
                        'invoice_code' => (string)($dealRow['invoice_code'] ?? ''),
                        'didar_deal_id' => (string)($dealRow['didar_deal_id'] ?? ''),
                        'deal_title' => (string)($dealRow['deal_title'] ?? ''),
                        'customer_code' => (string)($dealRow['customer_code'] ?? ''),
                        'owner_didar_id' => (string)($dealRow['owner_didar_id'] ?? ''),
                        'owner_name' => (string)($dealRow['owner_name'] ?? 'بدون مسئول'),
                        'payment_source' => 'deals_json'
                    ];
                }
            }
        }

        usort($payments, static function ($a, $b) {
            $dateCmp = strcmp((string)($b['payment_date'] ?? ''), (string)($a['payment_date'] ?? ''));
            if ($dateCmp !== 0) {
                return $dateCmp;
            }
            return ((int)($b['deal_id'] ?? 0)) <=> ((int)($a['deal_id'] ?? 0));
        });

        return array_slice($payments, 0, 2000);
    }

    private function buildDrilldownDealsBaseSql(
        array $filters,
        string $rowDate,
        string $rowOwnerDidarId,
        array &$paramsOut,
        bool $applyRowDate = true
    ): string {
        $whereDeals = $this->buildWhereClauseForDeals($filters, 'd');
        $paramsDeals = $this->buildParamsForDeals($filters);

        if ($applyRowDate && $rowDate !== '') {
            $whereDeals .= " AND DATE(COALESCE(d.register_time, d.created_at)) = ?";
            $paramsDeals[] = $rowDate;
        }
        if ($rowOwnerDidarId !== '') {
            $whereDeals .= " AND d.owner_didar_id = ?";
            $paramsDeals[] = $rowOwnerDidarId;
        }

        $paymentsByDealSql = $this->buildUnifiedPaymentsByDealSql();
        $legacyOpenClause = $this->buildLegacyDealFilterClause($filters, 'd2');
        $legacyOpenClause = $legacyOpenClause !== '' ? (' AND ' . $legacyOpenClause) : '';

        $paramsOut = $paramsDeals;

        $failureReasonExpr = $this->buildFailureReasonExpr('d');

        return "
            SELECT
                d.id as deal_id,
                d.didar_deal_id,
                d.title,
                DATE(COALESCE(d.register_time, d.created_at)) as deal_date,
                COALESCE(d.owner_didar_id, '') as owner_didar_id,
                COALESCE(u.display_name, 'بدون مسئول') as owner_name,
                COALESCE(d.contact_didar_id, '') as contact_didar_id,
                {$this->buildDealProductHintExpr('d')} as deal_product_hint,
                CASE
                    WHEN DATE(COALESCE(p.register_time, p.created_at)) = DATE(COALESCE(d.register_time, d.created_at)) THEN 'new'
                    ELSE 'old'
                END as lead_type,
                CASE
                    WHEN d.status = 'Won' THEN 'won'
                    WHEN d.status = 'Lost' THEN 'lost'
                    ELSE 'pending'
                END as deal_status_group,
                COALESCE(d.payable_amount, 0) as payable_amount,
                COALESCE(pay.total_paid, 0) as total_paid,
                pay.first_payment_date,
                DATE(COALESCE(d.lost_time, d.created_at)) as lost_date,
                CASE WHEN COALESCE(pay.total_paid, 0) > 0 THEN 1 ELSE 0 END as has_payment,
                CASE WHEN COALESCE(od.open_count, 0) > 0 THEN 1 ELSE 0 END as has_open_deal,
                {$failureReasonExpr} as failure_reason
            FROM deals d
            LEFT JOIN users u ON u.didar_user_id = d.owner_didar_id
            LEFT JOIN persons p ON p.didar_contact_id = d.contact_didar_id
            LEFT JOIN ({$paymentsByDealSql}) pay ON pay.deal_id = d.id
            LEFT JOIN (
                SELECT
                    d2.contact_didar_id,
                    COUNT(*) as open_count
                FROM deals d2
                WHERE d2.status NOT IN ('Won', 'Lost')
                  {$legacyOpenClause}
                GROUP BY d2.contact_didar_id
            ) od ON od.contact_didar_id = d.contact_didar_id
            WHERE {$whereDeals}
        ";
    }

    private function getWithoutPaymentCardDrilldown(array $filters, string $rowOwnerDidarId): array
    {
        if ($this->getKpiPaymentDateBasis($filters, 'sales') === 'ui_payment_date') {
            if ($rowOwnerDidarId !== '') {
                $filters['owner_didar_id'] = $rowOwnerDidarId;
                $filters['owner_didar_ids_csv'] = $rowOwnerDidarId;
                $filters['owner_didar_ids'] = [$rowOwnerDidarId];
            }
            $oracle = $this->getUiOracleKpiData($filters);
            $ids = array_map('intval', array_keys($oracle['sets']['partition_without_payment_valid'] ?? []));
            return $this->buildSimpleDealLeadDrilldownByIds($ids, $filters);
        }
        $pdo = $this->db->getPdo();
        $whereDeals = $this->buildWhereClauseForDeals($filters, 'd');
        $paramsDeals = $this->buildParamsForDeals($filters);
        if ($rowOwnerDidarId !== '') {
            $whereDeals .= " AND d.owner_didar_id = ?";
            $paramsDeals[] = $rowOwnerDidarId;
        }

        $paymentsByDealSql = $this->buildAbsolutePaymentPresenceByDealSql();
        $legacyOpenClause = $this->buildLegacyDealFilterClause($filters, 'd2');
        $legacyOpenClause = $legacyOpenClause !== '' ? (' AND ' . $legacyOpenClause) : '';
        $failureReasonExpr = $this->buildFailureReasonExpr('d');

        $dealsBaseSql = "
            SELECT
                d.id as deal_id,
                d.didar_deal_id,
                d.title,
                DATE(COALESCE(d.register_time, d.created_at)) as deal_date,
                COALESCE(d.owner_didar_id, '') as owner_didar_id,
                COALESCE(u.display_name, 'بدون مسئول') as owner_name,
                COALESCE(d.contact_didar_id, '') as contact_didar_id,
                {$this->buildDealProductHintExpr('d')} as deal_product_hint,
                CASE
                    WHEN DATE(COALESCE(p.register_time, p.created_at)) = DATE(COALESCE(d.register_time, d.created_at)) THEN 'new'
                    ELSE 'old'
                END as lead_type,
                CASE
                    WHEN d.status = 'Won' THEN 'won'
                    WHEN d.status = 'Lost' THEN 'lost'
                    ELSE 'pending'
                END as deal_status_group,
                COALESCE(d.payable_amount, 0) as payable_amount,
                COALESCE(pay.total_paid, 0) as total_paid,
                pay.first_payment_date,
                DATE(COALESCE(d.lost_time, d.created_at)) as lost_date,
                CASE WHEN COALESCE(pay.total_paid, 0) > 0 THEN 1 ELSE 0 END as has_payment,
                CASE WHEN COALESCE(od.open_count, 0) > 0 THEN 1 ELSE 0 END as has_open_deal,
                {$failureReasonExpr} as failure_reason
            FROM deals d
            LEFT JOIN users u ON u.didar_user_id = d.owner_didar_id
            LEFT JOIN persons p ON p.didar_contact_id = d.contact_didar_id
            LEFT JOIN ({$paymentsByDealSql}) pay ON pay.deal_id = d.id
            LEFT JOIN (
                SELECT
                    d2.contact_didar_id,
                    COUNT(*) as open_count
                FROM deals d2
                WHERE d2.status NOT IN ('Won', 'Lost')
                  {$legacyOpenClause}
                GROUP BY d2.contact_didar_id
            ) od ON od.contact_didar_id = d.contact_didar_id
            WHERE {$whereDeals}
        ";

        $matchedDealsSql = "
            SELECT DISTINCT b.*
            FROM ({$dealsBaseSql}) b
            WHERE b.deal_status_group = 'pending'
              AND (b.total_paid IS NULL OR b.total_paid = 0)
        ";

        $dealsSql = "
            SELECT
                b.deal_id,
                b.didar_deal_id,
                b.title,
                b.deal_date,
                b.owner_name,
                b.owner_didar_id,
                b.contact_didar_id,
                b.deal_status_group,
                b.payable_amount,
                b.total_paid,
                b.has_payment
            FROM ({$matchedDealsSql}) b
            ORDER BY b.deal_date DESC, b.deal_id DESC
            LIMIT 500
        ";
        $deals = $this->runSelectAll($dealsSql, $paramsDeals);

        $leadsSql = "
            SELECT DISTINCT
                p.didar_contact_id as contact_id,
                TRIM(CONCAT(COALESCE(p.first_name, ''), ' ', COALESCE(p.last_name, ''))) as full_name,
                p.mobile_phone,
                p.secondary_mobile_phone,
                DATE(COALESCE(p.register_time, p.created_at)) as lead_date,
                COALESCE(u.display_name, 'بدون مسئول') as owner_name
            FROM persons p
            LEFT JOIN users u ON u.didar_user_id = p.owner_didar_id
            INNER JOIN (
                SELECT DISTINCT contact_didar_id
                FROM ({$matchedDealsSql}) m
                WHERE COALESCE(m.contact_didar_id, '') <> ''
            ) md ON md.contact_didar_id = p.didar_contact_id
            ORDER BY lead_date DESC, full_name ASC
            LIMIT 500
        ";
        $leads = $this->runSelectAll($leadsSql, $paramsDeals);

        return [
            'leads' => $leads,
            'deals' => $deals,
            'activities' => [],
            'payments' => [],
            'meta' => [
                'total_leads' => count($leads),
                'total_deals' => count($deals),
                'total_activities' => 0,
                'total_payments' => 0
            ]
        ];
    }

    /**
     * Build drilldown SQL filtered by PAYMENT DATE (not register time)
     * Used for payment_status table to ensure consistency with summary card
     */
    private function buildDrilldownDealsByPaymentDateSql(
        array $filters,
        string $rowOwnerDidarId,
        array &$paramsOut
    ): string {
        $paymentsByDealSql = $this->buildKpiAllTimePaymentTotalsByDealSql($filters, 'payment');
        $paymentsByDealDateSql = $this->buildKpiPaymentsWindowByDealDateSql($filters, 'payment');
        $legacyOpenClause = $this->buildLegacyDealFilterClause($filters, 'd2');
        $legacyOpenClause = $legacyOpenClause !== '' ? (' AND ' . $legacyOpenClause) : '';

        $parts = [];
        $params = [];

        $legacyClause = $this->buildLegacyDealFilterClause($filters, 'd');
        if ($legacyClause !== '') {
            $parts[] = $legacyClause;
        }

        // Date filter is applied in the INNER JOIN subquery, not here

        if (!empty($filters['owner_didar_ids_csv'])) {
            $parts[] = "FIND_IN_SET(d.owner_didar_id, ?)";
            $params[] = $filters['owner_didar_ids_csv'];
        } elseif (!empty($filters['owner_didar_id']) && trim((string)$filters['owner_didar_id']) !== '') {
            $parts[] = "d.owner_didar_id = ?";
            $params[] = trim((string)$filters['owner_didar_id']);
        }

        if (!empty($filters['product_ids_csv'])) {
            $parts[] = "EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = d.id AND FIND_IN_SET(dp.product_id, ?))";
            $params[] = $filters['product_ids_csv'];
        } elseif (!empty($filters['product_id'])) {
            $parts[] = "EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = d.id AND dp.product_id = ?)";
            $params[] = $filters['product_id'];
        }

        $statusCsv = $this->normalizeCsvList($filters['deal_statuses_csv'] ?? '');
        if ($statusCsv !== '' && !$this->csvHasAll($statusCsv, ['pending', 'won', 'lost'])) {
            $parts[] = "(
                (FIND_IN_SET('pending', ?) > 0 AND d.status NOT IN ('Won', 'Lost'))
                OR (FIND_IN_SET('won', ?) > 0 AND d.status = 'Won')
                OR (FIND_IN_SET('lost', ?) > 0 AND d.status = 'Lost')
            )";
            $params[] = $statusCsv;
            $params[] = $statusCsv;
            $params[] = $statusCsv;
        }

        $leadTypeCsv = $this->normalizeCsvList($filters['deal_lead_types_csv'] ?? '');
        if ($leadTypeCsv !== '' && !$this->csvHasAll($leadTypeCsv, ['new', 'old'])) {
            $parts[] = "(
                (FIND_IN_SET('new', ?) > 0 AND EXISTS (
                    SELECT 1 FROM persons pflt
                    WHERE pflt.didar_contact_id = d.contact_didar_id
                      AND DATE(COALESCE(pflt.register_time, pflt.created_at)) = DATE(COALESCE(d.register_time, d.created_at))
                ))
                OR (FIND_IN_SET('old', ?) > 0 AND EXISTS (
                    SELECT 1 FROM persons pflt
                    WHERE pflt.didar_contact_id = d.contact_didar_id
                      AND DATE(COALESCE(pflt.register_time, pflt.created_at)) < DATE(COALESCE(d.register_time, d.created_at))
                ))
            )";
            $params[] = $leadTypeCsv;
            $params[] = $leadTypeCsv;
        }

        if ($rowOwnerDidarId !== '') {
            $parts[] = "d.owner_didar_id = ?";
            $params[] = $rowOwnerDidarId;
        }

        $whereDeals = !empty($parts) ? implode(' AND ', $parts) : '1=1';
        $failureReasonExpr = $this->buildFailureReasonExpr('d');

        // Params for INNER JOIN subquery (date_from, date_to) come FIRST
        // Then params for outer WHERE clause
        $dateFrom = !empty($filters['date_from']) ? (string)$filters['date_from'] : date('Y-m-01');
        $dateTo = !empty($filters['date_to']) ? (string)$filters['date_to'] : date('Y-m-t');
        $paramsOut = array_merge([$dateFrom, $dateTo], $params);

        return "
            SELECT
                d.id as deal_id,
                d.didar_deal_id,
                d.title,
                DATE(COALESCE(d.register_time, d.created_at)) as deal_date,
                COALESCE(d.owner_didar_id, '') as owner_didar_id,
                COALESCE(u.display_name, 'بدون مسئول') as owner_name,
                COALESCE(d.contact_didar_id, '') as contact_didar_id,
                {$this->buildDealProductHintExpr('d')} as deal_product_hint,
                CASE
                    WHEN DATE(COALESCE(p.register_time, p.created_at)) = DATE(COALESCE(d.register_time, d.created_at)) THEN 'new'
                    ELSE 'old'
                END as lead_type,
                CASE
                    WHEN d.status = 'Won' THEN 'won'
                    WHEN d.status = 'Lost' THEN 'lost'
                    ELSE 'pending'
                END as deal_status_group,
                COALESCE(d.payable_amount, 0) as payable_amount,
                COALESCE(pay.total_paid, 0) as total_paid,
                pay.first_payment_date,
                DATE(COALESCE(d.lost_time, d.created_at)) as lost_date,
                CASE WHEN COALESCE(pay.total_paid, 0) > 0 THEN 1 ELSE 0 END as has_payment,
                CASE WHEN COALESCE(od.open_count, 0) > 0 THEN 1 ELSE 0 END as has_open_deal,
                {$failureReasonExpr} as failure_reason
            FROM deals d
            INNER JOIN (
                SELECT DISTINCT pbd.deal_id
                FROM ({$paymentsByDealDateSql}) pbd
                WHERE pbd.payment_date >= ?
                  AND pbd.payment_date <= ?
            ) pw ON pw.deal_id = d.id
            LEFT JOIN users u ON u.didar_user_id = d.owner_didar_id
            LEFT JOIN persons p ON p.didar_contact_id = d.contact_didar_id
            LEFT JOIN ({$paymentsByDealSql}) pay ON pay.deal_id = d.id
            LEFT JOIN (
                SELECT
                    d2.contact_didar_id,
                    COUNT(*) as open_count
                FROM deals d2
                WHERE d2.status NOT IN ('Won', 'Lost')
                  {$legacyOpenClause}
                GROUP BY d2.contact_didar_id
            ) od ON od.contact_didar_id = d.contact_didar_id
            WHERE {$whereDeals}
        ";
    }

    private function buildDrilldownDealsByLostDateSql(
        array $filters,
        string $rowOwnerDidarId,
        array &$paramsOut
    ): string {
        $whereDeals = $this->buildWhereClauseForLostSales($filters);
        $paramsDeals = $this->buildParamsForLostSales($filters);

        if ($rowOwnerDidarId !== '') {
            $whereDeals .= " AND d.owner_didar_id = ?";
            $paramsDeals[] = $rowOwnerDidarId;
        }

        $paymentsByDealSql = $this->buildUnifiedPaymentsByDealSql();
        $legacyOpenClause = $this->buildLegacyDealFilterClause($filters, 'd2');
        $legacyOpenClause = $legacyOpenClause !== '' ? (' AND ' . $legacyOpenClause) : '';
        $paramsOut = $paramsDeals;
        $failureReasonExpr = $this->buildFailureReasonExpr('d');

        return "
            SELECT
                d.id as deal_id,
                d.didar_deal_id,
                d.title,
                DATE(COALESCE(d.register_time, d.created_at)) as deal_date,
                COALESCE(d.owner_didar_id, '') as owner_didar_id,
                COALESCE(u.display_name, 'بدون مسئول') as owner_name,
                COALESCE(d.contact_didar_id, '') as contact_didar_id,
                {$this->buildDealProductHintExpr('d')} as deal_product_hint,
                CASE
                    WHEN DATE(COALESCE(p.register_time, p.created_at)) = DATE(COALESCE(d.register_time, d.created_at)) THEN 'new'
                    ELSE 'old'
                END as lead_type,
                CASE
                    WHEN d.status = 'Won' THEN 'won'
                    WHEN d.status = 'Lost' THEN 'lost'
                    ELSE 'pending'
                END as deal_status_group,
                COALESCE(d.payable_amount, 0) as payable_amount,
                COALESCE(pay.total_paid, 0) as total_paid,
                pay.first_payment_date,
                DATE(COALESCE(d.lost_time, d.created_at)) as lost_date,
                CASE WHEN COALESCE(pay.total_paid, 0) > 0 THEN 1 ELSE 0 END as has_payment,
                CASE WHEN COALESCE(od.open_count, 0) > 0 THEN 1 ELSE 0 END as has_open_deal,
                {$failureReasonExpr} as failure_reason
            FROM deals d
            LEFT JOIN users u ON u.didar_user_id = d.owner_didar_id
            LEFT JOIN persons p ON p.didar_contact_id = d.contact_didar_id
            LEFT JOIN ({$paymentsByDealSql}) pay ON pay.deal_id = d.id
            LEFT JOIN (
                SELECT
                    d2.contact_didar_id,
                    COUNT(*) as open_count
                FROM deals d2
                WHERE d2.status NOT IN ('Won', 'Lost')
                  {$legacyOpenClause}
                GROUP BY d2.contact_didar_id
            ) od ON od.contact_didar_id = d.contact_didar_id
            WHERE d.status = 'Lost'
              AND {$whereDeals}
        ";
    }

    private function buildFailureReasonExpr(string $alias = 'd'): string
    {
        $parts = [];
        if ($this->db->hasColumn('deals', 'failure_reason_title')) {
            $parts[] = "NULLIF(TRIM({$alias}.failure_reason_title), '')";
        }
        if ($this->db->hasColumn('deals', 'failure_reason_text')) {
            $parts[] = "NULLIF(TRIM({$alias}.failure_reason_text), '')";
        }
        if ($this->db->hasColumn('deals', 'failure_reason_description')) {
            $parts[] = "NULLIF(TRIM({$alias}.failure_reason_description), '')";
        }
        if ($this->db->hasColumn('deals', 'failure_reason_code')) {
            $parts[] = "CASE TRIM({$alias}.failure_reason_code)
                WHEN 'no_financial_ability' THEN 'عدم توان مالی'
                WHEN 'no_purchase_intent' THEN 'فعلا قصد خرید ندارد'
                WHEN 'competitor_purchase' THEN 'خرید از رقبا'
                WHEN 'wrong_number' THEN 'شماره اشتباه'
                WHEN 'irrelevant_lead' THEN 'لید نامرتبط'
                WHEN 'other' THEN 'سایر'
                ELSE NULL
            END";
            $parts[] = "NULLIF(TRIM({$alias}.failure_reason_code), '')";
        }
        $parts[] = "'نامشخص'";
        return 'COALESCE(' . implode(', ', $parts) . ')';
    }

    private function buildNormalizedProductNameExpr(string $alias = 'p'): string
    {
        $baseExpr = $this->db->hasColumn('products', 'title')
            ? "COALESCE(NULLIF(TRIM({$alias}.title), ''), NULLIF(TRIM({$alias}.name), ''), '')"
            : "COALESCE(NULLIF(TRIM({$alias}.name), ''), '')";

        return "CASE
            WHEN TRIM({$baseExpr}) IN ('VIP', 'vip', 'Vip', 'کانال VIP', 'کانال vip', 'کانالvip', 'کانال  VIP') THEN 'VIP'
            WHEN TRIM({$baseExpr}) = '' THEN 'نامشخص'
            ELSE TRIM({$baseExpr})
        END";
    }

    /**
     * استخراج نشانه محصول از خود معامله:
     * 1) بخش بعد از | در عنوان معامله
     * 2) اولین مقدار requested_services
     */
    private function buildDealProductHintExpr(string $dealAlias = 'd'): string
    {
        $titlePart = "CASE
            WHEN COALESCE({$dealAlias}.title, '') <> '' AND LOCATE('|', {$dealAlias}.title) > 0
            THEN NULLIF(TRIM(SUBSTRING_INDEX({$dealAlias}.title, '|', -1)), '')
            ELSE NULL
        END";

        $requestedFirstRaw = "NULLIF(
            TRIM(BOTH '\"' FROM TRIM(BOTH ']' FROM TRIM(BOTH '[' FROM SUBSTRING_INDEX(COALESCE({$dealAlias}.requested_services, ''), ',', 1)))),
            ''
        )";

        $requestedPart = "CASE
            WHEN {$requestedFirstRaw} IS NOT NULL
                 AND {$requestedFirstRaw} NOT LIKE 'معامله %'
            THEN {$requestedFirstRaw}
            ELSE NULL
        END";

        return "COALESCE({$titlePart}, {$requestedPart})";
    }

    /**
     * نام محصول نهایی برای KPI:
     * اول hint از خود معامله، و اگر نبود fallback از products.
     */
    private function buildResolvedProductNameExpr(string $hintExpr, string $fallbackAlias = 'p'): string
    {
        $fallbackExpr = $this->buildNormalizedProductNameExpr($fallbackAlias);
        $hintTrim = "TRIM(COALESCE({$hintExpr}, ''))";

        return "CASE
            WHEN {$hintTrim} IN ('VIP', 'vip', 'Vip', 'کانال VIP', 'کانال vip', 'کانالvip', 'کانال  VIP') THEN 'VIP'
            WHEN {$hintTrim} <> '' THEN {$hintTrim}
            ELSE {$fallbackExpr}
        END";
    }

    /**
     * انتخاب محصول اصلی هر معامله:
     * آخرین سطر ثبت‌شده در deal_products (created_at جدیدتر، سپس id بزرگ‌تر)
     */
    private function buildPrimaryDealProductSql(): string
    {
        return "
            SELECT dp1.deal_id, dp1.product_id
            FROM deal_products dp1
            INNER JOIN (
                SELECT deal_id, MAX(id) as max_id
                FROM deal_products
                GROUP BY deal_id
            ) latest ON latest.deal_id = dp1.deal_id AND latest.max_id = dp1.id
        ";
    }

    /**
     * ساخت where clause بر اساس فیلترها
     */
    private function buildWhereClause(array $filters): string
    {
        return $this->buildWhereClauseForDeals($filters, 'd');
    }

    /**
     * ساخت where clause برای فیلترهای مبتنی بر معامله
     */
    private function buildWhereClauseForDeals(array $filters, string $alias = 'd'): string
    {
        $parts = [];
        $statusCsv = $this->normalizeCsvList($filters['deal_statuses_csv'] ?? '');
        $leadTypeCsv = $this->normalizeCsvList($filters['deal_lead_types_csv'] ?? '');

        $legacyClause = $this->buildLegacyDealFilterClause($filters, $alias);
        if ($legacyClause !== '') {
            $parts[] = $legacyClause;
        }

        if (!empty($filters['date_from'])) {
            $parts[] = "DATE(COALESCE({$alias}.register_time, {$alias}.created_at)) >= ?";
        }
        if (!empty($filters['date_to'])) {
            $parts[] = "DATE(COALESCE({$alias}.register_time, {$alias}.created_at)) <= ?";
        }

        if (!empty($filters['owner_didar_ids_csv'])) {
            $parts[] = "FIND_IN_SET({$alias}.owner_didar_id, ?)";
        } elseif (!empty($filters['owner_didar_id']) && trim($filters['owner_didar_id']) !== '') {
            $parts[] = "{$alias}.owner_didar_id = ?";
        }

        if (!empty($filters['product_ids_csv'])) {
            $parts[] = "EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = {$alias}.id AND FIND_IN_SET(dp.product_id, ?))";
        } elseif (!empty($filters['product_id'])) {
            $parts[] = "EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = {$alias}.id AND dp.product_id = ?)";
        }

        if ($statusCsv !== '' && !$this->csvHasAll($statusCsv, ['pending', 'won', 'lost'])) {
            $parts[] = "(
                (FIND_IN_SET('pending', ?) > 0 AND {$alias}.status NOT IN ('Won', 'Lost'))
                OR (FIND_IN_SET('won', ?) > 0 AND {$alias}.status = 'Won')
                OR (FIND_IN_SET('lost', ?) > 0 AND {$alias}.status = 'Lost')
            )";
        }

        if ($leadTypeCsv !== '' && !$this->csvHasAll($leadTypeCsv, ['new', 'old'])) {
            $parts[] = "(
                (FIND_IN_SET('new', ?) > 0 AND EXISTS (
                    SELECT 1 FROM persons pflt
                    WHERE pflt.didar_contact_id = {$alias}.contact_didar_id
                      AND DATE(COALESCE(pflt.register_time, pflt.created_at)) = DATE(COALESCE({$alias}.register_time, {$alias}.created_at))
                ))
                OR (FIND_IN_SET('old', ?) > 0 AND EXISTS (
                    SELECT 1 FROM persons pflt
                    WHERE pflt.didar_contact_id = {$alias}.contact_didar_id
                      AND DATE(COALESCE(pflt.register_time, pflt.created_at)) < DATE(COALESCE({$alias}.register_time, {$alias}.created_at))
                ))
            )";
        }

        return !empty($parts) ? implode(' AND ', $parts) : '1=1';
    }

    /**
     * همان where معاملات، بدون محدودیت تاریخ ثبت معامله
     * (برای سناریوهایی که تاریخ باید از پرداخت گرفته شود)
     */
    private function buildWhereClauseForDealsNoDate(array $filters, string $alias = 'd'): string
    {
        $parts = [];
        $statusCsv = $this->normalizeCsvList($filters['deal_statuses_csv'] ?? '');
        $leadTypeCsv = $this->normalizeCsvList($filters['deal_lead_types_csv'] ?? '');

        $legacyClause = $this->buildLegacyDealFilterClause($filters, $alias);
        if ($legacyClause !== '') {
            $parts[] = $legacyClause;
        }

        if (!empty($filters['owner_didar_ids_csv'])) {
            $parts[] = "FIND_IN_SET({$alias}.owner_didar_id, ?)";
        } elseif (!empty($filters['owner_didar_id']) && trim($filters['owner_didar_id']) !== '') {
            $parts[] = "{$alias}.owner_didar_id = ?";
        }

        if (!empty($filters['product_ids_csv'])) {
            $parts[] = "EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = {$alias}.id AND FIND_IN_SET(dp.product_id, ?))";
        } elseif (!empty($filters['product_id'])) {
            $parts[] = "EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = {$alias}.id AND dp.product_id = ?)";
        }

        if ($statusCsv !== '' && !$this->csvHasAll($statusCsv, ['pending', 'won', 'lost'])) {
            $parts[] = "(
                (FIND_IN_SET('pending', ?) > 0 AND {$alias}.status NOT IN ('Won', 'Lost'))
                OR (FIND_IN_SET('won', ?) > 0 AND {$alias}.status = 'Won')
                OR (FIND_IN_SET('lost', ?) > 0 AND {$alias}.status = 'Lost')
            )";
        }

        if ($leadTypeCsv !== '' && !$this->csvHasAll($leadTypeCsv, ['new', 'old'])) {
            $parts[] = "(
                (FIND_IN_SET('new', ?) > 0 AND EXISTS (
                    SELECT 1 FROM persons pflt
                    WHERE pflt.didar_contact_id = {$alias}.contact_didar_id
                      AND DATE(COALESCE(pflt.register_time, pflt.created_at)) = DATE(COALESCE({$alias}.register_time, {$alias}.created_at))
                ))
                OR (FIND_IN_SET('old', ?) > 0 AND EXISTS (
                    SELECT 1 FROM persons pflt
                    WHERE pflt.didar_contact_id = {$alias}.contact_didar_id
                      AND DATE(COALESCE(pflt.register_time, pflt.created_at)) < DATE(COALESCE({$alias}.register_time, {$alias}.created_at))
                ))
            )";
        }

        return !empty($parts) ? implode(' AND ', $parts) : '1=1';
    }

    /**
     * ساخت where clause برای Sales (بدون محدودیت status معامله)
     */
    private function buildWhereClauseForSales(array $filters): string
    {
        return $this->buildWhereClauseForDeals($filters, 'd');
    }

    private function getLegacyDealScope(array $filters): string
    {
        $scope = strtolower(trim((string)($filters['legacy_deal_scope'] ?? 'exclude')));
        return in_array($scope, ['exclude', 'only', 'all'], true) ? $scope : 'exclude';
    }

    private function buildLegacyDealFilterClause(array $filters, string $alias = 'd'): string
    {
        if (!$this->db->hasColumn('deals', 'is_legacy_didar')) {
            return '';
        }

        $scope = $this->getLegacyDealScope($filters);
        if ($scope === 'all') {
            return '';
        }

        return $scope === 'only'
            ? "COALESCE({$alias}.is_legacy_didar, 0) = 1"
            : "COALESCE({$alias}.is_legacy_didar, 0) = 0";
    }

    /**
     * ساخت where clause برای Lost Sales
     */
    private function buildWhereClauseForLostSales(array $filters): string
    {
        $parts = [];
        $statusCsv = $this->normalizeCsvList($filters['deal_statuses_csv'] ?? '');
        $leadTypeCsv = $this->normalizeCsvList($filters['deal_lead_types_csv'] ?? '');

        $legacyClause = $this->buildLegacyDealFilterClause($filters, 'd');
        if ($legacyClause !== '') {
            $parts[] = $legacyClause;
        }

        // فیلتر زمانی بر اساس lost_time یا fallback به register_time
        if (!empty($filters['date_from'])) {
            $parts[] = "DATE(COALESCE(d.lost_time, d.register_time, d.created_at)) >= ?";
        }
        if (!empty($filters['date_to'])) {
            $parts[] = "DATE(COALESCE(d.lost_time, d.register_time, d.created_at)) <= ?";
        }

        // فیلتر کاربر
        if (!empty($filters['owner_didar_ids_csv'])) {
            $parts[] = "FIND_IN_SET(d.owner_didar_id, ?)";
        } elseif (!empty($filters['owner_didar_id'])) {
            $parts[] = "d.owner_didar_id = ?";
        }

        // فیلتر محصول
        if (!empty($filters['product_ids_csv'])) {
            $parts[] = "EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = d.id AND FIND_IN_SET(dp.product_id, ?))";
        } elseif (!empty($filters['product_id'])) {
            $parts[] = "EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = d.id AND dp.product_id = ?)";
        }

        if ($statusCsv !== '' && !$this->csvHasValue($statusCsv, 'lost')) {
            $parts[] = "1 = 0";
        }

        if ($leadTypeCsv !== '' && !$this->csvHasAll($leadTypeCsv, ['new', 'old'])) {
            $parts[] = "(
                (FIND_IN_SET('new', ?) > 0 AND EXISTS (
                    SELECT 1 FROM persons pflt
                    WHERE pflt.didar_contact_id = d.contact_didar_id
                      AND DATE(COALESCE(pflt.register_time, pflt.created_at)) = DATE(COALESCE(d.register_time, d.created_at))
                ))
                OR (FIND_IN_SET('old', ?) > 0 AND EXISTS (
                    SELECT 1 FROM persons pflt
                    WHERE pflt.didar_contact_id = d.contact_didar_id
                      AND DATE(COALESCE(pflt.register_time, pflt.created_at)) < DATE(COALESCE(d.register_time, d.created_at))
                ))
            )";
        }

        return !empty($parts) ? implode(' AND ', $parts) : '1=1';
    }

    /**
     * ساخت where clause بدون transactions (برای Open Pipeline و Persons)
     */
    private function buildWhereClauseWithoutTransactions(array $filters): string
    {
        return $this->buildWhereClauseForDeals($filters, 'd');
    }

    /**
     * ساخت where clause برای Persons
     * NOTE: persons جدول product_id ندارد
     */
    private function buildWhereClauseForPersons(array $filters): string
    {
        $parts = [];

        // فیلتر زمانی
        if (!empty($filters['date_from'])) {
            $parts[] = "DATE(p.register_time) >= ?";
        }
        if (!empty($filters['date_to'])) {
            $parts[] = "DATE(p.register_time) <= ?";
        }

        // فیلتر کاربر
        if (!empty($filters['owner_didar_ids_csv'])) {
            $parts[] = "FIND_IN_SET(p.owner_didar_id, ?)";
        } elseif (!empty($filters['owner_didar_id'])) {
            $parts[] = "p.owner_didar_id = ?";
        }

        // نکته: product_id را برای persons نادیده می‌گیریم چون persons جدول product_id ندارد

        return !empty($parts) ? implode(' AND ', $parts) : '1=1';
    }

    /**
     * ساخت where clause برای جزئیات معاملات KPI
     * فیلتر تاریخ بر اساس اولین پرداخت (t5)
     */
    private function buildWhereClauseForDealDetails(array $filters): string
    {
        $parts = [];
        $statusCsv = $this->normalizeCsvList($filters['deal_statuses_csv'] ?? '');
        $leadTypeCsv = $this->normalizeCsvList($filters['deal_lead_types_csv'] ?? '');

        $legacyClause = $this->buildLegacyDealFilterClause($filters, 'd');
        if ($legacyClause !== '') {
            $parts[] = $legacyClause;
        }

        if (!empty($filters['date_from'])) {
            $parts[] = "DATE(COALESCE(d.register_time, d.created_at)) >= ?";
        }
        if (!empty($filters['date_to'])) {
            $parts[] = "DATE(COALESCE(d.register_time, d.created_at)) <= ?";
        }
        if (!empty($filters['owner_didar_ids_csv'])) {
            $parts[] = "FIND_IN_SET(d.owner_didar_id, ?)";
        } elseif (!empty($filters['owner_didar_id'])) {
            $parts[] = "d.owner_didar_id = ?";
        }
        if (!empty($filters['product_ids_csv'])) {
            $parts[] = "EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = d.id AND FIND_IN_SET(dp.product_id, ?))";
        } elseif (!empty($filters['product_id'])) {
            $parts[] = "EXISTS (SELECT 1 FROM deal_products dp WHERE dp.deal_id = d.id AND dp.product_id = ?)";
        }

        if ($statusCsv !== '' && !$this->csvHasAll($statusCsv, ['pending', 'won', 'lost'])) {
            $parts[] = "(
                (FIND_IN_SET('pending', ?) > 0 AND d.status NOT IN ('Won', 'Lost'))
                OR (FIND_IN_SET('won', ?) > 0 AND d.status = 'Won')
                OR (FIND_IN_SET('lost', ?) > 0 AND d.status = 'Lost')
            )";
        }

        if ($leadTypeCsv !== '' && !$this->csvHasAll($leadTypeCsv, ['new', 'old'])) {
            $parts[] = "(
                (FIND_IN_SET('new', ?) > 0 AND EXISTS (
                    SELECT 1 FROM persons pflt
                    WHERE pflt.didar_contact_id = d.contact_didar_id
                      AND DATE(COALESCE(pflt.register_time, pflt.created_at)) = DATE(COALESCE(d.register_time, d.created_at))
                ))
                OR (FIND_IN_SET('old', ?) > 0 AND EXISTS (
                    SELECT 1 FROM persons pflt
                    WHERE pflt.didar_contact_id = d.contact_didar_id
                      AND DATE(COALESCE(pflt.register_time, pflt.created_at)) < DATE(COALESCE(d.register_time, d.created_at))
                ))
            )";
        }

        return !empty($parts) ? implode(' AND ', $parts) : '1=1';
    }

    private function buildWhereClauseForDailySales(array $filters): string
    {
        return $this->buildWhereClauseForDeals($filters, 'd');
    }

    /**
     * ساخت پارامترها برای کوئری
     */
    private function buildParams(array $filters): array
    {
        return $this->buildParamsForDeals($filters);
    }

    /**
     * ساخت پارامترها برای Sales
     */
    private function buildParamsForSales(array $filters): array
    {
        return $this->buildParamsForDeals($filters);
    }

    /**
     * ساخت پارامترها برای Lost Sales
     */
    private function buildParamsForLostSales(array $filters): array
    {
        $params = [];
        $statusCsv = $this->normalizeCsvList($filters['deal_statuses_csv'] ?? '');
        $leadTypeCsv = $this->normalizeCsvList($filters['deal_lead_types_csv'] ?? '');

        if (!empty($filters['date_from'])) {
            $params[] = $filters['date_from'];
        }
        if (!empty($filters['date_to'])) {
            $params[] = $filters['date_to'];
        }
        if (!empty($filters['owner_didar_ids_csv'])) {
            $params[] = $filters['owner_didar_ids_csv'];
        } elseif (!empty($filters['owner_didar_id'])) {
            $params[] = $filters['owner_didar_id'];
        }
        if (!empty($filters['product_ids_csv'])) {
            $params[] = $filters['product_ids_csv'];
        } elseif (!empty($filters['product_id'])) {
            $params[] = $filters['product_id'];
        }
        if ($leadTypeCsv !== '' && !$this->csvHasAll($leadTypeCsv, ['new', 'old'])) {
            $params[] = $leadTypeCsv;
            $params[] = $leadTypeCsv;
        }

        return $params;
    }

    /**
     * ساخت پارامترها بدون transactions
     */
    private function buildParamsWithoutTransactions(array $filters): array
    {
        return $this->buildParamsForDeals($filters);
    }

    /**
     * ساخت پارامترها برای فیلترهای مبتنی بر معامله
     */
    private function buildParamsForDeals(array $filters): array
    {
        $params = [];
        $statusCsv = $this->normalizeCsvList($filters['deal_statuses_csv'] ?? '');
        $leadTypeCsv = $this->normalizeCsvList($filters['deal_lead_types_csv'] ?? '');

        if (!empty($filters['date_from'])) {
            $params[] = $filters['date_from'];
        }
        if (!empty($filters['date_to'])) {
            $params[] = $filters['date_to'];
        }
        if (!empty($filters['owner_didar_ids_csv'])) {
            $params[] = $filters['owner_didar_ids_csv'];
        } elseif (!empty($filters['owner_didar_id']) && trim($filters['owner_didar_id']) !== '') {
            $params[] = trim($filters['owner_didar_id']);
        }
        if (!empty($filters['product_ids_csv'])) {
            $params[] = $filters['product_ids_csv'];
        } elseif (!empty($filters['product_id'])) {
            $params[] = $filters['product_id'];
        }
        if ($statusCsv !== '' && !$this->csvHasAll($statusCsv, ['pending', 'won', 'lost'])) {
            $params[] = $statusCsv;
            $params[] = $statusCsv;
            $params[] = $statusCsv;
        }
        if ($leadTypeCsv !== '' && !$this->csvHasAll($leadTypeCsv, ['new', 'old'])) {
            $params[] = $leadTypeCsv;
            $params[] = $leadTypeCsv;
        }

        return $params;
    }

    /**
     * پارامترهای where بدون محدودیت تاریخ ثبت معامله
     */
    private function buildParamsForDealsNoDate(array $filters): array
    {
        $params = [];
        $statusCsv = $this->normalizeCsvList($filters['deal_statuses_csv'] ?? '');
        $leadTypeCsv = $this->normalizeCsvList($filters['deal_lead_types_csv'] ?? '');

        if (!empty($filters['owner_didar_ids_csv'])) {
            $params[] = $filters['owner_didar_ids_csv'];
        } elseif (!empty($filters['owner_didar_id']) && trim($filters['owner_didar_id']) !== '') {
            $params[] = trim($filters['owner_didar_id']);
        }
        if (!empty($filters['product_ids_csv'])) {
            $params[] = $filters['product_ids_csv'];
        } elseif (!empty($filters['product_id'])) {
            $params[] = $filters['product_id'];
        }
        if ($statusCsv !== '' && !$this->csvHasAll($statusCsv, ['pending', 'won', 'lost'])) {
            $params[] = $statusCsv;
            $params[] = $statusCsv;
            $params[] = $statusCsv;
        }
        if ($leadTypeCsv !== '' && !$this->csvHasAll($leadTypeCsv, ['new', 'old'])) {
            $params[] = $leadTypeCsv;
            $params[] = $leadTypeCsv;
        }

        return $params;
    }

    /**
     * ساخت پارامترها برای Persons
     */
    private function buildParamsForPersons(array $filters): array
    {
        $params = [];

        if (!empty($filters['date_from'])) {
            $params[] = $filters['date_from'];
        }
        if (!empty($filters['date_to'])) {
            $params[] = $filters['date_to'];
        }
        if (!empty($filters['owner_didar_ids_csv'])) {
            $params[] = $filters['owner_didar_ids_csv'];
        } elseif (!empty($filters['owner_didar_id'])) {
            $params[] = $filters['owner_didar_id'];
        }

        return $params;
    }

    private function getKpiPaymentDateBasis(array $filters, string $scope = 'payment'): string
    {
        $key = ($scope === 'sales') ? 'kpi_sales_date_basis' : 'kpi_payment_date_basis';
        $value = strtolower(trim((string)($filters[$key] ?? '')));
        return in_array($value, ['ui_payment_date', 'db_payment_date', 'system_created_at'], true)
            ? $value
            : 'ui_payment_date';
    }

    private function getKpiPaymentCountRule(array $filters, string $scope = 'payment'): string
    {
        $key = ($scope === 'sales') ? 'kpi_sales_count_rule' : 'kpi_payment_count_rule';
        $value = strtolower(trim((string)($filters[$key] ?? '')));
        return in_array($value, ['all_payments', 'first_payment_only'], true)
            ? $value
            : ($scope === 'sales' ? 'first_payment_only' : 'all_payments');
    }

    private function getKpiPaymentStatusScope(array $filters): string
    {
        $value = strtolower(trim((string)($filters['kpi_payment_status_scope'] ?? '')));
        return in_array($value, ['confirmed_only', 'all_statuses'], true)
            ? $value
            : 'confirmed_only';
    }

    private function buildKpiPaymentsByDealDateSql(array $filters, string $scope = 'payment'): string
    {
        $dateBasis = $this->getKpiPaymentDateBasis($filters, $scope);
        $statusScope = $this->getKpiPaymentStatusScope($filters);

        if ($dateBasis === 'db_payment_date' || $dateBasis === 'system_created_at') {
            $dateExpr = $dateBasis === 'system_created_at'
                ? 'DATE(t.created_at)'
                : "DATE(CASE WHEN LEFT(COALESCE(t.payment_date, ''), 10) = '0000-00-00' THEN NULL ELSE t.payment_date END)";
            $statusClause = $statusScope === 'confirmed_only'
                ? " AND LOWER(COALESCE(t.status, '')) = 'confirmed'"
                : '';

            return "
                SELECT
                    t.deal_id,
                    {$dateExpr} as payment_date,
                    SUM(COALESCE(t.amount, 0)) as amount
                FROM transactions t
                WHERE COALESCE(t.amount, 0) > 0
                  {$statusClause}
                GROUP BY t.deal_id, {$dateExpr}
                HAVING payment_date IS NOT NULL
            ";
        }

        return $this->buildUnifiedPaymentsByDealDateSqlForUiBasis($filters);
    }

    private function buildKpiSalesPaymentsByDealDateSql(array $filters): string
    {
        $countRule = $this->getKpiPaymentCountRule($filters, 'sales');
        if ($countRule === 'all_payments') {
            return $this->buildKpiPaymentsByDealDateSql($filters, 'sales');
        }
        return $this->buildKpiFirstPaymentDayByDealSql($filters, 'sales');
    }

    private function buildKpiPaymentsWindowByDealDateSql(array $filters, string $scope = 'payment'): string
    {
        $countRule = $this->getKpiPaymentCountRule($filters, $scope);
        if ($countRule === 'first_payment_only') {
            return $this->buildKpiFirstPaymentDayByDealSql($filters, $scope);
        }
        return $this->buildKpiPaymentsByDealDateSql($filters, $scope);
    }

    private function buildKpiFirstPaymentDayByDealSql(array $filters, string $scope = 'sales'): string
    {
        $paymentsByDealDateSql = $this->buildKpiPaymentsByDealDateSql($filters, $scope);
        return "
            SELECT
                p.deal_id,
                p.payment_date,
                p.amount
            FROM ({$paymentsByDealDateSql}) p
            INNER JOIN (
                SELECT deal_id, MIN(payment_date) as first_payment_date
                FROM ({$paymentsByDealDateSql}) fp
                GROUP BY deal_id
            ) f ON f.deal_id = p.deal_id AND f.first_payment_date = p.payment_date
        ";
    }

    private function buildKpiPaymentsByDealSql(array $filters, string $scope = 'payment', ?string $forceCountRule = null): string
    {
        $countRule = $forceCountRule ?? $this->getKpiPaymentCountRule($filters, $scope);
        $paymentsByDealDateSql = ($countRule === 'first_payment_only')
            ? $this->buildKpiFirstPaymentDayByDealSql($filters, $scope)
            : $this->buildKpiPaymentsByDealDateSql($filters, $scope);
        return "
            SELECT
                p.deal_id,
                SUM(p.amount) as total_paid,
                MIN(p.payment_date) as first_payment_date
            FROM ({$paymentsByDealDateSql}) p
            GROUP BY p.deal_id
        ";
    }

    /**
     * جمع کل پرداخت‌های هر معامله (همه‌زمان) برای KPIهای مالی.
     * در مبنای UI، مبلغ‌ها از JSON معامله خوانده می‌شوند (حتی اگر فقط date_persian داشته باشند)
     * و فقط وقتی JSON پرداخت نداشته باشد از transactions fallback می‌شود.
     * هدف: جلوگیری از double-count و همزمان از دست ندادن مبلغ‌های JSON با date_persian-only.
     */
    private function buildKpiAllTimePaymentTotalsByDealSql(array $filters, string $scope = 'payment'): string
    {
        $dateBasis = $this->getKpiPaymentDateBasis($filters, $scope);
        if ($dateBasis !== 'ui_payment_date') {
            return $this->buildKpiPaymentsByDealSql($filters, $scope, 'all_payments');
        }

        $statusScope = $this->getKpiPaymentStatusScope($filters);
        $statusClause = $statusScope === 'confirmed_only'
            ? " AND LOWER(COALESCE(t.status, '')) = 'confirmed'"
            : '';

        return "
            SELECT
                p.deal_id,
                SUM(COALESCE(p.amount, 0)) as total_paid,
                NULL as first_payment_date
            FROM (
                SELECT
                    d.id as deal_id,
                    CAST(
                        COALESCE(
                            NULLIF(JSON_UNQUOTE(JSON_EXTRACT(j.payment_obj, '$.amount')), ''),
                            '0'
                        ) AS DECIMAL(15,2)
                    ) as amount
                FROM deals d
                JOIN JSON_TABLE(
                    CASE
                        WHEN JSON_VALID(d.payments) THEN d.payments
                        ELSE JSON_ARRAY()
                    END,
                    '$[*]' COLUMNS (
                        payment_obj JSON PATH '$'
                    )
                ) j
                WHERE COALESCE(d.payments, '') <> ''
                  AND JSON_VALID(d.payments)

                UNION ALL

                SELECT
                    t.deal_id,
                    COALESCE(t.amount, 0) as amount
                FROM transactions t
                WHERE COALESCE(t.amount, 0) > 0
                  {$statusClause}
                  AND NOT EXISTS (
                    SELECT 1
                    FROM deals d2
                    WHERE d2.id = t.deal_id
                      AND COALESCE(d2.payments, '') <> ''
                      AND JSON_VALID(d2.payments)
                      AND COALESCE(JSON_LENGTH(d2.payments), 0) > 0
                  )
            ) p
            WHERE COALESCE(p.amount, 0) > 0
            GROUP BY p.deal_id
        ";
    }

    /**
     * وجود مطلق هرگونه پرداخت برای هر معامله (مستقل از بازه زمانی، مبنای تاریخ و وضعیت پرداخت).
     * برای KPI «معاملات بدون پرداخت» استفاده می‌شود تا فقط معاملاتی که واقعاً هیچ پرداختی ندارند برگردند.
     */
    private function buildAbsolutePaymentPresenceByDealSql(): string
    {
        return "
            SELECT
                p.deal_id,
                SUM(p.amount) as total_paid,
                NULL as first_payment_date
            FROM (
                SELECT
                    t.deal_id,
                    COALESCE(t.amount, 0) as amount
                FROM transactions t
                WHERE COALESCE(t.amount, 0) > 0

                UNION ALL

                SELECT
                    d.id as deal_id,
                    CAST(
                        COALESCE(
                            NULLIF(JSON_UNQUOTE(JSON_EXTRACT(j.payment_obj, '$.amount')), ''),
                            '0'
                        ) AS DECIMAL(15,2)
                    ) as amount
                FROM deals d
                JOIN JSON_TABLE(
                    CASE
                        WHEN JSON_VALID(d.payments) THEN d.payments
                        ELSE JSON_ARRAY()
                    END,
                    '$[*]' COLUMNS (
                        payment_obj JSON PATH '$'
                    )
                ) j
                WHERE COALESCE(d.payments, '') <> ''
                  AND JSON_VALID(d.payments)
            ) p
            WHERE COALESCE(p.amount, 0) > 0
            GROUP BY p.deal_id
        ";
    }

    private function buildUnifiedPaymentsByDealDateSqlForUiBasis(array $filters): string
    {
        $statusScope = $this->getKpiPaymentStatusScope($filters);
        $statusClause = $statusScope === 'confirmed_only'
            ? " AND LOWER(COALESCE(t.status, '')) = 'confirmed'"
            : '';
        return "
            SELECT
                src.deal_id,
                src.payment_date,
                SUM(src.amount) as amount
            FROM (
                SELECT
                    d.id as deal_id,
                    DATE(
                        CASE
                            WHEN LEFT(
                                COALESCE(
                                    NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(j.payment_obj, '$.date'))), ''),
                                    NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(j.payment_obj, '$.payment_date'))), '')
                                ),
                                10
                            ) = '0000-00-00'
                            THEN NULL
                            ELSE COALESCE(
                                NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(j.payment_obj, '$.date'))), ''),
                                NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(j.payment_obj, '$.payment_date'))), '')
                            )
                        END
                    ) as payment_date,
                    CAST(
                        COALESCE(
                            NULLIF(JSON_UNQUOTE(JSON_EXTRACT(j.payment_obj, '$.amount')), ''),
                            '0'
                        ) AS DECIMAL(15,2)
                    ) as amount
                FROM deals d
                JOIN JSON_TABLE(
                    CASE
                        WHEN JSON_VALID(d.payments) THEN d.payments
                        ELSE JSON_ARRAY()
                    END,
                    '$[*]' COLUMNS (
                        payment_obj JSON PATH '$'
                    )
                ) j
                WHERE COALESCE(d.payments, '') <> ''
                  AND JSON_VALID(d.payments)

                UNION ALL

                SELECT
                    t.deal_id,
                    DATE(CASE WHEN LEFT(COALESCE(t.payment_date, ''), 10) = '0000-00-00' THEN NULL ELSE t.payment_date END) as payment_date,
                    COALESCE(t.amount, 0) as amount
                FROM transactions t
                WHERE COALESCE(t.amount, 0) > 0
                  {$statusClause}
                  AND NOT EXISTS (
                    SELECT 1
                    FROM deals d2
                    WHERE d2.id = t.deal_id
                      AND COALESCE(d2.payments, '') <> ''
                      AND JSON_VALID(d2.payments)
                      AND COALESCE(JSON_LENGTH(d2.payments), 0) > 0
                  )
            ) src
            WHERE src.payment_date IS NOT NULL
              AND COALESCE(src.amount, 0) > 0
            GROUP BY src.deal_id, src.payment_date
        ";
    }

    /**
     * پرداخت‌های یکپارچه بر اساس معامله + تاریخ پرداخت.
     * اولویت با جدول transactions است؛ اگر معامله هیچ تراکنشی نداشت،
     * پرداخت‌های legacy از deals.payments استخراج می‌شوند.
     */
    private function buildUnifiedPaymentsByDealDateSql(): string
    {
        return "
            SELECT
                src.deal_id,
                src.payment_date,
                SUM(src.amount) as amount
            FROM (
                SELECT
                    d.id as deal_id,
                    DATE(
                        CASE
                            WHEN LEFT(
                                COALESCE(
                                    NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(j.payment_obj, '$.date'))), ''),
                                    NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(j.payment_obj, '$.payment_date'))), '')
                                ),
                                10
                            ) = '0000-00-00'
                            THEN NULL
                            ELSE COALESCE(
                                NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(j.payment_obj, '$.date'))), ''),
                                NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(j.payment_obj, '$.payment_date'))), '')
                            )
                        END
                    ) as payment_date,
                    CAST(
                        COALESCE(
                            NULLIF(JSON_UNQUOTE(JSON_EXTRACT(j.payment_obj, '$.amount')), ''),
                            '0'
                        ) AS DECIMAL(15,2)
                    ) as amount
                FROM deals d
                JOIN JSON_TABLE(
                    CASE
                        WHEN JSON_VALID(d.payments) THEN d.payments
                        ELSE JSON_ARRAY()
                    END,
                    '$[*]' COLUMNS (
                        payment_obj JSON PATH '$'
                    )
                ) j
                WHERE COALESCE(d.payments, '') <> ''
                  AND JSON_VALID(d.payments)

                UNION ALL

                SELECT
                    t.deal_id,
                    DATE(CASE WHEN LEFT(COALESCE(t.payment_date, ''), 10) = '0000-00-00' THEN NULL ELSE t.payment_date END) as payment_date,
                    COALESCE(t.amount, 0) as amount
                FROM transactions t
                WHERE COALESCE(t.amount, 0) > 0
                  AND NOT EXISTS (
                    SELECT 1
                    FROM deals d2
                    JOIN JSON_TABLE(
                        CASE
                            WHEN JSON_VALID(d2.payments) THEN d2.payments
                            ELSE JSON_ARRAY()
                        END,
                        '$[*]' COLUMNS (
                            json_date VARCHAR(64) PATH '$.date',
                            json_payment_date VARCHAR(64) PATH '$.payment_date'
                        )
                    ) jt2
                    WHERE d2.id = t.deal_id
                      AND NOT (
                          LEFT(COALESCE(NULLIF(TRIM(jt2.json_date), ''), NULLIF(TRIM(jt2.json_payment_date), '')), 10) = '0000-00-00'
                      )
                      AND COALESCE(
                          NULLIF(TRIM(jt2.json_date), ''),
                          NULLIF(TRIM(jt2.json_payment_date), '')
                      ) IS NOT NULL
                  )
            ) src
            WHERE src.payment_date IS NOT NULL
              AND COALESCE(src.amount, 0) > 0
            GROUP BY src.deal_id, src.payment_date
        ";
    }

    /**
     * فقط روز/مبلغ اولین پرداخت هر معامله (برای KPIهای فروش‌محور مبتنی بر «اولین فروش»)
     * اگر چند پرداخت در همان روز اول ثبت شده باشند، مبلغ همان روز تجمیع می‌شود.
     */
    private function buildUnifiedFirstPaymentDayByDealSql(): string
    {
        $paymentsByDealDateSql = $this->buildUnifiedPaymentsByDealDateSql();
        return "
            SELECT
                p.deal_id,
                p.payment_date,
                p.amount
            FROM ({$paymentsByDealDateSql}) p
            INNER JOIN (
                SELECT deal_id, MIN(payment_date) as first_payment_date
                FROM ({$paymentsByDealDateSql}) fp
                GROUP BY deal_id
            ) f ON f.deal_id = p.deal_id AND f.first_payment_date = p.payment_date
        ";
    }

    /**
     * تجمیع پرداخت یکپارچه به ازای هر معامله.
     */
    private function buildUnifiedPaymentsByDealSql(): string
    {
        $paymentsByDealDateSql = $this->buildUnifiedPaymentsByDealDateSql();
        return "
            SELECT
                p.deal_id,
                SUM(p.amount) as total_paid,
                MIN(p.payment_date) as first_payment_date
            FROM ({$paymentsByDealDateSql}) p
            GROUP BY p.deal_id
        ";
    }

    private function getPaymentWindowDealClassificationUiOracle(array $filters): array
    {
        $oracle = $this->getUiOracleKpiData($filters);
        $row = $oracle['financial_window'] ?? [];
        return [
            'paid_deals_count' => (int)($row['paid_deals_count'] ?? 0),
            'paid_deals_contract_sum' => (float)($row['paid_deals_contract_sum'] ?? 0),
            'paid_deals_collected_sum' => (float)($row['paid_deals_collected_sum'] ?? 0),
            'outstanding_count' => (int)($row['outstanding_count'] ?? 0),
            'outstanding_amount_sum' => (float)($row['outstanding_amount_sum'] ?? 0),
            'outstanding_contract_sum' => (float)($row['outstanding_contract_sum'] ?? 0),
            'outstanding_collected_sum' => (float)($row['outstanding_collected_sum'] ?? 0),
            'settled_count' => (int)($row['settled_count'] ?? 0),
            'settled_contract_sum' => (float)($row['settled_contract_sum'] ?? 0),
            'settled_collected_sum' => (float)($row['settled_collected_sum'] ?? 0),
        ];
    }

    private function getDealConversionRateUiOracle(array $filters): array
    {
        $oracle = $this->getUiOracleKpiData($filters);
        $result = $oracle['deal_conversion'] ?? ['total_deals' => 0, 'converted_deals' => 0];
        $totalDeals = (int)($result['total_deals'] ?? 0);
        $convertedDeals = (int)($result['converted_deals'] ?? 0);
        $conversionRate = $totalDeals > 0 ? round(($convertedDeals / $totalDeals) * 100, 2) : 0;
        $nonConversionRate = $totalDeals > 0 ? round((($totalDeals - $convertedDeals) / $totalDeals) * 100, 2) : 0;
        return [
            'total_deals' => $totalDeals,
            'converted_deals' => $convertedDeals,
            'conversion_rate' => $conversionRate,
            'non_conversion_rate' => $nonConversionRate,
        ];
    }

    private function getFinancialPerformanceCardDrilldownUiOracle(array $filters, string $mode, string $rowOwnerDidarId = ''): array
    {
        $oracle = $this->getUiOracleKpiData($filters);
        $ids = $mode === 'settled'
            ? array_keys($oracle['sets']['financial_settled'] ?? [])
            : array_keys($oracle['sets']['financial_outstanding'] ?? []);

        $ids = array_map('intval', array_filter($ids, static function ($value) {
            return (int)$value > 0;
        }));
        if (empty($ids)) {
            return ['leads' => [], 'deals' => [], 'activities' => [], 'payments' => [], 'meta' => ['total_leads' => 0, 'total_deals' => 0, 'total_activities' => 0, 'total_payments' => 0]];
        }

        if ($rowOwnerDidarId !== '') {
            $pdo = $this->db->getPdo();
            $placeholders = implode(',', array_fill(0, count($ids), '?'));
            $stmt = $pdo->prepare("SELECT id FROM deals WHERE id IN ({$placeholders}) AND owner_didar_id = ?");
            $stmt->execute(array_merge($ids, [$rowOwnerDidarId]));
            $ids = array_map('intval', array_column($stmt->fetchAll(PDO::FETCH_ASSOC) ?: [], 'id'));
            if (empty($ids)) {
                return ['leads' => [], 'deals' => [], 'activities' => [], 'payments' => [], 'meta' => ['total_leads' => 0, 'total_deals' => 0, 'total_activities' => 0, 'total_payments' => 0]];
            }
        }

        return $this->buildSimpleDealLeadDrilldownByIds($ids, $filters);
    }

    private function buildSimpleDealLeadDrilldownByIds(array $dealIds, array $filters, string $totalsBasis = 'payment'): array
    {
        $dealIds = array_values(array_unique(array_map('intval', $dealIds)));
        if (empty($dealIds)) {
            return ['leads' => [], 'deals' => [], 'activities' => [], 'payments' => [], 'meta' => ['total_leads' => 0, 'total_deals' => 0, 'total_activities' => 0, 'total_payments' => 0]];
        }
        $pdo = $this->db->getPdo();
        $placeholders = implode(',', array_fill(0, count($dealIds), '?'));
        $paymentsByDealSql = $this->buildKpiAllTimePaymentTotalsByDealSql($filters, $totalsBasis === 'sales' ? 'sales' : 'payment');

        $dealsSql = "
            SELECT
                d.id as deal_id,
                d.didar_deal_id,
                d.title,
                DATE(COALESCE(d.register_time, d.created_at)) as deal_date,
                COALESCE(u.display_name, 'بدون مسئول') as owner_name,
                COALESCE(d.owner_didar_id, '') as owner_didar_id,
                COALESCE(d.contact_didar_id, '') as contact_didar_id,
                COALESCE(p.customer_code, '') as customer_code,
                CASE
                    WHEN d.status = 'Won' THEN 'won'
                    WHEN d.status = 'Lost' THEN 'lost'
                    ELSE 'pending'
                END as deal_status_group,
                COALESCE(d.payable_amount, 0) as payable_amount,
                COALESCE(pay.total_paid, 0) as total_paid,
                CASE WHEN COALESCE(pay.total_paid, 0) > 0 THEN 1 ELSE 0 END as has_payment
            FROM deals d
            LEFT JOIN users u ON u.didar_user_id = d.owner_didar_id
            LEFT JOIN persons p ON p.didar_contact_id = d.contact_didar_id
            LEFT JOIN ({$paymentsByDealSql}) pay ON pay.deal_id = d.id
            WHERE d.id IN ({$placeholders})
            ORDER BY deal_date DESC, d.id DESC
        ";
        $deals = $this->runSelectAll($dealsSql, $dealIds);

        $contactIds = [];
        foreach ($deals as $deal) {
            $contactId = trim((string)($deal['contact_didar_id'] ?? ''));
            if ($contactId !== '') {
                $contactIds[$contactId] = true;
            }
        }
        $leads = [];
        if (!empty($contactIds)) {
            $contactList = array_keys($contactIds);
            $contactPlaceholders = implode(',', array_fill(0, count($contactList), '?'));
            $leadsSql = "
                SELECT
                    p.didar_contact_id as contact_id,
                    TRIM(CONCAT(COALESCE(p.first_name, ''), ' ', COALESCE(p.last_name, ''))) as full_name,
                    COALESCE(p.customer_code, '') as customer_code,
                    p.mobile_phone,
                    p.secondary_mobile_phone,
                    DATE(COALESCE(p.register_time, p.created_at)) as lead_date,
                    COALESCE(u.display_name, 'بدون مسئول') as owner_name
                FROM persons p
                LEFT JOIN users u ON u.didar_user_id = p.owner_didar_id
                WHERE p.didar_contact_id IN ({$contactPlaceholders})
                ORDER BY lead_date DESC, full_name ASC
                LIMIT 500
            ";
            $leads = $this->runSelectAll($leadsSql, $contactList);
        }

        $payments = $this->getPaymentsByDealIds($dealIds);

        return [
            'leads' => $leads,
            'deals' => $deals,
            'activities' => [],
            'payments' => $payments,
            'meta' => [
                'total_leads' => count($leads),
                'total_deals' => count($deals),
                'total_activities' => 0,
                'total_payments' => count($payments)
            ]
        ];
    }

    private function getUiOracleKpiData(array $filters): array
    {
        $cacheKey = md5(json_encode([
            'date_from' => (string)($filters['date_from'] ?? ''),
            'date_to' => (string)($filters['date_to'] ?? ''),
            'owner_didar_id' => (string)($filters['owner_didar_id'] ?? ''),
            'owner_didar_ids_csv' => (string)($filters['owner_didar_ids_csv'] ?? ''),
            'product_id' => (string)($filters['product_id'] ?? ''),
            'product_ids_csv' => (string)($filters['product_ids_csv'] ?? ''),
            'deal_statuses_csv' => (string)($filters['deal_statuses_csv'] ?? ''),
            'deal_lead_types_csv' => (string)($filters['deal_lead_types_csv'] ?? ''),
            'legacy_deal_scope' => (string)($filters['legacy_deal_scope'] ?? ''),
            'kpi_sales_count_rule' => (string)($filters['kpi_sales_count_rule'] ?? ''),
            'kpi_payment_count_rule' => (string)($filters['kpi_payment_count_rule'] ?? ''),
            'kpi_payment_status_scope' => (string)($filters['kpi_payment_status_scope'] ?? ''),
        ], JSON_UNESCAPED_UNICODE));
        if (isset($this->uiOracleCache[$cacheKey])) {
            return $this->uiOracleCache[$cacheKey];
        }

        $state = $this->buildUiOracleState($filters);
        $cohortIds = $this->uiOracleDealCohortIds($state, $filters);
        $noDateIds = $this->uiOracleDealNoDateFilteredIds($state, $filters);

        $salesBasis = $this->getKpiPaymentDateBasis($filters, 'sales');
        $salesStatusScope = $this->getKpiPaymentStatusScope($filters);
        $salesEventsWindow = $this->uiOraclePaymentEvents($state, $salesBasis, $salesStatusScope, true, $filters);
        $salesWindowAgg = $this->uiOracleAggregateByDeal($salesEventsWindow, $this->getKpiPaymentCountRule($filters, 'sales'));
        $salesFirstAllAgg = $this->uiOracleAggregateByDeal(
            $this->uiOraclePaymentEvents($state, $salesBasis, $salesStatusScope, false, $filters),
            'first_payment_only'
        );
        $salesAllAgg = $this->uiOracleAggregateByDeal(
            $this->uiOraclePaymentEvents($state, $salesBasis, $salesStatusScope, false, $filters),
            'all_payments'
        );

        $paymentWindowAgg = $this->uiOracleAggregateByDeal(
            $this->uiOraclePaymentEvents($state, 'ui_payment_date', $this->getKpiPaymentStatusScope($filters), true, $filters),
            $this->getKpiPaymentCountRule($filters, 'payment')
        );
        $paymentAllAgg = $this->uiOracleAggregateByDeal(
            $this->uiOraclePaymentEvents($state, 'ui_payment_date', $this->getKpiPaymentStatusScope($filters), false, $filters),
            'all_payments'
        );

        $salesCardQualified = [];
        $salesCardContract = 0.0;
        $salesCardCollected = 0.0;
        $rangeFrom = trim((string)($filters['date_from'] ?? ''));
        $rangeTo = trim((string)($filters['date_to'] ?? ''));
        foreach ($salesFirstAllAgg as $dealId => $firstAgg) {
            if (!isset($noDateIds[$dealId]) || !isset($state['deals'][$dealId])) {
                continue;
            }
            $firstDate = (string)($firstAgg['first'] ?? '');
            if ($firstDate === '') {
                continue;
            }
            if ($rangeFrom !== '' && $firstDate < $rangeFrom) {
                continue;
            }
            if ($rangeTo !== '' && $firstDate > $rangeTo) {
                continue;
            }
            $windowPaid = (float)($salesWindowAgg[$dealId]['total'] ?? 0);
            if ($windowPaid <= 0) {
                continue;
            }
            $salesCardQualified[$dealId] = true;
            $salesCardContract += (float)($state['deals'][$dealId]['payable_amount'] ?? 0);
            $salesCardCollected += (float)($salesAllAgg[$dealId]['total'] ?? 0);
        }

        $financial = [
            'paid_deals_count' => 0,
            'paid_deals_contract_sum' => 0.0,
            'paid_deals_collected_sum' => 0.0,
            'outstanding_count' => 0,
            'outstanding_amount_sum' => 0.0,
            'outstanding_contract_sum' => 0.0,
            'outstanding_collected_sum' => 0.0,
            'settled_count' => 0,
            'settled_contract_sum' => 0.0,
            'settled_collected_sum' => 0.0,
        ];
        $financialSettled = [];
        $financialOutstanding = [];
        $rangeFrom = trim((string)($filters['date_from'] ?? ''));
        $rangeTo = trim((string)($filters['date_to'] ?? ''));
        foreach ($paymentWindowAgg as $dealId => $windowRow) {
            if (!isset($noDateIds[$dealId])) {
                continue;
            }
            if (!isset($state['deals'][$dealId])) {
                continue;
            }
            $deal = $state['deals'][$dealId];
            $firstSalesDate = (string)($salesFirstAllAgg[$dealId]['first'] ?? '');
            if ($firstSalesDate === '') {
                continue;
            }
            if ($rangeFrom !== '' && $firstSalesDate < $rangeFrom) {
                continue;
            }
            if ($rangeTo !== '' && $firstSalesDate > $rangeTo) {
                continue;
            }
            $payable = (float)($deal['payable_amount'] ?? 0);
            $paidAll = (float)($paymentAllAgg[$dealId]['total'] ?? 0);

            $financial['paid_deals_count']++;
            $financial['paid_deals_contract_sum'] += $payable;
            $financial['paid_deals_collected_sum'] += $paidAll;

            $isSettled = (($payable <= 0 && $paidAll > 0) || ($payable > 0 && $paidAll >= $payable));
            $isOutstanding = ($payable > 0 && $paidAll > 0 && $paidAll < $payable);

            if ($isSettled) {
                $financial['settled_count']++;
                $financial['settled_contract_sum'] += $payable;
                $financial['settled_collected_sum'] += $paidAll;
                $financialSettled[$dealId] = true;
            } elseif ($isOutstanding) {
                $financial['outstanding_count']++;
                $financial['outstanding_amount_sum'] += ($payable - $paidAll);
                $financial['outstanding_contract_sum'] += $payable;
                $financial['outstanding_collected_sum'] += $paidAll;
                $financialOutstanding[$dealId] = true;
            }
        }

        $convCohort = [];
        $convConverted = [];
        $partitionLost = [];
        $partitionOpen = [];
        $partitionWon = [];
        $partitionWithoutPayment = [];
        $partitionWithPaymentNonLost = [];
        $partitionLostAmount = 0.0;
        $partitionOpenAmount = 0.0;
        $partitionWonAmount = 0.0;
        $partitionWithoutPaymentAmount = 0.0;
        foreach ($cohortIds as $dealId => $_) {
            if (!isset($state['deals'][$dealId])) {
                continue;
            }
            $dealDate = (string)($state['deals'][$dealId]['deal_date'] ?? '');
            $first = (string)($salesFirstAllAgg[$dealId]['first'] ?? '');
            if ($first !== '' && $dealDate !== '' && $first < $dealDate) {
                $firstInRange = true;
                if ($rangeFrom !== '' && $first < $rangeFrom) {
                    $firstInRange = false;
                }
                if ($rangeTo !== '' && $first > $rangeTo) {
                    $firstInRange = false;
                }
                if (!$firstInRange) {
                    continue;
                }
            }
            $convCohort[$dealId] = true;
            if ((float)($salesWindowAgg[$dealId]['total'] ?? 0) > 0) {
                $convConverted[$dealId] = true;
            }

            $deal = $state['deals'][$dealId];
            $status = (string)($deal['status'] ?? '');
            $payable = (float)($deal['payable_amount'] ?? 0);
            $paidAllForSalesBasis = (float)($salesAllAgg[$dealId]['total'] ?? 0);
            if ($status === 'Lost') {
                $partitionLost[$dealId] = true;
                $partitionLostAmount += $payable;
                continue;
            }
            if ($status === 'Won') {
                $partitionWon[$dealId] = true;
                $partitionWonAmount += $payable;
            } else {
                $partitionOpen[$dealId] = true;
                $partitionOpenAmount += $payable;
            }
            if ($paidAllForSalesBasis > 0) {
                $partitionWithPaymentNonLost[$dealId] = true;
            } else {
                $partitionWithoutPayment[$dealId] = true;
                $partitionWithoutPaymentAmount += $payable;
            }
        }

        // Option A extension:
        // اگر معامله براساس «فروش کل» (اولین واریزی در بازه) حساب شده باشد اما deal_date آن خارج از بازه باشد،
        // باید در مرجع نرخ تبدیل و bucketهای status (pending/won/lost) هم وارد شود.
        $crossDateSalesAdded = [];
        foreach ($salesCardQualified as $dealId => $_) {
            if (!isset($state['deals'][$dealId])) {
                continue;
            }
            if (!isset($noDateIds[$dealId])) {
                continue;
            }
            if (isset($convCohort[$dealId])) {
                continue;
            }
            $deal = $state['deals'][$dealId];
            $status = (string)($deal['status'] ?? '');
            $payable = (float)($deal['payable_amount'] ?? 0);
            $crossDateSalesAdded[$dealId] = true;
            $convCohort[$dealId] = true;
            $convConverted[$dealId] = true;
            if ($status === 'Lost') {
                $partitionLost[$dealId] = true;
                $partitionLostAmount += $payable;
            } elseif ($status === 'Won') {
                $partitionWon[$dealId] = true;
                $partitionWonAmount += $payable;
            } else {
                $partitionOpen[$dealId] = true;
                $partitionOpenAmount += $payable;
            }
            $partitionWithPaymentNonLost[$dealId] = true;
        }

        // مرجع نرخ تبدیل (Option A): فروش کل + بدون پرداخت + ناموفق + موفق
        $convReferenceIds = [];
        foreach ([$salesCardQualified, $partitionWithoutPayment, $partitionLost, $partitionWon] as $set) {
            foreach ($set as $dealId => $_) {
                $convReferenceIds[(string)$dealId] = true;
            }
        }
        $salesWonOverlap = [];
        foreach ($salesCardQualified as $dealId => $_) {
            if (isset($partitionWon[$dealId])) {
                $salesWonOverlap[$dealId] = true;
            }
        }

        $result = [
            'state' => $state,
            'aggs' => [
                'sales_window' => $salesWindowAgg,
                'sales_first_all' => $salesFirstAllAgg,
                'sales_all' => $salesAllAgg,
                'payment_window' => $paymentWindowAgg,
                'payment_all' => $paymentAllAgg,
            ],
            'sales_card' => [
                'deal_count' => count($salesCardQualified),
                'total_contract_amount' => $salesCardContract,
                'total_collected_amount' => $salesCardCollected,
            ],
            'financial_window' => $financial,
            'deal_conversion' => [
                'total_deals' => count($convReferenceIds),
                'converted_deals' => count($salesCardQualified),
            ],
            'conversion_partition' => [
                'lost_count' => count($partitionLost),
                'lost_amount' => $partitionLostAmount,
                'open_valid_count' => count($partitionOpen),
                'open_valid_amount' => $partitionOpenAmount,
                'won_count' => count($partitionWon),
                'won_amount' => $partitionWonAmount,
                'without_payment_count' => count($partitionWithoutPayment),
                'without_payment_amount' => $partitionWithoutPaymentAmount,
                'with_payment_non_lost_count' => count($partitionWithPaymentNonLost),
            ],
            'sets' => [
                'financial_settled' => $financialSettled,
                'financial_outstanding' => $financialOutstanding,
                'sales_card_qualified' => $salesCardQualified,
                'conversion_cohort' => $convCohort,
                'conversion_converted' => $convConverted,
                'conversion_reference' => $convReferenceIds,
                'conversion_cross_date_sales_added' => $crossDateSalesAdded,
                'conversion_sales_won_overlap' => $salesWonOverlap,
                'partition_lost_valid' => $partitionLost,
                'partition_open_valid' => $partitionOpen,
                'partition_won_valid' => $partitionWon,
                'partition_without_payment_valid' => $partitionWithoutPayment,
                'partition_with_payment_non_lost' => $partitionWithPaymentNonLost,
            ],
        ];
        $this->uiOracleCache[$cacheKey] = $result;
        return $result;
    }

    private function buildUiOracleState(array $filters): array
    {
        $whereDealsNoDate = $this->buildWhereClauseForDealsNoDate($filters, 'd');
        $paramsDealsNoDate = $this->buildParamsForDealsNoDate($filters);
        $dealsRows = $this->runSelectAll(
            "SELECT d.*, DATE(COALESCE(d.register_time, d.created_at)) as deal_date FROM deals d WHERE {$whereDealsNoDate}",
            $paramsDealsNoDate
        );
        $deals = [];
        foreach ($dealsRows as $row) {
            $deals[(string)$row['id']] = $row;
        }

        if (empty($deals)) {
            return [
                'deals' => [],
                'personsByContact' => [],
                'txByDeal' => [],
                'dealProducts' => [],
                'jsonByDeal' => [],
                'filters' => $filters,
            ];
        }

        $dealIds = array_keys($deals);
        $dealPlaceholders = implode(',', array_fill(0, count($dealIds), '?'));

        $contactIds = [];
        foreach ($deals as $deal) {
            $contactId = trim((string)($deal['contact_didar_id'] ?? ''));
            if ($contactId !== '') {
                $contactIds[$contactId] = true;
            }
        }
        $personsRows = [];
        if (!empty($contactIds)) {
            $contactList = array_keys($contactIds);
            $contactPlaceholders = implode(',', array_fill(0, count($contactList), '?'));
            $personsRows = $this->runSelectAll(
                "SELECT didar_contact_id, DATE(COALESCE(register_time, created_at)) as pdate FROM persons WHERE didar_contact_id IN ({$contactPlaceholders})",
                $contactList
            );
        }
        $personsByContact = [];
        foreach ($personsRows as $row) {
            $personsByContact[(string)$row['didar_contact_id']] = (string)($row['pdate'] ?? '');
        }

        $txRows = $this->runSelectAll(
            "SELECT deal_id, amount, payment_date, created_at, status FROM transactions WHERE COALESCE(amount,0) > 0 AND deal_id IN ({$dealPlaceholders})",
            $dealIds
        );
        $txByDeal = [];
        foreach ($txRows as $row) {
            $txByDeal[(string)$row['deal_id']][] = $row;
        }

        $dealProductsRows = $this->runSelectAll(
            "SELECT deal_id, product_id FROM deal_products WHERE deal_id IN ({$dealPlaceholders})",
            $dealIds
        );
        $dealProducts = [];
        foreach ($dealProductsRows as $row) {
            $dealProducts[(string)$row['deal_id']][(string)$row['product_id']] = true;
        }

        $jsonByDeal = [];
        foreach ($deals as $dealId => $deal) {
            $jsonByDeal[$dealId] = $this->uiOracleJsonPayments((string)($deal['payments'] ?? ''));
        }

        return [
            'deals' => $deals,
            'personsByContact' => $personsByContact,
            'txByDeal' => $txByDeal,
            'dealProducts' => $dealProducts,
            'jsonByDeal' => $jsonByDeal,
            'filters' => $filters,
        ];
    }

    private function uiOracleDealCohortIds(array $state, array $filters): array
    {
        $result = [];
        foreach ($state['deals'] as $dealId => $deal) {
            if ($this->uiOracleDealPass($deal, $state, $filters)) {
                $result[$dealId] = true;
            }
        }
        return $result;
    }

    private function uiOracleDealNoDateFilteredIds(array $state, array $filters): array
    {
        $result = [];
        foreach ($state['deals'] as $dealId => $deal) {
            if ($this->uiOracleDealPassNoDate($deal, $state, $filters)) {
                $result[$dealId] = true;
            }
        }
        return $result;
    }

    private function uiOracleDealPass(array $deal, array $state, array $filters): bool
    {
        $dealDate = (string)($deal['deal_date'] ?? '');
        if (!$this->uiOracleInRange($dealDate, $filters)) {
            return false;
        }
        return $this->uiOracleDealPassNoDate($deal, $state, $filters);
    }

    private function uiOracleDealPassNoDate(array $deal, array $state, array $filters): bool
    {
        $legacy = (int)($deal['is_legacy_didar'] ?? 0) === 1;
        $legacyScope = strtolower(trim((string)($filters['legacy_deal_scope'] ?? 'exclude')));
        if ($legacyScope === 'exclude' && $legacy) {
            return false;
        }
        if ($legacyScope === 'only' && !$legacy) {
            return false;
        }

        $ownerId = trim((string)($deal['owner_didar_id'] ?? ''));
        $ownerCsv = trim((string)($filters['owner_didar_ids_csv'] ?? ''));
        $ownerSingle = trim((string)($filters['owner_didar_id'] ?? ''));
        if ($ownerCsv !== '') {
            $owners = array_filter(array_map('trim', explode(',', $ownerCsv)), static function ($v) { return $v !== ''; });
            if (!in_array($ownerId, $owners, true)) {
                return false;
            }
        } elseif ($ownerSingle !== '' && $ownerId !== $ownerSingle) {
            return false;
        }

        $productCsv = trim((string)($filters['product_ids_csv'] ?? ''));
        $productSingle = trim((string)($filters['product_id'] ?? ''));
        if ($productCsv !== '' || $productSingle !== '') {
            $dealProductSet = $state['dealProducts'][(string)$deal['id']] ?? [];
            if ($productCsv !== '') {
                $allowed = array_filter(array_map('trim', explode(',', $productCsv)), static function ($v) { return $v !== ''; });
                $matched = false;
                foreach ($allowed as $pid) {
                    if (isset($dealProductSet[$pid])) {
                        $matched = true;
                        break;
                    }
                }
                if (!$matched) {
                    return false;
                }
            } elseif (!isset($dealProductSet[$productSingle])) {
                return false;
            }
        }

        $statusCsv = trim((string)($filters['deal_statuses_csv'] ?? ''));
        if ($statusCsv !== '') {
            $statusGroup = ((string)($deal['status'] ?? '') === 'Won')
                ? 'won'
                : (((string)($deal['status'] ?? '') === 'Lost') ? 'lost' : 'pending');
            $allowed = array_filter(array_map('trim', explode(',', $statusCsv)), static function ($v) { return $v !== ''; });
            if (!in_array($statusGroup, $allowed, true)) {
                return false;
            }
        }

        $leadTypeCsv = trim((string)($filters['deal_lead_types_csv'] ?? ''));
        if ($leadTypeCsv !== '') {
            $leadType = $this->uiOracleLeadType($deal, $state);
            $allowed = array_filter(array_map('trim', explode(',', $leadTypeCsv)), static function ($v) { return $v !== ''; });
            if (!in_array($leadType, $allowed, true)) {
                return false;
            }
        }

        return true;
    }

    private function resolveActivityPerformanceDealDate($dealId, array $deal, array $salesFirstAllAgg): string
    {
        $dealDate = trim((string)($deal['deal_date'] ?? ''));
        $firstPaymentDate = trim((string)($salesFirstAllAgg[(string)$dealId]['first'] ?? ''));
        if ($dealDate !== '' && $firstPaymentDate !== '' && $firstPaymentDate < $dealDate) {
            return $firstPaymentDate;
        }
        return $dealDate;
    }

    private function uiOracleLeadType(array $deal, array $state): string
    {
        $contactId = (string)($deal['contact_didar_id'] ?? '');
        $leadDate = (string)($state['personsByContact'][$contactId] ?? '');
        $dealDate = (string)($deal['deal_date'] ?? '');
        return ($leadDate !== '' && $leadDate === $dealDate) ? 'new' : 'old';
    }

    private function uiOraclePaymentEvents(array $state, string $basis, string $statusScope, bool $window, array $filters): array
    {
        $events = [];
        foreach ($state['deals'] as $dealId => $deal) {
            if ($basis === 'ui_payment_date') {
                $jsonPayments = $state['jsonByDeal'][$dealId] ?? [];
                if (!empty($jsonPayments)) {
                    foreach ($jsonPayments as $payment) {
                        $amount = (float)($payment['amount'] ?? 0);
                        $date = (string)($payment['date'] ?? '');
                        if ($amount <= 0 || $date === '') {
                            continue;
                        }
                        if ($window && !$this->uiOracleInRange($date, $filters)) {
                            continue;
                        }
                        $events[] = ['deal_id' => (string)$dealId, 'date' => $date, 'amount' => $amount];
                    }
                    continue;
                }
                foreach (($state['txByDeal'][$dealId] ?? []) as $tx) {
                    if (!$this->uiOracleTxPass($tx, $statusScope)) {
                        continue;
                    }
                    $date = $this->uiOracleNormalizeDate((string)($tx['payment_date'] ?? ''));
                    if ($date === '') {
                        continue;
                    }
                    if ($window && !$this->uiOracleInRange($date, $filters)) {
                        continue;
                    }
                    $events[] = ['deal_id' => (string)$dealId, 'date' => $date, 'amount' => (float)($tx['amount'] ?? 0)];
                }
                continue;
            }

            foreach (($state['txByDeal'][$dealId] ?? []) as $tx) {
                if (!$this->uiOracleTxPass($tx, $statusScope)) {
                    continue;
                }
                $date = $basis === 'system_created_at'
                    ? substr((string)($tx['created_at'] ?? ''), 0, 10)
                    : $this->uiOracleNormalizeDate((string)($tx['payment_date'] ?? ''));
                if ($date === '') {
                    continue;
                }
                if ($window && !$this->uiOracleInRange($date, $filters)) {
                    continue;
                }
                $events[] = ['deal_id' => (string)$dealId, 'date' => $date, 'amount' => (float)($tx['amount'] ?? 0)];
            }
        }
        return $events;
    }

    private function uiOracleAggregateByDeal(array $events, string $countRule): array
    {
        $rowsByDealDate = [];
        foreach ($events as $event) {
            $key = $event['deal_id'] . '|' . $event['date'];
            if (!isset($rowsByDealDate[$key])) {
                $rowsByDealDate[$key] = ['deal_id' => $event['deal_id'], 'date' => $event['date'], 'amount' => 0.0];
            }
            $rowsByDealDate[$key]['amount'] += (float)$event['amount'];
        }

        $grouped = [];
        foreach ($rowsByDealDate as $row) {
            $grouped[$row['deal_id']][] = $row;
        }

        $aggregated = [];
        foreach ($grouped as $dealId => $rows) {
            usort($rows, static function ($a, $b) {
                return strcmp((string)$a['date'], (string)$b['date']);
            });
            $firstDate = (string)$rows[0]['date'];
            $sum = 0.0;
            foreach ($rows as $row) {
                if ($countRule === 'first_payment_only' && (string)$row['date'] !== $firstDate) {
                    continue;
                }
                $sum += (float)$row['amount'];
            }
            $aggregated[(string)$dealId] = ['total' => $sum, 'first' => $firstDate];
        }

        return $aggregated;
    }

    private function uiOracleJsonPayments(string $rawPayments): array
    {
        $rawPayments = trim($rawPayments);
        if ($rawPayments === '') {
            return [];
        }
        $decoded = json_decode($rawPayments, true);
        if (!is_array($decoded)) {
            return [];
        }
        $result = [];
        foreach ($decoded as $item) {
            if (!is_array($item)) {
                continue;
            }
            $amount = (float)($item['amount'] ?? 0);
            if ($amount <= 0) {
                continue;
            }
            $date = $this->uiOracleNormalizeDate((string)($item['date'] ?? ''));
            if ($date === '') {
                $date = $this->uiOracleNormalizeDate((string)($item['payment_date'] ?? ''));
            }
            if ($date === '') {
                $date = $this->uiOracleJalaliToGregorianString((string)($item['date_persian'] ?? ''));
            }
            if ($date === '') {
                continue;
            }
            $result[] = ['date' => $date, 'amount' => $amount];
        }
        return $result;
    }

    private function uiOracleNormalizeDate(string $value): string
    {
        $value = trim($value);
        if ($value === '' || $value === '0000-00-00' || $value === '0000-00-00 00:00:00') {
            return '';
        }
        if (preg_match('/^\\d{4}[-\\/]\\d{2}[-\\/]\\d{2}$/', $value)) {
            return str_replace('/', '-', $value);
        }
        return '';
    }

    private function uiOracleInRange(string $date, array $filters): bool
    {
        $from = trim((string)($filters['date_from'] ?? ''));
        $to = trim((string)($filters['date_to'] ?? ''));
        if ($date === '') {
            return false;
        }
        if ($from !== '' && $date < $from) {
            return false;
        }
        if ($to !== '' && $date > $to) {
            return false;
        }
        return true;
    }

    private function uiOracleTxPass(array $tx, string $statusScope): bool
    {
        if ($statusScope === 'all_statuses') {
            return true;
        }
        return strtolower(trim((string)($tx['status'] ?? ''))) === 'confirmed';
    }

    private function uiOracleJalaliToGregorianString(string $jalali): string
    {
        $jalali = trim(str_replace(['-', '.'], '/', $jalali));
        $jalali = strtr($jalali, [
            '۰' => '0', '۱' => '1', '۲' => '2', '۳' => '3', '۴' => '4',
            '۵' => '5', '۶' => '6', '۷' => '7', '۸' => '8', '۹' => '9',
            '٠' => '0', '١' => '1', '٢' => '2', '٣' => '3', '٤' => '4',
            '٥' => '5', '٦' => '6', '٧' => '7', '٨' => '8', '٩' => '9',
        ]);
        if (!preg_match('/^(\\d{4})\\/(\\d{1,2})\\/(\\d{1,2})$/', $jalali, $m)) {
            return '';
        }
        [$gy, $gm, $gd] = $this->uiOracleJalaliToGregorian((int)$m[1], (int)$m[2], (int)$m[3]);
        if ($gy < 1900 || $gy > 2100 || $gm < 1 || $gm > 12 || $gd < 1 || $gd > 31) {
            return '';
        }
        return sprintf('%04d-%02d-%02d', $gy, $gm, $gd);
    }

    private function uiOracleJalaliToGregorian(int $jy, int $jm, int $jd): array
    {
        $jy += 1595;
        $days = -355668 + (365 * $jy) + intdiv($jy, 33) * 8 + intdiv((($jy % 33) + 3), 4) + $jd + ($jm < 7 ? (($jm - 1) * 31) : ((($jm - 7) * 30) + 186));
        $gy = 400 * intdiv($days, 146097);
        $days %= 146097;
        if ($days > 36524) {
            $gy += 100 * intdiv(--$days, 36524);
            $days %= 36524;
            if ($days >= 365) {
                $days++;
            }
        }
        $gy += 4 * intdiv($days, 1461);
        $days %= 1461;
        if ($days > 365) {
            $gy += intdiv($days - 1, 365);
            $days = ($days - 1) % 365;
        }
        $gd = $days + 1;
        $sal = [0, 31, (($gy % 4 === 0 && $gy % 100 !== 0) || ($gy % 400 === 0)) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
        $gm = 1;
        while ($gm <= 12 && $gd > $sal[$gm]) {
            $gd -= $sal[$gm];
            $gm++;
        }
        return [$gy, $gm, $gd];
    }

    /**
     * هم‌راستاسازی جداول KPI پیشرفته با منطق مرجع UI Oracle برای KPIهای فروش/مالی/تبدیل.
     * فقط جدول‌های هم‌پوشان با کارت‌ها/نمودارها override می‌شوند؛ بقیه به SQL فعلی واگذار می‌شوند.
     */
    private function applyUiOracleAdvancedKpiOverrides(array $filters, array $tables): array
    {
        $oracle = $this->getUiOracleKpiData($filters);
        $state = $oracle['state'] ?? [];
        $deals = $state['deals'] ?? [];
        if (empty($deals)) {
            return $tables;
        }

        $sets = $oracle['sets'] ?? [];
        $aggs = $oracle['aggs'] ?? [];
        $personsByContact = $state['personsByContact'] ?? [];
        $dealProducts = $state['dealProducts'] ?? [];

        $ownerIds = [];
        foreach ($deals as $deal) {
            $ownerId = trim((string)($deal['owner_didar_id'] ?? ''));
            $ownerIds[$ownerId] = true;
        }
        $ownerNames = $this->uiOracleOwnerNames(array_keys($ownerIds));
        $productNames = $this->uiOracleProductNames();

        $openCountsByContact = [];
        foreach ($deals as $dealId => $deal) {
            $status = (string)($deal['status'] ?? '');
            if ($status === 'Won' || $status === 'Lost') {
                continue;
            }
            if (!$this->uiOracleDealPassNoDate($deal, $state, $filters)) {
                continue;
            }
            $contactId = trim((string)($deal['contact_didar_id'] ?? ''));
            if ($contactId === '') {
                continue;
            }
            $openCountsByContact[$contactId] = ($openCountsByContact[$contactId] ?? 0) + 1;
        }

        $rowsByKey = [];
        $summaryByUser = [];
        $productStatus = [];
        $productSold = [];
        $productReq = [];
        $productFailed = [];

        $ensureRow = function (string $date, string $ownerId, string $ownerName) use (&$rowsByKey): array {
            $dayKey = $date . '|' . $ownerId;
            if (!isset($rowsByKey[$dayKey])) {
                $rowsByKey[$dayKey] = [
                    'date' => $date,
                    'owner_didar_id' => $ownerId,
                    'owner_name' => $ownerName,
                    'deal_total' => 0,
                    'deal_new_total' => 0,
                    'deal_old_total' => 0,
                    'deal_pending_total' => 0,
                    'deal_pending_new' => 0,
                    'deal_pending_old' => 0,
                    'deal_with_sales_total' => 0,
                    'deal_with_sales_new' => 0,
                    'deal_with_sales_old' => 0,
                    'deal_without_sales_total' => 0,
                    'deal_without_sales_new' => 0,
                    'deal_without_sales_old' => 0,
                    'deal_won_total' => 0,
                    'deal_won_new' => 0,
                    'deal_won_old' => 0,
                    'deal_lost_total' => 0,
                    'deal_lost_new' => 0,
                    'deal_lost_old' => 0,

                    'outstanding_deals_count' => 0,
                    'settled_deals_count' => 0,
                    'outstanding_amount_sum' => 0.0,
                    'settled_contract_amount_sum' => 0.0,
                    'payments_sum' => 0.0,

                    'conversion_rate_total_num' => 0,
                    'conversion_total_den' => 0,
                    'conversion_rate_new_num' => 0,
                    'conversion_new_den' => 0,
                    'conversion_rate_old_num' => 0,
                    'conversion_old_den' => 0,
                    'sales_total_count' => 0,
                    'sales_new_count' => 0,
                    'sales_old_count' => 0,
                    'total_deals_count' => 0,
                    'total_new_deals_count' => 0,
                    'total_old_deals_count' => 0,
                    'total_collected_amount' => 0.0,

                    'sum_conversion_days_total' => 0,
                    'count_conversion_days_total' => 0,
                    'sum_conversion_days_new' => 0,
                    'count_conversion_days_new' => 0,
                    'sum_conversion_days_old' => 0,
                    'count_conversion_days_old' => 0,
                    'sum_failure_days_total' => 0,
                    'count_failure_days_total' => 0,
                    'sum_failure_days_new' => 0,
                    'count_failure_days_new' => 0,
                    'sum_failure_days_old' => 0,
                    'count_failure_days_old' => 0,
                    'payments_after_deal_count' => 0,
                    'payments_before_deal_count' => 0,
                    'payments_same_day_count' => 0,
                ];
            }
            return $rowsByKey[$dayKey];
        };

        foreach ($deals as $dealId => $deal) {
            $dealId = (string)$dealId;

            $dealDate = (string)($deal['deal_date'] ?? '');
            $ownerId = trim((string)($deal['owner_didar_id'] ?? ''));
            $ownerName = (string)($ownerNames[$ownerId] ?? 'بدون مسئول');
            $contactId = trim((string)($deal['contact_didar_id'] ?? ''));
            $contactLeadDate = (string)($personsByContact[$contactId] ?? '');
            $leadType = ($contactLeadDate !== '' && $contactLeadDate === $dealDate) ? 'new' : 'old';
            $status = (string)($deal['status'] ?? '');
            $statusGroup = $status === 'Won' ? 'won' : ($status === 'Lost' ? 'lost' : 'pending');
            $payable = (float)($deal['payable_amount'] ?? 0);
            $inConversionCohort = isset($sets['conversion_cohort'][$dealId]);
            $salesHasPayment = isset($sets['sales_card_qualified'][$dealId]) ? 1 : 0;
            $conversionHasPayment = isset($sets['conversion_converted'][$dealId]) ? 1 : 0;
            $salesTotalPaid = (float)($aggs['sales_all'][$dealId]['total'] ?? 0);
            $salesFirstDate = (string)($aggs['sales_first_all'][$dealId]['first'] ?? '');
            $paymentAllPaid = (float)($aggs['payment_all'][$dealId]['total'] ?? 0);
            $isSettled = isset($sets['financial_settled'][$dealId]);
            $isOutstanding = isset($sets['financial_outstanding'][$dealId]);
            $hasOpenDeal = ($contactId !== '' && (($openCountsByContact[$contactId] ?? 0) > 0)) ? 1 : 0;
            $lostDate = substr((string)($deal['lost_time'] ?? ''), 0, 10);
            $failureReason = $this->uiOracleFailureReasonFromDeal($deal);

            // deal_status / conversion_status / conversion_time are cohort-valid and rowed by deal_date
            if ($inConversionCohort) {
                $r =& $rowsByKey[$dealDate . '|' . $ownerId];
                if (!isset($r)) {
                    $tmp = $ensureRow($dealDate, $ownerId, $ownerName);
                    $r =& $rowsByKey[$dealDate . '|' . $ownerId];
                }

                $r['deal_total']++;
                $r['total_deals_count']++;
                $r['conversion_total_den']++;
                if ($leadType === 'new') {
                    $r['deal_new_total']++;
                    $r['total_new_deals_count']++;
                    $r['conversion_new_den']++;
                } else {
                    $r['deal_old_total']++;
                    $r['total_old_deals_count']++;
                    $r['conversion_old_den']++;
                }

                if ($statusGroup === 'pending') {
                    $r['deal_pending_total']++;
                    if ($leadType === 'new') { $r['deal_pending_new']++; } else { $r['deal_pending_old']++; }
                } elseif ($statusGroup === 'won') {
                    $r['deal_won_total']++;
                    if ($leadType === 'new') { $r['deal_won_new']++; } else { $r['deal_won_old']++; }
                } elseif ($statusGroup === 'lost') {
                    $r['deal_lost_total']++;
                    if ($leadType === 'new') { $r['deal_lost_new']++; } else { $r['deal_lost_old']++; }
                }

                // Use sales-card-qualified for display consistency, but keep conversion numerators cohort-valid only.
                if ($salesHasPayment) {
                    $r['deal_with_sales_total']++;
                    $r['sales_total_count']++;
                    $r['total_collected_amount'] += $salesTotalPaid;
                    if ($leadType === 'new') {
                        $r['deal_with_sales_new']++;
                        $r['sales_new_count']++;
                    } else {
                        $r['deal_with_sales_old']++;
                        $r['sales_old_count']++;
                    }
                } else {
                    if (isset($sets['partition_without_payment_valid'][$dealId]) && $statusGroup === 'pending') {
                        $r['deal_without_sales_total']++;
                        if ($leadType === 'new') { $r['deal_without_sales_new']++; } else { $r['deal_without_sales_old']++; }
                    }
                }
                if ($conversionHasPayment) {
                    $r['conversion_rate_total_num']++;
                    if ($leadType === 'new') { $r['conversion_rate_new_num']++; } else { $r['conversion_rate_old_num']++; }
                }

                // conversion_time aligned with summary (exclude negative conversion days)
                if ($conversionHasPayment && $salesFirstDate !== '' && $dealDate !== '') {
                    $dd = (int)((new \DateTime($salesFirstDate))->diff(new \DateTime($dealDate)))->format('%r%a');
                    if ($dd >= 0) {
                        if ($dd > 0) { $r['payments_after_deal_count']++; } else { $r['payments_same_day_count']++; }
                        $r['sum_conversion_days_total'] += $dd;
                        $r['count_conversion_days_total']++;
                        if ($leadType === 'new') {
                            $r['sum_conversion_days_new'] += $dd;
                            $r['count_conversion_days_new']++;
                        } else {
                            $r['sum_conversion_days_old'] += $dd;
                            $r['count_conversion_days_old']++;
                        }
                    } else {
                        $r['payments_before_deal_count']++;
                    }
                }
                if ($statusGroup === 'lost' && $lostDate !== '' && $dealDate !== '') {
                    $fd = (int)((new \DateTime($lostDate))->diff(new \DateTime($dealDate)))->format('%r%a');
                    $r['sum_failure_days_total'] += $fd;
                    $r['count_failure_days_total']++;
                    if ($leadType === 'new') {
                        $r['sum_failure_days_new'] += $fd;
                        $r['count_failure_days_new']++;
                    } else {
                        $r['sum_failure_days_old'] += $fd;
                        $r['count_failure_days_old']++;
                    }
                }
                unset($r);
            }

            // payment_status / summary_by_user / product_* use sales-card-qualified cohort and row by first sales date
            if ($salesHasPayment && $salesFirstDate !== '') {
                $rPay =& $rowsByKey[$salesFirstDate . '|' . $ownerId];
                if (!isset($rPay)) {
                    $tmp = $ensureRow($salesFirstDate, $ownerId, $ownerName);
                    $rPay =& $rowsByKey[$salesFirstDate . '|' . $ownerId];
                }
                $rPay['payments_sum'] += $paymentAllPaid;
                if ($isSettled) {
                    $rPay['settled_deals_count']++;
                    $rPay['settled_contract_amount_sum'] += $payable;
                } elseif ($isOutstanding) {
                    $rPay['outstanding_deals_count']++;
                    $rPay['outstanding_amount_sum'] += max(0.0, $payable - $paymentAllPaid);
                }
                unset($rPay);
            }

            // summary_by_user
            if (!isset($summaryByUser[$ownerId])) {
                $summaryByUser[$ownerId] = [
                    'owner_didar_id' => $ownerId,
                    'owner_name' => $ownerName,
                    'total_deals_count' => 0,
                    'sales_total_count' => 0,
                    'total_new_deals_count' => 0,
                    'sales_new_count' => 0,
                    'total_collected_amount' => 0.0,
                    'outstanding_amount_sum' => 0.0,
                ];
            }
            $su =& $summaryByUser[$ownerId];
            if ($inConversionCohort) {
                $su['total_deals_count']++;
                if ($leadType === 'new') { $su['total_new_deals_count']++; }
            }
            if ($salesHasPayment) {
                $su['sales_total_count']++;
                $su['total_collected_amount'] += $salesTotalPaid;
                if ($leadType === 'new') { $su['sales_new_count']++; }
            }
            if ($salesHasPayment && $isOutstanding) {
                $su['outstanding_amount_sum'] += max(0.0, $payable - $paymentAllPaid);
            }

            // product tables (requested by deal_date cohort-valid, sold by sales-card-qualified)
            $primaryProductId = $this->uiOraclePrimaryProductIdForDeal($deal, $dealProducts);
            $productName = $this->uiOracleResolvedProductName($deal, $primaryProductId, $productNames);
            if ($productName !== '') {
                if ($inConversionCohort) {
                    $pkReq = $dealDate . '|' . $ownerId . '|' . $productName;
                    if (!isset($productReq[$pkReq])) {
                        $productReq[$pkReq] = ['date'=>$dealDate,'owner_didar_id'=>$ownerId,'owner_name'=>$ownerName,'product_name'=>$productName,'requested_new_count'=>0,'requested_old_count'=>0,'requested_total_count'=>0,'sold_total_count'=>0,'unsold_total_count'=>0];
                    }
                    $productReq[$pkReq]['requested_total_count']++;
                    if ($leadType === 'new') { $productReq[$pkReq]['requested_new_count']++; } else { $productReq[$pkReq]['requested_old_count']++; }
                    if (!$salesHasPayment) {
                        $productReq[$pkReq]['unsold_total_count']++;
                        if ($statusGroup === 'lost') {
                            $productFailed[$pkReq] = $productFailed[$pkReq] ?? ['date'=>$dealDate,'owner_didar_id'=>$ownerId,'owner_name'=>$ownerName,'product_name'=>$productName,'failed_new_count'=>0,'failed_old_count'=>0,'failed_total_count'=>0];
                            $productFailed[$pkReq]['failed_total_count']++;
                            if ($leadType === 'new') { $productFailed[$pkReq]['failed_new_count']++; } else { $productFailed[$pkReq]['failed_old_count']++; }
                        }
                    }
                }
                if ($salesHasPayment) {
                    $salesDate = $salesFirstDate !== '' ? $salesFirstDate : $dealDate;
                    $pkSales = $salesDate . '|' . $ownerId . '|' . $productName;
                    if (!isset($productStatus[$pkSales])) {
                        $productStatus[$pkSales] = [
                            'date' => $salesDate,
                            'owner_didar_id' => $ownerId,
                            'owner_name' => $ownerName,
                            'product_name' => $productName,
                            'sales_total_count' => 0,
                            'sales_new_count' => 0,
                            'sales_old_count' => 0,
                        ];
                    }
                    if (!isset($productSold[$pkSales])) {
                        $productSold[$pkSales] = ['date'=>$salesDate,'owner_didar_id'=>$ownerId,'owner_name'=>$ownerName,'product_name'=>$productName,'sold_new_count'=>0,'sold_old_count'=>0,'sold_total_count'=>0];
                    }
                    $productStatus[$pkSales]['sales_total_count']++;
                    $productSold[$pkSales]['sold_total_count']++;
                    if ($leadType === 'new') {
                        $productStatus[$pkSales]['sales_new_count']++;
                        $productSold[$pkSales]['sold_new_count']++;
                    } else {
                        $productStatus[$pkSales]['sales_old_count']++;
                        $productSold[$pkSales]['sold_old_count']++;
                    }
                    if ($inConversionCohort) {
                        $pkReqSales = $dealDate . '|' . $ownerId . '|' . $productName;
                        if (!isset($productReq[$pkReqSales])) {
                            $productReq[$pkReqSales] = ['date'=>$dealDate,'owner_didar_id'=>$ownerId,'owner_name'=>$ownerName,'product_name'=>$productName,'requested_new_count'=>0,'requested_old_count'=>0,'requested_total_count'=>0,'sold_total_count'=>0,'unsold_total_count'=>0];
                        }
                        $productReq[$pkReqSales]['sold_total_count']++;
                    }
                }
            }

            // payment_mix (distinct leads by deal_date/owner buckets)
            // We'll compute later from deal rows to avoid duplicate contact handling here.
            unset($r, $su);
        }

        // build payment_mix + failure_reasons from cohort-valid deals
        $paymentMixContacts = [];
        $failureRows = [];
        foreach ($deals as $dealId => $deal) {
            $dealId = (string)$dealId;
            if (!isset($sets['conversion_cohort'][$dealId])) {
                continue;
            }
            $dealDate = (string)($deal['deal_date'] ?? '');
            $ownerId = trim((string)($deal['owner_didar_id'] ?? ''));
            $ownerName = (string)($ownerNames[$ownerId] ?? 'بدون مسئول');
            $contactId = trim((string)($deal['contact_didar_id'] ?? ''));
            if ($contactId === '') {
                continue;
            }
            $contactLeadDate = (string)($personsByContact[$contactId] ?? '');
            $leadType = ($contactLeadDate !== '' && $contactLeadDate === $dealDate) ? 'new' : 'old';
            $status = (string)($deal['status'] ?? '');
            $statusGroup = $status === 'Won' ? 'won' : ($status === 'Lost' ? 'lost' : 'pending');
            $lostDate = substr((string)($deal['lost_time'] ?? ''), 0, 10);
            $isSalesQualified = isset($sets['sales_card_qualified'][$dealId]) ? 1 : 0;
            $mixDate = $isSalesQualified ? ((string)($aggs['sales_first_all'][$dealId]['first'] ?? '') ?: $dealDate) : $dealDate;
            $mixKey = $mixDate . '|' . $ownerId;
            if (!isset($paymentMixContacts[$mixKey])) {
                $paymentMixContacts[$mixKey] = [
                    'date' => $mixDate,
                    'owner_didar_id' => $ownerId,
                    'owner_name' => $ownerName,
                    'new_leads_with_payment' => [],
                    'new_leads_without_payment' => [],
                    'new_leads_failed' => [],
                    'old_leads_with_payment' => [],
                    'old_leads_without_payment' => [],
                    'old_leads_failed' => [],
                ];
            }
            $bucket =& $paymentMixContacts[$mixKey];
            // Count by deal (not distinct contact) to match KPI cards/cardinality across surfaces
            $bucketKey = $dealId;
            if ($leadType === 'new') {
                if (isset($sets['partition_lost_valid'][$dealId])) $bucket['new_leads_failed'][$bucketKey] = true;
                elseif (isset($sets['sales_card_qualified'][$dealId])) $bucket['new_leads_with_payment'][$bucketKey] = true;
                elseif (isset($sets['partition_without_payment_valid'][$dealId])) $bucket['new_leads_without_payment'][$bucketKey] = true;
            } else {
                if (isset($sets['partition_lost_valid'][$dealId])) $bucket['old_leads_failed'][$bucketKey] = true;
                elseif (isset($sets['sales_card_qualified'][$dealId])) $bucket['old_leads_with_payment'][$bucketKey] = true;
                elseif (isset($sets['partition_without_payment_valid'][$dealId])) $bucket['old_leads_without_payment'][$bucketKey] = true;
            }
            unset($bucket);

            if ($statusGroup === 'lost') {
                $failureReason = $this->uiOracleFailureReasonFromDeal($deal);
                $hasOpenDeal = ($contactId !== '' && (($openCountsByContact[$contactId] ?? 0) > 0)) ? 1 : 0;
                $failureDate = $lostDate !== '' ? $lostDate : $dealDate;
                $fk = $failureDate . '|' . $ownerId . '|' . $failureReason;
                if (!isset($failureRows[$fk])) {
                    $failureRows[$fk] = [
                        'date' => $failureDate,
                        'owner_didar_id' => $ownerId,
                        'owner_name' => $ownerName,
                        'failure_reason' => $failureReason,
                        'failed_without_current_count' => 0,
                        'failed_new_count' => 0,
                        'failed_old_count' => 0,
                    ];
                }
                if ($hasOpenDeal === 0) $failureRows[$fk]['failed_without_current_count']++;
                if ($leadType === 'new') $failureRows[$fk]['failed_new_count']++; else $failureRows[$fk]['failed_old_count']++;
            }
        }

        $tableDealStatus = [];
        $tablePaymentStatus = [];
        $tableConversionStatus = [];
        $tableConversionTime = [];
        $tableSummaryByUser = [];

        foreach ($rowsByKey as $row) {
            $tableDealStatus[] = [
                'date' => $row['date'],
                'owner_didar_id' => $row['owner_didar_id'],
                'owner_name' => $row['owner_name'],
                'deal_total' => (int)$row['deal_total'],
                'deal_new_total' => (int)$row['deal_new_total'],
                'deal_old_total' => (int)$row['deal_old_total'],
                'deal_pending_total' => (int)$row['deal_pending_total'],
                'deal_pending_new' => (int)$row['deal_pending_new'],
                'deal_pending_old' => (int)$row['deal_pending_old'],
                'deal_with_sales_total' => (int)$row['deal_with_sales_total'],
                'deal_with_sales_new' => (int)$row['deal_with_sales_new'],
                'deal_with_sales_old' => (int)$row['deal_with_sales_old'],
                'deal_without_sales_total' => (int)$row['deal_without_sales_total'],
                'deal_without_sales_new' => (int)$row['deal_without_sales_new'],
                'deal_without_sales_old' => (int)$row['deal_without_sales_old'],
                'deal_won_total' => (int)$row['deal_won_total'],
                'deal_won_new' => (int)$row['deal_won_new'],
                'deal_won_old' => (int)$row['deal_won_old'],
                'deal_lost_total' => (int)$row['deal_lost_total'],
                'deal_lost_new' => (int)$row['deal_lost_new'],
                'deal_lost_old' => (int)$row['deal_lost_old'],
            ];
            $tablePaymentStatus[] = [
                'date' => $row['date'],
                'owner_didar_id' => $row['owner_didar_id'],
                'owner_name' => $row['owner_name'],
                'outstanding_deals_count' => (int)$row['outstanding_deals_count'],
                'settled_deals_count' => (int)$row['settled_deals_count'],
                'outstanding_amount_sum' => (float)$row['outstanding_amount_sum'],
                'settled_contract_amount_sum' => (float)$row['settled_contract_amount_sum'],
                'payments_sum' => (float)$row['payments_sum'],
            ];

            $total = (int)$row['conversion_total_den'];
            $newTotal = (int)$row['conversion_new_den'];
            $oldTotal = (int)$row['conversion_old_den'];
            $salesTotal = (int)$row['sales_total_count'];
            $salesNew = (int)$row['sales_new_count'];
            $salesOld = (int)$row['sales_old_count'];
            $tableConversionStatus[] = [
                'date' => $row['date'],
                'owner_didar_id' => $row['owner_didar_id'],
                'owner_name' => $row['owner_name'],
                'conversion_rate_total' => $total > 0 ? round(($salesTotal / $total) * 100, 2) : 0.0,
                'non_conversion_rate_total' => $total > 0 ? round((($total - $salesTotal) / $total) * 100, 2) : 0.0,
                'conversion_rate_new' => $newTotal > 0 ? round(($salesNew / $newTotal) * 100, 2) : 0.0,
                'non_conversion_rate_new' => $newTotal > 0 ? round((($newTotal - $salesNew) / $newTotal) * 100, 2) : 0.0,
                'conversion_rate_old' => $oldTotal > 0 ? round(($salesOld / $oldTotal) * 100, 2) : 0.0,
                'non_conversion_rate_old' => $oldTotal > 0 ? round((($oldTotal - $salesOld) / $oldTotal) * 100, 2) : 0.0,
                'sales_total_count' => $salesTotal,
                'sales_new_count' => $salesNew,
                'sales_old_count' => $salesOld,
                'total_deals_count' => $total,
                'total_new_deals_count' => $newTotal,
                'total_old_deals_count' => $oldTotal,
                'total_collected_amount' => (float)$row['total_collected_amount'],
                'outstanding_amount_sum' => (float)$row['outstanding_amount_sum'],
            ];

            $ct = (int)$row['count_conversion_days_total'];
            $cn = (int)$row['count_conversion_days_new'];
            $co = (int)$row['count_conversion_days_old'];
            $ft = (int)$row['count_failure_days_total'];
            $fn = (int)$row['count_failure_days_new'];
            $fo = (int)$row['count_failure_days_old'];
            $tableConversionTime[] = [
                'date' => $row['date'],
                'owner_didar_id' => $row['owner_didar_id'],
                'owner_name' => $row['owner_name'],
                'avg_conversion_days_total' => $ct > 0 ? round(((float)$row['sum_conversion_days_total']) / $ct, 2) : 0.0,
                'sum_conversion_days_total' => (float)$row['sum_conversion_days_total'],
                'count_conversion_days_total' => $ct,
                'avg_conversion_days_new' => $cn > 0 ? round(((float)$row['sum_conversion_days_new']) / $cn, 2) : 0.0,
                'sum_conversion_days_new' => (float)$row['sum_conversion_days_new'],
                'count_conversion_days_new' => $cn,
                'avg_conversion_days_old' => $co > 0 ? round(((float)$row['sum_conversion_days_old']) / $co, 2) : 0.0,
                'sum_conversion_days_old' => (float)$row['sum_conversion_days_old'],
                'count_conversion_days_old' => $co,
                'avg_failure_days_total' => $ft > 0 ? round(((float)$row['sum_failure_days_total']) / $ft, 2) : 0.0,
                'sum_failure_days_total' => (float)$row['sum_failure_days_total'],
                'count_failure_days_total' => $ft,
                'avg_failure_days_new' => $fn > 0 ? round(((float)$row['sum_failure_days_new']) / $fn, 2) : 0.0,
                'sum_failure_days_new' => (float)$row['sum_failure_days_new'],
                'count_failure_days_new' => $fn,
                'avg_failure_days_old' => $fo > 0 ? round(((float)$row['sum_failure_days_old']) / $fo, 2) : 0.0,
                'sum_failure_days_old' => (float)$row['sum_failure_days_old'],
                'count_failure_days_old' => $fo,
                'payments_after_deal_count' => (int)$row['payments_after_deal_count'],
                'payments_before_deal_count' => (int)$row['payments_before_deal_count'],
                'payments_same_day_count' => (int)$row['payments_same_day_count'],
            ];
        }

        foreach ($summaryByUser as &$su) {
            $su['conversion_rate_total'] = (int)$su['total_deals_count'] > 0 ? round(($su['sales_total_count'] / $su['total_deals_count']) * 100, 2) : 0.0;
            $su['conversion_rate_new'] = (int)$su['total_new_deals_count'] > 0 ? round(($su['sales_new_count'] / $su['total_new_deals_count']) * 100, 2) : 0.0;
            foreach (['total_collected_amount','outstanding_amount_sum'] as $fld) {
                $su[$fld] = (float)$su[$fld];
            }
        }
        unset($su);
        $tableSummaryByUser = array_values($summaryByUser);

        $tablePaymentMix = [];
        foreach ($paymentMixContacts as $row) {
            $tablePaymentMix[] = [
                'date' => $row['date'],
                'owner_didar_id' => $row['owner_didar_id'],
                'owner_name' => $row['owner_name'],
                'new_leads_with_payment' => count($row['new_leads_with_payment']),
                'new_leads_without_payment' => count($row['new_leads_without_payment']),
                'new_leads_failed' => count($row['new_leads_failed']),
                'old_leads_with_payment' => count($row['old_leads_with_payment']),
                'old_leads_without_payment' => count($row['old_leads_without_payment']),
                'old_leads_failed' => count($row['old_leads_failed']),
            ];
        }

        $overrides = [
            'deal_status' => $this->uiOracleSortRows($tableDealStatus, ['date','owner_name']),
            'payment_mix' => $this->uiOracleSortRows($tablePaymentMix, ['date','owner_name']),
            'payment_status' => $this->uiOracleSortRows($tablePaymentStatus, ['date','owner_name']),
            'conversion_status' => $this->uiOracleSortRows($tableConversionStatus, ['date','owner_name']),
            'conversion_time' => $this->uiOracleSortRows($tableConversionTime, ['date','owner_name']),
            'failure_reasons' => $this->uiOracleSortRows(array_values($failureRows), ['date','owner_name','failure_reason']),
            'summary_by_user' => $this->uiOracleSortRows($tableSummaryByUser, ['owner_name']),
            'product_status' => $this->uiOracleSortRows(array_values($productStatus), ['date','owner_name','product_name']),
            'product_sold_by_owner_day' => $this->uiOracleSortRows(array_values($productSold), ['date','owner_name','product_name']),
            'product_requests_by_owner_day' => $this->uiOracleSortRows(array_values($productReq), ['date','owner_name','product_name']),
            'product_failed_by_owner_day' => $this->uiOracleSortRows(array_values($productFailed), ['date','owner_name','product_name']),
        ];

        foreach ($overrides as $key => $rows) {
            $tables[$key] = $rows;
        }
        return $tables;
    }

    private function uiOracleOwnerNames(array $ownerIds): array
    {
        $ownerIds = array_values(array_filter(array_map('strval', $ownerIds), static fn($v) => $v !== ''));
        if (empty($ownerIds)) {
            return [];
        }
        $placeholders = implode(',', array_fill(0, count($ownerIds), '?'));
        $rows = $this->runSelectAll("SELECT didar_user_id, display_name FROM users WHERE didar_user_id IN ({$placeholders})", $ownerIds);
        $out = [];
        foreach ($rows as $row) {
            $out[(string)$row['didar_user_id']] = (string)($row['display_name'] ?? 'بدون مسئول');
        }
        return $out;
    }

    private function uiOracleProductNames(): array
    {
        $rows = $this->runSelectAll("SELECT id, name FROM products");
        $out = [];
        foreach ($rows as $row) {
            $out[(string)$row['id']] = (string)($row['name'] ?? '');
        }
        return $out;
    }

    private function uiOraclePrimaryProductIdForDeal(array $deal, array $dealProducts): ?string
    {
        $dealId = (string)($deal['id'] ?? '');
        if ($dealId === '' || empty($dealProducts[$dealId])) {
            return null;
        }
        $ids = array_keys($dealProducts[$dealId]);
        sort($ids, SORT_NATURAL);
        return (string)($ids[0] ?? '');
    }

    private function uiOracleResolvedProductName(array $deal, ?string $productId, array $productNames): string
    {
        $hint = '';
        $title = trim((string)($deal['title'] ?? ''));
        if ($title !== '' && strpos($title, '|') !== false) {
            $parts = array_map('trim', explode('|', $title));
            $hint = (string)end($parts);
        }
        if ($hint === '') {
            $requested = trim((string)($deal['requested_services'] ?? ''));
            if ($requested !== '') {
                $pieces = preg_split('/[,،]/u', $requested);
                $hint = trim((string)($pieces[0] ?? ''));
            }
        }
        $name = $hint !== '' ? $hint : (string)($productNames[(string)$productId] ?? '');
        return $this->normalizeProductName($name);
    }

    private function normalizeProductName(string $name): string
    {
        $name = trim($name);
        if ($name === '') {
            return '';
        }
        $lower = function_exists('mb_strtolower') ? mb_strtolower($name, 'UTF-8') : strtolower($name);
        $containsVipChannel = function_exists('mb_strpos')
            ? (mb_strpos($lower, 'کانال vip') !== false)
            : (stripos($lower, 'vip') !== false && (strpos($name, 'کانال') !== false));
        if ($lower === 'vip' || $containsVipChannel) {
            return 'کانال VIP';
        }
        return $name;
    }

    private function uiOracleFailureReasonFromDeal(array $deal): string
    {
        $candidates = [
            trim((string)($deal['failure_reason_text'] ?? '')),
            trim((string)($deal['failure_reason_description'] ?? '')),
        ];
        foreach ($candidates as $candidate) {
            if ($candidate !== '') {
                return $candidate;
            }
        }
        $code = trim((string)($deal['failure_reason_code'] ?? ''));
        $map = [
            'no_financial_ability' => 'عدم توان مالی',
            'no_purchase_intent' => 'فعلا قصد خرید ندارد',
            'competitor_purchase' => 'خرید از رقبا',
            'wrong_number' => 'شماره اشتباه',
            'irrelevant_lead' => 'لید نامرتبط',
            'other' => 'سایر',
        ];
        if ($code !== '' && isset($map[$code])) {
            return $map[$code];
        }
        return $code !== '' ? $code : 'نامشخص';
    }

    private function buildLostReasonBreakdownFromOracle(array $oracle): array
    {
        $state = (array)($oracle['state'] ?? []);
        $deals = (array)($state['deals'] ?? []);
        $lostSet = (array)($oracle['sets']['partition_lost_valid'] ?? []);
        $buckets = [];

        foreach (array_keys($lostSet) as $dealId) {
            $deal = $deals[(string)$dealId] ?? null;
            if (!is_array($deal)) {
                continue;
            }
            $reason = $this->uiOracleFailureReasonFromDeal($deal);
            if ($reason === '') {
                $reason = 'نامشخص';
            }
            if (!isset($buckets[$reason])) {
                $buckets[$reason] = [
                    'reason' => $reason,
                    'count' => 0,
                    'total_amount' => 0.0,
                ];
            }
            $buckets[$reason]['count']++;
            $buckets[$reason]['total_amount'] += (float)($deal['payable_amount'] ?? 0);
        }

        $rows = array_values($buckets);
        usort($rows, static function ($a, $b) {
            return ((int)($b['count'] ?? 0)) <=> ((int)($a['count'] ?? 0));
        });
        return $rows;
    }

    private function uiOracleSortRows(array $rows, array $keys): array
    {
        usort($rows, function (array $a, array $b) use ($keys) {
            foreach ($keys as $key) {
                $av = (string)($a[$key] ?? '');
                $bv = (string)($b[$key] ?? '');
                $cmp = strcmp($av, $bv);
                if ($cmp !== 0) {
                    return $cmp;
                }
            }
            return 0;
        });
        return $rows;
    }

    private function runSelectAll(string $sql, array $params = []): array
    {
        $stmt = $this->db->getPdo()->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function normalizeCsvList($raw): string
    {
        if (is_array($raw)) {
            $items = $raw;
        } else {
            $raw = trim((string)$raw);
            if ($raw === '') {
                return '';
            }
            $items = explode(',', $raw);
        }

        $normalized = [];
        foreach ($items as $item) {
            $value = strtolower(trim((string)$item));
            if ($value === '' || $value === 'all') {
                continue;
            }
            $normalized[$value] = true;
        }

        return implode(',', array_keys($normalized));
    }

    private function csvHasValue(string $csv, string $needle): bool
    {
        if ($csv === '') {
            return false;
        }
        $values = explode(',', $csv);
        return in_array(strtolower($needle), $values, true);
    }

    private function csvHasAll(string $csv, array $needles): bool
    {
        if ($csv === '') {
            return false;
        }
        $values = explode(',', $csv);
        foreach ($needles as $needle) {
            if (!in_array(strtolower((string)$needle), $values, true)) {
                return false;
            }
        }
        return true;
    }
}
