<?php
namespace App\Controllers;

use App\Services\DealService;
use App\Services\VirtualStageService;
use App\Repositories\PersonRepository;
use App\Repositories\DealRepository;
use App\Services\AuthService;
use App\Services\AuditService;
use App\Utils\Sanitizer;
use App\Utils\Logger;

class DealController extends ApiController
{
    private $dealService;
    private $virtualStageService;
    private $personRepo;
    private $dealRepo;
    private $authService;
    private $auditService;
    private $db;

    private function canRbac(string $capabilityKey): bool
    {
        global $app;
        if (isset($app->services->rbac) && $app->services->rbac) {
            return (bool)$app->services->rbac->can($capabilityKey);
        }
        return false;
    }

    private function requireRbac(string $capabilityKey, string $message = 'دسترسی محدود'): void
    {
        if (!$this->canRbac($capabilityKey)) {
            $this->errorResponse($message, 403);
        }
    }

    private function getRbacScopeForResource(string $resourceKey): ?array
    {
        global $app;
        if (!isset($app->services->rbac) || !$app->services->rbac) {
            return null;
        }
        return (array)$app->services->rbac->getDataScope($resourceKey);
    }

    private function mapSelectedUserIdsToDidarIds(array $selectedUserIds): array
    {
        global $app;
        if (!isset($app->repositories->user) || !$app->repositories->user) {
            return [];
        }
        $didarIds = [];
        foreach ($selectedUserIds as $uid) {
            $userId = (int)$uid;
            if ($userId <= 0) continue;
            try {
                $user = $app->repositories->user->findById($userId);
                $didar = trim((string)($user->didar_user_id ?? ''));
                if ($didar !== '') {
                    $didarIds[$didar] = true;
                }
            } catch (\Throwable $e) {
                // Ignore invalid selected users.
            }
        }
        return array_keys($didarIds);
    }

    private function applyOwnerScopeToFilters(array &$filters, string $resourceKey, string $requestedOwnerDidarId, string $selfDidarId): void
    {
        global $app;
        if (isset($app->services->rbac) && $app->services->rbac) {
            $filters = $app->services->rbac->applyOwnerScope($filters, $resourceKey, $requestedOwnerDidarId);
        }
    }

    private function assertDealInScope($deal): void
    {
        $ownerDidarId = trim((string)($deal->owner_didar_id ?? ''));
        global $app;
        if (!isset($app->services->rbac) || !$app->services->rbac || !$app->services->rbac->isOwnerInScope('deals', $ownerDidarId)) {
            $this->errorResponse('دسترسی به این معامله در محدوده مجاز شما نیست', 403);
        }
    }

    private function assertOwnerInDealScopeByDidar(string $ownerDidarId): void
    {
        global $app;
        if (!isset($app->services->rbac) || !$app->services->rbac || !$app->services->rbac->isOwnerInScope('deals', $ownerDidarId)) {
            $this->errorResponse('دسترسی به این معامله در محدوده مجاز شما نیست', 403);
        }
    }

    private function normalizePaymentForPermissionCompare(array $payment): array
    {
        return [
            'amount' => (float)($payment['amount'] ?? 0),
            'date' => trim((string)($payment['date'] ?? '')),
            'date_persian' => trim((string)($payment['date_persian'] ?? '')),
            'method' => trim((string)($payment['method'] ?? $payment['payment_method'] ?? '')),
            'description' => trim((string)($payment['description'] ?? '')),
            'card_last4' => trim((string)($payment['card_last4'] ?? $payment['card_last_4'] ?? '')),
            'destination_bank' => trim((string)($payment['destination_bank'] ?? $payment['bank_destination'] ?? '')),
            'tracking_code_last4' => trim((string)($payment['tracking_code_last4'] ?? $payment['tracking_code'] ?? '')),
            'tracking_code' => trim((string)($payment['tracking_code'] ?? '')),
            'number' => trim((string)($payment['number'] ?? '')),
            'check_number' => trim((string)($payment['check_number'] ?? '')),
            'check_bank_name' => trim((string)($payment['check_bank_name'] ?? '')),
            'check_owner_name' => trim((string)($payment['check_owner_name'] ?? '')),
        ];
    }

    private function hasPositivePayments(array $payments): bool
    {
        foreach ($payments as $payment) {
            if ((float)($payment['amount'] ?? 0) > 0) {
                return true;
            }
        }
        return false;
    }

    private function getPaidDealAutoPipelineStage(float $payableAmount, float $paymentAmount): string
    {
        if ($paymentAmount <= 0) {
            return '';
        }
        if ($payableAmount > 0 && $paymentAmount >= $payableAmount) {
            return 'پرداخت';
        }
        return 'پیگیری مانده حساب';
    }

    private function moveContactOutOfKarizOnSuccessfulDeal(string $contactId, string $dealId = ''): void
    {
        if ($contactId === '') {
            return;
        }

        try {
            $this->virtualStageService->update($contactId, 'with_purchase', true);
            Logger::logInfo('Moved contact out of kariz after successful deal', [
                'contact_id' => $contactId,
                'deal_id' => $dealId,
                'target_stage' => 'with_purchase',
            ]);
        } catch (\Throwable $e) {
            Logger::logWarning('Failed to move contact out of kariz after successful deal', [
                'contact_id' => $contactId,
                'deal_id' => $dealId,
                'error' => $e->getMessage(),
            ]);
        }
    }

    private function syncSuccessfulDealSideEffects(string $dealId, string $contactId): void
    {
        if ($contactId === '') {
            return;
        }

        try {
            $this->dealService->updatePipelineStage($dealId, 'اتمام');
        } catch (\Throwable $e) {
            Logger::logWarning('Failed to sync pipeline_stage for successful deal', [
                'deal_id' => $dealId,
                'contact_id' => $contactId,
                'error' => $e->getMessage(),
            ]);
        }

        try {
            $person = $this->personRepo->findByDidarId($contactId);
            if ($person) {
                $this->personRepo->updateSaleStatus($contactId, 1);
            }
        } catch (\Throwable $e) {
            Logger::logWarning('Failed to sync sale_status for successful deal', [
                'deal_id' => $dealId,
                'contact_id' => $contactId,
                'error' => $e->getMessage(),
            ]);
        }

        try {
            $products = $this->dealRepo->getProductsByDealDidarId($dealId);
            $productIds = array_values(array_filter(array_map(static function ($p) {
                return $p['product_id'] ?? null;
            }, $products)));
            if (!empty($productIds)) {
                $this->personRepo->appendProducts($contactId, $productIds);
            }
        } catch (\Throwable $e) {
            Logger::logWarning('Failed to sync successful deal products', [
                'deal_id' => $dealId,
                'contact_id' => $contactId,
                'error' => $e->getMessage(),
            ]);
        }

        $this->moveContactOutOfKarizOnSuccessfulDeal($contactId, $dealId);
    }

    private function syncFailedDealSideEffects(string $dealId, string $contactId): void
    {
        if ($contactId === '') {
            return;
        }

        try {
            $this->dealService->updatePipelineStage($dealId, 'اتمام');
        } catch (\Throwable $e) {
            Logger::logWarning('Failed to sync pipeline_stage for failed deal', [
                'deal_id' => $dealId,
                'contact_id' => $contactId,
                'error' => $e->getMessage(),
            ]);
        }

        try {
            $this->virtualStageService->update($contactId, 'without_purchase', true);
            Logger::logInfo('Moved contact out of kariz after failed deal', [
                'contact_id' => $contactId,
                'deal_id' => $dealId,
                'target_stage' => 'without_purchase',
            ]);
        } catch (\Throwable $e) {
            Logger::logWarning('Failed to move contact out of kariz after failed deal', [
                'contact_id' => $contactId,
                'deal_id' => $dealId,
                'error' => $e->getMessage(),
            ]);
        }
    }

    private function calculateOutstandingFromDeal($deal): float
    {
        if (!$deal) {
            return 0.0;
        }

        $payableAmount = (float)($deal->payable_amount ?? 0);
        $paidAmount = 0.0;

        $payments = json_decode((string)($deal->payments ?? ''), true);
        if (is_array($payments)) {
            foreach ($payments as $payment) {
                $paidAmount += (float)($payment['amount'] ?? 0);
            }
        } else {
            $paidAmount = (float)($deal->payment_amount ?? 0);
        }

        $outstanding = $payableAmount - $paidAmount;
        return $outstanding > 0 ? $outstanding : 0.0;
    }

    private function calculatePaymentTotalFromDeal($deal): float
    {
        if (!$deal) {
            return 0.0;
        }

        $paidAmount = 0.0;
        $payments = json_decode((string)($deal->payments ?? ''), true);
        if (is_array($payments)) {
            foreach ($payments as $payment) {
                $paidAmount += (float)($payment['amount'] ?? 0);
            }
            return $paidAmount;
        }

        return (float)($deal->payment_amount ?? 0);
    }

    private function getSuccessRequiredPaymentAmount($deal): float
    {
        if (!$deal) {
            return 0.0;
        }

        if (property_exists($deal, 'payable_amount') && $deal->payable_amount !== null) {
            return max(0.0, (float)$deal->payable_amount);
        }

        return max(0.0, (float)($deal->service_cost ?? 0));
    }

    private function assertDealCanBeMarkedWon($deal): void
    {
        if (!$deal) {
            $this->errorResponse('معامله یافت نشد');
        }

        $requiredAmount = $this->getSuccessRequiredPaymentAmount($deal);
        $paidAmount = $this->calculatePaymentTotalFromDeal($deal);
        if ($requiredAmount > 0 && ($paidAmount + 0.0001) < $requiredAmount) {
            $this->errorResponse('برای موفق شدن معامله، مجموع پرداخت‌ها باید حداقل برابر مبلغ قابل پرداخت باشد');
        }
    }

    private function resolveAllowedServicePricing(string $serviceTitle): array
    {
        $serviceTitle = trim($serviceTitle);
        if ($serviceTitle === '') {
            return ['service_cost' => null, 'price_list_code' => ''];
        }

        try {
            $items = $this->dealService->getLookupItems('allowed_services', true);
        } catch (\Throwable $e) {
            $items = [];
        }

        foreach ((array)$items as $item) {
            $value = trim((string)($item['value'] ?? $item['title'] ?? ''));
            $title = trim((string)($item['title'] ?? $item['value'] ?? ''));
            if ($serviceTitle !== $value && $serviceTitle !== $title) {
                continue;
            }

            $meta = [];
            if (!empty($item['meta_json'])) {
                try {
                    $meta = json_decode((string)$item['meta_json'], true, 512, JSON_THROW_ON_ERROR);
                } catch (\Throwable $e) {
                    $meta = [];
                }
            }

            $rawPrice = $meta['price'] ?? null;
            $serviceCost = null;
            if ($rawPrice !== null && $rawPrice !== '') {
                $normalized = preg_replace('/[^\d.]/', '', str_replace(',', '', (string)$rawPrice));
                $serviceCost = (float)($normalized !== '' ? $normalized : 0);
            }

            return [
                'service_cost' => ($serviceCost !== null && $serviceCost > 0) ? $serviceCost : null,
                'price_list_code' => trim((string)($meta['price_list_code'] ?? '')),
            ];
        }

        return ['service_cost' => null, 'price_list_code' => ''];
    }

    private function restorePaidDealLockedFields(array &$data, $existingDeal, array $existingRequestedServices, array $existingProducts): void
    {
        if (!$existingDeal) {
            return;
        }

        $data['title'] = (string)($existingDeal->title ?? $data['title'] ?? '');
        $data['requested_services'] = $existingRequestedServices;
        $data['products'] = $existingProducts;
        $data['service_cost'] = (float)($existingDeal->service_cost ?? 0);
        $data['estimated_price'] = (float)($existingDeal->estimated_price ?? ($data['service_cost'] ?? 0));
        $data['final_price'] = (float)($existingDeal->final_price ?? ($existingDeal->payable_amount ?? 0));
        $data['price_list_code'] = (string)($existingDeal->price_list_code ?? '');
        $data['has_discount'] = (int)($existingDeal->has_discount ?? 0);
        $data['discount_type'] = (string)($existingDeal->discount_type ?? '');
        $data['discount_occasion'] = (string)($existingDeal->discount_occasion ?? '');
        $data['discount_amount'] = (float)($existingDeal->discount_amount ?? 0);
        $data['refund_amount'] = (float)($existingDeal->refund_amount ?? 0);
        $data['refund_description'] = (string)($existingDeal->refund_description ?? '');
        $data['payable_amount'] = (float)($existingDeal->payable_amount ?? 0);
    }

    private function detectPaymentPermissionChanges(array $existingPayments, array $validatedPayments): array
    {
        $changes = ['create' => false, 'edit' => false, 'delete' => false];
        $existingCount = count($existingPayments);
        $newCount = count($validatedPayments);
        if ($newCount > $existingCount) $changes['create'] = true;
        if ($newCount < $existingCount) $changes['delete'] = true;

        $compareCount = min($existingCount, $newCount);
        for ($i = 0; $i < $compareCount; $i++) {
            $old = $this->normalizePaymentForPermissionCompare((array)($existingPayments[$i] ?? []));
            $new = $this->normalizePaymentForPermissionCompare((array)($validatedPayments[$i] ?? []));
            if ($old !== $new) {
                $changes['edit'] = true;
                break;
            }
        }
        return $changes;
    }

    private function normalizeDealPermissionValue(string $key, $value)
    {
        if (in_array($key, ['service_cost', 'payable_amount', 'discount_amount', 'refund_amount'], true)) {
            return round((float)$value, 2);
        }
        if ($key === 'status') {
            $s = trim((string)$value);
            if ($s === '') return '';
            return strtolower($s);
        }
        if (in_array($key, ['has_discount', 'has_previous_purchase'], true)) {
            if ($value === null || $value === '') {
                return null;
            }
            return (int)$value;
        }
        if ($key === 'products') {
            $arr = is_array($value) ? $value : [];
            $arr = array_values(array_filter(array_map(static function ($v) {
                $s = trim((string)$v);
                return $s === '' ? null : $s;
            }, $arr)));
            sort($arr, SORT_NATURAL);
            return $arr;
        }
        if ($key === 'requested_services') {
            $arr = is_array($value) ? $value : [];
            $arr = array_values(array_filter(array_map(static function ($v) {
                $s = preg_replace('/\s+/', ' ', trim((string)$v));
                return $s === '' ? null : $s;
            }, $arr)));
            sort($arr, SORT_NATURAL);
            return $arr;
        }
        if ($value === null) return null;
        return trim((string)$value);
    }

    private function hasNonPaymentDealChanges($existingDeal, array $data, array $ignoreKeys = []): bool
    {
        if (!$existingDeal) return true;

        $existingRequested = [];
        if (!empty($existingDeal->requested_services)) {
            $decoded = json_decode((string)$existingDeal->requested_services, true);
            if (is_array($decoded)) {
                $existingRequested = $decoded;
            }
        }

        $existingProducts = [];
        try {
            $rows = $this->dealRepo->getProductsByDealDidarId((string)$existingDeal->didar_deal_id);
            foreach ((array)$rows as $r) {
                if (isset($r['product_id'])) {
                    $existingProducts[] = (string)$r['product_id'];
                }
            }
        } catch (\Throwable $e) {
            return true;
        }

        $compareMap = [
            'title' => $existingDeal->title ?? null,
            'status' => $existingDeal->status ?? null,
            'pipeline_stage' => $existingDeal->pipeline_stage ?? null,
            'service_cost' => $existingDeal->service_cost ?? null,
            'price_list_code' => $existingDeal->price_list_code ?? null,
            'payable_amount' => $existingDeal->payable_amount ?? null,
            'payment_conditions' => $existingDeal->payment_conditions ?? null,
            'has_discount' => $existingDeal->has_discount ?? null,
            'discount_type' => $existingDeal->discount_type ?? null,
            'discount_occasion' => $existingDeal->discount_occasion ?? null,
            'discount_amount' => $existingDeal->discount_amount ?? null,
            'refund_amount' => $existingDeal->refund_amount ?? null,
            'refund_description' => $existingDeal->refund_description ?? null,
            'financial_level' => $existingDeal->financial_level ?? null,
            'asset_estimation' => $existingDeal->asset_estimation ?? null,
            'income_estimation' => $existingDeal->income_estimation ?? null,
            'has_previous_purchase' => $existingDeal->has_previous_purchase ?? null,
            'failure_reason_code' => $existingDeal->failure_reason_code ?? null,
            'failure_reason_text' => $existingDeal->failure_reason_text ?? null,
            'failure_reason_description' => $existingDeal->failure_reason_description ?? null,
            'acquaintance_source' => $existingDeal->acquaintance_source ?? null,
            'acquaintance_detail' => $existingDeal->acquaintance_detail ?? null,
            'content_topic' => $existingDeal->content_topic ?? null,
            'requested_services' => $existingRequested,
            'products' => $existingProducts,
        ];

        foreach ($compareMap as $key => $oldVal) {
            if (in_array($key, $ignoreKeys, true)) {
                continue;
            }
            $newVal = $data[$key] ?? ($key === 'products' ? [] : null);
            if ($this->normalizeDealPermissionValue($key, $oldVal) !== $this->normalizeDealPermissionValue($key, $newVal)) {
                return true;
            }
        }
        return false;
    }

    public function __construct(
        DealService $dealService,
        VirtualStageService $virtualStageService,
        PersonRepository $personRepo,
        DealRepository $dealRepo,
        AuthService $authService,
        AuditService $auditService,
        $db = null
    ) {
        $this->dealService = $dealService;
        $this->virtualStageService = $virtualStageService;
        $this->personRepo = $personRepo;
        $this->dealRepo = $dealRepo;
        $this->authService = $authService;
        $this->auditService = $auditService;
        $this->db = $db;
    }

    public function getDeals()
    {
        $this->requireRbac('deals.view', 'دسترسی محدود - شما مجاز به مشاهده معاملات نیستید');
        $isAdmin = $this->authService->isAdmin();
        $isAdminOrCrm = $this->authService->isAdminOrCrmSpecialist();
        $didarId = $_SESSION['didar_id'] ?? '';
        $requestedOwnerId = Sanitizer::sanitize($_POST['owner_id'] ?? '');
        
        $filters = [];
        if (!$isAdminOrCrm && !empty($didarId)) {
            $filters['owner_didar_id'] = $didarId;
        }

        // Admin/CRM can filter by owner
        if ($isAdminOrCrm && $requestedOwnerId) {
            $filters['owner_didar_id'] = $requestedOwnerId;
        }
        
        $status = Sanitizer::sanitize($_POST['status'] ?? 'all');
        $search = Sanitizer::sanitize($_POST['search'] ?? '');
        $customerCodeSearch = Sanitizer::sanitize($_POST['customer_code_search'] ?? '');
        $pipelineStage = Sanitizer::sanitize($_POST['pipeline_stage'] ?? 'all');
        $registerFrom = Sanitizer::sanitize($_POST['register_date_from'] ?? '');
        $registerTo = Sanitizer::sanitize($_POST['register_date_to'] ?? '');
        $activityFrom = Sanitizer::sanitize($_POST['activity_date_from'] ?? '');
        $activityTo = Sanitizer::sanitize($_POST['activity_date_to'] ?? '');
        $legacy = $_POST['legacy'] ?? '';
        $limit = isset($_POST['limit']) ? (int)$_POST['limit'] : null;
        $offset = isset($_POST['offset']) ? (int)$_POST['offset'] : null;
        $cursorTime = Sanitizer::sanitize($_POST['cursor_time'] ?? '');
        $cursorId = Sanitizer::sanitize($_POST['cursor_id'] ?? '');
        $useCursor = isset($_POST['use_cursor']) ? (int)$_POST['use_cursor'] === 1 : false;
        
        if ($status !== 'all') {
            $filters['status'] = $status;
        }
        
        if ($pipelineStage !== 'all') {
            $filters['pipeline_stage'] = $pipelineStage;
        }

        if ($registerFrom) {
            $filters['register_date_from'] = $registerFrom;
        }
        if ($registerTo) {
            $filters['register_date_to'] = $registerTo;
        }

        if ($activityFrom) {
            $filters['activity_date_from'] = $activityFrom;
        }
        if ($activityTo) {
            $filters['activity_date_to'] = $activityTo;
        }
        
        if ($search) {
            $filters['search'] = $search;
        }
        if ($customerCodeSearch) {
            $filters['customer_code_search'] = $customerCodeSearch;
        }

        if ($legacy !== '') {
            $filters['legacy'] = Sanitizer::sanitize($legacy);
        }
        if ($limit !== null) {
            $filters['limit'] = $limit;
        }
        if ($offset !== null) {
            $filters['offset'] = $offset;
        }
        if ($cursorTime) {
            $filters['cursor_time'] = $cursorTime;
        }
        if ($cursorId) {
            $filters['cursor_id'] = $cursorId;
        }
        if ($useCursor && empty($filters['cursor_time']) && $offset === null) {
            // Cursor mode can be explicitly requested without a cursor for first page.
            // Repository will simply return first page ordered by register_time.
            $filters['use_cursor'] = true;
        }

        // RBAC data scope (self/all/selected_users) overrides legacy owner visibility logic.
        $this->applyOwnerScopeToFilters($filters, 'deals', $requestedOwnerId, (string)$didarId);
        
        $deals = $this->dealService->getDeals($filters);
        // Attach products in batch (avoid N+1)
        $dealDidarIds = array_values(array_filter(array_map(function ($d) {
            return (string)($d['didar_deal_id'] ?? '');
        }, $deals)));
        $productsMap = $this->dealRepo->getProductsByDealDidarIds($dealDidarIds);
        $deals = array_map(function($d) use ($productsMap) {
            $key = (string)($d['didar_deal_id'] ?? '');
            $d['products'] = $productsMap[$key] ?? [];
            return $d;
        }, $deals);

        $pagination = [
            'mode' => ($cursorTime || $useCursor) ? 'cursor' : 'offset',
            'limit' => (int)($filters['limit'] ?? 100),
        ];
        if (!empty($deals)) {
            $last = $deals[count($deals) - 1];
            $pagination['next_cursor_time'] = substr((string)($last['register_time'] ?? ''), 0, 19);
            $pagination['next_cursor_id'] = (string)($last['didar_deal_id'] ?? '');
            $pagination['last_row_id'] = (int)($last['id'] ?? 0);
        } else {
            $pagination['next_cursor_time'] = '';
            $pagination['next_cursor_id'] = '';
            $pagination['last_row_id'] = 0;
        }
        $pagination['rows_count'] = count($deals);

        $this->successResponse(['deals' => $deals, 'pagination' => $pagination]);
    }

    private function logFieldChanges(string $entityType, string $entityId, array $oldData, array $newData): void
    {
        if (!$this->db) {
            \App\Utils\Logger::logError("DealController.logFieldChanges: DB connection not initialized");
            return;
        }
        $actorId = $_SESSION['didar_id'] ?? 'system';
        $actorName = $_SESSION['name'] ?? 'System';
        $pdo = $this->db->getPdo();
        
        $sql = "INSERT INTO field_audit_logs (entity_type, entity_id, field_name, old_value, new_value, actor_didar_id, actor_name) 
                VALUES (?, ?, ?, ?, ?, ?, ?)";
        $stmt = $pdo->prepare($sql);
        
        foreach ($newData as $field => $newValue) {
            $oldValue = $oldData[$field] ?? null;
            
            // Normalize values for comparison
            $normOld = is_array($oldValue) ? json_encode($oldValue) : (string)$oldValue;
            $normNew = is_array($newValue) ? json_encode($newValue) : (string)$newValue;
            
            if ($normOld !== $normNew) {
                $stmt->execute([
                    $entityType,
                    $entityId,
                    $field,
                    $normOld,
                    $normNew,
                    $actorId,
                    $actorName
                ]);
            }
        }
    }

    public function getLegacyDeals()
    {
        $this->requireRbac('deals.view', 'دسترسی محدود - شما مجاز به مشاهده معاملات نیستید');
        $contactId = Sanitizer::sanitize($_POST['contact_id'] ?? '');
        if (empty($contactId)) {
            $this->errorResponse('O\'U+OO3UØ O\'OrOæ OU,OýOU.UO OO3O¦');
        }

        $person = $this->personRepo->findByIdOrDidarId($contactId);
        if (!$person) {
            $this->errorResponse('O\'OrOæ UOOU?O¦ U+O\'O_');
        }
        $this->assertOwnerInDealScopeByDidar((string)($person->owner_didar_id ?? ''));

        $legacyDeals = $this->dealRepo->findByContactIdLegacy($person->didar_contact_id, true);
        $legacyArray = array_map(function($deal) {
            return $deal->toArray();
        }, $legacyDeals);

        $this->successResponse(['deals' => $legacyArray]);
    }

    public function save()
    {
        $products = [];
        if (!empty($_POST['product_ids'])) {
            if (is_array($_POST['product_ids'])) {
                $products = $_POST['product_ids'];
            } else {
                # comma separated
                $products = array_filter(array_map('trim', explode(',', $_POST['product_ids'])));
            }
        }

        # Requested services (can be array, JSON string, or comma separated)
        $requestedServices = [];
        if (isset($_POST['requested_services'])) {
            if (is_array($_POST['requested_services'])) {
                $requestedServices = $_POST['requested_services'];
            } else {
                $raw = trim((string)$_POST['requested_services']);
                if ($raw != '') {
                    $decoded = json_decode($raw, true);
                    if (is_array($decoded)) {
                        $requestedServices = $decoded;
                    } else {
                        $requestedServices = array_filter(array_map('trim', explode(',', $raw)));
                    }
                }
            }
        }
        // اگر هنوز خالی است ولی عنوان خدمات قابل ارائه موجود است، آن را هم به عنوان خدمت درخواستی اضافه کن
        if (empty($requestedServices) && !empty($_POST['title'])) {
            $requestedServices = [Sanitizer::sanitize($_POST['title'])];
        }

        $isNewDeal = empty($_POST['deal_id']);
        if ($isNewDeal) {
            $this->requireRbac('deals.create', 'دسترسی محدود - شما مجاز به ایجاد معامله نیستید');
        } else {
            $this->requireRbac('deals.edit', 'دسترسی محدود - شما مجاز به ویرایش معامله نیستید');
        }
        \App\Utils\Logger::logInfo('DealController.save start', [
            'is_new' => $isNewDeal,
            'incoming_title' => $_POST['title'] ?? '',
            'contact_id' => $_POST['contact_id'] ?? '',
            'deal_id' => $_POST['deal_id'] ?? '',
            'user_id' => $_SESSION['didar_id'] ?? 'unknown',
            'user_name' => $_SESSION['name'] ?? 'unknown',
            'form_data_keys' => array_keys($_POST),
            'server_time' => date('Y-m-d H:i:s'),
            'session_id' => session_id()
        ]);
        $title = Sanitizer::sanitize($_POST['title'] ?? '');
        $rawContactId = Sanitizer::sanitize($_POST['contact_id'] ?? '');
        $resolvedPerson = $this->resolvePersonByContactId($rawContactId);
        $normalizedContactId = $resolvedPerson ? $resolvedPerson->didar_contact_id : $rawContactId;

        if ($resolvedPerson && empty(trim((string)($resolvedPerson->customer_code ?? ''))) && $this->personRepo) {
            $this->personRepo->assignCustomerCodeIfMissing($resolvedPerson->didar_contact_id);
            $resolvedPerson = $this->resolvePersonByContactId($rawContactId);
        }

        // For new deals, generate title automatically if not provided
        if ($isNewDeal && empty($title)) {
            try {
                if (!empty($normalizedContactId)) {
                    $title = $this->dealService->generateDealTitle($normalizedContactId);
                }
            } catch (\Exception $e) {
                \App\Utils\Logger::logError('Failed to generate deal title', $e, ['contact_id' => $_POST['contact_id'] ?? '']);
                // Continue with empty title - will be caught by validation
            }
        }

        if ($isNewDeal && $this->isGeneratedDealTitle($title)) {
            Logger::logWarning('Deal save blocked: generated title used as service', [
                'deal_title' => $title,
                'contact_id' => $_POST['contact_id'] ?? '',
                'deal_id' => $_POST['deal_id'] ?? '',
                'user_id' => $_SESSION['didar_id'] ?? 'unknown',
                'user_name' => $_SESSION['name'] ?? 'unknown',
                'server_time' => date('Y-m-d H:i:s')
            ]);
            $this->errorResponse("\u{0644}\u{0637}\u{0641}\u{0627}\u{064b} \u{062e}\u{062f}\u{0645}\u{0627}\u{062a} \u{0642}\u{0627}\u{0628}\u{0644} \u{0627}\u{0631}\u{0627}\u{0626}\u{0647} \u{0631}\u{0627} \u{0627}\u{0646}\u{062a}\u{062e}\u{0627}\u{0628} \u{06a9}\u{0646}\u{06cc}\u{062f}.");
        }

        // VALIDATION: Ensure deal phone matches contact's primary phone
        if (!empty($title) && !empty($_POST['contact_id'])) {
            Logger::logInfo('DEAL_PHONE_VALIDATION_START', [
                'deal_title' => $title,
                'contact_id' => $_POST['contact_id'],
                'is_new_deal' => $isNewDeal
            ]);

            $dealPhone = $this->extractPhoneFromDealTitle($title);
            $contactPrimaryPhone = $this->getContactPrimaryPhone($_POST['contact_id']);

            Logger::logInfo('DEAL_PHONE_VALIDATION_EXTRACTED', [
                'deal_phone' => $dealPhone,
                'contact_primary_phone' => $contactPrimaryPhone
            ]);

            if (!empty($dealPhone) && !empty($contactPrimaryPhone)) {
                // Normalize both phones for comparison
                $normalizedDealPhone = $this->normalizePhoneForComparison($dealPhone);
                $normalizedContactPhone = $this->normalizePhoneForComparison($contactPrimaryPhone);

                Logger::logInfo('DEAL_PHONE_VALIDATION_NORMALIZED', [
                    'normalized_deal_phone' => $normalizedDealPhone,
                    'normalized_contact_phone' => $normalizedContactPhone,
                    'phones_match' => $normalizedDealPhone === $normalizedContactPhone
                ]);

                if ($normalizedDealPhone !== $normalizedContactPhone) {
                    Logger::logWarning('DEAL_PHONE_VALIDATION_FAILED: Deal phone does not match contact primary phone', [
                        'deal_title' => $title,
                        'extracted_deal_phone' => $dealPhone,
                        'normalized_deal_phone' => $normalizedDealPhone,
                        'contact_primary_phone' => $contactPrimaryPhone,
                        'normalized_contact_phone' => $normalizedContactPhone,
                        'contact_id' => $_POST['contact_id'],
                        'user_id' => $_SESSION['didar_id'] ?? 'unknown',
                        'user_name' => $_SESSION['name'] ?? 'unknown',
                        'server_time' => date('Y-m-d H:i:s')
                    ]);
                    $this->errorResponse('شماره تماس معامله با شماره تماس اصلی لید مطابقت ندارد. لطفاً از شماره صحیح استفاده کنید.');
                }

                Logger::logInfo('DEAL_PHONE_VALIDATION_PASSED', [
                    'deal_title' => $title,
                    'deal_phone' => $dealPhone,
                    'contact_phone' => $contactPrimaryPhone,
                    'contact_id' => $_POST['contact_id'],
                    'validation' => 'PASSED'
                ]);
            } else {
                Logger::logInfo('DEAL_PHONE_VALIDATION_SKIPPED', [
                    'reason' => 'missing_deal_phone_or_contact_phone',
                    'deal_phone' => $dealPhone,
                    'contact_primary_phone' => $contactPrimaryPhone,
                    'contact_id' => $_POST['contact_id'] ?? '',
                    'deal_id' => $_POST['deal_id'] ?? '',
                    'is_new_deal' => $isNewDeal,
                    'incoming_title' => $title,
                    'user_id' => $_SESSION['didar_id'] ?? 'unknown',
                    'user_name' => $_SESSION['name'] ?? 'unknown',
                    'server_time' => date('Y-m-d H:i:s')
                ]);
            }
        }

        // Payments and financial fields (optional for new deals)
        $paymentsRaw = $_POST['payments'] ?? '';
        if (is_array($paymentsRaw)) {
            $paymentsRaw = json_encode($paymentsRaw, JSON_UNESCAPED_UNICODE);
        }
        $paymentsPayload = [];
        if (!empty($paymentsRaw)) {
            $decodedPayments = json_decode($paymentsRaw, true);
            if (is_array($decodedPayments)) {
                // Normalize payment keys to standard format before processing
                $paymentsPayload = array_map(function($payment) {
                    // Normalize card_last4 (support both card_last4 and card_last_4, use card_last4 as standard)
                    if (isset($payment['card_last_4']) && !isset($payment['card_last4'])) {
                        $payment['card_last4'] = $payment['card_last_4'];
                    }
                    // Normalize destination_bank (support both destination_bank and bank_destination, use destination_bank as standard)
                    if (isset($payment['bank_destination']) && !isset($payment['destination_bank'])) {
                        $payment['destination_bank'] = $payment['bank_destination'];
                    }
                    // Normalize method (support both method and payment_method, use method as standard)
                    if (isset($payment['payment_method']) && !isset($payment['method'])) {
                        $payment['method'] = $payment['payment_method'];
                    }
                    return $payment;
                }, $decodedPayments);
                
                // ✅ Validate payment fields based on field configuration
                // This respects field permissions and conditional requirements
                global $app;
                if (isset($app->services->fieldPermission)) {
                    $userRole = $_SESSION['role'] ?? 'agent';
                    $isExisting = !$isNewDeal;
                    
                    // Load existing deal for comparison if updating
                    $existingPayments = [];
                    $existingDealData = null;
                    if (!$isNewDeal) {
                        $existingDealData = $this->dealRepo->findByDidarId(Sanitizer::sanitize($_POST['deal_id']));
                        if ($existingDealData && !empty($existingDealData->payments)) {
                            $existingPaymentsRaw = json_decode($existingDealData->payments, true) ?: [];
                            // Normalize existing payments keys to standard format
                            $existingPayments = array_map(function($payment) {
                                // Normalize card_last4 (support both card_last4 and card_last_4, use card_last4 as standard)
                                if (isset($payment['card_last_4']) && !isset($payment['card_last4'])) {
                                    $payment['card_last4'] = $payment['card_last_4'];
                                }
                                // Normalize destination_bank (support both destination_bank and bank_destination, use destination_bank as standard)
                                if (isset($payment['bank_destination']) && !isset($payment['destination_bank'])) {
                                    $payment['destination_bank'] = $payment['bank_destination'];
                                }
                                // Normalize method (support both method and payment_method, use method as standard)
                                if (isset($payment['payment_method']) && !isset($payment['method'])) {
                                    $payment['method'] = $payment['payment_method'];
                                }
                                return $payment;
                            }, $existingPaymentsRaw);
                        }
                    }

                    // Check each payment record for field-level constraints
                    // Note: paymentsPayload and existingPayments are already normalized at this point
                    foreach ($paymentsPayload as $idx => $payment) {
                        $existingPayment = $existingPayments[$idx] ?? null;
                        $isNewPayment = ($existingPayment === null);

                        // Helper to check if a field has actually changed
                        // Since payments are normalized, we only need to check the standard key
                        $hasChanged = function($fieldName, $newValue) use ($existingPayment) {
                            if (!$existingPayment) return true; // New record
                            
                            // Get old value using standard key (payments are normalized)
                            $oldValue = $existingPayment[$fieldName] ?? null;
                            
                            // Normalize: treat null, empty string, and '0' as empty
                            $oldNormalized = ($oldValue === null || $oldValue === '' || $oldValue === '0') ? '' : trim((string)$oldValue);
                            $newNormalized = ($newValue === null || $newValue === '' || $newValue === '0') ? '' : trim((string)$newValue);
                            
                            return $oldNormalized !== $newNormalized;
                        };

                        // At this point, payment keys are already normalized, so use standard keys directly
                        $methodValue = $payment['method'] ?? '';
                        $cardValue   = $payment['card_last4'] ?? '';
                        $bankValue   = $payment['destination_bank'] ?? '';

                        // 1. Validate payment_method with editable_after_create support
                        if ($hasChanged('method', $methodValue) && !empty($methodValue)) {
                            if (!$app->services->fieldPermission->canUserEditField('payment_method', $userRole, !$isNewPayment)) {
                                \App\Utils\Logger::logWarning('DEAL_PAYMENT_METHOD_EDIT_DENIED', [
                                    'deal_id' => $_POST['deal_id'] ?? '',
                                    'payment_idx' => $idx,
                                    'user_role' => $userRole,
                                    'method_value' => $methodValue,
                                    'is_new_payment' => $isNewPayment,
                                    'old_value' => $existingPayment['method'] ?? null,
                                    'new_value' => $methodValue
                                ]);
                                $this->errorResponse("شما مجاز به تغییر نحوه پرداخت نیستید (ردیف " . ($idx + 1) . ")");
                            }
                        }

                        // 2. Validate conditional requirements using FieldPermissionService
                        // This replaces hardcoded card_last4 logic
                        $paymentDataForLogic = [
                            'payment_method' => $methodValue,
                            'method' => $methodValue,
                            'destination_bank' => $bankValue,
                            'card_last4' => $cardValue,
                            // Add keys that might be used in conditional logic strings
                            'payment_methods' => $methodValue,
                            'payment_destination_banks' => $bankValue
                        ];

                        // Check if card_last4 is required based on conditional logic in settings
                        if ($app->services->fieldPermission->checkConditionalVisibility('payment_card_last4', $paymentDataForLogic)) {
                            if ($app->services->fieldPermission->isFieldRequired('payment_card_last4') && empty($cardValue)) {
                                $this->errorResponse("4 رقم آخر کارت الزامی است (ردیف " . ($idx + 1) . ")");
                            }
                        }

                        // Check if destination_bank is required based on conditional logic in settings
                        if ($app->services->fieldPermission->checkConditionalVisibility('destination_bank', $paymentDataForLogic)) {
                            if ($app->services->fieldPermission->isFieldRequired('destination_bank') && empty($bankValue)) {
                                $this->errorResponse("انتخاب بانک مقصد الزامی است (ردیف " . ($idx + 1) . ")");
                            }
                        }
                        
                        // 3. Validate card_last4 with editable_after_create support
                        if ($hasChanged('card_last4', $cardValue) && !empty($cardValue)) {
                            if (!$app->services->fieldPermission->canUserEditField('payment_card_last4', $userRole, !$isNewPayment)) {
                                \App\Utils\Logger::logWarning('DEAL_PAYMENT_CARD_EDIT_DENIED', [
                                    'deal_id' => $_POST['deal_id'] ?? '',
                                    'payment_idx' => $idx,
                                    'user_role' => $userRole,
                                    'card_value' => $cardValue,
                                    'is_new_payment' => $isNewPayment,
                                    'old_value' => $existingPayment['card_last4'] ?? null,
                                    'new_value' => $cardValue
                                ]);
                                $this->errorResponse("شما مجاز به تغییر 4 رقم کارت نیستید (ردیف " . ($idx + 1) . ")");
                            }
                            
                            // Apply validation pattern from config if set, otherwise use default 4-digit regex
                            $config = $app->services->fieldPermission->getFieldConfig('payment_card_last4');
                            $pattern = $config['validation_pattern'] ?? '^\d{4}$';
                            if (!empty($pattern)) {
                                if (@preg_match('/' . str_replace('/', '\/', $pattern) . '/', $cardValue) === 0) {
                                    $this->errorResponse($config['validation_message'] ?? "قالب 4 رقم آخر کارت صحیح نیست (ردیف " . ($idx + 1) . ")");
                                }
                            }
                        }
                        
                        // 4. Validate destination_bank with editable_after_create support
                        if ($hasChanged('destination_bank', $bankValue) && !empty($bankValue)) {
                            if (!$app->services->fieldPermission->canUserEditField('destination_bank', $userRole, !$isNewPayment)) {
                                \App\Utils\Logger::logWarning('DEAL_PAYMENT_BANK_EDIT_DENIED', [
                                    'deal_id' => $_POST['deal_id'] ?? '',
                                    'payment_idx' => $idx,
                                    'user_role' => $userRole,
                                    'bank_value' => $bankValue,
                                    'is_new_payment' => $isNewPayment,
                                    'old_value' => $existingPayment['destination_bank'] ?? null,
                                    'new_value' => $bankValue,
                                    'config_found' => $app->services->fieldPermission->getFieldConfig('destination_bank')
                                ]);
                                $this->errorResponse("شما مجاز به تغییر بانک مقصد نیستید (ردیف " . ($idx + 1) . ")");
                            }
                        }
                    }

                    // Prevent deletion of payments without permission (use canUserDeleteField)
                    if (count($existingPayments) > count($paymentsPayload)) {
                        if (!$app->services->fieldPermission->canUserDeleteField('payment_delete', $userRole)) {
                            $this->errorResponse('شما مجاز به حذف پرداخت‌ها نیستید');
                        }
                    }
                }
            }
        }

        $statusInput = Sanitizer::sanitize($_POST['deal_status'] ?? '');
        $pipelineStageInput = Sanitizer::sanitize($_POST['pipeline_stage'] ?? '');
        $stageMeansSuccess = in_array($pipelineStageInput, ['deal_success', 'اتمام']);
        $stageMeansFailed = in_array($pipelineStageInput, ['deal_failed', 'معامله ناموفق']);
        if ($stageMeansSuccess) {
            $statusInput = 'won';
            $pipelineStageInput = 'اتمام';
        } elseif ($stageMeansFailed) {
            $statusInput = 'lost';
            $pipelineStageInput = 'اتمام';
        }
        
        // Lost deals are closed and removed from the active kariz.
        if ($statusInput === 'lost' && empty($pipelineStageInput)) {
            $pipelineStageInput = 'اتمام';
        }
        
        $serviceCostInput = $_POST['service_cost'] ?? null;
        $priceListCodeInput = Sanitizer::sanitize($_POST['price_list_code'] ?? '');
        $payableAmountInput = $_POST['payable_amount'] ?? null;
        $paymentConditionsInput = Sanitizer::sanitize($_POST['payment_conditions'] ?? '');
        $dealAcquaintanceSourceInput = Sanitizer::sanitize($_POST['acquaintance_source'] ?? '');
        $dealAcquaintanceDetailInput = Sanitizer::sanitize($_POST['acquaintance_detail'] ?? '');
        $dealContentTopicInput = Sanitizer::sanitize($_POST['content_topic'] ?? '');
        $fromDuplicateLeadModal = !empty($_POST['from_duplicate_lead_modal']);

        $normalizeMoney = function ($val): float {
            $s = is_string($val) ? $val : (string)($val ?? '');
            $s = str_replace(',', '', $s);
            $s = preg_replace('/[^\d.]/', '', $s);
            return floatval($s ?: 0);
        };

        $refundAmountInput = $normalizeMoney($_POST['refund_amount'] ?? 0);
        $refundDescriptionInput = Sanitizer::sanitize($_POST['refund_description'] ?? '');
        $hasDiscountInput = intval($_POST['has_discount'] ?? 0);
        $discountTypeInput = Sanitizer::sanitize($_POST['discount_type'] ?? '');
        $discountOccasionInput = Sanitizer::sanitize($_POST['discount_occasion'] ?? '');
        $discountAmountInput = $normalizeMoney($_POST['discount_amount'] ?? 0);

        $existingDeal = null;
        $incomingDealId = Sanitizer::sanitize($_POST['deal_id'] ?? '');
        if (!$isNewDeal && $incomingDealId !== '') {
            $existingDeal = $this->dealRepo->findByDidarId($incomingDealId);
        }
        if (!$isNewDeal && $existingDeal) {
            if (!array_key_exists('acquaintance_source', $_POST)) {
                $dealAcquaintanceSourceInput = (string)($existingDeal->acquaintance_source ?? '');
            }
            if (!array_key_exists('acquaintance_detail', $_POST)) {
                $dealAcquaintanceDetailInput = (string)($existingDeal->acquaintance_detail ?? '');
            }
            if (!array_key_exists('content_topic', $_POST)) {
                $dealContentTopicInput = (string)($existingDeal->content_topic ?? '');
            }
        }
        if ($isNewDeal && $fromDuplicateLeadModal && $resolvedPerson) {
            if ($dealAcquaintanceSourceInput === '') {
                $dealAcquaintanceSourceInput = (string)($resolvedPerson->acquaintance_source ?? '');
            }
            if ($dealAcquaintanceDetailInput === '') {
                $dealAcquaintanceDetailInput = (string)($resolvedPerson->acquaintance_detail ?? '');
            }
            if ($dealContentTopicInput === '') {
                $dealContentTopicInput = (string)($resolvedPerson->content_topic ?? '');
            }
        }

        $normalizeServiceForPricing = function($value): string {
            $s = preg_replace('/\s+/', ' ', trim((string)$value));
            return $s ?? '';
        };
        $existingServiceForPricing = '';
        if ($existingDeal && !empty($existingDeal->requested_services)) {
            $decodedExistingServices = json_decode((string)$existingDeal->requested_services, true);
            if (is_array($decodedExistingServices) && !empty($decodedExistingServices)) {
                $existingServiceForPricing = $normalizeServiceForPricing($decodedExistingServices[0] ?? '');
            }
        }
        if ($existingServiceForPricing === '' && $existingDeal && !empty($existingDeal->title)) {
            $existingTitleForPricing = (string)$existingDeal->title;
            $existingServiceForPricing = strpos($existingTitleForPricing, '|') !== false
                ? $normalizeServiceForPricing(substr($existingTitleForPricing, strrpos($existingTitleForPricing, '|') + 1))
                : $normalizeServiceForPricing($existingTitleForPricing);
        }
        $incomingServiceForPricing = $normalizeServiceForPricing($title);
        $serviceChangedForPricing = $existingDeal && $existingServiceForPricing !== '' && $incomingServiceForPricing !== ''
            ? $existingServiceForPricing !== $incomingServiceForPricing
            : false;

        $resolvedPricing = ($isNewDeal || $serviceChangedForPricing)
            ? $this->resolveAllowedServicePricing($title)
            : ['service_cost' => null, 'price_list_code' => ''];
        $resolvedServiceCost = $resolvedPricing['service_cost'];
        $resolvedPriceListCode = (string)($resolvedPricing['price_list_code'] ?? '');
        if ($resolvedServiceCost !== null) {
            $serviceCostInput = $resolvedServiceCost;
        }
        if ($resolvedPriceListCode !== '') {
            $priceListCodeInput = $resolvedPriceListCode;
        }
        $effectiveDiscountAmount = ($hasDiscountInput === 1 && $discountTypeInput !== 'refund') ? $discountAmountInput : 0;
        if ($resolvedServiceCost !== null) {
            $payableAmountInput = max(0, $resolvedServiceCost - $effectiveDiscountAmount);
        }
        if (!$isNewDeal && $existingDeal && $resolvedServiceCost === null) {
            $incomingServiceCost = $normalizeMoney($serviceCostInput);
            $incomingPayableAmount = $normalizeMoney($payableAmountInput);
            $existingServiceCost = (float)($existingDeal->service_cost ?? 0);
            $existingPayableAmount = (float)($existingDeal->payable_amount ?? 0);
            if ($incomingServiceCost <= 0 && $existingServiceCost > 0) {
                $serviceCostInput = $existingServiceCost;
            }
            if ($incomingPayableAmount <= 0 && $existingPayableAmount > 0) {
                $payableAmountInput = $existingPayableAmount;
            }
            if ($priceListCodeInput === '' && trim((string)($existingDeal->price_list_code ?? '')) !== '') {
                $priceListCodeInput = (string)$existingDeal->price_list_code;
            }
            if ($incomingServiceCost <= 0 || $incomingPayableAmount <= 0 || $resolvedPriceListCode === '') {
                \App\Utils\Logger::logWarning('Preserved existing deal pricing because lookup pricing was unresolved', [
                    'deal_id' => $incomingDealId,
                    'title' => $title,
                    'incoming_service_cost' => $incomingServiceCost,
                    'incoming_payable_amount' => $incomingPayableAmount,
                    'existing_service_cost' => $existingServiceCost,
                    'existing_payable_amount' => $existingPayableAmount,
                    'incoming_price_list_code_empty' => $priceListCodeInput === '',
                    'role' => $_SESSION['role'] ?? null,
                    'user_id' => $_SESSION['user_id'] ?? null,
                ]);
            }
        }

        $failureReasonCodeInput = Sanitizer::sanitize($_POST['failure_reason'] ?? '');
        $failureReasonDescriptionInput = Sanitizer::sanitize($_POST['failure_other_description'] ?? '');
        $failureReasonTextInput = Sanitizer::sanitize($_POST['failure_reason_text'] ?? '');

        $failureReasonTextFromCodeMap = [
            'no_financial_ability' => 'عدم توان مالی',
            'no_purchase_intent' => 'فعلا قصد خرید ندارد',
            'competitor_purchase' => 'خرید از رقبا',
            'wrong_number' => 'شماره اشتباه',
            'irrelevant_lead' => 'لید نامرتبط',
            'other' => 'سایر',
        ];

        $failureReasonTextFromCode = function(string $code) use ($failureReasonTextFromCodeMap): string {
            $map = [
                'no_financial_ability' => 'عدم توان مالی',
                'no_purchase_intent' => 'فعلا قصد خرید ندارد',
                'competitor_purchase' => 'خرید از رقبا',
                'wrong_number' => 'شماره اشتباه',
                'irrelevant_lead' => 'لید نامرتبط',
                'other' => 'سایر',
            ];
            return $map[$code] ?? '';
        };

        $normalizedFailureCode = $failureReasonCodeInput !== '' ? $failureReasonCodeInput : null;
        $normalizedFailureDesc = $failureReasonDescriptionInput !== '' ? $failureReasonDescriptionInput : null;
        $normalizedFailureText = $failureReasonTextInput !== '' ? $failureReasonTextInput : null;

        if ($statusInput === 'lost') {
            // Keep backend in sync with frontend: for lost deals, reason and description are required.
            if (empty($normalizedFailureCode)) {
                if (!empty($normalizedFailureText)) {
                    $reverse = array_flip($failureReasonTextFromCodeMap ?? []);
                    if (isset($reverse[$normalizedFailureText])) {
                        $normalizedFailureCode = $reverse[$normalizedFailureText];
                    }
                }
                if (empty($normalizedFailureCode)) {
                    $this->errorResponse('دلیل ناموفق بودن معامله الزامی است');
                }
            }
            if (empty($normalizedFailureDesc)) {
                $this->errorResponse('توضیحات دلیل ناموفق بودن معامله الزامی است');
            }

            if (empty($normalizedFailureText)) {
                $base = $failureReasonTextFromCode($normalizedFailureCode);
                $normalizedFailureText = $base !== '' ? $base : $normalizedFailureCode;
            }
            if (empty($normalizedFailureText) && !empty($normalizedFailureDesc)) {
                $normalizedFailureText = $normalizedFailureDesc;
            }
        } else {
            // For non-lost deals, clear any sent failure reason data.
            $normalizedFailureCode = null;
            $normalizedFailureDesc = null;
            $normalizedFailureText = null;
        }

        $data = [
            'didar_deal_id' => Sanitizer::sanitize($_POST['deal_id'] ?? ''),
            'contact_didar_id' => $normalizedContactId,
            'title' => $title,
            'status' => $statusInput,
            'pipeline_stage' => $pipelineStageInput,
            'estimated_price' => isset($serviceCostInput) ? $normalizeMoney($serviceCostInput) : null,
            'final_price' => isset($payableAmountInput) ? $normalizeMoney($payableAmountInput) : null,
            'service_cost' => isset($serviceCostInput) ? $normalizeMoney($serviceCostInput) : null,
            'price_list_code' => $priceListCodeInput,
            'has_discount' => $hasDiscountInput,
            'discount_type' => $discountTypeInput,
            'discount_occasion' => $discountOccasionInput,
            'discount_amount' => $discountAmountInput,
            'refund_amount' => $refundAmountInput,
            'refund_description' => $refundDescriptionInput,
            'payable_amount' => isset($payableAmountInput) ? $normalizeMoney($payableAmountInput) : null,
            'payment_conditions' => $paymentConditionsInput,
            'payment_amount' => floatval($_POST['payment_amount'] ?? 0),
            'payments' => '',
            'acquaintance_source' => $dealAcquaintanceSourceInput,
            'acquaintance_detail' => $dealAcquaintanceDetailInput,
            'content_topic' => $dealContentTopicInput,
            'financial_level' => Sanitizer::sanitize($_POST['financial_level'] ?? ''),
            'asset_estimation' => Sanitizer::sanitize($_POST['asset_estimation'] ?? ''),
            'income_estimation' => Sanitizer::sanitize($_POST['income_estimation'] ?? ''),
            'has_previous_purchase' => isset($_POST['has_previous_purchase']) && $_POST['has_previous_purchase'] !== '' ? (int)$_POST['has_previous_purchase'] : null,
            'owner_didar_id' => $_SESSION['didar_id'] ?? '',
            'created_by_didar_id' => ($isNewDeal || !$existingDeal) ? ($_SESSION['didar_id'] ?? '') : ($existingDeal->created_by_didar_id ?? ''),
            'created_by_name' => ($isNewDeal || !$existingDeal)
                ? (($_SESSION['display_name'] ?? '') !== '' ? ($_SESSION['display_name'] ?? '') : ($_SESSION['name'] ?? ''))
                : ($existingDeal->created_by_name ?? ''),
            'created_via' => ($isNewDeal || !$existingDeal) ? 'manual' : ($existingDeal->created_via ?? 'manual'),
            'register_time' => ($isNewDeal || !$existingDeal)
                ? date('Y-m-d H:i:s')
                : ($existingDeal->register_time ?? date('Y-m-d H:i:s')),
            'last_sync' => date('Y-m-d H:i:s'),
            'products' => $products,
            'requested_services' => $requestedServices
        ];

        if ($isNewDeal && !$fromDuplicateLeadModal) {
            if ($data['acquaintance_source'] === '') {
                $this->errorResponse('فیلد شیوه آشنایی معامله الزامی است');
            }
            if ($data['content_topic'] === '') {
                $this->errorResponse('فیلد موضوع محتوا معامله الزامی است');
            }
        }

        if (!empty($title)) {
            $data['requested_services'] = [$title];
        }

        // Use the robust repository method to find the person
        $person = $resolvedPerson ?: $this->resolvePersonByContactId($data['contact_didar_id']);

        if (!$person) {
            $this->errorResponse('لید با این شناسه یافت نشد');
        }

        // Standardize to the real Didar GUID for ALL internal processing
        $data['contact_didar_id'] = $person->didar_contact_id;
        // --- END STRICT LOOKUP ---

        // Failure reason (stored for reporting)
        $data['failure_reason_code'] = $normalizedFailureCode;
        $data['failure_reason_text'] = $normalizedFailureText;
        $data['failure_reason_description'] = $normalizedFailureDesc;

        // ✅ Validation based on field configurations
        global $app;
        if (isset($app->repositories->fieldConfig)) {
            $configs = $app->repositories->fieldConfig->getByEntity('deal');
            
            // Map field_configurations field names to actual POST field names
            $fieldNameMapping = [
                'deal_title' => 'title',  // deal_title in config maps to title in POST
                'deal_acquaintance_source' => 'acquaintance_source',
                'deal_acquaintance_detail' => 'acquaintance_detail',
                'deal_content_topic' => 'content_topic',
            ];
            $dealMarketingConfigFields = ['deal_acquaintance_source', 'deal_acquaintance_detail', 'deal_content_topic'];
            
            foreach ($configs as $config) {
                $fieldName = $config['field_name'];
                $fieldLabel = !empty($config['field_label']) ? $config['field_label'] : $fieldName;

                // Skip payment sub-fields; آنها در بخش پرداخت جداگانه اعتبارسنجی می‌شوند
                // Also skip fields that are actually lookup group names or reference payment-related lookup groups
                $paymentRelatedFields = [
                    'payment_card_last4', 'payment_card_last_4', 'card_last4', 'card_last_4',
                    'destination_bank', 'bank_destination', 'destination_banks', 'payment_destination_banks',
                    'payment_method', 'payment_methods', 
                    'payment_delete'
                ];
                $paymentRelatedLookups = ['payment_methods', 'payment_destination_banks'];
                
                if (in_array($fieldName, $paymentRelatedFields) || 
                    in_array($config['lookup_group_code'] ?? '', $paymentRelatedLookups)) {
                    continue;
                }

                // Skip lookup groups - they don't exist as direct fields in $_POST for deals
                if (!empty($config['lookup_group_code']) && $fieldName === $config['lookup_group_code']) {
                    continue;
                }

                // Skip inactive fields
                if (empty($config['is_active'])) {
                    continue;
                }

                // Map field name to actual POST field name
                $postFieldName = $fieldNameMapping[$fieldName] ?? $fieldName;
                $val = $_POST[$postFieldName] ?? null;
                if (!$isNewDeal && in_array($fieldName, $dealMarketingConfigFields, true) && !array_key_exists($postFieldName, $_POST)) {
                    continue;
                }
                
                // Conditional Visibility Check: Only validate if field should be visible
                // Pass $_POST as formData for condition evaluation
                if (!$app->services->fieldPermission->checkConditionalVisibility($fieldName, $_POST)) {
                    continue;
                }

                // Required check - only validate if field exists in POST or is a new deal
                if ($config['is_required'] && in_array($fieldName, $dealMarketingConfigFields, true) && (!$isNewDeal || $fromDuplicateLeadModal)) {
                    continue;
                }
                if ($config['is_required'] && (is_null($val) || $val === '')) {
                    \App\Utils\Logger::logWarning('DEAL_FIELD_REQUIRED_VALIDATION_FAILED', [
                        'deal_id' => $_POST['deal_id'] ?? '',
                        'field_name' => $fieldName,
                        'post_field_name' => $postFieldName,
                        'field_label' => $fieldLabel,
                        'is_required' => $config['is_required'],
                        'value' => $val,
                        'is_new_deal' => $isNewDeal,
                        'all_post_keys' => array_keys($_POST)
                    ]);
                    $this->errorResponse("فیلد {$fieldLabel} الزامی است");
                }
                
                // Regex Pattern check
                if (!empty($config['validation_pattern']) && !empty($val)) {
                    // Safety: use @ to suppress errors if regex is invalid
                    if (@preg_match('/' . str_replace('/', '\/', $config['validation_pattern']) . '/', $val) === 0) {
                        $this->errorResponse("قالب فیلد {$fieldLabel} صحیح نیست");
                    }
                }
            }
        }

        // Refund validation (as a discount type)
        if ((int)($data['has_discount'] ?? 0) === 1 && ($data['discount_type'] ?? '') === 'refund') {
            if (($data['refund_amount'] ?? 0) <= 0) {
                $this->errorResponse('مبلغ مرجوعی الزامی است');
            }
            if (empty(trim((string)($data['refund_description'] ?? '')))) {
                $this->errorResponse('توضیحات مرجوعی الزامی است');
            }
        } else {
            $data['refund_amount'] = 0;
            $data['refund_description'] = '';
        }

        if (!$isNewDeal && !empty($data['didar_deal_id'])) {
            \App\Utils\Logger::logInfo('DealController.save existing deal lookup', [
                'deal_id' => $data['didar_deal_id'],
                'contact_id' => $data['contact_didar_id'],
                'existing_deal_found' => (bool) $existingDeal,
                'existing_contact_id' => $existingDeal ? $existingDeal->contact_didar_id : null,
                'existing_title' => $existingDeal ? $existingDeal->title : null,
                'user_id' => $_SESSION['didar_id'] ?? 'unknown',
                'user_name' => $_SESSION['name'] ?? 'unknown',
                'server_time' => date('Y-m-d H:i:s')
            ]);
        }
        if ($existingDeal && !empty($existingDeal->contact_didar_id) && $existingDeal->contact_didar_id !== $data['contact_didar_id']) {
            Logger::logWarning('Deal save blocked: deal_id does not belong to contact', [
                'deal_id' => $data['didar_deal_id'],
                'incoming_contact_id' => $data['contact_didar_id'],
                'existing_contact_id' => $existingDeal->contact_didar_id,
                'incoming_title' => $data['title'] ?? '',
                'requested_services' => $data['requested_services'] ?? [],
                'user_id' => $_SESSION['didar_id'] ?? 'unknown',
                'user_name' => $_SESSION['name'] ?? 'unknown',
                'server_time' => date('Y-m-d H:i:s')
            ]);
            $this->errorResponse('شناسه معامله با لید انتخاب‌شده مطابقت ندارد. لطفا معامله درست را انتخاب کنید.');
        }

        // Preserve پایه عنوان معامله (شماره و ترتیب) و فقط خدمات را به انتهای آن اضافه کن
        $baseTitle = $data['title'];
        if ($isNewDeal) {
            try {
                $baseTitle = $this->dealService->generateDealTitle($data['contact_didar_id']);
            } catch (\Exception $e) {
                \App\Utils\Logger::logWarning('Failed to generate base title for new deal', [
                    'contact_id' => $data['contact_didar_id'],
                    'error' => $e->getMessage()
                ]);
            }
        } elseif ($existingDeal && !empty($existingDeal->title)) {
            $parts = explode('|', $existingDeal->title, 2);
            $baseTitle = trim($parts[0]);
        }
        if (empty($baseTitle) && !empty($data['contact_didar_id'])) {
            try {
                $baseTitle = $this->dealService->generateDealTitle($data['contact_didar_id']);
            } catch (\Exception $e) {
                \App\Utils\Logger::logWarning('Failed to generate base title for existing deal', [
                    'contact_id' => $data['contact_didar_id'],
                    'error' => $e->getMessage()
                ]);
            }
        }
        $servicesText = '';
        $existingRequestedServices = [];
        if ($existingDeal && !empty($existingDeal->requested_services)) {
            $decodedRequested = json_decode($existingDeal->requested_services, true);
            if (is_array($decodedRequested)) {
                $existingRequestedServices = $decodedRequested;
            }
        }
        $normalizeService = function($value): string {
            $s = trim((string)$value);
            $s = preg_replace('/\s+/', ' ', $s);
            return trim($s);
        };
        $baseTitleNormalized = $normalizeService($baseTitle);
        $filterServices = function(array $services) use ($normalizeService, $baseTitleNormalized) {
            $out = [];
            foreach ($services as $service) {
                $s = $normalizeService($service);
                if ($s === '') {
                    continue;
                }
                if ($baseTitleNormalized !== '' && $s === $baseTitleNormalized) {
                    continue;
                }
                if ($this->isGeneratedDealTitle($s)) {
                    continue;
                }
                $out[] = $s;
            }
            return array_values(array_unique($out));
        };

        if (!empty($existingRequestedServices)) {
            $existingRequestedServices = $filterServices((array)$existingRequestedServices);
        }
        if (!empty($requestedServices)) {
            $requestedServices = $filterServices((array)$requestedServices);
        }
        // Replace requested services on edit when client submits them.
        // Only fallback to existing data if nothing is submitted.
        if (empty($requestedServices) && !empty($existingRequestedServices)) {
            $requestedServices = $existingRequestedServices;
        }
        if (!empty($requestedServices)) {
            $requestedServices = array_values(array_filter(array_map('trim', (array)$requestedServices)));
        }
        if (!empty($requestedServices)) {
            $servicesText = is_array($requestedServices) ? implode('، ', array_unique(array_map('trim', $requestedServices))) : trim((string)$requestedServices);
        } elseif ($existingDeal && !empty($existingDeal->title) && strpos($existingDeal->title, '|') !== false) {
            $servicesText = trim(substr($existingDeal->title, strpos($existingDeal->title, '|') + 1));
        }
        $data['title'] = $servicesText ? trim($baseTitle . ' | ' . $servicesText) : $baseTitle;
        $data['requested_services'] = $requestedServices;

        if (empty($data['contact_didar_id'])) {
            $this->errorResponse('شناسه لید الزامی است');
        }

        // For new deals, title is generated automatically
        if (empty($data['title'])) {
            $this->errorResponse('عنوان معامله الزامی است');
        }

        if (empty($data['status'])) {
            $this->errorResponse('انتخاب وضعیت معامله الزامی است');
        }
        
        // Pipeline stage is only required if status is NOT lost (lost deals automatically get 'معامله ناموفق')
        if (empty($data['pipeline_stage']) && $data['status'] !== 'lost') {
            $this->errorResponse('انتخاب مرحله کاریز معامله الزامی است');
        }

        $requiredFieldMap = [
            'service_cost' => ['value' => $serviceCostInput, 'label' => 'هزینه خدمات'],
            'price_list_code' => ['value' => $priceListCodeInput, 'label' => 'کد لیست قیمت'],
            'payable_amount' => ['value' => $payableAmountInput, 'label' => 'مبلغ قابل پرداخت']
        ];
        foreach ($requiredFieldMap as $key => $item) {
            // Only validate if key exists in POST (manual submission). Automated flows may skip these.
            if (array_key_exists($key, $_POST)) {
                $value = $item['value'];
                if ($value === null || $value === '' || (is_string($value) && trim($value) === '')) {
                    $this->errorResponse($item['label'] . ' الزامی است');
                }
            }
        }

        $existingPaymentsDecoded = [];
        if ($existingDeal && !empty($existingDeal->payments)) {
            $decodedExisting = json_decode($existingDeal->payments, true);
            if (is_array($decodedExisting)) {
                $existingPaymentsDecoded = $decodedExisting;
            }
        }
        $existingPaymentsCount = count($existingPaymentsDecoded);
        if (empty($paymentsPayload) && !empty($existingPaymentsDecoded)) {
            $paymentsPayload = $existingPaymentsDecoded;
        }
        $existingProducts = [];
        if ($existingDeal && !empty($existingDeal->didar_deal_id)) {
            try {
                foreach ((array)$this->dealRepo->getProductsByDealDidarId((string)$existingDeal->didar_deal_id) as $row) {
                    if (isset($row['product_id'])) {
                        $existingProducts[] = (string)$row['product_id'];
                    }
                }
            } catch (\Throwable $e) {
                $existingProducts = [];
            }
        }

        $rulesMap = $this->buildFieldRulesMap();
        $userRole = $_SESSION['role'] ?? '';

        $dateRule = $this->resolveFieldRule($rulesMap, 'deal.payments.date', ['required' => false]);
        $amountRule = $this->resolveFieldRule($rulesMap, 'deal.payments.amount', ['required' => false]);
        $methodRule = $this->resolveFieldRule($rulesMap, 'deal.payments.method', ['required' => false]);
        $descriptionRule = $this->resolveFieldRule($rulesMap, 'deal.payments.description', ['required' => false]);
        $cardRule = $this->resolveFieldRule($rulesMap, 'deal.payments.card_last4', ['required' => false]);
        $bankRule = $this->resolveFieldRule($rulesMap, 'deal.payments.bank_destination', ['required' => false]);

        $normalizedStatus = strtolower((string)$data['status']);
        $successPaymentRequirement = array_key_exists('payable_amount', $data) && $data['payable_amount'] !== null
            ? max(0.0, floatval($data['payable_amount']))
            : max(0.0, floatval($data['service_cost'] ?? 0));
        $paymentsRequired = ($normalizedStatus === 'won' || $stageMeansSuccess) && $successPaymentRequirement > 0;
        $validatedPayments = [];
        foreach ($paymentsPayload as $idx => $payment) {
            $isEntryNew = $isNewDeal || $idx >= $existingPaymentsCount;
            $existingPayment = $existingPaymentsDecoded[$idx] ?? [];

            $date = trim((string)($payment['date'] ?? ''));
            $datePersian = trim((string)($payment['date_persian'] ?? ''));
            $amount = isset($payment['amount']) ? floatval($payment['amount']) : 0;
            // Normalize method key
            $method = Sanitizer::sanitize($payment['method'] ?? $payment['payment_method'] ?? '');
            $description = Sanitizer::sanitize($payment['description'] ?? '');
            // Normalize bank key (support both destination_bank and bank_destination, use destination_bank as standard)
            $bankDestination = Sanitizer::sanitize($payment['destination_bank'] ?? $payment['bank_destination'] ?? '');
            $bankDestinationOther = Sanitizer::sanitize($payment['destination_bank_other'] ?? '');
            $accountOwnerName = Sanitizer::sanitize($payment['account_owner_name'] ?? '');
            $destinationCardNumber = preg_replace('/\D+/', '', (string)Sanitizer::sanitize($payment['destination_card_number'] ?? ''));
            if (strlen($destinationCardNumber) > 4) {
                $destinationCardNumber = substr($destinationCardNumber, -4);
            }
            $destinationCardNumberOther = Sanitizer::sanitize($payment['destination_card_number_other'] ?? '');
            $paymentTime = Sanitizer::sanitize($payment['payment_time'] ?? '');
            $cardReaderDevice = Sanitizer::sanitize($payment['card_reader_device'] ?? '');
            $currency = Sanitizer::sanitize($payment['currency'] ?? '');
            $currencyOther = Sanitizer::sanitize($payment['currency_other'] ?? '');
            $currencyAmount = isset($payment['currency_amount']) ? floatval($payment['currency_amount']) : 0;
            $checkBankName = Sanitizer::sanitize($payment['check_bank_name'] ?? '');
            $checkDate = Sanitizer::sanitize($payment['check_date'] ?? '');
            $checkNumber = Sanitizer::sanitize($payment['check_number'] ?? '');
            $checkOwnerName = Sanitizer::sanitize($payment['check_owner_name'] ?? '');

            $canEditDate = $this->canEditField($dateRule, $isEntryNew, $userRole);
            $canEditAmount = $this->canEditField($amountRule, $isEntryNew, $userRole);
            $canEditMethod = $this->canEditField($methodRule, $isEntryNew, $userRole);
            $canEditDescription = $this->canEditField($descriptionRule, $isEntryNew, $userRole);
            $canEditCard = $this->canEditField($cardRule, $isEntryNew, $userRole);
            $canEditBank = $this->canEditField($bankRule, $isEntryNew, $userRole);

            if (!$canEditDate) {
                if (isset($existingPayment['date'])) {
                    $date = trim((string)$existingPayment['date']);
                }
                if (isset($existingPayment['date_persian'])) {
                    $datePersian = trim((string)$existingPayment['date_persian']);
                }
            }
            if (!$canEditAmount && isset($existingPayment['amount'])) {
                $amount = floatval($existingPayment['amount']);
            }
            if (!$canEditMethod && isset($existingPayment['method'])) {
                $method = Sanitizer::sanitize($existingPayment['method']);
            }
            if (!$canEditDescription && isset($existingPayment['description'])) {
                $description = Sanitizer::sanitize($existingPayment['description']);
            }
            // Normalize existing payment bank key
            if (!$canEditBank) {
                $existingBank = $existingPayment['destination_bank'] ?? $existingPayment['bank_destination'] ?? '';
                if ($existingBank) {
                    $bankDestination = Sanitizer::sanitize($existingBank);
                }
            }

            // Normalize card_last4 key for hasAnyValue check
            $cardLast4ForCheck = $payment['card_last4'] ?? $payment['card_last_4'] ?? '';
            $hasAnyValue = (
                $date !== '' || $datePersian !== '' || $amount > 0 || $method !== '' || $description !== '' ||
                $bankDestination !== '' || trim((string)$cardLast4ForCheck) !== '' || $destinationCardNumber !== '' ||
                $paymentTime !== '' || $cardReaderDevice !== '' || $currency !== '' || $currencyAmount > 0 ||
                $checkBankName !== '' || $checkDate !== '' || $checkNumber !== '' || $checkOwnerName !== ''
            );
            if (!$hasAnyValue) {
                continue;
            }

            $isCardTransfer = $this->isCardTransferMethod($method);
            $isCardReader = $this->isCardReaderMethod($method);
            $isCash = $this->isCashMethod($method);
            $isCheck = $this->isCheckMethod($method);
            $requiresCardLast4 = $isCardTransfer || $isCardReader;
            $requiresBankDestination = $isCardTransfer;

            $requireDate = false;
            $requireAmount = false;
            $requireMethod = false;
            $requireDescription = false;
            $requireCardLast4 = false;
            $requireBank = false;

            if ($requireDate && $date === '' && $datePersian === '') {
                $this->errorResponse('تاریخ پرداخت شماره ' . ($idx + 1) . ' الزامی است');
            }
            if ($requireAmount && $amount <= 0) {
                $this->errorResponse('مبلغ پرداخت شماره ' . ($idx + 1) . ' الزامی است');
            }
            if ($requireMethod && $method === '') {
                $this->errorResponse('نحوه پرداخت شماره ' . ($idx + 1) . ' الزامی است');
            }
            if ($requireDescription && $description === '') {
                $this->errorResponse('توضیح پرداخت شماره ' . ($idx + 1) . ' الزامی است');
            }

            // Normalize card_last4 key (support both card_last4 and card_last_4)
            $cardLast4Raw = $payment['card_last4'] ?? $payment['card_last_4'] ?? '';
            $cardLast4 = preg_replace('/\D+/', '', (string)$cardLast4Raw);
            if (!$canEditCard) {
                $existingCardLast4 = $existingPayment['card_last4'] ?? $existingPayment['card_last_4'] ?? '';
                if ($existingCardLast4) {
                    $cardLast4 = preg_replace('/\D+/', '', (string)$existingCardLast4);
                }
            }
            if (strlen($cardLast4) > 4) {
                $cardLast4 = substr($cardLast4, -4);
            }
            if ($requireCardLast4 && !preg_match('/^\d{4}$/', $cardLast4)) {
                $this->errorResponse('Card last 4 is required for payment row ' . ($idx + 1));
            }
            if ($requireBank && $bankDestination === '') {
                $this->errorResponse('Bank destination is required for payment row ' . ($idx + 1));
            }
            if (!$isCheck) {
                $accountOwnerName = '';
            }

            // Store with standard keys (card_last4 and destination_bank)
            $validatedPayments[] = [
                'date' => $date,
                'date_persian' => $datePersian,
                'amount' => $amount,
                'method' => $method,
                'description' => $description,
                'card_last4' => $cardLast4,  // Standard key (without underscore)
                'destination_bank' => $bankDestination,  // Standard key
                'destination_bank_other' => $bankDestinationOther,
                'account_owner_name' => $accountOwnerName,
                'destination_card_number' => $destinationCardNumber,
                'destination_card_number_other' => $destinationCardNumberOther,
                'payment_time' => $paymentTime,
                'card_reader_device' => $cardReaderDevice,
                'currency' => $currency,
                'currency_other' => $currencyOther,
                'currency_amount' => $currencyAmount,
                'check_bank_name' => $checkBankName,
                'check_date' => $checkDate,
                'check_number' => $checkNumber,
                'check_owner_name' => $checkOwnerName
            ];
        }

        $isFirstPaymentNow = ($existingPaymentsCount === 0 && count($validatedPayments) > 0);
        $paymentChanges = $this->detectPaymentPermissionChanges($existingPaymentsDecoded, $validatedPayments);
        if ($paymentChanges['create']) {
            $this->requireRbac('action.payments.create', 'دسترسی محدود - شما مجاز به ثبت پرداخت نیستید');
        }
        if ($paymentChanges['edit']) {
            $this->requireRbac('action.payments.edit', 'دسترسی محدود - شما مجاز به ویرایش پرداخت نیستید');
        }
        if ($paymentChanges['delete']) {
            $this->requireRbac('action.payments.delete', 'دسترسی محدود - شما مجاز به حذف پرداخت نیستید');
        }

        if ($paymentsRequired && empty($validatedPayments)) {
            $this->errorResponse('ثبت حداقل یک پرداخت کامل برای معامله الزامی است');
        }
        if (!$paymentsRequired && empty($validatedPayments) && !empty($paymentsPayload)) {
            $this->errorResponse('اطلاعات پرداخت ناقص است');
        }

        if (empty($validatedPayments) && $existingDeal && !empty($existingDeal->payments)) {
            $storedPayments = json_decode($existingDeal->payments, true);
            if (is_array($storedPayments) && !empty($storedPayments)) {
                $validatedPayments = $storedPayments;
            }
        }

        $data['payments'] = !empty($validatedPayments)
            ? json_encode($validatedPayments, JSON_UNESCAPED_UNICODE)
            : '';
        $data['payment_amount'] = array_reduce($validatedPayments, function($sum, $p) {
            return $sum + (isset($p['amount']) ? floatval($p['amount']) : 0);
        }, 0);
        $payableAmount = isset($data['payable_amount']) ? floatval($data['payable_amount']) : 0;
        $paymentAmount = floatval($data['payment_amount']);
        if ($paymentAmount > 0 && $payableAmount <= 0) {
            $this->errorResponse('ثبت پرداخت بدون مبلغ قرارداد مجاز نیست');
        }

        $adminUnlockPaidDealFields = $this->authService->isAdmin() && (int)($_POST['admin_unlock_paid_deal_fields'] ?? 0) === 1;
        $dealHadPaymentsBefore = $this->hasPositivePayments($existingPaymentsDecoded);
        $dealHasPaymentsAfterSave = $this->hasPositivePayments($validatedPayments);
        if (!$isNewDeal && $existingDeal && $dealHadPaymentsBefore && !$adminUnlockPaidDealFields) {
            $this->restorePaidDealLockedFields($data, $existingDeal, $existingRequestedServices, $existingProducts);
        }
        if ($dealHasPaymentsAfterSave && !$adminUnlockPaidDealFields) {
            if (empty($data['status'])) {
                $data['status'] = 'Pending';
            }
            $data['pipeline_stage'] = $this->getPaidDealAutoPipelineStage((float)($data['payable_amount'] ?? 0), $paymentAmount);
        }
        $successRequiredAmount = array_key_exists('payable_amount', $data) && $data['payable_amount'] !== null
            ? max(0.0, (float)$data['payable_amount'])
            : max(0.0, (float)($data['service_cost'] ?? 0));
        if (strtolower((string)($data['status'] ?? '')) === 'won' && $successRequiredAmount > 0 && ($paymentAmount + 0.0001) < $successRequiredAmount) {
            $this->errorResponse('برای موفق شدن معامله، مجموع پرداخت‌ها باید حداقل برابر مبلغ قابل پرداخت باشد');
        }
        if (!$isNewDeal) {
            $ignoreKeys = (!$adminUnlockPaidDealFields && $dealHadPaymentsBefore) ? ['payment_conditions', 'pipeline_stage'] : [];
            $hasNonPaymentChanges = $this->hasNonPaymentDealChanges($existingDeal, $data, $ignoreKeys);
            if ($hasNonPaymentChanges) {
                $this->requireRbac('action.deals.edit', 'دسترسی محدود - شما مجاز به ویرایش معامله نیستید');
            }
        }

        \App\Utils\Logger::logInfo('DealController.save validated payments', [
            'deal_id' => $data['didar_deal_id'],
            'contact_id' => $data['contact_didar_id'],
            'payments_count' => count($validatedPayments),
            'status' => $data['status'],
            'pipeline_stage' => $data['pipeline_stage']
        ]);
        
        # The person has already been loaded and validated earlier
        # and $data['contact_didar_id'] is already standardized to a GUID.
        
        // Enforce owner consistency and permission
        $currentUserId = $_SESSION['didar_id'] ?? '';
        \App\Utils\Logger::logInfo('DealController.save validating permissions', [
            'current_user_id' => $currentUserId,
            'contact_owner_id' => $person->owner_didar_id,
            'contact_id' => $person->didar_contact_id,
            'rbac_scope_check' => 'deals'
        ]);

        $this->assertOwnerInDealScopeByDidar((string)($person->owner_didar_id ?? ''));
        // Force deal owner to match lead owner
        $data['owner_didar_id'] = $person->owner_didar_id;
        
        // Extra logging for troubleshooting
        \App\Utils\Logger::logInfo('DealController.save payload - DETAILED', [
            'didar_deal_id' => $data['didar_deal_id'],
            'contact_didar_id' => $data['contact_didar_id'],
            'title' => $data['title'],
            'base_title_used' => $baseTitle,
            'services_text' => $servicesText,
            'requested_services_count' => count($requestedServices),
            'pipeline_stage' => $data['pipeline_stage'],
            'status' => $data['status'],
            'payable_amount' => $data['payable_amount'],
            'payment_amount' => $data['payment_amount'],
            'payments_present' => !empty($data['payments']),
            'requested_services' => $data['requested_services'],
            'user_id' => $_SESSION['didar_id'] ?? 'unknown',
            'user_name' => $_SESSION['name'] ?? 'unknown',
            'contact_owner_id' => $person->owner_didar_id,
            'contact_name' => trim(($person->first_name ?? '') . ' ' . ($person->last_name ?? '')),
            'existing_deal' => $existingDeal ? 'YES' : 'NO',
            'existing_deal_id' => $existingDeal ? $existingDeal->didar_deal_id : null,
            'existing_deal_status' => $existingDeal ? $existingDeal->status : null,
            'server_time' => date('Y-m-d H:i:s'),
            'session_data' => [
                'didar_id' => $_SESSION['didar_id'] ?? null,
                'name' => $_SESSION['name'] ?? null,
                'user_id' => $_SESSION['user_id'] ?? null
            ]
        ]);

        try {
            $pdo = $this->db->getPdo();
            if (!$pdo->inTransaction()) {
                $pdo->beginTransaction();
            }

            // Log changes before saving if updating
            if ($existingDeal) {
                $this->logFieldChanges('deal', $existingDeal->didar_deal_id, $existingDeal->toArray(), $data);
            }

            $savedDeal = $this->dealService->saveDeal($data);

            if (!empty($validatedPayments)) {
                $syncDealId = $savedDeal->didar_deal_id ?? $data['didar_deal_id'];
                $invoiceCode = $this->dealService->ensureInvoiceCodeForDeal((string)$syncDealId, $validatedPayments);
                if ($invoiceCode) {
                    $data['invoice_code'] = $invoiceCode;
                }
                $this->dealService->syncTransactionsFromPayments((string)$syncDealId, $validatedPayments);
            }

            if ($pdo->inTransaction()) {
                $pdo->commit();
            }

            if (!empty($requestedServices) || !empty($data['title'])) {
                try {
                    $this->dealService->ensureDealProductsFromText(
                        $data['didar_deal_id'],
                        $requestedServices,
                        $data['title']
                    );
                } catch (\Exception $e) {
                    \App\Utils\Logger::logWarning('ensureDealProductsFromText failed', [
                        'deal_id' => $data['didar_deal_id'],
                        'error' => $e->getMessage()
                    ]);
                }
            }

            // AUDIT TRAIL: Log deal creation/update for data integrity tracking
            \App\Utils\Logger::logInfo('AUDIT: Deal saved successfully', [
                'action' => $isNewDeal ? 'CREATE_DEAL' : 'UPDATE_DEAL',
                'deal_id' => $data['didar_deal_id'],
                'contact_id' => $data['contact_didar_id'],
                'contact_owner_id' => $data['owner_didar_id'],
                'user_id' => $_SESSION['didar_id'] ?? 'unknown',
                'user_name' => $_SESSION['name'] ?? 'unknown',
                'deal_title' => $data['title'],
                'deal_status' => $data['status'],
                'pipeline_stage' => $data['pipeline_stage'],
                'payable_amount' => $data['payable_amount'],
                'payment_amount' => $data['payment_amount'],
                'server_time' => date('Y-m-d H:i:s'),
                'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
                'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
            ]);
        } catch (\Exception $e) {
            try {
                $pdo = $this->db->getPdo();
                if ($pdo->inTransaction()) {
                    $pdo->rollBack();
                }
            } catch (\Throwable $rollbackError) {
                \App\Utils\Logger::logWarning('Deal save rollback failed', [
                    'deal_id' => $data['didar_deal_id'] ?? '',
                    'error' => $rollbackError->getMessage()
                ]);
            }
            \App\Utils\Logger::logError('Failed to save deal', $e, [
                'deal_id' => $data['didar_deal_id'],
                'contact_id' => $data['contact_didar_id'],
                'user_id' => $_SESSION['didar_id'] ?? 'unknown'
            ]);
            $this->errorResponse($e->getMessage() ?: 'خطا در ذخیره معامله');
        }

        // Update person financial fields so profile modal shows them
        $finFields = [];
        if (!empty($data['financial_level'])) $finFields['financial_level'] = $data['financial_level'];
        if (!empty($data['asset_estimation'])) $finFields['asset_estimation'] = $data['asset_estimation'];
        if (!empty($data['income_estimation'])) $finFields['income_estimation'] = $data['income_estimation'];
        if ($data['has_previous_purchase'] !== null) $finFields['has_previous_purchase'] = $data['has_previous_purchase'];
        if (!empty($finFields)) {
            try {
                $pdo = $this->personRepo->getDb()->getPdo();
                $setParts = [];
                $params = [];
                foreach ($finFields as $k => $v) {
                    $setParts[] = "$k = ?";
                    $params[] = $v;
                }
                $params[] = $data['contact_didar_id'];
                $stmt = $pdo->prepare("UPDATE persons SET " . implode(', ', $setParts) . " WHERE didar_contact_id = ?");
                $stmt->execute($params);
            } catch (\Exception $e) {
                \App\Utils\Logger::logWarning('Failed to update person financial fields on deal save', [
                    'contact_id' => $data['contact_didar_id'],
                    'error' => $e->getMessage()
                ]);
            }
        }

        if (strtolower((string)($data['status'] ?? '')) === 'won') {
            $this->syncSuccessfulDealSideEffects((string)$data['didar_deal_id'], (string)$data['contact_didar_id']);
        } elseif (strtolower((string)($data['status'] ?? '')) === 'lost') {
            $this->syncFailedDealSideEffects((string)$data['didar_deal_id'], (string)$data['contact_didar_id']);
        }

        $this->successResponse([], 'معامله با موفقیت ثبت شد');
    }

    public function setStatus()
    {
        $this->requireRbac('deals.status.edit', 'دسترسی محدود - شما مجاز به تغییر وضعیت معامله نیستید');
        $dealId = Sanitizer::sanitize($_POST['deal_id'] ?? '');
        $status = Sanitizer::sanitize($_POST['status'] ?? '');
        $contactId = Sanitizer::sanitize($_POST['contact_id'] ?? '');
        $pipelineStage = Sanitizer::sanitize($_POST['pipeline_stage'] ?? '');
        $failureReasonCode = Sanitizer::sanitize($_POST['failure_reason'] ?? '');
        $failureReasonDescription = Sanitizer::sanitize($_POST['failure_other_description'] ?? '');
        $failureReasonText = Sanitizer::sanitize($_POST['failure_reason_text'] ?? '');
        
        if (empty($dealId) || empty($status)) {
            $this->errorResponse('شناسه معامله و وضعیت الزامی است');
        }
        $scopeDeal = $this->dealRepo->findByDidarId($dealId);
        if (!$scopeDeal) {
            $this->errorResponse('معامله یافت نشد', 404);
        }
        $this->assertDealInScope($scopeDeal);
        \App\Utils\Logger::logInfo('setStatus called', [
            'deal_id' => $dealId,
            'status' => $status,
            'contact_id' => $contactId,
            'pipeline_stage' => $pipelineStage
        ]);
        
        $statusText = $status === '1' ? 'Won' : 'Lost';
        $timeField = $status === '1' ? 'won_time' : 'lost_time';

        // Ensure payments exist for successful status
        if ($status === '1') {
            $deal = $this->dealRepo->findByDidarId($dealId);
            $this->assertDealCanBeMarkedWon($deal);
            $requiredAmount = $this->getSuccessRequiredPaymentAmount($deal);
            $paymentsDecoded = [];
            if ($deal && !empty($deal->payments)) {
                $paymentsDecoded = json_decode($deal->payments, true) ?: [];
            }
            $hasValidPayment = false;
            foreach ($paymentsDecoded as $p) {
                $amount = isset($p['amount']) ? floatval($p['amount']) : 0;
                $date = trim((string)($p['date'] ?? ''));
                $datePersian = trim((string)($p['date_persian'] ?? ''));
                $method = trim((string)($p['method'] ?? ''));
                if ($amount > 0 && ($date !== '' || $datePersian !== '') && $method !== '') {
                    $hasValidPayment = true;
                    break;
                }
            }
            if ($requiredAmount > 0 && !$hasValidPayment) {
                \App\Utils\Logger::logWarning('setStatus blocked: no valid payments for Won', ['deal_id' => $dealId, 'contact_id' => $contactId, 'payments_count' => count($paymentsDecoded)]);
                $this->errorResponse('برای ثبت معامله موفق، پرداخت کامل ثبت نشده است');
            }
        }
        
        $this->dealService->updateStatus($dealId, $statusText, $timeField);
        if ($statusText === 'Lost') {
            $this->dealService->updatePipelineStage($dealId, 'اتمام');
        } elseif (!empty($pipelineStage)) {
            $this->dealService->updatePipelineStage($dealId, $pipelineStage);
        }

        // Persist failure reason for reporting (only for Lost)
        if ($statusText === 'Lost') {
            $map = [
                'no_financial_ability' => 'عدم توان مالی',
                'no_purchase_intent' => 'فعلا قصد خرید ندارد',
                'competitor_purchase' => 'خرید از رقبا',
                'wrong_number' => 'شماره اشتباه',
                'irrelevant_lead' => 'لید نامرتبط',
                'other' => 'سایر',
            ];
            $code = $failureReasonCode !== '' ? $failureReasonCode : null;
            $desc = $failureReasonDescription !== '' ? $failureReasonDescription : null;
            $text = $failureReasonText !== '' ? $failureReasonText : null;
            if (!$code && $text) {
                $reverse = array_flip($map);
                if (isset($reverse[$text])) {
                    $code = $reverse[$text];
                }
            }
            if (empty($code)) {
                $this->errorResponse('دلیل ناموفق بودن معامله الزامی است');
            }
            if ($text === null && $code) {
                $text = $map[$code] ?? $code;
            }
            // Note: older clients might send Persian text instead of code in failure_reason
            if ($code && !isset($map[$code]) && $text === null) {
                $text = $code;
                $code = null;
            }
            $this->dealRepo->updateFailureReason($dealId, $code, $text, $desc);
        } else {
            // Clearing reason when marking as Won
            $this->dealRepo->updateFailureReason($dealId, null, null, null);
        }

        // Sync lead sale_status with successful deal
        if (!empty($contactId)) {
            $saleStatus = $status === '1' ? 1 : 0;
            try {
                $person = $this->personRepo->findByDidarId($contactId);
                $currentSaleStatus = $person ? (int) $person->sale_status : null;
                $shouldUpdate = true;
                // اگر یک بار موفق شده، با معامله ناموفق، لید را ناموفق نکن
                if ($saleStatus === 0 && $currentSaleStatus === 1) {
                    $shouldUpdate = false;
                }
                if ($shouldUpdate) {
                    $this->personRepo->updateSaleStatus($contactId, $saleStatus);
                }
            } catch (\Exception $e) {
                \App\Utils\Logger::logWarning('Failed to update sale_status for contact', [
                    'contact_id' => $contactId,
                    'status' => $saleStatus,
                    'error' => $e->getMessage()
                ]);
            }
        }
        if ($status === '1' && !empty($contactId)) {
            $this->syncSuccessfulDealSideEffects($dealId, $contactId);
        } elseif (!empty($contactId)) {
            $this->syncFailedDealSideEffects($dealId, $contactId);
        }
        $this->successResponse([], 'وضعیت معامله با موفقیت تغییر یافت');
    }

    public function forceSuccessAndExitKariz()
    {
        $this->requireRbac('deals.force_success', 'دسترسی محدود - شما مجاز به نهایی‌سازی اجباری معامله نیستید');
        $dealId = Sanitizer::sanitize($_POST['deal_id'] ?? '');
        if ($dealId === '') {
            $this->errorResponse('شناسه معامله الزامی است');
        }

        $deal = $this->dealRepo->findByDidarId($dealId);
        if (!$deal) {
            $this->errorResponse('معامله یافت نشد', 404);
        }
        $this->assertDealInScope($deal);

        $contactId = trim((string)($deal->contact_didar_id ?? ''));
        if ($contactId === '') {
            $this->errorResponse('شناسه لید معامله نامعتبر است');
        }

        if (trim((string)($deal->status ?? '')) !== 'Won') {
            $this->errorResponse('این عملیات فقط برای معاملات موفق مجاز است');
        }

        $this->assertDealCanBeMarkedWon($deal);

        $this->dealService->updateStatus($dealId, 'Won', 'won_time');
        $this->dealService->updatePipelineStage($dealId, 'اتمام');
        $this->dealRepo->updateFailureReason($dealId, null, null, null);
        $this->syncSuccessfulDealSideEffects($dealId, $contactId);

        if ($this->auditService) {
            $this->auditService->log('force_success_exit_kariz', 'deal', $dealId, [
                'contact_id' => $contactId,
                'pipeline_stage' => 'اتمام',
                'status' => 'Won',
            ]);
        }

        $this->successResponse([], 'معامله با موفقیت نهایی شد و از کاریز خارج شد');
    }

    public function setPipelineStage()
    {
        $this->requireRbac('deals.pipeline.edit', 'دسترسی محدود - شما مجاز به ویرایش مرحله معامله نیستید');
        $dealId = Sanitizer::sanitize($_POST['deal_id'] ?? '');
        $stage = Sanitizer::sanitize($_POST['pipeline_stage'] ?? '');
        if (empty($dealId) || empty($stage)) {
            $this->errorResponse('شناسه معامله و مرحله الزامی است');
        }
        $scopeDeal = $this->dealRepo->findByDidarId($dealId);
        if (!$scopeDeal) {
            $this->errorResponse('معامله یافت نشد', 404);
        }
        $this->assertDealInScope($scopeDeal);
        $stageNormalized = $stage;
        $isSuccessStage = in_array($stage, ['deal_success', 'اتمام', 'موفق', 'معامله موفق', 'پرداخت']);
        $isFailedStage = in_array($stage, ['deal_failed', 'معامله ناموفق', 'ناموفق']);
        if ($isSuccessStage) {
            $deal = $this->dealRepo->findByDidarId($dealId);
            $this->assertDealCanBeMarkedWon($deal);
            $stageNormalized = 'اتمام';
            $this->dealService->updateStatus($dealId, 'Won', 'won_time');
        } elseif ($isFailedStage) {
            $stageNormalized = 'اتمام';
            $this->dealService->updateStatus($dealId, 'Lost', 'lost_time');
        }
        $this->dealService->updatePipelineStage($dealId, $stageNormalized);
        $deal = $this->dealRepo->findByDidarId($dealId);
        $contactId = trim((string)($deal->contact_didar_id ?? ''));
        if ($isSuccessStage && $contactId !== '') {
            $this->syncSuccessfulDealSideEffects($dealId, $contactId);
        } elseif ($isFailedStage && $contactId !== '') {
            $this->syncFailedDealSideEffects($dealId, $contactId);
        } elseif ($contactId !== '') {
            try {
                $this->virtualStageService->update($contactId);
            } catch (\Throwable $e) {
                Logger::logWarning('Failed to refresh virtual stage after pipeline stage change', [
                    'deal_id' => $dealId,
                    'contact_id' => $contactId,
                    'error' => $e->getMessage(),
                ]);
            }
        }
        $this->successResponse([], 'مرحله کاریز معامله به‌روزرسانی شد');
    }

    public function setCourseStatus()
    {
        $this->requireRbac('deals.course.edit', 'دسترسی محدود - شما مجاز به ویرایش وضعیت دوره نیستید');
        $data = [
            'deal_id' => Sanitizer::sanitize($_POST['deal_id'] ?? ''),
            'contact_id' => Sanitizer::sanitize($_POST['contact_id'] ?? ''),
            'course_virtual' => Sanitizer::sanitize($_POST['course_virtual'] ?? ''),
            'course_online' => Sanitizer::sanitize($_POST['course_online'] ?? ''),
            'access_created' => Sanitizer::sanitize($_POST['access_created'] ?? ''),
            'link_sent' => Sanitizer::sanitize($_POST['link_sent'] ?? ''),
            'course_date' => Sanitizer::sanitize($_POST['course_date'] ?? ''),
            'next_stage' => Sanitizer::sanitize($_POST['next_stage'] ?? 'برگزاری دوره'),
            'refund' => Sanitizer::sanitize($_POST['refund'] ?? ''),
            'refund_reason' => Sanitizer::sanitize($_POST['refund_reason'] ?? '')
        ];

        if (empty($data['deal_id'])) {
            $this->errorResponse('شناسه معامله الزامی است');
        }
        $scopeDeal = $this->dealRepo->findByDidarId($data['deal_id']);
        if (!$scopeDeal) {
            $this->errorResponse('معامله یافت نشد', 404);
        }
        $this->assertDealInScope($scopeDeal);

        $this->dealService->updateCourseStatus($data);
        $this->successResponse([], 'وضعیت دوره/پشتیبانی معامله به‌روز شد');
    }

    public function delete()
    {
        $this->requireRbac('deals.delete', 'دسترسی محدود - شما مجاز به حذف معامله نیستید');
        
        $dealId = Sanitizer::sanitize($_POST['deal_id'] ?? '');
        
        if (empty($dealId)) {
            $this->errorResponse('شناسه معامله الزامی است');
        }

        $deal = $this->dealRepo->findByDidarId($dealId);
        if (!$deal) {
            $this->errorResponse('معامله یافت نشد', 404);
        }
        $this->assertDealInScope($deal);
        if ($this->auditService) {
            $this->auditService->log('delete', 'deal', $dealId, [
                'deal' => $deal ? $deal->toArray() : null,
                'deal_title' => $deal ? ($deal->title ?? '') : '',
            ]);
        }

        $this->dealService->deleteDeal($dealId);
        $this->successResponse([], 'معامله با موفقیت حذف شد');
    }

    public function getDeal()
    {
        $this->requireRbac('deals.detail.view', 'دسترسی محدود - شما مجاز به مشاهده جزئیات معامله نیستید');
        $dealId = Sanitizer::sanitize($_POST['deal_id'] ?? '');
        if (empty($dealId)) {
            $this->errorResponse('شناسه معامله الزامی است');
        }
        
        $deal = $this->dealService->getDeal($dealId);
        if (!$deal) {
            $this->errorResponse('معامله یافت نشد');
        }
        $this->assertDealInScope($deal);
        
        // Parse payments JSON
        $payments = [];
        if (!empty($deal->payments)) {
            $payments = json_decode($deal->payments, true) ?: [];
        }
        
        $dealArray = $deal->toArray();
        $dealArray['payments'] = $payments;
        $dealArray['has_registered_payments'] = $this->hasPositivePayments($payments);
        $dealArray['registered_payments_count'] = count(array_filter($payments, static function ($payment) {
            return (float)($payment['amount'] ?? 0) > 0;
        }));
        
        // Add explicit contact info to prevent frontend from doing a wrong lookup
        if (isset($deal->contact_name)) {
            $dealArray['contact_name'] = $deal->contact_name;
        }
        if (isset($deal->contact_mobile)) {
            $dealArray['contact_mobile'] = $deal->contact_mobile;
        }
        if (isset($deal->local_person_id)) {
            $dealArray['local_person_id'] = $deal->local_person_id;
        }

        // Load products
        $products = $this->dealRepo->getProductsByDealDidarId($deal->didar_deal_id);
        $dealArray['products'] = $products;
        
        $this->successResponse(['deal' => $dealArray]);
    }

    public function reactivate()
    {
        $this->requireRbac('deals.reactivate', 'دسترسی محدود - شما مجاز به فعال‌سازی مجدد معامله نیستید');

        $dealId = Sanitizer::sanitize($_POST['deal_id'] ?? '');
        $newPipelineStage = Sanitizer::sanitize($_POST['new_pipeline_stage'] ?? '');
        $reactivationReason = Sanitizer::sanitize($_POST['reactivation_reason'] ?? '');

        if (empty($dealId)) {
            $this->errorResponse('شناسه معامله الزامی است');
        }

        if (empty($newPipelineStage)) {
            $this->errorResponse('مرحله کاریز جدید الزامی است');
        }

        // بررسی وجود معامله
        $deal = $this->dealRepo->findByDidarId($dealId);
        if (!$deal) {
            $this->errorResponse('معامله یافت نشد');
        }
        $this->assertDealInScope($deal);

        // بررسی اینکه معامله بسته شده باشد
        if ($deal->status === 'Pending') {
            $this->errorResponse('این معامله هم اکنون فعال است');
        }

        if ($deal->status !== 'Won' && $deal->status !== 'Lost') {
            $this->errorResponse('فقط معاملات بسته شده (موفق یا ناموفق) قابل فعال‌سازی مجدد هستند');
        }

        \App\Utils\Logger::logInfo('Deal reactivation requested', [
            'deal_id' => $dealId,
            'old_status' => $deal->status,
            'old_stage' => $deal->pipeline_stage,
            'new_stage' => $newPipelineStage,
            'reason' => $reactivationReason
        ]);

        try {
            $this->dealService->reactivateDeal($dealId, $newPipelineStage, $reactivationReason);

            // Audit logging
            if ($this->auditService) {
                $this->auditService->log('reactivate', 'deal', $dealId, [
                    'old_status' => $deal->status,
                    'old_stage' => $deal->pipeline_stage,
                    'new_stage' => $newPipelineStage,
                    'reason' => $reactivationReason,
                    'user_id' => $_SESSION['user_id'] ?? null,
                    'user_name' => $_SESSION['name'] ?? 'Unknown'
                ]);
            }

            $this->successResponse([], 'معامله با موفقیت فعال شد');

        } catch (\Exception $e) {
            \App\Utils\Logger::logError('Failed to reactivate deal', $e, [
                'deal_id' => $dealId,
                'new_stage' => $newPipelineStage
            ]);
            $this->errorResponse('خطا در فعال‌سازی مجدد معامله: ' . $e->getMessage());
        }
    }

    /**
     * Extract phone number from deal title
     * Expected format: "معامله اول 9127103488 | خدمات"
     */
    private function isGeneratedDealTitle(string $title): bool
    {
        $t = trim($title);
        if ($t === '') {
            return false;
        }
        if (strpos($t, '|') !== false) {
            return false;
        }
        return preg_match('/^Ù…Ø¹Ø§Ù…Ù„Ù‡\s+\S+\s+\d{10,12}$/u', $t) === 1;
    }

    private function extractPhoneFromDealTitle(string $title): string
    {
        Logger::logInfo('extractPhoneFromDealTitle called', [
            'input_title' => $title
        ]);

        // Remove any content after " | " separator
        $titlePart = explode(' | ', $title, 2)[0];

        Logger::logInfo('extractPhoneFromDealTitle processing', [
            'title_part' => $titlePart
        ]);

        // Look for phone number pattern in the title part
        // Pattern: "معامله [ordinal] [phone_number]"
        if (preg_match('/معامله\s+\S+\s+(\d{10,12})/u', $titlePart, $matches)) {
            Logger::logInfo('extractPhoneFromDealTitle found match', [
                'matched_phone' => $matches[1],
                'pattern' => 'معامله_ordinal_phone'
            ]);
            return $matches[1];
        }

        // Fallback: try to find any 10-12 digit number
        if (preg_match('/(\d{10,12})/', $titlePart, $matches)) {
            Logger::logInfo('extractPhoneFromDealTitle found fallback match', [
                'matched_phone' => $matches[1],
                'pattern' => 'any_10-12_digits'
            ]);
            return $matches[1];
        }

        Logger::logInfo('extractPhoneFromDealTitle no match found', [
            'title_part' => $titlePart
        ]);
        return '';
    }

    /**
     * Get contact's primary phone number
     */
    private function getContactPrimaryPhone(string $contactId): string
    {
        Logger::logInfo('getContactPrimaryPhone called', [
            'contact_id' => $contactId
        ]);

        // Use the robust repository method to find the person
        $person = $this->resolvePersonByContactId($contactId);
        
        $primaryPhone = $person ? ($person->mobile_phone ?? '') : '';

        Logger::logInfo('getContactPrimaryPhone result', [
            'contact_id' => $contactId,
            'person_found' => (bool) $person,
            'primary_phone' => $primaryPhone
        ]);

        return $primaryPhone;
    }

    /**
     * Normalize phone number for comparison
     * Remove country code, spaces, and standardize format
     */
    private function normalizePhoneForComparison(string $phone): string
    {
        Logger::logInfo('normalizePhoneForComparison called', [
            'input_phone' => $phone
        ]);

        // Remove all non-digit characters
        $cleaned = preg_replace('/\D/', '', $phone);

        Logger::logInfo('normalizePhoneForComparison cleaned', [
            'original' => $phone,
            'cleaned' => $cleaned
        ]);

        // Remove country code if present (98 or +98)
        if (str_starts_with($cleaned, '98') && strlen($cleaned) > 10) {
            $cleaned = substr($cleaned, 2);
            Logger::logInfo('normalizePhoneForComparison removed 98 prefix', ['cleaned' => $cleaned]);
        }
        if (str_starts_with($cleaned, '+98') && strlen($cleaned) > 10) {
            $cleaned = substr($cleaned, 3);
            Logger::logInfo('normalizePhoneForComparison removed +98 prefix', ['cleaned' => $cleaned]);
        }

        // Ensure it's a valid mobile number (10 digits starting with 9)
        if (strlen($cleaned) === 10 && str_starts_with($cleaned, '9')) {
            Logger::logInfo('normalizePhoneForComparison valid 10-digit mobile', ['final' => $cleaned]);
            return $cleaned;
        }

        // If it's 11 digits starting with 09, remove the 0
        if (strlen($cleaned) === 11 && str_starts_with($cleaned, '09')) {
            $final = substr($cleaned, 1);
            Logger::logInfo('normalizePhoneForComparison converted 11-digit to 10-digit', ['original' => $cleaned, 'final' => $final]);
            return $final;
        }

        Logger::logInfo('normalizePhoneForComparison final result', ['final' => $cleaned]);
        return $cleaned;
    }

    private function parseLookupMeta(?string $metaJson): array
    {
        if ($metaJson === null || $metaJson === '') {
            return [];
        }
        $decoded = json_decode($metaJson, true);
        return is_array($decoded) ? $decoded : [];
    }

    private function getLookupItemsSafe(string $groupCode, bool $includeInactive = false): array
    {
        if (!method_exists($this->dealService, 'getLookupItems')) {
            return [];
        }
        try {
            return $this->dealService->getLookupItems($groupCode, $includeInactive);
        } catch (\Throwable $e) {
            return [];
        }
    }

    private function buildFieldRulesMap(): array
    {
        $items = $this->getLookupItemsSafe('field_rules', false);
        $map = [];
        foreach ($items as $item) {
            $key = $item['value'] ?? $item['title'] ?? '';
            if ($key === '') {
                continue;
            }
            $meta = $this->parseLookupMeta($item['meta_json'] ?? null);
            $map[$key] = [
                'required' => array_key_exists('required', $meta) ? (bool)$meta['required'] : null,
                'editable' => array_key_exists('editable', $meta) ? (bool)$meta['editable'] : null,
                'editable_after_create' => array_key_exists('editable_after_create', $meta) ? (bool)$meta['editable_after_create'] : null,
                'editable_roles' => $meta['editable_roles'] ?? [],
            ];
        }
        return $map;
    }

    private function resolveFieldRule(array $rulesMap, string $key, array $defaults = []): array
    {
        $rule = $rulesMap[$key] ?? [];
        $roles = $rule['editable_roles'] ?? ($defaults['editable_roles'] ?? []);
        if (is_string($roles)) {
            $roles = array_values(array_filter(array_map('trim', explode(',', $roles))));
        }
        if (!is_array($roles)) {
            $roles = [];
        }
        return [
            'required' => array_key_exists('required', $rule) ? (bool)$rule['required'] : (bool)($defaults['required'] ?? false),
            'editable' => array_key_exists('editable', $rule) ? (bool)$rule['editable'] : (bool)($defaults['editable'] ?? true),
            'editable_after_create' => array_key_exists('editable_after_create', $rule) ? (bool)$rule['editable_after_create'] : (bool)($defaults['editable_after_create'] ?? true),
            'editable_roles' => $roles,
        ];
    }

    private function canEditField(array $rule, bool $isNewEntry, string $role): bool
    {
        if (array_key_exists('editable', $rule) && !$rule['editable']) {
            return false;
        }
        if (!$isNewEntry && array_key_exists('editable_after_create', $rule) && !$rule['editable_after_create']) {
            return false;
        }
        $roles = $rule['editable_roles'] ?? [];
        if (!empty($roles) && !in_array($role, $roles, true)) {
            return false;
        }
        return true;
    }

    private function shouldRequireField(array $rule, bool $isNewEntry, bool $canEdit): bool
    {
        if (empty($rule['required'])) {
            return false;
        }
        return $isNewEntry ? true : $canEdit;
    }

    private function buildPaymentMethodMetaMap(): array
    {
        $items = $this->getLookupItemsSafe('payment_methods', true);
        $map = [];
        foreach ($items as $item) {
            $key = $item['value'] ?? $item['title'] ?? '';
            if ($key === '') {
                continue;
            }
            $meta = $this->parseLookupMeta($item['meta_json'] ?? null);
            $map[$key] = [
                'requires_card_last4' => !empty($meta['requires_card_last4']),
                'requires_bank_destination' => !empty($meta['requires_bank_destination']),
            ];
        }
        if (empty($map)) {
            $map = [
                'card_reader' => ['requires_card_last4' => true, 'requires_bank_destination' => true],
                'card_to_card' => ['requires_card_last4' => true, 'requires_bank_destination' => true],
                'check' => ['requires_card_last4' => false, 'requires_bank_destination' => false],
            ];
        }
        return $map;
    }

    private function normalizeMethodCode(string $method): string
    {
        $method = strtolower(trim($method));
        return preg_replace('/[\s\-\/]+/', '_', $method);
    }

    private function isCardTransferMethod(string $method): bool
    {
        $code = $this->normalizeMethodCode($method);
        return in_array($code, ['card_to_card', 'card_to_card_lookup', 'card_to_card_transfer'], true)
            || strpos($code, 'sheba') !== false
            || strpos($code, 'shaba') !== false
            || strpos($code, 'pol') !== false
            || strpos($code, 'pool') !== false;
    }

    private function isCardReaderMethod(string $method): bool
    {
        $code = $this->normalizeMethodCode($method);
        return in_array($code, ['card_reader', 'pos', 'pos_machine'], true);
    }

    private function isCashMethod(string $method): bool
    {
        $code = $this->normalizeMethodCode($method);
        return in_array($code, ['cash', 'نقد', 'وجه_نقد'], true);
    }

    private function isCheckMethod(string $method): bool
    {
        $code = $this->normalizeMethodCode($method);
        return in_array($code, ['check', 'cheque', 'چک'], true);
    }

    private function resolvePersonByContactId(string $contactId)
    {
        if ($contactId === '') {
            return null;
        }

        $person = $this->personRepo->findByDidarId($contactId);
        if ($person) {
            return $person;
        }

        return $this->personRepo->findByIdOrDidarId($contactId);
    }

    private function getWonDealsNeedingKarizReconcile(int $limit = 3000): array
    {
        if (!$this->db) {
            return [];
        }

        $pdo = $this->db->getPdo();
        $sql = "
            SELECT
                d.didar_deal_id,
                d.title,
                d.contact_didar_id,
                d.pipeline_stage,
                d.register_time,
                CASE WHEN COALESCE(d.pipeline_stage, '') <> 'اتمام' THEN 1 ELSE 0 END AS needs_pipeline_fix,
                CASE
                    WHEN d.contact_didar_id IS NOT NULL
                     AND d.contact_didar_id <> ''
                     AND NOT EXISTS (
                        SELECT 1
                        FROM virtual_stages vs
                        WHERE vs.contact_didar_id = d.contact_didar_id
                          AND vs.stage_name = 'with_purchase'
                     )
                    THEN 1 ELSE 0
                END AS needs_virtual_fix
            FROM deals d
            WHERE d.status = 'Won'
              AND (
                    COALESCE(d.pipeline_stage, '') <> 'اتمام'
                    OR (
                        d.contact_didar_id IS NOT NULL
                        AND d.contact_didar_id <> ''
                        AND NOT EXISTS (
                            SELECT 1
                            FROM virtual_stages vs
                            WHERE vs.contact_didar_id = d.contact_didar_id
                              AND vs.stage_name = 'with_purchase'
                        )
                    )
                  )
            ORDER BY d.register_time DESC, d.id DESC
            LIMIT " . max(1, (int)$limit);

        $stmt = $pdo->query($sql);
        return $stmt ? (array)$stmt->fetchAll(\PDO::FETCH_ASSOC) : [];
    }

    public function previewWonKarizReconcile()
    {
        $this->requireRbac('deals.reconcile', 'دسترسی محدود - شما مجاز به پیش‌نمایش ترمیم معاملات نیستید');

        $rows = $this->getWonDealsNeedingKarizReconcile(5000);
        $pipelineCount = 0;
        $virtualCount = 0;
        foreach ($rows as $row) {
            if ((int)($row['needs_pipeline_fix'] ?? 0) === 1) {
                $pipelineCount++;
            }
            if ((int)($row['needs_virtual_fix'] ?? 0) === 1) {
                $virtualCount++;
            }
        }

        $this->successResponse([
            'report' => [
                'total_candidates' => count($rows),
                'needs_pipeline_fix' => $pipelineCount,
                'needs_virtual_fix' => $virtualCount,
                'preview' => array_slice($rows, 0, 120),
            ],
        ]);
    }

    public function runWonKarizReconcile()
    {
        $this->requireRbac('deals.reconcile', 'دسترسی محدود - شما مجاز به اجرای ترمیم معاملات نیستید');

        $rows = $this->getWonDealsNeedingKarizReconcile(5000);
        $fixedPipeline = 0;
        $fixedVirtual = 0;
        $errors = [];

        foreach ($rows as $row) {
            $dealId = trim((string)($row['didar_deal_id'] ?? ''));
            $contactId = trim((string)($row['contact_didar_id'] ?? ''));
            if ($dealId === '') {
                continue;
            }

            try {
                if ((int)($row['needs_pipeline_fix'] ?? 0) === 1) {
                    $this->dealService->updatePipelineStage($dealId, 'اتمام');
                    $fixedPipeline++;
                }

                if ((int)($row['needs_virtual_fix'] ?? 0) === 1 && $contactId !== '') {
                    try {
                        $this->personRepo->updateSaleStatus($contactId, 1);
                    } catch (\Throwable $ignored) {
                        // Sale-status sync failure should not block virtual-stage repair.
                    }
                    $this->virtualStageService->update($contactId, 'with_purchase', true);
                    $fixedVirtual++;
                }
            } catch (\Throwable $e) {
                $errors[] = [
                    'deal_id' => $dealId,
                    'contact_id' => $contactId,
                    'error' => $e->getMessage(),
                ];
            }
        }

        if ($this->auditService) {
            $this->auditService->log('reconcile_won_kariz', 'deal', null, [
                'total_candidates' => count($rows),
                'fixed_pipeline' => $fixedPipeline,
                'fixed_virtual' => $fixedVirtual,
                'errors_count' => count($errors),
            ]);
        }

        $this->successResponse([
            'report' => [
                'total_candidates' => count($rows),
                'fixed_pipeline' => $fixedPipeline,
                'fixed_virtual' => $fixedVirtual,
                'errors_count' => count($errors),
                'errors' => array_slice($errors, 0, 100),
            ],
        ], 'ترمیم گروهی معاملات موفق انجام شد');
    }
}
