"""
Memory Bridge Store for GSD-RLM.

Provides the MemoryBridge class for multi-level persistent context
storage integrated with the .planning directory structure (MEM-09).

Storage locations:
- L0_SESSION: .planning/sessions/{session_id}.json
- L1_PHASE: .planning/phases/{phase_id}/FACTS.md
- L2_PROJECT: .planning/facts.json
- L3_WORKSPACE: ~/.gsd-rlm/workspace_facts.json
"""

import json
import os
import tempfile
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional

from gsd_rlm.memory.bridge.facts import BridgeFact, BridgeLevel


class MemoryBridge:
    """
    Multi-level memory bridge for persistent context storage.

    Stores facts at different scope levels (L0-L3) with automatic
    routing to appropriate storage locations based on the .planning
    directory structure.

    Attributes:
        project_root: Root directory of the project containing .planning/.
        workspace_dir: User-level storage directory (~/.gsd-rlm/).

    Example:
        >>> bridge = MemoryBridge(Path("/path/to/project"))
        >>> fact = BridgeFact.create(
        ...     level=BridgeLevel.L1_PHASE,
        ...     scope_id="03-memory-systems",
        ...     key="python_version",
        ...     value="3.12"
        ... )
        >>> bridge.store_fact(fact)
        >>> facts = bridge.get_facts(BridgeLevel.L1_PHASE, "03-memory-systems")
    """

    # Default workspace directory for L3 facts
    DEFAULT_WORKSPACE_DIR = ".gsd-rlm"

    def __init__(self, project_root: Path, workspace_dir: Optional[Path] = None):
        """
        Initialize the MemoryBridge.

        Args:
            project_root: Root directory of the project (should contain .planning/).
            workspace_dir: Optional custom workspace directory for L3 facts.
                          Defaults to ~/.gsd-rlm/.
        """
        self.project_root = Path(project_root)
        self.planning_dir = self.project_root / ".planning"

        # L3 workspace directory (user-level persistence)
        if workspace_dir:
            self.workspace_dir = Path(workspace_dir)
        else:
            self.workspace_dir = Path.home() / self.DEFAULT_WORKSPACE_DIR

        # Ensure directories exist
        self._ensure_directories()

    def _ensure_directories(self) -> None:
        """Create necessary directories if they don't exist."""
        # Ensure .planning directory exists
        self.planning_dir.mkdir(parents=True, exist_ok=True)

        # Ensure sessions directory exists
        sessions_dir = self.planning_dir / "sessions"
        sessions_dir.mkdir(parents=True, exist_ok=True)

        # Ensure workspace directory exists
        self.workspace_dir.mkdir(parents=True, exist_ok=True)

    # =================================================================
    # Public API
    # =================================================================

    def store_fact(self, fact: BridgeFact) -> None:
        """
        Store a fact at the appropriate level.

        Routes the fact to the level-specific storage handler based
        on the fact's level attribute.

        Args:
            fact: The BridgeFact to store.
        """
        handlers = {
            BridgeLevel.L0_SESSION: self._store_session_fact,
            BridgeLevel.L1_PHASE: self._store_phase_fact,
            BridgeLevel.L2_PROJECT: self._store_project_fact,
            BridgeLevel.L3_WORKSPACE: self._store_workspace_fact,
        }

        handler = handlers.get(fact.level)
        if handler:
            handler(fact)
        else:
            raise ValueError(f"Unknown BridgeLevel: {fact.level}")

    def get_facts(
        self,
        level: BridgeLevel,
        scope_id: str,
        keys: Optional[List[str]] = None,
    ) -> Dict[str, BridgeFact]:
        """
        Retrieve facts from a specific level and scope.

        Args:
            level: The scope level to retrieve from.
            scope_id: The scope identifier (session_id, phase_id, etc.).
            keys: Optional list of specific keys to retrieve.
                  If None, retrieves all facts for the scope.

        Returns:
            Dictionary mapping fact keys to BridgeFact instances.
        """
        handlers = {
            BridgeLevel.L0_SESSION: self._get_session_facts,
            BridgeLevel.L1_PHASE: self._get_phase_facts,
            BridgeLevel.L2_PROJECT: self._get_project_facts,
            BridgeLevel.L3_WORKSPACE: self._get_workspace_facts,
        }

        handler = handlers.get(level)
        if not handler:
            return {}

        facts = handler(scope_id)

        # Filter by keys if specified
        if keys:
            facts = {k: v for k, v in facts.items() if k in keys}

        # Filter out expired facts
        facts = {k: v for k, v in facts.items() if not v.is_expired()}

        return facts

    def search_facts(
        self,
        query: str,
        levels: Optional[List[BridgeLevel]] = None,
        k: int = 10,
    ) -> List[BridgeFact]:
        """
        Search for facts matching a query.

        Performs a simple text-based search across fact keys and values.
        For semantic search, embeddings should be computed separately.

        Args:
            query: Search query string.
            levels: Optional list of levels to search. If None, searches all.
            k: Maximum number of results to return.

        Returns:
            List of matching BridgeFact instances, ordered by relevance.
        """
        if levels is None:
            levels = list(BridgeLevel)

        results: List[tuple[float, BridgeFact]] = []
        query_lower = query.lower()

        for level in levels:
            # Get all scopes for this level
            scope_ids = self._get_scope_ids_for_level(level)

            for scope_id in scope_ids:
                facts = self.get_facts(level, scope_id)

                for fact in facts.values():
                    # Simple text matching on key and string value
                    score = self._calculate_relevance(fact, query_lower)
                    if score > 0:
                        results.append((score, fact))

        # Sort by relevance score (descending) and take top k
        results.sort(key=lambda x: x[0], reverse=True)
        return [fact for _, fact in results[:k]]

    def delete_fact(self, fact_id: str) -> bool:
        """
        Delete a fact by its ID.

        Searches all levels for the fact and removes it.

        Args:
            fact_id: The unique identifier of the fact to delete.

        Returns:
            True if the fact was found and deleted, False otherwise.
        """
        # Try each level
        for level in BridgeLevel:
            scope_ids = self._get_scope_ids_for_level(level)

            for scope_id in scope_ids:
                if self._delete_fact_from_scope(level, scope_id, fact_id):
                    return True

        return False

    def get_fact_by_id(self, fact_id: str) -> Optional[BridgeFact]:
        """
        Retrieve a specific fact by its ID.

        Args:
            fact_id: The unique identifier of the fact.

        Returns:
            The BridgeFact if found, None otherwise.
        """
        for level in BridgeLevel:
            scope_ids = self._get_scope_ids_for_level(level)

            for scope_id in scope_ids:
                facts = self.get_facts(level, scope_id)
                for fact in facts.values():
                    if fact.fact_id == fact_id:
                        return fact

        return None

    # =================================================================
    # L0 Session Storage (.planning/sessions/{session_id}.json)
    # =================================================================

    def _store_session_fact(self, fact: BridgeFact) -> None:
        """Store a session-level fact."""
        sessions_dir = self.planning_dir / "sessions"
        sessions_dir.mkdir(parents=True, exist_ok=True)

        session_file = sessions_dir / f"{fact.scope_id}.json"
        facts_data = self._load_json_file(session_file)

        facts_data[fact.key] = fact.to_dict()
        self._write_json_file(session_file, facts_data)

    def _get_session_facts(self, scope_id: str) -> Dict[str, BridgeFact]:
        """Retrieve session-level facts."""
        session_file = self.planning_dir / "sessions" / f"{scope_id}.json"
        return self._load_facts_from_json(session_file)

    # =================================================================
    # L1 Phase Storage (.planning/phases/{phase_id}/FACTS.md)
    # =================================================================

    def _store_phase_fact(self, fact: BridgeFact) -> None:
        """Store a phase-level fact in FACTS.md."""
        phase_dir = self.planning_dir / "phases" / fact.scope_id
        phase_dir.mkdir(parents=True, exist_ok=True)

        facts_file = phase_dir / "FACTS.md"

        # Load existing facts
        existing_facts = self._parse_facts_md(facts_file)

        # Update or add the fact
        existing_facts[fact.key] = fact

        # Write back to markdown
        self._write_facts_md(facts_file, existing_facts)

    def _get_phase_facts(self, scope_id: str) -> Dict[str, BridgeFact]:
        """Retrieve phase-level facts from FACTS.md."""
        facts_file = self.planning_dir / "phases" / scope_id / "FACTS.md"
        return self._parse_facts_md(facts_file)

    def _parse_facts_md(self, filepath: Path) -> Dict[str, BridgeFact]:
        """Parse facts from a FACTS.md file."""
        if not filepath.exists():
            return {}

        facts: Dict[str, BridgeFact] = {}
        content = filepath.read_text(encoding="utf-8")

        # Parse YAML-style fact blocks
        # Format:
        # ## fact_key
        # ```yaml
        # fact_id: xxx
        # level: L1_PHASE
        # ...
        # ```
        import re

        pattern = r"##\s+([^\n]+)\n```yaml\n(.*?)```"
        matches = re.findall(pattern, content, re.DOTALL)

        for key, yaml_content in matches:
            key = key.strip()
            try:
                # Simple YAML parsing for our format
                data = self._parse_simple_yaml(yaml_content)
                if "fact_id" in data:
                    fact = BridgeFact.from_dict(data)
                    facts[key] = fact
            except (ValueError, KeyError):
                continue

        return facts

    def _write_facts_md(self, filepath: Path, facts: Dict[str, BridgeFact]) -> None:
        """Write facts to a FACTS.md file."""
        lines = [
            "# Phase Facts",
            "",
            "This file contains facts stored at the phase level.",
            "Auto-generated by MemoryBridge. Do not edit manually.",
            "",
        ]

        for key, fact in sorted(facts.items()):
            lines.append(f"## {key}")
            lines.append("```yaml")
            lines.append(self._dict_to_yaml(fact.to_dict()))
            lines.append("```")
            lines.append("")

        self._atomic_write(filepath, "\n".join(lines))

    # =================================================================
    # L2 Project Storage (.planning/facts.json)
    # =================================================================

    def _store_project_fact(self, fact: BridgeFact) -> None:
        """Store a project-level fact."""
        facts_file = self.planning_dir / "facts.json"
        facts_data = self._load_json_file(facts_file)

        facts_data[fact.key] = fact.to_dict()
        self._write_json_file(facts_file, facts_data)

    def _get_project_facts(self, scope_id: str) -> Dict[str, BridgeFact]:
        """Retrieve project-level facts."""
        # scope_id is ignored for project level (single project facts file)
        facts_file = self.planning_dir / "facts.json"
        return self._load_facts_from_json(facts_file)

    # =================================================================
    # L3 Workspace Storage (~/.gsd-rlm/workspace_facts.json)
    # =================================================================

    def _store_workspace_fact(self, fact: BridgeFact) -> None:
        """Store a workspace-level fact."""
        facts_file = self.workspace_dir / "workspace_facts.json"
        facts_data = self._load_json_file(facts_file)

        # Use scope_id as namespace within the file
        if fact.scope_id not in facts_data:
            facts_data[fact.scope_id] = {}

        facts_data[fact.scope_id][fact.key] = fact.to_dict()
        self._write_json_file(facts_file, facts_data)

    def _get_workspace_facts(self, scope_id: str) -> Dict[str, BridgeFact]:
        """Retrieve workspace-level facts."""
        facts_file = self.workspace_dir / "workspace_facts.json"
        all_facts = self._load_json_file(facts_file)

        scope_facts = all_facts.get(scope_id, {})
        result: Dict[str, BridgeFact] = {}

        for key, data in scope_facts.items():
            try:
                result[key] = BridgeFact.from_dict(data)
            except (ValueError, KeyError):
                continue

        return result

    # =================================================================
    # Helper Methods
    # =================================================================

    def _get_scope_ids_for_level(self, level: BridgeLevel) -> List[str]:
        """Get all scope IDs that exist for a given level."""
        if level == BridgeLevel.L0_SESSION:
            sessions_dir = self.planning_dir / "sessions"
            if sessions_dir.exists():
                return [p.stem for p in sessions_dir.glob("*.json")]
            return []

        elif level == BridgeLevel.L1_PHASE:
            phases_dir = self.planning_dir / "phases"
            if phases_dir.exists():
                return [p.name for p in phases_dir.iterdir() if p.is_dir()]
            return []

        elif level == BridgeLevel.L2_PROJECT:
            # Single project scope
            facts_file = self.planning_dir / "facts.json"
            if facts_file.exists():
                return ["project"]
            return []

        elif level == BridgeLevel.L3_WORKSPACE:
            facts_file = self.workspace_dir / "workspace_facts.json"
            data = self._load_json_file(facts_file)
            return list(data.keys())

        return []

    def _delete_fact_from_scope(
        self, level: BridgeLevel, scope_id: str, fact_id: str
    ) -> bool:
        """Delete a fact from a specific scope."""
        facts = self.get_facts(level, scope_id)

        for key, fact in facts.items():
            if fact.fact_id == fact_id:
                # Create new fact dict without this fact
                if level == BridgeLevel.L0_SESSION:
                    session_file = self.planning_dir / "sessions" / f"{scope_id}.json"
                    data = self._load_json_file(session_file)
                    if key in data:
                        del data[key]
                        self._write_json_file(session_file, data)
                        return True

                elif level == BridgeLevel.L1_PHASE:
                    facts_file = self.planning_dir / "phases" / scope_id / "FACTS.md"
                    all_facts = self._parse_facts_md(facts_file)
                    if key in all_facts:
                        del all_facts[key]
                        self._write_facts_md(facts_file, all_facts)
                        return True

                elif level == BridgeLevel.L2_PROJECT:
                    facts_file = self.planning_dir / "facts.json"
                    data = self._load_json_file(facts_file)
                    if key in data:
                        del data[key]
                        self._write_json_file(facts_file, data)
                        return True

                elif level == BridgeLevel.L3_WORKSPACE:
                    facts_file = self.workspace_dir / "workspace_facts.json"
                    data = self._load_json_file(facts_file)
                    if scope_id in data and key in data[scope_id]:
                        del data[scope_id][key]
                        self._write_json_file(facts_file, data)
                        return True

        return False

    def _calculate_relevance(self, fact: BridgeFact, query: str) -> float:
        """Calculate relevance score for a fact against a query."""
        score = 0.0

        # Key match (higher weight)
        if query in fact.key.lower():
            score += 2.0

        # Value match
        value_str = str(fact.value).lower()
        if query in value_str:
            score += 1.0

        # Source match
        if query in fact.source.lower():
            score += 0.5

        # Exact key match bonus
        if fact.key.lower() == query:
            score += 3.0

        return score

    def _load_json_file(self, filepath: Path) -> Dict[str, Any]:
        """Load JSON file with error handling."""
        if not filepath.exists():
            return {}

        try:
            content = filepath.read_text(encoding="utf-8")
            return json.loads(content)
        except (json.JSONDecodeError, OSError):
            return {}

    def _write_json_file(self, filepath: Path, data: Dict[str, Any]) -> None:
        """Write JSON file with atomic write."""
        content = json.dumps(data, indent=2, ensure_ascii=False)
        self._atomic_write(filepath, content)

    def _load_facts_from_json(self, filepath: Path) -> Dict[str, BridgeFact]:
        """Load facts from a JSON file."""
        data = self._load_json_file(filepath)
        facts: Dict[str, BridgeFact] = {}

        for key, fact_data in data.items():
            try:
                facts[key] = BridgeFact.from_dict(fact_data)
            except (ValueError, KeyError):
                continue

        return facts

    def _atomic_write(self, filepath: Path, content: str) -> None:
        """Write to file atomically using temp file + rename."""
        filepath.parent.mkdir(parents=True, exist_ok=True)

        # Write to temp file first
        fd, temp_path = tempfile.mkstemp(
            dir=filepath.parent,
            prefix=f".{filepath.name}.",
            suffix=".tmp",
        )

        try:
            with os.fdopen(fd, "w", encoding="utf-8") as f:
                f.write(content)

            # Atomic rename
            os.replace(temp_path, str(filepath))
        except Exception:
            # Clean up temp file on error
            if os.path.exists(temp_path):
                os.unlink(temp_path)
            raise

    def _parse_simple_yaml(self, yaml_content: str) -> Dict[str, Any]:
        """Simple YAML parser for our fact format."""
        result: Dict[str, Any] = {}

        for line in yaml_content.strip().split("\n"):
            if ":" not in line:
                continue

            key, _, value = line.partition(":")
            key = key.strip()
            value = value.strip()

            # Remove quotes
            if value.startswith('"') and value.endswith('"'):
                value = value[1:-1]
            elif value.startswith("'") and value.endswith("'"):
                value = value[1:-1]

            # Handle None (before other checks)
            if value.lower() == "null" or value == "":
                value = None
                result[key] = value
                continue

            # Handle booleans (before number conversion)
            if value.lower() == "true":
                value = True
                result[key] = value
                continue
            elif value.lower() == "false":
                value = False
                result[key] = value
                continue

            # Handle numbers
            try:
                # Type checker needs explicit str check after continue statements
                assert isinstance(value, str)
                if "." in value:
                    value = float(value)
                else:
                    value = int(value)
            except (ValueError, AssertionError):
                pass

            result[key] = value

        return result

    def _dict_to_yaml(self, data: Dict[str, Any], indent: int = 0) -> str:
        """Convert dict to simple YAML format."""
        lines = []
        prefix = "  " * indent

        for key, value in data.items():
            if value is None:
                lines.append(f"{prefix}{key}: null")
            elif isinstance(value, bool):
                lines.append(f"{prefix}{key}: {str(value).lower()}")
            elif isinstance(value, (int, float)):
                lines.append(f"{prefix}{key}: {value}")
            elif isinstance(value, str):
                # Escape special characters if needed
                if "\n" in value or ":" in value or '"' in value:
                    lines.append(f'{prefix}{key}: "{value}"')
                else:
                    lines.append(f"{prefix}{key}: {value}")
            elif isinstance(value, list):
                lines.append(f"{prefix}{key}:")
                for item in value:
                    lines.append(f"{prefix}  - {item}")
            elif isinstance(value, dict):
                lines.append(f"{prefix}{key}:")
                lines.append(self._dict_to_yaml(value, indent + 1))
            else:
                lines.append(f"{prefix}{key}: {value}")

        return "\n".join(lines)
