# Copyright (C) 2023 - 2026 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""CLI for PyDyna.

Usage:
    python -m ansys.dyna.core agent --env cursor
    python -m ansys.dyna.core agent --env vscode --copy
    python -m ansys.dyna.core agent --env vscode --pointer
    python -m ansys.dyna.core agent --print
"""

import argparse
from datetime import datetime, timezone
import json
from pathlib import Path
import shutil
import sys

# Package metadata
PACKAGE_NAMESPACE = "ansys.dyna.core"
PACKAGE_NAME = "ansys-dyna-core"
PACKAGE_ECOSYSTEM = "pypi"


def find_workspace_root() -> Path | None:
    """Find workspace root by walking up from cwd."""
    cwd = Path.cwd()
    for parent in [cwd, *cwd.parents]:
        if (parent / ".git").exists():
            return parent
        if (parent / "pyproject.toml").exists():
            return parent
    return None


def get_package_dir() -> Path:
    """Get the package directory containing agent instructions."""
    return Path(__file__).parent


def get_instructions_path() -> Path:
    """Get path to agent instructions file."""
    return get_package_dir() / "AGENT.md"


def get_extended_docs_dir() -> Path:
    """Get path to extended agent documentation directory."""
    return get_package_dir() / "agent"


def get_instructions_content() -> str:
    """Read the agent instructions from installed package."""
    return get_instructions_path().read_text(encoding="utf-8")


def rewrite_links_for_copy(content: str, subdir_name: str) -> str:
    """Rewrite relative links in content to work from copied location."""
    import re

    pattern = r"\]\(agent/"
    replacement = f"]({subdir_name}/"
    return re.sub(pattern, replacement, content)


def format_for_cursor(content: str, source_path: Path) -> str:
    """Wrap content in Cursor rules MDC format."""
    return f"""---
description: PyDyna usage instructions for AI assistants
globs: ["**/*.py", "**/*.k", "**/*.key"]
alwaysApply: false
---

<!-- Auto-generated from: {source_path} -->

{content}
"""


def ensure_gitignore(workspace: Path) -> None:
    """Add .agent/ to .gitignore."""
    gitignore = workspace / ".gitignore"
    marker = ".agent/"
    if gitignore.exists():
        content = gitignore.read_text()
        if marker not in content:
            with gitignore.open("a") as f:
                f.write(f"\n# Agent instructions (auto-generated)\n{marker}\n")
    else:
        gitignore.write_text(f"# Agent instructions (auto-generated)\n{marker}\n")


# =============================================================================
# Manifest Management
# =============================================================================


def load_manifest(agent_dir: Path) -> dict:
    """Load the manifest.json file, or return empty manifest."""
    manifest_file = agent_dir / "manifest.json"
    if manifest_file.exists():
        try:
            return json.loads(manifest_file.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, OSError):
            pass
    return {"version": "1.0", "packages": []}


def save_manifest(agent_dir: Path, manifest: dict) -> None:
    """Save the manifest.json file."""
    manifest_file = agent_dir / "manifest.json"
    manifest_file.write_text(json.dumps(manifest, indent=2), encoding="utf-8")


def update_manifest_entry(
    manifest: dict,
    namespace: str,
    ecosystem: str,
    package_name: str,
    entry_file: str,
    extended_docs: list[str],
    mode: str,
    source_path: str,
) -> dict:
    """Add or update a package entry in the manifest."""
    # Find existing entry
    packages = manifest.get("packages", [])
    entry = None
    for pkg in packages:
        if pkg.get("namespace") == namespace:
            entry = pkg
            break

    if entry is None:
        entry = {"namespace": namespace}
        packages.append(entry)

    # Update entry
    entry["ecosystem"] = ecosystem
    entry["package_name"] = package_name
    entry["entry_file"] = entry_file
    entry["extended_docs"] = extended_docs
    entry["mode"] = mode
    entry["source"] = source_path
    entry["installed_at"] = datetime.now(timezone.utc).isoformat()

    manifest["packages"] = packages
    return manifest


def generate_readme(agent_dir: Path, manifest: dict) -> None:
    """Generate README.md from manifest."""
    packages = manifest.get("packages", [])

    lines = [
        "# Agent Instructions",
        "",
        "This directory contains agent instructions from installed packages.",
        "Agents can read these files for context about how to use the packages.",
        "",
        "## Installed Packages",
        "",
    ]

    if packages:
        lines.append("| Namespace | Ecosystem | Package | Entry File |")
        lines.append("|-----------|-----------|---------|------------|")
        for pkg in packages:
            namespace = pkg.get("namespace", "?")
            ecosystem = pkg.get("ecosystem", "?")
            package_name = pkg.get("package_name", "?")
            entry_file = pkg.get("entry_file", "?")
            lines.append(f"| `{namespace}` | {ecosystem} | {package_name} | [{entry_file}]({entry_file}) |")
        lines.append("")
    else:
        lines.append("*No packages installed yet.*")
        lines.append("")

    lines.extend(
        [
            "## Usage",
            "",
            "Each package provides its own installation command. For Python packages with",
            "agent instructions, you can typically run:",
            "",
            "```bash",
            "python -m <package> agent --copy",
            "```",
            "",
            "## Scanning Dependencies",
            "",
            "To install agent instructions from all packages in your requirements:",
            "",
            "```bash",
            "# Future: python -m agent_instructions scan requirements.txt",
            "# For now, run each package's agent command individually",
            "```",
            "",
            "---",
            "*Auto-generated manifest. Regenerate by running package agent commands.*",
            "",
        ]
    )

    readme_file = agent_dir / "README.md"
    readme_file.write_text("\n".join(lines), encoding="utf-8")


# =============================================================================
# File Copy Operations
# =============================================================================


def copy_all_instructions(workspace: Path, mode: str = "copy") -> tuple[Path, list[str]]:
    """Copy all agent instruction files to workspace with working links.

    Returns
    -------
    tuple[Path, list[str]]
        Main file path and list of extended doc relative paths.
    """
    agent_dir = workspace / ".agent"
    agent_dir.mkdir(parents=True, exist_ok=True)

    # Main file with rewritten links
    main_content = get_instructions_content()
    main_content = rewrite_links_for_copy(main_content, PACKAGE_NAMESPACE)
    main_file = agent_dir / f"{PACKAGE_NAMESPACE}.md"
    main_file.write_text(main_content, encoding="utf-8")

    # Extended docs
    extended_dir = agent_dir / PACKAGE_NAMESPACE
    extended_dir.mkdir(parents=True, exist_ok=True)

    source_dir = get_extended_docs_dir()
    extended_docs = []

    if source_dir.exists():
        for src_file in source_dir.glob("*.md"):
            dst_file = extended_dir / src_file.name
            shutil.copy2(src_file, dst_file)
            extended_docs.append(f"{PACKAGE_NAMESPACE}/{src_file.name}")

    # Update manifest
    manifest = load_manifest(agent_dir)
    manifest = update_manifest_entry(
        manifest=manifest,
        namespace=PACKAGE_NAMESPACE,
        ecosystem=PACKAGE_ECOSYSTEM,
        package_name=PACKAGE_NAME,
        entry_file=f"{PACKAGE_NAMESPACE}.md",
        extended_docs=extended_docs,
        mode=mode,
        source_path=str(get_instructions_path()),
    )
    save_manifest(agent_dir, manifest)
    generate_readme(agent_dir, manifest)

    return main_file, extended_docs


def register_pointer_in_manifest(workspace: Path, mode: str = "pointer") -> None:
    """Register a pointer-mode entry in the manifest."""
    agent_dir = workspace / ".agent"
    agent_dir.mkdir(parents=True, exist_ok=True)

    source_path = get_instructions_path()
    extended_dir = get_extended_docs_dir()
    extended_docs = []
    if extended_dir.exists():
        for doc in extended_dir.glob("*.md"):
            extended_docs.append(f"agent/{doc.name}")

    manifest = load_manifest(agent_dir)
    manifest = update_manifest_entry(
        manifest=manifest,
        namespace=PACKAGE_NAMESPACE,
        ecosystem=PACKAGE_ECOSYSTEM,
        package_name=PACKAGE_NAME,
        entry_file=str(source_path),
        extended_docs=extended_docs,
        mode=mode,
        source_path=str(source_path),
    )
    save_manifest(agent_dir, manifest)
    generate_readme(agent_dir, manifest)


# =============================================================================
# Pointer Block Management
# =============================================================================


def _make_pointer_block(
    pydyna_ref: str,
    regenerate_cmd: str,
    extended_docs: list[str] | None = None,
) -> tuple[str, str, str]:
    """Create a pointer block for single-file instruction tools."""
    start = "<!-- pydyna-agent-instructions:start -->"
    end = "<!-- pydyna-agent-instructions:end -->"

    extended_hint = ""
    if extended_docs:
        extended_hint = " (links to extended topics)"

    block = f"""{start}
## PyDyna (ansys-dyna-core)

**IMPORTANT:** Before writing any code using `ansys.dyna.core`, you MUST:

1. **Read** the file `{pydyna_ref}` in this workspace
2. **Read linked files** as needed for your task{extended_hint}
3. **Search** docs before assuming patterns (NODE/PART use DataFrames, not simple attributes)
4. **Do not guess** - if uncertain, use `help(keywords.KeywordName)` or ask for clarification

Regenerate: `{regenerate_cmd}`
{end}"""

    return start, end, block


def _update_single_file_instructions(
    workspace: Path,
    pydyna_path: Path,
    target_file: Path,
    regenerate_cmd: str,
    default_header: str,
    extended_docs: list[str] | None = None,
) -> Path:
    """Append/refresh a pointer section in a single-file instructions file."""
    try:
        rel = pydyna_path.relative_to(workspace)
        pydyna_ref = str(rel).replace("\\", "/")
    except ValueError:
        pydyna_ref = str(pydyna_path).replace("\\", "/")

    start, end, block = _make_pointer_block(pydyna_ref, regenerate_cmd, extended_docs)

    if target_file.exists():
        text = target_file.read_text(encoding="utf-8", errors="replace")

        if start in text and end in text and text.index(start) < text.index(end):
            pre = text[: text.index(start)].rstrip()
            post = text[text.index(end) + len(end) :].lstrip()
            if pre:
                new_text = f"{pre}\n\n{block}\n"
            else:
                new_text = f"{block}\n"
            if post:
                new_text += f"\n{post}"
            new_text = new_text.rstrip() + "\n"
        else:
            new_text = text.rstrip() + "\n\n" + block + "\n"
    else:
        target_file.parent.mkdir(parents=True, exist_ok=True)
        new_text = f"{default_header}\n\n{block}\n"

    target_file.write_text(new_text, encoding="utf-8")
    return target_file


def update_copilot_instructions(
    workspace: Path,
    pydyna_path: Path,
    mode: str,
    extended_docs: list[str] | None = None,
) -> Path:
    """Update .github/copilot-instructions.md with a pointer."""
    github_dir = workspace / ".github"
    github_dir.mkdir(parents=True, exist_ok=True)
    copilot_file = github_dir / "copilot-instructions.md"

    return _update_single_file_instructions(
        workspace=workspace,
        pydyna_path=pydyna_path,
        target_file=copilot_file,
        regenerate_cmd=f"python -m ansys.dyna.core agent --env vscode --{mode}",
        default_header="# Copilot Instructions",
        extended_docs=extended_docs,
    )


def update_claude_instructions(
    workspace: Path,
    pydyna_path: Path,
    mode: str,
    extended_docs: list[str] | None = None,
) -> Path:
    """Update CLAUDE.md with a pointer."""
    claude_file = workspace / "CLAUDE.md"

    return _update_single_file_instructions(
        workspace=workspace,
        pydyna_path=pydyna_path,
        target_file=claude_file,
        regenerate_cmd=f"python -m ansys.dyna.core agent --env claude --{mode}",
        default_header="# Claude Code Instructions",
        extended_docs=extended_docs,
    )


# =============================================================================
# Command Handlers
# =============================================================================


def cmd_agent_copy(args: argparse.Namespace, workspace: Path) -> int:
    """Handle --copy mode: copy all files with working links."""
    env = args.env

    if env == "cursor":
        # Cursor: single self-contained MDC file
        content = get_instructions_content()
        formatted = format_for_cursor(content, get_instructions_path())
        cursor_dir = workspace / ".cursor" / "rules"
        cursor_dir.mkdir(parents=True, exist_ok=True)
        output_path = cursor_dir / "pydyna.mdc"
        output_path.write_text(formatted, encoding="utf-8")
        print(f"[OK] PyDyna instructions written to: {output_path}")
        return 0

    # All other environments: copy to .agent/ with extended docs
    main_file, extended_docs = copy_all_instructions(workspace, mode="copy")
    ensure_gitignore(workspace)

    print(f"[OK] PyDyna instructions copied to: {main_file}")
    if extended_docs:
        print(f"     Extended docs: {len(extended_docs)} files")
    print(f"     Manifest updated: {workspace / '.agent' / 'manifest.json'}")

    # For single-file tools, also add pointer
    if env in ("vscode", "copilot"):
        pointer_file = update_copilot_instructions(workspace, main_file, "copy")
        print(f"[OK] Added pointer to: {pointer_file}")
        print("     (Your existing instructions were preserved)")

    elif env == "claude":
        pointer_file = update_claude_instructions(workspace, main_file, "copy")
        print(f"[OK] Added pointer to: {pointer_file}")
        print("     (Your existing instructions were preserved)")

    return 0


def cmd_agent_pointer(args: argparse.Namespace, workspace: Path) -> int:
    """Handle --pointer mode: just add pointer to installed package."""
    env = args.env
    source_path = get_instructions_path()

    # Get extended doc paths relative to main file
    extended_dir = get_extended_docs_dir()
    extended_docs = []
    if extended_dir.exists():
        for doc in extended_dir.glob("*.md"):
            extended_docs.append(f"agent/{doc.name}")

    if env == "cursor":
        # Cursor doesn't support pointer mode well - use copy instead
        print("Note: Cursor works best with --copy mode. Using --copy.", file=sys.stderr)
        return cmd_agent_copy(args, workspace)

    # Register in manifest even for pointer mode
    register_pointer_in_manifest(workspace, mode="pointer")
    ensure_gitignore(workspace)

    if env in ("vscode", "copilot"):
        pointer_file = update_copilot_instructions(workspace, source_path, "pointer", extended_docs)
        print(f"[OK] Added pointer to installed package in: {pointer_file}")
        print(f"     Points to: {source_path}")
        print(f"     Manifest updated: {workspace / '.agent' / 'manifest.json'}")
        print("     (Your existing instructions were preserved)")

    elif env == "claude":
        pointer_file = update_claude_instructions(workspace, source_path, "pointer", extended_docs)
        print(f"[OK] Added pointer to installed package in: {pointer_file}")
        print(f"     Points to: {source_path}")
        print(f"     Manifest updated: {workspace / '.agent' / 'manifest.json'}")
        print("     (Your existing instructions were preserved)")

    elif env == "generic":
        print("PyDyna agent instructions are installed at:")
        print(f"  {source_path}")
        if extended_docs:
            print("\nExtended documentation:")
            for doc in extended_docs:
                print(f"  {source_path.parent / doc}")
        print(f"\nManifest updated: {workspace / '.agent' / 'manifest.json'}")

    return 0


def cmd_agent(args: argparse.Namespace) -> int:
    """Handle 'agent' subcommand."""
    content = get_instructions_content()

    # Print mode
    if args.print:
        try:
            print(content)
        except UnicodeEncodeError:
            sys.stdout.buffer.write(content.encode("utf-8"))
        return 0

    # Find workspace
    if args.workspace:
        workspace = Path(args.workspace)
    else:
        workspace = find_workspace_root()
        if workspace is None:
            print("Error: Could not detect workspace root.", file=sys.stderr)
            print("Run from a git repo or specify --workspace PATH", file=sys.stderr)
            return 1

    # Determine mode
    if args.pointer:
        return cmd_agent_pointer(args, workspace)
    else:
        # Default to copy mode
        return cmd_agent_copy(args, workspace)


def main() -> int:
    """Main entry point for CLI."""
    parser = argparse.ArgumentParser(
        prog="python -m ansys.dyna.core",
        description="PyDyna CLI utilities",
    )
    subparsers = parser.add_subparsers(dest="command")

    # agent subcommand
    agent_parser = subparsers.add_parser(
        "agent",
        help="Install agent instructions for AI assistants",
    )
    agent_parser.add_argument(
        "--env",
        choices=["cursor", "vscode", "copilot", "claude", "generic"],
        default="generic",
        help="Target environment (default: generic)",
    )

    # Mode selection (mutually exclusive)
    mode_group = agent_parser.add_mutually_exclusive_group()
    mode_group.add_argument(
        "--copy",
        action="store_true",
        help="Copy instruction files to workspace (default). Works with sandboxed agents.",
    )
    mode_group.add_argument(
        "--pointer",
        action="store_true",
        help="Just add pointer to installed package. Requires agent has file system access.",
    )

    agent_parser.add_argument(
        "--print",
        action="store_true",
        help="Print to stdout instead of writing file",
    )
    agent_parser.add_argument(
        "--workspace",
        help="Workspace root (auto-detected if not specified)",
    )

    args = parser.parse_args()

    if args.command == "agent":
        return cmd_agent(args)
    else:
        parser.print_help()
        return 0


if __name__ == "__main__":
    sys.exit(main())
