"""
CodeContext Installer

Handles installation and uninstallation of CodeContext hooks for Claude Code integration.
Supports both global (~/.claude/) and project-local (./.claude/) installations.

Key principles:
- Always backup settings.json before modification
- Preserve existing user hooks (only touch CodeContext entries)
- Use marker files for reliable runtime mode detection
- Never delete user data without explicit confirmation
"""

import json
import os
import platform
import shutil
import sys
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

# Version for marker file
__version__ = "0.1.0"


class InstallError(Exception):
    """Installation error with user-friendly message."""
    pass


# ============== PACKAGE DATA LOCATION ==============

def get_package_root() -> Path:
    """
    Get the root directory of the installed package.

    In development mode (pip install -e .): points to repo root
    In installed mode: points to site-packages location
    """
    return Path(__file__).parent.parent.parent


def get_bundled_hooks_dir() -> Path:
    """
    Find hooks directory bundled with the pip package.

    Search order:
    1. share/codecontext/hooks (pip installed)
    2. hooks/ relative to package root (dev mode)
    """
    package_root = get_package_root()

    # Try pip install location (shared data)
    share_hooks = package_root.parent / "share" / "codecontext" / "hooks"
    if share_hooks.exists():
        return share_hooks

    # Try development mode (hooks at repo root)
    dev_hooks = package_root / "hooks"
    if dev_hooks.exists():
        return dev_hooks

    # Fallback: check relative to this file
    local_hooks = Path(__file__).parent.parent.parent / "hooks"
    if local_hooks.exists():
        return local_hooks

    raise InstallError(
        f"Bundled hooks not found. Searched:\n"
        f"  - {share_hooks}\n"
        f"  - {dev_hooks}\n"
        f"  - {local_hooks}"
    )


def get_bundled_skills_dir() -> Path:
    """
    Find skills directory bundled with the pip package.

    Search order:
    1. share/codecontext/skills (pip installed)
    2. skills/ relative to package root (dev mode)
    """
    package_root = get_package_root()

    # Try pip install location (shared data)
    share_skills = package_root.parent / "share" / "codecontext" / "skills"
    if share_skills.exists():
        return share_skills

    # Try development mode (skills at repo root)
    dev_skills = package_root / "skills"
    if dev_skills.exists():
        return dev_skills

    # Fallback: check relative to this file
    local_skills = Path(__file__).parent.parent.parent / "skills"
    if local_skills.exists():
        return local_skills

    raise InstallError(
        f"Bundled skills not found. Searched:\n"
        f"  - {share_skills}\n"
        f"  - {dev_skills}\n"
        f"  - {local_skills}"
    )


# ============== FILE OPERATIONS ==============

def copy_hooks(dest_dir: Path) -> List[str]:
    """
    Copy all hook files from bundled package to destination.

    Args:
        dest_dir: Destination directory (e.g., ~/.claude/hooks/codecontext/)

    Returns:
        List of copied file names
    """
    src_dir = get_bundled_hooks_dir()
    dest_dir.mkdir(parents=True, exist_ok=True)

    copied = []
    for src_file in src_dir.iterdir():
        if src_file.suffix == ".py" or src_file.name == "README.md":
            shutil.copy2(src_file, dest_dir / src_file.name)
            copied.append(src_file.name)

    return copied


def copy_skills(dest_dir: Path) -> List[str]:
    """
    Copy SKILL.md from bundled package to destination.

    Args:
        dest_dir: Destination directory (e.g., ~/.claude/skills/)

    Returns:
        List of copied file names
    """
    src_dir = get_bundled_skills_dir()
    skill_src = src_dir / "codecontext" / "SKILL.md"

    if not skill_src.exists():
        raise InstallError(f"SKILL.md not found at {skill_src}")

    dest_skill_dir = dest_dir / "codecontext"
    dest_skill_dir.mkdir(parents=True, exist_ok=True)

    shutil.copy2(skill_src, dest_skill_dir / "SKILL.md")
    return ["codecontext/SKILL.md"]


# ============== SETTINGS.JSON OPERATIONS ==============

def backup_settings_json(settings_path: Path) -> Optional[Path]:
    """
    Create timestamped backup of settings.json.

    Args:
        settings_path: Path to settings.json

    Returns:
        Path to backup file, or None if original doesn't exist
    """
    if not settings_path.exists():
        return None

    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_path = settings_path.parent / f"settings.backup.{timestamp}.json"
    shutil.copy2(settings_path, backup_path)
    return backup_path


def build_hook_config(hooks_dir: Path, use_relative: bool = False) -> Dict[str, List[Dict]]:
    """
    Build hook configuration for settings.json.

    Args:
        hooks_dir: Directory containing hook files
        use_relative: If True, use relative paths (for project mode)

    Returns:
        Hook configuration dictionary for settings.json
    """
    if use_relative:
        # Project mode: relative path from project root
        base_path = ".claude/hooks/codecontext"
    else:
        # Global mode: absolute path (tilde doesn't expand in JSON!)
        base_path = str(hooks_dir).replace("\\", "/")

    # Define hook mappings
    hooks = {
        "UserPromptSubmit": "user_prompt_submit.py",
        "Stop": "stop.py",
        "SessionStart": "session_start.py",
        "SessionEnd": "session_end.py",
    }

    config = {}
    for hook_type, filename in hooks.items():
        command = f'python "{base_path}/{filename}"'
        entry = {
            "matcher": "",
            "hooks": [{
                "type": "command",
                "command": command,
            }]
        }
        # Add timeout for session hooks (can take longer)
        if hook_type in ("SessionStart", "SessionEnd"):
            entry["hooks"][0]["timeout"] = 300

        config[hook_type] = [entry]

    return config


def is_codecontext_hook(hook_entry: Dict) -> bool:
    """Check if a hook entry belongs to CodeContext."""
    hooks_list = hook_entry.get("hooks", [])
    for hook in hooks_list:
        command = hook.get("command", "").lower()
        if "codecontext" in command:
            return True
    return False


def merge_settings(existing: Dict, hooks_config: Dict) -> Dict:
    """
    Merge new hook config into existing settings.

    Strategy:
    1. Remove existing CodeContext hooks (by detecting "codecontext" in command)
    2. Add new CodeContext hooks
    3. Preserve ALL other hooks unchanged

    Args:
        existing: Current settings.json content
        hooks_config: New hook configuration to add

    Returns:
        Merged settings dictionary
    """
    result = existing.copy()

    if "hooks" not in result:
        result["hooks"] = {}

    for hook_type, new_entries in hooks_config.items():
        if hook_type not in result["hooks"]:
            result["hooks"][hook_type] = []

        # Filter out existing CodeContext hooks
        filtered = [
            entry for entry in result["hooks"][hook_type]
            if not is_codecontext_hook(entry)
        ]

        # Add new CodeContext hooks
        result["hooks"][hook_type] = filtered + new_entries

    return result


def remove_codecontext_from_settings(settings_path: Path) -> bool:
    """
    Remove ONLY CodeContext hooks from settings.json.

    Args:
        settings_path: Path to settings.json

    Returns:
        True if file was modified, False if no changes needed
    """
    if not settings_path.exists():
        return False

    try:
        content = json.loads(settings_path.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, IOError):
        return False

    if "hooks" not in content:
        return False

    modified = False
    for hook_type in list(content["hooks"].keys()):
        original_count = len(content["hooks"][hook_type])
        content["hooks"][hook_type] = [
            entry for entry in content["hooks"][hook_type]
            if not is_codecontext_hook(entry)
        ]
        if len(content["hooks"][hook_type]) != original_count:
            modified = True

        # Remove empty hook type entries
        if not content["hooks"][hook_type]:
            del content["hooks"][hook_type]

    # Remove hooks key if empty
    if not content["hooks"]:
        del content["hooks"]

    if modified:
        # Backup before modifying
        backup_settings_json(settings_path)
        settings_path.write_text(json.dumps(content, indent=2), encoding="utf-8")

    return modified


def write_settings(settings_path: Path, content: Dict) -> None:
    """Write settings.json with proper formatting."""
    settings_path.parent.mkdir(parents=True, exist_ok=True)
    settings_path.write_text(json.dumps(content, indent=2), encoding="utf-8")


# ============== MARKER FILE ==============

def create_install_marker(data_dir: Path, mode: str, project_path: Optional[Path] = None) -> Path:
    """
    Create marker file for runtime mode detection.

    Args:
        data_dir: Data directory (~/.codecontext or ./.codecontext)
        mode: "global" or "project"
        project_path: Project path (for project mode)

    Returns:
        Path to created marker file
    """
    marker_path = data_dir / ".codecontext_mode"
    data_dir.mkdir(parents=True, exist_ok=True)

    marker_data = {
        "mode": mode,
        "project_path": str(project_path) if project_path else None,
        "installed_at": datetime.now().isoformat(),
        "version": __version__,
        "platform": platform.system(),
    }

    marker_path.write_text(json.dumps(marker_data, indent=2), encoding="utf-8")
    return marker_path


def read_install_marker(data_dir: Path) -> Optional[Dict]:
    """Read marker file if it exists."""
    marker_path = data_dir / ".codecontext_mode"
    if not marker_path.exists():
        return None

    try:
        return json.loads(marker_path.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, IOError):
        return None


def delete_install_marker(data_dir: Path) -> bool:
    """Delete marker file if it exists."""
    marker_path = data_dir / ".codecontext_mode"
    if marker_path.exists():
        marker_path.unlink()
        return True
    return False


# ============== MAIN INSTALLATION FUNCTIONS ==============

def install_global() -> Dict[str, Any]:
    """
    Install CodeContext globally for all projects.

    Installation locations:
    - Hooks: ~/.claude/hooks/codecontext/
    - Skills: ~/.claude/skills/codecontext/
    - Data: ~/.codecontext/
    - Settings: ~/.claude/settings.json

    Returns:
        Dictionary with installation results
    """
    home = Path.home()
    claude_dir = home / ".claude"
    hooks_dir = claude_dir / "hooks" / "codecontext"
    skills_dir = claude_dir / "skills"
    data_dir = home / ".codecontext"
    settings_path = claude_dir / "settings.json"

    results = {
        "mode": "global",
        "hooks_dir": str(hooks_dir),
        "skills_dir": str(skills_dir),
        "data_dir": str(data_dir),
        "settings_path": str(settings_path),
        "backup_path": None,
        "hooks_copied": [],
        "skills_copied": [],
    }

    # 1. Backup settings.json
    backup = backup_settings_json(settings_path)
    if backup:
        results["backup_path"] = str(backup)

    # 2. Copy hooks
    results["hooks_copied"] = copy_hooks(hooks_dir)

    # 3. Copy skills
    results["skills_copied"] = copy_skills(skills_dir)

    # 4. Create marker file
    create_install_marker(data_dir, "global")

    # 5. Update settings.json
    hook_config = build_hook_config(hooks_dir, use_relative=False)

    existing = {}
    if settings_path.exists():
        try:
            existing = json.loads(settings_path.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, IOError):
            pass

    merged = merge_settings(existing, hook_config)
    write_settings(settings_path, merged)

    return results


def install_project(project_path: Path) -> Dict[str, Any]:
    """
    Install CodeContext for a specific project.

    Installation locations (relative to project_path):
    - Hooks: ./.claude/hooks/codecontext/
    - Skills: ./.claude/skills/codecontext/
    - Data: ./.codecontext/
    - Settings: ./.claude/settings.json

    Args:
        project_path: Path to the project directory

    Returns:
        Dictionary with installation results
    """
    project_path = project_path.resolve()
    claude_dir = project_path / ".claude"
    hooks_dir = claude_dir / "hooks" / "codecontext"
    skills_dir = claude_dir / "skills"
    data_dir = project_path / ".codecontext"
    settings_path = claude_dir / "settings.json"

    results = {
        "mode": "project",
        "project_path": str(project_path),
        "hooks_dir": str(hooks_dir),
        "skills_dir": str(skills_dir),
        "data_dir": str(data_dir),
        "settings_path": str(settings_path),
        "backup_path": None,
        "hooks_copied": [],
        "skills_copied": [],
    }

    # 1. Backup settings.json
    backup = backup_settings_json(settings_path)
    if backup:
        results["backup_path"] = str(backup)

    # 2. Copy hooks
    results["hooks_copied"] = copy_hooks(hooks_dir)

    # 3. Copy skills
    results["skills_copied"] = copy_skills(skills_dir)

    # 4. Create marker file
    create_install_marker(data_dir, "project", project_path)

    # 5. Update settings.json (use relative paths for project mode)
    hook_config = build_hook_config(hooks_dir, use_relative=True)

    existing = {}
    if settings_path.exists():
        try:
            existing = json.loads(settings_path.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, IOError):
            pass

    merged = merge_settings(existing, hook_config)
    write_settings(settings_path, merged)

    return results


# ============== UNINSTALLATION ==============

def uninstall_global(keep_data: bool = True) -> Dict[str, Any]:
    """
    Remove global CodeContext installation.

    Args:
        keep_data: If True, preserve ~/.codecontext/ data (default)

    Returns:
        Dictionary with uninstallation results
    """
    home = Path.home()
    claude_dir = home / ".claude"
    hooks_dir = claude_dir / "hooks" / "codecontext"
    skills_dir = claude_dir / "skills" / "codecontext"
    data_dir = home / ".codecontext"
    settings_path = claude_dir / "settings.json"

    results = {
        "mode": "global",
        "hooks_removed": False,
        "skills_removed": False,
        "settings_updated": False,
        "data_removed": False,
        "data_dir": str(data_dir),
    }

    # 1. Remove hooks directory
    if hooks_dir.exists():
        shutil.rmtree(hooks_dir)
        results["hooks_removed"] = True

    # 2. Remove skills directory
    if skills_dir.exists():
        shutil.rmtree(skills_dir)
        results["skills_removed"] = True

    # 3. Update settings.json (remove CodeContext entries)
    results["settings_updated"] = remove_codecontext_from_settings(settings_path)

    # 4. Delete marker file
    delete_install_marker(data_dir)

    # 5. Optionally remove data directory
    if not keep_data and data_dir.exists():
        shutil.rmtree(data_dir)
        results["data_removed"] = True

    return results


def uninstall_project(project_path: Path, keep_data: bool = True) -> Dict[str, Any]:
    """
    Remove project-local CodeContext installation.

    Args:
        project_path: Path to the project directory
        keep_data: If True, preserve ./.codecontext/ data (default)

    Returns:
        Dictionary with uninstallation results
    """
    project_path = project_path.resolve()
    claude_dir = project_path / ".claude"
    hooks_dir = claude_dir / "hooks" / "codecontext"
    skills_dir = claude_dir / "skills" / "codecontext"
    data_dir = project_path / ".codecontext"
    settings_path = claude_dir / "settings.json"

    results = {
        "mode": "project",
        "project_path": str(project_path),
        "hooks_removed": False,
        "skills_removed": False,
        "settings_updated": False,
        "data_removed": False,
        "data_dir": str(data_dir),
    }

    # 1. Remove hooks directory
    if hooks_dir.exists():
        shutil.rmtree(hooks_dir)
        results["hooks_removed"] = True

    # 2. Remove skills directory
    if skills_dir.exists():
        shutil.rmtree(skills_dir)
        results["skills_removed"] = True

    # 3. Update settings.json (remove CodeContext entries)
    results["settings_updated"] = remove_codecontext_from_settings(settings_path)

    # 4. Delete marker file
    delete_install_marker(data_dir)

    # 5. Optionally remove data directory
    if not keep_data and data_dir.exists():
        shutil.rmtree(data_dir)
        results["data_removed"] = True

    return results


# ============== DETECTION ==============

def detect_existing_installation() -> Dict[str, Any]:
    """
    Detect any existing CodeContext installation.

    Returns:
        Dictionary with detection results:
        - has_global: True if global installation exists
        - has_project: True if project installation exists in cwd
        - global_data_exists: True if ~/.codecontext/ has data
        - project_data_exists: True if ./.codecontext/ has data
    """
    home = Path.home()
    cwd = Path.cwd()

    global_marker = home / ".codecontext" / ".codecontext_mode"
    project_marker = cwd / ".codecontext" / ".codecontext_mode"
    global_data = home / ".codecontext"
    project_data = cwd / ".codecontext"

    return {
        "has_global": global_marker.exists(),
        "has_project": project_marker.exists(),
        "global_data_exists": global_data.exists() and any(global_data.iterdir()),
        "project_data_exists": project_data.exists() and any(project_data.iterdir()),
        "global_marker": read_install_marker(home / ".codecontext"),
        "project_marker": read_install_marker(cwd / ".codecontext"),
    }
