/**
 * MÓDULO PET - VISIÓN AI Y 3D HIPERREALISTA
 * Permite capturar foto, simula análisis de gráfica PET y genera 3D.
 */

class PetVisionModule {
    constructor() {
        this.input = document.getElementById('pet-camera-input');
        this.dataInput = document.getElementById('pet-data-input'); // Nuevo input para CSV
        this.image = document.getElementById('pet-scanned-image');
        this.grid = document.getElementById('pet-vision-grid');
        this.laser = document.getElementById('pet-laser-scanner');
        this.overlay = document.getElementById('pet-vision-overlay');
        this.resultsBoard = document.getElementById('vision-results-board');
        this.canvasContainer = document.getElementById('pet-3d-canvas-container');

        // Parámetros IA simulados
        this.extractedData = null;

        // Three.js variables
        this.scene = null;
        this.camera = null;
        this.renderer = null;
        this.controls = null;
        this.pileMesh = null;
        this.animationId = null;

        this.init();
    }

    init() {
        if (this.input) {
            this.input.addEventListener('change', (e) => this.handleImageUpload(e));
        }
        if (this.dataInput) {
            this.dataInput.addEventListener('change', (e) => this.handleDataUpload(e));
        }

        // Pre-inicializar escena 3D (vacía)
        this.initThreeScene();
    }

    handleImageUpload(e) {
        const file = e.target.files[0];
        if (!file) return;

        this.prepareUIPanel();

        // Leer imagen
        const reader = new FileReader();
        reader.onload = (event) => {
            this.currentImageBase64 = event.target.result;
            this.image.src = this.currentImageBase64;
            this.startAiAnalysis();
        };
        reader.readAsDataURL(file);
    }

    prepareUIPanel() {
        this.grid.style.display = 'grid'; // Asegurarse de quitar el 'none' nativo
        // Timeout ligero para que la transición CSS funcione
        setTimeout(() => {
            this.grid.classList.add('active');
        }, 50);

        // Limpiar estado
        this.resultsBoard.innerHTML = '';
        this.image.classList.remove('analyzed');
        this.image.style.display = 'block'; // Volver a mostrar si venía de un CSV
        if (this.pileMesh) {
            this.scene.remove(this.pileMesh);
            this.pileMesh = null;
        }

        let titleEl = document.getElementById('pet-ocr-title');
        if (titleEl) {
            titleEl.remove();
        }
    }

    handleDataUpload(e) {
        const file = e.target.files[0];
        if (!file) return;

        this.prepareUIPanel();
        this.image.style.display = 'none'; // Ocultar imagen, mostraremos solo 3D y texto
        this.typeResult(`>>> LEYENDO ARCHIVO DE DATOS: ${file.name} <<<`, 100);

        const reader = new FileReader();
        reader.onload = (event) => {
            const rawText = event.target.result;
            this.typeResult(`Archivo numérico en memoria. Parseando formato PIT/PET...`, 600);

            // Parsear TSV / CSV
            try {
                this.extractedRawData = this.parsePETData(rawText);
                this.typeResult(`>>> DATOS BRUTOS EXTRAÍDOS <<<`, 1200);
                this.typeResult(`- Pilote: ${this.extractedRawData.pileName}`, 1600);
                this.typeResult(`- Muestras (Samples): ${this.extractedRawData.samples}`, 2000);
                this.typeResult(`>>> GENERANDO MODELO 3D DE PRECISIÓN MILIMÉTRICA... <<<`, 2800);

                setTimeout(() => {
                    this.build3DModelFromRaw(this.extractedRawData);
                }, 3200);

            } catch (err) {
                console.error("Error al parsear archivo de datos PET", err);
                this.typeResult(`[ERROR] Formato de archivo irreconocible. Revisa que sea el TSV del PIT/PET.`, 1200);
            }
        };
        reader.readAsText(file);
    }

    parsePETData(rawText) {
        const lines = rawText.split(/\r?\n/);
        let pileName = "Desconocido";
        let numSamples = 0;
        let avgColumnIndex = 0;
        let rawSignal = [];
        let parsingData = false;

        for (let i = 0; i < lines.length; i++) {
            const line = lines[i].trim();
            if (!line) continue;

            // Separar por tabulaciones o comas
            const cl = line.split(/[\t,;]+/);

            if (!parsingData) {
                if (cl[0].toLowerCase().includes("pile name")) {
                    pileName = cl[1] ? cl[1].trim() : "Desconocido";
                }
                if (cl[0].toLowerCase().includes("samples")) {
                    numSamples = parseInt(cl[1]) || 0;
                }

                // Buscar encabezado de la tabla
                if (cl[0].toLowerCase() === "avg") {
                    avgColumnIndex = 0;
                    parsingData = true;
                    continue;
                } else if (cl.indexOf("Avg") !== -1) {
                    avgColumnIndex = cl.indexOf("Avg");
                    parsingData = true;
                    continue;
                }
            } else {
                // Leyendo las filas de datos numéricos
                const val = parseFloat(cl[avgColumnIndex]);
                if (!isNaN(val)) {
                    rawSignal.push(val);
                }
            }
        }

        if (rawSignal.length === 0) {
            throw new Error("No se encontró la columna 'Avg' de impactos.");
        }

        // Solicitar longitud al usuario ya que no suele venir explícita en el TXT de sensores
        let userLength = window.prompt("Introduzca la longitud total del pilote en metros (ej. 15.0):", "12.0");
        let finalLength = parseFloat(userLength);
        if (isNaN(finalLength) || finalLength <= 0) {
            finalLength = 12.0; // Fallback
        }

        return {
            pileName: pileName,
            projectName: "Exportación Directa",
            samples: rawSignal.length,
            signal: rawSignal, // Array de valores acústicos
            length: finalLength
        };
    }

    startAiAnalysis() {
        // UI Animación
        this.laser.style.display = 'block';
        this.overlay.style.display = 'flex';

        // Animación GSAP del láser arriba y abajo
        if (window.gsap) {
            this.laserAnim = gsap.to(this.laser, {
                top: "100%",
                duration: 1.5,
                yoyo: true,
                repeat: -1,
                ease: "linear"
            });
            // El análisis real corre por separado
            this.finishAiAnalysis();
        } else {
            // Fallback si GSAP no carga
            let topPosition = 0;
            let dir = 2; // px por frame aprox
            this.laserInterval = setInterval(() => {
                topPosition += dir;
                if (topPosition >= 296 || topPosition <= 0) dir = -dir;
                this.laser.style.top = topPosition + 'px';
            }, 16);

            // El análisis real corre por separado
            this.finishAiAnalysis();
        }

        // Escribir logs falsos en la consola de resultados
        this.typeResult("[Sistema] Inicializando red neuronal de visión...", 500);
        this.typeResult("[Análisis] Detectando picos de impedancia acústica...", 1500);
        this.typeResult("[Cálculo] Extrayendo onda de velocidad y fuerza...", 2500);
        this.typeResult("[Procesamiento] Localizando anomalías de sección...", 3500);
    }

    typeResult(text, delay) {
        setTimeout(() => {
            const line = document.createElement('div');
            line.className = 'vision-result-line';
            line.textContent = text;
            this.resultsBoard.scrollTop = this.resultsBoard.scrollHeight;
        }, delay);
    }

    calculateRealWorldDimensions(imageBase64) {
        return new Promise((resolve, reject) => {
            // --- NUEVO: API Key en Cliente para saltar problemas NAS ---
            let apiKey = localStorage.getItem('pet_openai_key');
            if (!apiKey) {
                apiKey = window.prompt("Introduce tu OPENAI_API_KEY para habilitar la Visión IA en este dispositivo:", "sk-proj-...");
                if (!apiKey || apiKey.trim() === '' || apiKey.includes('sk-proj-...')) {
                    return reject(new Error("API Key requerida para el análisis IA."));
                }
                localStorage.setItem('pet_openai_key', apiKey.trim());
            }

            // Preparar payload enviando la clave desde el cliente al puente
            const payload = {
                image: imageBase64,
                client_api_key: apiKey.trim()
            };

            fetch('/api/analyze-pet', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(payload)
            })
                .then(response => {
                    if (!response.ok) {
                        return response.json().then(errorData => {
                            // Si el server dice que la clave falló, borrarla del localstorage
                            if (response.status === 401 || response.status === 500 || (errorData.error && errorData.error.includes('Incorrect API key'))) {
                                localStorage.removeItem('pet_openai_key');
                            }
                            throw new Error(errorData.error || `Error del servidor de IA: ${response.status}`);
                        }).catch(parseErr => {
                            throw new Error(`Error del servidor de IA: ${response.status}`);
                        });
                    }
                    return response.json();
                })
                .then(data => {
                    console.log("Datos del servidor AI:", data);
                    resolve({
                        projectName: data.projectName || "Desconocido",
                        pileName: data.pileName || "Desconocido",
                        length: data.length || 10.0,
                        diameter: 0.60, // Fijo visual
                        defects: data.defects || [],
                        status: data.status || 'ok'
                    });
                })
                .catch(err => {
                    console.error("AI Bridge Error:", err);
                    reject(err);
                });
        });
    }

    async finishAiAnalysis() {
        // Ejecutar cálculo real en el backend Ruby -> OpenAI
        try {
            this.extractedData = await this.calculateRealWorldDimensions(this.currentImageBase64);
        } catch (error) {
            console.error("Error al obtener datos de la IA:", error);
            this.typeResult(`[ERROR] Fallo en el análisis de IA: ${error.message}`, 200);
            this.typeResult(`[ERROR] Usando fallback procedimental por seguridad...`, 600);

            // Fallback procedimental por seguridad
            this.extractedData = {
                projectName: "Simulado Local",
                pileName: "P-???",
                length: (Math.random() * 8 + 8).toFixed(2), // 8m a 16m
                diameter: 0.60,
                defects: [
                    {
                        depth: (Math.random() * 4 + 2).toFixed(2),
                        severity: Math.random() > 0.5 ? 'estriccion_leve' : 'ensanchamiento_leve'
                    }
                ],
                status: 'fallback'
            };
        }

        // Parar la animación de escaneo
        if (this.laserAnim) this.laserAnim.kill();
        if (this.laserInterval) clearInterval(this.laserInterval);

        this.laser.style.display = 'none';
        this.overlay.style.display = 'none';
        this.image.classList.add('analyzed');

        // Mostrar nombres de OCR en el UI
        let titleEl = document.getElementById('pet-ocr-title');
        if (!titleEl) {
            titleEl = document.createElement('div');
            titleEl.id = 'pet-ocr-title';
            titleEl.style.cssText = `
                position: absolute; top: 10px; left: 10px; z-index: 10;
                color: #00ffcc; font-family: monospace; font-size: 14px;
                background: rgba(0,0,0,0.6); padding: 5px 10px; border-radius: 4px;
                border-left: 3px solid #00ffcc;
                text-shadow: 0 0 5px rgba(0, 255, 204, 0.5);
            `;
            this.canvasContainer.appendChild(titleEl);
        }
        titleEl.innerHTML = `<strong>PROJECT:</strong> ${this.extractedData.projectName}<br/><strong>PILE:</strong> ${this.extractedData.pileName}`;

        this.typeResult(`>>> ANÁLISIS OCR COMPLETADO <<<`, 200);
        this.typeResult(`- Proyecto: ${this.extractedData.projectName}`, 600);
        this.typeResult(`- Pilote ID: ${this.extractedData.pileName}`, 1000);
        this.typeResult(`---------------------------------`, 1400);
        this.typeResult(`>>> DATOS GEOMÉTRICOS <<<`, 1800);
        this.typeResult(`- Longitud Total: ${this.extractedData.length} metros`, 2200);

        const totalDefects = this.extractedData.defects.length;
        this.typeResult(`- Anomalías Detectadas: ${totalDefects}`, 2600);

        let delay = 3000;
        if (totalDefects > 0) {
            this.extractedData.defects.forEach((def, index) => {
                let tipoVisual = def.severity;
                if (tipoVisual === 'estriccion_severa') tipoVisual = "Estricción Severa";
                if (tipoVisual === 'estriccion_leve') tipoVisual = "Estricción Leve";
                if (tipoVisual === 'ensanchamiento_severo') tipoVisual = "Ensanchamiento Severo";
                if (tipoVisual === 'ensanchamiento_leve') tipoVisual = "Ensanchamiento Leve";

                this.typeResult(`  [#${index + 1}] A cota -${def.depth}m -> ${tipoVisual}`, delay);
                delay += 400;
            });
        } else {
            this.typeResult(`  (Perfil continuo óptimo)`, delay);
            delay += 400;
        }

        this.typeResult(`>>> GENERANDO MODELO 3D CONTINUO... <<<`, delay + 400);

        setTimeout(() => {
            this.build3DModel(this.extractedData);
        }, delay + 800);
    }

    // ==========================================
    // THREE.JS - MODELO 3D PROCEDIMENTAL
    // ==========================================

    initThreeScene() {
        if (!THREE) {
            console.error("Three.js no está cargado para Pet Vision.");
            return;
        }

        const rect = this.canvasContainer.getBoundingClientRect();
        const width = rect.width || 400;
        const height = rect.height || 400;

        // Escena
        this.scene = new THREE.Scene();

        // Cámara
        this.camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
        // Posicionaremos la cámara para ver bien un pilote largo
        this.camera.position.set(0, 5, 20);

        // Renderer
        this.renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
        this.renderer.setSize(width, height);
        this.renderer.setPixelRatio(window.devicePixelRatio);
        this.renderer.shadowMap.enabled = true;
        this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
        this.canvasContainer.appendChild(this.renderer.domElement);

        // Controles
        this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
        this.controls.enableDamping = true;
        this.controls.dampingFactor = 0.05;

        // Luces para hiperrealismo
        const ambientLight = new THREE.AmbientLight(0x404040, 1.5); // Soft white
        this.scene.add(ambientLight);

        const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
        dirLight.position.set(10, 20, 10);
        dirLight.castShadow = true;
        this.scene.add(dirLight);

        const blueLight = new THREE.PointLight(0x00ffff, 1, 50);
        blueLight.position.set(-10, 0, 10);
        this.scene.add(blueLight);

        // Resize handler local
        window.addEventListener('resize', () => {
            if (!this.grid.classList.contains('active') && !this.canvasContainer.classList.contains('fullscreen-3d')) return;
            const r = this.canvasContainer.getBoundingClientRect();
            // Evitar resoluciones 0 y errores del viewport
            const width = r.width || window.innerWidth;
            const height = r.height || window.innerHeight;
            this.camera.aspect = width / height;
            this.camera.updateProjectionMatrix();
            this.renderer.setSize(width, height);
        });

        // Doble click para Pantalla Completa
        this.canvasContainer.addEventListener('dblclick', () => {
            this.canvasContainer.classList.toggle('fullscreen-3d');
            // Forzar resize inmediato de Three.js para adaptar al nuevo tamaño
            setTimeout(() => window.dispatchEvent(new Event('resize')), 50);
        });

        this.animate();
    }

    createConcreteTexture() {
        // Generar una textura rudimentaria procedimental de hormigón en canvas
        const canvas = document.createElement('canvas');
        canvas.width = 512;
        canvas.height = 512;
        const ctx = canvas.getContext('2d');

        // Base gris
        ctx.fillStyle = '#8a8d8f';
        ctx.fillRect(0, 0, 512, 512);

        // Función local pseudo-aleatoria determinista simple basada en semilla
        let seed = 12345;
        const seededRandom = () => {
            seed = (seed * 9301 + 49297) % 233280;
            return seed / 233280;
        };

        // Ruido y manchas
        for (let i = 0; i < 3000; i++) {
            const x = seededRandom() * 512;
            const y = seededRandom() * 512;
            const r = seededRandom() * 2;
            const c = seededRandom() > 0.5 ? '#6a6d6f' : '#aab0b5';
            ctx.fillStyle = c;
            ctx.beginPath();
            ctx.arc(x, y, r, 0, Math.PI * 2);
            ctx.fill();
        }

        const texture = new THREE.CanvasTexture(canvas);
        texture.wrapS = THREE.RepeatWrapping;
        texture.wrapT = THREE.RepeatWrapping;
        texture.repeat.set(1, 4); // Repetir verticalmente
        return texture;
    }

    build3DModelFromRaw(data) {
        // En base al Raw Data del fichero numérico (Columna Avg)
        const length = data.length || 10;
        const signal = data.signal || [];

        // UI Nombres
        let titleEl = document.getElementById('pet-ocr-title');
        if (!titleEl) {
            titleEl = document.createElement('div');
            titleEl.id = 'pet-ocr-title';
            titleEl.style.cssText = `
                position: absolute; top: 10px; left: 10px; z-index: 10;
                color: #00ffcc; font-family: monospace; font-size: 14px;
                background: rgba(0,0,0,0.6); padding: 5px 10px; border-radius: 4px;
                border-left: 3px solid #ff00ff;
                text-shadow: 0 0 5px rgba(255, 0, 255, 0.5);
            `;
            this.canvasContainer.appendChild(titleEl);
        }
        titleEl.innerHTML = `<strong>DATOS BRUTOS</strong><br/><strong>PILE:</strong> ${data.pileName} <br/><strong>SAMPLES:</strong> ${signal.length}`;

        const maxDrawHeight = length; // AHORA DINÁMICO SEGÚN EL INPUT DEL USUARIO
        const radius = 1;
        const radSegs = 32;
        // Para precisión milimétrica, le damos tantos segmentos de altura como muestras (limitado para no reventar la GPU)
        const heightSegs = Math.min(signal.length, 300);
        const geometry = new THREE.CylinderGeometry(radius, radius, maxDrawHeight, radSegs, heightSegs);

        const positionAttribute = geometry.attributes.position;
        const vertex = new THREE.Vector3();

        // --- NUEVO: INTEGRACIÓN ACÚSTICA (Perfil de Impedancia Real) ---
        // La señal cruda es una onda alterna que vibra como un muelle (eco).
        // Si usamos la onda cruda, se genera un "tornillo" en 3D.
        // La física dicta que debemos INTEGRAR (sumar el área) el reflectograma 
        // para obtener la forma geométrica de la sección transversal.
        let runningSum = 0;
        const leak = 0.95; // Factor de decaimiento para estabilizar la forma central
        const integratedSignal = [];
        for (let i = 0; i < signal.length; i++) {
            // Extraer el cambio físico real
            runningSum = (runningSum * leak) + (signal[i] * 3.0);
            integratedSignal.push(runningSum);
        }

        // Luego suavizamos el resultado integrado para que el hormigón quede liso
        const windowSize = 25;
        const smoothedSignal = [];
        for (let i = 0; i < integratedSignal.length; i++) {
            let sum = 0;
            let count = 0;
            for (let j = Math.max(0, i - windowSize); j <= Math.min(integratedSignal.length - 1, i + windowSize); j++) {
                sum += integratedSignal[j];
                count++;
            }
            smoothedSignal.push(sum / count);
        }
        // Calcular máxima amplitud integrada para normalizar proporciones
        const maxSmoothedAmp = Math.max(...smoothedSignal.map(Math.abs)) || 1;
        // ----------------------------------------------------

        for (let i = 0; i < positionAttribute.count; i++) {
            vertex.fromBufferAttribute(positionAttribute, i);
            if (vertex.x === 0 && vertex.z === 0) continue;

            // Extraer a qué % de "altura" del cilindro estamos (-maxDrawHeight/2 a maxDrawHeight/2)
            // Threejs renderiza de +Y (arriba) a -Y (abajo). Queremos que la punta (+Y) sea el sample 0
            const percentDepth = 1.0 - ((vertex.y + (maxDrawHeight / 2)) / maxDrawHeight);

            // Encontrar qué índice de Sample en la señal (Avg) corresponde a este % de profundidad
            let signalIndex = Math.floor(percentDepth * signal.length);
            if (signalIndex < 0) signalIndex = 0;
            if (signalIndex >= signal.length) signalIndex = signal.length - 1;

            // El valor acústico (SUAVIZADO)
            const ampValue = smoothedSignal[signalIndex];

            // Mapeamos el valor a estricción/ensanchamiento. 
            // Esto dependerá de la polaridad de tu sensor. Asumimos que amp positiva = cuello (<1.0 r), negativa = bulbo (>1.0 r)
            // Reducimos enormemente la sensibilidad (de 0.4 a 0.15) para que solo defectos ENORMES modifiquen la forma visual.
            const scaleFactor = 1.0 - ((ampValue / maxSmoothedAmp) * 0.15); // Varía de 0.85 a 1.15

            // --- NUEVO: FILTRO DE EXTREMOS (IMPACTO Y REBOTE) ---
            // Aplicar ventana de exclusión (fade out) en los extremos (Top 5% y Bottom 5%) 
            // para ignorar el pico de impacto del martillo y el rebote en la punta, según ASTM D5882.
            let edgeMask = 1.0;
            if (percentDepth < 0.05) {
                // Transición suave de 0 a 1 en el primer 5% (cabeza)
                edgeMask = percentDepth / 0.05;
            } else if (percentDepth > 0.95) {
                // Transición suave de 1 a 0 en el último 5% (punta/rebote)
                edgeMask = (1.0 - percentDepth) / 0.05;
            }

            // Aplicamos la máscara: si edgeMask es 0, el radio es 1.0 (cilindro perfecto).
            // Si edgeMask es 1, aplicamos toda la deformación calculada acústicamente.
            let finalRadiusMod = 1.0 + ((scaleFactor - 1.0) * edgeMask);

            // Eliminado el ruido geométrico pseudo-aleatorio porque los segmentos
            // radiales causaban un artefacto de espiral matemática perfecta (efecto tornillo).
            // La rugosidad visual la aporta únicamente la textura de hormigón.

            vertex.x *= finalRadiusMod;
            vertex.z *= finalRadiusMod;
            positionAttribute.setXYZ(i, vertex.x, vertex.y, vertex.z);
        }

        geometry.computeVertexNormals();

        const material = new THREE.MeshStandardMaterial({
            map: this.createConcreteTexture(),
            roughness: 0.8,
            metalness: 0.1,
            color: 0xffffff
        });

        this.pileMesh = new THREE.Mesh(geometry, material);
        this.pileMesh.castShadow = true;
        this.pileMesh.receiveShadow = true;
        this.scene.add(this.pileMesh);

        this.camera.position.set(0, 5, maxDrawHeight + 2);
        this.controls.target.set(0, maxDrawHeight / 2, 0);
        this.controls.update();

        // 🧠 --- Disparar Diagnóstico Experto (NotebookLM) ---
        this.fetchExpertDiagnosis({
            projectName: data.projectName || 'Datos CSV Crudos',
            pileName: data.pileName || 'Desconocido',
            length: length,
            defects: [{ depth: "Varias a lo largo del fuste detectadas matemáticamente", severity: "Ver modelo 3D" }],
            status: "Basado en mediciones numéricas"
        });
    }

    build3DModel(data) {
        const length = parseFloat(data.length) || 10;
        const defects = data.defects || [];

        // Función auxiliar para obtener el factor de severidad
        const getSeverityFactor = (severityString) => {
            const str = (severityString || "").toLowerCase();
            if (str.includes('estriccion_severa')) return 0.4;
            if (str.includes('estriccion_leve')) return 0.7;
            if (str.includes('ensanchamiento_severo')) return 1.6;
            if (str.includes('ensanchamiento_leve')) return 1.3;
            return 1.0;
        };

        // Para escalado visual, hacemos que el pilote mida en 3D siempre un tope
        const maxDrawHeight = 15;
        const scaleFact = maxDrawHeight / length;
        const drawHeight = length * scaleFact; // será maxDrawHeight

        // Crear geometría cilindrica
        // Radio, Altura, Segmentos radiales, Segmentos de altura
        const radius = 1; // Base visual
        const radSegs = 32;
        const heightSegs = 40;
        const geometry = new THREE.CylinderGeometry(radius, radius, drawHeight, radSegs, heightSegs);

        // Modificar vértices para el defecto
        // La Y en geometry de three.js va de +drawHeight/2 (top) a -drawHeight/2 (bottom)
        const positionAttribute = geometry.attributes.position;
        const vertex = new THREE.Vector3();

        for (let i = 0; i < positionAttribute.count; i++) {
            vertex.fromBufferAttribute(positionAttribute, i);

            // Ignorar vértices del centro (tapas superior e inferior)
            if (vertex.x === 0 && vertex.z === 0) continue;

            let finalRadiusMod = 1.0;
            let finalColorMod = 1.0; // 1 = gris, <1 rojo, >1 verde

            // Iterar sobre todos los defectos para sumar/restar al perfil
            defects.forEach(def => {
                const defectY3d = (drawHeight / 2) - (parseFloat(def.depth) * scaleFact);
                const distToDefect = Math.abs(vertex.y - defectY3d);
                const influenceZone = 1.5; // tamaño de curva

                if (distToDefect < influenceZone) {
                    const force = Math.cos((distToDefect / influenceZone) * (Math.PI / 2));
                    const sevFact = getSeverityFactor(def.severity);
                    const localMod = 1.0 + ((sevFact - 1.0) * force);

                    // Si hay varios defectos solapados, multiplicamos las deformaciones
                    finalRadiusMod *= localMod;

                    // Para color: acercamos a sevFact
                    if (force > 0.5) finalColorMod = sevFact;
                }
            });

            // Igual que en raw, evitamos modificar la geometría base con ruido pseudoaleatorio 
            // para no generar artefactos de espiral, dejando la textura encargada de la rugosidad.
            vertex.x *= finalRadiusMod;
            vertex.z *= finalRadiusMod;
            positionAttribute.setXYZ(i, vertex.x, vertex.y, vertex.z);
        }

        geometry.computeVertexNormals();

        let overrideColor = 0xffffff;
        if (defects.length > 0) {
            // Evaluamos el color dominante para el material basándonos en si la mayoría son bulbos o cuellos
            const avgSev = defects.reduce((acc, def) => acc + getSeverityFactor(def.severity), 0) / defects.length;
            if (avgSev < 0.9) overrideColor = 0xffdddd;
            if (avgSev > 1.1) overrideColor = 0xccffcc;
        }

        // Material 
        const material = new THREE.MeshStandardMaterial({
            map: this.createConcreteTexture(),
            roughness: 0.8,
            metalness: 0.1,
            color: overrideColor
        });

        this.pileMesh = new THREE.Mesh(geometry, material);
        this.pileMesh.castShadow = true;
        this.pileMesh.receiveShadow = true;

        this.scene.add(this.pileMesh);

        // Ajustar cámara para centrar
        this.camera.position.set(0, 0, drawHeight + 2);
        this.controls.target.set(0, 0, 0);

        // Animacion intro: venir de abajo
        this.pileMesh.position.y = -drawHeight;
        if (window.gsap) {
            gsap.to(this.pileMesh.position, {
                y: 0,
                duration: 2,
                ease: "power2.out"
            });
        } else {
            this.pileMesh.position.y = 0;
        }

        // 🧠 --- Disparar Diagnóstico Experto (NotebookLM) ---
        this.fetchExpertDiagnosis(data);
    }

    animate() {
        this.animationId = requestAnimationFrame(() => this.animate());

        // Rotación orbital lenta pasiva
        if (this.pileMesh && !this.controls.state) { // Girar si el usuario no lo está tocando
            this.pileMesh.rotation.y += 0.005;
        }

        this.controls.update();
        this.renderer.render(this.scene, this.camera);
    }

    fetchExpertDiagnosis(analysisData) {
        const expertPanel = document.getElementById('expert-diagnosis-panel');
        const expertContent = document.getElementById('expert-diagnosis-content');

        if (!expertPanel || !expertContent) return;

        // Mostrar panel con animación de carga
        expertPanel.style.display = 'block';
        expertContent.innerHTML = '<span class="loading-pulse"> Consultando Cuaderno NotebookLM Técnico y Normativa ASTM D5882...</span>';

        let apiKey = localStorage.getItem('pet_openai_key') || '';

        const payload = {
            analysisData: analysisData,
            client_api_key: apiKey
        };

        fetch('/api/expert-diagnosis', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload)
        })
            .then(response => {
                if (!response.ok) throw new Error("Fallo en la consulta al sistema experto.");
                return response.json();
            })
            .then(data => {
                const textToType = data.diagnosis || "[Error] No se pudo generar la narrativa experta.";
                expertContent.innerHTML = ''; // Limpiar loader

                // Simular escritura letra a letra del experto
                let i = 0;
                const typeWriter = setInterval(() => {
                    if (i < textToType.length) {
                        expertContent.innerHTML += textToType.charAt(i);
                        i++;
                    } else {
                        clearInterval(typeWriter);
                        // Asegurar formateo básico de saltos de línea y negritas si GPT las devuelve
                        expertContent.innerHTML = textToType.replace(/\n/g, '<br/>').replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
                    }
                }, 10); // 10ms por caracter
            })
            .catch(err => {
                console.error("Expert Diagnosis Error:", err);
                expertContent.innerHTML = '<span style="color:var(--neon-magenta);">[Error de conexión con el Asesor Técnico] El puente con NotebookLM se ha interrumpido o falta la API Key.</span>';
            });
    }
}

// Inicializar cuando DOM cargue
document.addEventListener('DOMContentLoaded', () => {
    window.petVisionModule = new PetVisionModule();
});
