"""Configuration command handling for user-driven config changes."""

from __future__ import annotations

import asyncio
import shlex
from typing import TYPE_CHECKING, Any

import yaml
from pydantic import ValidationError

from mindroom import yaml_io
from mindroom.api import config_lifecycle
from mindroom.config.main import (
    Config,
    ConfigRuntimeValidationError,
    format_invalid_config_message,
    load_config_or_user_error,
)
from mindroom.logging_config import get_logger
from mindroom.redaction import redact_sensitive_data

if TYPE_CHECKING:
    from mindroom.constants import RuntimePaths

logger = get_logger(__name__)

_CONFIG_CHANGE_REJECTED_MESSAGE = "Changes were NOT applied."


def _parse_config_args(args_text: str) -> tuple[str, list[str]]:
    """Parse config command arguments.

    Args:
        args_text: Raw argument text from command

    Returns:
        Tuple of (operation, arguments)

    """
    if not args_text:
        return "show", []

    # Use shlex to handle quoted strings properly
    try:
        parts = shlex.split(args_text)
    except ValueError as e:
        # Handle parsing errors (e.g., unmatched quotes)
        # Return a special operation that will trigger an error message
        return "parse_error", [str(e)]

    if not parts:
        return "show", []

    operation = parts[0].lower()
    args = parts[1:] if len(parts) > 1 else []
    return operation, args


def _get_nested_value(data: Any, path: str) -> Any:  # noqa: ANN401
    """Get a value from nested dict using dot notation.

    Args:
        data: The dictionary to search
        path: Dot-separated path (e.g., "agents.analyst.display_name")

    Returns:
        The value at the path

    Raises:
        KeyError: If path doesn't exist

    """
    keys = path.split(".")
    current = data

    for key in keys:
        # Handle array indexing
        if key.isdigit():  # noqa: SIM108
            current = current[int(key)]
        else:
            current = current[key]

    return current


def _set_nested_value(data: Any, path: str, value: Any) -> None:  # noqa: ANN401
    """Set a value in nested dict using dot notation.

    Args:
        data: The dictionary to modify
        path: Dot-separated path (e.g., "agents.analyst.display_name")
        value: Value to set

    Raises:
        KeyError: If parent path doesn't exist

    """
    keys = path.split(".")
    current = data

    # Navigate to the parent of the target
    for key in keys[:-1]:
        if key.isdigit():
            current = current[int(key)]
        elif key not in current:
            # Auto-create missing intermediate dicts
            current[key] = {}
            current = current[key]
        else:
            current = current[key]

    # Set the final value
    final_key = keys[-1]
    if final_key.isdigit():
        current[int(final_key)] = value
    else:
        current[final_key] = value


def _parse_value(value_str: str) -> Any:  # noqa: ANN401
    """Parse a string value into appropriate Python type.

    Args:
        value_str: String representation of value

    Returns:
        Parsed value (str, int, float, bool, list, or dict)

    """
    # Try to parse as YAML first (handles unquoted strings in arrays/dicts)
    # YAML is a superset of JSON, so this handles both formats
    # Examples that work:
    #   [item1, item2]          -> ['item1', 'item2']
    #   ["item1", "item2"]      -> ['item1', 'item2']
    #   {key: value}            -> {'key': 'value'}
    #   {"key": "value"}        -> {'key': 'value'}
    try:
        return yaml_io.safe_load(value_str)
    except yaml.YAMLError:
        pass

    # If YAML parsing fails, return as string
    # This handles cases where the string itself contains special YAML characters
    return value_str


def _format_value(value: Any) -> str:  # noqa: ANN401
    """Format a value for display as YAML.

    Args:
        value: Value to format

    Returns:
        YAML formatted string representation

    """
    # Use yaml.dump for consistent formatting
    yaml_str = yaml.dump(value, default_flow_style=False, sort_keys=False, allow_unicode=True)
    # Remove trailing newline and document end marker that yaml.dump adds
    yaml_str = yaml_str.rstrip()
    if yaml_str.endswith("..."):
        yaml_str = yaml_str[:-3].rstrip()
    return yaml_str


def _display_key_for_path(path: str) -> str | None:
    for part in reversed(path.split(".")):
        if not part.isdigit():
            return part
    return None


def _redact_value_for_display(value: Any, path: str | None = None) -> Any:  # noqa: ANN401
    if path is None:
        return redact_sensitive_data(value)
    key = _display_key_for_path(path)
    if key is None:
        return redact_sensitive_data(value)
    redacted = redact_sensitive_data({key: value})
    return redacted[key] if isinstance(redacted, dict) else redacted


async def handle_config_command(  # noqa: C901, PLR0911, PLR0912
    args_text: str,
    runtime_paths: RuntimePaths,
) -> tuple[str, dict[str, Any] | None]:
    """Handle config command execution.

    Args:
        args_text: The command arguments
        runtime_paths: Runtime context carrying the active config path

    Returns:
        Tuple of (response message, config change dict or None)
        The config change dict contains info needed for confirmation

    """
    operation, args = _parse_config_args(args_text)
    path = runtime_paths.config_path
    load_error_footer = _CONFIG_CHANGE_REJECTED_MESSAGE if operation == "set" else None

    # Config loading and validation execute plugin modules and walk the
    # filesystem; keep them off the event loop (#1260).
    config, load_error = await asyncio.to_thread(
        load_config_or_user_error,
        runtime_paths,
        footer=load_error_footer,
        tolerate_plugin_load_errors=True,
    )
    if load_error:
        return load_error, None
    assert config is not None
    config_dict = config.authored_model_dump()

    if operation == "show":
        # Show entire config
        safe_config_dict = _redact_value_for_display(config_dict)
        yaml_str = yaml.dump(safe_config_dict, default_flow_style=False, sort_keys=False, allow_unicode=True)
        return f"**Current Configuration:**\n```yaml\n{yaml_str}```", None

    if operation == "get":
        if not args:
            return (
                "❌ Please specify a configuration path to get\nExample: `!config get agents.analyst.display_name`",
                None,
            )

        config_path_str = args[0]
        try:
            value = _get_nested_value(config_dict, config_path_str)
        except (KeyError, IndexError) as e:
            return f"❌ Configuration path not found: `{config_path_str}`\nError: {e}", None
        else:
            formatted = _format_value(_redact_value_for_display(value, config_path_str))
            return f"**Configuration value for `{config_path_str}`:**\n```yaml\n{formatted}\n```", None

    elif operation == "set":
        if len(args) < 2:
            return (
                '❌ Please specify a path and value\nExample: `!config set agents.analyst.display_name "New Name"`',
                None,
            )

        config_path_str = args[0]
        # Join remaining args as the value (handles unquoted strings with spaces)
        value_str = " ".join(args[1:])

        # Parse the value - YAML parsing handles both quoted and unquoted formats
        value = _parse_value(value_str)

        # Get the current value for comparison
        try:
            old_value = _get_nested_value(config_dict, config_path_str)
        except (KeyError, IndexError):
            old_value = None  # Path doesn't exist yet

        # Create a copy to test the change
        test_config_dict = config_dict.copy()

        try:
            # Verify the path exists or can be created
            _set_nested_value(test_config_dict, config_path_str, value)

            # Validate the modified config
            await asyncio.to_thread(Config.validate_with_runtime, test_config_dict, runtime_paths)
        except (KeyError, IndexError) as e:
            return f"❌ Configuration path error: `{config_path_str}`\nError: {e}", None
        except (ValidationError, ConfigRuntimeValidationError) as e:
            return format_invalid_config_message(e, footer=_CONFIG_CHANGE_REJECTED_MESSAGE), None
        else:
            # Format the preview message
            formatted_old = (
                _format_value(_redact_value_for_display(old_value, config_path_str))
                if old_value is not None
                else "Not set"
            )
            formatted_new = _format_value(_redact_value_for_display(value, config_path_str))

            preview_msg = (
                f"**Configuration Change Preview**\n\n"
                f"📝 **Path:** `{config_path_str}`\n\n"
                f"**Current value:**\n```yaml\n{formatted_old}\n```\n"
                f"**New value:**\n```yaml\n{formatted_new}\n```\n\n"
                f"React with ✅ to confirm or ❌ to cancel this change."
            )

            # Return the preview and the change info for confirmation
            change_info = {
                "config_path": config_path_str,
                "old_value": old_value,
                "new_value": value,
                "path": str(path),
            }

            return preview_msg, change_info

    elif operation == "parse_error":
        # Handle parsing errors (e.g., unmatched quotes)
        error_msg = args[0] if args else "Unknown parsing error"
        return (
            f"❌ **Command parsing error:**\n{error_msg}\n\n"
            "**Common issues:**\n"
            "• Unmatched quotes: Make sure quotes are properly paired\n"
            '• For JSON arrays/objects, use matching quotes: `["item1", "item2"]`\n'
            "• Or use single quotes consistently: `['item1', 'item2']`\n\n"
            "**Example:**\n"
            '`!config set agents.analyst.tools ["tool1", "tool2"]`'
        ), None

    else:
        available_ops = ["show", "get", "set"]
        return (
            f"❌ Unknown operation: '{operation}'\n"
            f"Available operations: {', '.join(available_ops)}\n\n"
            "Try `!help config` for usage examples."
        ), None


async def apply_config_change(
    config_path_str: str,
    new_value: Any,  # noqa: ANN401
    runtime_paths: RuntimePaths,
) -> str:
    """Apply a confirmed configuration change.

    Args:
        config_path_str: The configuration path (e.g., "agents.analyst.role")
        new_value: The new value to set
        runtime_paths: Runtime context carrying the active config path

    Returns:
        Success or error message

    """
    path = runtime_paths.config_path

    try:
        # Load the current configuration off the event loop (#1260).
        config, load_error = await asyncio.to_thread(
            load_config_or_user_error,
            runtime_paths,
            footer=_CONFIG_CHANGE_REJECTED_MESSAGE,
            tolerate_plugin_load_errors=True,
        )
        if load_error:
            return load_error
        assert config is not None
        config_dict = config.authored_model_dump()

        # Apply the specific change
        _set_nested_value(config_dict, config_path_str, new_value)

        try:
            await asyncio.to_thread(
                config_lifecycle.validate_and_persist_config_payload,
                config_dict,
                runtime_paths,
            )
        except (ValidationError, ConfigRuntimeValidationError) as ve:
            return format_invalid_config_message(ve, footer=_CONFIG_CHANGE_REJECTED_MESSAGE)

        return (  # noqa: TRY300
            f"✅ **Configuration updated successfully!**\n\n"
            f"Changes saved to {path} and will affect new agent interactions."
        )
    except Exception as e:
        logger.exception("Failed to apply config change")
        return f"❌ Failed to apply configuration change: {e}"
