"""Input resolution module for dotprompt.

Handles file loading, type detection, path security, and schema inference
for all input types (None, dict, list[dict]) and file-based inputs via
``resolve_files`` (str, list[str]).
"""

from __future__ import annotations

import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal

from dotpromptz.yamlio import YamlError, load_yaml

_ARRAY_ITEM_MARKER = '*'
_ENUM_SUFFIX = '__enum'
_ContextKind = Literal['object', 'array', 'pending']
_SUPPORTED_INPUT_EXTENSIONS = frozenset({'.json', '.yaml', '.yml', '.jsonl'})
_SUPPORTED_INPUT_EXTENSIONS_TEXT = ', '.join(sorted(_SUPPORTED_INPUT_EXTENSIONS))


@dataclass
class _CommentContext:
    """Track indentation and path while scanning YAML line comments."""

    indent: int
    kind: _ContextKind
    path: tuple[str, ...]


def resolve_input(
    raw_input: Any,
    prompt_file_path: Path,
    defaults: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
    """Resolve raw input to a list of dicts.

    When *defaults* is provided, each resolved record is merged with
    ``{**defaults, **record}`` so that per-record values override defaults.

    Args:
        raw_input: Input to resolve (None, dict, list[dict]).
        prompt_file_path: Path to .prompt file (for resolving relative paths).
        defaults: Optional fixed values merged into every record.

    Returns:
        list[dict]: Normalized input data as list of dicts.

    Raises:
        ValueError: On unsupported types or file format errors.
    """
    records = _resolve_raw_input(raw_input, prompt_file_path)
    if defaults and records:
        records = [{**defaults, **record} for record in records]
    return records


def _resolve_raw_input(raw_input: Any, prompt_file_path: Path) -> list[dict[str, Any]]:
    """Core resolution logic without defaults merging.

    Args:
        raw_input: Input to resolve (None, dict, list[dict]).
        prompt_file_path: Path to .prompt file (for resolving relative paths).

    Returns:
        list[dict]: Normalized input data as list of dicts.

    Raises:
        ValueError: On unsupported types.
    """
    # None → empty list
    if raw_input is None:
        return []

    # dict → single-item list
    if isinstance(raw_input, dict):
        return [raw_input]

    # list → must be list[dict]
    if isinstance(raw_input, list):
        if not raw_input:
            return []

        first = raw_input[0]

        if isinstance(first, dict):
            if not all(isinstance(item, dict) for item in raw_input):
                raise ValueError('All items in data list must be dicts')
            return raw_input

        # Invalid list type
        raise ValueError(f'data list must contain dicts, not {type(first).__name__}')
    # Invalid types (bool, int, float, str, etc.)
    raise ValueError(f'Unsupported input type: {type(raw_input).__name__}. Expected None, dict, or list[dict]')


def _validate_file_path(file_ref: str, prompt_dir: Path) -> Path:
    """Resolve and validate a file path.

    Args:
        file_ref: File path string (relative or absolute).
        prompt_dir: Directory containing .prompt file (base for relative paths).

    Returns:
        Resolved absolute Path object.

    Raises:
        ValueError: If file does not exist.
    """
    # Resolve relative to prompt_dir
    if Path(file_ref).is_absolute():
        resolved = Path(file_ref).resolve()
    else:
        resolved = (prompt_dir / file_ref).resolve()

    # Check file exists
    if not resolved.exists():
        raise ValueError(f'File not found: {file_ref}')

    return resolved


def _load_file(path: Path) -> dict | list[dict]:
    """Load file content based on extension.

    Args:
        path: Absolute path to file.

    Returns:
        dict (single mode) or list[dict] (batch mode).

    Raises:
        ValueError: On unsupported extension, read errors, or invalid format.
    """
    ext = path.suffix.lower()

    # Read file content
    try:
        content = path.read_text(encoding='utf-8')
    except OSError as exc:
        raise ValueError(f'Failed to read file {path}: {exc}') from exc

    # JSON format
    if ext == '.json':
        try:
            data = json.loads(content)
        except json.JSONDecodeError as exc:
            raise ValueError(f'Invalid JSON in {path}: {exc}') from exc

        # Validate: must be dict or list[dict]
        if isinstance(data, dict):
            return data
        if isinstance(data, list):
            if not all(isinstance(item, dict) for item in data):
                raise ValueError(f'JSON array in {path} must contain only objects (dicts)')
            return data
        raise ValueError(f'JSON in {path} must be object or array, not {type(data).__name__}')

    # YAML format
    if ext in ('.yaml', '.yml'):
        try:
            data = load_yaml(content)
        except YamlError as exc:
            raise ValueError(f'Invalid YAML in {path}: {exc}') from exc

        # Validate: must be dict or list[dict]
        if isinstance(data, dict):
            return data
        if isinstance(data, list):
            if not all(isinstance(item, dict) for item in data):
                raise ValueError(f'YAML array in {path} must contain only objects (dicts)')
            return data
        raise ValueError(f'YAML in {path} must be object or array, not {type(data).__name__}')

    # JSONL format (always returns list)
    if ext == '.jsonl':
        items = []
        for line_num, line in enumerate(content.splitlines(), start=1):
            stripped = line.strip()
            if not stripped:  # Skip blank lines
                continue
            try:
                obj = json.loads(stripped)
            except json.JSONDecodeError as exc:
                raise ValueError(f'Invalid JSON at line {line_num} in {path}: {exc}') from exc
            if not isinstance(obj, dict):
                raise ValueError(f'JSONL line {line_num} in {path} must be object, not {type(obj).__name__}')
            items.append(obj)
        return items

    raise ValueError(f'Unsupported file format: {ext}. Supported: .json, .yaml, .yml, .jsonl')


def resolve_files(
    files_config: str | list[str],
    prompt_file_path: Path,
    defaults: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
    """Resolve file-based input to a list of dicts.

    Dispatches to the appropriate loader based on the type of *files_config*:

    - ``str`` — a single file path or directory path
    - ``list[str]`` — multiple file/directory paths

    When *defaults* is provided, each resolved record is merged with
    ``{**defaults, **record}`` so that per-record values override defaults.

    Args:
        files_config: File loading configuration.
        prompt_file_path: Path to the ``.prompt`` file (for resolving relative paths).
        defaults: Optional fixed values merged into every record.

    Returns:
        list[dict]: Normalized input data as list of dicts.

    Raises:
        ValueError: On invalid configuration, missing files, or format errors.
    """
    prompt_dir = prompt_file_path.parent

    if isinstance(files_config, str):
        records = _resolve_file_path_input(files_config, prompt_dir)
    elif isinstance(files_config, list):
        records = _resolve_file_list_input(files_config, prompt_dir)
    else:
        raise ValueError(f'Unsupported files config type: {type(files_config).__name__}')

    if defaults and records:
        records = [{**defaults, **record} for record in records]
    return records


def _resolve_file_path_input(file_ref: str, prompt_dir: Path) -> list[dict[str, Any]]:
    """Load a single file/directory path and return records.

    Args:
        file_ref: File or directory path string (relative or absolute).
        prompt_dir: Directory containing .prompt file.

    Returns:
        list[dict]: Records loaded from the file.

    Raises:
        ValueError: On missing file or invalid format.
    """
    resolved = _validate_file_path(file_ref, prompt_dir)

    if resolved.is_file():
        return _load_records_from_paths([resolved])

    if resolved.is_dir():
        files = _collect_directory_files(resolved, file_ref)
        return _load_records_from_paths(files)

    raise ValueError(f'Path is neither file nor directory: {file_ref}')


def _resolve_file_list_input(file_refs: list[str], prompt_dir: Path) -> list[dict[str, Any]]:
    """Load multiple file/directory paths and concatenate records.

    Args:
        file_refs: List of file or directory path strings.
        prompt_dir: Directory containing .prompt file.

    Returns:
        list[dict]: Concatenated records from all files.

    Raises:
        ValueError: On missing files or invalid format.
    """
    expanded_files: list[Path] = []
    contains_directory = False

    for file_ref in file_refs:
        if not isinstance(file_ref, str):
            raise ValueError(f'files list items must be strings, not {type(file_ref).__name__}')

        resolved = _validate_file_path(file_ref, prompt_dir)

        if resolved.is_file():
            expanded_files.append(resolved)
        elif resolved.is_dir():
            contains_directory = True
            expanded_files.extend(_collect_directory_files(resolved, file_ref))
        else:
            raise ValueError(f'Path is neither file nor directory: {file_ref}')

    if contains_directory:
        _validate_uniform_file_suffixes(expanded_files, context='All files referenced by input.files')

    return _load_records_from_paths(expanded_files)


def _collect_directory_files(directory_path: Path, file_ref: str) -> list[Path]:
    """Collect and validate top-level files from a directory path.

    Args:
        directory_path: Directory path to scan.
        file_ref: Original user-provided path (for error messages).

    Returns:
        Sorted list of top-level files by file name.

    Raises:
        ValueError: If directory is empty, has unsupported files, or has mixed suffixes.
    """
    files = sorted((path for path in directory_path.iterdir() if path.is_file()), key=lambda path: path.name)
    if not files:
        raise ValueError(f'Directory is empty: {file_ref}')

    unsupported_files = [path.name for path in files if path.suffix.lower() not in _SUPPORTED_INPUT_EXTENSIONS]
    if unsupported_files:
        names = ', '.join(sorted(unsupported_files))
        raise ValueError(
            f'Unsupported file format in directory {file_ref}: {names}. Supported: {_SUPPORTED_INPUT_EXTENSIONS_TEXT}'
        )

    _validate_uniform_file_suffixes(files, context=f'All files in directory {file_ref}')
    return files


def _validate_uniform_file_suffixes(file_paths: list[Path], *, context: str) -> None:
    """Validate that all files share the same lower-cased suffix.

    Args:
        file_paths: File paths to validate.
        context: Error-message prefix describing validation scope.

    Raises:
        ValueError: If unsupported suffix exists or suffixes are mixed.
    """
    if not file_paths:
        return

    unsupported_suffixes = sorted({
        path.suffix.lower() for path in file_paths if path.suffix.lower() not in _SUPPORTED_INPUT_EXTENSIONS
    })
    if unsupported_suffixes:
        suffixes_text = ', '.join(unsupported_suffixes)
        raise ValueError(
            f'{context} contain unsupported suffix(es): {suffixes_text}. Supported: {_SUPPORTED_INPUT_EXTENSIONS_TEXT}'
        )

    suffixes = {path.suffix.lower() for path in file_paths}
    if len(suffixes) > 1:
        found = ', '.join(sorted(suffixes))
        raise ValueError(f'{context} must use the same file suffix. Found: {found}')


def _load_records_from_paths(file_paths: list[Path]) -> list[dict[str, Any]]:
    """Load all file paths and normalize into one list of records.

    Args:
        file_paths: File paths to load.

    Returns:
        Concatenated records from all paths.

    Raises:
        ValueError: If any file payload is not dict/list[dict].
    """
    all_items: list[dict[str, Any]] = []
    for file_path in file_paths:
        data = _load_file(file_path)
        if isinstance(data, dict):
            all_items.append(data)
        elif isinstance(data, list):
            all_items.extend(data)
        else:
            raise ValueError(f'File {file_path} must contain dict or list[dict], not {type(data).__name__}')
    return all_items


def infer_schema(data: dict[str, Any]) -> dict:
    """Infer JSON Schema from a dict.

    Args:
        data: Dictionary to infer schema from.

    Returns:
        JSON Schema dict with type "object" and properties.
    """
    properties = {}

    for key, value in data.items():
        properties[key] = _infer_type(value)

    return {'type': 'object', 'properties': properties}


def infer_schema_from_example(example: str) -> dict[str, Any]:
    """Infer strict JSON Schema from YAML example text.

    Rules:
        - All object fields are ``required``.
        - Union types are not supported.
        - ``null``, empty arrays, and empty objects are rejected.
        - Arrays must be homogeneously typed.
        - ``<field>__enum`` is recognized as enum metadata.
        - Inline YAML comments map to ``description``.

    Args:
        example: YAML sample text.

    Returns:
        Inferred JSON Schema.

    Raises:
        ValueError: If parsing fails or strict inference rules are violated.
    """
    if not isinstance(example, str):
        raise ValueError('output.example must be a YAML string.')

    if not example.strip():
        raise ValueError('output.example must not be empty.')

    try:
        parsed = load_yaml(example)
    except YamlError as exc:
        raise ValueError(f'Invalid output.example YAML: {exc}') from exc

    if not isinstance(parsed, (dict, list)):
        raise ValueError(f'output.example must be a YAML mapping or list (object/array), got {type(parsed).__name__}.')

    comments = _extract_inline_comments(example)
    used_comments: set[tuple[str, ...]] = set()
    return _infer_strict_type(parsed, path=(), comments=comments, used_comments=used_comments)


def _infer_type(value: Any) -> dict:
    """Infer JSON Schema type for a value.

    Args:
        value: Python value to infer type from.

    Returns:
        JSON Schema type dict (e.g. {"type": "string"}).
    """
    # None → string (nullable)
    if value is None:
        return {'type': 'string'}

    # bool BEFORE int (bool is subclass of int in Python)
    if isinstance(value, bool):
        return {'type': 'boolean'}

    # int
    if isinstance(value, int):
        return {'type': 'integer'}

    # float
    if isinstance(value, float):
        return {'type': 'number'}

    # str
    if isinstance(value, str):
        return {'type': 'string'}

    # list → array
    if isinstance(value, list):
        if not value:
            # Empty list → array with no items constraint
            return {'type': 'array'}
        # Infer from first element
        return {'type': 'array', 'items': _infer_type(value[0])}

    # dict → nested object
    if isinstance(value, dict):
        nested_props = {}
        for key, item in value.items():
            nested_props[key] = _infer_type(item)
        return {'type': 'object', 'properties': nested_props}

    # Fallback for unknown types
    return {'type': 'string'}


def _infer_strict_type(
    value: Any,
    *,
    path: tuple[str, ...],
    comments: dict[tuple[str, ...], str],
    used_comments: set[tuple[str, ...]],
) -> dict[str, Any]:
    """Infer strict JSON Schema recursively."""
    if value is None:
        raise _schema_error(path, 'Null values are not supported in output.example.')

    if isinstance(value, bool):
        return _with_description({'type': 'boolean'}, path=path, comments=comments, used_comments=used_comments)

    if isinstance(value, int):
        return _with_description({'type': 'integer'}, path=path, comments=comments, used_comments=used_comments)

    if isinstance(value, float):
        return _with_description({'type': 'number'}, path=path, comments=comments, used_comments=used_comments)

    if isinstance(value, str):
        return _with_description({'type': 'string'}, path=path, comments=comments, used_comments=used_comments)

    if isinstance(value, list):
        if not value:
            raise _schema_error(path, 'Empty arrays are not supported in output.example.')

        items_path = (*path, _ARRAY_ITEM_MARKER)
        item_schema = _infer_strict_type(value[0], path=items_path, comments=comments, used_comments=used_comments)
        for index, item in enumerate(value[1:], start=1):
            current = _infer_strict_type(item, path=items_path, comments=comments, used_comments=used_comments)
            if current != item_schema:
                raise _schema_error(
                    (*path, str(index)),
                    (
                        'Array items must share the same schema. '
                        f'Expected {_schema_signature(item_schema)}, got {_schema_signature(current)}.'
                    ),
                )

        return _with_description(
            {'type': 'array', 'items': item_schema},
            path=path,
            comments=comments,
            used_comments=used_comments,
        )

    if isinstance(value, dict):
        if not value:
            raise _schema_error(path, 'Empty objects are not supported in output.example.')

        enum_map = _collect_enum_map(value, path=path)
        properties: dict[str, Any] = {}
        required: list[str] = []
        for key, item in value.items():
            if not isinstance(key, str):
                raise _schema_error(path, f'Object keys must be strings, got {type(key).__name__}.')
            if key.endswith(_ENUM_SUFFIX):
                continue

            key_path = (*path, key)
            field_schema = _infer_strict_type(item, path=key_path, comments=comments, used_comments=used_comments)
            enum_values = enum_map.get(key)
            if enum_values is not None:
                _validate_enum_value(item, enum_values, path=key_path)
                field_schema = {**field_schema, 'enum': enum_values}

            properties[key] = field_schema
            required.append(key)

        return _with_description(
            {
                'type': 'object',
                'properties': properties,
                'required': required,
                'additionalProperties': False,
            },
            path=path,
            comments=comments,
            used_comments=used_comments,
        )

    raise _schema_error(path, f'Unsupported value type for schema inference: {type(value).__name__}.')


def _collect_enum_map(data: dict[str, Any], *, path: tuple[str, ...]) -> dict[str, list[Any]]:
    """Collect and validate ``<field>__enum`` declarations."""
    result: dict[str, list[Any]] = {}
    for key, enum_values in data.items():
        if not isinstance(key, str) or not key.endswith(_ENUM_SUFFIX):
            continue

        field_name = key[: -len(_ENUM_SUFFIX)]
        if not field_name:
            raise _schema_error((*path, key), f'Invalid enum field name {key!r}.')
        if field_name not in data:
            raise _schema_error((*path, key), f'Enum field {key!r} requires sibling field {field_name!r}.')
        if not isinstance(enum_values, list):
            raise _schema_error((*path, key), f'Enum field {key!r} must be a non-empty array.')
        if not enum_values:
            raise _schema_error((*path, key), f'Enum field {key!r} must not be empty.')

        item_type = _scalar_type(enum_values[0], path=(*path, key, '0'))
        for index, enum_item in enumerate(enum_values[1:], start=1):
            current_type = _scalar_type(enum_item, path=(*path, key, str(index)))
            if current_type != item_type:
                raise _schema_error(
                    (*path, key, str(index)),
                    (f'Enum field {key!r} items must share one type. Expected {item_type}, got {current_type}.'),
                )

        _validate_enum_uniqueness(enum_values, path=(*path, key))
        result[field_name] = enum_values

    return result


def _validate_enum_value(value: Any, enum_values: list[Any], *, path: tuple[str, ...]) -> None:
    """Validate enum type and membership against current sample value."""
    value_type = _scalar_type(value, path=path)
    enum_type = _scalar_type(enum_values[0], path=(*path, _ENUM_SUFFIX, '0'))
    if value_type != enum_type:
        raise _schema_error(
            path,
            f'Field type {value_type} does not match enum type {enum_type}.',
        )
    if value not in enum_values:
        raise _schema_error(path, f'Field value {value!r} is not declared in enum {enum_values!r}.')


def _validate_enum_uniqueness(enum_values: list[Any], *, path: tuple[str, ...]) -> None:
    """Validate enum values are unique."""
    try:
        if len(set(enum_values)) != len(enum_values):
            raise _schema_error(path, 'Enum values must be unique.')
    except TypeError as exc:
        raise _schema_error(path, 'Enum values must be hashable scalar values.') from exc


def _scalar_type(value: Any, *, path: tuple[str, ...]) -> str:
    """Return strict scalar JSON Schema type name."""
    if value is None:
        raise _schema_error(path, 'Null is not allowed in enum declarations.')
    if isinstance(value, bool):
        return 'boolean'
    if isinstance(value, int):
        return 'integer'
    if isinstance(value, float):
        return 'number'
    if isinstance(value, str):
        return 'string'
    raise _schema_error(path, f'Enum values must be scalar types, got {type(value).__name__}.')


def _with_description(
    schema: dict[str, Any],
    *,
    path: tuple[str, ...],
    comments: dict[tuple[str, ...], str],
    used_comments: set[tuple[str, ...]],
) -> dict[str, Any]:
    """Attach inline comment as schema description when available."""
    comment = comments.get(path)
    if comment:
        schema = {**schema, 'description': comment}
        used_comments.add(path)
    return schema


def _extract_inline_comments(source: str) -> dict[tuple[str, ...], str]:
    """Extract inline ``# ...`` comments keyed by normalized schema path."""
    comments: dict[tuple[str, ...], str] = {}
    # Root can be either object or array depending on the first non-empty line.
    stack: list[_CommentContext] = [_CommentContext(indent=-1, kind='pending', path=())]

    for line_num, raw_line in enumerate(source.splitlines(), start=1):
        if not raw_line.strip():
            continue
        if raw_line.lstrip().startswith('#'):
            continue
        if '\t' in raw_line[: len(raw_line) - len(raw_line.lstrip())]:
            raise ValueError(f'Tab indentation is not supported in output.example (line {line_num}).')

        indent = len(raw_line) - len(raw_line.lstrip(' '))
        line = raw_line.strip()
        content, comment = _split_inline_comment(line)
        if not content:
            continue

        while len(stack) > 1 and indent <= stack[-1].indent:
            stack.pop()

        ctx = stack[-1]
        if content.startswith('-'):
            remainder = content[1:]
            if remainder and not remainder.startswith(' '):
                raise ValueError(f'Invalid YAML list item syntax in output.example at line {line_num}.')

            if ctx.kind == 'pending':
                ctx.kind = 'array'
            if ctx.kind != 'array':
                raise ValueError(f'List item is outside an array context in output.example at line {line_num}.')

            item_path = (*ctx.path, _ARRAY_ITEM_MARKER)
            body = remainder.strip()
            if not body:
                _append_comment(comments, item_path, comment, line_num)
                stack.append(_CommentContext(indent=indent, kind='pending', path=item_path))
                continue

            entry = _split_mapping_entry(body)
            if entry is None:
                _append_comment(comments, item_path, comment, line_num)
                _reject_flow_style_value(body, line_num)
                continue

            stack.append(_CommentContext(indent=indent, kind='object', path=item_path))
            _register_mapping_comment(
                key=entry[0],
                value_text=entry[1],
                comment=comment,
                line_num=line_num,
                indent=indent,
                stack=stack,
                comments=comments,
            )
            continue

        if ctx.kind == 'pending':
            ctx.kind = 'object'
        if ctx.kind != 'object':
            raise ValueError(f'Mapping item is outside an object context in output.example at line {line_num}.')

        entry = _split_mapping_entry(content)
        if entry is None:
            raise ValueError(f'Invalid YAML mapping syntax in output.example at line {line_num}.')

        _register_mapping_comment(
            key=entry[0],
            value_text=entry[1],
            comment=comment,
            line_num=line_num,
            indent=indent,
            stack=stack,
            comments=comments,
        )

    return comments


def _register_mapping_comment(
    *,
    key: str,
    value_text: str,
    comment: str | None,
    line_num: int,
    indent: int,
    stack: list[_CommentContext],
    comments: dict[tuple[str, ...], str],
) -> None:
    """Register object key comments and pending nested context."""
    field_path = (*stack[-1].path, key)
    if not key.endswith(_ENUM_SUFFIX):
        _append_comment(comments, field_path, comment, line_num)
    _reject_flow_style_value(value_text, line_num)
    if not value_text:
        stack.append(_CommentContext(indent=indent, kind='pending', path=field_path))


def _reject_flow_style_value(value_text: str, line_num: int) -> None:
    """Reject flow style (``[]`` / ``{}``) for predictable comment mapping."""
    if value_text.startswith('[') or value_text.startswith('{'):
        raise ValueError(
            'Flow-style YAML collections are not supported in output.example; '
            f'use block style instead (line {line_num}).'
        )


def _append_comment(
    comments: dict[tuple[str, ...], str],
    path: tuple[str, ...],
    comment: str | None,
    line_num: int,
) -> None:
    """Append one comment, rejecting conflicting descriptions."""
    if comment is None:
        return
    normalized = comment.strip()
    if not normalized:
        return

    previous = comments.get(path)
    if previous is not None and previous != normalized:
        raise ValueError(
            f'Conflicting comments for field {_format_path(path)}: {previous!r} vs {normalized!r} (line {line_num}).'
        )
    comments[path] = normalized


def _split_mapping_entry(content: str) -> tuple[str, str] | None:
    """Split ``key: value`` while respecting quoted text."""
    in_single = False
    in_double = False
    escaped = False
    for index, char in enumerate(content):
        if char == '\\' and in_double and not escaped:
            escaped = True
            continue
        if char == "'" and not in_double:
            in_single = not in_single
        elif char == '"' and not in_single and not escaped:
            in_double = not in_double
        elif char == ':' and not in_single and not in_double:
            next_char = content[index + 1 : index + 2]
            if next_char and not next_char.isspace():
                escaped = False
                continue
            key = _normalize_yaml_key(content[:index].strip())
            if not key:
                return None
            return key, content[index + 1 :].strip()
        escaped = False
    return None


def _split_inline_comment(content: str) -> tuple[str, str | None]:
    """Split ``value # comment`` while respecting quoted text."""
    in_single = False
    in_double = False
    escaped = False
    for index, char in enumerate(content):
        if char == '\\' and in_double and not escaped:
            escaped = True
            continue
        if char == "'" and not in_double:
            in_single = not in_single
        elif char == '"' and not in_single and not escaped:
            in_double = not in_double
        elif char == '#' and not in_single and not in_double:
            return content[:index].rstrip(), content[index + 1 :].strip()
        escaped = False
    return content.rstrip(), None


def _normalize_yaml_key(key: str) -> str:
    """Normalize key token to plain string."""
    if len(key) >= 2 and key[0] == key[-1] and key[0] in ("'", '"'):
        return key[1:-1]
    return key


def _schema_signature(schema: dict[str, Any]) -> str:
    """Build compact schema signature for error messages."""
    schema_type = schema.get('type')
    if schema_type == 'object':
        keys = sorted(schema.get('properties', {}).keys())
        return f'object<{",".join(keys)}>'
    if schema_type == 'array':
        items = schema.get('items')
        if isinstance(items, dict):
            return f'array<{_schema_signature(items)}>'
        return 'array<?>'
    return str(schema_type)


def _schema_error(path: tuple[str, ...], message: str) -> ValueError:
    """Construct a path-aware schema inference error."""
    return ValueError(f'{message} (path: {_format_path(path)})')


def _format_path(path: tuple[str, ...]) -> str:
    """Format tuple path into dotted notation with array markers."""
    if not path:
        return '<root>'

    result = ''
    for token in path:
        if token == _ARRAY_ITEM_MARKER:
            result += '[*]'
            continue
        result = f'{result}.{token}' if result else token
    return result
