"""Configuration management commands for CleverAgents CLI.

The ``agents config`` command group manages runtime configuration values
persisted in a TOML file at ``~/.cleveragents/config.toml``.

## Commands

| Command                         | Description                             |
|---------------------------------|-----------------------------------------|
| ``agents config set <K> <V>``   | Set a configuration value               |
| ``agents config get <K>``       | Get a configuration value               |
| ``agents config list [PATTERN]``| List configuration values               |

## Key Format

Keys use the hierarchical dot-path names defined in the specification
(e.g. ``core.log.level``, ``plan.budget.per-plan``).  Resolution is
delegated to :class:`ConfigService` which implements the five-level
precedence chain.

Based on implementation_plan.md task A8.cli.
"""

from __future__ import annotations

import fnmatch
import re
from pathlib import Path
from typing import Annotated, Any

import typer
from rich.panel import Panel
from rich.table import Table

from cleveragents.application.container import get_container
from cleveragents.application.services.config_service import (
    _REGISTRY,
    ConfigScope,
    ConfigService,
)
from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.cli.renderers import _get_console

app = typer.Typer(help="Manage configuration settings for CleverAgents.")
console = _get_console()

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

_CONFIG_DIR = Path.home() / ".cleveragents"
_CONFIG_PATH = _CONFIG_DIR / "config.toml"

_SECRET_PATTERNS: re.Pattern[str] = re.compile(
    r"(api[_\-]?key|token|secret|password)", re.IGNORECASE
)

_FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)"


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _get_service() -> ConfigService:
    """Return a ``ConfigService`` wired to the standard config paths."""
    container = get_container()
    return ConfigService(
        config_dir=_CONFIG_DIR,
        config_path=_CONFIG_PATH,
        event_bus=container.event_bus(),
    )


def _is_secret_key(key: str) -> bool:
    """Return ``True`` when *key* looks like it stores a secret."""
    return _SECRET_PATTERNS.search(key) is not None


def _mask_value(value: str) -> str:
    """Replace a value with ``****``."""
    return "****"


# Legacy helpers retained for backward compatibility with tests that
# patch or call them directly.  They now delegate to ConfigService.


def _settings_fields() -> dict[str, Any]:
    """Return all registered config keys mapped to their resolved values."""
    svc = _get_service()
    results: dict[str, Any] = {}
    for key in _REGISTRY:
        resolved = svc.resolve(key)
        results[key] = resolved.value
    return results


def _settings_defaults() -> dict[str, Any]:
    """Return all registered config keys mapped to their default values."""
    return {key: entry.default for key, entry in _REGISTRY.items()}


def _normalize_key(key: str) -> str:
    """Pass-through normalisation — keys are matched as-is against the registry.

    Backward compatibility: underscores are converted to dots so that
    ``log_level`` resolves to ``core.log.level`` if there is a single
    match.  If the key already exists in the registry, it is returned
    unchanged.
    """
    stripped = key.strip()
    if stripped in _REGISTRY:
        return stripped
    # Try underscore-to-dot conversion
    dotted = stripped.replace("_", ".")
    if dotted in _REGISTRY:
        return dotted
    return stripped


def _validate_key(key: str) -> str:
    """Validate and normalise *key*, raising :class:`typer.BadParameter` on failure."""
    normalized = _normalize_key(key)
    if not normalized:
        raise typer.BadParameter("Key must not be empty.")
    if normalized not in _REGISTRY:
        valid_sample = ", ".join(sorted(_REGISTRY)[:8])
        raise typer.BadParameter(
            f"Unknown configuration key: '{key}' "
            f"(normalized: '{normalized}'). "
            f"Valid keys include: {valid_sample} ..."
        )
    return normalized


def _env_var_for_key(key: str) -> str:
    """Return the environment-variable name for a registered key."""
    entry = _REGISTRY.get(key)
    if entry is not None:
        return entry.env_var
    return f"CLEVERAGENTS_{key.upper().replace('.', '_').replace('-', '_')}"


def _resolve_source(key: str) -> str:
    """Determine which level supplied the winning value for *key*."""
    svc = _get_service()
    try:
        resolved = svc.resolve(key)
        return resolved.source.value
    except (ValueError, KeyError):
        return "default"


def _resolution_chain(key: str) -> list[dict[str, Any]]:
    """Build the resolution chain for *key* via ConfigService."""
    svc = _get_service()
    try:
        resolved = svc.resolve(key, verbose=True)
        return resolved.chain
    except (ValueError, KeyError):
        return []


def _read_config_file() -> dict[str, Any]:
    """Read the TOML config file and return its contents as a flat dict."""
    svc = _get_service()
    return svc.read_config()


def _write_config_file(data: dict[str, Any]) -> None:
    """Write *data* to the TOML config file, creating dirs if needed."""
    svc = _get_service()
    svc.write_config(data)


# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------


@app.command("set")
def config_set(
    key: Annotated[str, typer.Argument(help="Configuration key (dot-path)")],
    value: Annotated[str, typer.Argument(help="Value to set")],
    scope: Annotated[
        str | None,
        typer.Option(
            "--scope",
            "-s",
            help="Config scope: global, project, or local (default: global)",
        ),
    ] = None,
    project: Annotated[
        str | None,
        typer.Option("--project", "-p", help="Project name for scoped override"),
    ] = None,
    fmt: Annotated[str, typer.Option("--format", "-f", help=_FORMAT_HELP)] = "rich",
) -> None:
    """Set a configuration value.

    Writes the value to the config file for the specified scope.
    Scopes: ``global`` (~/.cleveragents/config.toml),
    ``project`` (<project-root>/config.toml),
    ``local`` (<project-root>/config.local.toml).

    Examples::

        agents config set core.log.level DEBUG
        agents config set plan.concurrency 8 --scope local
        agents config set core.automation-profile manual --project local/prod
    """
    normalized = _validate_key(key)
    svc = _get_service()
    entry = _REGISTRY[normalized]

    # Coerce to the registered type
    coerced: Any = svc.validate_type(normalized, value)

    # Resolve the target scope
    config_scope: ConfigScope | None = None
    if scope is not None:
        try:
            config_scope = ConfigScope(scope.lower())
        except ValueError as exc:
            raise typer.BadParameter(
                f"Invalid scope: '{scope}'. Use 'global', 'project', or 'local'."
            ) from exc

    # Read previous value
    config_data = svc.read_config()

    if project is not None:
        # Legacy project-scoped set (--project flag)
        if not entry.project_scopable:
            raise typer.BadParameter(f"Key '{normalized}' is not project-scopable.")
        project_section = config_data.get("project", {})
        if not isinstance(project_section, dict):
            project_section = {}
        proj_overrides = project_section.get(project, {})
        if not isinstance(proj_overrides, dict):
            proj_overrides = {}
        previous = proj_overrides.get(normalized)
        proj_overrides[normalized] = coerced
        project_section[project] = proj_overrides
        config_data["project"] = project_section
        svc.write_config(config_data)
        scope_display = f"project:{project}"
    elif config_scope is not None:
        # New three-scope set (--scope flag)
        if config_scope == ConfigScope.GLOBAL:
            previous = config_data.get(normalized)
        elif config_scope == ConfigScope.PROJECT:
            previous = svc.read_project_config().get(normalized)
        else:
            previous = svc.read_local_config().get(normalized)
        svc.set_value(normalized, coerced, scope=config_scope)
        scope_display = config_scope.value
    else:
        previous = config_data.get(normalized)
        config_data[normalized] = coerced
        svc.write_config(config_data)
        scope_display = "global"

    result: dict[str, Any] = {
        "key": normalized,
        "value": coerced,
        "previous_value": previous,
        "source": "config_file",
        "scope": scope_display,
    }

    if fmt != OutputFormat.RICH.value:
        typer.echo(format_output(result, fmt))
        return

    prev_display = str(previous) if previous is not None else "(unset)"
    console.print(
        Panel(
            f"[bold]Key:[/bold] {normalized}\n"
            f"[bold]Value:[/bold] {coerced}\n"
            f"[bold]Previous:[/bold] {prev_display}\n"
            f"[bold]Source:[/bold] config_file\n"
            f"[bold]Scope:[/bold] {scope_display}",
            title="Configuration Updated",
            expand=False,
        )
    )


@app.command("get")
def config_get(
    key: Annotated[str, typer.Argument(help="Configuration key (dot-path)")],
    verbose: Annotated[
        bool,
        typer.Option("--verbose", "-v", help="Show full resolution chain"),
    ] = False,
    project: Annotated[
        str | None,
        typer.Option("--project", "-p", help="Project name for scoped resolution"),
    ] = None,
    fmt: Annotated[str, typer.Option("--format", "-f", help=_FORMAT_HELP)] = "rich",
) -> None:
    """Get a configuration value with its resolution chain.

    Shows the effective value and which source it was resolved from
    (CLI flag, environment variable, project config, global file, or
    default).

    Use ``--verbose`` to display the full five-level resolution chain.

    Examples::

        agents config get core.log.level
        agents config get plan.budget.per-plan --verbose
        agents config get core.automation-profile --project local/prod
    """
    normalized = _validate_key(key)
    svc = _get_service()

    resolved = svc.resolve(
        normalized,
        project_name=project,
        verbose=verbose,
    )

    result: dict[str, Any] = {
        "key": normalized,
        "value": resolved.value,
        "source": resolved.source.value,
        "type": type(resolved.value).__name__ if resolved.value is not None else "None",
    }
    if verbose:
        result["resolution_chain"] = resolved.chain

    if fmt != OutputFormat.RICH.value:
        # Serialise Path objects to strings for JSON/YAML
        if hasattr(result["value"], "__fspath__"):
            result["value"] = str(result["value"])
        for entry in result.get("resolution_chain", []):
            if hasattr(entry.get("value"), "__fspath__"):
                entry["value"] = str(entry["value"])
        typer.echo(format_output(result, fmt))
        return

    console.print(
        Panel(
            f"[bold]Key:[/bold] {normalized}\n"
            f"[bold]Value:[/bold] {resolved.value}\n"
            f"[bold]Source:[/bold] {resolved.source.value}\n"
            "[bold]Type:[/bold] "
            + (type(resolved.value).__name__ if resolved.value is not None else "None"),
            title="Configuration Value",
            expand=False,
        )
    )

    if verbose and resolved.chain:
        console.print("\n[bold]Resolution chain[/bold] (highest → lowest priority):")
        for chain_entry in resolved.chain:
            src = chain_entry["source"]
            val = chain_entry.get("value")
            is_winner = src == resolved.source.value and val is not None
            marker = " [green]◀ active[/green]" if is_winner else ""
            display_val = str(val) if val is not None else "(not set)"
            console.print(f"  {src:14s} → {display_val}{marker}")


@app.command("list")
def config_list(
    pattern: Annotated[
        str | None,
        typer.Argument(help="Glob or regex filter for key names (e.g. 'plan.*')"),
    ] = None,
    filter_values: Annotated[
        str | None,
        typer.Option("--filter-values", help="Regex filter for values"),
    ] = None,
    show_secrets: Annotated[
        bool,
        typer.Option("--show-secrets", help="Show secret values unmasked"),
    ] = False,
    project: Annotated[
        str | None,
        typer.Option("--project", "-p", help="Project name for scoped resolution"),
    ] = None,
    fmt: Annotated[str, typer.Option("--format", "-f", help=_FORMAT_HELP)] = "rich",
) -> None:
    """List all configuration values.

    Optionally filter by key glob pattern (e.g. ``plan.*``, ``context.pipeline.*``)
    and/or value regex.  Secret values (API keys, tokens, passwords) are
    masked by default; use ``--show-secrets`` to reveal them.

    Examples::

        agents config list
        agents config list "plan.*"
        agents config list "context.pipeline.*"
        agents config list --filter-values "DEBUG"
        agents config list --show-secrets
    """
    # Compile regex patterns (fail fast on invalid regex)
    key_re: re.Pattern[str] | None = None
    val_re: re.Pattern[str] | None = None

    if pattern is not None:
        try:
            key_re = re.compile(pattern)
        except re.error as exc:
            console.print(f"[red]Invalid key regex:[/red] {exc}")
            raise typer.Exit(code=1) from exc

    if filter_values is not None:
        try:
            val_re = re.compile(filter_values)
        except re.error as exc:
            console.print(f"[red]Invalid value regex:[/red] {exc}")
            raise typer.Exit(code=1) from exc

    svc = _get_service()

    # When --project is given, show only project-scoped overrides
    if project is not None:
        proj_overrides = svc.get_project_overrides(project)
        settings_list: list[dict[str, Any]] = []
        for key in sorted(proj_overrides):
            raw_value = proj_overrides[key]
            str_value = str(raw_value) if raw_value is not None else ""

            # Key filter
            if (
                key_re is not None
                and not key_re.search(key)
                and (pattern is None or not fnmatch.fnmatch(key, pattern))
            ):
                continue

            # Value filter
            if val_re is not None and not val_re.search(str_value):
                continue

            # Mask secrets unless --show-secrets
            display_value: Any = raw_value
            if _is_secret_key(key) and not show_secrets and raw_value is not None:
                display_value = _mask_value(str(raw_value))

            settings_list.append(
                {
                    "key": key,
                    "value": display_value,
                    "source": f"project:{project}",
                    "modified": True,
                }
            )

        if not settings_list:
            console.print(
                "[yellow]No project-scoped overrides match the filter.[/yellow]"
            )
            return

        if fmt != OutputFormat.RICH.value:
            for entry in settings_list:
                if hasattr(entry["value"], "__fspath__"):
                    entry["value"] = str(entry["value"])
            typer.echo(format_output(settings_list, fmt))
            return

        table = Table(
            title=f"Project '{project}' overrides ({len(settings_list)} settings)"
        )
        table.add_column("Key", style="cyan")
        table.add_column("Value", style="white")
        table.add_column("Source", style="yellow")
        table.add_column("Modified", justify="center")

        for entry in settings_list:
            mod_marker = "[green]yes[/green]" if entry["modified"] else ""
            table.add_row(
                entry["key"],
                str(entry["value"]),
                entry["source"],
                mod_marker,
            )

        console.print(table)
        return

    all_resolved = svc.resolve_all(project_name=project)
    defaults = _settings_defaults()

    settings_list_all: list[dict[str, Any]] = []
    for key in sorted(_REGISTRY):
        # Key filter: support both glob (fnmatch) and regex
        if (
            key_re is not None
            and not key_re.search(key)
            and (pattern is None or not fnmatch.fnmatch(key, pattern))
        ):
            continue

        resolved = all_resolved[key]
        raw_value = resolved.value
        str_value = str(raw_value) if raw_value is not None else ""

        # Value filter
        if val_re is not None and not val_re.search(str_value):
            continue

        # Modified flag: value differs from default
        default_val = defaults.get(key)
        modified = raw_value != default_val

        # Mask secrets unless --show-secrets
        display_value: Any = raw_value
        if _is_secret_key(key) and not show_secrets and raw_value is not None:
            display_value = _mask_value(str(raw_value))

        settings_list_all.append(
            {
                "key": key,
                "value": display_value,
                "source": resolved.source.value,
                "modified": modified,
            }
        )

    if not settings_list_all:
        console.print("[yellow]No configuration values match the filter.[/yellow]")
        return

    if fmt != OutputFormat.RICH.value:
        # Serialise Path objects for JSON/YAML
        for entry in settings_list_all:
            if hasattr(entry["value"], "__fspath__"):
                entry["value"] = str(entry["value"])
        typer.echo(format_output(settings_list_all, fmt))
        return

    table = Table(title=f"Configuration ({len(settings_list_all)} settings)")
    table.add_column("Key", style="cyan")
    table.add_column("Value", style="white")
    table.add_column("Source", style="yellow")
    table.add_column("Modified", justify="center")

    for entry in settings_list_all:
        mod_marker = "[green]yes[/green]" if entry["modified"] else ""
        table.add_row(
            entry["key"],
            str(entry["value"]),
            entry["source"],
            mod_marker,
        )

    console.print(table)
