#!/usr/bin/env python3
"""
OmniClaw Context Mapper
Auto-generates OMNICLAW.md project context documents by scanning
the project directory, rules, dependencies, and recent decisions.
"""

import logging
import os
import time
from pathlib import Path
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, field

logger = logging.getLogger("OmniClaw.ContextMapper")

# Directories/files to always ignore when scanning
DEFAULT_IGNORE = {
    ".git", "__pycache__", ".venv", "venv", "node_modules",
    ".mypy_cache", ".pytest_cache", ".tox", "dist", "build",
    "*.egg-info", ".eggs", ".idea", ".vscode", "*.pyc", "*.pyo",
}

# Known rule/config file patterns
RULE_FILE_PATTERNS = [
    ".claude.md", "CLAUDE.md", ".ai_rules", ".cursorrules",
    ".editorconfig", ".eslintrc*", ".prettierrc*",
    "config.yaml", "config.yml", "config.json",
    "pyproject.toml", "setup.cfg", "tsconfig.json",
    "Makefile", "Dockerfile", "docker-compose.yml",
]


@dataclass
class ProjectFile:
    """Represents a file in the project tree"""
    path: str
    relative_path: str
    size_bytes: int
    extension: str
    is_directory: bool = False
    children: List['ProjectFile'] = field(default_factory=list)


class ContextMapper:
    """
    Auto-generates comprehensive project context documents.
    
    Scans the project root for structure, rules, dependencies,
    and recent decisions to produce a complete OMNICLAW.md context file.
    """
    
    def __init__(self, decision_store=None):
        """
        Args:
            decision_store: Optional DecisionArchaeologist instance for recent decisions
        """
        self.decision_store = decision_store
        self._cache: Dict[str, str] = {}
        self._last_generated: Dict[str, float] = {}
        
        logger.info("ContextMapper initialized")
    
    def generate_project_doc(self, root_dir: str, 
                              max_depth: int = 4,
                              include_decisions: bool = True) -> str:
        """
        Generate a complete project context document.
        
        Args:
            root_dir: Root directory of the project
            max_depth: Maximum directory traversal depth
            include_decisions: Whether to include recent decisions
            
        Returns:
            Complete markdown document string
        """
        root = Path(root_dir).resolve()
        project_name = root.name
        
        logger.info(f"Generating context document for: {project_name}")
        
        sections = []
        
        # Header
        sections.append(f"# {project_name} — Project Context\n")
        sections.append(f"*Auto-generated by OmniClaw ContextMapper at "
                       f"{time.strftime('%Y-%m-%d %H:%M:%S')}*\n")
        
        # Project structure
        sections.append("## Project Structure\n")
        sections.append("```")
        tree = self._generate_file_tree(root, max_depth=max_depth)
        sections.append(tree)
        sections.append("```\n")
        
        # Active rules & configuration
        rules = self._scan_for_rule_files(root)
        if rules:
            sections.append("## Active Rules & Configuration\n")
            for rule_file, content_preview in rules.items():
                sections.append(f"### `{rule_file}`\n")
                sections.append(f"```\n{content_preview}\n```\n")
        
        # Dependencies
        deps = self._scan_dependencies(root)
        if deps:
            sections.append("## Dependencies\n")
            for dep_file, dep_list in deps.items():
                sections.append(f"### `{dep_file}`\n")
                for dep in dep_list:
                    sections.append(f"- {dep}")
                sections.append("")
        
        # Module overview
        modules = self._scan_python_modules(root)
        if modules:
            sections.append("## Module Overview\n")
            for mod_path, docstring in modules.items():
                sections.append(f"- **`{mod_path}`**: {docstring}")
            sections.append("")
        
        # Recent decisions
        if include_decisions and self.decision_store:
            decisions = self._get_recent_decisions()
            if decisions:
                sections.append("## Recent Decisions\n")
                for d in decisions:
                    sections.append(f"- **{d.get('decision', 'N/A')}** — "
                                  f"{d.get('reasoning', 'No reasoning logged')}")
                sections.append("")
        
        doc = "\n".join(sections)
        
        # Cache
        self._cache[root_dir] = doc
        self._last_generated[root_dir] = time.time()
        
        logger.info(f"Context document generated: {len(doc)} chars")
        return doc
    
    def save_context_doc(self, root_dir: str, output_path: Optional[str] = None,
                          **kwargs) -> str:
        """
        Generate and save the context document to a file.
        
        Args:
            root_dir: Root directory of the project
            output_path: Where to save (defaults to root_dir/OMNICLAW.md)
            
        Returns:
            Path where the file was saved
        """
        doc = self.generate_project_doc(root_dir, **kwargs)
        
        if output_path is None:
            output_path = os.path.join(root_dir, "OMNICLAW.md")
        
        with open(output_path, 'w', encoding='utf-8') as f:
            f.write(doc)
        
        logger.info(f"Context document saved to: {output_path}")
        return output_path
    
    def _generate_file_tree(self, root: Path, prefix: str = "", 
                             depth: int = 0, max_depth: int = 4) -> str:
        """Generate an ASCII file tree"""
        if depth > max_depth:
            return ""
        
        lines = []
        
        try:
            entries = sorted(root.iterdir(), key=lambda e: (not e.is_dir(), e.name.lower()))
        except PermissionError:
            return f"{prefix}[permission denied]"
        
        # Filter ignored entries
        entries = [e for e in entries if not self._should_ignore(e.name)]
        
        for i, entry in enumerate(entries):
            is_last = (i == len(entries) - 1)
            connector = "└── " if is_last else "├── "
            extension = "    " if is_last else "│   "
            
            if entry.is_dir():
                lines.append(f"{prefix}{connector}{entry.name}/")
                subtree = self._generate_file_tree(
                    entry, prefix + extension, depth + 1, max_depth
                )
                if subtree:
                    lines.append(subtree)
            else:
                size = self._format_size(entry.stat().st_size)
                lines.append(f"{prefix}{connector}{entry.name} ({size})")
        
        return "\n".join(lines)
    
    def _should_ignore(self, name: str) -> bool:
        """Check if a file/directory should be ignored"""
        if name in DEFAULT_IGNORE:
            return True
        for pattern in DEFAULT_IGNORE:
            if pattern.startswith("*") and name.endswith(pattern[1:]):
                return True
        return False
    
    def _scan_for_rule_files(self, root: Path) -> Dict[str, str]:
        """Scan for rule/config files and return previews"""
        rules = {}
        
        for pattern in RULE_FILE_PATTERNS:
            if "*" in pattern:
                matches = list(root.glob(pattern))
            else:
                match = root / pattern
                matches = [match] if match.exists() else []
            
            for match in matches:
                if match.is_file():
                    try:
                        content = match.read_text(encoding='utf-8', errors='ignore')
                        # Truncate long files
                        preview = content[:2000]
                        if len(content) > 2000:
                            preview += f"\n... ({len(content) - 2000} more chars)"
                        rules[match.name] = preview
                    except Exception:
                        rules[match.name] = "[could not read]"
        
        return rules
    
    def _scan_dependencies(self, root: Path) -> Dict[str, List[str]]:
        """Scan for dependency files"""
        deps = {}
        
        # requirements.txt
        req_file = root / "requirements.txt"
        if req_file.exists():
            try:
                lines = req_file.read_text(encoding='utf-8').strip().split('\n')
                deps["requirements.txt"] = [
                    l.strip() for l in lines 
                    if l.strip() and not l.strip().startswith('#')
                ]
            except Exception:
                pass
        
        # setup.py — extract install_requires if present
        setup_file = root / "setup.py"
        if setup_file.exists():
            try:
                content = setup_file.read_text(encoding='utf-8')
                if "install_requires" in content:
                    # Simple extraction between brackets
                    start = content.index("install_requires")
                    bracket_start = content.index("[", start)
                    bracket_end = content.index("]", bracket_start)
                    deps_str = content[bracket_start + 1:bracket_end]
                    dep_list = [
                        d.strip().strip("'\"").strip(",")
                        for d in deps_str.split('\n')
                        if d.strip() and d.strip() not in ['[', ']']
                    ]
                    deps["setup.py (install_requires)"] = [d for d in dep_list if d]
            except Exception:
                pass
        
        # package.json
        pkg_file = root / "package.json"
        if pkg_file.exists():
            try:
                import json
                pkg = json.loads(pkg_file.read_text(encoding='utf-8'))
                all_deps = {}
                all_deps.update(pkg.get("dependencies", {}))
                all_deps.update(pkg.get("devDependencies", {}))
                deps["package.json"] = [f"{k}@{v}" for k, v in all_deps.items()]
            except Exception:
                pass
        
        return deps
    
    def _scan_python_modules(self, root: Path) -> Dict[str, str]:
        """Scan Python files and extract module docstrings"""
        modules = {}
        
        for py_file in root.rglob("*.py"):
            if self._should_ignore(py_file.parent.name) or py_file.name.startswith("__"):
                continue
            
            relative = py_file.relative_to(root)
            try:
                content = py_file.read_text(encoding='utf-8', errors='ignore')
                # Extract first docstring
                docstring = self._extract_docstring(content)
                modules[str(relative)] = docstring or "No docstring"
            except Exception:
                modules[str(relative)] = "[could not read]"
        
        return modules
    
    def _extract_docstring(self, content: str) -> Optional[str]:
        """Extract the module-level docstring"""
        lines = content.strip().split('\n')
        in_docstring = False
        docstring_lines = []
        delimiter = None
        
        for line in lines:
            stripped = line.strip()
            
            # Skip shebang and comments
            if stripped.startswith('#') or stripped.startswith('#!/'):
                continue
            
            if not in_docstring:
                if stripped.startswith('"""') or stripped.startswith("'''"):
                    delimiter = stripped[:3]
                    if stripped.count(delimiter) >= 2:
                        # Single-line docstring
                        return stripped.strip(delimiter).strip()
                    in_docstring = True
                    rest = stripped[3:].strip()
                    if rest:
                        docstring_lines.append(rest)
                elif stripped and not stripped.startswith('import') and not stripped.startswith('from'):
                    break  # No docstring found
            else:
                if delimiter and delimiter in stripped:
                    rest = stripped.replace(delimiter, '').strip()
                    if rest:
                        docstring_lines.append(rest)
                    break
                docstring_lines.append(stripped)
        
        if docstring_lines:
            return docstring_lines[0]  # Return first line of docstring
        return None
    
    def _get_recent_decisions(self, limit: int = 10) -> List[Dict]:
        """Get recent decisions from the decision store"""
        if not self.decision_store:
            return []
        try:
            return self.decision_store.get_recent(limit=limit)
        except Exception:
            return []
    
    @staticmethod
    def _format_size(size_bytes: int) -> str:
        """Format file size in human-readable form"""
        if size_bytes < 1024:
            return f"{size_bytes}B"
        elif size_bytes < 1024 * 1024:
            return f"{size_bytes / 1024:.1f}KB"
        else:
            return f"{size_bytes / (1024 * 1024):.1f}MB"
