import sys
import os
from module_utils import find_module_path
import glob

# Asegurar que las rutas relevantes estén en sys.path
venv_site_packages = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "veny", "Lib", "site-packages"))
if venv_site_packages not in sys.path:
    sys.path.insert(0, venv_site_packages)

# Ajustar rutas dinámicamente para módulos específicos
for module in ["fusepy", "zstd", "numpy", "Pillow", "anytree"]:
    module_paths = glob.glob(os.path.join(venv_site_packages, f"{module}*"))
    for module_path in module_paths:
        if module_path not in sys.path:
            sys.path.insert(0, module_path)

import os
import sys
import errno
import fuse
import hashlib
import pickle
import math
import threading
import subprocess
from collections import defaultdict
from time import time

# Light-weight Node/DictExporter to avoid importing heavy external dependencies at module import time
class Node:
    def __init__(self, name, parent=None):
        self.name = name
        self.parent = parent
        self.children = []
        if parent is not None:
            parent.children.append(self)
    def __repr__(self):
        return f"Node({self.name})"
    @staticmethod
    def from_dict(d):
        def build(node_dict):
            n = Node(node_dict.get('name'))
            for ch in node_dict.get('children', []):
                child = build(ch)
                child.parent = n
                n.children.append(child)
            return n
        return build(d)

class DictExporter:
    def export(self, node):
        def dump(n):
            return {'name': n.name, 'children': [dump(c) for c in getattr(n, 'children', [])]}
        return dump(node)

import concurrent.futures
# Import numpy robustly (may be optional for some environments)
try:
    import numpy as np
except Exception:
    np = None
try:
    from PIL import Image
except ImportError:
    Image = None
import io

# Prefer zstd compression when available; otherwise fall back to zlib
try:
    import zstd
except Exception:
    try:
        import zlib as zstd
    except Exception:
        zstd = None

fuse.fuse_python_api = (0, 2)

# QuantumFigure class: Represents a data block with quantum-like properties
class QuantumFigure:
    def __init__(self, shape, data, block_size=4096):
        self.shape = shape
        self.data = data
        self.block_size = block_size
        self.states = {}  # Superposition states (label -> processed data)
        self.metadata = {}  # Extended metadata (radius, angle, etc.)
        self.entangled_figures = []  # Simulated quantum entanglement
        self.process_data()

    def process_data(self):
        """Process data to extract geometric properties."""
        data_length = len(self.data)
        radius = math.sqrt(data_length)
        angle = (data_length * 137.5) % 360
        diameter = radius * 2
        density = data_length / self.block_size
        overlap = 0.0  # Placeholder for real overlap calculation
        self.metadata = {
            'radius': radius,
            'angle': angle,
            'diameter': diameter,
            'density': density,
            'overlap': overlap
        }
        self.states['default'] = self.compress_data(self.data)

    def compress_data(self, data):
        """Compress data using zstd when available, otherwise fall back to zlib."""
        if data is None:
            return b""
        try:
            if zstd:
                # zstd may be the zstd package or zlib fallback; call compress with a level where appropriate
                if hasattr(zstd, 'compress'):
                    return zstd.compress(data, 22)
            # fallback to zlib
            import zlib
            return zlib.compress(data)
        except Exception as e:
            # Final fallback: store raw data
            print(f"Compression failed: {e}; storing raw data")
            return data

    def decompress_data(self, compressed_data):
        """Decompress data using zstd or zlib."""
        try:
            if zstd and hasattr(zstd, 'decompress'):
                return zstd.decompress(compressed_data)
            import zlib
            return zlib.decompress(compressed_data)
        except Exception as e:
            # If decompression fails, assume data was stored raw
            print(f"Decompression failed: {e}; returning raw data")
            return compressed_data

    def entangle(self, other_figure):
        """Simulate quantum entanglement between figures."""
        if other_figure not in self.entangled_figures:
            self.entangled_figures.append(other_figure)
            other_figure.entangle(self)

    def collapse_state(self, measurement='default'):
        """Simulate collapsing to a measured state."""
        compressed = self.states.get(measurement, None)
        if compressed is not None:
            return self.decompress_data(compressed)
        return None

    def update_state(self, state_label, new_data):
        """Update or add a new superposition state."""
        self.states[state_label] = new_data

    def __repr__(self):
        return f"QuantumFigure(shape={self.shape}, metadata={self.metadata})"

class DedupFS:
    def __init__(self, mountpoint, *args, **kwargs):
        self.files = {}
        self.data_blocks = {}
        self.block_size = 4096  # 4KB blocks
        self.merkle_tree = Node("root")
        self.snapshots = {}
        self.circular_positions = {}  # Store circular positions for blocks
        self.circular_metadata = {}  # Store additional metadata for circular encoding
        self.quantum_figures = {}  # Store QuantumFigure instances for each block
        self.fs_lock = threading.Lock()  # Agregar el bloqueo faltante.
        self.mountpoint = mountpoint

    def hash_block(self, data):
        return hashlib.sha256(data).hexdigest()

    def add_to_merkle_tree(self, block_hash):
        Node(block_hash, parent=self.merkle_tree)

    def circular_encode(self, data, index):
        """Convert data into circular coordinates."""
        radius = math.sqrt(len(data))  # Radius based on data length
        angle = (index * 137.5) % 360  # Pseudo-random distribution
        return (radius, angle)

    def advanced_circular_encode(self, data, index):
        """
        Advanced circular encoding with extended metadata.
        Returns radius, angle, diameter, density, and overlap.
        """
        radius = math.sqrt(len(data))
        angle = (index * 137.5 + radius) % 360  # Simulated advanced mapping
        diameter = radius * 2
        density = len(data) / self.block_size
        overlap = 0.0  # Placeholder for real overlap calculation
        return {
            'radius': radius,
            'angle': angle,
            'diameter': diameter,
            'density': density,
            'overlap': overlap
        }

    def getattr(self, path, fh=None):
        """Get attributes of a file or directory."""
        return dict(st_mode=(0o755 | 0o040000), st_nlink=2)

    def readdir(self, path, fh):
        """List files in a directory."""
        return ['.', '..'] + list(self.files.keys())

    def read(self, path, size, offset, fh):
        """
        Reconstruct the file using the collapsed state of each QuantumFigure,
        ensuring the original data is recovered.
        Compatible with both normal and compacted Global Index.
        """
        if path not in self.files:
            raise fuse.FuseOSError(errno.ENOENT)

        # Verificar si el índice global está compactado
        compacted_index = self.files.get("compacted_index")
        if compacted_index:
            # Reconstruir desde el índice compacto
            compacted_data = compacted_index.collapse_state()
            if not compacted_data:
                raise RuntimeError("Error: No se pudo descomprimir el índice compacto.")

            # Dividir los datos compactados en bloques individuales
            block_size = self.block_size
            blocks = [compacted_data[i:i + block_size] for i in range(0, len(compacted_data), block_size)]

            # Reconstruir el archivo solicitado
            file_blocks = [blocks[i] for i in self.files[path]['blocks']]
            file_data = b"".join(file_blocks)
        else:
            # Reconstruir desde el índice normal
            file_blocks = []
            for block_hash in self.files[path]['blocks']:
                qf = self.quantum_figures.get(block_hash)
                if qf is not None:
                    file_blocks.append(qf.collapse_state())
                else:
                    file_blocks.append(self.data_blocks[block_hash])  # Fallback
            file_data = b"".join(file_blocks)

        return file_data[offset:offset + size]

    def choose_compression(self, path, buf):
        """
        Selecciona un método de compresión avanzado basado en la extensión del archivo.
        Incluye soporte para LZX (si está disponible) y otros algoritmos.
        """
        ext = os.path.splitext(path)[1].lower()
        if ext == ".txt" and zstd:
            return zstd.compress(buf, 22)
        elif ext in [".png", ".webp"]:
            return self.lossless_image_compress(buf)
        elif ext in [".exe", ".dll"]:  # Archivos binarios donde LZX es eficiente
            return self.lzx_compress(buf)
        else:
            return buf if not zstd else zstd.compress(buf, 22)

    def lzx_compress(self, buf):
        """
        Implementa compresión alternativa usando lzma (en lugar de la librería ficticia pylzx).
        """
        try:
            import lzma
            return lzma.compress(buf)
        except Exception as e:
            print(f"LZX-like compression failed, falling back: {e}")
            return buf

    def flac_compress(self, buf):
        """
        Placeholder para compresión FLAC.
        En producción se integraría una librería como PyFLAC o se llamaría a un comando externo.
        """
        return buf

    def lossless_image_compress(self, buf):
        """
        Usa Pillow para comprimir imágenes en formato PNG (lossless).
        Si falla o Pillow no está disponible, se hace fallback a zstd.
        """
        if Image is not None:
            try:
                image = Image.open(io.BytesIO(buf))
                output = io.BytesIO()
                image.save(output, format="PNG")
                return output.getvalue()
            except Exception as e:
                print(f"Image compression failed: {e}")
        return zstd.compress(buf, 22) if zstd else buf  # Pass compression level as a positional argument

    def lossless_video_compress(self, buf):
        """
        Placeholder para compresión de video sin pérdida.
        En producción se integrarán herramientas como FFmpeg en modo lossless.
        """
        return buf

    def write(self, path, buf, offset, fh):
        """
        Compress the buffer, split it into blocks, and encapsulate each block
        in a QuantumFigure. Entangle each block with the previous one.
        """
        if path not in self.files:
            self.files[path] = {'blocks': []}
        compressed_buf = self.choose_compression(path, buf)  # Use selected compression
        if len(compressed_buf) == 0:
            raise ValueError(f"Error: El archivo '{path}' comprimido tiene tamaño 0.")
        prev_qf = None  # Reference to the previous QuantumFigure
        for i in range(0, len(compressed_buf), self.block_size):
            block = compressed_buf[i:i + self.block_size]
            if len(block) == 0:
                continue  # Ignora bloques vacíos
            block_hash = self.hash_block(block)
            if block_hash not in self.data_blocks:
                self.data_blocks[block_hash] = block
                meta = self.advanced_circular_encode(block, i)  # Use advanced encoding
                self.circular_positions[block_hash] = (meta['radius'], meta['angle'])
                self.circular_metadata[block_hash] = meta  # Store extended metadata
                # Create a QuantumFigure for this block
                qf = QuantumFigure("circle", block, block_size=self.block_size)
                self.quantum_figures[block_hash] = qf
                # Entangle with the previous QuantumFigure
                if prev_qf is not None:
                    prev_qf.entangle(qf)
                prev_qf = qf
            self.files[path]['blocks'].append(block_hash)
        return len(buf)

    def snapshot(self, name):
        self.snapshots[name] = pickle.dumps((self.files, self.data_blocks, DictExporter().export(self.merkle_tree)))

    def restore_snapshot(self, name):
        if name in self.snapshots:
            self.files, self.data_blocks, tree_dict = pickle.loads(self.snapshots[name])
            self.merkle_tree = Node.from_dict(tree_dict)
            # Ensure circular metadata and positions are restored properly
            self.circular_positions = {hash: (0, 0) for hash in self.data_blocks}  # Reset positions
            self.circular_metadata = {}  # Reset metadata

    def garbage_collect_orphans(self):
        """
        Removes orphan blocks (not referenced by any file) from data_blocks, circular metadata, and quantum_figures.
        """
        with self.fs_lock:  # Usar el bloqueo correctamente.
            referenced = set()
            for file_entry in self.files.values():
                referenced.update(file_entry.get('blocks', []))
            orphan_keys = set(self.data_blocks.keys()) - referenced
            for key in orphan_keys:
                del self.data_blocks[key]
                self.circular_positions.pop(key, None)
                self.circular_metadata.pop(key, None)
                self.quantum_figures.pop(key, None)
            print(f"Garbage collection completed. Removed orphans: {orphan_keys}")

    def rebuild_merkle_tree(self):
        """
        Rebuild the Merkle tree based on the currently referenced blocks.
        Useful after garbage collection or major updates.
        """
        self.merkle_tree = Node("root")
        for file_entry in self.files.values():
            for block_hash in file_entry.get('blocks', []):
                self.add_to_merkle_tree(block_hash)
        print("Merkle tree rebuilt.")

    def create_global_figure(self, path):
        """
        Create a global QuantumFigure that unifies all QuantumFigures
        associated with a file by combining their metadata and states.
        """
        if path not in self.files:
            return None
        blocks = self.files[path]['blocks']
        if not blocks:

            return None

        # Function to extract metadata and state from each block
        def extract_metadata(block_hash):
            qf = self.quantum_figures.get(block_hash)
            if qf is not None:
                meta = qf.metadata
                return np.array([
                    meta.get('radius', 0.0),
                    meta.get('angle', 0.0),
                    meta.get('diameter', 0.0),
                    meta.get('density', 0.0),
                    meta.get('overlap', 0.0)
                ], dtype=float), qf.collapse_state()
            else:
                block = self.data_blocks.get(block_hash, b"")
                return np.zeros(5, dtype=float), block

        # Parallel extraction of metadata and states
        metadata_list = []
        state_list = []
        with concurrent.futures.ThreadPoolExecutor() as executor:
            futures = {executor.submit(extract_metadata, bh): bh for bh in blocks}
            for future in concurrent.futures.as_completed(futures):
                meta_vec, state = future.result()
                metadata_list.append(meta_vec)
                state_list.append(state)

        # Combine metadata (e.g., calculate the mean)
        if metadata_list:
            global_meta = np.mean(metadata_list, axis=0)
        else:
            global_meta = np.zeros(5, dtype=float)

        # Combine states (e.g., concatenate and reverse)
        combined_state = b"".join(state_list)[::-1]

        # Create the global QuantumFigure
        global_qf = QuantumFigure("global", combined_state, block_size=self.block_size)
        global_qf.metadata = {
            'radius': float(global_meta[0]),
            'angle': float(global_meta[1]),
            'diameter': float(global_meta[2]),
            'density': float(global_meta[3]),
            'overlap': float(global_meta[4])
        }

        return global_qf

    def measure_redundancy(self):
        """
        Measure empirical redundancy by comparing total block occurrences to unique blocks.
        Returns the percentage of redundant blocks.
        """
        total_blocks = 0
        for file_entry in self.files.values():
            total_blocks += len(file_entry.get('blocks', []))
        unique_blocks = len(self.data_blocks)
        if total_blocks == 0:
            return 0.0
        redundancy_ratio = 1.0 - (unique_blocks / total_blocks)
        print(f"Total blocks: {total_blocks}, Unique blocks: {unique_blocks}, Redundancy: {redundancy_ratio*100:.2f}%")
        return redundancy_ratio

    def superpose_similar_quantum_figures(self, precision=2):
        """
        Fusiona QuantumFigures similares utilizando una combinación ponderada de características
        y una compresión avanzada para los estados fusionados.
        """
        if zstd is None:
            print("zstd no está disponible. No se puede fusionar figuras cuánticas.")
            return
        groups = {}
        with self.fs_lock:
            for bh, qf in self.quantum_figures.items():
                meta = qf.metadata
                key = tuple(round(meta.get(k, 0), precision) for k in ['radius', 'angle', 'diameter', 'density', 'overlap'])
                groups.setdefault(key, []).append(bh)
            
            for key, hashes in groups.items():
                if len(hashes) > 1:
                    meta_vectors = []
                    state_list = []
                    for h in hashes:
                        qf = self.quantum_figures[h]
                        meta_vectors.append(np.array([qf.metadata.get(k, 0.0) for k in ['radius', 'angle', 'diameter', 'density', 'overlap']]))
                        state_list.append(qf.collapse_state())
                    
                    # Combinación ponderada de metadatos
                    weights = np.linspace(1, 2, len(meta_vectors))  # Ejemplo de pesos crecientes
                    merged_meta = np.average(meta_vectors, axis=0, weights=weights)
                    
                    # Compresión avanzada de estados fusionados
                    merged_state = zstd.compress(b"".join(state_list), 22)
                    
                    # Crear una nueva QuantumFigure fusionada
                    merged_qf = QuantumFigure("merged", merged_state, block_size=self.block_size)
                    merged_qf.metadata = {
                        'radius': float(merged_meta[0]),
                        'angle': float(merged_meta[1]),
                        'diameter': float(merged_meta[2]),
                        'density': float(merged_meta[3]),
                        'overlap': float(merged_meta[4])
                    }
                    for h in hashes:
                        self.quantum_figures[h] = merged_qf
                    print(f"Merged {len(hashes)} quantum figures into one for key {key}.")

    def test_reconstruction(self, path):
        """
        Test the reconstruction of a file to ensure data integrity.
        """
        if path not in self.files:
            print(f"Archivo no encontrado: {path}")
            return
        reconstructed_data = self.read(path, size=sys.maxsize, offset=0, fh=None)
        original_data = b"".join(self.data_blocks[block] for block in self.files[path]['blocks'])
        if reconstructed_data == original_data:
            print(f"Reconstrucción exitosa para el archivo: {path}")
        else:
            print(f"Error en la reconstrucción del archivo: {path}")

    def mount(self):
        """Fallback mount: populate the mountpoint with reconstructed physical files."""
        print(f"DEBUG: Intentando montar el sistema de archivos en {self.mountpoint} (fallback mode)...")
        os.makedirs(self.mountpoint, exist_ok=True)
        for path in list(self.files.keys()):
            if path == 'compacted_index':
                continue
            try:
                reconstructed = b"".join([self.quantum_figures.get(bh).collapse_state() if self.quantum_figures.get(bh) else self.data_blocks.get(bh, b"") for bh in self.files[path]['blocks']])
                outpath = os.path.join(self.mountpoint, path)
                os.makedirs(os.path.dirname(outpath), exist_ok=True)
                with open(outpath, 'wb') as f:
                    f.write(reconstructed)
            except Exception as e:
                print(f"WARNING: Could not reconstruct {path}: {e}")
        print("INFO: Mount fallback completed; files copied to mountpoint.")
class QuantumMetadataManager:
    def __init__(self, dedup_fs: DedupFS):
        self.dedup_fs = dedup_fs

    def compact_global_index(self):
        """
        Compacta el índice global combinando bloques redundantes y eliminando duplicados.
        """
        print("Iniciando compactación del índice global...")
        compacted_data = b""
        for block_hash, block_data in self.dedup_fs.data_blocks.items():
            compacted_data += block_data

        # Crear una figura cuántica global para el índice compacto
        compacted_qf = QuantumFigure("global", compacted_data, block_size=self.dedup_fs.block_size)
        compacted_qf.metadata = {
            'radius': np.sqrt(len(compacted_data)),
            'angle': (len(compacted_data) * 137.5) % 360,
            'density': len(compacted_data) / self.dedup_fs.block_size,
        }

        # Actualizar el índice global
        self.dedup_fs.quantum_figures["global_index"] = compacted_qf
        print(f"Índice global compactado. Tamaño: {len(compacted_data)} bytes.")

    def rebuild_merkle_tree(self):
        """
        Reconstruye el árbol Merkle basado en los bloques actualmente referenciados.
        """
        print("Reconstruyendo el árbol Merkle...")
        self.dedup_fs.merkle_tree = Node("root")
        for block_hash in self.dedup_fs.data_blocks.keys():
            Node(block_hash, parent=self.dedup_fs.merkle_tree)
        print("Árbol Merkle reconstruido.")

    def garbage_collect_orphans(self):
        """
        Elimina bloques huérfanos (no referenciados por ningún archivo) del índice global.
        """
        print("Iniciando recolección de basura...")
        referenced_blocks = set()
        for file_entry in self.dedup_fs.files.values():
            referenced_blocks.update(file_entry.get('blocks', []))

        orphan_blocks = set(self.dedup_fs.data_blocks.keys()) - referenced_blocks
        for block_hash in orphan_blocks:
            del self.dedup_fs.data_blocks[block_hash]
            self.dedup_fs.quantum_figures.pop(block_hash, None)

        print(f"Recolección de basura completada. Bloques eliminados: {len(orphan_blocks)}")

    def save_metadata(self, filepath):
        """
        Guarda los metadatos actuales en un archivo.
        """
        print(f"Guardando metadatos en {filepath}...")
        metadata = {
            "files": self.dedup_fs.files,
            "data_blocks": self.dedup_fs.data_blocks,
            "quantum_figures": self.dedup_fs.quantum_figures,
            "merkle_tree": DictExporter().export(self.dedup_fs.merkle_tree),
        }
        with open(filepath, "wb") as f:
            pickle.dump(metadata, f)
        print("Metadatos guardados correctamente.")

    def load_metadata(self, filepath):
        """
        Carga los metadatos desde un archivo.
        """
        print(f"Cargando metadatos desde {filepath}...")
        with open(filepath, "rb") as f:
            metadata = pickle.load(f)
        self.dedup_fs.files = metadata["files"]
        self.dedup_fs.data_blocks = metadata["data_blocks"]
        self.dedup_fs.quantum_figures = metadata["quantum_figures"]
        self.dedup_fs.merkle_tree = Node.from_dict(metadata["merkle_tree"])
        print("Metadatos cargados correctamente.")

    def measure_redundancy(self):
        """
        Mide la redundancia en los bloques de datos.
        """
        total_blocks = len(self.dedup_fs.data_blocks)
        unique_blocks = len(set(self.dedup_fs.data_blocks.values()))
        redundancy = 1 - (unique_blocks / total_blocks) if total_blocks > 0 else 0
        print(f"Redundancia medida: {redundancy * 100:.2f}%")
        return redundancy

if __name__ == "__main__":
    # Ejemplo de uso
    mountpoint = sys.argv[1] if len(sys.argv) > 1 else "/mnt"
    dedup_fs = DedupFS(mountpoint)
    manager = QuantumMetadataManager(dedup_fs)

    # Compactar índice global
    manager.compact_global_index()

    # Reconstruir árbol Merkle
    manager.rebuild_merkle_tree()

    # Recolectar basura
    manager.garbage_collect_orphans()

    # Medir redundancia
    manager.measure_redundancy()
