"""Virtual Workspace Backend - Symlink-free workspace using metadata mapping."""

import logging
import json
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Union
from fnmatch import fnmatch

from openhcs.io.disk import DiskStorageBackend
from openhcs.io.metadata_writer import get_metadata_path
from openhcs.io.exceptions import StorageResolutionError
from openhcs.io.base import ReadOnlyBackend

logger = logging.getLogger(__name__)


class VirtualWorkspaceBackend(ReadOnlyBackend):
    """
    Read-only path translation layer for virtual workspace.

    Maps virtual filenames to real plate files using workspace_mapping from
    openhcs_metadata.json (plate-relative paths), then delegates I/O to DiskStorageBackend.

    This is NOT a storage backend - it's a path resolver. It does not support save operations.

    Follows OMERO backend pattern:
    - Explicit initialization with plate_root
    - Fail-loud path resolution
    - No path inspection or 'workspace' searching

    Uses PLATE-RELATIVE paths (no workspace directory):
    - Mapping: {"Images/r01c01f05.tif": "Images/r01c01f01.tif"}
    - Resolution: plate_root / "Images/r01c01f05.tif" → plate_root / "Images/r01c01f01.tif"

    Example:
        backend = VirtualWorkspaceBackend(plate_root=Path("/data/plate"))
        # Input: plate_root / "Images/r01c01f05.tif" (doesn't exist)
        # Resolves to: plate_root / "Images/r01c01f01.tif" (exists)
    """
    
    _backend_type = 'virtual_workspace'  # Auto-registers via metaclass
    
    def __init__(self, plate_root: Path):
        """
        Initialize with explicit plate root.

        Args:
            plate_root: Path to plate directory containing openhcs_metadata.json

        Raises:
            FileNotFoundError: If metadata file doesn't exist
            ValueError: If no workspace_mapping in metadata
        """
        self.plate_root = Path(plate_root)
        self.disk_backend = DiskStorageBackend()
        self._mapping_cache: Optional[Dict[str, str]] = None
        self._cache_mtime: Optional[float] = None

        # Load mapping eagerly - fail loud if metadata missing
        self._load_mapping()

    @staticmethod
    def _normalize_relative_path(path_str: str) -> str:
        """
        Normalize relative path for internal mapping lookups.

        Converts Windows backslashes to forward slashes and normalizes
        '.' (current directory) to empty string for root directory.

        Args:
            path_str: Relative path string to normalize

        Returns:
            Normalized path string with forward slashes, empty string for root
        """
        normalized = path_str.replace('\\', '/')
        return '' if normalized == '.' else normalized
    
    def _load_mapping(self) -> Dict[str, str]:
        """
        Load workspace_mapping from metadata with mtime-based caching.
        
        Returns:
            Combined mapping from all subdirectories
            
        Raises:
            FileNotFoundError: If metadata file doesn't exist
            ValueError: If no workspace_mapping in metadata
        """
        metadata_path = get_metadata_path(self.plate_root)
        if not metadata_path.exists():
            raise FileNotFoundError(
                f"Metadata not found: {metadata_path}\n"
                f"Plate root: {self.plate_root}"
            )
        
        # Check cache with mtime invalidation
        current_mtime = metadata_path.stat().st_mtime
        if self._mapping_cache is not None and self._cache_mtime == current_mtime:
            return self._mapping_cache
        
        # Load and combine mappings from all subdirectories
        with open(metadata_path, 'r') as f:
            metadata = json.load(f)
        
        combined_mapping = {}
        for subdir_data in metadata.get('subdirectories', {}).values():
            workspace_mapping = subdir_data.get('workspace_mapping', {})
            combined_mapping.update(workspace_mapping)
        
        if not combined_mapping:
            raise ValueError(
                f"No workspace_mapping in {metadata_path}\n"
                f"Plate root: {self.plate_root}\n"
                f"This is not a virtual workspace."
            )
        
        # Cache it
        self._mapping_cache = combined_mapping
        self._cache_mtime = current_mtime
        
        logger.info(f"Loaded {len(combined_mapping)} mappings for {self.plate_root}")
        return combined_mapping
    
    def _resolve_path(self, path: Union[str, Path]) -> str:
        """
        Resolve virtual path to real plate path using plate-relative mapping.

        Pure mapping-based resolution - no physical path fallbacks.
        Follows OMERO backend pattern: all paths go through mapping.

        Args:
            path: Absolute or relative path (e.g., "/data/plate/Images/r01c01f05.tif" or "Images/r01c01f05.tif")

        Returns:
            Real absolute path: e.g., "/data/plate/Images/r01c01f01.tif"

        Raises:
            StorageResolutionError: If path not in mapping
        """
        path_obj = Path(path)

        # Convert to plate-relative path
        try:
            relative_path = path_obj.relative_to(self.plate_root)
        except ValueError:
            # Already relative or different root
            relative_path = path_obj

        # Normalize Windows backslashes to forward slashes
        relative_str = str(relative_path).replace('\\', '/')

        # Load mapping if not cached
        if self._mapping_cache is None:
            self._load_mapping()

        # Resolve via mapping - fail loud if not in mapping
        if relative_str not in self._mapping_cache:
            raise StorageResolutionError(
                f"Path not in virtual workspace mapping: {relative_str}\n"
                f"Plate root: {self.plate_root}\n"
                f"Available virtual paths: {len(self._mapping_cache)}\n"
                f"This path must be accessed through the virtual workspace mapping."
            )

        real_relative = self._mapping_cache[relative_str]
        real_absolute = self.plate_root / real_relative
        logger.debug(f"Resolved virtual → real: {relative_str} → {real_relative}")
        return str(real_absolute)
    
    def load(self, file_path: Union[str, Path], **kwargs) -> Any:
        """Load file from virtual workspace."""
        real_path = self._resolve_path(file_path)
        return self.disk_backend.load(real_path, **kwargs)
    
    def load_batch(self, file_paths: List[Union[str, Path]], **kwargs) -> List[Any]:
        """Load multiple files from virtual workspace."""
        real_paths = [self._resolve_path(fp) for fp in file_paths]
        return self.disk_backend.load_batch(real_paths, **kwargs)
    
    def list_files(self, directory: Union[str, Path], pattern: Optional[str] = None,
                  extensions: Optional[Set[str]] = None, recursive: bool = False,
                  **kwargs) -> List[str]:
        """
        List files in directory (returns absolute paths of virtual files).

        Returns absolute virtual paths from mapping that match the directory.

        Raises:
            ValueError: If mapping not loaded
        """
        dir_path = Path(directory)

        # Convert to plate-relative
        try:
            relative_dir = dir_path.relative_to(self.plate_root)
        except ValueError:
            # Already relative
            relative_dir = dir_path

        # Normalize to forward slashes for comparison with JSON mapping
        relative_dir_str = self._normalize_relative_path(str(relative_dir))

        # Load mapping - fail loud if missing
        if self._mapping_cache is None:
            self._load_mapping()

        logger.info(f"VirtualWorkspace.list_files called: directory={directory}, recursive={recursive}, pattern={pattern}, extensions={extensions}")
        logger.info(f"  plate_root={self.plate_root}")
        logger.info(f"  relative_dir_str='{relative_dir_str}'")
        logger.info(f"  mapping has {len(self._mapping_cache)} entries")

        # Filter paths in this directory
        results = []
        for virtual_relative in self._mapping_cache.keys():
            # Check directory match using string comparison with forward slashes
            if recursive:
                # For recursive, check if virtual_relative starts with directory prefix
                if relative_dir_str:
                    if not virtual_relative.startswith(relative_dir_str + '/') and virtual_relative != relative_dir_str:
                        continue
                # else: relative_dir_str is empty (root), include all files
            else:
                # For non-recursive, check if parent directory matches
                vpath_parent = self._normalize_relative_path(str(Path(virtual_relative).parent))
                if vpath_parent != relative_dir_str:
                    continue

            # Apply filters
            vpath = Path(virtual_relative)
            if pattern and not fnmatch(vpath.name, pattern):
                continue
            if extensions and vpath.suffix not in extensions:
                continue

            # Return absolute path
            results.append(str(self.plate_root / virtual_relative))

        logger.info(f"  VirtualWorkspace.list_files returning {len(results)} files")
        if len(results) == 0 and len(self._mapping_cache) > 0:
            # Log first few mapping keys to help debug
            sample_keys = list(self._mapping_cache.keys())[:3]
            logger.info(f"  Sample mapping keys: {sample_keys}")
            if not recursive and relative_dir_str == '':
                sample_parents = [str(Path(k).parent).replace('\\', '/') for k in sample_keys]
                logger.info(f"  Sample parent dirs: {sample_parents}")
                logger.info(f"  Expected parent to match: '{relative_dir_str}'")

        return sorted(results)

    def list_dir(self, path: Union[str, Path]) -> List[str]:
        """
        List directory entries (names only, not full paths).

        For virtual workspace, this returns the unique directory names
        that exist in the mapping under the given path.
        """
        path = Path(path)

        # Convert to plate-relative path
        if path.is_absolute():
            try:
                relative_path = path.relative_to(self.plate_root)
            except ValueError:
                # Path is not under plate_root
                raise FileNotFoundError(f"Path not under plate root: {path}")
        else:
            relative_path = path

        # Normalize to string with forward slashes
        relative_str = self._normalize_relative_path(str(relative_path))

        # Collect all unique directory/file names under this path
        entries = set()
        for virtual_relative in self._mapping_cache.keys():
            # Check if this virtual path is under the requested directory
            if relative_str:
                # Looking for children of a subdirectory
                if not virtual_relative.startswith(relative_str + '/'):
                    continue
                # Get the part after the directory prefix
                remainder = virtual_relative[len(relative_str) + 1:]
            else:
                # Looking for top-level entries
                remainder = virtual_relative

            # Get the first component (immediate child)
            first_component = remainder.split('/')[0] if '/' in remainder else remainder
            if first_component:
                entries.add(first_component)

        return sorted(entries)

    def exists(self, path: Union[str, Path]) -> bool:
        """Check if virtual path exists (file in mapping or directory containing files)."""
        if self._mapping_cache is None:
            self._load_mapping()

        try:
            relative_str = str(Path(path).relative_to(self.plate_root))
        except ValueError:
            relative_str = str(path)

        # Normalize Windows backslashes to forward slashes and '.' to ''
        relative_str = self._normalize_relative_path(relative_str)

        # File in mapping or directory prefix
        # For root directory (empty string), check if mapping has any files
        if relative_str == '':
            return len(self._mapping_cache) > 0

        return (relative_str in self._mapping_cache or
                any(vp.startswith(relative_str + '/') for vp in self._mapping_cache))
    
    def is_file(self, path: Union[str, Path]) -> bool:
        """Check if virtual path is a file (exists in mapping directly)."""
        if self._mapping_cache is None:
            self._load_mapping()

        try:
            relative_str = str(Path(path).relative_to(self.plate_root))
        except ValueError:
            relative_str = str(path)

        # Normalize Windows backslashes to forward slashes
        relative_str = relative_str.replace('\\', '/')

        # File if it's directly in the mapping
        return relative_str in self._mapping_cache

    def is_dir(self, path: Union[str, Path]) -> bool:
        """Check if virtual path is a directory (has files under it)."""
        if self._mapping_cache is None:
            self._load_mapping()

        try:
            relative_str = str(Path(path).relative_to(self.plate_root))
        except ValueError:
            relative_str = str(path)

        # Normalize to string with forward slashes and '.' to ''
        relative_str = self._normalize_relative_path(relative_str)

        # Directory if any virtual path starts with this prefix
        if relative_str:
            return any(vp.startswith(relative_str + '/') for vp in self._mapping_cache)
        else:
            # Root is always a directory if mapping exists
            return len(self._mapping_cache) > 0

