<?php
/**
 * Clase para comparar y actualizar esquemas de base de datos
 * Parsea archivos SQL y compara con la estructura actual de la BBDD
 */

class SQLSchemaUpdater {
    private $db;
    
    public function __construct() {
        // Usar la función conectar() del sistema
        if (function_exists('conectar')) {
            try {
                $this->db = conectar();
            } catch (Exception $e) {
                $this->db = null;
            }
        } else {
            $this->db = null;
        }
    }
    
    /**
     * Parsea un archivo SQL y extrae la estructura de tablas
     * También guarda la sentencia SQL original para poder crear tablas nuevas
     */
    public function parseSQL($sqlFile): array {
        $content = file_get_contents($sqlFile);
        $tables = [];
        
        // Buscar CREATE TABLE statements completos (incluyendo ENGINE si existe)
        preg_match_all('/(CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"]?(\w+)[`"]?\s*\((.*?)\)\s*(?:ENGINE[^;]*)?);/is', $content, $matches, PREG_SET_ORDER);
        
        foreach ($matches as $match) {
            $fullStatement = $match[1] . ';';
            $tableName = $match[2];
            $columnsContent = $match[3];
            
            $tables[$tableName] = [
                'columns' => $this->parseColumns($columnsContent),
                'create_sql' => $fullStatement,
                'extra_sql' => []
            ];
        }
        
        // Buscar sentencias ALTER TABLE que añaden atributos como AUTO_INCREMENT
        preg_match_all('/ALTER\s+TABLE\s+[`"]?(\w+)[`"]?\s+MODIFY\s+[`"]?(\w+)[`"]?\s+([^;]+AUTO_INCREMENT[^;]*);/is', $content, $alterMatches, PREG_SET_ORDER);
        
        foreach ($alterMatches as $match) {
            $tableName = $match[1];
            $columnName = $match[2];
            
            if (isset($tables[$tableName]['columns'][$columnName])) {
                $tables[$tableName]['columns'][$columnName]['auto_increment'] = true;
                $tables[$tableName]['extra_sql'][] = $match[0];
            }
        }
        
        // Buscar PRIMARY KEY u otros ALTER TABLE relevantes
        preg_match_all('/ALTER\s+TABLE\s+[`"]?(\w+)[`"]?\s+ADD\s+(PRIMARY\s+KEY|UNIQUE|KEY|INDEX)\s*([^;]+);/is', $content, $pkMatches, PREG_SET_ORDER);
        
        foreach ($pkMatches as $match) {
            $tableName = $match[1];
            if (isset($tables[$tableName])) {
                $tables[$tableName]['extra_sql'][] = $match[0];
            }
        }
        
        return $tables;
    }
    
    /**
     * Parsea las columnas de una definición de tabla
     */
    private function parseColumns($columnsContent): array {
        $columns = [];
        $lines = explode("\n", $columnsContent);
        
        foreach ($lines as $line) {
            $line = trim($line);
            
            // Ignorar claves primarias, índices, constraints
            if (empty($line) || 
                preg_match('/^(PRIMARY|KEY|INDEX|UNIQUE|CONSTRAINT|FOREIGN)/i', $line)) {
                continue;
            }
            
            // Extraer nombre de columna
            if (preg_match('/^[`"]?(\w+)[`"]?\s+(.+?)(?:,|$)/i', $line, $match)) {
                $columnName = $match[1];
                $definition = trim($match[2], ', ');
                
                // Ignorar definiciones de clave
                if (!preg_match('/^(PRIMARY|KEY|INDEX)/i', $columnName)) {
                    $columns[$columnName] = [
                        'name' => $columnName,
                        'definition' => $definition,
                        'type' => $this->extractType($definition),
                        'nullable' => !preg_match('/NOT\s+NULL/i', $definition),
                        'default' => $this->extractDefault($definition),
                        'auto_increment' => (bool) preg_match('/AUTO_INCREMENT/i', $definition)
                    ];
                }
            }
        }
        
        return $columns;
    }
    
    /**
     * Extrae el tipo de dato de una definición de columna
     */
    private function extractType($definition): string {
        if (preg_match('/^(\w+(?:\([^)]+\))?)/i', $definition, $match)) {
            return strtoupper($match[1]);
        }
        return 'VARCHAR(255)';
    }
    
    /**
     * Extrae el valor por defecto de una definición de columna
     */
    private function extractDefault($definition): ?string {
        if (preg_match('/DEFAULT\s+([\'"]?[^\s,]+[\'"]?)/i', $definition, $match)) {
            return trim($match[1], "'\"");
        }
        return null;
    }
    
    /**
     * Obtiene el esquema actual de la base de datos
     */
    public function getCurrentSchema(): array {
        $tables = [];
        
        // Verificar que la conexión existe
        if (!$this->db) {
            return $tables;
        }
        
        $result = $this->db->query("SHOW TABLES");
        if (!$result) {
            return $tables;
        }
        
        while ($row = $result->fetch_row()) {
            $tableName = $row[0];
            $tables[$tableName] = $this->getTableColumns($tableName);
        }
        
        return $tables;
    }
    
    /**
     * Obtiene las columnas de una tabla existente
     */
    private function getTableColumns($tableName): array {
        $columns = [];
        
        $result = $this->db->query("DESCRIBE `$tableName`");
        while ($row = $result->fetch_assoc()) {
            $columns[$row['Field']] = [
                'name' => $row['Field'],
                'type' => strtoupper($row['Type']),
                'nullable' => $row['Null'] === 'YES',
                'default' => $row['Default'],
                'key' => $row['Key'],
                'extra' => $row['Extra']
            ];
        }
        
        return $columns;
    }
    
    /**
     * Compara dos esquemas y devuelve las diferencias
     */
    public function compareSchemas(array $newSchema, array $currentSchema): array {
        $changes = [];
        
        foreach ($newSchema as $tableName => $tableData) {
            // Obtener columnas (nueva estructura con 'columns' o estructura antigua directa)
            $newColumns = isset($tableData['columns']) ? $tableData['columns'] : $tableData;
            $createSql = isset($tableData['create_sql']) ? $tableData['create_sql'] : null;
            $extraSql = isset($tableData['extra_sql']) ? $tableData['extra_sql'] : [];
            
            if (!isset($currentSchema[$tableName])) {
                // Tabla nueva - crear toda la tabla
                $changes[] = [
                    'type' => 'CREATE_TABLE',
                    'table' => $tableName,
                    'columns' => $newColumns,
                    'create_sql' => $createSql,
                    'extra_sql' => $extraSql
                ];
                continue;
            }
            
            // Verificar si la tabla existente perdió su PRIMARY KEY y restaurarlo
            $hasPrimaryKey = false;
            foreach ($currentSchema[$tableName] as $currentCol) {
                if ($currentCol['key'] === 'PRI') {
                    $hasPrimaryKey = true;
                    break;
                }
            }
            if (!$hasPrimaryKey && !empty($extraSql)) {
                foreach ($extraSql as $sqlCommand) {
                    if (stripos($sqlCommand, 'PRIMARY KEY') !== false) {
                        $changes[] = [
                            'type' => 'EXECUTE_SQL',
                            'table' => $tableName,
                            'sql' => $sqlCommand,
                            'description' => 'Restaurando PRIMARY KEY faltante'
                        ];
                    }
                }
            }
            
            $currentColumns = $currentSchema[$tableName];
            
            // Buscar columnas nuevas o modificadas
            foreach ($newColumns as $columnName => $columnDef) {
                if (!isset($currentColumns[$columnName])) {
                    // Columna nueva
                    $changes[] = [
                        'type' => 'ADD_COLUMN',
                        'table' => $tableName,
                        'column' => $columnName,
                        'definition' => $columnDef
                    ];
                } else {
                    // Columna existe - comparar todos los atributos
                    $currentCol = $currentColumns[$columnName];
                    $currentType = $this->normalizeType($currentCol['type']);
                    $newType = $this->normalizeType($columnDef['type']);
                    
                    // Detectar si falta AUTO_INCREMENT
                    $newHasAutoIncrement = isset($columnDef['auto_increment']) && $columnDef['auto_increment'];
                    $currentHasAutoIncrement = isset($currentCol['extra']) && 
                        stripos($currentCol['extra'], 'auto_increment') !== false;
                    $missingAutoIncrement = $newHasAutoIncrement && !$currentHasAutoIncrement;
                    
                    // Comparar nullable (NULL vs NOT NULL)
                    $currentNullable = isset($currentCol['nullable']) ? $currentCol['nullable'] : true;
                    $newNullable = isset($columnDef['nullable']) ? $columnDef['nullable'] : true;
                    $nullableChanged = $currentNullable !== $newNullable;
                    
                    // Comparar default value
                    $currentDefault = isset($currentCol['default']) ? $currentCol['default'] : null;
                    $newDefault = isset($columnDef['default']) ? $columnDef['default'] : null;
                    
                    // Normalizar para comparación: tratar '' o el string 'NULL' como null real
                    $normalizedCurrentDefault = ($currentDefault === '' || $currentDefault === null || strtoupper((string)$currentDefault) === 'NULL') ? null : (string)$currentDefault;
                    $normalizedNewDefault = ($newDefault === '' || $newDefault === null || strtoupper((string)$newDefault) === 'NULL') ? null : (string)$newDefault;
                    
                    $defaultChanged = $normalizedCurrentDefault !== $normalizedNewDefault;
                    
                    if ($currentType !== $newType || $missingAutoIncrement || $nullableChanged || $defaultChanged) {
                        // Construir descripción del cambio
                        $changeDesc = [];
                        if ($currentType !== $newType) $changeDesc[] = "tipo: {$currentCol['type']} -> {$columnDef['type']}";
                        if ($missingAutoIncrement) $changeDesc[] = "+ AUTO_INCREMENT";
                        if ($nullableChanged) $changeDesc[] = ($newNullable ? 'NOT NULL -> NULL' : 'NULL -> NOT NULL');
                        if ($defaultChanged) $changeDesc[] = "default: " . ($currentDefault ?? 'NULL') . " -> " . ($newDefault ?? 'NULL');
                        
                        $changes[] = [
                            'type' => 'MODIFY_COLUMN',
                            'table' => $tableName,
                            'column' => $columnName,
                            'definition' => $columnDef,
                            'old_type' => $currentCol['type'],
                            'new_type' => implode(', ', $changeDesc),
                            'current_column' => $currentCol
                        ];
                    }
                }
            }
            
            // Buscar columnas eliminadas - se eliminarán automáticamente
            foreach ($currentColumns as $columnName => $columnDef) {
                if (!isset($newColumns[$columnName])) {
                    $changes[] = [
                        'type' => 'DROP_COLUMN',
                        'table' => $tableName,
                        'column' => $columnName
                    ];
                }
            }
        }
        
        return $changes;
    }
    
    /**
     * Aplica los cambios de esquema a la base de datos
     */
    public function applyChanges(array $changes): array {
        $log = [];
        
        // Desactivar modo estricto de mysqli para que errores no lancen excepciones
        // y se puedan capturar con $this->db->error
        $previousReportMode = null;
        if (function_exists('mysqli_report')) {
            $previousReportMode = true;
            mysqli_report(MYSQLI_REPORT_OFF);
        }
        
        foreach ($changes as $change) {
            try {
                switch ($change['type']) {
                    case 'ADD_COLUMN':
                        $sql = $this->buildAddColumnSQL($change);
                        if ($this->db->query($sql)) {
                            $log[] = "✓ Agregada columna {$change['column']} a {$change['table']}";
                        } else {
                            $log[] = "✗ Error al agregar {$change['column']} en {$change['table']}: " . $this->db->error . " | SQL: " . $sql;
                        }
                        break;
                        
                    case 'CREATE_TABLE':
                        // Crear tabla automáticamente si tenemos el SQL
                        if (!empty($change['create_sql'])) {
                            if ($this->db->query($change['create_sql'])) {
                                $log[] = "✓ Tabla creada: {$change['table']}";
                                
                                // Ejecutar sentencias ALTER TABLE adicionales asociadas (PKs, AUTO_INCREMENT)
                                if (!empty($change['extra_sql'])) {
                                    foreach ($change['extra_sql'] as $extraQuery) {
                                        if ($this->db->query($extraQuery)) {
                                            $log[] = "  ✓ (Atributo extra aplicado)";
                                        } else {
                                            $log[] = "  ✗ Error al aplicar extra `{$extraQuery}`: " . $this->db->error;
                                        }
                                    }
                                }
                            } else {
                                $log[] = "✗ Error al crear tabla {$change['table']}: " . $this->db->error;
                            }
                        } else {
                            $log[] = "⚠ Tabla nueva detectada: {$change['table']} (crear manualmente, no hay SQL)";
                        }
                        break;
                        
                    case 'EXECUTE_SQL':
                        if ($this->db->query($change['sql'])) {
                            $log[] = "✓ {$change['description']}";
                        } else {
                            $log[] = "✗ Error en {$change['table']}: " . $this->db->error . " | SQL: " . $change['sql'];
                        }
                        break;
                        
                    case 'DROP_COLUMN':
                        $sql = $this->buildDropColumnSQL($change);
                        if ($this->db->query($sql)) {
                            $log[] = "✓ Eliminada columna {$change['column']} de {$change['table']}";
                        } else {
                            $log[] = "✗ Error al eliminar {$change['column']}: " . $this->db->error;
                        }
                        break;
                        
                    case 'MODIFY_COLUMN':
                        $sql = $this->buildModifyColumnSQL($change);
                        if ($this->db->query($sql)) {
                            $log[] = "✓ Modificada columna {$change['column']} en {$change['table']} ({$change['old_type']} -> {$change['new_type']})";
                        } else {
                            $log[] = "✗ Error al modificar {$change['column']} en {$change['table']}: " . $this->db->error . " | SQL: " . $sql;
                        }
                        break;
                }
            } catch (Exception $e) {
                $table = $change['table'] ?? '?';
                $column = $change['column'] ?? '?';
                $log[] = "✗ Error en {$table}.{$column}: " . $e->getMessage();
            }
        }
        
        // Restaurar modo de reporte de mysqli
        if ($previousReportMode !== null) {
            mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
        }
        
        return $log;
    }
    
    /**
     * Construye la sentencia SQL para agregar una columna
     */
    private function buildAddColumnSQL(array $change): string {
        $table = $change['table'];
        $column = $change['column'];
        $def = $change['definition'];
        
        $sql = "ALTER TABLE `$table` ADD COLUMN `$column` {$def['type']}";
        
        if (!$def['nullable']) {
            $sql .= " NOT NULL";
        } else {
            $sql .= " NULL";
        }
        
        if ($def['default'] !== null) {
            $default = $def['default'];
            if (is_numeric($default) || in_array(strtoupper($default), ['NULL', 'CURRENT_TIMESTAMP'])) {
                $sql .= " DEFAULT $default";
            } else {
                $sql .= " DEFAULT '$default'";
            }
        } elseif ($def['nullable']) {
            $sql .= " DEFAULT NULL";
        }
        
        return $sql;
    }
    
    /**
     * Construye la sentencia SQL para eliminar una columna
     */
    private function buildDropColumnSQL(array $change): string {
        $table = $change['table'];
        $column = $change['column'];
        
        return "ALTER TABLE `$table` DROP COLUMN `$column`";
    }
    
    /**
     * Construye la sentencia SQL para modificar una columna
     */
    private function buildModifyColumnSQL(array $change): string {
        $table = $change['table'];
        $column = $change['column'];
        $def = $change['definition'];
        $currentCol = isset($change['current_column']) ? $change['current_column'] : [];
        
        // Determinar qué tipo usar: si el tipo base no cambió, preservar el tipo actual de la BD
        // para evitar truncamiento de datos (el parser puede perder modificadores como UNSIGNED)
        $currentType = isset($currentCol['type']) ? $currentCol['type'] : '';
        $newType = $def['type'];
        $normalizedCurrent = $this->normalizeType($currentType);
        $normalizedNew = $this->normalizeType($newType);
        
        if ($normalizedCurrent === $normalizedNew && !empty($currentType)) {
            // El tipo base no cambió, usar el tipo exacto de la BD para evitar truncamiento
            $typeToUse = $currentType;
        } else {
            // El tipo sí cambió, usar el nuevo tipo del SQL
            $typeToUse = $newType;
        }
        
        $sql = "ALTER TABLE `$table` MODIFY COLUMN `$column` $typeToUse";
        
        if (!$def['nullable']) {
            $sql .= " NOT NULL";
        } else {
            $sql .= " NULL";
        }
        
        if ($def['default'] !== null) {
            $default = $def['default'];
            if (is_numeric($default) || in_array(strtoupper($default), ['NULL', 'CURRENT_TIMESTAMP'])) {
                $sql .= " DEFAULT $default";
            } else {
                $sql .= " DEFAULT '$default'";
            }
        } elseif ($def['nullable']) {
            $sql .= " DEFAULT NULL";
        }
        
        // Añadir AUTO_INCREMENT si el nuevo esquema lo define, o preservarlo si la columna original lo tenía
        $hasAutoIncrement = false;
        if (isset($def['auto_increment']) && $def['auto_increment']) {
            $hasAutoIncrement = true;
        } elseif (isset($currentCol['extra']) && stripos($currentCol['extra'], 'auto_increment') !== false) {
            $hasAutoIncrement = true;
        }
        if ($hasAutoIncrement) {
            $sql .= " AUTO_INCREMENT";
        }
        
        return $sql;
    }
    
    /**
     * Normaliza un tipo de dato para comparación
     * Extrae el tipo base ignorando tamaño y modificadores para comparaciones más robustas
     */
    private function normalizeType(string $type): string {
        $type = strtoupper(trim($type));
        
        // Extraer solo el tipo base sin paréntesis ni modificadores
        // Ejemplo: "TINYINT(3) UNSIGNED" -> "TINYINT"
        // Ejemplo: "INT(11)" -> "INT"
        // Ejemplo: "VARCHAR(255)" -> "VARCHAR(255)" (mantener tamaño para varchar)
        
        // Para tipos numéricos, solo comparar el tipo base
        if (preg_match('/^(TINYINT|SMALLINT|MEDIUMINT|INT|BIGINT|FLOAT|DOUBLE|DECIMAL)/i', $type, $match)) {
            return strtoupper($match[1]);
        }
        
        // Para VARCHAR/CHAR, mantener el tamaño porque es importante
        if (preg_match('/^(VARCHAR|CHAR)\((\d+)\)/i', $type, $match)) {
            return strtoupper($match[1]) . '(' . $match[2] . ')';
        }
        
        // Para otros tipos, devolver el tipo completo sin espacios extras
        $type = preg_replace('/\s+/', ' ', $type);
        // Pero quitar UNSIGNED, AUTO_INCREMENT, etc. para la comparación
        $type = preg_replace('/\s*(UNSIGNED|AUTO_INCREMENT|NOT NULL|NULL|DEFAULT\s+\S+)/i', '', $type);
        return trim($type);
    }
}
