"""
Nextcloud Remote File Reader
Supports multiple file types including LiDAR (.laz, .las), compressed numpy (.npz), and more
"""

import io
import requests
import pandas as pd
import numpy as np
from typing import Union, List, Optional, Any
from pathlib import Path


class NextcloudReader:
    def __init__(self, nextcloud_url: str, username: str, app_password: str):
        """
        Initialize Nextcloud reader
        
        Args:
            nextcloud_url: Your Nextcloud server URL (e.g., 'https://cloud.example.com')
            username: Your Nextcloud username
            app_password: Your Nextcloud app password
        """
        self.nextcloud_url = nextcloud_url.rstrip('/')
        self.webdav_url = f"{self.nextcloud_url}/remote.php/dav/files/{username}"
        self.auth = (username, app_password)
    
    def read_file_content(self, remote_path: str) -> bytes:
        """
        Read raw file content from Nextcloud
        
        Args:
            remote_path: Path to file on Nextcloud (e.g., '/inputs/folder1/data.csv')
            
        Returns:
            File content as bytes
        """
        remote_path = remote_path.lstrip('/')
        url = f"{self.webdav_url}/{remote_path}"
        
        try:
            response = requests.get(url, auth=self.auth)
            response.raise_for_status()
            return response.content
        except requests.exceptions.HTTPError as e:
            raise Exception(f"Failed to read file {remote_path}: {e}")
    
    def read_text_file(self, remote_path: str, encoding: str = 'utf-8') -> str:
        """
        Read text file from Nextcloud
        
        Args:
            remote_path: Path to file on Nextcloud
            encoding: Text encoding (default: 'utf-8')
            
        Returns:
            File content as string
        """
        content = self.read_file_content(remote_path)
        return content.decode(encoding)
    
    def read_csv(self, remote_path: str, **kwargs) -> pd.DataFrame:
        """
        Read CSV file from Nextcloud into pandas DataFrame
        
        Args:
            remote_path: Path to CSV file on Nextcloud
            **kwargs: Additional arguments passed to pd.read_csv()
            
        Returns:
            pandas DataFrame
        """
        content = self.read_file_content(remote_path)
        return pd.read_csv(io.BytesIO(content), **kwargs)
    
    def read_excel(self, remote_path: str, **kwargs) -> Union[pd.DataFrame, dict]:
        """
        Read Excel file from Nextcloud into pandas DataFrame
        
        Args:
            remote_path: Path to Excel file on Nextcloud
            **kwargs: Additional arguments passed to pd.read_excel()
            
        Returns:
            pandas DataFrame or dict of DataFrames (if sheet_name=None)
        """
        content = self.read_file_content(remote_path)
        return pd.read_excel(io.BytesIO(content), **kwargs)
    
    def read_json(self, remote_path: str, **kwargs) -> Union[dict, list, pd.DataFrame]:
        """
        Read JSON file from Nextcloud
        
        Args:
            remote_path: Path to JSON file on Nextcloud
            **kwargs: Additional arguments passed to pd.read_json()
            
        Returns:
            pandas DataFrame, dict, or list depending on JSON structure
        """
        content = self.read_file_content(remote_path)
        return pd.read_json(io.BytesIO(content), **kwargs)
    
    def read_numpy(self, remote_path: str, **kwargs) -> np.ndarray:
        """
        Read numpy array from Nextcloud (.npy file)
        
        Args:
            remote_path: Path to .npy file on Nextcloud
            **kwargs: Additional arguments passed to np.load()
            
        Returns:
            numpy array
        """
        content = self.read_file_content(remote_path)
        return np.load(io.BytesIO(content), **kwargs)
    
    def read_npz(self, remote_path: str, **kwargs) -> np.lib.npyio.NpzFile:
        """
        Read compressed numpy archive from Nextcloud (.npz file)
        
        Args:
            remote_path: Path to .npz file on Nextcloud
            **kwargs: Additional arguments passed to np.load()
            
        Returns:
            NpzFile object (dict-like, access arrays with file['array_name'])
        
        Example:
            npz_data = nc.read_npz('/path/to/data.npz')
            array1 = npz_data['array1']
            array2 = npz_data['array2']
            npz_data.close()  # Don't forget to close
        """
        content = self.read_file_content(remote_path)
        return np.load(io.BytesIO(content), **kwargs)
    
    def read_pickle(self, remote_path: str) -> Any:
        """
        Read pickled Python object from Nextcloud (.pkl, .pickle file)
        
        Args:
            remote_path: Path to pickle file on Nextcloud
            
        Returns:
            Unpickled Python object
        """
        import pickle
        content = self.read_file_content(remote_path)
        return pickle.loads(content)
    
    def read_parquet(self, remote_path: str, **kwargs) -> pd.DataFrame:
        """
        Read Parquet file from Nextcloud
        
        Args:
            remote_path: Path to Parquet file on Nextcloud
            **kwargs: Additional arguments passed to pd.read_parquet()
            
        Returns:
            pandas DataFrame
        """
        content = self.read_file_content(remote_path)
        return pd.read_parquet(io.BytesIO(content), **kwargs)
    
    def read_feather(self, remote_path: str, **kwargs) -> pd.DataFrame:
        """
        Read Feather file from Nextcloud
        
        Args:
            remote_path: Path to Feather file on Nextcloud
            **kwargs: Additional arguments passed to pd.read_feather()
            
        Returns:
            pandas DataFrame
        """
        content = self.read_file_content(remote_path)
        return pd.read_feather(io.BytesIO(content), **kwargs)
    
    def read_hdf(self, remote_path: str, key: str, **kwargs) -> pd.DataFrame:
        """
        Read HDF5 file from Nextcloud
        
        Args:
            remote_path: Path to HDF5 file on Nextcloud
            key: Group identifier in the store
            **kwargs: Additional arguments passed to pd.read_hdf()
            
        Returns:
            pandas DataFrame
        """
        content = self.read_file_content(remote_path)
        return pd.read_hdf(io.BytesIO(content), key=key, **kwargs)
    
    def read_las(self, remote_path: str):
        """
        Read LAS (LiDAR) file from Nextcloud
        
        Args:
            remote_path: Path to .las file on Nextcloud
            
        Returns:
            laspy LasData object
            
        Note: Requires laspy library: pip install laspy
        """
        try:
            import laspy
        except ImportError:
            raise ImportError("laspy is required to read LAS files. Install with: pip install laspy")
        
        content = self.read_file_content(remote_path)
        return laspy.read(io.BytesIO(content))
    
    def read_laz(self, remote_path: str):
        """
        Read LAZ (compressed LiDAR) file from Nextcloud
        
        Args:
            remote_path: Path to .laz file on Nextcloud
            
        Returns:
            laspy LasData object
            
        Note: Requires laspy with LAZ support: pip install laspy[laszip]
        """
        try:
            import laspy
        except ImportError:
            raise ImportError("laspy is required to read LAZ files. Install with: pip install laspy[laszip]")
        
        content = self.read_file_content(remote_path)
        return laspy.read(io.BytesIO(content))
    
    def read_shapefile(self, remote_path: str):
        """
        Read Shapefile from Nextcloud
        
        Args:
            remote_path: Path to .shp file on Nextcloud
            
        Returns:
            geopandas GeoDataFrame
            
        Note: Requires geopandas: pip install geopandas
        Note: You need to have all shapefile components (.shp, .shx, .dbf, etc.) on Nextcloud
        """
        try:
            import geopandas as gpd
        except ImportError:
            raise ImportError("geopandas is required to read shapefiles. Install with: pip install geopandas")
        
        content = self.read_file_content(remote_path)
        return gpd.read_file(io.BytesIO(content))
    
    def read_dbf(self, remote_path: str) -> pd.DataFrame:
        """
        Read DBF (dBase) file from Nextcloud
        
        Args:
            remote_path: Path to .dbf file on Nextcloud
            
        Returns:
            pandas DataFrame
            
        Note: DBF files are commonly used as attribute tables in shapefiles
        Note: Requires simpledbf or geopandas: pip install simpledbf
        """
        try:
            from simpledbf import Dbf5
            content = self.read_file_content(remote_path)
            # simpledbf needs a file path, so use tempfile
            import tempfile
            with tempfile.NamedTemporaryFile(delete=False, suffix='.dbf') as tmp:
                tmp.write(content)
                tmp_path = tmp.name
            
            dbf = Dbf5(tmp_path)
            df = dbf.to_dataframe()
            
            # Clean up temp file
            import os
            os.unlink(tmp_path)
            return df
            
        except ImportError:
            # Fallback to using geopandas if available
            try:
                import geopandas as gpd
                content = self.read_file_content(remote_path)
                import tempfile
                with tempfile.NamedTemporaryFile(delete=False, suffix='.dbf') as tmp:
                    tmp.write(content)
                    tmp_path = tmp.name
                
                # Read as a simple table
                import fiona
                with fiona.open(tmp_path) as src:
                    data = [feature['properties'] for feature in src]
                
                import os
                os.unlink(tmp_path)
                return pd.DataFrame(data)
            except:
                raise ImportError(
                    "simpledbf or geopandas required to read DBF files. "
                    "Install with: pip install simpledbf OR pip install geopandas"
                )
    
    def read_prj(self, remote_path: str) -> str:
        """
        Read PRJ (projection) file from Nextcloud
        
        Args:
            remote_path: Path to .prj file on Nextcloud
            
        Returns:
            Projection string (WKT format)
            
        Note: PRJ files contain coordinate system information for shapefiles
        """
        content = self.read_text_file(remote_path)
        return content.strip()
    
    def read_shx(self, remote_path: str) -> bytes:
        """
        Read SHX (shapefile index) file from Nextcloud
        
        Args:
            remote_path: Path to .shx file on Nextcloud
            
        Returns:
            Raw binary content
            
        Note: SHX files are shapefile index files (binary format)
        Note: Typically used together with .shp files, not read standalone
        """
        return self.read_file_content(remote_path)
    
    def read_hdf5(self, remote_path: str, **kwargs):
        """
        Read HDF5 file from Nextcloud
        
        Args:
            remote_path: Path to .hdf5, .h5, or .hd5 file on Nextcloud
            **kwargs: Additional arguments passed to h5py.File()
            
        Returns:
            h5py File object (use in context manager)
            
        Note: Requires h5py: pip install h5py
        
        Example:
            with nc.read_hdf5('/path/to/data.hdf5') as f:
                dataset = f['dataset_name'][:]
        """
        try:
            import h5py
        except ImportError:
            raise ImportError("h5py is required to read HDF5 files. Install with: pip install h5py")
        
        content = self.read_file_content(remote_path)
        return h5py.File(io.BytesIO(content), 'r', **kwargs)
    
    def read_geojson(self, remote_path: str):
        """
        Read GeoJSON file from Nextcloud
        
        Args:
            remote_path: Path to .geojson file on Nextcloud
            
        Returns:
            geopandas GeoDataFrame
            
        Note: Requires geopandas: pip install geopandas
        """
        try:
            import geopandas as gpd
        except ImportError:
            raise ImportError("geopandas is required to read GeoJSON. Install with: pip install geopandas")
        
        content = self.read_file_content(remote_path)
        return gpd.read_file(io.BytesIO(content))
    
    def read_kml(self, remote_path: str):
        """
        Read KML file from Nextcloud
        
        Args:
            remote_path: Path to .kml file on Nextcloud
            
        Returns:
            geopandas GeoDataFrame
            
        Note: Requires geopandas with fiona: pip install geopandas fiona
        Note: For more advanced KML parsing, consider using fastkml or pykml
        """
        try:
            import geopandas as gpd
        except ImportError:
            raise ImportError("geopandas is required to read KML. Install with: pip install geopandas fiona")
        
        # Enable KML driver
        try:
            import fiona
            fiona.drvsupport.supported_drivers['KML'] = 'rw'
        except:
            pass
        
        content = self.read_file_content(remote_path)
        return gpd.read_file(io.BytesIO(content), driver='KML')
    
    def read_kmz(self, remote_path: str):
        """
        Read KMZ file from Nextcloud (compressed KML)
        
        Args:
            remote_path: Path to .kmz file on Nextcloud
            
        Returns:
            geopandas GeoDataFrame
            
        Note: Requires geopandas with fiona: pip install geopandas fiona
        Note: KMZ is a zipped KML file
        """
        import zipfile
        try:
            import geopandas as gpd
        except ImportError:
            raise ImportError("geopandas is required to read KMZ. Install with: pip install geopandas fiona")
        
        # Enable KML driver
        try:
            import fiona
            fiona.drvsupport.supported_drivers['KML'] = 'rw'
        except:
            pass
        
        # KMZ is a zipped KML file
        content = self.read_file_content(remote_path)
        with zipfile.ZipFile(io.BytesIO(content)) as kmz:
            # Find the KML file inside (usually doc.kml)
            kml_files = [name for name in kmz.namelist() if name.endswith('.kml')]
            if not kml_files:
                raise ValueError("No KML file found in KMZ archive")
            
            # Read the first KML file
            with kmz.open(kml_files[0]) as kml_file:
                kml_content = kml_file.read()
                return gpd.read_file(io.BytesIO(kml_content), driver='KML')
    
    def read_netcdf(self, remote_path: str):
        """
        Read NetCDF file from Nextcloud
        
        Args:
            remote_path: Path to .nc file on Nextcloud
            
        Returns:
            xarray Dataset
            
        Note: Requires xarray and netcdf4: pip install xarray netcdf4
        """
        try:
            import xarray as xr
        except ImportError:
            raise ImportError("xarray is required to read NetCDF. Install with: pip install xarray netcdf4")
        
        content = self.read_file_content(remote_path)
        return xr.open_dataset(io.BytesIO(content))
    
    def read_zarr(self, remote_path: str):
        """
        Read Zarr file from Nextcloud
        
        Args:
            remote_path: Path to .zarr file on Nextcloud
            
        Returns:
            zarr array or group
            
        Note: Requires zarr: pip install zarr
        Note: For Zarr directories, this might need special handling
        """
        try:
            import zarr
        except ImportError:
            raise ImportError("zarr is required to read Zarr files. Install with: pip install zarr")
        
        content = self.read_file_content(remote_path)
        return zarr.load(io.BytesIO(content))
    
    def read_image(self, remote_path: str):
        """
        Read image file from Nextcloud
        
        Args:
            remote_path: Path to image file on Nextcloud
            
        Returns:
            PIL Image object
        """
        from PIL import Image
        content = self.read_file_content(remote_path)
        return Image.open(io.BytesIO(content))
    
    def read_yaml(self, remote_path: str):
        """
        Read YAML file from Nextcloud
        
        Args:
            remote_path: Path to .yaml or .yml file on Nextcloud
            
        Returns:
            Python dict/list
            
        Note: Requires pyyaml: pip install pyyaml
        """
        try:
            import yaml
        except ImportError:
            raise ImportError("pyyaml is required to read YAML files. Install with: pip install pyyaml")
        
        content = self.read_text_file(remote_path)
        return yaml.safe_load(content)
    
    def read_xml(self, remote_path: str):
        """
        Read XML file from Nextcloud
        
        Args:
            remote_path: Path to .xml file on Nextcloud
            
        Returns:
            ElementTree root element
        """
        from xml.etree import ElementTree as ET
        content = self.read_text_file(remote_path)
        return ET.fromstring(content)
    
    def read_zip(self, remote_path: str):
        """
        Read ZIP archive from Nextcloud
        
        Args:
            remote_path: Path to .zip file on Nextcloud
            
        Returns:
            zipfile.ZipFile object (use in context manager)
            
        Example:
            with nc.read_zip('/path/to/archive.zip') as zf:
                file_list = zf.namelist()
                content = zf.read('file_inside.txt')
        """
        import zipfile
        content = self.read_file_content(remote_path)
        return zipfile.ZipFile(io.BytesIO(content))
    
    def read_tar(self, remote_path: str, mode: str = 'r'):
        """
        Read TAR archive from Nextcloud
        
        Args:
            remote_path: Path to .tar, .tar.gz, .tgz file on Nextcloud
            mode: Mode string ('r', 'r:gz', 'r:bz2', 'r:xz')
            
        Returns:
            tarfile.TarFile object
            
        Example:
            with nc.read_tar('/path/to/archive.tar.gz', 'r:gz') as tf:
                members = tf.getmembers()
                content = tf.extractfile('file.txt').read()
        """
        import tarfile
        content = self.read_file_content(remote_path)
        return tarfile.open(fileobj=io.BytesIO(content), mode=mode)
    
    def read_auto(self, remote_path: str, **kwargs) -> Any:
        """
        Automatically detect file type and read accordingly
        
        Args:
            remote_path: Path to file on Nextcloud
            **kwargs: Additional arguments passed to the specific reader
            
        Returns:
            Appropriate Python object based on file extension
        """
        ext = Path(remote_path).suffix.lower()
        
        readers = {
            '.csv': self.read_csv,
            '.xlsx': self.read_excel,
            '.xls': self.read_excel,
            '.json': self.read_json,
            '.txt': self.read_text_file,
            '.npy': self.read_numpy,
            '.npz': self.read_npz,
            '.pkl': self.read_pickle,
            '.pickle': self.read_pickle,
            '.parquet': self.read_parquet,
            '.feather': self.read_feather,
            '.las': self.read_las,
            '.laz': self.read_laz,
            '.shp': self.read_shapefile,
            '.dbf': self.read_dbf,
            '.prj': self.read_prj,
            '.shx': self.read_shx,
            '.geojson': self.read_geojson,
            '.kml': self.read_kml,
            '.kmz': self.read_kmz,
            '.nc': self.read_netcdf,
            '.hdf5': self.read_hdf5,
            '.h5': self.read_hdf5,
            '.hd5': self.read_hdf5,
            '.yaml': self.read_yaml,
            '.yml': self.read_yaml,
            '.xml': self.read_xml,
            '.png': self.read_image,
            '.jpg': self.read_image,
            '.jpeg': self.read_image,
            '.gif': self.read_image,
            '.bmp': self.read_image,
            '.tiff': self.read_image,
            '.tif': self.read_image,
            '.zip': self.read_zip,
        }
        
        if ext in readers:
            return readers[ext](remote_path, **kwargs)
        else:
            print(f"Warning: No specific reader for {ext}, returning raw bytes")
            return self.read_file_content(remote_path)
    
    def list_folder(self, remote_path: str = '/') -> List[dict]:
        """
        List contents of a folder on Nextcloud
        
        Args:
            remote_path: Path to folder on Nextcloud
            
        Returns:
            List of dictionaries with file/folder information
        """
        from xml.etree import ElementTree as ET
        
        remote_path = remote_path.lstrip('/')
        url = f"{self.webdav_url}/{remote_path}"
        
        headers = {
            'Depth': '1'
        }
        
        try:
            response = requests.request('PROPFIND', url, auth=self.auth, headers=headers)
            response.raise_for_status()
            
            # Parse XML response
            root = ET.fromstring(response.content)
            
            items = []
            for response_elem in root.findall('{DAV:}response'):
                href = response_elem.find('{DAV:}href').text
                
                # Skip the parent folder
                if href.endswith(f'/{remote_path}/') or href.endswith(f'/{remote_path}'):
                    continue
                
                # Get properties
                propstat = response_elem.find('{DAV:}propstat')
                prop = propstat.find('{DAV:}prop')
                
                # Determine if it's a collection (folder)
                resourcetype = prop.find('{DAV:}resourcetype')
                is_folder = resourcetype.find('{DAV:}collection') is not None
                
                # Get file size (if it's a file)
                size_elem = prop.find('{DAV:}getcontentlength')
                size = int(size_elem.text) if size_elem is not None and size_elem.text else 0
                
                # Extract filename from href
                filename = href.rstrip('/').split('/')[-1]
                
                items.append({
                    'name': filename,
                    'path': href,
                    'is_folder': is_folder,
                    'size': size
                })
            
            return items
            
        except Exception as e:
            raise Exception(f"Failed to list folder {remote_path}: {e}")
    
    def file_exists(self, remote_path: str) -> bool:
        """
        Check if a file exists on Nextcloud
        
        Args:
            remote_path: Path to file on Nextcloud
            
        Returns:
            True if file exists, False otherwise
        """
        remote_path = remote_path.lstrip('/')
        url = f"{self.webdav_url}/{remote_path}"
        
        try:
            response = requests.head(url, auth=self.auth)
            return response.status_code == 200
        except:
            return False
    
    def download_file(self, remote_path: str, local_path: str, create_dirs: bool = True) -> bool:
        """
        Download a file from Nextcloud to local disk
        
        Args:
            remote_path: Path to file on Nextcloud (e.g., '/inputs/data.csv')
            local_path: Path to save file locally (e.g., './downloads/data.csv')
            create_dirs: If True, create parent directories if they don't exist
            
        Returns:
            True if successful, False otherwise
            
        Example:
            nc.download_file('/inputs/data.csv', './local/data.csv')
        """
        try:
            # Get file content
            content = self.read_file_content(remote_path)
            
            # Create parent directories if needed
            if create_dirs:
                local_path_obj = Path(local_path)
                local_path_obj.parent.mkdir(parents=True, exist_ok=True)
            
            # Write to local file
            with open(local_path, 'wb') as f:
                f.write(content)
            
            print(f"✓ Downloaded: {remote_path} -> {local_path}")
            return True
            
        except Exception as e:
            print(f"✗ Failed to download {remote_path}: {e}")
            return False
    
    def download_folder(self, remote_path: str, local_path: str, recursive: bool = True) -> bool:
        """
        Download an entire folder from Nextcloud to local disk
        
        Args:
            remote_path: Path to folder on Nextcloud (e.g., '/inputs/folder1')
            local_path: Path to save folder locally (e.g., './downloads')
            recursive: If True, download all subfolders and files recursively
            
        Returns:
            True if successful, False otherwise
            
        Example:
            # Downloads /inputs/folder1 to ./downloads/folder1
            nc.download_folder('/inputs/folder1', './downloads')
        """
        try:
            remote_path = remote_path.rstrip('/')
            folder_name = Path(remote_path).name
            
            # Create local base directory
            local_base = Path(local_path) / folder_name
            local_base.mkdir(parents=True, exist_ok=True)
            
            # List contents of remote folder
            items = self.list_folder(remote_path)
            
            for item in items:
                item_name = item['name']
                remote_item_path = f"{remote_path}/{item_name}"
                local_item_path = local_base / item_name
                
                if item['is_folder']:
                    if recursive:
                        # Recursively download subfolder
                        self._download_folder_recursive(remote_item_path, local_item_path)
                    else:
                        # Just create the directory
                        local_item_path.mkdir(parents=True, exist_ok=True)
                        print(f"✓ Created directory: {local_item_path}")
                else:
                    # Download file
                    self.download_file(remote_item_path, str(local_item_path), create_dirs=False)
            
            print(f"\n✓ Folder download complete: {remote_path} -> {local_base}")
            return True
            
        except Exception as e:
            print(f"✗ Failed to download folder {remote_path}: {e}")
            return False
    
    def _download_folder_recursive(self, remote_path: str, local_path: Path) -> None:
        """
        Helper method for recursive folder download
        
        Args:
            remote_path: Remote folder path
            local_path: Local folder path (Path object)
        """
        # Create local directory
        local_path.mkdir(parents=True, exist_ok=True)
        
        # List contents
        items = self.list_folder(remote_path)
        
        for item in items:
            item_name = item['name']
            remote_item_path = f"{remote_path}/{item_name}"
            local_item_path = local_path / item_name
            
            if item['is_folder']:
                # Recursively download subfolder
                self._download_folder_recursive(remote_item_path, local_item_path)
            else:
                # Download file
                self.download_file(remote_item_path, str(local_item_path), create_dirs=False)
    
    def download_files(self, remote_files: List[str], local_dir: str, 
                      preserve_structure: bool = False) -> dict:
        """
        Download multiple files from Nextcloud
        
        Args:
            remote_files: List of remote file paths
            local_dir: Local directory to save files
            preserve_structure: If True, preserve folder structure from Nextcloud
            
        Returns:
            Dictionary with results: {'successful': [...], 'failed': [...]}
            
        Example:
            files = ['/inputs/data1.csv', '/inputs/data2.csv', '/reports/summary.pdf']
            results = nc.download_files(files, './downloads')
        """
        results = {'successful': [], 'failed': []}
        
        # Create local directory
        Path(local_dir).mkdir(parents=True, exist_ok=True)
        
        for remote_file in remote_files:
            try:
                if preserve_structure:
                    # Keep the folder structure
                    # /inputs/folder/file.csv -> local_dir/inputs/folder/file.csv
                    relative_path = remote_file.lstrip('/')
                    local_file = Path(local_dir) / relative_path
                else:
                    # Flatten - just filename
                    # /inputs/folder/file.csv -> local_dir/file.csv
                    filename = Path(remote_file).name
                    local_file = Path(local_dir) / filename
                
                if self.download_file(remote_file, str(local_file)):
                    results['successful'].append(remote_file)
                else:
                    results['failed'].append(remote_file)
                    
            except Exception as e:
                print(f"✗ Error downloading {remote_file}: {e}")
                results['failed'].append(remote_file)
        
        print(f"\n✓ Download complete: {len(results['successful'])} successful, {len(results['failed'])} failed")
        return results
    
    def move_file(self, source_path: str, destination_path: str, overwrite: bool = False) -> bool:
        """
        Move a file from one location to another on Nextcloud
        
        Args:
            source_path: Current path of file on Nextcloud (e.g., '/inputs/old.csv')
            destination_path: New path for file on Nextcloud (e.g., '/archive/old.csv')
            overwrite: If True, overwrite destination if it exists
            
        Returns:
            True if successful, False otherwise
            
        Example:
            nc.move_file('/inputs/data.csv', '/archive/data.csv')
        """
        source_path = source_path.lstrip('/')
        destination_path = destination_path.lstrip('/')
        
        source_url = f"{self.webdav_url}/{source_path}"
        dest_url = f"{self.webdav_url}/{destination_path}"
        
        headers = {
            'Destination': dest_url,
            'Overwrite': 'T' if overwrite else 'F'
        }
        
        try:
            response = requests.request('MOVE', source_url, auth=self.auth, headers=headers)
            
            if response.status_code in [201, 204]:  # 201 = created, 204 = no content (moved)
                print(f"✓ Moved: {source_path} -> {destination_path}")
                return True
            elif response.status_code == 412:  # Precondition failed (destination exists)
                print(f"✗ Failed to move: Destination exists (use overwrite=True to replace)")
                return False
            else:
                print(f"✗ Failed to move {source_path}: HTTP {response.status_code}")
                return False
                
        except Exception as e:
            print(f"✗ Error moving {source_path}: {e}")
            return False
    
    def move_folder(self, source_path: str, destination_path: str, overwrite: bool = False) -> bool:
        """
        Move a folder from one location to another on Nextcloud
        
        Args:
            source_path: Current path of folder on Nextcloud (e.g., '/inputs/old_folder')
            destination_path: New path for folder on Nextcloud (e.g., '/archive/old_folder')
            overwrite: If True, overwrite destination if it exists
            
        Returns:
            True if successful, False otherwise
            
        Example:
            nc.move_folder('/inputs/project1', '/archive/project1')
        """
        # Same as move_file - WebDAV MOVE works for both files and folders
        return self.move_file(source_path, destination_path, overwrite)
    
    def rename_file(self, file_path: str, new_name: str) -> bool:
        """
        Rename a file on Nextcloud
        
        Args:
            file_path: Current path of file on Nextcloud (e.g., '/inputs/old_name.csv')
            new_name: New filename (just the name, not full path) (e.g., 'new_name.csv')
            
        Returns:
            True if successful, False otherwise
            
        Example:
            nc.rename_file('/inputs/data.csv', 'data_backup.csv')
        """
        # Get the parent directory
        parent_dir = str(Path(file_path).parent)
        if parent_dir == '.':
            parent_dir = '/'
        
        # Create new path
        new_path = f"{parent_dir}/{new_name}".replace('//', '/')
        
        return self.move_file(file_path, new_path)
    
    def rename_folder(self, folder_path: str, new_name: str) -> bool:
        """
        Rename a folder on Nextcloud
        
        Args:
            folder_path: Current path of folder on Nextcloud (e.g., '/inputs/old_folder')
            new_name: New folder name (just the name, not full path) (e.g., 'new_folder')
            
        Returns:
            True if successful, False otherwise
            
        Example:
            nc.rename_folder('/inputs/project1', 'project1_archived')
        """
        return self.rename_file(folder_path, new_name)
    
    
    def move_files(self, file_moves: List[tuple], overwrite: bool = False) -> dict:
        """
        Move multiple files on Nextcloud
        
        Args:
            file_moves: List of tuples (source_path, destination_path)
            overwrite: If True, overwrite destinations if they exist
            
        Returns:
            Dictionary with results: {'successful': [...], 'failed': [...]}
            
        Example:
            moves = [
                ('/inputs/old1.csv', '/archive/old1.csv'),
                ('/inputs/old2.csv', '/archive/old2.csv'),
            ]
            results = nc.move_files(moves)
        """
        results = {'successful': [], 'failed': []}
        
        for source, destination in file_moves:
            if self.move_file(source, destination, overwrite):
                results['successful'].append((source, destination))
            else:
                results['failed'].append((source, destination))
        
        print(f"\n✓ Move complete: {len(results['successful'])} successful, {len(results['failed'])} failed")
        return results


# Example usage
if __name__ == "__main__":
    # Configuration
    NEXTCLOUD_URL = "https://your-nextcloud-server.com"
    USERNAME = "your_username"
    APP_PASSWORD = "your_app_password"
    
    # Initialize reader
    nc = NextcloudReader(NEXTCLOUD_URL, USERNAME, APP_PASSWORD)
    
    # Example 1: Read LAS file (LiDAR point cloud)
    # las_data = nc.read_las('/inputs/pointclouds/scan.las')
    # print(f"Point count: {len(las_data.points)}")
    # print(f"Point format: {las_data.point_format}")
    # xyz = np.vstack([las_data.x, las_data.y, las_data.z]).T
    
    # Example 2: Read LAZ file (compressed LiDAR)
    # laz_data = nc.read_laz('/inputs/pointclouds/scan.laz')
    
    # Example 3: Read NPZ file (compressed numpy archive)
    # npz_data = nc.read_npz('/inputs/arrays/data.npz')
    # array1 = npz_data['array1']
    # array2 = npz_data['array2']
    # npz_data.close()
    
    # Example 4: Auto-detect file type and read
    # data = nc.read_auto('/inputs/folder1/unknown_file.csv')
    
    # Example 5: Read Parquet file
    # df = nc.read_parquet('/inputs/data/large_dataset.parquet')
    
    # Example 6: Read GeoJSON
    # gdf = nc.read_geojson('/inputs/maps/boundaries.geojson')
    
    # Example 7: Read KML file (Google Earth)
    # kml_data = nc.read_kml('/inputs/maps/locations.kml')
    
    # Example 8: Read KMZ file (compressed KML)
    # kmz_data = nc.read_kmz('/inputs/maps/tour.kmz')
    
    # Example 9: Read HDF5 file
    # with nc.read_hdf5('/inputs/data/measurements.hdf5') as h5f:
    #     dataset = h5f['temperature'][:]
    
    # Example 10: Read DBF file (shapefile attributes)
    # df_dbf = nc.read_dbf('/inputs/gis/attributes.dbf')
    
    # Example 11: Read PRJ file (projection info)
    # projection = nc.read_prj('/inputs/gis/data.prj')
    # print(f"Projection: {projection}")
    
    # Example 12: Download a single file to local disk
    # nc.download_file('/inputs/data.csv', './downloads/data.csv')
    
    # Example 13: Download entire folder to local disk
    # nc.download_folder('/inputs/folder1', './downloads')
    
    # Example 14: Download multiple files
    # files_to_download = [
    #     '/inputs/data1.csv',
    #     '/inputs/data2.csv',
    #     '/reports/summary.pdf'
    # ]
    # results = nc.download_files(files_to_download, './downloads')
    # print(f"Downloaded: {len(results['successful'])} files")
    
    # Example 15: Move a file
    # nc.move_file('/inputs/old_data.csv', '/archive/old_data.csv')
    
    # Example 16: Rename a file
    # nc.rename_file('/inputs/data.csv', 'data_backup.csv')
    
    # Example 17: Move a folder
    # nc.move_folder('/inputs/project1', '/archive/project1')
    
    # Example 18: Move multiple files
    # moves = [
    #     ('/inputs/old1.csv', '/archive/old1.csv'),
    #     ('/inputs/old2.csv', '/archive/old2.csv'),
    # ]
    # results = nc.move_files(moves)
    
