"""
Command introspection for machine-readable help output.

Generates JSON schemas from Click command definitions for AI agent consumption.
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import Any, Dict, List

import click

from g_gremlin import command_metadata


def extract_command_schema(
    cmd: click.Command,
    cmd_name: str,
    cmd_id: str | None = None,
    group_name: str | None = None,
) -> Dict[str, Any]:
    """
    Extract structured schema from a Click command.

    Returns JSON-serializable dict with command metadata, arguments, options,
    examples, and exit codes. For command groups, includes subcommands list.
    """
    command_id = cmd_id or cmd_name
    schema: Dict[str, Any] = {
        "command": cmd_name,
        "command_id": command_id,
        "short_description": _extract_short_description(cmd.help or ""),
        "description": cmd.help or "",
        "arguments": _extract_arguments(cmd),
        "options": _extract_options(cmd),
        "flags": _extract_flags(cmd),
        "examples": _extract_examples(cmd.help or ""),
        "exit_codes": _extract_exit_codes(cmd.help or ""),
        "exit_code_actions": command_metadata.resolve_exit_code_actions(command_id),
        "defaults": _extract_defaults(cmd),
    }

    is_group = isinstance(cmd, click.Group)
    supports = command_metadata.derive_mechanical_support(cmd)
    schema.update(supports)
    schema["safety"] = command_metadata.resolve_safety_for_schema(cmd, command_id, group_name, is_group)

    # If this is a command group, add subcommands
    if is_group:
        subcommands = _extract_subcommands(cmd, cmd_name)
        if subcommands:
            schema["subcommands"] = subcommands
            schema["is_group"] = True
            schema["kind"] = "namespace"

    return schema


def _extract_subcommands(group: click.Group, parent_name: str) -> List[Dict[str, Any]]:
    """Extract subcommand information from a Click group."""
    subcommands = []

    for name, subcmd in group.commands.items():
        subcmd_info: Dict[str, Any] = {
            "name": name,
            "full_command": f"{parent_name} {name}",
            "short_description": _extract_short_description(subcmd.help or ""),
        }

        # Check if subcommand is also a group (nested groups)
        if isinstance(subcmd, click.Group):
            subcmd_info["is_group"] = True
            nested = _extract_subcommands(subcmd, f"{parent_name} {name}")
            if nested:
                subcmd_info["subcommands"] = nested

        subcommands.append(subcmd_info)

    # Sort by name for consistent output
    subcommands.sort(key=lambda x: x["name"])
    return subcommands


def _extract_short_description(help_text: str) -> str:
    """Extract the first line/sentence as short description."""
    if not help_text:
        return ""

    # Get first line or sentence
    lines = help_text.strip().split("\n")
    first_line = lines[0].strip()

    # If it's a full sentence, return it
    if first_line.endswith("."):
        return first_line

    # Otherwise find first sentence
    match = re.search(r"^([^.!?]+[.!?])", help_text)
    if match:
        return match.group(1).strip()

    return first_line


def _extract_arguments(cmd: click.Command) -> List[Dict[str, Any]]:
    """Extract argument schemas."""
    arguments = []

    for param in cmd.params:
        if isinstance(param, click.Argument):
            arg_schema = {
                "name": param.name,
                "required": param.required,
                "type": _get_param_type(param),
            }

            # Add metavar if available (e.g., SHEET_ID, CSV_PATH)
            if param.metavar:
                arg_schema["metavar"] = param.metavar

            arguments.append(arg_schema)

    return arguments


def _extract_options(cmd: click.Command) -> List[Dict[str, Any]]:
    """Extract option schemas."""
    options = []

    for param in cmd.params:
        if isinstance(param, click.Option):
            # Convert Path objects to strings for JSON serialization
            default_value = param.default
            if isinstance(default_value, Path):
                default_value = str(default_value)
            elif default_value is not None and not isinstance(default_value, (str, int, float, bool, list, dict)):
                default_value = str(default_value)

            option_schema = {
                "names": param.opts,
                "type": _get_param_type(param),
                "required": param.required,
                "default": default_value if default_value is not None else None,
                "help": param.help or "",
            }

            # Add choices if it's a Choice type
            if isinstance(param.type, click.Choice):
                option_schema["choices"] = param.type.choices

            # Add flag info
            if param.is_flag:
                option_schema["is_flag"] = True

            # Add multiple info
            if param.multiple:
                option_schema["multiple"] = True

            options.append(option_schema)

    return options


def _extract_flags(cmd: click.Command) -> List[Dict[str, Any]]:
    """Extract flag schemas (boolean options)."""
    flags: List[Dict[str, Any]] = []
    for param in cmd.params:
        if isinstance(param, click.Option) and param.is_flag:
            flag_schema = {
                "names": param.opts,
                "required": param.required,
                "default": param.default if param.default is not None else None,
                "help": param.help or "",
            }
            flags.append(flag_schema)
    return flags


def _get_param_type(param: click.Parameter) -> str:
    """Get human-readable type name for parameter."""
    param_type = param.type

    if isinstance(param_type, click.Choice):
        return f"choice({','.join(param_type.choices)})"
    elif isinstance(param_type, click.Path):
        return "path"

    # Check type name for built-in types
    type_name = type(param_type).__name__
    if type_name == "IntParamType" or str(param_type).lower() == "int":
        return "integer"
    elif type_name == "FloatParamType" or str(param_type).lower() == "float":
        return "float"
    elif type_name == "BoolParamType" or str(param_type).lower() in ("bool", "boolean"):
        return "boolean"
    else:
        return "string"


def _extract_examples(help_text: str) -> List[str]:
    """Extract example commands from help text."""
    if not help_text:
        return []

    examples = []
    in_examples = False

    for line in help_text.split("\n"):
        stripped = line.strip()

        # Detect examples section
        if "Examples:" in line or "Common Usage:" in line:
            in_examples = True
            continue

        # Exit examples section at next major heading
        if in_examples and stripped and not stripped.startswith("#") and not stripped.startswith("g-gremlin"):
            # Check if it's a new section (not indented, contains colon)
            if not line.startswith(" ") and ":" in stripped:
                in_examples = False
                continue

        # Extract example lines (start with g-gremlin or indented with #)
        if in_examples:
            if stripped.startswith("g-gremlin"):
                # Extract just the command, removing comments
                example = stripped.split("#")[0].strip()
                examples.append(example)

    return examples


def _extract_exit_codes(help_text: str) -> Dict[str, str]:
    """Extract exit code documentation from help text."""
    exit_codes = {
        "0": "Success",
        "1": "Issues found (lint/validation/etc.)",
        "2": "Usage error or missing prereqs",
        "3": "Remote/API failure",
    }

    # Look for explicit exit code documentation in help text
    in_exit_codes = False
    for line in help_text.split("\n"):
        stripped = line.strip()

        if "Exit Codes:" in line or "Exit codes:" in line:
            in_exit_codes = True
            continue

        if in_exit_codes:
            # Look for pattern like "0 - Success" or "1: Error"
            match = re.match(r"(\d+)\s*[-:]\s*(.+)", stripped)
            if match:
                code, description = match.groups()
                exit_codes[code] = description.strip()
            elif stripped and not stripped.startswith(("0", "1", "2", "3")):
                # Exited exit codes section
                break

    return exit_codes


def _extract_defaults(cmd: click.Command) -> Dict[str, Any]:
    """Extract default behavior info."""
    defaults: Dict[str, Any] = {}

    help_text = cmd.help or ""

    # Check for dry-run default
    if "--apply" in help_text and "dry-run" in help_text.lower():
        defaults["dry_run"] = True
        defaults["apply_flag"] = "--apply"

    # Check for output format defaults
    if "--output" in help_text or "--format" in help_text:
        defaults["supports_structured_output"] = True

    return defaults


def format_command_help_json(schema: Dict[str, Any]) -> str:
    """Format command schema as pretty-printed JSON."""
    import json
    return json.dumps(schema, indent=2, sort_keys=False)
