"""
iq tool commands - Manage tools.
"""

from __future__ import annotations

import asyncio
import hashlib
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any

import click
from rich.prompt import Confirm, Prompt
from rich.table import Table

from iq_sdk.cli import console
from iq_sdk.config import SiteConfig, load_settings
from iq_sdk.properties import ExposureCategory

if TYPE_CHECKING:
    from iq_sdk.client import APIResult, RemoteTool, SiteStatus

# ============================================================================
# DATA STRUCTURES
# ============================================================================


@dataclass
class ToolVersion:
    """Represents a tool version."""

    major: int
    minor: int

    def __str__(self) -> str:
        return f"{self.major}.{self.minor}"

    @property
    def file_suffix(self) -> str:
        return f"{self.major}_{self.minor}"

    @classmethod
    def parse(cls, s: str) -> ToolVersion:
        """Parse version from string like '1.0' or '1_0'."""
        s = s.replace("_", ".")
        parts = s.split(".")
        return cls(major=int(parts[0]), minor=int(parts[1]) if len(parts) > 1 else 0)


@dataclass
class PropertyDef:
    """Property definition collected from user input."""

    name: str
    label: str
    data_type: str
    required: bool
    default: Any
    python_type: str = ""

    def __post_init__(self):
        # Map data_type to Python type string
        type_mapping = {
            "str": "str",
            "int": "int",
            "float": "float",
            "bool": "bool",
            "list": "list",
            "dict": "dict",
            "date": "date",
            "datetime": "datetime",
            "file": "str",
            "image": "str",
            "json": "dict",
        }
        self.python_type = type_mapping.get(self.data_type.lower(), "str")


@dataclass
class ToolContext:
    """Context for tool template rendering."""

    name: str
    name_snake: str
    class_name: str
    class_name_base: str
    tool_file_base: str
    version: ToolVersion
    major_version: int
    minor_version: int
    general_properties: list[PropertyDef] = field(default_factory=list)
    input_properties: list[PropertyDef] = field(default_factory=list)
    output_properties: list[PropertyDef] = field(default_factory=list)
    versions: list[ToolVersion] = field(default_factory=list)
    is_minor_update: bool = False
    shared_properties: dict[str, list[str]] = field(default_factory=dict)
    copy_properties_from: Path | None = None  # Path to copy properties.py from

    def to_dict(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "name_snake": self.name_snake,
            "class_name": self.class_name,
            "class_name_base": self.class_name_base,
            "tool_file_base": self.tool_file_base,
            "version": str(self.version),
            "major_version": self.major_version,
            "minor_version": self.minor_version,
            "general_properties": self.general_properties,
            "input_properties": self.input_properties,
            "output_properties": self.output_properties,
            "versions": self.versions,
            "shared_properties": self.shared_properties,
        }


# ============================================================================
# HELPER FUNCTIONS
# ============================================================================


def to_snake_case(name: str) -> str:
    """Convert name to snake_case."""
    # Replace hyphens with underscores
    name = name.replace("-", "_")
    # Convert CamelCase to snake_case
    name = re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
    # Clean up multiple underscores
    name = re.sub(r"_+", "_", name)
    return name.strip("_")


def to_class_name(name: str) -> str:
    """Convert name to PascalCase class name."""
    # Split by underscores and hyphens
    parts = re.split(r"[-_]", name)
    return "".join(word.title() for word in parts if word)


def get_tool_dir_name(name: str, major_version: int) -> str:
    """Get tool directory name with major version suffix."""
    snake = to_snake_case(name)
    return f"{snake}_{major_version}"


def get_tool_file_name(name: str, version: ToolVersion) -> str:
    """Get tool file name with full version suffix."""
    snake = to_snake_case(name)
    return f"{snake}_{version.file_suffix}.py"


def find_existing_tool(tools_path: Path, name: str) -> tuple[Path | None, list[ToolVersion]]:
    """
    Find existing tool directories and versions.

    Returns:
        Tuple of (latest_dir, list_of_versions)
    """
    snake = to_snake_case(name)
    pattern = re.compile(rf"^{re.escape(snake)}_(\d+)$")

    versions: list[ToolVersion] = []
    latest_dir: Path | None = None
    latest_major = 0

    if not tools_path.exists():
        return None, []

    for d in tools_path.iterdir():
        if not d.is_dir():
            continue
        match = pattern.match(d.name)
        if match:
            major = int(match.group(1))

            # Find all .py files with version suffix in this dir
            file_pattern = re.compile(rf"^{re.escape(snake)}_(\d+)_(\d+)\.py$")
            for f in d.iterdir():
                if f.is_file():
                    file_match = file_pattern.match(f.name)
                    if file_match:
                        v = ToolVersion(
                            major=int(file_match.group(1)),
                            minor=int(file_match.group(2)),
                        )
                        versions.append(v)

            if major > latest_major:
                latest_major = major
                latest_dir = d

    # Sort versions
    versions.sort(key=lambda v: (v.major, v.minor))

    return latest_dir, versions


def collect_property_name(category_name: str) -> str | None:
    """
    Interactively collect just the property name from user.

    Returns:
        Property name (snake_case) or None if user wants to skip
    """
    console.print(f"\n[cyan]Add property for {category_name}:[/cyan]")

    name = Prompt.ask(
        "  Property name (or N to skip)",
        default="N",
    ).strip()

    if name.upper() == "N" or not name:
        return None

    # Validate and convert to snake_case
    return to_snake_case(name)


def collect_property_details(name: str) -> PropertyDef:
    """
    Collect remaining property details after name is known.

    Args:
        name: The property name (already validated)

    Returns:
        Complete PropertyDef
    """
    label = Prompt.ask(
        "  Label",
        default=name.replace("_", " ").title(),
    )

    data_type = Prompt.ask(
        "  Data type",
        choices=[
            "str",
            "int",
            "float",
            "bool",
            "list",
            "dict",
            "date",
            "datetime",
            "file",
            "image",
            "json",
        ],
        default="str",
    )

    required = Confirm.ask("  Required?", default=True)

    default: Any = None
    if not required:
        default_str = Prompt.ask(
            "  Default value",
            default="None",
        )
        if default_str.lower() != "none":
            # Try to parse the default value
            if data_type == "int":
                try:
                    default = int(default_str)
                except ValueError:
                    default = None
            elif data_type == "float":
                try:
                    default = float(default_str)
                except ValueError:
                    default = None
            elif data_type == "bool":
                default = default_str.lower() in ("true", "1", "yes")
            else:
                default = default_str

    return PropertyDef(
        name=name,
        label=label,
        data_type=data_type,
        required=required,
        default=default,
    )


def collect_properties_for_category(
    category: ExposureCategory,
    existing_names: set[str],
) -> tuple[list[PropertyDef], set[str]]:
    """
    Collect all properties for a category.

    If a property name already exists in existing_names, it's noted
    as a shared property (added to multiple categories) and skipped
    WITHOUT asking for details.

    Returns:
        Tuple of (new_properties, shared_property_names)
    """
    props: list[PropertyDef] = []
    shared_names: set[str] = set()

    console.print(f"\n[bold]{category.info.icon} {category.info.name}[/bold]")
    console.print(f"[dim]{category.info.description}[/dim]")

    while True:
        # First, just get the name
        name = collect_property_name(category.info.name)
        if name is None:
            break

        # Check if property already exists (duplicate) BEFORE asking for details
        if name in existing_names:
            console.print(
                f"  [green]✓ Added known property '{name}' to {category.info.name}[/green]"
            )
            shared_names.add(name)
            continue

        # Only now collect the remaining details
        prop = collect_property_details(name)
        props.append(prop)
        existing_names.add(name)
        console.print(f"  [green]✓ Added: {prop.name}[/green]")

    return props, shared_names


def render_template_file(template_path: Path, context: dict) -> str:
    """Render a Jinja2 template file with custom filters."""
    from jinja2 import BaseLoader, Environment

    content = template_path.read_text()

    # Mapping from short data type names to DataType enum names
    datatype_mapping = {
        "str": "STRING",
        "string": "STRING",
        "int": "INTEGER",
        "integer": "INTEGER",
        "float": "FLOAT",
        "bool": "BOOLEAN",
        "boolean": "BOOLEAN",
        "list": "LIST",
        "dict": "DICT",
        "date": "DATE",
        "datetime": "DATETIME",
        "file": "FILE",
        "image": "IMAGE",
        "json": "JSON",
        "any": "ANY",
    }

    def datatype_enum(value: str) -> str:
        """Convert short data type name to DataType enum name."""
        return datatype_mapping.get(value.lower(), "STRING")

    # Create environment with custom filters
    env = Environment(loader=BaseLoader())
    env.filters["repr"] = repr  # Add Python's repr as a filter
    env.filters["datatype_enum"] = datatype_enum  # Map to DataType enum names

    template = env.from_string(content)
    return template.render(**context)


def create_tool_files(
    tools_path: Path,
    ctx: ToolContext,
    templates_dir: Path,
) -> Path:
    """
    Create tool files from templates.

    Returns:
        Path to the created tool directory
    """
    tool_dir = tools_path / get_tool_dir_name(ctx.name, ctx.major_version)
    tool_dir.mkdir(parents=True, exist_ok=True)

    template_context = ctx.to_dict()

    # Create or update __init__.py
    init_template = templates_dir / "tool" / "__init__.py.j2"
    init_content = render_template_file(init_template, template_context)
    (tool_dir / "__init__.py").write_text(init_content)

    # Create properties.py only for new tools/major versions (not minor updates)
    if not ctx.is_minor_update:
        if ctx.copy_properties_from and ctx.copy_properties_from.exists():
            # Copy properties from previous major version
            import shutil

            shutil.copy(ctx.copy_properties_from, tool_dir / "properties.py")
        else:
            # Generate new properties from template
            props_template = templates_dir / "tool" / "properties.py.j2"
            props_content = render_template_file(props_template, template_context)
            (tool_dir / "properties.py").write_text(props_content)

    # Create the tool file
    tool_template = templates_dir / "tool" / "tool.py.j2"
    tool_content = render_template_file(tool_template, template_context)
    tool_file = tool_dir / get_tool_file_name(ctx.name, ctx.version)
    tool_file.write_text(tool_content)

    return tool_dir


# ============================================================================
# CLI COMMANDS
# ============================================================================


@click.group(context_settings={"help_option_names": ["-h", "--help"]})
def tool() -> None:
    """Manage tools."""
    pass


@tool.command("add")
@click.argument("name")
@click.option(
    "--output", "-o", "tools_dir", default=None, help="Tools directory (default: from settings)"
)
@click.option("--no-interactive", "-n", is_flag=True, help="Skip interactive property input")
@click.pass_context
def tool_add(
    click_ctx: click.Context, name: str, tools_dir: str | None, no_interactive: bool
) -> None:
    """
    Create a new tool.

    For new tools, creates version 1.0. For existing tools, prompts
    for major (breaking) or minor (compatible) version update.
    """
    from iq_sdk.cli import TEMPLATES_DIR, require_project

    # Require valid project directory
    require_project(click_ctx)

    # Get tools path from settings or CLI override
    config_file = click_ctx.obj.get("config_file") if click_ctx.obj else None
    settings = load_settings(config_file)
    tools_path = Path(tools_dir) if tools_dir else settings.get_tools_path()
    snake_name = to_snake_case(name)
    class_name_base = to_class_name(name)

    # Check for existing tool
    existing_dir, existing_versions = find_existing_tool(tools_path, name)

    if existing_versions:
        # Tool exists - show versions and ask for action
        latest = existing_versions[-1]
        console.print(f"\n[yellow]Tool '{name}' already exists[/yellow]")

        # Show existing versions grouped by major
        versions_by_major: dict[int, list[ToolVersion]] = {}
        for v in existing_versions:
            versions_by_major.setdefault(v.major, []).append(v)

        console.print("\n[bold]Existing versions:[/bold]")
        for major, minors in sorted(versions_by_major.items()):
            minor_strs = [f"{major}.{v.minor}" for v in sorted(minors, key=lambda x: x.minor)]
            console.print(f"  v{major}.x: {', '.join(minor_strs)}")

        console.print(f"\n[dim]Latest: v{latest}[/dim]")
        console.print()

        version_type = Prompt.ask(
            "Create (M)ajor, mi(N)or, or (C)ancel",
            choices=["m", "n", "c"],
            default="n",
        )

        if version_type == "c":
            console.print("[dim]Cancelled[/dim]")
            return

        if version_type == "m":
            # Major version - new directory, copies properties from previous major
            version = ToolVersion(major=latest.major + 1, minor=0)
            is_minor = False
            is_major_upgrade = True
            console.print(f"[cyan]Creating major version {version}[/cyan]")
        else:
            # Minor version - same directory, same interface
            version = ToolVersion(major=latest.major, minor=latest.minor + 1)
            is_minor = True
            is_major_upgrade = False
            console.print(f"[cyan]Creating minor version {version} (same interface)[/cyan]")
    else:
        # New tool
        version = ToolVersion(major=1, minor=0)
        is_minor = False
        is_major_upgrade = False
        console.print(f"\n[cyan]Creating new tool '{name}' v{version}[/cyan]")

    # Build class name with version
    class_name = f"{class_name_base}_{version.file_suffix}"

    # Create context
    ctx = ToolContext(
        name=name,
        name_snake=snake_name,
        class_name=class_name,
        class_name_base=class_name_base,
        tool_file_base=snake_name,
        version=version,
        major_version=version.major,
        minor_version=version.minor,
        versions=[v for v in existing_versions if v.major == version.major] + [version],
        is_minor_update=is_minor,
    )

    # Handle properties based on version type
    shared_property_names: dict[str, list[str]] = {}  # name -> list of categories
    copy_properties_from: Path | None = None

    if is_major_upgrade:
        # Major upgrade: copy properties from previous major version
        prev_major_dir = tools_path / get_tool_dir_name(name, latest.major)
        prev_props_file = prev_major_dir / "properties.py"
        if prev_props_file.exists():
            copy_properties_from = prev_props_file
            console.print(f"[dim]Copying properties from v{latest.major}.x[/dim]")
    elif not is_minor and not no_interactive:
        # New tool: collect properties interactively
        console.print("\n[bold]Define tool properties[/bold]")
        console.print(
            "[dim]Properties define the tool's interface. Press Enter/N to skip a category.[/dim]"
        )
        console.print(
            "[dim]If you enter a property name that already exists, it will be shared across categories.[/dim]"
        )

        existing_names: set[str] = set()

        ctx.general_properties, general_shared = collect_properties_for_category(
            ExposureCategory.GENERAL, existing_names
        )
        for prop_name in general_shared:
            shared_property_names.setdefault(prop_name, ["GENERAL"]).append("GENERAL")

        ctx.input_properties, input_shared = collect_properties_for_category(
            ExposureCategory.INPUT, existing_names
        )
        for prop_name in input_shared:
            shared_property_names.setdefault(prop_name, []).append("INPUT")

        ctx.output_properties, output_shared = collect_properties_for_category(
            ExposureCategory.OUTPUT, existing_names
        )
        for prop_name in output_shared:
            shared_property_names.setdefault(prop_name, []).append("OUTPUT")

        # Store shared properties info in context for template
        ctx.shared_properties = shared_property_names

    # Store copy source in context
    ctx.copy_properties_from = copy_properties_from

    # Create files
    tool_dir = create_tool_files(tools_path, ctx, TEMPLATES_DIR)
    tool_file = get_tool_file_name(name, version)

    console.print(f"\n[bold green]✓ Tool '{name}' v{version} created[/bold green]")
    console.print(f"  Directory: {tool_dir}")
    console.print(f"  Tool file: {tool_dir / tool_file}")

    if not is_minor:
        console.print(f"  Properties: {tool_dir / 'properties.py'}")
    else:
        console.print(
            f"\n[dim]Minor update uses existing properties.py from v{version.major}.0[/dim]"
        )

    console.print("\n[dim]Edit the tool file to implement your logic.[/dim]")


@dataclass
class LocalToolInfo:
    """
    Information about a local tool.

    Naming conventions:
    - name: Base name (e.g. "my_tool")
    - version: ToolVersion object (e.g. 1.0)
    - dir_name: Directory name (e.g. "my_tool_1")
    - file_name: File name without extension (e.g. "my_tool_1_0")
    - module_name: Python module path (e.g. "tools.my_tool_1.my_tool_1_0")
    - class_name: Expected class name (e.g. "MyTool_1_0")
    - full_name: Name for host communication (e.g. "demo.my_tool_1_0")
    """

    name: str
    version: ToolVersion
    path: Path  # Path to the tool file (not directory)
    digest: str
    properties_json: str
    tool_class: type | None = None  # Loaded tool class (set after import)

    @property
    def dir_name(self) -> str:
        """Directory name (e.g. 'my_tool_1')."""
        return f"{self.name}_{self.version.major}"

    @property
    def file_name(self) -> str:
        """File name without extension (e.g. 'my_tool_1_0')."""
        return f"{self.name}_{self.version.file_suffix}"

    @property
    def module_name(self) -> str:
        """Python module path (e.g. 'tools.my_tool_1.my_tool_1_0')."""
        return f"tools.{self.dir_name}.{self.file_name}"

    @property
    def class_name(self) -> str:
        """Expected class name (e.g. 'MyTool_1_0')."""
        # Convert snake_case to PascalCase and append version
        parts = self.name.split("_")
        pascal_name = "".join(p.capitalize() for p in parts)
        return f"{pascal_name}_{self.version.file_suffix}"

    def get_full_name(self, project_name: str) -> str:
        """
        Get full name for host communication.

        Args:
            project_name: Project name (e.g. "demo")

        Returns:
            Full name like "demo.my_tool.1.0"
        """
        return f"{project_name}.{self.name}.{self.version}"

    @property
    def display_version(self) -> str:
        """Version for display (e.g. '1.0')."""
        return str(self.version)


def _load_properties_json(props_file: Path) -> str:
    """
    Load properties.py and serialize the PropertyRegistry to JSON.

    Dynamically imports the properties module and extracts the 'properties'
    registry, then serializes it to JSON.

    Args:
        props_file: Path to properties.py file

    Returns:
        JSON string of serialized properties, or "{}" on error
    """
    import importlib.util
    import sys

    try:
        # Create a unique module name based on path
        module_name = f"_props_{props_file.parent.name}_{id(props_file)}"

        # Load the module dynamically
        spec = importlib.util.spec_from_file_location(module_name, props_file)
        if spec is None or spec.loader is None:
            return "{}"

        module = importlib.util.module_from_spec(spec)
        sys.modules[module_name] = module
        spec.loader.exec_module(module)

        # Look for 'properties' variable (PropertyRegistry instance)
        if hasattr(module, "properties"):
            registry = module.properties
            if hasattr(registry, "to_json"):
                return registry.to_json()

        # Fallback: return empty
        return "{}"

    except Exception as e:
        # Log error but don't fail - return empty properties
        console.print(f"[yellow]Warning: Could not load properties from {props_file}: {e}[/yellow]")
        return "{}"
    finally:
        # Clean up the temporary module
        if module_name in sys.modules:
            del sys.modules[module_name]


def get_local_tools(tools_path: Path) -> dict[str, list[LocalToolInfo]]:
    """
    Scan local tools directory and compute digests.

    Returns:
        Dict mapping base_name to list of LocalToolInfo
    """
    tools: dict[str, list[LocalToolInfo]] = {}

    if not tools_path.exists():
        return tools

    for tool_dir in tools_path.iterdir():
        if not tool_dir.is_dir() or tool_dir.name.startswith("_"):
            continue

        # Parse directory name for major version
        match = re.match(r"^(.+)_(\d+)$", tool_dir.name)
        if not match:
            continue

        base_name = match.group(1)

        # Load and serialize properties from properties.py
        properties_json = "{}"
        props_file = tool_dir / "properties.py"
        if props_file.exists():
            properties_json = _load_properties_json(props_file)

        # Find version files
        file_pattern = re.compile(rf"^{re.escape(base_name)}_(\d+)_(\d+)\.py$")
        for f in tool_dir.iterdir():
            if f.is_file():
                file_match = file_pattern.match(f.name)
                if file_match:
                    v = ToolVersion(
                        major=int(file_match.group(1)),
                        minor=int(file_match.group(2)),
                    )
                    # Compute digest
                    digest = compute_tool_digest(base_name, str(v), properties_json)

                    if base_name not in tools:
                        tools[base_name] = []
                    tools[base_name].append(
                        LocalToolInfo(
                            name=base_name,
                            version=v,
                            path=f,
                            digest=digest,
                            properties_json=properties_json,
                        )
                    )

    # Sort versions
    for base_name in tools:
        tools[base_name].sort(key=lambda x: (x.version.major, x.version.minor))

    return tools


def load_tool_class(info: LocalToolInfo) -> type | None:
    """
    Load the tool class for a LocalToolInfo.

    Imports the tool module and retrieves the registered class from the registry.
    Handles package structure for relative imports to work correctly.

    Args:
        info: LocalToolInfo with path to the tool file

    Returns:
        Tool class or None if loading failed
    """
    import importlib.util
    import sys
    import types

    from iq_sdk.tool_registry import find_tool_class, get_tool_class

    tool_file = info.path
    if not tool_file.exists():
        return None

    # Paths
    tool_dir = tool_file.parent  # e.g. tools/my_tool_1/
    tools_dir = tool_dir.parent  # e.g. tools/
    project_dir = tools_dir.parent  # e.g. demo/

    # Ensure project directory is in sys.path
    project_dir_str = str(project_dir)
    if project_dir_str not in sys.path:
        sys.path.insert(0, project_dir_str)

    # Module names
    tools_pkg = "tools"
    tool_pkg = f"tools.{info.dir_name}"  # e.g. tools.my_tool_1
    module_name = info.module_name  # e.g. tools.my_tool_1.my_tool_1_0

    try:
        # Create parent packages if they don't exist
        if tools_pkg not in sys.modules:
            tools_module = types.ModuleType(tools_pkg)
            tools_module.__path__ = [str(tools_dir)]
            sys.modules[tools_pkg] = tools_module

        if tool_pkg not in sys.modules:
            # Load the tool package (directory with __init__.py)
            init_file = tool_dir / "__init__.py"
            if init_file.exists():
                spec = importlib.util.spec_from_file_location(
                    tool_pkg, init_file, submodule_search_locations=[str(tool_dir)]
                )
                if spec and spec.loader:
                    pkg_module = importlib.util.module_from_spec(spec)
                    pkg_module.__path__ = [str(tool_dir)]
                    sys.modules[tool_pkg] = pkg_module
                    spec.loader.exec_module(pkg_module)
            else:
                # Create empty package
                pkg_module = types.ModuleType(tool_pkg)
                pkg_module.__path__ = [str(tool_dir)]
                sys.modules[tool_pkg] = pkg_module

        # Now load the actual tool module
        spec = importlib.util.spec_from_file_location(module_name, tool_file)
        if spec and spec.loader:
            module = importlib.util.module_from_spec(spec)
            sys.modules[module_name] = module
            spec.loader.exec_module(module)

            # Get the registered class from the registry
            tool_cls = get_tool_class(module_name)
            if tool_cls:
                return tool_cls

            # Fallback: search registry for matching file name
            return find_tool_class(info.file_name)

    except Exception as e:
        # Clean up on error
        if module_name in sys.modules:
            del sys.modules[module_name]
        console.print(f"[yellow]Warning: Error loading {info.file_name}: {e}[/yellow]")

    return None


def compute_tool_digest(tool_name: str, version: str, properties_json: str) -> str:
    """Compute digest for a tool version."""
    content = f"{tool_name}.{version}.{properties_json}"
    return hashlib.sha256(content.encode()).hexdigest()[:16]


async def check_site_status(site: SiteConfig, project_name: str) -> SiteStatus:
    """Check site connectivity and permissions."""
    from iq_sdk.client import IQApiClient

    async with IQApiClient.from_site(site) as client:
        status = await client.check_status(namespace=project_name)
        status.site_id = site.id
        return status


async def get_remote_tools(
    site: SiteConfig, project_name: str
) -> tuple[SiteStatus, list[RemoteTool]]:
    """Get remote tools for a project from a site."""
    from iq_sdk.client import IQApiClient
    from iq_sdk.client.remote import SiteStatus
    from iq_sdk.client.remote_tool import RemoteTool

    status = SiteStatus(site_id=site.id)
    tools: list[RemoteTool] = []

    try:
        async with IQApiClient.from_site(site) as client:
            status = await client.check_status(namespace=project_name)
            status.site_id = site.id

            if status.reachable and status.authenticated:
                tools = await RemoteTool.find_all_by_project(client, project_name)

    except Exception as e:
        status.error = str(e)

    return status, tools


@tool.command("ls")
@click.option(
    "--dir", "-d", "tools_dir", default=None, help="Tools directory (default: from settings)"
)
@click.option("--remote", "-r", is_flag=True, help="Show remote sync status")
@click.option("--site", "-s", "site_id", default=None, help="Filter by site ID")
@click.pass_context
def tool_ls(ctx: click.Context, tools_dir: str | None, remote: bool, site_id: str | None) -> None:
    """List available tools."""
    from iq_sdk.cli import require_project

    # Require valid project directory
    require_project(ctx)

    config_file = ctx.obj.get("config_file") if ctx.obj else None

    # Load settings
    settings = load_settings(config_file)
    project_name = settings.project_name

    # Get tools path from settings or CLI override
    tools_path = Path(tools_dir) if tools_dir else settings.get_tools_path()

    # Display project name
    console.print(f"\n[bold cyan]Project:[/bold cyan] {project_name}\n")

    # Get local tools
    local_tools = get_local_tools(tools_path)

    if not remote:
        # Simple local listing
        if not local_tools:
            console.print("[dim]No tools found in tools/[/dim]")
            return

        table = Table(title="Local Tools")
        table.add_column("Full Name", style="cyan")
        table.add_column("Version", style="green")
        table.add_column("Digest", style="dim")
        table.add_column("Path", style="dim")

        for _base_name, versions in sorted(local_tools.items()):
            for info in versions:
                table.add_row(
                    info.get_full_name(project_name),
                    str(info.version),
                    info.digest[:8],
                    str(info.path.parent),
                )

        console.print(table)
        return

    # Check mode - show per-site status
    all_sites = settings.get_sites()
    if site_id:
        all_sites = [s for s in all_sites if s.id == site_id]
        if not all_sites:
            console.print(f"[red]Site '{site_id}' not found[/red]")
            sys.exit(1)

    for site in all_sites:
        console.print(f"\n[bold]Site: {site.id}[/bold]")
        console.print(f"[dim]Host: {site.get_host()}[/dim]\n")

        # Check site status
        status, remote_tools = asyncio.run(get_remote_tools(site, project_name))

        # Display connectivity status
        if status.reachable:
            console.print(f"  ✓ Host reachable ({status.latency_ms}ms)")
        else:
            console.print(f"  [red]✗ Host unreachable: {status.error}[/red]")
            continue

        if status.authenticated:
            console.print("  ✓ Authenticated")
        else:
            console.print(f"  [red]✗ Authentication failed: {status.error}[/red]")
            continue

        # Permissions
        perms = []
        if status.can_read:
            perms.append("read")
        if status.can_create:
            perms.append("create")
        if status.can_update:
            perms.append("update")
        if status.can_delete:
            perms.append("delete")
        if status.can_register_worker:
            perms.append("worker")

        if perms:
            console.print(f"  ✓ Permissions: {', '.join(perms)}")
        else:
            console.print("  [yellow]⚠ No permissions[/yellow]")

        # Check for API errors (from find_all)
        if status.error:
            console.print(f"  [red]✗ API Error: {status.error}[/red]")

        console.print()

        # Build comparison table
        table = Table()
        table.add_column("Full Name", style="cyan")
        table.add_column("Version", style="green")
        table.add_column("Local", style="dim")
        table.add_column("Remote", style="dim")
        table.add_column("Status")

        # Create remote lookup
        remote_lookup: dict[str, dict[str, Any]] = {}
        for rt in remote_tools:
            key = f"{rt.tool_name}:{rt.version}"
            remote_lookup[key] = {
                "digest": rt.digest,
                "enabled": rt.active,
            }

        # Track which remote tools we've seen
        seen_remote: set[str] = set()

        # Local tools
        for base_name, versions in sorted(local_tools.items()):
            for info in versions:
                key = f"{base_name}:{info.version}"
                seen_remote.add(key)

                local_digest = info.digest[:8]
                remote_info = remote_lookup.get(key)

                if remote_info:
                    remote_digest = remote_info["digest"][:8] if remote_info["digest"] else "-"
                    if info.digest == remote_info.get("digest"):
                        status_str = "[green]✓ In Sync[/green]"
                    else:
                        status_str = "[yellow]⚠ Changed[/yellow]"
                else:
                    remote_digest = "-"
                    status_str = "[blue]+ New[/blue]"

                table.add_row(
                    info.get_full_name(project_name),
                    str(info.version),
                    local_digest,
                    remote_digest,
                    status_str,
                )

        # Remote-only tools (deleted locally)
        for rt in remote_tools:
            key = f"{rt.tool_name}:{rt.version}"
            if key not in seen_remote:
                # Construct full name from remote tool info
                remote_full_name = f"{project_name}.{rt.tool_name}_{rt.version.replace('.', '_')}"
                table.add_row(
                    remote_full_name,
                    rt.version,
                    "-",
                    rt.digest[:8] if rt.digest else "-",
                    "[red]✗ Deleted locally[/red]",
                )

        console.print(table)


@tool.command("apply")
@click.option(
    "--dir", "-d", "tools_dir", default=None, help="Tools directory (default: from settings)"
)
@click.option("--site", "-s", "site_ids", multiple=True, help="Sites to apply to")
@click.option("--tool", "-t", "tool_names", multiple=True, help="Tools to apply")
@click.option("--version", "-v", "versions", multiple=True, help="Versions to apply")
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompts")
@click.option("--dry-run", is_flag=True, help="Show what would be done without doing it")
@click.pass_context
def tool_apply(
    ctx: click.Context,
    tools_dir: str | None,
    site_ids: tuple,
    tool_names: tuple,
    versions: tuple,
    yes: bool,
    dry_run: bool,
) -> None:
    """
    Apply local tools to remote sites.

    Syncs local tool definitions with remote sites.
    Creates, updates, or deletes tools as needed (like terraform apply).
    """
    from iq_sdk.cli import require_project

    # Require valid project directory
    require_project(ctx)

    config_file = ctx.obj.get("config_file") if ctx.obj else None

    settings = load_settings(config_file)
    project_name = settings.project_name

    # Get tools path from settings or CLI override
    tools_path = Path(tools_dir) if tools_dir else settings.get_tools_path()

    console.print(f"\n[bold cyan]Project:[/bold cyan] {project_name}\n")

    # Get local tools
    local_tools = get_local_tools(tools_path)

    # Convert filter values for later use
    tool_filters = list(tool_names) if tool_names else []
    version_set = set(versions) if versions else set()

    def matches_tool_filter(name: str) -> bool:
        """Check if name matches any tool filter (substring match)."""
        if not tool_filters:
            return True
        return any(f in name for f in tool_filters)

    # Filter local tools by tool names (substring match)
    if tool_filters:
        local_tools = {k: v for k, v in local_tools.items() if matches_tool_filter(k)}

    # Filter local tools by versions
    if version_set:
        for base_name in list(local_tools.keys()):
            local_tools[base_name] = [
                info for info in local_tools[base_name] if str(info.version) in version_set
            ]
            if not local_tools[base_name]:
                del local_tools[base_name]

    # Note: We don't abort early here, because we also need to check remote-only
    # tools that may need to be deleted

    # Get target sites
    all_sites = settings.get_sites()
    if site_ids:
        all_sites = [s for s in all_sites if s.id in site_ids]
        if not all_sites:
            console.print("[red]No matching sites found[/red]")
            sys.exit(1)

    # Track if any tools were found across all sites
    any_tools_found = bool(local_tools)

    # Process each site
    for site in all_sites:
        console.print(f"\n[bold]Site: {site.id}[/bold]")
        console.print(f"[dim]Host: {site.get_host()}[/dim]\n")

        # Get site status and remote tools
        status, remote_tools = asyncio.run(get_remote_tools(site, project_name))

        if not status.reachable:
            console.print(f"  [red]✗ Unreachable: {status.error}[/red]")
            continue

        if not status.authenticated:
            console.print(f"  [red]✗ Auth failed: {status.error}[/red]")
            continue

        # Build remote lookup
        remote_lookup: dict[str, Any] = {}
        for rt in remote_tools:
            key = f"{rt.tool_name}:{rt.version}"
            remote_lookup[key] = rt

        # Determine actions
        actions: list[tuple[str, str, LocalToolInfo | None, Any]] = []

        for base_name, tool_versions in local_tools.items():
            for info in tool_versions:
                key = f"{base_name}:{info.version}"
                remote = remote_lookup.pop(key, None)

                if remote is None:
                    actions.append(("create", key, info, None))
                elif info.digest != remote.digest:
                    actions.append(("update", key, info, remote))
                # else: in sync, no action

        # Remote-only tools (deleted locally) - check for deletions
        for key, remote in remote_lookup.items():
            tool_name, version = key.split(":")

            # If filtering by tool name, only include matching tools (substring match)
            if tool_filters and not matches_tool_filter(tool_name):
                continue

            # If filtering by version, only include matching versions
            if version_set and version not in version_set:
                continue

            actions.append(("delete", key, None, remote))
            any_tools_found = True

        if not actions:
            console.print("  [green]✓ All tools in sync[/green]")
            continue

        if dry_run:
            console.print("  [dim]Dry run - no changes made[/dim]\n")

        # Process actions
        for action, key, local_info, remote in actions:
            tool_name, version = key.split(":")

            # Build full display name
            if local_info:
                display_name = local_info.get_full_name(project_name)
            else:
                # Remote-only tool
                display_name = f"{project_name}.{tool_name}_{version.replace('.', '_')}"

            if action == "create":
                status_icon = "[blue]+[/blue]"
                action_text = "Create"
            elif action == "update":
                status_icon = "[yellow]~[/yellow]"
                action_text = "Update"
            else:
                status_icon = "[red]-[/red]"
                action_text = "Delete"

            if dry_run:
                console.print(f"  {status_icon} {action_text}: {display_name}")
                continue

            # Ask for confirmation if not --yes
            if not yes:
                confirm = Prompt.ask(
                    f"  {status_icon} {action_text} {display_name}?",
                    choices=["y", "n"],
                    default="y",
                )
                if confirm.lower() != "y":
                    console.print("    [dim]Skipped[/dim]")
                    continue

            # Execute action
            result = asyncio.run(
                _execute_push_action(site, project_name, action, local_info, remote)
            )

            if result.success:
                console.print(f"  {status_icon} {action_text}: {display_name} [green]✓[/green]")
            else:
                console.print(
                    f"  {status_icon} {action_text}: {display_name} [red]✗ {result.error}[/red]"
                )

    # If no tools were found at all (local or remote), show message
    if not any_tools_found:
        console.print("[yellow]No matching tools found[/yellow]")

    console.print()


async def _execute_push_action(
    site: SiteConfig,
    project_name: str,
    action: str,
    local_info: LocalToolInfo | None,
    remote: Any,
) -> APIResult:
    """Execute a single push action."""
    from iq_sdk.client import IQApiClient
    from iq_sdk.client.remote import APIResult
    from iq_sdk.client.remote_tool import RemoteTool

    async with IQApiClient.from_site(site) as client:
        if action == "create" and local_info:
            tool = RemoteTool(
                namespace=project_name,
                tool_name=local_info.name,
                version=str(local_info.version),
                properties_json=local_info.properties_json,
                digest=local_info.digest,
            )
            return await tool.save(client)

        elif action == "update" and local_info and remote:
            remote.properties_json = local_info.properties_json
            remote.digest = local_info.digest
            return await remote.save(client)

        elif action == "delete" and remote:
            return await remote.delete(client)

        return APIResult(success=False, error="Invalid action")


@tool.command("register")
@click.argument("tool_name")
@click.option("--site", "-s", "site_ids", multiple=True, help="Sites to register with")
@click.pass_context
def tool_register(ctx: click.Context, tool_name: str, site_ids: tuple) -> None:
    """Register a tool with site(s)."""
    config_file = ctx.obj.get("config_file") if ctx.obj else None
    settings = load_settings(config_file)

    # Get target sites
    if site_ids:
        sites = settings.get_sites(list(site_ids))
    else:
        sites = [settings.site]

    if not sites:
        console.print("[red]No matching sites found[/red]")
        sys.exit(1)

    console.print(f"[bold blue]Registering tool '{tool_name}'...[/bold blue]")
    for s in sites:
        console.print(f"  → {s.id} ({s.get_host()})")

    console.print("\n[yellow]Note: Tool registration requires running worker connection.[/yellow]")
    console.print(f"Start a worker with: iq worker start --tool {tool_name}")


@tool.command("unregister")
@click.argument("tool_name")
@click.option("--site", "-s", "site_ids", multiple=True, help="Sites to unregister from")
@click.pass_context
def tool_unregister(ctx: click.Context, tool_name: str, site_ids: tuple) -> None:
    """Unregister a tool from site(s)."""
    config_file = ctx.obj.get("config_file") if ctx.obj else None
    settings = load_settings(config_file)

    # Get target sites
    if site_ids:
        sites = settings.get_sites(list(site_ids))
    else:
        sites = [settings.site]

    if not sites:
        console.print("[red]No matching sites found[/red]")
        sys.exit(1)

    console.print(f"[bold blue]Unregistering tool '{tool_name}'...[/bold blue]")
    for s in sites:
        console.print(f"  → {s.id} ({s.get_host()})")

    console.print("\n[yellow]Note: Tool unregistration handled on worker disconnect.[/yellow]")


@tool.command("rm")
@click.argument("tool_name")
@click.option(
    "--dir", "-d", "tools_dir", default=None, help="Tools directory (default: from settings)"
)
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
@click.option("--all", "-a", "delete_all", is_flag=True, help="Delete all versions")
@click.option(
    "--version",
    "-v",
    "version_str",
    default=None,
    help="Specific version to delete (e.g., 1.0 or 1.x)",
)
@click.pass_context
def tool_rm(
    click_ctx: click.Context,
    tool_name: str,
    tools_dir: str | None,
    yes: bool,
    delete_all: bool,
    version_str: str | None,
) -> None:
    """
    Delete a tool and its versions.

    Shows all available versions and allows deletion of:
    - A single minor version (e.g., 1.0)
    - A major version with all minors (e.g., all 1.x)
    - All versions of the tool

    Examples:
        iq tool delete my-tool --all -y      # Delete all, no confirm
        iq tool delete my-tool -v 1.0 -y     # Delete specific version
        iq tool delete my-tool -v 1.x        # Delete all 1.x versions
    """
    import shutil

    from iq_sdk.cli import require_project

    # Require valid project directory
    require_project(click_ctx)

    # Get tools path from settings or CLI override
    config_file = click_ctx.obj.get("config_file") if click_ctx.obj else None
    settings = load_settings(config_file)
    tools_path = Path(tools_dir) if tools_dir else settings.get_tools_path()
    snake_name = to_snake_case(tool_name)

    # Find all versions
    existing_dir, existing_versions = find_existing_tool(tools_path, tool_name)

    if not existing_versions:
        console.print(f"[red]Tool '{tool_name}' not found[/red]")
        sys.exit(1)

    # Group by major version
    versions_by_major: dict[int, list[ToolVersion]] = {}
    for v in existing_versions:
        versions_by_major.setdefault(v.major, []).append(v)

    # Show available versions
    console.print(f"\n[bold]Tool '{tool_name}' versions:[/bold]")
    for major, minors in sorted(versions_by_major.items()):
        minor_strs = [f"{major}.{v.minor}" for v in sorted(minors, key=lambda x: x.minor)]
        console.print(f"  Major {major}: {', '.join(minor_strs)}")

    console.print()

    # Determine selection
    if delete_all:
        selection = "A"
    elif version_str:
        selection = version_str
    else:
        # Build selection options
        options = []
        for major, minors in sorted(versions_by_major.items()):
            for v in sorted(minors, key=lambda x: x.minor):
                options.append(f"{v.major}.{v.minor}")
            # Always allow major number (e.g., "1" or "2") to delete all minors
            options.append(str(major))
        options.append("A")

        default_selection = "A" if len(existing_versions) == 1 else options[0]
        selection = Prompt.ask(
            "Delete which version? (e.g., '1.0', '1' for all 1.x, 'A' for all)",
            choices=options,
            default=default_selection,
        )

    # Determine what to delete
    to_delete: list[tuple[Path, ToolVersion]] = []
    dirs_to_check: set[Path] = set()

    if selection.upper() == "A":
        # Delete all
        for v in existing_versions:
            tool_dir = tools_path / get_tool_dir_name(tool_name, v.major)
            tool_file = tool_dir / get_tool_file_name(tool_name, v)
            to_delete.append((tool_file, v))
            dirs_to_check.add(tool_dir)
    elif selection.endswith(".x") or selection.isdigit():
        # Delete all minors of a major (e.g., "1.x" or just "1")
        major = int(selection.replace(".x", ""))
        for v in existing_versions:
            if v.major == major:
                tool_dir = tools_path / get_tool_dir_name(tool_name, v.major)
                tool_file = tool_dir / get_tool_file_name(tool_name, v)
                to_delete.append((tool_file, v))
                dirs_to_check.add(tool_dir)
    else:
        # Delete specific version
        version = ToolVersion.parse(selection)
        tool_dir = tools_path / get_tool_dir_name(tool_name, version.major)
        tool_file = tool_dir / get_tool_file_name(tool_name, version)
        to_delete.append((tool_file, version))
        dirs_to_check.add(tool_dir)

    # Confirm
    if not yes:
        console.print("\n[yellow]Will delete:[/yellow]")
        for path, _v in to_delete:
            console.print(f"  • {path}")

        # Check if directories will be removed
        for d in dirs_to_check:
            remaining_files = [
                f
                for f in d.iterdir()
                if f.is_file()
                and f.suffix == ".py"
                and f.name not in ["__init__.py", "properties.py"]
                and not any(str(p) == str(f) for p, _ in to_delete)
            ]
            if not remaining_files:
                console.print(f"  • {d}/ (entire directory)")

        if not Confirm.ask("\nProceed?", default=False):
            console.print("[dim]Cancelled[/dim]")
            return

    # Delete files
    deleted_count = 0
    for path, _v in to_delete:
        if path.exists():
            path.unlink()
            deleted_count += 1
            console.print(f"[green]✓ Deleted {path.name}[/green]")

    # Check if directories should be removed (no more tool files)
    for d in dirs_to_check:
        if d.exists():
            remaining_tool_files = [
                f
                for f in d.iterdir()
                if f.is_file()
                and f.suffix == ".py"
                and f.name not in ["__init__.py", "properties.py"]
                and re.match(rf"^{re.escape(snake_name)}_\d+_\d+\.py$", f.name)
            ]
            if not remaining_tool_files:
                shutil.rmtree(d)
                console.print(f"[green]✓ Deleted directory {d}[/green]")

    console.print(
        f"\n[bold green]✓ Deleted {deleted_count} version(s) of '{tool_name}'[/bold green]"
    )
