from __future__ import annotations

import shlex

from flow_xhs.cli.spec.enums import ExampleCategory, RequirednessMode, ValueKind
from flow_xhs.cli.spec.global_options import build_global_option_registry
from flow_xhs.cli.spec.model import CliSpec, CommandSpec, DocTopicSpec

VALID_HELP_SECTIONS = {
    "usage",
    "summary",
    "options",
    "required_inputs",
    "conditional_inputs",
    "optional_inputs",
    "input_sources",
    "execution_controls",
    "output_controls",
    "advanced",
    "constraints",
    "examples",
}


class SpecValidationError(ValueError):
    pass


def validate_cli_spec(spec: CliSpec) -> None:
    command_ids: set[str] = set()
    command_paths: set[tuple[str, ...]] = set()
    topic_ids: set[str] = set()
    global_option_registry = build_global_option_registry(spec)

    for topic in spec.topics:
        if topic.id in topic_ids:
            raise SpecValidationError(f"duplicate topic id: {topic.id}")
        topic_ids.add(topic.id)

    for command in spec.commands:
        if command.id in command_ids:
            raise SpecValidationError(f"duplicate command id: {command.id}")
        command_ids.add(command.id)

        if command.path in command_paths:
            raise SpecValidationError(f"duplicate command path: {' '.join(command.path)}")
        command_paths.add(command.path)

        if not command.examples:
            raise SpecValidationError(f"command missing examples: {command.id}")
        example_categories = {example.category for example in command.examples}
        missing_categories = {
            ExampleCategory.MINIMAL,
            ExampleCategory.COMMON,
            ExampleCategory.AUTOMATION,
        } - example_categories
        if missing_categories:
            formatted = ", ".join(category.value for category in sorted(missing_categories, key=lambda item: item.value))
            raise SpecValidationError(f"command missing example categories in {command.id}: {formatted}")
        help_spec = command.help
        if help_spec is not None:
            for section in help_spec.section_order:
                if section not in VALID_HELP_SECTIONS:
                    raise SpecValidationError(f"unknown help section in {command.id}: {section}")

        param_ids: set[str] = set()
        for param in command.parameters:
            if param.id in param_ids:
                raise SpecValidationError(f"duplicate parameter id in {command.id}: {param.id}")
            param_ids.add(param.id)

            if param.requiredness.mode == RequirednessMode.CONDITIONAL and param.requiredness.condition is None:
                raise SpecValidationError(f"conditional parameter missing condition: {command.id}.{param.id}")

            if param.value_type.kind == ValueKind.ENUM and not param.allowed_values:
                raise SpecValidationError(f"enum parameter missing values: {command.id}.{param.id}")

        for constraint in command.constraints:
            for target in constraint.targets:
                if target not in param_ids:
                    raise SpecValidationError(f"constraint target missing in {command.id}: {target}")

        for example in command.examples:
            _validate_example_command(
                spec,
                command,
                example.command,
                global_value_options=global_option_registry.value_options,
            )


def get_command_spec(spec: CliSpec, path: tuple[str, ...]) -> CommandSpec | None:
    for command in spec.commands:
        if command.path == path:
            return command
    return None


def get_topic_spec(spec: CliSpec, topic_id: str) -> DocTopicSpec | None:
    for topic in spec.topics:
        if topic.id == topic_id:
            return topic
    return None


def _validate_example_command(
    spec: CliSpec,
    command: CommandSpec,
    example_command: str,
    *,
    global_value_options: frozenset[str],
) -> None:
    invocation = example_command.split("|")[-1].strip()
    if not invocation.startswith(spec.cli_name):
        raise SpecValidationError(f"example must invoke {spec.cli_name}: {command.id} -> {example_command}")
    try:
        tokens = shlex.split(invocation)
    except ValueError as exc:
        raise SpecValidationError(f"example command is not shell-parseable in {command.id}: {example_command}") from exc
    path = _extract_example_path(tokens[1:], global_value_options=global_value_options)
    if not path:
        raise SpecValidationError(f"example missing command path in {command.id}: {example_command}")
    direct = get_command_spec(spec, path)
    if direct is None:
        raise SpecValidationError(f"example path not found in spec for {command.id}: {' '.join(path)}")
    if direct is not command:
        raise SpecValidationError(f"example resolves to different command in {command.id}: {' '.join(path)}")


def _extract_example_path(args: list[str], *, global_value_options: frozenset[str]) -> tuple[str, ...]:
    i = 0
    while i < len(args):
        token = args[i]
        if token.startswith("-"):
            if token in global_value_options and "=" not in token and i + 1 < len(args):
                i += 2
                continue
            i += 1
            continue
        break
    path: list[str] = []
    while i < len(args):
        token = args[i]
        if token.startswith("-"):
            break
        path.append(token)
        i += 1
    return tuple(path)
