"""Configuration management commands for CleverAgents CLI.

Manages runtime configuration values persisted in
``~/.cleveragents/config.toml``.  Commands: ``set``, ``get``, ``list``.
Keys use hierarchical dot-path names (e.g. ``core.log.level``).
Resolution is delegated to :class:`ConfigService`.

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

from __future__ import annotations

import fnmatch
import json
import re
import time
from datetime import UTC, datetime
from pathlib import Path
from typing import Annotated, Any

import typer
import yaml
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.commands._config_display import (
    build_origin,
    build_spec_resolution_chain,
    find_winner_in_chain,
    spec_source_short,
    spec_type_name,
)
from cleveragents.cli.commands._config_helpers import (
    _is_secret_key,
    _mask_value,
    _settings_defaults,
    _validate_key,
)
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()

_CONFIG_DIR = Path.home() / ".cleveragents"
_CONFIG_PATH = _CONFIG_DIR / "config.toml"
_FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)"


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 _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 _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)


@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.

    Scopes: ``global``, ``project``, ``local``.

    Examples::

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

    # Coerce to the registered type
    coerced: Any = svc.validate_type(normalized, value)
    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
    config_data = svc.read_config()
    if project is not None:
        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:
        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)")],
    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",
    verbose: Annotated[
        bool,
        typer.Option(
            "--verbose",
            "-v",
            help="[Deprecated] No-op; resolution chain is always shown.",
        ),
    ] = False,
) -> None:
    """Get a configuration value with its resolution chain.

    Shows the Config, Origin, and Resolution Chain panels per spec.
    The full chain and Winner indicator are always displayed.

    Examples::

        agents config get core.log.level
        agents config get core.automation-profile --project local/prod
    """
    started_at = datetime.now(tz=UTC)
    t0 = time.monotonic()
    normalized = _validate_key(key)
    svc = _get_service()
    entry = _REGISTRY[normalized]
    # Always verbose — spec requires the full chain in all output formats.
    resolved = svc.resolve(normalized, project_name=project, verbose=True)
    elapsed_ms = round((time.monotonic() - t0) * 1000)
    overridden = resolved.value != entry.default
    spec_chain = build_spec_resolution_chain(resolved.chain)
    winner = find_winner_in_chain(resolved.chain, resolved.source.value)
    origin = build_origin(resolved.chain, resolved.source.value, entry.default)
    spec_type = spec_type_name(resolved.value)
    short_source = spec_source_short(resolved.source.value)
    display_value: Any = resolved.value
    if hasattr(display_value, "__fspath__"):
        display_value = str(display_value)

    if fmt != OutputFormat.RICH.value:
        data: dict[str, Any] = {
            "key": normalized,
            "value": display_value,
            "source": short_source,
            "overridden": overridden,
            "type": spec_type,
            "origin": origin,
            "resolution_chain": spec_chain,
            "winner": winner,
        }
        # Serialise Path objects in chain values
        for chain_entry in data["resolution_chain"]:
            if hasattr(chain_entry.get("value"), "__fspath__"):
                chain_entry["value"] = str(chain_entry["value"])

        envelope: dict[str, Any] = {
            "command": "config get",
            "status": "ok",
            "exit_code": 0,
            "data": data,
            "timing": {
                "started": started_at.isoformat(),
                "duration_ms": elapsed_ms,
            },
            "messages": ["Config read"],
        }
        if fmt == OutputFormat.YAML.value:
            typer.echo(
                yaml.safe_dump(envelope, default_flow_style=False, sort_keys=False)
            )
        else:
            typer.echo(json.dumps(envelope, indent=2, default=str))
        return

    # Rich output: three panels per spec
    overridden_display = "yes" if overridden else "no"
    console.print(
        Panel(
            f"[bold]Key:[/bold] {normalized}\n"
            f"[bold]Value:[/bold] {display_value}\n"
            f"[bold]Source:[/bold] {short_source}\n"
            f"[bold]Overridden:[/bold] {overridden_display}\n"
            f"[bold]Type:[/bold] {spec_type}",
            title="Config",
            expand=False,
        )
    )
    origin_file = origin.get("file") or "(unknown)"
    origin_line = origin.get("line")
    origin_default = origin.get("default")
    origin_line_display = str(origin_line) if origin_line is not None else "(unknown)"
    origin_default_display = (
        str(origin_default) if origin_default is not None else "(none)"
    )
    console.print(
        Panel(
            f"[bold]File:[/bold] {origin_file}\n"
            f"[bold]Line:[/bold] {origin_line_display}\n"
            f"[bold]Default:[/bold] {origin_default_display}",
            title="Origin",
            expand=False,
        )
    )
    chain_lines: list[str] = []
    for chain_entry in spec_chain:
        val = chain_entry.get("value")
        val_display = str(val) if val is not None else "(not set)"
        chain_lines.append(
            f"{chain_entry['level']}. {chain_entry['source']}: {val_display}"
        )
    winner_src = winner.get("source", "")
    winner_level = winner.get("level", "")
    chain_lines.append(f"[bold]Winner:[/bold] {winner_src} (level {winner_level})")

    console.print(
        Panel(
            "\n".join(chain_lines),
            title="Resolution Chain",
            expand=False,
        )
    )

    console.print("[green]✓ OK[/green] Config read")


@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.

    Filter by key glob (e.g. ``plan.*``) and/or value regex.
    Secrets are masked by default; use ``--show-secrets`` to reveal.

    Examples::

        agents config list "plan.*"
        agents config list --filter-values "DEBUG" --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()
    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 ""

            if (
                key_re is not None
                and not key_re.search(key)
                and (pattern is None or not fnmatch.fnmatch(key, pattern))
            ):
                continue
            if val_re is not None and not val_re.search(str_value):
                continue
            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

        default_val = defaults.get(key)
        modified = raw_value != default_val
        display_value = 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:
        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)
