<?php
/**
 * SimpleXLSXReader - Lector de archivos Excel XLSX nativo en PHP
 * No requiere dependencias externas (PhpSpreadsheet, SimpleXLSX, etc.)
 *
 * Un archivo XLSX es un ZIP que contiene archivos XML
 */

namespace dokuwiki\plugin\beztaxcalendar\lib;

class SimpleXLSXReader {

    protected $zipPath;
    protected $sharedStrings = [];
    protected $sheets = [];
    protected $errors = [];
    protected $dateFormatIds = [];  // IDs de formato que son fechas

    /**
     * Constructor
     * @param string $filePath Ruta al archivo XLSX
     */
    public function __construct($filePath = null) {
        if ($filePath) {
            $this->open($filePath);
        }
    }

    /**
     * Abrir archivo XLSX
     * @param string $filePath
     * @return bool
     */
    public function open($filePath) {
        $this->errors = [];
        $this->sharedStrings = [];
        $this->sheets = [];
        $this->dateFormatIds = [];

        if (!file_exists($filePath)) {
            $this->errors[] = "Archivo no encontrado: {$filePath}";
            return false;
        }

        if (!class_exists('ZipArchive')) {
            $this->errors[] = "La extension ZipArchive de PHP es requerida para leer archivos XLSX";
            return false;
        }

        $this->zipPath = $filePath;

        $zip = new \ZipArchive();
        $result = $zip->open($filePath);

        if ($result !== true) {
            $this->errors[] = "No se pudo abrir el archivo ZIP. Codigo de error: {$result}";
            return false;
        }

        // Leer shared strings (textos compartidos)
        $this->loadSharedStrings($zip);

        // Leer estilos para detectar formatos de fecha
        $this->loadStyles($zip);

        // Leer hojas de trabajo
        $this->loadWorksheets($zip);

        $zip->close();

        return empty($this->errors);
    }

    /**
     * Cargar cadenas compartidas del archivo
     */
    protected function loadSharedStrings(\ZipArchive $zip) {
        $content = $zip->getFromName('xl/sharedStrings.xml');
        if (!$content) {
            // Puede que no haya shared strings (si solo hay numeros)
            return;
        }

        $xml = @simplexml_load_string($content);
        if (!$xml) {
            return;
        }

        // Registrar namespace
        $xml->registerXPathNamespace('s', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');

        foreach ($xml->si as $si) {
            // El texto puede estar en <t> directamente o en <r><t>
            $text = '';
            if (isset($si->t)) {
                $text = (string)$si->t;
            } elseif (isset($si->r)) {
                foreach ($si->r as $r) {
                    if (isset($r->t)) {
                        $text .= (string)$r->t;
                    }
                }
            }
            $this->sharedStrings[] = $text;
        }
    }

    /**
     * Cargar estilos (styles.xml) para detectar formatos de fecha
     */
    protected function loadStyles(\ZipArchive $zip) {
        $content = $zip->getFromName('xl/styles.xml');
        if (!$content) return;

        $xml = @simplexml_load_string($content);
        if (!$xml) return;

        $ns = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
        $xml->registerXPathNamespace('s', $ns);

        // IDs de formato built-in de Excel que son fechas/hora
        // 14-22: formatos de fecha, 45-47: formatos de hora
        $builtinDateIds = [14,15,16,17,18,19,20,21,22,45,46,47];

        // Recoger numFmtId personalizados que son fecha
        $customDateFmtIds = [];
        if (isset($xml->numFmts->numFmt)) {
            foreach ($xml->numFmts->numFmt as $fmt) {
                $fmtId = (int)$fmt['numFmtId'];
                $code = (string)$fmt['formatCode'];
                // Si el formatCode contiene d, m, y, h → es fecha/hora
                if (preg_match('/[dmyh]/i', $code) && !preg_match('/^[#0,.]+$/', $code)) {
                    $customDateFmtIds[] = $fmtId;
                }
            }
        }

        // Leer cellXfs para mapear styleIndex → numFmtId
        if (isset($xml->cellXfs->xf)) {
            $idx = 0;
            foreach ($xml->cellXfs->xf as $xf) {
                $numFmtId = (int)$xf['numFmtId'];
                if (in_array($numFmtId, $builtinDateIds) || in_array($numFmtId, $customDateFmtIds)) {
                    $this->dateFormatIds[$idx] = true;
                }
                $idx++;
            }
        }
    }

    /**
     * Cargar hojas de trabajo
     */
    protected function loadWorksheets(\ZipArchive $zip) {
        // Buscar hojas disponibles
        $workbookContent = $zip->getFromName('xl/workbook.xml');
        if (!$workbookContent) {
            $this->errors[] = "No se encontro el archivo workbook.xml";
            return;
        }

        $workbook = @simplexml_load_string($workbookContent);
        if (!$workbook) {
            $this->errors[] = "Error al parsear workbook.xml";
            return;
        }

        $workbook->registerXPathNamespace('s', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');

        // Leer relaciones para mapear rId a archivo
        $relsContent = $zip->getFromName('xl/_rels/workbook.xml.rels');
        $sheetFiles = [];
        if ($relsContent) {
            $rels = @simplexml_load_string($relsContent);
            if ($rels) {
                foreach ($rels->Relationship as $rel) {
                    $id = (string)$rel['Id'];
                    $target = (string)$rel['Target'];
                    if (strpos($target, 'worksheets/') !== false || strpos($target, 'worksheets\\') !== false) {
                        // Limpiar la ruta: quitar barra inicial y prefijo xl/ si existe
                        $target = ltrim($target, '/\\');
                        if (strpos($target, 'xl/') === 0 || strpos($target, 'xl\\') === 0) {
                            $sheetFiles[$id] = $target;
                        } else {
                            $sheetFiles[$id] = 'xl/' . $target;
                        }
                    }
                }
            }
        }

        $sheetIndex = 1;
        foreach ($workbook->sheets->sheet as $sheet) {
            $sheetName = (string)$sheet['name'];
            $rId = (string)$sheet->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships')['id'];

            $sheetFile = isset($sheetFiles[$rId]) ? $sheetFiles[$rId] : null;

            if (!$sheetFile) {
                // Fallback: intentar por orden
                $sheetFile = "xl/worksheets/sheet{$sheetIndex}.xml";
            }
            $sheetIndex++;

            $sheetContent = $zip->getFromName($sheetFile);

            if ($sheetContent) {
                $this->sheets[$sheetName] = $this->parseWorksheet($sheetContent);
            }
        }

        // Si no se encontraron hojas, intentar sheet1.xml directamente
        if (empty($this->sheets)) {
            $sheetContent = $zip->getFromName("xl/worksheets/sheet1.xml");
            if ($sheetContent) {
                $this->sheets['Sheet1'] = $this->parseWorksheet($sheetContent);
            }
        }
    }

    /**
     * Parsear una hoja de trabajo
     * @param string $content Contenido XML de la hoja
     * @return array Filas de datos
     */
    protected function parseWorksheet($content) {
        $rows = [];

        $xml = @simplexml_load_string($content);
        if (!$xml) {
            return $rows;
        }

        $xml->registerXPathNamespace('s', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');

        // Obtener datos de filas
        if (!isset($xml->sheetData->row)) {
            return $rows;
        }

        foreach ($xml->sheetData->row as $row) {
            $rowData = [];
            $rowNum = (int)$row['r'];

            foreach ($row->c as $cell) {
                $cellRef = (string)$cell['r'];
                $colIndex = $this->columnToIndex($cellRef);
                $value = $this->getCellValue($cell);

                // Llenar celdas vacias intermedias
                while (count($rowData) < $colIndex) {
                    $rowData[] = '';
                }

                $rowData[$colIndex] = $value;
            }

            $rows[$rowNum] = $rowData;
        }

        // Reindexar desde 0
        return array_values($rows);
    }

    /**
     * Obtener valor de una celda
     */
    protected function getCellValue(\SimpleXMLElement $cell) {
        $type = (string)$cell['t'];
        $value = isset($cell->v) ? (string)$cell->v : '';
        $styleIndex = (string)$cell['s'];

        switch ($type) {
            case 's': // Shared string
                $index = (int)$value;
                return isset($this->sharedStrings[$index]) ? $this->sharedStrings[$index] : '';

            case 'b': // Boolean
                return $value === '1' ? true : false;

            case 'inlineStr': // Inline string
                return isset($cell->is->t) ? (string)$cell->is->t : '';

            case 'e': // Error
                return '#ERROR';

            case 'str': // Formula string
                return $value;

            default:
                if (!is_numeric($value)) return $value;

                $numValue = floatval($value);

                // Metodo 1: Si el estilo de la celda es formato fecha → convertir
                if ($styleIndex !== '' && isset($this->dateFormatIds[(int)$styleIndex])) {
                    return $this->excelDateToString($numValue);
                }

                // Metodo 2: Fallback por rango numerico (entero entre 40000-55000)
                if (abs($numValue - round($numValue)) < 0.0001 && $numValue >= 40000 && $numValue < 55000) {
                    return $this->excelDateToString($numValue);
                }

                return $value;
        }
    }

    /**
     * Convertir referencia de columna a indice (A=0, B=1, AA=26, etc.)
     */
    protected function columnToIndex($cellRef) {
        preg_match('/^([A-Z]+)/', $cellRef, $matches);
        if (empty($matches[1])) {
            return 0;
        }

        $col = $matches[1];
        $index = 0;
        $length = strlen($col);

        for ($i = 0; $i < $length; $i++) {
            $index = $index * 26 + (ord($col[$i]) - ord('A') + 1);
        }

        return $index - 1;
    }

    /**
     * Convertir fecha serial de Excel a string Y-m-d
     */
    protected function excelDateToString($serial) {
        // Excel serial: dias desde 1899-12-30 (con bug de 1900 bisiesto)
        // Usar 25569 (serial de 1970-01-01) como offset al epoch Unix
        // NO restar 1 por el bug — el offset 25569 ya lo compensa
        // Usar gmdate para evitar desfase por timezone del servidor
        $timestamp = ($serial - 25569) * 86400;
        return gmdate('Y-m-d', $timestamp);
    }

    /**
     * Obtener todas las filas de la primera hoja
     * @return array
     */
    public function rows() {
        if (empty($this->sheets)) {
            return [];
        }
        return reset($this->sheets);
    }

    /**
     * Obtener filas de una hoja especifica
     * @param string|int $sheet Nombre o indice de la hoja
     * @return array
     */
    public function getSheet($sheet = 0) {
        if (is_int($sheet)) {
            $sheets = array_values($this->sheets);
            return isset($sheets[$sheet]) ? $sheets[$sheet] : [];
        }
        return isset($this->sheets[$sheet]) ? $this->sheets[$sheet] : [];
    }

    /**
     * Obtener nombres de las hojas
     * @return array
     */
    public function getSheetNames() {
        return array_keys($this->sheets);
    }

    /**
     * Obtener errores
     * @return array
     */
    public function getErrors() {
        return $this->errors;
    }

    /**
     * Verificar si hubo errores
     * @return bool
     */
    public function hasErrors() {
        return !empty($this->errors);
    }

    /**
     * Metodo estatico para parsear archivo
     * @param string $filePath
     * @return array|false
     */
    public static function parse($filePath) {
        $reader = new self($filePath);
        if ($reader->hasErrors()) {
            return false;
        }
        return $reader->rows();
    }

    /**
     * Metodo estatico para parsear y obtener array asociativo usando primera fila como headers
     * @param string $filePath
     * @return array|false
     */
    public static function parseWithHeaders($filePath) {
        $reader = new self($filePath);
        if ($reader->hasErrors()) {
            return false;
        }

        $rows = $reader->rows();
        if (empty($rows)) {
            return [];
        }

        $headers = array_shift($rows);
        // Limpiar headers
        $headers = array_map(function($h) {
            return strtolower(trim(preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $h)));
        }, $headers);

        $result = [];
        foreach ($rows as $row) {
            // Asegurar que la fila tenga el mismo numero de columnas que headers
            $row = array_pad($row, count($headers), '');
            $row = array_slice($row, 0, count($headers));

            $assoc = array_combine($headers, $row);
            if ($assoc !== false) {
                $result[] = $assoc;
            }
        }

        return $result;
    }
}
