<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\ActivityLog;
use App\Models\Deal;
use App\Models\LeadCharge;
use App\Models\Project;
use App\Models\SupplierLeadCost;
use App\Models\User;
use App\Services\Pd\PdAuditLogger;
use App\Services\SupplierResolver;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;

/**
 * Сделки — single-resource CRUD через REST API (index/show/store/update).
 *
 * Sprint 3 Phase A (audit O-refactor-01): bulk transition/destroy/restore
 * вынесены в `DealBulkActionController`, `export()` — в `DealExportController`.
 * Этот класс остаётся только для CRUD по одной записи.
 *
 * NB: webhook-flow (приём из crm.bp-gr.ru) — отдельный endpoint
 * `SupplierWebhookController` + `RouteSupplierLeadJob` (шеринг-канал).
 * Этот controller — для ручных action'ов из UI.
 *
 * J1 (Sprint 3F): auth:sanctum+tenant, tenant_id из auth()->user().
 *
 * Manual-create отличается от webhook'а:
 *   - source_crm_id = NULL (не из webhook).
 *   - НЕ списывает баланс (manual leads — не закупка у supplier'а).
 *   - НЕ создаёт SupplierLeadCost / BalanceTransaction.
 *   - Антифрод-дедуп НЕ применяется (manual — admin знает что вводит).
 *   - Project резолвится / создаётся по name (как и в webhook).
 *   - Если manager_id передан — должен принадлежать tenant'у (FK guard).
 */
class DealController extends Controller
{
    /**
     * GET /api/deals?status_in[]=...&project_id=...&manager_id=...&search=...&limit=...&offset=...
     *
     * Список сделок tenant'а с relations (project.name, manager.first/last/email).
     * Используется в `DealsView`/`KanbanView` вместо MOCK_DEALS.
     *
     * Сортировка: ORDER BY received_at DESC (свежие сверху). На MVP без
     * курсорной пагинации — limit/offset простой OFFSET-paging достаточен
     * для UI-разводки (на prod при росте datasets — переход на keyset на
     * (received_at, id)).
     *
     * RLS: SET LOCAL app.current_tenant_id внутри транзакции (PgBouncer-safe).
     */
    public function index(Request $request): JsonResponse
    {
        $tenantId = (int) $request->user()->tenant_id;

        $request->validate([
            'received_from' => 'nullable|date',
            'received_to' => 'nullable|date',
        ]);

        $statuses = (array) $request->query('status_in', []);
        $projectId = $request->query('project_id') !== null ? (int) $request->query('project_id') : null;
        $managerId = $request->query('manager_id') !== null ? (int) $request->query('manager_id') : null;
        $search = trim((string) $request->query('search', ''));
        $limit = max(1, min(500, (int) $request->query('limit', '100')));
        $offset = max(0, (int) $request->query('offset', '0'));
        $onlyDeleted = $request->boolean('only_deleted');
        $countOnly = $request->boolean('count_only');
        $cursorRaw = (string) $request->query('cursor', '');
        $receivedFrom = trim((string) $request->query('received_from', ''));
        $receivedTo = trim((string) $request->query('received_to', ''));

        // Sprint 4 Phase A (audit O-perf-04): keyset pagination через cursor.
        // При передаче cursor — keyset через PG row constructor (received_at, id) < (?, ?),
        // O(1) на любой глубине. Без cursor — старое OFFSET-поведение (backward-compat).
        $cursor = null;
        if ($cursorRaw !== '') {
            $decoded = base64_decode($cursorRaw, true);
            if ($decoded === false) {
                return response()->json(['message' => 'Невалидный cursor (не base64).'], 422);
            }
            $parsed = json_decode($decoded, true);
            if (! is_array($parsed) || ! isset($parsed['r'], $parsed['i'])) {
                return response()->json(['message' => 'Невалидный cursor (нет полей r/i).'], 422);
            }
            $cursor = ['r' => (string) $parsed['r'], 'i' => (int) $parsed['i']];
        }

        [$deals, $total, $nextCursor] = DB::transaction(function () use ($tenantId, $statuses, $projectId, $managerId, $search, $limit, $offset, $onlyDeleted, $cursor, $countOnly, $receivedFrom, $receivedTo) {
            DB::statement('SET LOCAL app.current_tenant_id = '.$tenantId);

            // Defense-in-depth: явный where(tenant_id) поверх RLS — на тестах
            // через `postgres` superuser RLS обходится BYPASSRLS, app-фильтр
            // гарантирует изоляцию даже при misconfig RLS.
            //
            // only_deleted=true: «Корзина» — показываем только soft-deleted.
            // withTrashed() обходит global scope SoftDeletes; явный
            // whereNotNull('deleted_at') фильтрует только удалённые.
            $query = Deal::query()
                ->select('deals.*')
                ->where('tenant_id', $tenantId)
                ->with(['project:id,name,signal_type,signal_identifier,sms_keyword,sms_senders', 'manager:id,email,first_name,last_name']);

            if ($onlyDeleted) {
                $query->withTrashed()->whereNotNull('deleted_at');
            }

            if ($statuses !== []) {
                $query->whereIn('status', array_filter($statuses, 'is_string'));
            }
            if ($projectId !== null) {
                $query->where('project_id', $projectId);
            }
            if ($managerId !== null) {
                $query->where('manager_id', $managerId);
            }
            if ($search !== '') {
                $like = '%'.$search.'%';
                $query->where(function ($q) use ($like) {
                    $q->where('phone', 'ilike', $like)
                        ->orWhere('contact_name', 'ilike', $like);
                });
            }
            if ($receivedFrom !== '') {
                $query->where('received_at', '>=', Carbon::parse($receivedFrom)->startOfDay());
            }
            if ($receivedTo !== '') {
                // received_to включительно — до конца дня (+1 день, строгое <).
                $query->where('received_at', '<', Carbon::parse($receivedTo)->addDay()->startOfDay());
            }

            // Audit B2: count_only — отдаём только COUNT(*), пропуская SELECT строк
            // и cursor/offset-логику (лёгкий запрос для бейджа в сайдбаре).
            if ($countOnly) {
                return [collect(), $query->count(), null];
            }

            if ($cursor !== null) {
                // Keyset: PG row constructor через индекс на (received_at DESC, id DESC).
                // Не считаем total (дорого без COUNT(*); клиент при необходимости
                // вызывает endpoint без cursor для total на первой странице).
                $query->whereRaw('(received_at, id) < (?, ?)', [$cursor['r'], $cursor['i']]);
                $rows = $query->orderByDesc('received_at')->orderByDesc('id')
                    ->limit($limit + 1)->get();
                $hasNext = $rows->count() > $limit;
                if ($hasNext) {
                    $rows = $rows->slice(0, $limit)->values();
                }
                $next = null;
                if ($hasNext && $rows->isNotEmpty()) {
                    $last = $rows->last();
                    $next = base64_encode((string) json_encode([
                        'r' => $last->received_at?->toIso8601String(),
                        'i' => $last->id,
                    ]));
                }

                return [$rows, null, $next];
            }

            // Backward-compat OFFSET-путь: total + offset для существующего frontend.
            // +1 fetch trick — узнаём про следующую страницу одним SELECT'ом без COUNT.
            $total = (clone $query)->count();
            $rows = $query->orderByDesc('received_at')->orderByDesc('id')
                ->limit($limit + 1)->offset($offset)->get();
            $hasNext = $rows->count() > $limit;
            if ($hasNext) {
                $rows = $rows->slice(0, $limit)->values();
            }
            $next = null;
            if ($hasNext && $rows->isNotEmpty()) {
                $last = $rows->last();
                $next = base64_encode((string) json_encode([
                    'r' => $last->received_at?->toIso8601String(),
                    'i' => $last->id,
                ]));
            }

            return [$rows, $total, $next];
        });

        if ($countOnly) {
            return response()->json(['total' => $total]);
        }

        // U4 (UX-аудит 25.06): стоимость лида должна быть видна и в листинге
        // (панель деталей и канбан рисуют из строки листинга, show отдельно не
        // дозапрашивают). Запрос — в своей транзакции с app.current_tenant_id,
        // иначе RLS на lead_charges вернёт 0 строк (как в show). rub-провенанс.
        $costByDeal = collect();
        $dealIds = $deals->pluck('id')->all();
        if ($dealIds !== []) {
            $costByDeal = DB::transaction(function () use ($tenantId, $dealIds) {
                DB::statement('SET LOCAL app.current_tenant_id = '.$tenantId);

                return LeadCharge::query()
                    ->where('tenant_id', $tenantId)
                    ->whereIn('deal_id', $dealIds)
                    ->where('charge_source', 'rub')
                    ->pluck('price_per_lead_kopecks', 'deal_id');
            });
        }

        $payload = [
            'deals' => $deals->map(fn (Deal $d) => [
                'id' => $d->id,
                'tenant_id' => $d->tenant_id,
                'project_id' => $d->project_id,
                'project_name' => $d->project?->name,
                'phone' => $d->phone,
                'contact_name' => $d->contact_name,
                'status' => $d->status,
                'manager_id' => $d->manager_id,
                'manager_name' => $d->manager
                    ? ManagerController::formatName($d->manager->first_name, $d->manager->last_name, $d->manager->email)
                    : null,
                'manager_initials' => $d->manager
                    ? ManagerController::formatInitials($d->manager->first_name, $d->manager->last_name, $d->manager->email)
                    : null,
                'received_at' => $d->received_at?->toIso8601String(),
                'comment' => $d->comment,
                'city' => $d->city,
                'project_signal_type' => $d->project?->signal_type,
                'project_signal_identifier' => $d->project?->signal_identifier,
                'project_sms_keyword' => $d->project?->sms_keyword,
                'project_sms_senders' => $d->project?->sms_senders,
                'cost_kopecks' => $costByDeal[$d->id] ?? null,
            ]),
            'limit' => $limit,
            'next_cursor' => $nextCursor,
        ];

        if ($cursor === null) {
            $payload['total'] = $total;
            $payload['offset'] = $offset;
        }

        return response()->json($payload);
    }

    /**
     * GET /api/deals/{id} — детали сделки + recent activity events.
     *
     * Используется в DealDetailDrawer (правая панель). Возвращает deal с
     * relations + до 50 последних activity_log событий по этой сделке.
     *
     * RLS-обёртка + defense-in-depth `where(tenant_id)`. Если сделка не
     * принадлежит tenant'у (или не существует) — 404.
     */
    public function show(Request $request, int $id, PdAuditLogger $pdLog): JsonResponse
    {
        $tenantId = (int) $request->user()->tenant_id;

        [$deal, $events, $charge] = DB::transaction(function () use ($tenantId, $id) {
            DB::statement('SET LOCAL app.current_tenant_id = '.$tenantId);

            $deal = Deal::query()
                ->where('tenant_id', $tenantId)
                ->where('id', $id)
                ->with(['project:id,name,signal_type,signal_identifier,sms_keyword,sms_senders', 'manager:id,email,first_name,last_name'])
                ->first();

            if ($deal === null) {
                return [null, [], null];
            }

            $events = ActivityLog::query()
                ->where('tenant_id', $tenantId)
                ->where('deal_id', $id)
                ->with('user:id,email,first_name,last_name')
                ->orderByDesc('created_at')
                ->orderByDesc('id')
                ->limit(50)
                ->get();

            // F2: реальная стоимость лида — снимок списания из lead_charges
            // (rub-провенанс). Запрос в транзакции, где выставлен app.current_tenant_id.
            $charge = LeadCharge::query()
                ->where('tenant_id', $tenantId)
                ->where('deal_id', $id)
                ->first();

            return [$deal, $events, $charge];
        });

        if ($deal === null) {
            return response()->json(['message' => 'Сделка не найдена.'], 404);
        }

        $pdLog->record(
            action: 'viewed',
            subjectType: 'lead',
            subjectId: $deal->id,
            purpose: 'lead_card_view',
            tenantId: (int) $request->user()->tenant_id,
            actorTenantUserId: (int) $request->user()->id,
            actorAdminUserId: null,
            ip: $request->ip(),
        );

        return response()->json([
            'deal' => [
                'id' => $deal->id,
                'tenant_id' => $deal->tenant_id,
                'project_id' => $deal->project_id,
                'project_name' => $deal->project?->name,
                'phone' => $deal->phone,
                'contact_name' => $deal->contact_name,
                'comment' => $deal->comment,
                'status' => $deal->status,
                'manager_id' => $deal->manager_id,
                'manager_name' => $deal->manager
                    ? ManagerController::formatName($deal->manager->first_name, $deal->manager->last_name, $deal->manager->email)
                    : null,
                'manager_initials' => $deal->manager
                    ? ManagerController::formatInitials($deal->manager->first_name, $deal->manager->last_name, $deal->manager->email)
                    : null,
                'received_at' => $deal->received_at?->toIso8601String(),
                'assigned_at' => $deal->assigned_at?->toIso8601String(),
                'project_signal_type' => $deal->project?->signal_type,
                'project_signal_identifier' => $deal->project?->signal_identifier,
                'project_sms_keyword' => $deal->project?->sms_keyword,
                'project_sms_senders' => $deal->project?->sms_senders,
                // F2: стоимость лида = снимок rub-списания (копейки) или null (prepaid/не списано).
                'cost_kopecks' => ($charge && $charge->charge_source === 'rub') ? $charge->price_per_lead_kopecks : null,
            ],
            'events' => $events->map(fn (ActivityLog $e) => [
                'id' => $e->id,
                'event' => $e->event,
                'context' => $e->context,
                'created_at' => $e->created_at?->toIso8601String(),
                'actor' => $e->user
                    ? [
                        'id' => $e->user->id,
                        'name' => ManagerController::formatName($e->user->first_name, $e->user->last_name, $e->user->email),
                        'initials' => ManagerController::formatInitials($e->user->first_name, $e->user->last_name, $e->user->email),
                    ]
                    : null,
            ]),
        ]);
    }

    /**
     * PATCH /api/deals/{id} — частичное редактирование сделки из DealDetailDrawer.
     *
     * Body (все поля optional, должно быть хотя бы одно): {comment?,
     * manager_id?, status?}.
     *
     * Каждое изменение пишется в ActivityLog с правильным event-type:
     *   - comment → deal.commented (context.text — новый комментарий полностью)
     *   - manager_id → deal.assigned (context.from/to)
     *   - status → deal.status_changed (context.from/to/source='manual')
     *
     * NO-OP (значение не меняется) — ActivityLog НЕ пишется.
     *
     * Manager FK guard: новый manager_id должен принадлежать tenant'у (как в store).
     * Status validation: slug должен существовать в lead_statuses (как в transition).
     *
     * RLS + defense-in-depth where(tenant_id) — 404 если сделка чужая.
     */
    public function update(Request $request, int $id): JsonResponse
    {
        $validated = $request->validate([
            'comment' => 'nullable|string|max:5000',
            'manager_id' => 'nullable|integer|min:1',
            'status' => 'nullable|string|max:50',
        ]);

        $tenantId = (int) $request->user()->tenant_id;

        // Validate status slug если передан.
        if (array_key_exists('status', $validated) && $validated['status'] !== null) {
            $statusExists = DB::table('lead_statuses')->where('slug', $validated['status'])->exists();
            if (! $statusExists) {
                return response()->json([
                    'message' => 'Неизвестный статус.',
                    'errors' => ['status' => ['Slug не найден в lead_statuses.']],
                ], 422);
            }
        }

        // Manager FK guard.
        if (array_key_exists('manager_id', $validated) && $validated['manager_id'] !== null) {
            $managerExists = User::query()
                ->where('id', $validated['manager_id'])
                ->where('tenant_id', $tenantId)
                ->whereNull('deleted_at')
                ->where('is_active', true)
                ->exists();
            if (! $managerExists) {
                return response()->json([
                    'message' => 'Менеджер не найден в этом тенанте.',
                    'errors' => ['manager_id' => ['Не принадлежит вашему тенанту или не активен.']],
                ], 422);
            }
        }

        $deal = DB::transaction(function () use ($validated, $tenantId, $id) {
            DB::statement('SET LOCAL app.current_tenant_id = '.$tenantId);

            $deal = Deal::query()
                ->where('tenant_id', $tenantId)
                ->where('id', $id)
                ->first();

            if ($deal === null) {
                return null;
            }

            // Применяем изменения с записью ActivityLog для каждого изменённого поля.
            if (array_key_exists('comment', $validated) && $deal->comment !== $validated['comment']) {
                $deal->comment = $validated['comment'];
                ActivityLog::create([
                    'tenant_id' => $tenantId,
                    'user_id' => request()->user()?->id,
                    'deal_id' => $deal->id,
                    'event' => 'deal.commented',
                    'context' => ['text' => $validated['comment'] ?? ''],
                    'ip_address' => request()->ip(),
                    'user_agent' => request()->userAgent(),
                ]);
            }

            if (array_key_exists('manager_id', $validated) && $deal->manager_id !== $validated['manager_id']) {
                $previousManager = $deal->manager_id;
                $deal->manager_id = $validated['manager_id'];
                $deal->assigned_at = $validated['manager_id'] !== null ? now() : null;
                ActivityLog::create([
                    'tenant_id' => $tenantId,
                    'user_id' => request()->user()?->id,
                    'deal_id' => $deal->id,
                    'event' => ActivityLog::EVENT_DEAL_ASSIGNED,
                    'context' => ['from' => $previousManager, 'to' => $validated['manager_id']],
                    'ip_address' => request()->ip(),
                    'user_agent' => request()->userAgent(),
                ]);
            }

            if (array_key_exists('status', $validated) && $validated['status'] !== null && $deal->status !== $validated['status']) {
                $previousStatus = $deal->status;
                $deal->status = $validated['status'];
                ActivityLog::create([
                    'tenant_id' => $tenantId,
                    'user_id' => request()->user()?->id,
                    'deal_id' => $deal->id,
                    'event' => ActivityLog::EVENT_DEAL_STATUS_CHANGED,
                    'context' => ['from' => $previousStatus, 'to' => $validated['status'], 'source' => 'manual'],
                    'ip_address' => request()->ip(),
                    'user_agent' => request()->userAgent(),
                ]);
            }

            $deal->save();

            return $deal;
        });

        if ($deal === null) {
            return response()->json(['message' => 'Сделка не найдена.'], 404);
        }

        return response()->json([
            'deal' => [
                'id' => $deal->id,
                'tenant_id' => $deal->tenant_id,
                'project_id' => $deal->project_id,
                'phone' => $deal->phone,
                'contact_name' => $deal->contact_name,
                'comment' => $deal->comment,
                'status' => $deal->status,
                'manager_id' => $deal->manager_id,
                'received_at' => $deal->received_at?->toIso8601String(),
                'assigned_at' => $deal->assigned_at?->toIso8601String(),
                'project_signal_type' => $deal->project?->signal_type,
                'project_signal_identifier' => $deal->project?->signal_identifier,
                'project_sms_keyword' => $deal->project?->sms_keyword,
                'project_sms_senders' => $deal->project?->sms_senders,
            ],
        ]);
    }

    /** POST /api/deals — manual create */
    public function store(Request $request, PdAuditLogger $pdLog): JsonResponse
    {
        $validated = $request->validate([
            'project_name' => 'required|string|max:255',
            'phone' => 'required|string|max:20',
            'contact_name' => 'nullable|string|max:255',
            'status' => 'nullable|string|max:50',
            'manager_id' => 'nullable|integer|min:1',
            'comment' => 'nullable|string|max:5000',
        ]);

        $tenantId = (int) $request->user()->tenant_id;

        // Manager FK guard: если manager_id передан, он должен принадлежать
        // этому tenant'у. Иначе можно назначить чужого менеджера на свою сделку.
        if (isset($validated['manager_id'])) {
            $managerExists = User::query()
                ->where('id', $validated['manager_id'])
                ->where('tenant_id', $tenantId)
                ->whereNull('deleted_at')
                ->where('is_active', true)
                ->exists();
            if (! $managerExists) {
                return response()->json([
                    'message' => 'Менеджер не найден в этом тенанте.',
                    'errors' => ['manager_id' => ['Не принадлежит вашему тенанту или не активен.']],
                ], 422);
            }
        }

        $statusSlug = $validated['status'] ?? 'new';

        // Транзакция + RLS: SET LOCAL внутри (PgBouncer-safe).
        $deal = DB::transaction(function () use ($validated, $tenantId, $statusSlug) {
            DB::statement('SET LOCAL app.current_tenant_id = '.$tenantId);

            $project = Project::firstOrCreate(
                ['tenant_id' => $tenantId, 'name' => $validated['project_name']],
                ['type' => 'manual'],
            );

            $deal = Deal::create([
                'tenant_id' => $tenantId,
                'source_crm_id' => null, // manual
                'project_id' => $project->id,
                'phone' => $validated['phone'],
                'status' => $statusSlug,
                'contact_name' => $validated['contact_name'] ?? null,
                'comment' => $validated['comment'] ?? null,
                'manager_id' => $validated['manager_id'] ?? null,
                'assigned_at' => isset($validated['manager_id']) ? now() : null,
                'received_at' => now(),
            ]);

            // SupplierLeadCost для manual-leads — если у проекта есть активный
            // supplier через project_suppliers (m2m). Manual НЕ списывает
            // баланс (Ю-2: реселлерская модель работает только при закупке у
            // supplier'а), но cost-аналитика всё равно нужна — owner проекта
            // мог самостоятельно купить лид и ввести руками.
            $resolver = app(SupplierResolver::class);
            $supplierId = $resolver->resolveForProject($project);
            if ($supplierId !== null) {
                SupplierLeadCost::create([
                    'deal_id' => $deal->id,
                    'received_at' => $deal->received_at,
                    'supplier_id' => $supplierId,
                    'cost_rub' => $resolver->costRubSnapshot($supplierId),
                    'supplier_lead_id' => null, // manual: нет внешнего id
                    'created_at' => now(),
                ]);
            }

            ActivityLog::create([
                'tenant_id' => $tenantId,
                'user_id' => request()->user()?->id,
                'deal_id' => $deal->id,
                'event' => ActivityLog::EVENT_DEAL_CREATED,
                'context' => ['source' => 'manual'],
                'ip_address' => request()->ip(),
                'user_agent' => request()->userAgent(),
            ]);

            return $deal;
        });

        $pdLog->record(
            action: 'created', subjectType: 'lead', subjectId: $deal->id,
            purpose: 'lead_create_manual', tenantId: (int) $deal->tenant_id,
            actorTenantUserId: (int) $request->user()->id,
            actorAdminUserId: null, ip: $request->ip(),
        );

        return response()->json([
            'deal' => [
                'id' => $deal->id,
                'tenant_id' => $deal->tenant_id,
                'project_id' => $deal->project_id,
                'phone' => $deal->phone,
                'status' => $deal->status,
                'contact_name' => $deal->contact_name,
                'manager_id' => $deal->manager_id,
                'received_at' => $deal->received_at->toIso8601String(),
            ],
            'message' => 'Сделка создана.',
        ], 201);
    }
}
