"""
Shared utilities for spawning Claude CLI subprocesses.

All CodeContext scripts that call Claude CLI should use these utilities
to ensure proper environment setup (prevents recursive hooks).
"""

import os
import shutil
from pathlib import Path
from typing import Optional, Dict


# ============== PATH CONSTANTS ==============
# New unified directory structure at ~/.codecontext/

CODECONTEXT_ROOT = Path.home() / ".codecontext"
PROJECTS_DIR = CODECONTEXT_ROOT / "projects"
GLOBAL_DIR = CODECONTEXT_ROOT / "global"
DEBUG_DIR = CODECONTEXT_ROOT / "debug"

# Legacy path (for migration detection)
LEGACY_ROOT = Path.home() / ".autocoder" / "codecontext"

# Script name to session folder mapping (descriptive names)
SCRIPT_FOLDERS = {
    "memory_extractor": "memory-extraction",
    "summary_generator": "summary-generation",
    "conflict_resolver": "conflict-resolution",
    "memory_consolidator": "memory-consolidation",
    # Legacy names (for backward compatibility during transition)
    "extract": "memory-extraction",
    "summarize": "summary-generation",
    "resolve": "conflict-resolution",
    "consolidate": "memory-consolidation",
}


# ============== PATH ENCODING ==============

def is_encoded_path(path: str) -> bool:
    """
    Check if a path string is already encoded.

    Encoded paths have no path separators and no colon at position 1.
    Examples of encoded: "C--Users-Sam-my-app", "-home-sam-app"
    Examples of not encoded: "C:\\Users\\Sam", "/home/sam", "C:/Users"
    """
    # Has path separators? Not encoded
    if "\\" in path or "/" in path:
        return False
    # Has drive letter colon (e.g., "C:")? Not encoded
    if len(path) > 1 and path[1] == ":":
        return False
    # Looks like an encoded Windows path (C--...) or Unix path (-home-...)
    if path.startswith("C--") or path.startswith("D--") or path.startswith("-"):
        return True
    # No separators and no drive letter - could be a simple name, treat as encoded
    return True


def encode_project_path(path: str) -> str:
    """
    Encode project path to create safe directory names.

    Converts full paths to encoded folder names by replacing
    path separators and special characters with hyphens.

    If the path is already encoded, returns it unchanged.

    Example:
        C:\\Users\\Sam\\my-app → C--Users-Sam-my-app
        /home/sam/projects/app → -home-sam-projects-app
        C--Users-Sam-my-app → C--Users-Sam-my-app (unchanged)

    Args:
        path: Full path to the project directory, or already-encoded name

    Returns:
        Encoded string safe for use as directory name
    """
    # Skip encoding if already encoded
    if is_encoded_path(path):
        return path

    # Normalize and resolve to absolute path
    resolved = str(Path(path).resolve())

    # Replace all path separators and colon with hyphens
    encoded = resolved.replace("\\", "-").replace("/", "-").replace(":", "-")

    return encoded


def decode_project_path(encoded: str) -> Optional[str]:
    """
    Attempt to decode an encoded project path back to original.

    Note: This is a best-effort operation - some information may be lost
    during encoding (e.g., distinguishing between / and \\ on Windows).

    Args:
        encoded: Encoded project path string

    Returns:
        Best-guess original path, or None if undecodable
    """
    # Simple reverse: restore Windows-style path separators
    # This won't be perfect but helps for display purposes
    if encoded.startswith("C--") or encoded.startswith("D--"):
        # Windows path - restore drive letter and separators
        parts = encoded.split("-")
        if len(parts) >= 2:
            drive = parts[0] + ":"
            rest = "\\".join(parts[1:])
            return f"{drive}\\{rest}"
    elif encoded.startswith("-"):
        # Unix path - restore forward slashes
        return encoded.replace("-", "/")

    return None


def get_display_name(path: str) -> str:
    """
    Get a human-readable display name from a project path.

    Accepts either original path or encoded path. Returns "project (parent)".

    Example:
        C:\\Users\\Sam\\builds\\claude-desktop\\agentic-coder
        → "agentic-coder (claude-desktop)"

        C--Users-Sam-builds-claude-desktop-agentic-coder
        → "agentic-coder (claude-desktop)"  (reads from .metadata.json)

    Args:
        path: Original path OR encoded path string

    Returns:
        Human-readable "project (parent)" format
    """
    import json

    if not path:
        return "default"

    # If it's an original path (has separators), extract directly
    if "\\" in path or "/" in path:
        parts = path.replace("/", "\\").split("\\")
        parts = [p for p in parts if p and ":" not in p]  # Remove empty and drive letter
        if len(parts) >= 2:
            return f"{parts[-1]} ({parts[-2]})"
        elif len(parts) == 1:
            return parts[0]
        return path

    # It's encoded - try to read from metadata file
    metadata_file = PROJECTS_DIR / path / ".metadata.json"
    if metadata_file.exists():
        try:
            metadata = json.loads(metadata_file.read_text())
            original = metadata.get("original_path")
            if original:
                return get_display_name(original)  # Recurse with original path
        except (json.JSONDecodeError, IOError):
            pass

    # Fallback: return encoded name as-is
    return path


# ============== DIRECTORY HELPERS ==============

def get_project_dir(project_path: str) -> Path:
    """
    Get/create project directory for a given project path.

    Creates the directory structure:
        ~/.codecontext/projects/{encoded-path}/

    Also saves original path to .metadata.json for display purposes.

    Args:
        project_path: Full path to the project (e.g., C:\\Users\\Sam\\my-app)

    Returns:
        Path to the project's data directory
    """
    import json

    encoded = encode_project_path(project_path)
    project_dir = PROJECTS_DIR / encoded

    # Create base structure
    project_dir.mkdir(parents=True, exist_ok=True)

    # Save original path metadata if this is a real path (not already encoded)
    metadata_file = project_dir / ".metadata.json"
    if not is_encoded_path(project_path) and not metadata_file.exists():
        metadata = {
            "original_path": str(Path(project_path).resolve()),
            "encoded_name": encoded
        }
        metadata_file.write_text(json.dumps(metadata, indent=2))

    return project_dir


def get_vector_db_path(project_path: str) -> Path:
    """
    Get the vector database path for a project.

    Args:
        project_path: Full path to the project

    Returns:
        Path to the project's vector_db directory
    """
    return get_project_dir(project_path) / "vector_db"


def get_session_dir(project_path: str, script_name: str) -> Path:
    """
    Get/create dedicated session directory for CLI subprocess.

    This sets the cwd for Claude CLI calls so that session JSONL files
    are logged to a separate encoded path in ~/.claude/projects/,
    keeping subprocess sessions isolated from user sessions.

    Args:
        project_path: Full path to the project (e.g., C:\\Users\\Sam\\my-app)
        script_name: One of 'memory_extractor', 'summary_generator',
                     'conflict_resolver', 'memory_consolidator'
                     (or legacy: 'extract', 'summarize', 'resolve', 'consolidate')

    Returns:
        Path to session directory (created if needed)
    """
    # Map script name to folder name
    folder_name = SCRIPT_FOLDERS.get(script_name, script_name)

    # Build path: ~/.codecontext/projects/{encoded}/sessions/{folder}/
    session_dir = get_project_dir(project_path) / "sessions" / folder_name

    # Create directory if needed
    session_dir.mkdir(parents=True, exist_ok=True)

    return session_dir


def get_state_dir(project_path: str) -> Path:
    """
    Get/create state directory for a project.

    State files track runtime information like chunk positions.

    Args:
        project_path: Full path to the project

    Returns:
        Path to the project's state directory
    """
    state_dir = get_project_dir(project_path) / "state"
    state_dir.mkdir(parents=True, exist_ok=True)
    return state_dir


def get_logs_dir(project_path: str) -> Path:
    """
    Get/create logs directory for a project.

    Args:
        project_path: Full path to the project

    Returns:
        Path to the project's logs directory
    """
    logs_dir = get_project_dir(project_path) / "logs"
    logs_dir.mkdir(parents=True, exist_ok=True)
    return logs_dir


def get_summary_path(project_path: str) -> Path:
    """
    Get the summary.json path for a project.

    Args:
        project_path: Full path to the project

    Returns:
        Path to summary.json file
    """
    return get_project_dir(project_path) / "summary.json"


# ============== SUBPROCESS ENVIRONMENT ==============

def get_subprocess_env() -> Dict[str, str]:
    """
    Get environment dict for Claude CLI subprocesses.

    Sets CODECONTEXT_SUBPROCESS=1 to signal hooks to skip processing.
    This prevents recursive hook execution when triage/consolidation
    spawns Claude CLI sessions.
    """
    env = os.environ.copy()
    env["CODECONTEXT_SUBPROCESS"] = "1"
    return env

# ============== CONFIG ACCESS ==============

CONFIG_FILE = CODECONTEXT_ROOT / "config.json"


def get_debug_mode() -> bool:
    """
    Check if debug mode is enabled from config file.

    When debug mode is OFF (default): --no-session-persistence is added
    When debug mode is ON: JSONL files are created for debugging

    Returns:
        True if debug mode is enabled, False otherwise
    """
    import json
    if CONFIG_FILE.exists():
        try:
            config = json.loads(CONFIG_FILE.read_text())
            return config.get("debug_mode", False)
        except (json.JSONDecodeError, IOError):
            pass
    return False


def build_claude_cli_args(prompt: str, model: str = "haiku", use_stdin: bool = False) -> list:
    """
    Build CLI arguments with debug mode awareness.

    When debug mode is OFF: adds --no-session-persistence (no JSONL files)
    When debug mode is ON: omits the flag (JSONL files created for debugging)

    Args:
        prompt: The prompt text (or "-" for stdin mode)
        model: Claude model to use (default: "haiku")
        use_stdin: If True, use "-p -" for stdin input

    Returns:
        List of CLI arguments (excluding the claude executable itself)
    """
    args = [
        "--print",
        "--dangerously-skip-permissions",
        "--model", model,
    ]

    # Add --no-session-persistence when debug mode is OFF (default)
    if not get_debug_mode():
        args.append("--no-session-persistence")

    # Add prompt argument
    if use_stdin:
        args.extend(["-p", "-"])
    else:
        args.extend(["-p", prompt])

    return args


def find_claude_cli() -> Optional[str]:
    """
    Find Claude Code CLI executable.

    Checks PATH first, then common Windows locations.

    Returns:
        Path to claude executable, or None if not found.
    """
    # Try PATH first
    claude_path = shutil.which("claude")
    if claude_path:
        return claude_path

    # Check common Windows locations
    home = Path.home()
    candidates = [
        home / ".local" / "bin" / "claude.exe",
        home / ".local" / "bin" / "claude",
        home / "AppData" / "Local" / "Programs" / "claude" / "claude.exe",
        Path("C:/Program Files/claude/claude.exe"),
        Path("C:/Program Files (x86)/claude/claude.exe"),
    ]

    for candidate in candidates:
        if candidate.exists():
            return str(candidate)

    return None
