<?php

namespace App\Http\Controllers;

use App\Models\Cliente;
use App\Models\MetodoPago;
use App\Models\Venta;
use App\Models\VentaPago;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
use RuntimeException;
use Symfony\Component\HttpFoundation\StreamedResponse;

class CuentasCobrarController extends Controller
{
    public function index(Request $request): View
    {
        $empresaId = tenant_empresa_id();
        abort_if(! $empresaId, 403, 'No hay empresa activa.');

        $filters = $this->readFilters($request);
        $query = $this->buildFilterQuery($filters);
        $queryString = $query !== '' ? ('?'.$query) : '';

        $report = $this->loadReportData($empresaId, $filters);

        $customers = Cliente::query()
            ->where('empresa_id', $empresaId)
            ->where('activo', 1)
            ->orderBy('nombre')
            ->get(['id', 'nombre'])
            ->map(static fn (Cliente $c): array => [
                'id' => (int) $c->id,
                'nombre' => (string) $c->nombre,
            ])
            ->all();

        $payments = $this->recentPayments($empresaId);

        return view('modules.cuentas_cobrar.index', [
            'title' => 'Cuentas por cobrar',
            'summary' => $report['summary'],
            'accounts' => $report['accounts'],
            'aging' => $report['aging'],
            'payments' => $payments,
            'paymentMethods' => $this->paymentMethodsPayload($empresaId),
            'customers' => $customers,
            'filters' => $filters,
            'exportCsvUrl' => route('cuentas-cobrar.export.csv').$queryString,
            'exportXlsxUrl' => route('cuentas-cobrar.export.xlsx').$queryString,
            'exportPdfUrl' => route('cuentas-cobrar.export.pdf').$queryString,
        ]);
    }

    public function collect(Request $request, Venta $venta): RedirectResponse
    {
        $empresaId = tenant_empresa_id();
        abort_if(! $empresaId, 403, 'No hay empresa activa.');

        if ((int) $venta->empresa_id !== (int) $empresaId) {
            abort(404, 'La venta no pertenece al tenant activo.');
        }

        $validated = $request->validate([
            'monto' => ['required', 'numeric', 'gt:0'],
            'metodo_pago' => ['required', 'string', 'max:50'],
            'referencia' => ['nullable', 'string', 'max:120'],
        ]);

        try {
            DB::transaction(function () use ($empresaId, $venta, $validated): void {
                $sale = Venta::query()
                    ->where('empresa_id', $empresaId)
                    ->whereKey($venta->id)
                    ->lockForUpdate()
                    ->first();

                if (! $sale) {
                    throw ValidationException::withMessages(['venta' => 'Venta no encontrada.']);
                }

                if ((string) $sale->estado === 'anulada') {
                    throw ValidationException::withMessages(['venta' => 'No se puede abonar a una venta anulada.']);
                }

                $pagadoActual = (float) VentaPago::query()
                    ->where('empresa_id', $empresaId)
                    ->where('venta_id', $sale->id)
                    ->sum('monto');

                $saldo = round((float) $sale->total - $pagadoActual, 2);
                $monto = round((float) $validated['monto'], 2);

                if ($saldo <= 0) {
                    throw ValidationException::withMessages(['monto' => 'La venta ya se encuentra pagada.']);
                }

                if ($monto > $saldo + 0.0001) {
                    throw ValidationException::withMessages(['monto' => 'El monto excede el saldo pendiente ('.number_format($saldo, 2).').']);
                }

                VentaPago::query()->create([
                    'venta_id' => (int) $sale->id,
                    'empresa_id' => $empresaId,
                    'metodo_pago' => strtolower(trim((string) $validated['metodo_pago'])),
                    'monto' => $monto,
                    'referencia' => trim((string) ($validated['referencia'] ?? '')) !== ''
                        ? trim((string) $validated['referencia'])
                        : null,
                    'created_at' => now(),
                ]);

                $nuevoPagado = $pagadoActual + $monto;
                $nuevoEstado = $nuevoPagado + 0.0001 >= (float) $sale->total ? 'pagada' : 'pendiente';
                if ((string) $sale->estado !== $nuevoEstado) {
                    $sale->estado = $nuevoEstado;
                    $sale->save();
                }
            });
        } catch (ValidationException $e) {
            return redirect()->route('cuentas-cobrar.index')
                ->withErrors($e->errors())
                ->with('error', collect($e->errors())->flatten()->first() ?: 'No fue posible registrar el abono.');
        } catch (\Throwable $e) {
            return redirect()->route('cuentas-cobrar.index')
                ->with('error', 'No fue posible registrar el abono: '.$e->getMessage());
        }

        return redirect()->route('cuentas-cobrar.index')
            ->with('status', 'Abono registrado correctamente.');
    }

    public function exportCsv(Request $request): StreamedResponse
    {
        $empresaId = tenant_empresa_id();
        abort_if(! $empresaId, 403, 'No hay empresa activa.');

        $filters = $this->readFilters($request);
        $report = $this->loadReportData($empresaId, $filters);
        $filename = 'cartera_'.date('Ymd_His').'.csv';

        return response()->streamDownload(function () use ($report, $filters): void {
            $out = fopen('php://output', 'wb');
            fwrite($out, "\xEF\xBB\xBF");
            fputcsv($out, ['Reporte de cartera - Veltrix POS'], ';');
            fputcsv($out, ['Generado', date('Y-m-d H:i:s')], ';');
            fputcsv($out, ['Filtros', $this->filterDescription($filters)], ';');
            fputcsv($out, [''], ';');
            fputcsv($out, ['Venta', 'Fecha', 'Cliente', 'Documento', 'Cajero', 'Total', 'Abonado', 'Saldo', 'Dias cartera', 'Rango', 'Estado'], ';');

            foreach ($report['accounts'] as $row) {
                fputcsv($out, [
                    (string) $row['id'],
                    (string) $row['fecha'],
                    $this->sanitizeCsvString((string) ($row['cliente'] ?? 'Consumidor final')),
                    $this->sanitizeCsvString((string) ($row['cliente_documento'] ?? '')),
                    $this->sanitizeCsvString((string) ($row['cajero'] ?? '')),
                    number_format((float) $row['total'], 2, '.', ''),
                    number_format((float) $row['pagado'], 2, '.', ''),
                    number_format((float) $row['saldo'], 2, '.', ''),
                    (string) (int) $row['dias_cartera'],
                    (string) $row['rango_antiguedad'],
                    (string) $row['estado_cobro'],
                ], ';');
            }

            fputcsv($out, [''], ';');
            fputcsv($out, [
                'Totales', '', '', '', '',
                number_format((float) $report['summary']['total_credito'], 2, '.', ''),
                number_format((float) $report['summary']['total_abonado'], 2, '.', ''),
                number_format((float) $report['summary']['total_saldo'], 2, '.', ''),
                '', '', '',
            ], ';');

            fputcsv($out, ['Antiguedad', 'Saldo', 'Cuentas'], ';');
            foreach (['0_30' => '0-30', '31_60' => '31-60', '61_90' => '61-90', '91_mas' => '91+'] as $key => $label) {
                fputcsv($out, [
                    $label,
                    number_format((float) $report['aging']['saldo_'.$key], 2, '.', ''),
                    (string) (int) $report['aging']['cuentas_'.$key],
                ], ';');
            }
            fclose($out);
        }, $filename, [
            'Content-Type' => 'text/csv; charset=UTF-8',
        ]);
    }

    public function exportXlsx(Request $request): Response
    {
        $empresaId = tenant_empresa_id();
        abort_if(! $empresaId, 403, 'No hay empresa activa.');

        $filters = $this->readFilters($request);
        $report = $this->loadReportData($empresaId, $filters);

        $rows = $this->buildSpreadsheetRows($filters, $report);
        $binary = $this->buildXlsxBinary($rows, 'Cartera');
        $filename = 'cartera_'.date('Ymd_His').'.xlsx';

        return response($binary, 200, [
            'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            'Content-Disposition' => 'attachment; filename="'.$filename.'"',
            'Cache-Control' => 'max-age=0',
        ]);
    }

    public function exportPdf(Request $request): Response
    {
        $empresaId = tenant_empresa_id();
        abort_if(! $empresaId, 403, 'No hay empresa activa.');

        $filters = $this->readFilters($request);
        $report = $this->loadReportData($empresaId, $filters);

        $lines = $this->buildPdfLines($filters, $report);
        $binary = $this->buildPdfBinary($lines);
        $filename = 'cartera_'.date('Ymd_His').'.pdf';

        return response($binary, 200, [
            'Content-Type' => 'application/pdf',
            'Content-Disposition' => 'attachment; filename="'.$filename.'"',
            'Cache-Control' => 'private, max-age=0, must-revalidate',
            'Pragma' => 'public',
        ]);
    }

    private function loadReportData(int $empresaId, array $filters): array
    {
        $accounts = $this->openSales($empresaId, $filters);
        $summary = $this->summarize($accounts);
        $aging = $this->agingSummary($accounts);

        return [
            'accounts' => $accounts,
            'summary' => $summary,
            'aging' => $aging,
        ];
    }

    private function paymentMethodsPayload(int $empresaId): array
    {
        $paymentMethods = MetodoPago::query()
            ->where('activo', 1)
            ->where(static function ($query) use ($empresaId): void {
                $query->where('empresa_id', $empresaId)
                    ->orWhereNull('empresa_id');
            })
            ->orderByRaw('CASE WHEN empresa_id IS NULL THEN 1 ELSE 0 END')
            ->orderBy('nombre')
            ->get(['empresa_id', 'codigo', 'nombre']);

        if ($paymentMethods->isEmpty()) {
            $paymentMethods = collect([
                ['codigo' => 'efectivo', 'nombre' => 'Efectivo'],
                ['codigo' => 'tarjeta', 'nombre' => 'Tarjeta'],
                ['codigo' => 'transferencia', 'nombre' => 'Transferencia'],
            ]);
        }

        return $paymentMethods
            ->map(static function ($method): array {
                if (is_array($method)) {
                    return [
                        'codigo' => strtolower(trim((string) ($method['codigo'] ?? 'efectivo'))),
                        'nombre' => trim((string) ($method['nombre'] ?? 'Metodo')),
                    ];
                }

                return [
                    'codigo' => strtolower(trim((string) ($method->codigo ?? 'efectivo'))),
                    'nombre' => trim((string) ($method->nombre ?? 'Metodo')),
                ];
            })
            ->unique('codigo')
            ->values()
            ->all();
    }

    private function openSales(int $empresaId, array $filters): array
    {
        $tenantSucursalId = tenant_sucursal_id();

        $query = Venta::query()
            ->from('ventas as v')
            ->leftJoin('clientes as c', 'c.id', '=', 'v.cliente_id')
            ->leftJoin('usuarios as u', 'u.id', '=', 'v.usuario_id')
            ->where('v.empresa_id', $empresaId)
            ->where('v.estado', '<>', 'anulada')
            ->when($tenantSucursalId, static function ($q, int $sucursalId): void {
                $q->where(static function ($nested) use ($sucursalId): void {
                    $nested->whereNull('v.sucursal_id')->orWhere('v.sucursal_id', $sucursalId);
                });
            });

        if (($filters['cliente_id'] ?? 0) > 0) {
            $query->where('v.cliente_id', (int) $filters['cliente_id']);
        }

        if (! empty($filters['fecha_desde'])) {
            $query->whereDate('v.fecha', '>=', $filters['fecha_desde']);
        }
        if (! empty($filters['fecha_hasta'])) {
            $query->whereDate('v.fecha', '<=', $filters['fecha_hasta']);
        }

        $rows = $query
            ->select([
                'v.id', 'v.fecha', 'v.total', 'v.estado',
                'c.nombre as cliente_nombre', 'c.documento as cliente_documento',
                'u.nombre as cajero_nombre',
                DB::raw('(SELECT COALESCE(SUM(monto),0) FROM venta_pagos vp WHERE vp.venta_id = v.id AND vp.empresa_id = v.empresa_id) as pagado'),
            ])
            ->orderBy('v.fecha')
            ->orderBy('v.id')
            ->get();

        $estadoFiltro = (string) ($filters['estado'] ?? 'todos');
        $today = new \DateTimeImmutable('today');
        $accounts = [];

        foreach ($rows as $row) {
            $total = (float) $row->total;
            $pagado = (float) $row->pagado;
            $saldo = round($total - $pagado, 2);

            if ($saldo <= 0) {
                continue;
            }

            $estadoCobro = $pagado <= 0 ? 'pendiente' : 'parcial';
            if ($estadoFiltro !== 'todos' && $estadoFiltro !== $estadoCobro) {
                continue;
            }

            $fecha = $row->fecha instanceof \DateTimeInterface
                ? $row->fecha
                : new \DateTimeImmutable((string) $row->fecha);
            $dias = max(0, (int) $today->diff($fecha)->format('%r%a') * -1);
            if ($dias < 0) {
                $dias = 0;
            }
            $rango = $this->agingRange($dias);

            $accounts[] = [
                'id' => (int) $row->id,
                'fecha' => $fecha->format('Y-m-d H:i:s'),
                'cliente' => (string) ($row->cliente_nombre ?? 'Consumidor final'),
                'cliente_documento' => (string) ($row->cliente_documento ?? ''),
                'cajero' => (string) ($row->cajero_nombre ?? ''),
                'total' => $total,
                'pagado' => $pagado,
                'saldo' => $saldo,
                'dias_cartera' => $dias,
                'rango_antiguedad' => $rango,
                'estado_cobro' => $estadoCobro,
            ];
        }

        return $accounts;
    }

    private function summarize(array $accounts): array
    {
        $totalCredito = 0.0;
        $totalAbonado = 0.0;
        $totalSaldo = 0.0;
        $pendientes = 0;
        $parciales = 0;

        foreach ($accounts as $a) {
            $totalCredito += (float) $a['total'];
            $totalAbonado += (float) $a['pagado'];
            $totalSaldo += (float) $a['saldo'];
            if ($a['estado_cobro'] === 'parcial') {
                $parciales++;
            } else {
                $pendientes++;
            }
        }

        return [
            'total_credito' => round($totalCredito, 2),
            'total_abonado' => round($totalAbonado, 2),
            'total_saldo' => round($totalSaldo, 2),
            'cuentas_pendientes' => $pendientes,
            'cuentas_parciales' => $parciales,
        ];
    }

    private function agingSummary(array $accounts): array
    {
        $buckets = [
            '0_30' => ['saldo' => 0.0, 'cuentas' => 0],
            '31_60' => ['saldo' => 0.0, 'cuentas' => 0],
            '61_90' => ['saldo' => 0.0, 'cuentas' => 0],
            '91_mas' => ['saldo' => 0.0, 'cuentas' => 0],
        ];

        foreach ($accounts as $a) {
            $dias = (int) $a['dias_cartera'];
            $key = match (true) {
                $dias <= 30 => '0_30',
                $dias <= 60 => '31_60',
                $dias <= 90 => '61_90',
                default => '91_mas',
            };
            $buckets[$key]['saldo'] += (float) $a['saldo'];
            $buckets[$key]['cuentas']++;
        }

        return [
            'saldo_0_30' => round($buckets['0_30']['saldo'], 2),
            'saldo_31_60' => round($buckets['31_60']['saldo'], 2),
            'saldo_61_90' => round($buckets['61_90']['saldo'], 2),
            'saldo_91_mas' => round($buckets['91_mas']['saldo'], 2),
            'cuentas_0_30' => $buckets['0_30']['cuentas'],
            'cuentas_31_60' => $buckets['31_60']['cuentas'],
            'cuentas_61_90' => $buckets['61_90']['cuentas'],
            'cuentas_91_mas' => $buckets['91_mas']['cuentas'],
        ];
    }

    private function agingRange(int $dias): string
    {
        return match (true) {
            $dias <= 30 => '0-30',
            $dias <= 60 => '31-60',
            $dias <= 90 => '61-90',
            default => '91+',
        };
    }

    private function recentPayments(int $empresaId, int $limit = 25): array
    {
        $rows = VentaPago::query()
            ->from('venta_pagos as vp')
            ->leftJoin('ventas as v', 'v.id', '=', 'vp.venta_id')
            ->leftJoin('clientes as c', 'c.id', '=', 'v.cliente_id')
            ->where('vp.empresa_id', $empresaId)
            ->select([
                'vp.id', 'vp.venta_id', 'vp.metodo_pago', 'vp.monto', 'vp.referencia', 'vp.created_at',
                'c.nombre as cliente_nombre',
            ])
            ->orderByDesc('vp.id')
            ->limit($limit)
            ->get();

        return $rows->map(static fn ($r): array => [
            'id' => (int) $r->id,
            'venta_id' => (int) $r->venta_id,
            'cliente' => (string) ($r->cliente_nombre ?? 'Consumidor final'),
            'fecha_pago' => (string) ($r->created_at ?? ''),
            'metodo_pago' => (string) ($r->metodo_pago ?? ''),
            'referencia' => (string) ($r->referencia ?? ''),
            'monto' => (float) $r->monto,
        ])->all();
    }

    private function readFilters(Request $request): array
    {
        $status = strtolower(trim((string) $request->query('estado', 'todos')));
        if (! in_array($status, ['todos', 'pendiente', 'parcial'], true)) {
            $status = 'todos';
        }

        $dateFrom = trim((string) $request->query('fecha_desde', ''));
        if ($dateFrom !== '' && ! preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom)) {
            $dateFrom = '';
        }

        $dateTo = trim((string) $request->query('fecha_hasta', ''));
        if ($dateTo !== '' && ! preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo)) {
            $dateTo = '';
        }

        return [
            'cliente_id' => max(0, (int) $request->query('cliente_id', 0)),
            'fecha_desde' => $dateFrom,
            'fecha_hasta' => $dateTo,
            'estado' => $status,
        ];
    }

    private function buildFilterQuery(array $filters): string
    {
        $params = [];
        if ((int) ($filters['cliente_id'] ?? 0) > 0) {
            $params['cliente_id'] = (int) $filters['cliente_id'];
        }
        if (! empty($filters['fecha_desde'])) {
            $params['fecha_desde'] = (string) $filters['fecha_desde'];
        }
        if (! empty($filters['fecha_hasta'])) {
            $params['fecha_hasta'] = (string) $filters['fecha_hasta'];
        }
        $estado = (string) ($filters['estado'] ?? 'todos');
        if ($estado !== '' && $estado !== 'todos') {
            $params['estado'] = $estado;
        }

        return http_build_query($params);
    }

    private function filterDescription(array $filters): string
    {
        return 'Cliente ID: '.(($filters['cliente_id'] ?? 0) > 0 ? (int) $filters['cliente_id'] : 'Todos')
            .' | Desde: '.(($filters['fecha_desde'] ?? '') !== '' ? (string) $filters['fecha_desde'] : 'N/A')
            .' | Hasta: '.(($filters['fecha_hasta'] ?? '') !== '' ? (string) $filters['fecha_hasta'] : 'N/A')
            .' | Estado: '.(string) ($filters['estado'] ?? 'todos');
    }

    private function sanitizeCsvString(string $value): string
    {
        $trimmed = ltrim($value);
        if ($trimmed !== '' && preg_match('/^[=\-+@]/', $trimmed) === 1) {
            return "'".$value;
        }

        return $value;
    }

    private function buildSpreadsheetRows(array $filters, array $report): array
    {
        $rows = [];
        $rows[] = ['Reporte de cartera - Veltrix POS'];
        $rows[] = ['Generado', date('Y-m-d H:i:s')];
        $rows[] = ['Filtros', $this->filterDescription($filters)];
        $rows[] = [''];
        $rows[] = ['Venta', 'Fecha', 'Cliente', 'Documento', 'Cajero', 'Total', 'Abonado', 'Saldo', 'Dias cartera', 'Rango', 'Estado'];

        foreach ($report['accounts'] as $row) {
            $rows[] = [
                (int) $row['id'],
                (string) $row['fecha'],
                (string) $row['cliente'],
                (string) $row['cliente_documento'],
                (string) $row['cajero'],
                (float) $row['total'],
                (float) $row['pagado'],
                (float) $row['saldo'],
                (int) $row['dias_cartera'],
                (string) $row['rango_antiguedad'],
                (string) $row['estado_cobro'],
            ];
        }

        $rows[] = [''];
        $rows[] = ['Totales', '', '', '', '',
            (float) $report['summary']['total_credito'],
            (float) $report['summary']['total_abonado'],
            (float) $report['summary']['total_saldo'],
            '', '', '',
        ];
        $rows[] = [''];
        $rows[] = ['Antiguedad', 'Saldo', 'Cuentas'];
        foreach (['0_30' => '0-30', '31_60' => '31-60', '61_90' => '61-90', '91_mas' => '91+'] as $k => $label) {
            $rows[] = [$label, (float) $report['aging']['saldo_'.$k], (int) $report['aging']['cuentas_'.$k]];
        }

        return $rows;
    }

    private function buildXlsxBinary(array $rows, string $sheetName): string
    {
        if (! class_exists(\ZipArchive::class)) {
            throw new RuntimeException('La extension ZIP de PHP no esta habilitada para exportar XLSX.');
        }

        $tempFile = tempnam(sys_get_temp_dir(), 'veltrix_xlsx_');
        if ($tempFile === false) {
            throw new RuntimeException('No fue posible crear archivo temporal para XLSX.');
        }

        $zip = new \ZipArchive();
        if ($zip->open($tempFile, \ZipArchive::OVERWRITE) !== true) {
            @unlink($tempFile);
            throw new RuntimeException('No fue posible abrir archivo ZIP para XLSX.');
        }

        $sheetXml = $this->buildWorksheetXml($rows);
        $safeSheetName = htmlspecialchars(substr($sheetName, 0, 31), ENT_XML1 | ENT_QUOTES, 'UTF-8');

        $zip->addFromString('[Content_Types].xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            .'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
            .'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
            .'<Default Extension="xml" ContentType="application/xml"/>'
            .'<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
            .'<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>'
            .'<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>'
            .'</Types>');

        $zip->addFromString('_rels/.rels', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            .'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
            .'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>'
            .'</Relationships>');

        $zip->addFromString('xl/workbook.xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            .'<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"'
            .' xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
            .'<sheets><sheet name="'.$safeSheetName.'" sheetId="1" r:id="rId1"/></sheets>'
            .'</workbook>');

        $zip->addFromString('xl/_rels/workbook.xml.rels', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            .'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
            .'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>'
            .'<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>'
            .'</Relationships>');

        $zip->addFromString('xl/styles.xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            .'<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
            .'<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>'
            .'<fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>'
            .'<borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>'
            .'<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>'
            .'<cellXfs count="2">'
            .'<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>'
            .'<xf numFmtId="4" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>'
            .'</cellXfs>'
            .'<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>'
            .'</styleSheet>');

        $zip->addFromString('xl/worksheets/sheet1.xml', $sheetXml);
        $zip->close();

        $binary = file_get_contents($tempFile);
        @unlink($tempFile);

        if ($binary === false) {
            throw new RuntimeException('No fue posible leer el archivo XLSX generado.');
        }

        return $binary;
    }

    private function buildWorksheetXml(array $rows): string
    {
        $sheetData = '';

        foreach ($rows as $rowIndex => $rowValues) {
            $excelRow = $rowIndex + 1;
            $sheetData .= '<row r="'.$excelRow.'">';

            foreach ((array) $rowValues as $colIndex => $value) {
                $col = $this->xlsxColumnName($colIndex + 1);
                $cellRef = $col.$excelRow;

                if (is_int($value) || is_float($value)) {
                    $sheetData .= '<c r="'.$cellRef.'" s="1"><v>'.(string) $value.'</v></c>';
                    continue;
                }

                $s = (string) $value;
                $trim = ltrim($s);
                if ($trim !== '' && preg_match('/^[=\-+@]/', $trim) === 1) {
                    $s = "'".$s;
                }
                $escaped = htmlspecialchars($s, ENT_XML1 | ENT_QUOTES, 'UTF-8');
                $spaceAttr = preg_match('/^\s|\s$/', $s) === 1 ? ' xml:space="preserve"' : '';
                $sheetData .= '<c r="'.$cellRef.'" t="inlineStr"><is><t'.$spaceAttr.'>'.$escaped.'</t></is></c>';
            }

            $sheetData .= '</row>';
        }

        return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
            .'<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
            .'<sheetData>'.$sheetData.'</sheetData>'
            .'</worksheet>';
    }

    private function xlsxColumnName(int $index): string
    {
        $name = '';
        while ($index > 0) {
            $mod = ($index - 1) % 26;
            $name = chr(65 + $mod).$name;
            $index = (int) floor(($index - 1) / 26);
        }

        return $name;
    }

    private function buildPdfLines(array $filters, array $report): array
    {
        $lines = [];
        $lines[] = 'Reporte de cartera - Veltrix POS';
        $lines[] = 'Generado: '.date('Y-m-d H:i:s');
        $lines[] = $this->filterDescription($filters);
        $lines[] = str_repeat('-', 110);
        $lines[] = sprintf('%-7s %-16s %-24s %12s %12s %12s %6s %-7s %-8s',
            'Venta', 'Fecha', 'Cliente', 'Total', 'Abonado', 'Saldo', 'Dias', 'Rango', 'Estado');
        $lines[] = str_repeat('-', 110);

        foreach ($report['accounts'] as $r) {
            $lines[] = sprintf('%-7s %-16s %-24s %12s %12s %12s %6s %-7s %-8s',
                '#'.$r['id'],
                substr((string) $r['fecha'], 0, 16),
                substr((string) $r['cliente'], 0, 24),
                number_format((float) $r['total'], 2, '.', ''),
                number_format((float) $r['pagado'], 2, '.', ''),
                number_format((float) $r['saldo'], 2, '.', ''),
                (string) (int) $r['dias_cartera'],
                (string) $r['rango_antiguedad'],
                (string) $r['estado_cobro']
            );
        }

        $lines[] = str_repeat('-', 110);
        $lines[] = 'Totales -> Credito: '.number_format((float) $report['summary']['total_credito'], 2, '.', '')
            .' | Abonado: '.number_format((float) $report['summary']['total_abonado'], 2, '.', '')
            .' | Saldo: '.number_format((float) $report['summary']['total_saldo'], 2, '.', '');
        $lines[] = 'Antiguedad -> 0-30: '.number_format((float) $report['aging']['saldo_0_30'], 2, '.', '')
            .' | 31-60: '.number_format((float) $report['aging']['saldo_31_60'], 2, '.', '')
            .' | 61-90: '.number_format((float) $report['aging']['saldo_61_90'], 2, '.', '')
            .' | 91+: '.number_format((float) $report['aging']['saldo_91_mas'], 2, '.', '');

        return $lines;
    }

    private function buildPdfBinary(array $lines): string
    {
        $linesPerPage = 52;
        $chunks = array_chunk($lines, $linesPerPage);
        if ($chunks === []) {
            $chunks = [['Reporte de cartera']];
        }

        $objects = [
            1 => '',
            2 => '',
            3 => '<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>',
        ];

        $nextId = 4;
        $pageIds = [];

        foreach ($chunks as $chunk) {
            $content = "BT\n/F1 9 Tf\n12 TL\n40 800 Td\n";
            foreach ($chunk as $i => $line) {
                $content .= '('.$this->escapePdfText($line).") Tj\n";
                if ($i < (count($chunk) - 1)) {
                    $content .= "T*\n";
                }
            }
            $content .= 'ET';

            $contentId = $nextId++;
            $objects[$contentId] = '<< /Length '.strlen($content)." >>\nstream\n".$content."\nendstream";
            $pageId = $nextId++;
            $objects[$pageId] = '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R >> >> /Contents '.$contentId.' 0 R >>';
            $pageIds[] = $pageId;
        }

        $kids = implode(' ', array_map(static fn (int $id): string => $id.' 0 R', $pageIds));
        $objects[2] = '<< /Type /Pages /Count '.count($pageIds).' /Kids [ '.$kids.' ] >>';
        $objects[1] = '<< /Type /Catalog /Pages 2 0 R >>';

        ksort($objects);
        $maxId = (int) max(array_keys($objects));

        $pdf = "%PDF-1.4\n";
        $offsets = [];
        for ($i = 1; $i <= $maxId; $i++) {
            if (! isset($objects[$i])) {
                $objects[$i] = '<<>>';
            }
            $offsets[$i] = strlen($pdf);
            $pdf .= $i." 0 obj\n".$objects[$i]."\nendobj\n";
        }

        $xrefPos = strlen($pdf);
        $pdf .= "xref\n";
        $pdf .= '0 '.($maxId + 1)."\n";
        $pdf .= "0000000000 65535 f \n";
        for ($i = 1; $i <= $maxId; $i++) {
            $pdf .= sprintf('%010d 00000 n ', $offsets[$i])."\n";
        }
        $pdf .= 'trailer'."\n".'<< /Size '.($maxId + 1).' /Root 1 0 R >>'."\n";
        $pdf .= 'startxref'."\n".$xrefPos."\n%%EOF";

        return $pdf;
    }

    private function escapePdfText(string $text): string
    {
        $normalized = $text;
        if (function_exists('iconv')) {
            $converted = @iconv('UTF-8', 'Windows-1252//TRANSLIT//IGNORE', $text);
            if ($converted !== false) {
                $normalized = $converted;
            }
        }
        $normalized = preg_replace('/[^\x20-\x7E\x80-\xFF]/', '', $normalized) ?? '';
        $normalized = str_replace('\\', '\\\\', $normalized);
        $normalized = str_replace('(', '\(', $normalized);
        $normalized = str_replace(')', '\)', $normalized);

        return $normalized;
    }
}
