from pathlib import Path
from typing import Any
from .helpers import config, sanitize_filename, validate_path_security
import json
import re


class InvocationLoader:
    """Handles loading and management of custom invocation files.

        Provides secure loading of invocation text files with support for
        multiple formats and comprehensive search order prioritization.

        Search Order:
            1. ./name (exact match)
            2. ./name.txt
            3. ./name.md
            4. ~/.config/tarot-oracle/invocations/name
            5. ~/.config/tarot-oracle/invocations/name.txt
            6. ~/.config/tarot-oracle/invocations/name.md
    """

    def load_invocation(self, name: str) -> str | None:
        """Load invocation text by name using search order with security
            validation:

            Search order:
                1. ./{name}
                2. ./{name}.txt
                3. ./{name}.md
                4. ~/.config/tarot-oracle/invocations/{name}
                5. ~/.config/tarot-oracle/invocations/{name}.txt
                6. ~/.config/tarot-oracle/invocations/{name}.md
         """
        conf = config()
        safe_name = sanitize_filename(name)
        if safe_name is None:
            return None

        search_paths = [
            Path.cwd() / safe_name,
            Path.cwd() / f"{safe_name}.txt",
            Path.cwd() / f"{safe_name}.md",
            Path(conf.path("invocations")) / safe_name,
            Path(conf.path("invocations")) / f"{safe_name}.txt",
            Path(conf.path("invocations")) / f"{safe_name}.md"
        ]

        for path in search_paths:
            if path.exists() and path.is_file():
                resolved = path.resolve()
                validate_path_security(resolved, str(path))
                try:
                    with open(resolved, 'r', encoding='utf-8') as f:
                        return f.read().strip()
                except (OSError, UnicodeDecodeError):
                    continue
        return None

    def list_invocations(self) -> list[dict[str, str]]:
        """Scan ~/.config/tarot-oracle/invocations/ and return invocation metadata
            containing filename and preview.
        """
        conf = config()
        invocations_dir = Path(conf.path("invocations"))
        if not invocations_dir.exists() or not invocations_dir.is_dir():
            return []

        invocations = []

        # Find all .txt and .md files in the invocations directory
        for text_file in invocations_dir.glob("*"):
            if text_file.suffix in ['.txt', '.md'] and text_file.is_file():
                try:
                    with open(text_file, 'r', encoding='utf-8') as f:
                        preview = f.read(100).strip()
                        if preview:
                            preview = preview.replace('\n', ' ')[:97] + '...' \
                                if len(preview) > 100 else preview
                        else:
                            preview = "Empty invocation file"

                    invocations.append({
                        "filename": text_file.name,
                        "name": text_file.stem,
                        "preview": preview
                    })
                except (OSError, UnicodeDecodeError):
                    # Skip files that can't be read
                    continue

        return invocations

    def save_invocation(self, source_path: str) -> str:
        """Save invocation from CWD to config directory. Validates readable
            text, prompts for overwrite confirmation if exists. Returns saved
            path.
        """
        conf = config()
        source = Path(source_path).resolve()

        if not source.exists():
            raise ValueError(f"Source file not found: {source_path}")

        # Validate extension
        if source.suffix not in ['.txt', '.md', '']:
            raise ValueError("Invocation must be .txt or .md file")

        # Read and validate content
        try:
            with open(source, 'r', encoding='utf-8') as f:
                content = f.read().strip()
            if not content:
                raise ValueError("Invocation file is empty")
        except (OSError, UnicodeDecodeError) as e:
            raise ValueError(f"Failed to read invocation: {e}")

        # Use filename stem as invocation name
        safe_name = sanitize_filename(source.stem)
        if safe_name is None:
            raise ValueError("Invalid invocation name")

        invocations_dir = Path(conf.path("invocations"))
        invocations_dir.mkdir(parents=True, exist_ok=True)

        # Enforce .txt extension
        target_path = invocations_dir / f"{safe_name}.txt"

        # Check for existing file
        if target_path.exists():
            print(
                f"Invocation '{safe_name}.txt' already exists in config directory."
            )
            response = input("Overwrite? (y/N): ").strip().lower()
            if response != 'y':
                print("Save cancelled.")
                raise KeyboardInterrupt()

        # Write content
        try:
            with open(target_path, 'w', encoding='utf-8') as f:
                f.write(content)
            return str(target_path)
        except OSError as e:
            raise OSError(f"Failed to save invocation: {e}")


class SpreadLoader:
    """Handles loading and management of custom spread configurations.

        Provides secure loading of custom spread configurations in JSON
        format with comprehensive validation, search order prioritization,
        and semantic analysis support. Supports variable placeholder
        syntax and guidance rule systems.

        Search Order:
            1. ./{name}.json
            2. ~/.config/tarot-oracle/spreads/{name}.json
    """

    def load_spread(self, name: str) -> dict[str, Any] | None:
        """Load spread configuration by name using search order with security
            validation from current directory and ~/.config/tarot-oracle/spreads/.
        """
        conf = config()
        safe_name = sanitize_filename(name)
        if safe_name is None:
            return None

        search_paths = [
            Path.cwd() / f"{safe_name}.json",
            Path(conf.path("spreads")) / f"{safe_name}.json"
        ]

        for path in search_paths:
            if path.exists() and path.is_file():
                resolved = path.resolve()
                validate_path_security(resolved, str(path))
                try:
                    with open(resolved, 'r', encoding='utf-8') as f:
                        spread_config = json.load(f)
                    return self._validate_spread_config(spread_config, str(path))
                except (OSError, json.JSONDecodeError, ValueError):
                    continue
        return None

    def _validate_spread_config(
            self, spread_config: dict[str, Any], path: str
        ) -> dict[str, Any]:
        """Validate spread configuration structure and content, returning validated
            config or raising ValueError if invalid.
        """
        # Validate required fields
        if 'name' not in spread_config:
            spread_name = spread_config.get('name', 'unknown')
            raise ValueError(
                f"Spread configuration must include 'name' field (spread: "
                f"{spread_name})"
            )

        if 'layout' not in spread_config:
            spread_name = spread_config.get('name', 'unknown')
            raise ValueError(
                f"Spread configuration must include 'layout' field (spread: "
                f"{spread_name})"
            )

        # Validate layout - support both matrix format and position dictionary format
        layout = spread_config['layout']
        if not isinstance(layout, list):
            spread_name = spread_config.get('name', 'unknown')
            raise ValueError(f"Spread 'layout' must be a list (spread: {spread_name})")

        # Check if this is a matrix layout (nested lists of integers)
        if layout and isinstance(layout[0], list):
            # Matrix layout format - validate that it contains integers or 0
            for i, row in enumerate(layout):
                if not isinstance(row, list):
                    spread_name = spread_config.get('name', 'unknown')
                    raise ValueError(
                        f"Layout row {i} must be a list (spread: {spread_name})"
                    )
                for j, cell in enumerate(row):
                    if not isinstance(cell, int):
                        spread_name = spread_config.get('name', 'unknown')
                        raise ValueError(
                            f"Layout cell [{i}][{j}] must be an integer (spread: "
                            f"{spread_name})"
                        )
        else:
            # Position dictionary format - traditional validation
            for i, position in enumerate(layout):
                if not isinstance(position, dict):
                    spread_name = spread_config.get('name', 'unknown')
                    raise ValueError(
                        f"Layout position {i} must be a dictionary (spread: "
                        f"{spread_name})"
                    )
                if 'position' not in position:
                    spread_name = spread_config.get('name', 'unknown')
                    raise ValueError(
                        f"Layout position {i} must include 'position' field "
                        f"(spread: {spread_name})"
                    )

        # Validate semantic groups if present
        if 'semantic_groups' in spread_config:
            semantic_groups = spread_config['semantic_groups']
            if not isinstance(semantic_groups, dict):
                spread_name = spread_config.get('name', 'unknown')
                raise ValueError(
                    f"semantic_groups must be a dictionary (spread: {spread_name})"
                )

        # Validate semantics matrix if present
        if 'semantics' in spread_config:
            semantics = spread_config['semantics']
            if not isinstance(semantics, list):
                spread_name = spread_config.get('name', 'unknown')
                raise ValueError(f"semantics must be a list (spread: {spread_name})")

            # Check if this is a matrix format (nested lists)
            if semantics and isinstance(semantics[0], list):
                # Matrix format - validate that it contains strings or empty values
                for i, row in enumerate(semantics):
                    if not isinstance(row, list):
                        spread_name = spread_config.get('name', 'unknown')
                        raise ValueError(
                            f"Semantics row {i} must be a list (spread: {spread_name})"
                        )
                    for j, cell in enumerate(row):
                        if cell is not None and not isinstance(cell, str):
                            spread_name = spread_config.get('name', 'unknown')
                            raise ValueError(
                                f"Semantics cell [{i}][{j}] must be a string or "
                                f"null (spread: {spread_name})"
                            )
            else:
                # Dictionary format - traditional validation
                for i, semantic in enumerate(semantics):
                    if not isinstance(semantic, dict):
                        spread_name = spread_config.get('name', 'unknown')
                        raise ValueError(
                            f"Semantics entry {i} must be a dictionary (spread: "
                            f"{spread_name})"
                        )

        # Validate variable placeholders in semantics (only for matrix format)
        semantics = spread_config.get('semantics', [])
        if semantics and isinstance(semantics[0], list):
            self._validate_variable_placeholders_matrix(semantics, path)

        return spread_config

    def _validate_variable_placeholders_matrix(
            self, semantics: list[list[str|None]], path: str
        ) -> None:
        """Validate variable placeholder syntax in semantics matrix, raising
            ValueError if invalid variables are found.
        """
        # Valid variable patterns
        valid_variables = {
            'water', 'fire', 'air', 'earth', 'spirit'
        }

        pattern = r'\$\{([^}]+)\}'

        for i, row in enumerate(semantics):
            for j, cell in enumerate(row):
                if isinstance(cell, str):
                    # Find all variable placeholders
                    matches = re.findall(pattern, cell)
                    for match in matches:
                        if match not in valid_variables:
                            raise ValueError(
                                f"Invalid variable placeholder '${{{match}}}' in "
                                "semantics[{i}][{j}]. Valid variables: "
                                f"{', '.join(sorted(valid_variables))}"
                            )

    def _validate_variable_placeholders(self, semantics: list[dict], path: str) -> None:
        """Validate variable placeholder syntax in semantics, raising ValueError
            if invalid variables are found.
        """
        # Valid variable patterns
        valid_variables = {
            'water', 'fire', 'air', 'earth', 'spirit'
        }

        pattern = r'\$\{([^}]+)\}'

        for semantic in semantics:
            for key, value in semantic.items():
                if isinstance(value, str):
                    # Find all variable placeholders
                    matches = re.findall(pattern, value)
                    for match in matches:
                        if match not in valid_variables:
                            raise ValueError(
                                f"Invalid variable placeholder '${{{match}}}' in "
                                "semantics. Valid variables: "
                                f"{', '.join(sorted(valid_variables))}"
                            )

    def list_spreads(self) -> list[dict[str, str]]:
        """Scan ~/.tarot-oracle/spreads/ and return spread metadata.
            Returns a list of dictionaries containing spread metadata
            (filename, name, description)
        """
        conf = config()
        spreads_dir = Path(conf.path("spreads"))
        if not spreads_dir.exists() or not spreads_dir.is_dir():
            return []

        spreads = []

        # Find all .json files in the spreads directory
        for json_file in spreads_dir.glob("*.json"):
            try:
                spread_data = self.load_spread(json_file.stem)
                if spread_data:
                    spreads.append({
                        "filename": json_file.name,
                        "name": spread_data.get("name", "Unnamed Spread"),
                        "description": spread_data.get(
                            "description", "No description available"
                        )
                    })
            except Exception:
                # Skip invalid spread files
                continue

        return spreads

    def save_spread(self, name: str, spread_config: dict[str, Any]) -> str:
        """Save a spread configuration to the spreads directory, returning file
            path or raising ValueError/OSError if invalid or unwritable.
        """
        conf = config()
        # Validate the configuration first
        validated_config = self._validate_spread_config(spread_config, "new spread")

        # Ensure spreads directory exists
        spreads_dir = Path(conf.path("spreads"))
        spreads_dir.mkdir(parents=True, exist_ok=True)

        # Sanitize filename
        safe_name = sanitize_filename(name)
        if safe_name is None:
            raise ValueError("Invalid spread name")

        file_path = spreads_dir / f"{safe_name}.json"

        try:
            with open(file_path, 'w', encoding='utf-8') as f:
                json.dump(validated_config, f, indent=2)
            return str(file_path)
        except OSError as e:
            raise OSError(f"Failed to save spread: {e}")
