"""
Project directory detection and management.

A valid iq-sdk project is identified by:
1. A .iq-project.yml marker file in the project root
2. A config/ directory
3. A tools/ directory

Projects are tracked in ~/.config/iq-sdk/projects (one path per line).
The first line is the active project.
"""

from __future__ import annotations

import os
from pathlib import Path

import click
import yaml
from rich.console import Console

from iq_sdk import __version__

# Use stderr for user messages so stdout is clean for eval
console = Console(stderr=True)


# Marker file name
PROJECT_MARKER = ".iq-project.yml"

# Required directories
REQUIRED_DIRS = ["config", "tools"]

# Registry file location
REGISTRY_DIR = Path.home() / ".config" / "iq-sdk"
REGISTRY_FILE = REGISTRY_DIR / "projects"
ACTIVE_FILE = REGISTRY_DIR / "active"

# Special marker for "no active project"
NO_ACTIVE = "---"

# Track which variables were loaded from .env (for display purposes)
_dotenv_loaded_vars: set[str] = set()


def get_dotenv_loaded_vars() -> set[str]:
    """Get the set of variable names that were loaded from .env."""
    return _dotenv_loaded_vars.copy()


def was_loaded_from_dotenv(var_name: str) -> bool:
    """Check if a variable was loaded from the project's .env file."""
    return var_name in _dotenv_loaded_vars


def load_project_dotenv() -> bool:
    """
    Load .env file from the active project or cwd.

    Only sets variables that are not already set in the environment.
    Tracks which variables were loaded for display purposes.

    Search order:
    1. Active project from registry
    2. Valid project in current directory or parents

    Returns:
        True if .env was loaded, False otherwise
    """
    from dotenv import dotenv_values

    global _dotenv_loaded_vars
    _dotenv_loaded_vars = set()

    # Find project directory
    project_dir = get_active_project()

    # If no active project, try to find one from cwd
    if not project_dir:
        project_dir = _find_project_in_cwd()

    if not project_dir:
        return False

    # Check for .env file
    env_file = project_dir / ".env"
    if not env_file.exists():
        return False

    # Load values from .env
    try:
        env_values = dotenv_values(env_file)
    except Exception:
        return False

    # Set only variables that are not already set
    for key, value in env_values.items():
        if key and value is not None and key not in os.environ:
            os.environ[key] = value
            _dotenv_loaded_vars.add(key)

    return True


# ============================================================================
# PROJECT REGISTRY
# ============================================================================


def get_registry_path() -> Path:
    """Get path to the projects registry file."""
    return REGISTRY_FILE


def get_active_path() -> Path:
    """Get path to the active project file."""
    return ACTIVE_FILE


def read_registry() -> list[Path]:
    """
    Read the projects registry.

    Returns list of all known project paths.
    Invalid/non-existent projects are automatically removed.
    """
    registry_path = get_registry_path()
    if not registry_path.exists():
        return []

    projects = []
    try:
        with open(registry_path) as f:
            for line in f:
                line = line.strip()
                if line and line != NO_ACTIVE:
                    path = Path(line)
                    # Only include valid, existing projects
                    if is_valid_project(path):
                        projects.append(path)
    except OSError:
        return []

    return projects


def write_registry(projects: list[Path]) -> None:
    """
    Write the projects registry.

    Validates all projects before writing (removes invalid ones).
    """
    # Filter to only valid projects
    valid_projects = [p for p in projects if is_valid_project(p)]

    # Remove duplicates while preserving order
    seen = set()
    unique_projects = []
    for p in valid_projects:
        resolved = p.resolve()
        if resolved not in seen:
            seen.add(resolved)
            unique_projects.append(resolved)

    # Ensure directory exists
    registry_path = get_registry_path()
    registry_path.parent.mkdir(parents=True, exist_ok=True)

    # Write registry
    with open(registry_path, "w") as f:
        for project in unique_projects:
            f.write(f"{project}\n")


def get_active_project() -> Path | None:
    """Get the currently active project."""
    active_path = get_active_path()
    if not active_path.exists():
        return None

    try:
        content = active_path.read_text().strip()
        if content and content != NO_ACTIVE:
            path = Path(content)
            if is_valid_project(path):
                return path
    except OSError:
        pass

    return None


def set_active_project(project_path: Path) -> None:
    """
    Set a project as active.

    Also adds it to the registry if not already there.
    """
    resolved = project_path.resolve()

    # Ensure directory exists
    active_path = get_active_path()
    active_path.parent.mkdir(parents=True, exist_ok=True)

    # Write active project
    active_path.write_text(str(resolved))

    # Also add to registry (if not already there)
    projects = read_registry()
    if not any(p.resolve() == resolved for p in projects):
        projects.insert(0, resolved)
        write_registry(projects)
    else:
        # Move to front of registry
        projects = [p for p in projects if p.resolve() != resolved]
        projects.insert(0, resolved)
        write_registry(projects)


def clear_active_project() -> None:
    """Deactivate current project (keeps registry for history)."""
    active_path = get_active_path()
    active_path.parent.mkdir(parents=True, exist_ok=True)
    active_path.write_text(NO_ACTIVE)


def register_project(project_path: Path) -> None:
    """
    Register a new project (adds to registry without making it active).

    If already registered, does nothing.
    """
    resolved = project_path.resolve()
    projects = read_registry()

    # Check if already registered
    if any(p.resolve() == resolved for p in projects):
        return

    # Add to end
    projects.append(resolved)
    write_registry(projects)


def find_project_by_name(name: str) -> Path | None:
    """
    Find a registered project by partial name match.

    Matches against the directory name of registered projects.
    Returns the first match, or None if not found.
    """
    projects = read_registry()

    # Exact match first
    for project in projects:
        if project.name == name:
            return project

    # Partial match
    for project in projects:
        if name in project.name:
            return project

    return None


def get_recent_projects(count: int = 3) -> list[Path]:
    """Get recently used projects (excluding currently active)."""
    projects = read_registry()
    active = get_active_project()

    # Filter out active project
    if active:
        projects = [p for p in projects if p.resolve() != active.resolve()]

    return projects[:count]


def get_project_marker_content() -> dict:
    """Get content for the project marker file."""
    return {
        "iq_sdk_version": __version__,
        "marker": "iq-sdk-project",
    }


def create_project_marker(project_path: Path) -> None:
    """Create the project marker file."""
    marker_path = project_path / PROJECT_MARKER
    content = get_project_marker_content()

    with open(marker_path, "w") as f:
        yaml.dump(content, f, default_flow_style=False)


def is_valid_project(path: Path) -> bool:
    """
    Check if a directory is a valid iq-sdk project.

    Requirements:
    1. .iq-project.yml marker file exists
    2. config/ directory exists
    3. tools/ directory exists
    """
    if not path.is_dir():
        return False

    # Check marker file
    marker_path = path / PROJECT_MARKER
    if not marker_path.exists():
        return False

    # Check required directories
    for dir_name in REQUIRED_DIRS:
        if not (path / dir_name).is_dir():
            return False

    return True


def find_project_root(start_path: Path | None = None, max_levels: int = 3) -> Path | None:
    """
    Find the project root directory.

    Search order:
    1. Active project from registry
    2. start_path (or current directory)
    3. Parent directories (up to max_levels)

    Args:
        start_path: Starting directory for search. Defaults to cwd.
        max_levels: Maximum parent levels to search

    Returns:
        Path to project root, or None if not found
    """
    # Check registry for active project first
    active = get_active_project()
    if active and is_valid_project(active):
        return active

    # Start from specified path or current directory
    current = start_path or Path.cwd()
    current = current.resolve()

    # Check current and parent directories
    for _ in range(max_levels + 1):
        if is_valid_project(current):
            return current

        parent = current.parent
        if parent == current:
            # Reached filesystem root
            break
        current = parent

    return None


def get_project_dir() -> Path:
    """
    Get the active project directory.

    Raises click.ClickException if no project found.
    """
    project = find_project_root()
    if project is None:
        raise click.ClickException(
            "No iq-sdk project found.\n"
            "Run 'iq init' to create a new project, or 'iq use .' to set project directory."
        )
    return project


def get_project_info(project_path: Path) -> dict:
    """Read project marker file and return info."""
    marker_path = project_path / PROJECT_MARKER
    if marker_path.exists():
        with open(marker_path) as f:
            return yaml.safe_load(f) or {}
    return {}


# ============================================================================
# CLI COMMAND
# ============================================================================


@click.command("use")
@click.argument("path", required=False, type=click.Path())
@click.option("--off", is_flag=True, help="Deactivate project")
def use(path: str | None, off: bool) -> None:
    """
    Set or show the active project directory.

    \b
    Usage:
      iq use                # Show current project status
      iq use .              # Activate project in current/parent dir
      iq use /path          # Activate specific project
      iq use myproject      # Activate known project by name
      iq use --off          # Deactivate project
    """
    # --off: deactivate current project (keeps registry for history)
    if off:
        clear_active_project()
        console.print("[dim]Project deactivated[/dim]")
        return

    # No path: show status
    if path is None:
        active = get_active_project()
        if active:
            info = get_project_info(active)
            console.print(f"[green]Active project:[/green] {active}")
            console.print(f"  SDK version: {info.get('iq_sdk_version', 'unknown')}")
        else:
            # Try to find project from cwd
            project = _find_project_in_cwd()
            if project:
                console.print("[dim]No active project[/dim]")
                console.print(f"[dim]Would use:[/dim] {project}")
                console.print("\n[dim]To activate run: iq use .[/dim]")
            else:
                console.print("[dim]No active project[/dim]")
                console.print("\nRun 'iq init' to create a new project")

        # Show recent projects
        recent = get_recent_projects(3)
        if recent:
            console.print("\n[dim]Recent projects:[/dim]")
            for i, proj in enumerate(recent, 1):
                console.print(f"  {i}. {proj.name} [dim]({proj})[/dim]")
        return

    # Path provided: activate project
    # First, check if it's a known project name
    if not os.path.exists(path) and not path.startswith("/") and not path.startswith("."):
        found = find_project_by_name(path)
        if found:
            project_root = found
        else:
            console.print(f"[red]Unknown project: {path}[/red]")
            console.print("\n[dim]Known projects:[/dim]")
            for proj in read_registry()[:5]:
                console.print(f"  {proj.name} [dim]({proj})[/dim]")
            raise SystemExit(1)
    else:
        # It's a path - resolve it
        start_path = Path(path).resolve()

        if not start_path.exists():
            console.print(f"[red]Path does not exist: {start_path}[/red]")
            raise SystemExit(1)

        found_project = _find_project_in_path(start_path)

        if found_project is None:
            console.print(
                f"[red]No iq-sdk project found in {start_path} or parent directories[/red]"
            )
            console.print("\nTo create a new project: iq init .")
            raise SystemExit(1)

        project_root = found_project

    # Set as active project
    set_active_project(project_root)

    # Get project info
    info = get_project_info(project_root)
    sdk_version = info.get("iq_sdk_version", "unknown")

    console.print(f"[green]Activated:[/green] {project_root}")
    console.print(f"[dim]SDK version: {sdk_version}[/dim]")


def _find_project_in_cwd(max_levels: int = 3) -> Path | None:
    """Find project in current directory or parents (without registry)."""
    current = Path.cwd().resolve()

    for _ in range(max_levels + 1):
        if is_valid_project(current):
            return current

        parent = current.parent
        if parent == current:
            break
        current = parent

    return None


def _find_project_in_path(start_path: Path, max_levels: int = 3) -> Path | None:
    """Find project in given path or parents (without registry)."""
    current = start_path.resolve()

    for _ in range(max_levels + 1):
        if is_valid_project(current):
            return current

        parent = current.parent
        if parent == current:
            break
        current = parent

    return None


@click.command("info")
def project_info() -> None:
    """Show current project information."""
    try:
        project = get_project_dir()
    except click.ClickException:
        console.print("[red]No active project[/red]")
        console.print("\nRun 'iq init' to create a new project")
        console.print("Or 'eval $(iq use .)' to activate an existing project")
        return

    info = get_project_info(project)

    console.print("[bold]Project Information[/bold]\n")
    console.print(f"  Path: {project}")
    console.print(f"  SDK Version: {info.get('iq_sdk_version', 'unknown')}")

    # Check directories
    console.print("\n[bold]Directories[/bold]")
    for dir_name in REQUIRED_DIRS:
        dir_path = project / dir_name
        exists = dir_path.is_dir()
        status = "[green]✓[/green]" if exists else "[red]✗[/red]"
        console.print(f"  {status} {dir_name}/")

    # Check config files
    config_dir = project / "config"
    if config_dir.is_dir():
        configs = list(config_dir.glob("*.yml")) + list(config_dir.glob("*.yaml"))
        if configs:
            console.print("\n[bold]Config Files[/bold]")
            for cfg in sorted(configs):
                console.print(f"  {cfg.name}")

    # Show IQ_PROJECT_DIR status
    env_dir = os.environ.get("IQ_PROJECT_DIR")
    console.print("\n[bold]Environment[/bold]")
    if env_dir:
        console.print(f"  IQ_PROJECT_DIR: {env_dir}")
    else:
        console.print("  IQ_PROJECT_DIR: [dim]not set (using cwd)[/dim]")
