"""Data file and skill staging utilities for agents.

Handles copying user-provided data files and skill directories
into the sandbox so the LLM can access them via shell commands.
"""

from __future__ import annotations

import shutil
from pathlib import Path
from typing import Any

from fluxibly.logging import Logger

logger = Logger(component="data")


def stage_data_files(
    data_items: list[dict[str, Any]],
    sandbox_dir: str,
) -> list[dict[str, Any]]:
    """Copy data files into ``sandbox_dir/data/`` for agent access.

    Each item must have ``file_name`` and either ``path`` (host file to copy)
    or ``content`` (str/bytes to write).  Items with neither are skipped.

    Returns a new list of dicts with ``sandbox_path`` added to each
    successfully staged item.
    """
    data_dir = Path(sandbox_dir) / "data"
    data_dir.mkdir(parents=True, exist_ok=True)

    staged: list[dict[str, Any]] = []

    for item in data_items:
        file_name = item.get("file_name")
        if not file_name:
            logger.warning(
                "Skipping data item without file_name: {item}", item=item
            )
            continue

        dest = data_dir / file_name
        src_path = item.get("path")
        content = item.get("content")

        if src_path:
            shutil.copy2(src_path, dest)
            logger.info(
                "Staged data file (copy): {src} → {dest}",
                src=src_path,
                dest=str(dest),
            )
        elif content is not None:
            if isinstance(content, bytes):
                dest.write_bytes(content)
            else:
                dest.write_text(str(content))
            logger.info("Staged data file (content): {dest}", dest=str(dest))
        else:
            logger.warning(
                "Skipping data item with no path or content: {name}",
                name=item.get("name", file_name),
            )
            continue

        staged.append({**item, "sandbox_path": str(dest)})

    logger.info(
        "Staged {n} data file(s) into {dir}", n=len(staged), dir=str(data_dir)
    )
    return staged


def format_data_prompt(staged_data: list[dict[str, Any]]) -> str:
    """Build a structured text block describing staged data files.

    Returns a markdown-formatted section suitable for appending to the
    system prompt.
    """
    has_id = any(item.get("id") for item in staged_data)

    lines: list[str] = [
        "",
        "## Available Data Files",
        "",
        "The following data files have been provided and are available "
        "in the sandbox. When referencing these files in code or commands, "
        "use the sandbox path listed below.",
        "",
    ]

    # Table header
    if has_id:
        lines.append("| # | Name | File | Sandbox Path | ID |")
        lines.append("|---|------|------|--------------|----|")
    else:
        lines.append("| # | Name | File | Sandbox Path |")
        lines.append("|---|------|------|--------------|")

    for idx, item in enumerate(staged_data, 1):
        name = item.get("name", "")
        file_name = item.get("file_name", "")
        sandbox_path = item.get("sandbox_path", "")

        if has_id:
            item_id = item.get("id") or "\u2014"
            lines.append(
                f"| {idx} | {name} | {file_name} | {sandbox_path} | {item_id} |"
            )
        else:
            lines.append(f"| {idx} | {name} | {file_name} | {sandbox_path} |")

    lines.append("")
    lines.append(
        "Note: If any original host paths were provided, those files "
        "have been copied to the sandbox paths shown above. Always use "
        "the sandbox paths for file operations."
    )

    return "\n".join(lines)


def inject_data_prompt(
    messages: list[dict[str, Any]],
    staged_data: list[dict[str, Any]],
) -> list[dict[str, Any]]:
    """Append data file info to the system message.

    If no system message exists, one is created.
    """
    if not staged_data:
        return messages

    data_block = format_data_prompt(staged_data)

    # Find and update existing system message
    for i, msg in enumerate(messages):
        if msg.get("role") == "system":
            messages[i] = {
                **msg,
                "content": msg["content"] + "\n" + data_block,
            }
            return messages

    # No system message — prepend one
    messages.insert(0, {"role": "system", "content": data_block.lstrip("\n")})
    return messages


# ══════════════════════════════════════════════════════════════
# Skill Staging
# ══════════════════════════════════════════════════════════════


def stage_skill(skill_path: str, sandbox_dir: str) -> str:
    """Copy a skill directory into ``sandbox_dir/skills/<name>/``.

    Copies the entire skill directory tree (SKILL.md, scripts/, etc.)
    so that the LLM can reference scripts via sandbox-relative paths.

    Args:
        skill_path: Host path to the skill directory (contains SKILL.md).
        sandbox_dir: Session sandbox directory.

    Returns:
        The sandbox path to the staged skill directory.
    """
    src = Path(skill_path)
    skill_name = src.name
    dest = Path(sandbox_dir) / "skills" / skill_name

    if dest.exists():
        logger.debug(
            "Skill already staged: {name} at {dest}",
            name=skill_name,
            dest=str(dest),
        )
        return str(dest)

    shutil.copytree(src, dest)
    logger.info(
        "Staged skill: {name} ({src} → {dest})",
        name=skill_name,
        src=str(src),
        dest=str(dest),
    )
    return str(dest)


def format_sandbox_files_prompt(sandbox_dir: str) -> str:
    """Build a file tree of the sandbox for the system prompt.

    Lists all user-visible files in the sandbox (``data/``, ``skills/``)
    so the LLM knows what scripts and data are available.
    Hidden files (dotfiles, ``__pycache__``) are excluded.

    Args:
        sandbox_dir: Absolute path to the session sandbox.

    Returns:
        Markdown-formatted file tree string, or empty string if nothing to show.
    """
    root = Path(sandbox_dir)
    # Only list user-visible directories
    visible_dirs = ["data", "skills"]
    lines: list[str] = []

    for dir_name in visible_dirs:
        dir_path = root / dir_name
        if not dir_path.is_dir():
            continue
        _walk_tree(dir_path, root, lines, depth=0)

    if not lines:
        return ""

    header = [
        "",
        "## Sandbox File Tree",
        "",
        f"Working directory: `{sandbox_dir}`",
        "",
        "```",
    ]
    footer = ["```", ""]
    return "\n".join(header + lines + footer)


def _walk_tree(path: Path, root: Path, lines: list[str], depth: int) -> None:
    """Recursively build indented file tree lines."""
    rel = path.relative_to(root)
    indent = "  " * depth
    if path.is_dir():
        lines.append(f"{indent}{rel.name}/")
        for child in sorted(path.iterdir()):
            if child.name.startswith(".") or child.name == "__pycache__":
                continue
            _walk_tree(child, root, lines, depth + 1)
    else:
        lines.append(f"{indent}{rel.name}")


def inject_sandbox_files_prompt(
    messages: list[dict[str, Any]],
    sandbox_dir: str,
) -> list[dict[str, Any]]:
    """Append sandbox file tree to the system message.

    Shows the LLM what files (scripts, data) are available in the sandbox.
    """
    block = format_sandbox_files_prompt(sandbox_dir)
    if not block:
        return messages

    for i, msg in enumerate(messages):
        if msg.get("role") == "system":
            messages[i] = {
                **msg,
                "content": msg["content"] + "\n" + block,
            }
            return messages

    messages.insert(0, {"role": "system", "content": block.lstrip("\n")})
    return messages


def collect_requirements(
    skill_paths: list[str],
    tool_yaml_paths: list[Path] | None = None,
) -> list[str]:
    """Collect ``requirements.txt`` file paths from skills and tools.

    Skills: looks for ``<skill_dir>/requirements.txt``.
    Tools: looks for ``<tool_yaml_dir>/requirements.txt`` (sibling of tool YAML).

    Args:
        skill_paths: List of skill directory paths (sandbox or host).
        tool_yaml_paths: Optional list of tool YAML file paths.

    Returns:
        List of absolute paths to ``requirements.txt`` files found.
    """
    found: list[str] = []

    for skill_dir in skill_paths:
        req = Path(skill_dir) / "requirements.txt"
        if req.exists():
            found.append(str(req))

    for yaml_path in tool_yaml_paths or []:
        req = yaml_path.parent / "requirements.txt"
        if req.exists():
            found.append(str(req))

    return found
