"""Validation management commands for CleverAgents CLI.

The ``agents validation`` command group manages validations (pass/fail tool
subtypes) and their lifecycle attachments to resources.

## Commands

| Command                        | Description                              |
|--------------------------------|------------------------------------------|
| ``agents validation add``      | Register validation from YAML config     |
| ``agents validation attach``   | Attach validation to a resource          |
| ``agents validation detach``   | Detach a validation attachment           |

## Config-Based Add

Validations are registered via a YAML configuration file, with optional
``--required`` or ``--informational`` flags to override the mode:

```bash
agents validation add --config ./validations/coverage-check.yaml
agents validation add --config ./validations/coverage-check.yaml --required
```

### YAML Configuration File

```yaml
name: local/coverage-check
description: Check code coverage meets threshold
source: custom
mode: required
code: |
  def run(inputs):
      return {"passed": inputs["coverage"] >= 80}
```

## Attach / Detach Lifecycle

```bash
# Attach validation to a resource
agents validation attach git-checkout/my-repo local/coverage-check

# Detach by attachment ID
agents validation detach 01HXYZ1234567890ABCDEFGHIJ
```

Based on implementation_plan.md -- Task C1.tool.cli.
"""

from __future__ import annotations

from pathlib import Path
from typing import Annotated, Any

import typer
import yaml
from rich.console import Console
from rich.panel import Panel

from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.core.exceptions import (
    CleverAgentsError,
    NotFoundError,
    ToolTypeMismatchError,
    ValidationError,
)
from cleveragents.domain.models.core.tool import Validation, ValidationMode

# Create sub-app for validation commands
app = typer.Typer(help="Manage validations (pass/fail tools) and resource attachments.")
console = Console()

# Reusable --format option description
_FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)"


def _get_tool_registry_service() -> Any:
    """Get the ToolRegistryService from the DI container.

    Delegates to get_container().tool_registry_service() so that
    service wiring is managed exclusively by the container (Forgejo #3006).
    """
    from cleveragents.application.container import get_container

    return get_container().tool_registry_service()


def _validation_spec_dict(tool: Any) -> dict[str, Any]:
    """Return validation data as a dict for CLI rendering."""
    if hasattr(tool, "as_cli_dict"):
        return dict(tool.as_cli_dict())

    if isinstance(tool, dict):
        result: dict[str, Any] = {
            "name": tool.get("name", ""),
            "description": tool.get("description", ""),
            "source": tool.get("source", ""),
            "tool_type": tool.get("tool_type", "validation"),
            "mode": tool.get("mode", "required"),
        }
        if tool.get("wraps"):
            result["wraps"] = tool["wraps"]
        if tool.get("transform"):
            result["transform"] = tool["transform"]
        return result

    return {"name": str(tool)}


def _attachment_dict(attachment: Any) -> dict[str, Any]:
    """Convert an attachment to a plain dict for output formatting."""
    if isinstance(attachment, dict):
        return {
            "attachment_id": attachment.get("attachment_id", ""),
            "validation_name": attachment.get("validation_name", ""),
            "resource_id": attachment.get("resource_id", ""),
            "mode": attachment.get("mode", "required"),
            "project_name": attachment.get("project_name"),
            "plan_id": attachment.get("plan_id"),
            "created_at": attachment.get("created_at", ""),
        }
    return {
        "attachment_id": getattr(attachment, "attachment_id", ""),
        "validation_name": getattr(attachment, "validation_name", ""),
        "resource_id": getattr(attachment, "resource_id", ""),
        "mode": getattr(attachment, "mode", "required"),
        "project_name": getattr(attachment, "project_name", None),
        "plan_id": getattr(attachment, "plan_id", None),
        "created_at": str(getattr(attachment, "created_at", "")),
    }


def _print_validation(
    tool: Any,
    title: str = "Validation",
    fmt: str = OutputFormat.RICH.value,
) -> None:
    """Print validation details in the requested format."""
    data = _validation_spec_dict(tool)
    if fmt != OutputFormat.RICH.value:
        console.print(format_output(data, fmt))
        return

    name = data.get("name", "")
    desc = data.get("description", "")
    source = data.get("source", "")
    mode = data.get("mode", "required")

    details = (
        f"[bold]Name:[/bold] {name}\n"
        f"[bold]Description:[/bold] {desc}\n"
        f"[bold]Source:[/bold] {source}\n"
        f"[bold]Mode:[/bold] {mode}"
    )

    wraps = data.get("wraps")
    if wraps:
        details += f"\n[bold]Wraps:[/bold] {wraps}"
    transform = data.get("transform")
    if transform:
        details += f"\n[bold]Transform:[/bold] {transform}"

    console.print(Panel(details, title=title, expand=False))


@app.command("add")
def add(
    config: Annotated[
        Path,
        typer.Option(
            "--config",
            "-c",
            help="Path to the validation YAML configuration file",
            exists=False,
        ),
    ],
    required: Annotated[
        bool,
        typer.Option(
            "--required",
            help="Set validation mode to 'required' (overrides YAML config)",
        ),
    ] = False,
    informational: Annotated[
        bool,
        typer.Option(
            "--informational",
            help="Set validation mode to 'informational' (overrides YAML config)",
        ),
    ] = False,
    update: Annotated[
        bool,
        typer.Option("--update", help="Update if validation already exists"),
    ] = False,
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Register a new validation from a YAML configuration file.

    The validation is fully defined by the YAML configuration file
    specified with ``--config``.  Optionally, ``--required`` or
    ``--informational`` can override the ``mode`` field in the YAML.

    Examples:
        agents validation add --config ./validations/coverage-check.yaml
        agents validation add --config ./validations/coverage-check.yaml --required
        agents validation add --config ./validations/coverage-check.yaml --update
    """
    try:
        if required and informational:
            console.print(
                "[red]Error:[/red] --required and --informational are "
                "mutually exclusive"
            )
            raise typer.Abort()

        if not config.exists():
            raise FileNotFoundError(f"Config file not found: {config}")

        raw = config.read_text(encoding="utf-8")
        config_dict = yaml.safe_load(raw)
        if not isinstance(config_dict, dict):
            raise ValueError("YAML config must be a mapping")

        # Apply CLI mode override before building the Validation object.
        if required:
            config_dict["mode"] = ValidationMode.REQUIRED.value
        elif informational:
            config_dict["mode"] = ValidationMode.INFORMATIONAL.value

        validation = Validation.from_config(config_dict)
        service = _get_tool_registry_service()

        if update:
            existing = service.get_tool(validation.name)
            if existing is not None:
                registered = service.update_tool(validation)
                _print_validation(registered, title="Validation Updated", fmt=fmt)
                return

        registered = service.register_tool(validation)
        _print_validation(registered, title="Validation Registered", fmt=fmt)

    except FileNotFoundError as exc:
        console.print(f"[red]Config file error:[/red] {exc}")
        raise typer.Abort() from exc
    except yaml.YAMLError as exc:
        console.print(f"[red]YAML parse error:[/red] {exc}")
        raise typer.Abort() from exc
    except ValueError as exc:
        console.print(f"[red]Schema validation error:[/red] {exc}")
        raise typer.Abort() from exc
    except ValidationError as exc:
        console.print(f"[red]Validation Error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except CleverAgentsError as exc:
        console.print(f"[red]Error:[/red] {exc.message}")
        raise typer.Abort() from exc


@app.command(
    "attach",
    context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
)
def attach(
    ctx: typer.Context,
    resource: Annotated[
        str,
        typer.Argument(help="Resource reference to attach validation to"),
    ],
    validation_name: Annotated[
        str,
        typer.Argument(help="Namespaced name of the validation"),
    ],
    project: Annotated[
        str | None,
        typer.Option("--project", "-p", help="Project scope"),
    ] = None,
    plan_id: Annotated[
        str | None,
        typer.Option("--plan", help="Plan ID scope"),
    ] = None,
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Attach a validation to a resource.

    The validation mode (required or informational) is determined by the
    validation's registered definition, not overridden at attach time.

    Validation-specific arguments are passed as named options using the
    ``--key value`` format (e.g. ``--coverage-threshold 90``).  Hyphens in
    option names are converted to underscores in the stored argument dict
    (e.g. ``--coverage-threshold`` becomes ``coverage_threshold``).

    Examples:
        agents validation attach git-checkout/my-repo local/coverage-check
        agents validation attach --project myproj git-checkout/my-repo local/lint
        agents validation attach --project local/api-service \\
            local/api-repo local/run-tests --coverage-threshold 90
    """
    try:
        # Parse extra named options from ctx.args (--key value format).
        # ctx.args contains all unrecognised tokens after Typer's own parsing.
        extra_args: dict[str, str] | None = None
        raw_extra = list(ctx.args)
        if raw_extra:
            extra_args = {}
            i = 0
            while i < len(raw_extra):
                token = raw_extra[i]
                if not token.startswith("--"):
                    console.print(
                        f"[red]Invalid argument format:[/red] {token!r} "
                        "(expected --key value named option format, "
                        "e.g. --coverage-threshold 90)"
                    )
                    raise typer.Abort()
                key = token[2:].replace(
                    "-", "_"
                )  # --coverage-threshold → coverage_threshold
                if i + 1 >= len(raw_extra) or raw_extra[i + 1].startswith("--"):
                    console.print(
                        f"[red]Missing value for option:[/red] {token} "
                        "(expected --key value, e.g. --coverage-threshold 90)"
                    )
                    raise typer.Abort()
                val = raw_extra[i + 1]
                extra_args[key] = val
                i += 2

        service = _get_tool_registry_service()
        attachment = service.attach_validation(
            validation_name=validation_name,
            resource_id=resource,
            project_name=project,
            plan_id=plan_id,
            args=extra_args,
        )

        att_data = _attachment_dict(attachment)

        if fmt != OutputFormat.RICH.value:
            console.print(format_output(att_data, fmt))
            return

        att_id = att_data.get("attachment_id", "")
        console.print(
            f"[green]Attached validation:[/green] {validation_name} -> "
            f"{resource} (id: {att_id})"
        )

    except NotFoundError as exc:
        console.print(f"[red]Validation not found:[/red] {validation_name}")
        raise typer.Abort() from exc
    except ToolTypeMismatchError as exc:
        console.print(f"[red]Type error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except ValidationError as exc:
        console.print(f"[red]Validation Error:[/red] {exc.message}")
        raise typer.Abort() from exc
    except CleverAgentsError as exc:
        console.print(f"[red]Error:[/red] {exc.message}")
        raise typer.Abort() from exc


@app.command("detach")
def detach(
    attachment_id: Annotated[
        str,
        typer.Argument(help="Attachment ID to detach"),
    ],
    yes: Annotated[
        bool,
        typer.Option("--yes", "-y", help="Skip confirmation prompt"),
    ] = False,
    fmt: Annotated[
        str,
        typer.Option("--format", "-f", help=_FORMAT_HELP),
    ] = "rich",
) -> None:
    """Detach a validation from a resource.

    Examples:
        agents validation detach 01HXYZ1234567890ABCDEFGHIJ
        agents validation detach --yes 01HXYZ1234567890ABCDEFGHIJ
    """
    try:
        if not yes:
            confirm = typer.confirm(f"Detach validation attachment '{attachment_id}'?")
            if not confirm:
                console.print("[yellow]Aborted.[/yellow]")
                raise typer.Abort()

        service = _get_tool_registry_service()
        removed = service.detach_validation(attachment_id)

        if not removed:
            console.print(f"[red]Attachment not found:[/red] {attachment_id}")
            raise typer.Abort()

        if fmt != OutputFormat.RICH.value:
            data = {"attachment_id": attachment_id, "detached": True}
            console.print(format_output(data, fmt))
            return

        console.print(f"[green]Detached validation:[/green] {attachment_id}")

    except CleverAgentsError as exc:
        console.print(f"[red]Error:[/red] {exc.message}")
        raise typer.Abort() from exc
