<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\BalanceTransaction;
use App\Models\Deal;
use App\Models\LeadCharge;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\StreamedResponse;

/**
 * Tenant-scoped доступ к lead_charges (read-only ledger) — Plan 4 Task 11.
 *
 * RLS защищает изоляцию через SetTenantContext middleware
 * (auth:sanctum + tenant). См. spec §6.3:
 * docs/superpowers/specs/2026-05-11-plan4-billing-csv-admin-design.md.
 *
 * Эндпоинты:
 *   GET  /api/billing/charges?page=N&period=current_month|last_month|90d&charge_source=prepaid|rub
 *   POST /api/billing/charges/export — CSV через StreamedResponse + chunkById(500).
 *
 * RLS-контекст устанавливается на уровне middleware `tenant` (SetTenantContext),
 * который оборачивает HTTP-запрос в транзакцию с `SET LOCAL app.current_tenant_id`.
 * Для StreamedResponse callback'а транзакция middleware'а уже закрыта на момент
 * вызова — поэтому export открывает свою транзакцию внутри callback'а с явным
 * SET LOCAL (паттерн из DealExportController).
 */
class TenantChargesController extends Controller
{
    public function index(Request $request): JsonResponse
    {
        $tenantId = (int) $request->user()->tenant_id;

        // Explicit tenant_id фильтр — defense-in-depth поверх RLS. В тестах PG
        // superuser BYPASSRLS, и без явного where() запрос вернул бы строки
        // других тенантов (см. InAppNotificationController паттерн).
        $query = LeadCharge::query()
            ->where('tenant_id', $tenantId)
            ->orderBy('charged_at', 'desc');

        $this->applyFilters($query, $request);

        $page = $query->paginate(20);

        return response()->json([
            'data' => $page->items(),
            'meta' => [
                'current_page' => $page->currentPage(),
                'last_page' => $page->lastPage(),
                'total' => $page->total(),
                'per_page' => $page->perPage(),
            ],
        ]);
    }

    public function export(Request $request): StreamedResponse
    {
        $tenantId = (int) $request->user()->tenant_id;
        $period = $request->input('period');
        $source = $request->input('charge_source');

        $filename = 'charges_'.now()->format('Y-m-d_His').'.csv';

        return new StreamedResponse(function () use ($tenantId, $period, $source) {
            // RLS-контекст должен быть установлен внутри транзакции на момент
            // фактического SELECT. StreamedResponse callback вызывается уже после
            // закрытия middleware-транзакции, поэтому открываем свою явную.
            // Паттерн из DealExportController.
            DB::transaction(function () use ($tenantId, $period, $source) {
                DB::statement('SET LOCAL app.current_tenant_id = '.$tenantId);

                $out = fopen('php://output', 'w');
                if ($out === false) {
                    return;
                }
                // BOM для Excel (CSV в UTF-8).
                fwrite($out, "\xEF\xBB\xBF");
                fputcsv($out, ['charged_at', 'deal_id', 'tier_no', 'charge_source', 'price_rub', 'balance_rub_after']);

                // Explicit tenant_id фильтр — defense-in-depth поверх RLS
                // (см. комментарий в index()).
                // LEFT JOIN balance_transactions для заполнения balance_rub_after.
                // Условие type='lead_charge' исключает topup/refund которые тоже
                // могут ссылаться на deal через related_id.
                $query = DB::table('lead_charges as lc')
                    ->select([
                        'lc.id',
                        'lc.charged_at',
                        'lc.deal_id',
                        'lc.tier_no',
                        'lc.charge_source',
                        'lc.price_per_lead_kopecks',
                        'bt.balance_rub_after',
                    ])
                    ->leftJoin('balance_transactions as bt', function ($j) use ($tenantId) {
                        $j->on('bt.related_id', '=', 'lc.deal_id')
                            ->where('bt.related_type', '=', Deal::class)
                            ->where('bt.type', '=', BalanceTransaction::TYPE_LEAD_CHARGE)
                            ->where('bt.tenant_id', '=', $tenantId);
                    })
                    ->where('lc.tenant_id', $tenantId)
                    ->orderBy('lc.charged_at', 'desc')
                    ->orderBy('lc.id', 'desc');

                if (is_string($period) && $period !== '') {
                    $now = Carbon::now('Europe/Moscow');
                    if ($period === 'current_month') {
                        $query->where('lc.charged_at', '>=', $now->copy()->startOfMonth());
                    } elseif ($period === 'last_month') {
                        $query->whereBetween('lc.charged_at', [
                            $now->copy()->subMonth()->startOfMonth(),
                            $now->copy()->subMonth()->endOfMonth(),
                        ]);
                    } elseif ($period === '90d') {
                        $query->where('lc.charged_at', '>=', $now->copy()->subDays(90));
                    }
                }
                if ($source !== null && $source !== '') {
                    $query->where('lc.charge_source', $source);
                }

                // chunk() вместо chunkById() — chunkById несовместим с JOIN-запросами
                // (ломает пагинацию при неуникальном id в select).
                $query->chunk(500, function ($rows) use ($out) {
                    foreach ($rows as $r) {
                        fputcsv($out, [
                            Carbon::parse($r->charged_at)->toIso8601String(),
                            (string) $r->deal_id,
                            (string) $r->tier_no,
                            (string) $r->charge_source,
                            number_format($r->price_per_lead_kopecks / 100, 2, '.', ''),
                            $r->balance_rub_after ?? '',
                        ]);
                    }
                });

                fclose($out);
            });
        }, 200, [
            'Content-Type' => 'text/csv; charset=UTF-8',
            'Content-Disposition' => "attachment; filename=\"{$filename}\"",
        ]);
    }

    /**
     * @param  Builder<LeadCharge>  $query
     */
    private function applyFilters($query, Request $request): void
    {
        $this->applyPeriodFilter($query, $request->query('period'));

        $source = $request->query('charge_source');
        if (is_string($source) && $source !== '') {
            $query->where('charge_source', $source);
        }
    }

    /**
     * @param  Builder<LeadCharge>  $query
     */
    private function applyPeriodFilter($query, mixed $period): void
    {
        if (! is_string($period) || $period === '') {
            return;
        }

        $now = Carbon::now('Europe/Moscow');

        if ($period === 'current_month') {
            $query->where('charged_at', '>=', $now->copy()->startOfMonth());
        } elseif ($period === 'last_month') {
            $query->whereBetween('charged_at', [
                $now->copy()->subMonth()->startOfMonth(),
                $now->copy()->subMonth()->endOfMonth(),
            ]);
        } elseif ($period === '90d') {
            $query->where('charged_at', '>=', $now->copy()->subDays(90));
        }
    }
}
