<?php

namespace App\Http\Controllers\Banco;

use App\Http\Controllers\Controller;
use App\Models\Caja\Movimientos;
use App\Models\Cliente\Cliente;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use Throwable;
use ZipArchive;

class MigrarDatosController extends Controller
{
    private const SESSION_PREVIEW_KEY = 'banco_migrardatos_preview';

    public function index()
    {   $id_sede = auth()->user() ? auth()->user()->id_sede : null;
         if (is_null($id_sede) || (int)$id_sede <= 0) {
            abort(403, 'No puede acceder: no tiene una sede asignada.');
        }
        $preview = Session::get(self::SESSION_PREVIEW_KEY);

        return view('Banco.migrardatos.index', [
            'preview' => $preview,
        ]);
    }

    public function preview(Request $request)
    {
        $user = auth()->user();
        $idSede = $user->id_sede ?? null;

        if (is_null($idSede) || (int) $idSede <= 0) {
            abort(403, 'No puede acceder: no tiene una sede asignada.');
        }

        $data = $request->validate([
            'tipo_moneda' => ['required', 'in:1,2'],
            'archivo_excel' => ['required', 'file', 'mimes:xlsx', 'max:10240'],
        ]);

        $uploadedFile = $request->file('archivo_excel');
        $storedFile = $this->storeUploadedExcel($uploadedFile);

        try {
            $rows = $this->readXlsxRows($storedFile['path']);
        } catch (\Throwable $e) {
            report($e);

            return back()->withErrors([
                'archivo_excel' => 'No se pudo leer el archivo Excel. Verifique que sea un .xlsx válido.',
            ])->withInput();
        }

        if (empty($rows)) {
            return back()->withErrors([
                'archivo_excel' => 'El archivo Excel no contiene filas para migrar.',
            ])->withInput();
        }

        $expectedHeaders = [
            'fecha_operacion',
            'descripcion',
            'monto',
            'numero_operacion',
            'codigo_banco',
            'codigo_ingreso_egreso',
            'id_proyecto',
            'tipo_cambio',
        ];

        $headerRow = array_keys($rows[0]);
        $missingHeaders = array_values(array_diff($expectedHeaders, $headerRow));

        if (!empty($missingHeaders)) {
            return back()->withErrors([
                'archivo_excel' => 'Faltan columnas obligatorias en el Excel: ' . implode(', ', $missingHeaders),
            ])->withInput();
        }

        $previewRows = [];
        $errorCount = 0;
        $tipoMoneda = (int) $data['tipo_moneda'];
        $tipoMonedaLabel = $tipoMoneda === 1 ? 'SOLES' : 'DOLLAR';

        foreach ($rows as $index => $row) {
            $previewRow = $this->buildPreviewRow(
                rowNumber: $index + 1,
                row: $row,
                idSede: (int) $idSede,
                tipoMoneda: $tipoMoneda,
                tipoMonedaLabel: $tipoMonedaLabel,
            );

            if (!empty($previewRow['errors'])) {
                $errorCount++;
            }

            $previewRows[] = $previewRow;
        }

        $preview = [
            'archivo' => $uploadedFile->getClientOriginalName(),
            'archivo_guardado' => $storedFile['name'],
            'archivo_ruta' => 'excel_migracion_banco/' . $storedFile['name'],
            'tipo_moneda' => $tipoMoneda,
            'tipo_moneda_label' => $tipoMonedaLabel,
            'rows' => $previewRows,
            'error_count' => $errorCount,
            'success_count' => count($previewRows) - $errorCount,
            'generated_at' => now()->format('d/m/Y H:i:s'),
        ];

        Session::put(self::SESSION_PREVIEW_KEY, $preview);

        return redirect()
            ->route('admin.migrardatos.index')
            ->with('success', $errorCount > 0
                ? 'Se generó la vista previa, pero existen filas con errores. Corrija el Excel antes de guardar.'
                : 'Vista previa generada correctamente. Revise la información antes de guardar.');
    }

    public function store(Request $request)
    {
        $user = auth()->user();
        $idSede = $user->id_sede ?? null;

        if (is_null($idSede) || (int) $idSede <= 0) {
            abort(403, 'No puede acceder: no tiene una sede asignada.');
        }

        $preview = Session::get(self::SESSION_PREVIEW_KEY);

        if (!$preview || empty($preview['rows'])) {
            return redirect()
                ->route('admin.migrardatos.index')
                ->withErrors(['archivo_excel' => 'Primero debe cargar y validar un archivo Excel.']);
        }

        if (!empty($preview['error_count'])) {
            return redirect()
                ->route('admin.migrardatos.index')
                ->withErrors(['archivo_excel' => 'No se puede guardar mientras existan filas con errores en la vista previa.']);
        }

        $apertura = DB::table('apertura_cierre')
            ->where('id_sede', $idSede)
            ->where('estado', 'ABIERTO')
            ->orderByDesc('id')
            ->first();

        if (!$apertura) {
            return redirect()
                ->route('admin.migrardatos.index')
                ->withErrors(['archivo_excel' => 'No existe una apertura en estado ABIERTO para esta sede.']);
        }

        $parametroComprobante = DB::table('parametro_comprobante')
            ->where('id_sede', $idSede)
            ->first();

        $serie = $parametroComprobante->serie ?? 'MIG';
        $created = 0;
        $updated = 0;
        $migrationTimestamp = now();
        $migrationFileName = !empty($preview['archivo_guardado'])
            ? $preview['archivo_guardado']
            : $this->appendTimestampToFileName($preview['archivo'], $migrationTimestamp);

        try {
            DB::transaction(function () use (
                $preview,
                $idSede,
                $user,
                $apertura,
                $serie,
                $migrationTimestamp,
                $migrationFileName,
                &$created,
                &$updated,
            ) {
                $clienteMigracion = Cliente::firstOrCreate(
                    ['dni' => '00000000'],
                    [
                        'nombres' => 'CLIENTE',
                        'apellido_paterno' => 'MIGRACION',
                        'apellido_materno' => 'BANCO',
                        'estado' => 'AC',
                    ]
                );

                if (empty($clienteMigracion->estado)) {
                    $clienteMigracion->estado = 'AC';
                    $clienteMigracion->save();
                }

                foreach ($preview['rows'] as $row) {
                    $fechaMovimiento = !empty($row['fecha_operacion_db'])
                        ? Carbon::parse($row['fecha_operacion_db'])->startOfDay()
                        : now();

                    $payload = [
                        'fecha_operacion' => $row['fecha_operacion_db'],
                        'descripcion' => $row['descripcion'],
                        'fecha_movimiento' => $fechaMovimiento->format('Y-m-d H:i:s'),
                        'compra' => $row['tipo_cambio'],
                        'venta' => $row['tipo_cambio'],
                        'id_proyecto' => !empty($row['id_proyecto']) ? $row['id_proyecto'] : null,
                        'id_cliente' => $clienteMigracion->id,
                        'id_apertura_cierre' => $apertura->id,
                        'id_codigoingreso' => $row['id_codigoingreso'],
                        'id_codigoingresodetalle' => $row['id_codigoingresodetalle'],
                        'id_sede' => $idSede,
                        'concepto' => $row['descripcion'],
                        'monto' => $row['monto_soles'],
                        'monto_dolar' => $row['monto_dolar'],
                        'detalle' => $row['descripcion'],
                        'id_usuario' => $user->id,
                        'tipo_movimiento' => $row['tipo_movimiento'],
                        'tipo_moneda' => $row['tipo_moneda'],
                        'estado' => 'AC',
                        'created_at' => $migrationTimestamp,
                        'updated_at' => $migrationTimestamp,
                        'numero_operacion' => $row['numero_operacion'],
                        'codigo_banco' => $row['codigo_banco'],
                        'codigo_ingreso_egreso' => $row['codigo_ingreso_egreso'],
                        'tipo_cambio' => $row['tipo_cambio'],
                        'nombre_archivo' => $migrationFileName,
                        'bancos' => 'SI',
                        'es_migrado_banco' => 1,
                    ];

                    $payload['serie'] = $serie;
                    $payload['numero'] = '00000000';
                    $payload['codigo'] = $this->generateMovementCode();
                    $payload['comprobante_archivo'] = $this->generateVoucherFileName(
                        $payload['codigo'],
                        $payload['serie'],
                        $payload['numero'],
                        $fechaMovimiento
                    );

                    $existing = Movimientos::query()
                        ->where('id_sede', $idSede)
                        ->where('numero_operacion', $row['numero_operacion'])
                        ->where('codigo_banco', $row['codigo_banco'])
                        ->where('tipo_moneda', $row['tipo_moneda'])
                        ->where(function ($query) {
                            $query->where('es_migrado_banco', 1)
                                ->orWhere('bancos', 'SI');
                        })
                        ->first();

                    if ($existing) {
                        $existing->fill($payload);
                        $existing->updated_at = $migrationTimestamp;
                        $existing->save();
                        $updated++;
                    } else {
                        Movimientos::create($payload);
                        $created++;
                    }
                }
            });
        } catch (Throwable $e) {
            report($e);

            return redirect()
                ->route('admin.migrardatos.index')
                ->withErrors([
                    'archivo_excel' => 'No se pudo guardar la migracion en movimientos. Detalle: ' . $e->getMessage(),
                ]);
        }

        Session::forget(self::SESSION_PREVIEW_KEY);

        return redirect()
            ->route('admin.migrardatos.index')
            ->with('success', "Migración completada. Registros creados: {$created}.");
    }

    public function create()
    {
        Session::forget(self::SESSION_PREVIEW_KEY);

        return redirect()->route('admin.migrardatos.index');
    }

    private function buildPreviewRow(int $rowNumber, array $row, int $idSede, int $tipoMoneda, string $tipoMonedaLabel): array
    {
        $requiredFields = [
            'fecha_operacion',
            'descripcion',
            'monto',
            'numero_operacion',
            'codigo_banco',
            'codigo_ingreso_egreso',
            'id_proyecto',
            'tipo_cambio',
        ];

        $errors = [];

        foreach ($requiredFields as $field) {
            if (!array_key_exists($field, $row) || trim((string) $row[$field]) === '') {
                $errors[] = "El campo {$field} está en blanco.";
            }
        }

        $fechaOperacion = $this->normalizeExcelDate($row['fecha_operacion'] ?? null);
        if (!$fechaOperacion) {
            $errors[] = 'La fecha_operacion no tiene un formato válido.';
        }

        $montoOriginal = $this->normalizeNumber($row['monto'] ?? null);
        if ($montoOriginal === null) {
            $errors[] = 'El monto no es válido.';
        } elseif ((float) $montoOriginal === 0.0) {
            $errors[] = 'El monto no puede ser cero.';
        }

        $tipoCambio = $this->normalizeNumber($row['tipo_cambio'] ?? null);
        if ($tipoCambio === null || (float) $tipoCambio <= 0) {
            $errors[] = 'El tipo_cambio debe ser mayor a cero.';
        }

        $proyectoId = (int) ($row['id_proyecto'] ?? 0);
        $proyecto = null;
        if ($proyectoId < 0) {
            $errors[] = 'El id_proyecto no es válido.';
        } elseif ($proyectoId > 0) {
            $proyecto = DB::table('proyectos')
                ->select('id', 'codigo', 'nombre')
                ->where('id', $proyectoId)
                ->where('id_sede', $idSede)
                ->first();

            if (!$proyecto) {
                $errors[] = 'El proyecto no existe para la sede del usuario.';
            }
        }

        $tipoMovimiento = ((float) $montoOriginal < 0) ? 2 : 1;
        $codigoNormalizado = trim((string) ($row['codigo_ingreso_egreso'] ?? ''));
        $codigoResolucion = $this->resolveCodeReferences($codigoNormalizado, $tipoMovimiento, $idSede);

        if ($codigoResolucion['belongs_to_opposite']) {
            $errors[] = $tipoMovimiento === 1
                ? 'El codigo_ingreso_egreso pertenece a egresos, pero el monto es positivo.'
                : 'El codigo_ingreso_egreso pertenece a ingresos, pero el monto es negativo.';
        }

        if (!$codigoResolucion['id_codigoingreso']) {
            $errors[] = 'No se encontró un código principal válido para codigo_ingreso_egreso.';
        }

        if (!$codigoResolucion['id_codigoingresodetalle']) {
            $errors[] = 'No se encontró un código detalle válido para codigo_ingreso_egreso.';
        }

        $montoAbsoluto = abs((float) $montoOriginal);
        $montoSoles = $tipoMoneda === 1
            ? $montoAbsoluto
            : round($montoAbsoluto * (float) $tipoCambio, 2);

        $montoDolar = $tipoMoneda === 1
            ? round($montoAbsoluto * (float) $tipoCambio, 2)
            : $montoAbsoluto;

        return [
            'row_number' => $rowNumber,
            'fecha_operacion' => $fechaOperacion ? Carbon::parse($fechaOperacion)->format('d/m/Y') : (string) ($row['fecha_operacion'] ?? ''),
            'fecha_operacion_db' => $fechaOperacion,
            'descripcion' => trim((string) ($row['descripcion'] ?? '')),
            'monto_original' => $montoOriginal,
            'monto_soles' => $montoSoles,
            'monto_dolar' => $montoDolar,
            'numero_operacion' => trim((string) ($row['numero_operacion'] ?? '')),
            'codigo_banco' => trim((string) ($row['codigo_banco'] ?? '')),
            'codigo_ingreso_egreso' => $codigoNormalizado,
            'id_proyecto' => $proyectoId === 0 ? 0 : $proyecto?->id,
            'proyecto_texto' => $proyectoId === 0 ? '0 - SIN PROYECTO' : ($proyecto ? ($proyecto->codigo . ' - ' . $proyecto->nombre) : ''),
            'tipo_cambio' => (float) $tipoCambio,
            'tipo_movimiento' => $tipoMovimiento,
            'tipo_movimiento_label' => $tipoMovimiento === 1 ? 'INGRESO' : 'EGRESO',
            'tipo_moneda' => $tipoMoneda,
            'tipo_moneda_label' => $tipoMonedaLabel,
            'id_codigoingreso' => $codigoResolucion['id_codigoingreso'],
            'id_codigoingresodetalle' => $codigoResolucion['id_codigoingresodetalle'],
            'codigo_resuelto' => $codigoResolucion['descripcion'],
            'errors' => $errors,
        ];
    }

    private function appendTimestampToFileName(string $fileName, Carbon $timestamp): string
    {
        $info = pathinfo($fileName);
        $name = $info['filename'] ?? $fileName;
        $extension = isset($info['extension']) && $info['extension'] !== ''
            ? '.' . $info['extension']
            : '';

        return $name . ' ' . $timestamp->format('d-m-Y H-i-s') . $extension;
    }

    private function storeUploadedExcel($uploadedFile): array
    {
        $targetDirectory = public_path('excel_migracion_banco');

        if (!File::exists($targetDirectory)) {
            File::makeDirectory($targetDirectory, 0755, true);
        }

        $storedFileName = $this->appendTimestampToFileName(
            $uploadedFile->getClientOriginalName(),
            now()
        );

        $uploadedFile->move($targetDirectory, $storedFileName);

        return [
            'name' => $storedFileName,
            'path' => $targetDirectory . DIRECTORY_SEPARATOR . $storedFileName,
        ];
    }

    private function resolveCodeReferences(string $codigo, int $tipoMovimiento, int $idSede): array
    {
        if ($codigo === '') {
            return [
                'id_codigoingreso' => null,
                'id_codigoingresodetalle' => null,
                'descripcion' => '',
                'belongs_to_opposite' => false,
            ];
        }

        if ($tipoMovimiento === 1) {
            $detalle = DB::table('codigo_ingreso_detalles')
                ->select('id', 'id_codigo_ingreso', 'codigo', 'nombre')
                ->when(
                    Schema::hasColumn('codigo_ingreso_detalles', 'id_sede'),
                    fn ($query) => $query->where('id_sede', $idSede)
                )
                ->where('codigo', $codigo)
                ->first();

            if ($detalle) {
                return [
                    'id_codigoingreso' => (int) $detalle->id_codigo_ingreso,
                    'id_codigoingresodetalle' => (int) $detalle->id,
                    'descripcion' => $detalle->codigo . ' - ' . $detalle->nombre,
                    'belongs_to_opposite' => false,
                ];
            }

            $principal = DB::table('codigo_ingresos')
                ->select('id', 'codigo', 'nombre')
                ->when(
                    Schema::hasColumn('codigo_ingresos', 'id_sede'),
                    fn ($query) => $query->where('id_sede', $idSede)
                )
                ->where('codigo', $codigo)
                ->first();

            if ($principal) {
                $detallePrincipal = DB::table('codigo_ingreso_detalles')
                    ->select('id')
                    ->when(
                        Schema::hasColumn('codigo_ingreso_detalles', 'id_sede'),
                        fn ($query) => $query->where('id_sede', $idSede)
                    )
                    ->where('id_codigo_ingreso', $principal->id)
                    ->orderBy('id')
                    ->first();

                return [
                    'id_codigoingreso' => (int) $principal->id,
                    'id_codigoingresodetalle' => $detallePrincipal?->id ? (int) $detallePrincipal->id : null,
                    'descripcion' => $principal->codigo . ' - ' . $principal->nombre,
                    'belongs_to_opposite' => false,
                ];
            }

            if ($this->codeExistsInEgresoTables($codigo, $idSede)) {
                return [
                    'id_codigoingreso' => null,
                    'id_codigoingresodetalle' => null,
                    'descripcion' => '',
                    'belongs_to_opposite' => true,
                ];
            }
        }

        $detalle = DB::table('codigo_egreso_detalles')
            ->select('id', 'id_codigo_egreso', 'codigo', 'nombre')
            ->when(
                Schema::hasColumn('codigo_egreso_detalles', 'id_sede'),
                fn ($query) => $query->where('id_sede', $idSede)
            )
            ->where('codigo', $codigo)
            ->first();

        if ($detalle) {
            return [
                'id_codigoingreso' => (int) $detalle->id_codigo_egreso,
                'id_codigoingresodetalle' => (int) $detalle->id,
                'descripcion' => $detalle->codigo . ' - ' . $detalle->nombre,
                'belongs_to_opposite' => false,
            ];
        }

        $principal = DB::table('codigo_egresos')
            ->select('id', 'codigo', 'nombre')
            ->when(
                Schema::hasColumn('codigo_egresos', 'id_sede'),
                fn ($query) => $query->where('id_sede', $idSede)
            )
            ->where('codigo', $codigo)
            ->first();

        if ($principal) {
            $detallePrincipal = DB::table('codigo_egreso_detalles')
                ->select('id')
                ->when(
                    Schema::hasColumn('codigo_egreso_detalles', 'id_sede'),
                    fn ($query) => $query->where('id_sede', $idSede)
                )
                ->where('id_codigo_egreso', $principal->id)
                ->orderBy('id')
                ->first();

            return [
                'id_codigoingreso' => (int) $principal->id,
                'id_codigoingresodetalle' => $detallePrincipal?->id ? (int) $detallePrincipal->id : null,
                'descripcion' => $principal->codigo . ' - ' . $principal->nombre,
                'belongs_to_opposite' => false,
            ];
        }

        if ($this->codeExistsInIngresoTables($codigo, $idSede)) {
            return [
                'id_codigoingreso' => null,
                'id_codigoingresodetalle' => null,
                'descripcion' => '',
                'belongs_to_opposite' => true,
            ];
        }

        return [
            'id_codigoingreso' => null,
            'id_codigoingresodetalle' => null,
            'descripcion' => '',
            'belongs_to_opposite' => false,
        ];
    }

    private function codeExistsInIngresoTables(string $codigo, int $idSede): bool
    {
        $existsInPrincipal = DB::table('codigo_ingresos')
            ->when(
                Schema::hasColumn('codigo_ingresos', 'id_sede'),
                fn ($query) => $query->where('id_sede', $idSede)
            )
            ->where('codigo', $codigo)
            ->exists();

        if ($existsInPrincipal) {
            return true;
        }

        return DB::table('codigo_ingreso_detalles')
            ->when(
                Schema::hasColumn('codigo_ingreso_detalles', 'id_sede'),
                fn ($query) => $query->where('id_sede', $idSede)
            )
            ->where('codigo', $codigo)
            ->exists();
    }

    private function codeExistsInEgresoTables(string $codigo, int $idSede): bool
    {
        $existsInPrincipal = DB::table('codigo_egresos')
            ->when(
                Schema::hasColumn('codigo_egresos', 'id_sede'),
                fn ($query) => $query->where('id_sede', $idSede)
            )
            ->where('codigo', $codigo)
            ->exists();

        if ($existsInPrincipal) {
            return true;
        }

        return DB::table('codigo_egreso_detalles')
            ->when(
                Schema::hasColumn('codigo_egreso_detalles', 'id_sede'),
                fn ($query) => $query->where('id_sede', $idSede)
            )
            ->where('codigo', $codigo)
            ->exists();
    }

    private function readXlsxRows(string $path): array
    {
        [$sharedStringsXml, $worksheetXml, $workbookRelsXml] = $this->extractXlsxXmlContents($path);

        $sharedStrings = $this->loadSharedStringsFromXml($sharedStringsXml);
        $worksheetPath = $this->resolveFirstWorksheetPathFromXml($workbookRelsXml);

        if ($worksheetXml === null && $worksheetPath !== null) {
            [, $worksheetXml] = $this->extractWorksheetXmlWithFallback($path, $worksheetPath);
        }

        if ($worksheetXml === false || $worksheetXml === null || trim((string) $worksheetXml) === '') {
            throw new \RuntimeException('No se pudo leer la primera hoja del Excel.');
        }

        $worksheet = simplexml_load_string($worksheetXml);
        if (!$worksheet) {
            throw new \RuntimeException('La hoja del Excel no tiene un formato válido.');
        }

        $worksheet->registerXPathNamespace('main', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
        $rows = $worksheet->xpath('//main:sheetData/main:row') ?: [];

        $parsedRows = [];
        $headers = [];

        foreach ($rows as $rowIndex => $row) {
            $current = [];

            foreach ($row->c as $cell) {
                $reference = (string) $cell['r'];
                $columnIndex = $this->columnLetterToIndex(preg_replace('/\d+/', '', $reference));
                $current[$columnIndex] = $this->extractCellValue($cell, $sharedStrings);
            }

            if ($rowIndex === 0) {
                ksort($current);
                $headers = array_map(fn ($value) => $this->normalizeHeader($value), array_values($current));
                continue;
            }

            if (empty($headers)) {
                continue;
            }

            $normalizedRow = [];
            foreach ($headers as $columnIndex => $header) {
                $normalizedRow[$header] = isset($current[$columnIndex]) ? trim((string) $current[$columnIndex]) : '';
            }

            if (count(array_filter($normalizedRow, fn ($value) => $value !== '')) === 0) {
                continue;
            }

            $parsedRows[] = $normalizedRow;
        }

        return $parsedRows;
    }

    private function extractXlsxXmlContents(string $path): array
    {
        if (class_exists(ZipArchive::class)) {
            $zip = new ZipArchive();
            if ($zip->open($path) === true) {
                $sharedStringsXml = $zip->getFromName('xl/sharedStrings.xml') ?: null;
                $workbookRelsXml = $zip->getFromName('xl/_rels/workbook.xml.rels') ?: null;
                $worksheetPath = $this->resolveFirstWorksheetPathFromXml($workbookRelsXml);
                $worksheetXml = $worksheetPath ? ($zip->getFromName($worksheetPath) ?: null) : ($zip->getFromName('xl/worksheets/sheet1.xml') ?: null);
                $zip->close();

                return [$sharedStringsXml, $worksheetXml, $workbookRelsXml];
            }
        }

        return $this->extractWorksheetXmlWithFallback($path);
    }

    private function extractWorksheetXmlWithFallback(string $path, ?string $worksheetPath = null): array
    {
        $tempBase = storage_path('app/tmp_xlsx_' . Str::random(12));
        $tempZip = $tempBase . '.zip';

        File::copy($path, $tempZip);

        try {
            $phar = new \PharData($tempZip);
            $phar->extractTo($tempBase, null, true);

            $sharedStringsXml = File::exists($tempBase . '/xl/sharedStrings.xml')
                ? File::get($tempBase . '/xl/sharedStrings.xml')
                : null;

            $workbookRelsXml = File::exists($tempBase . '/xl/_rels/workbook.xml.rels')
                ? File::get($tempBase . '/xl/_rels/workbook.xml.rels')
                : null;

            $worksheetPath = $worksheetPath ?: $this->resolveFirstWorksheetPathFromXml($workbookRelsXml) ?: 'xl/worksheets/sheet1.xml';
            $worksheetFullPath = $tempBase . '/' . str_replace('/', DIRECTORY_SEPARATOR, $worksheetPath);
            $worksheetXml = File::exists($worksheetFullPath) ? File::get($worksheetFullPath) : null;

            return [$sharedStringsXml, $worksheetXml, $workbookRelsXml];
        } finally {
            if (File::exists($tempZip)) {
                File::delete($tempZip);
            }

            if (File::isDirectory($tempBase)) {
                File::deleteDirectory($tempBase);
            }
        }
    }

    private function loadSharedStringsFromXml(?string $xml): array
    {
        if (!$xml) {
            return [];
        }

        $shared = simplexml_load_string($xml);
        if (!$shared) {
            return [];
        }

        $shared->registerXPathNamespace('main', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
        $items = $shared->xpath('//main:si') ?: [];

        $result = [];
        foreach ($items as $item) {
            $item->registerXPathNamespace('main', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
            $texts = $item->xpath('.//main:t') ?: [];
            $result[] = collect($texts)
                ->map(fn ($text) => (string) $text)
                ->implode('');
        }

        return $result;
    }

    private function resolveFirstWorksheetPathFromXml(?string $relsXml): ?string
    {
        if (!$relsXml) {
            return 'xl/worksheets/sheet1.xml';
        }

        $rels = simplexml_load_string($relsXml);
        if (!$rels) {
            return 'xl/worksheets/sheet1.xml';
        }

        foreach ($rels->Relationship as $relationship) {
            $type = (string) $relationship['Type'];
            if (Str::endsWith($type, '/worksheet')) {
                return 'xl/' . ltrim((string) $relationship['Target'], '/');
            }
        }

        return 'xl/worksheets/sheet1.xml';
    }

    private function extractCellValue(\SimpleXMLElement $cell, array $sharedStrings): string
    {
        $type = (string) $cell['t'];

        if ($type === 'inlineStr') {
            return trim((string) ($cell->is->t ?? ''));
        }

        $value = isset($cell->v) ? (string) $cell->v : '';

        if ($type === 's') {
            return isset($sharedStrings[(int) $value]) ? (string) $sharedStrings[(int) $value] : '';
        }

        return trim($value);
    }

    private function columnLetterToIndex(string $letters): int
    {
        $letters = strtoupper($letters);
        $index = 0;

        foreach (str_split($letters) as $char) {
            $index = ($index * 26) + (ord($char) - 64);
        }

        return $index - 1;
    }

    private function normalizeHeader(string $value): string
    {
        $value = trim(Str::lower($value));
        $value = str_replace([' ', '-'], '_', $value);

        return preg_replace('/_+/', '_', $value) ?: $value;
    }

    private function normalizeExcelDate(mixed $value): ?string
    {
        $value = trim((string) $value);
        if ($value === '') {
            return null;
        }

        if (is_numeric($value)) {
            return Carbon::create(1899, 12, 30)->addDays((int) $value)->format('Y-m-d');
        }

        foreach (['d/m/Y', 'd-m-Y', 'Y-m-d'] as $format) {
            try {
                return Carbon::createFromFormat($format, $value)->format('Y-m-d');
            } catch (\Throwable $e) {
            }
        }

        return null;
    }

    private function normalizeNumber(mixed $value): ?float
    {
        if ($value === null) {
            return null;
        }

        $value = trim((string) $value);
        if ($value === '') {
            return null;
        }

        $normalized = str_replace([' ', ','], ['', ''], $value);

        return is_numeric($normalized) ? (float) $normalized : null;
    }

    private function generateMovementCode(): string
    {
        $characters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ8954237';
        $max = strlen($characters) - 1;
        $code = '';

        for ($i = 0; $i < 6; $i++) {
            $code .= $characters[random_int(0, $max)];
        }

        return $code;
    }

    private function generateVoucherFileName(string $codigo, string $serie, string $numero, Carbon $fechaHora): string
    {
        return strtoupper(trim($codigo)) . '_' .
            strtoupper(trim($serie)) . '_' .
            trim($numero) . '_' .
            'BM' . '_' .
            $fechaHora->format('YmdHis') . '.pdf';
    }
}
