"""
:mod:`etlplus.cli.app` module.

Defines the main `Typer` application for the ``etlplus`` command-line
interface (CLI).

Typer-First Interface
---------------------
The CLI is implemented using `Typer` (Click) for parsing, help text, and
subcommand dispatch. The Typer layer focuses on ergonomics (git-style
subcommands, optional inference of resource types, stdin/stdout piping, and
quality-of-life flags), while delegating business logic to the existing
``cmd_*`` handlers.

Namespace Adapter
-----------------
The command handlers continue to accept an ``argparse.Namespace`` for
backwards compatibility with existing ``cmd_*`` functions and tests. The
Typer commands adapt parsed arguments into an ``argparse.Namespace`` and then
call the corresponding ``cmd_*`` handler.

Subcommands
-----------
- ``extract``: extract data from files, databases, or REST APIs
- ``validate``: validate data against rules
- ``transform``: transform records
- ``load``: load data to files, databases, or REST APIs
- ``render``: render SQL DDL from table schema specs

Notes
-----
- Use ``-`` to read from stdin or to write to stdout.
- Commands ``extract`` and ``transform`` support the command-line option
    ``--from`` to override inferred resource types.
- Commands ``transform`` and ``load`` support the command-line option ``--to``
    to override inferred resource types.
"""

# Pylint struggles with large CLI surfaces that legitimately require
# numerous arguments in a single module.
# pylint: disable=too-many-lines
# pylint: disable=too-many-arguments,too-many-positional-arguments

from __future__ import annotations

import argparse
import sys
from collections.abc import Collection
from dataclasses import dataclass
from pathlib import Path
from typing import Annotated
from typing import Final

import typer

from .. import __version__
from ..enums import DataConnectorType
from ..enums import FileFormat
from ..utils import json_type
from .handlers import cmd_extract
from .handlers import cmd_list
from .handlers import cmd_load
from .handlers import cmd_pipeline
from .handlers import cmd_render
from .handlers import cmd_run
from .handlers import cmd_transform
from .handlers import cmd_validate

# SECTION: EXPORTS ========================================================== #


__all__ = [
    # Apps
    'app',
]


# SECTION: INTERNAL CONSTANTS =============================================== #


_DB_SCHEMES = (
    'postgres://',
    'postgresql://',
    'mysql://',
)

_SOURCE_CHOICES: Final[frozenset[str]] = frozenset(DataConnectorType.choices())
_FORMAT_CHOICES: Final[frozenset[str]] = frozenset(FileFormat.choices())


# SECTION: CONSTANTS ======================================================== #


CLI_DESCRIPTION: Final[str] = '\n'.join(
    [
        'ETLPlus - A Swiss Army knife for simple ETL operations.',
        '',
        '    Provide a subcommand and options. Examples:',
        '',
        '    etlplus extract in.csv > out.json',
        '    etlplus validate in.json --rules \'{"required": ["id"]}\'',
        (
            '    etlplus transform --from file in.json '
            '--operations \'{"select": ["id"]}\' --to file -o out.json'
        ),
        '    etlplus extract in.csv | etlplus load --to file out.json',
        '    cat data.json | etlplus load --to api https://example.com/data',
        '',
        '    Override format inference when extensions are misleading:',
        '',
        '    etlplus extract data.txt --source-format csv',
        '    etlplus load payload.bin --target-format json',
    ],
)

CLI_EPILOG: Final[str] = '\n'.join(
    [
        'Tip:',
        '    --source-format and --target-format override format inference '
        'based on filename extensions when needed.',
    ],
)

PROJECT_URL: Final[str] = 'https://github.com/Dagitali/ETLPlus'


# SECTION: TYPE ALIASES ==================================================== #


SourceInputArg = Annotated[
    str,
    typer.Argument(
        ...,
        metavar='SOURCE',
        help=(
            'Extract from SOURCE. Use --from/--source-type to override the '
            'inferred connector when needed.'
        ),
    ),
]

StreamingSourceArg = Annotated[
    str,
    typer.Argument(
        ...,
        metavar='SOURCE',
        help=(
            'Data source to transform or validate (path, JSON payload, or '
            '- for stdin).'
        ),
    ),
]

TargetInputArg = Annotated[
    str,
    typer.Argument(
        ...,
        metavar='TARGET',
        help=(
            'Load JSON data from stdin into TARGET. Use --to/--target-type '
            'to override connector inference when needed. Source data must '
            'be piped into stdin.'
        ),
    ),
]

SourceOverrideOption = Annotated[
    str | None,
    typer.Option(
        '--source-type',
        metavar='CONNECTOR',
        show_default=False,
        rich_help_panel='I/O overrides',
        help='Override the inferred source type (file, database, api).',
    ),
]

TargetOverrideOption = Annotated[
    str | None,
    typer.Option(
        '--target-type',
        metavar='CONNECTOR',
        show_default=False,
        rich_help_panel='I/O overrides',
        help='Override the inferred target type (file, database, api).',
    ),
]

SourceFormatOption = Annotated[
    str | None,
    typer.Option(
        '--source-format',
        metavar='FORMAT',
        show_default=False,
        rich_help_panel='Format overrides',
        help=(
            'Input payload format when SOURCE is - or an inline payload. '
            'File sources infer format from the extension.'
        ),
    ),
]

StdinFormatOption = Annotated[
    str | None,
    typer.Option(
        '--source-format',
        metavar='FORMAT',
        show_default=False,
        rich_help_panel='Format overrides',
        help='Input payload format when reading from stdin (default: json).',
    ),
]

TargetFormatOption = Annotated[
    str | None,
    typer.Option(
        '--target-format',
        metavar='FORMAT',
        show_default=False,
        rich_help_panel='Format overrides',
        help=(
            'Payload format when TARGET is - or a non-file connector. File '
            'targets infer format from the extension.'
        ),
    ),
]

OperationsJSONOption = Annotated[
    str,
    typer.Option(
        '--operations',
        help='Transformation operations as JSON string.',
    ),
]

RulesJSONOption = Annotated[
    str,
    typer.Option(
        '--rules',
        help='Validation rules as JSON string.',
    ),
]

TargetPathOption = Annotated[
    str | None,
    typer.Option(
        '--target',
        metavar='PATH',
        help='Target file for transformed or validated output (- for stdout).',
    ),
]

PipelineConfigOption = Annotated[
    str,
    typer.Option(
        ...,
        '--config',
        metavar='PATH',
        help='Path to pipeline YAML configuration file.',
    ),
]

RenderConfigOption = Annotated[
    str | None,
    typer.Option(
        '--config',
        metavar='PATH',
        help='Pipeline YAML that includes table_schemas for rendering.',
        show_default=False,
    ),
]

RenderOutputOption = Annotated[
    str | None,
    typer.Option(
        '--output',
        '-o',
        metavar='PATH',
        help='Write rendered SQL to PATH (default: stdout).',
    ),
]

RenderSpecOption = Annotated[
    str | None,
    typer.Option(
        '--spec',
        metavar='PATH',
        help='Standalone table spec file (.yml/.yaml/.json).',
        show_default=False,
    ),
]

RenderTableOption = Annotated[
    str | None,
    typer.Option(
        '--table',
        metavar='NAME',
        help='Filter to a single table name from table_schemas.',
    ),
]

RenderTemplateOption = Annotated[
    str,
    typer.Option(
        '--template',
        '-t',
        metavar='KEY|PATH',
        help='Template key (ddl/view) or path to a Jinja template file.',
        show_default=True,
    ),
]

RenderTemplatePathOption = Annotated[
    str | None,
    typer.Option(
        '--template-path',
        metavar='PATH',
        help=(
            'Explicit path to a Jinja template file (overrides template key).'
        ),
    ),
]


# SECTION: DATA CLASSES ===================================================== #


@dataclass(slots=True)
class CliState:
    """Mutable container for runtime CLI toggles."""

    pretty: bool = True
    quiet: bool = False
    verbose: bool = False


# SECTION: INTERNAL FUNCTIONS =============================================== #


def _ensure_state(
    ctx: typer.Context,
) -> CliState:
    """
    Return the :class:`CliState` stored on the :mod:`typer` context.

    Parameters
    ----------
    ctx : typer.Context
        Typer execution context provided to the command.

    Returns
    -------
    CliState
        Mutable CLI flag container stored on ``ctx``.
    """
    if not isinstance(getattr(ctx, 'obj', None), CliState):
        ctx.obj = CliState()
    return ctx.obj


def _format_namespace_kwargs(
    *,
    format_value: str | None,
    default: str,
) -> dict[str, object]:
    """
    Return common namespace kwargs for format handling.

    Parameters
    ----------
    format_value : str | None
        User-provided format value from the CLI option.
    default : str
        Default format to use when none is provided.

    Returns
    -------
    dict[str, object]
        Keyword arguments for format-related namespace attributes.
    """
    return {
        'format': (format_value or default),
        '_format_explicit': (format_value is not None),
    }


def _infer_resource_type(
    value: str,
) -> str:
    """
    Infer the resource type from a path, URL, or DSN string.

    Parameters
    ----------
    value : str
        Raw CLI argument that represents a source or target.

    Returns
    -------
    str
        One of ``file``, ``database``, or ``api`` based on heuristics.

    Raises
    ------
    ValueError
        If the resource type could not be inferred.
    """
    val = (value or '').strip()
    low = val.lower()

    match (val, low):
        case ('-', _):
            return 'file'
        case (_, inferred) if inferred.startswith(('http://', 'https://')):
            return 'api'
        case (_, inferred) if inferred.startswith(_DB_SCHEMES):
            return 'database'

    path = Path(val)
    if path.exists() or path.suffix:
        return 'file'

    raise ValueError(
        'Could not infer resource type. Use --from/--to to specify it.',
    )


def _infer_resource_type_or_exit(
    value: str,
) -> str:
    """Infer a resource type and map ``ValueError`` to ``BadParameter``.

    Parameters
    ----------
    value : str
        CLI value describing a source/target.

    Returns
    -------
    str
        Inferred resource type.

    Raises
    ------
    typer.BadParameter
        If heuristics fail to infer a resource type.
    """
    try:
        return _infer_resource_type(value)
    except ValueError as exc:  # pragma: no cover - exercised indirectly
        raise typer.BadParameter(str(exc)) from exc


def _infer_resource_type_soft(
    value: str | None,
) -> str | None:
    """
    Make a best-effort inference that tolerates inline payloads.

    Parameters
    ----------
    value : str | None
        CLI value describing a source/target.

    Returns
    -------
    str | None
        Inferred resource type, or ``None`` if inference failed.
    """
    if value is None:
        return None
    try:
        return _infer_resource_type(value)
    except ValueError:
        return None


def _log_inferred_resource(
    state: CliState,
    *,
    role: str,
    value: str,
    resource_type: str | None,
) -> None:
    """
    Emit a uniform verbose message for inferred resource types.

    Parameters
    ----------
    state : CliState
        Current CLI state stored on the Typer context.
    role : str
        Friendly label for the resource (e.g., ``source`` or ``target``).
    value : str
        Resource value provided on the CLI.
    resource_type : str | None
        Inferred resource type or ``None`` if not inferred.
    """
    if not state.verbose or resource_type is None:
        return
    print(
        f'Inferred {role}_type={resource_type} for {role}={value}',
        file=sys.stderr,
    )


def _ns(
    **kwargs: object,
) -> argparse.Namespace:
    """Build an :class:`argparse.Namespace` for the legacy handlers.

    Parameters
    ----------
    **kwargs : object
        Attributes applied to the resulting namespace.

    Returns
    -------
    argparse.Namespace
        Namespace compatible with the ``cmd_*`` handler signatures.
    """
    return argparse.Namespace(**kwargs)


def _optional_choice(
    value: str | None,
    choices: Collection[str],
    *,
    label: str,
) -> str | None:
    """
    Validate optional CLI choice inputs while preserving ``None``.

    Parameters
    ----------
    value : str | None
        Candidate value provided by the CLI option.
    choices : Collection[str]
        Allowed options for the parameter.
    label : str
        Friendly label rendered in error messages.

    Returns
    -------
    str | None
        Sanitized choice or ``None`` when the option is omitted.
    """
    if value is None:
        return None
    return _validate_choice(value, choices, label=label)


def _resolve_resource_type(
    *,
    explicit_type: str | None,
    override_type: str | None,
    value: str,
    label: str,
    conflict_error: str | None = None,
    legacy_file_error: str | None = None,
) -> str:
    """
    Resolve resource type preference order and validate it.

    Parameters
    ----------
    explicit_type : str | None
        Explicit resource type provided by the user.
    override_type : str | None
        Resource type provided by an overriding option.
    value : str
        Resource value to infer type from if no explicit or override type is
        given.
    label : str
        Friendly label for error messages.
    conflict_error : str | None
        Error message to raise if there is a conflict between explicit and
        override types.
    legacy_file_error : str | None
        Error message to raise if the explicit type is a legacy 'file' type.

    Returns
    -------
    str
        Resolved and validated resource type.

    Raises
    ------
    typer.BadParameter
        If there is a conflict between explicit and override types, or if the
        explicit type is a legacy 'file' type.
    """
    if explicit_type is not None:
        if override_type is not None and conflict_error:
            raise typer.BadParameter(conflict_error)
        if legacy_file_error and explicit_type.strip().lower() == 'file':
            raise typer.BadParameter(legacy_file_error)
        candidate = explicit_type
    else:
        candidate = override_type or _infer_resource_type_or_exit(value)
    return _validate_choice(candidate, _SOURCE_CHOICES, label=label)


def _stateful_namespace(
    state: CliState,
    *,
    command: str,
    **kwargs: object,
) -> argparse.Namespace:
    """
    Attach CLI state toggles to a handler namespace.

    Parameters
    ----------
    state : CliState
        Current CLI state stored on the Typer context.
    command : str
        Logical command name (e.g., ``extract``).
    **kwargs : object
        Additional attributes required by the handler.

    Returns
    -------
    argparse.Namespace
        Namespace compatible with the ``cmd_*`` handler signatures.
    """
    return _ns(
        command=command,
        pretty=state.pretty,
        quiet=state.quiet,
        verbose=state.verbose,
        **kwargs,
    )


def _validate_choice(
    value: str,
    choices: Collection[str],
    *,
    label: str,
) -> str:
    """
    Validate CLI input against a whitelist of choices.

    Parameters
    ----------
    value : str
        Candidate value from the CLI option or argument.
    choices: Collection[str]
        Allowed values for the option.
    label : str
        Friendly label rendered in the validation error message.

    Returns
    -------
    str
        Sanitized and validated value.

    Raises
    ------
    typer.BadParameter
        If ``value`` is not present in ``choices``.
    """
    v = (value or '').strip()
    if v in choices:
        return v
    allowed = ', '.join(sorted(choices))
    raise typer.BadParameter(
        f"Invalid {label} '{value}'. Choose from: {allowed}",
    )


# SECTION: TYPER APP ======================================================== #


# Typer application instance (subcommands are registered below).
app = typer.Typer(
    name='etlplus',
    # help='ETLPlus - A Swiss Army knife for simple ETL operations.',
    help=CLI_DESCRIPTION,
    epilog=CLI_EPILOG,
    add_completion=True,
    no_args_is_help=False,
    rich_markup_mode='markdown',
)


@app.callback(invoke_without_command=True)
def _root(
    ctx: typer.Context,
    version: bool = typer.Option(
        False,
        '--version',
        '-V',
        is_eager=True,
        help='Show the version and exit.',
    ),
    pretty: bool = typer.Option(
        True,
        '--pretty/--no-pretty',
        help='Pretty-print JSON output (default: pretty).',
    ),
    quiet: bool = typer.Option(
        False,
        '--quiet',
        '-q',
        help='Suppress warnings and non-essential output.',
    ),
    verbose: bool = typer.Option(
        False,
        '--verbose',
        '-v',
        help='Emit extra diagnostics to stderr.',
    ),
) -> None:
    """
    Seed the Typer context with runtime flags and handle root-only options.

    Parameters
    ----------
    ctx : typer.Context
        Typer execution context provided to the command.
    version : bool
        If True, print the etlplus version and exit.
    pretty : bool
        Whether to pretty-print JSON output.
    quiet : bool
        Whether to suppress warnings and non-essential output.
    verbose : bool
        Whether to emit extra diagnostics to stderr.

    Raises
    ------
    typer.Exit
        If ``--version`` is provided or no subcommand is invoked.
    """
    ctx.obj = CliState(pretty=pretty, quiet=quiet, verbose=verbose)

    if version:
        typer.echo(f'etlplus {__version__}')
        raise typer.Exit(0)

    if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
        typer.echo(ctx.command.get_help(ctx))
        raise typer.Exit(0)


@app.command('extract')
def extract_cmd(
    ctx: typer.Context,
    source: SourceInputArg,
    source_format: SourceFormatOption | None = None,
    source_type: SourceOverrideOption | None = None,
) -> int:
    """
    Extract data from files, databases, or REST APIs.

    Parameters
    ----------
    ctx : typer.Context
        Typer execution context provided to the command.
    source : SourceInputArg
        Data source (file path, URL, DSN, or ``-`` for stdin).
    source_format : SourceFormatOption | None, optional
        Payload format when not a file.
    source_type : SourceOverrideOption | None, optional
        Override the inferred source type.

    Returns
    -------
    int
        Zero on success.

    Examples
    --------
    - Extract from a file (type inferred):
        etlplus extract in.csv
    - Extract from a file (explicit via flag):
        etlplus extract --from file in.csv
    - Extract from an API:
        etlplus extract https://example.com/data.json
        etlplus extract --from api https://example.com/data.json
    - Extract from a database DSN:
        etlplus extract --from database postgresql://user:pass@host/db
    - Pipe into transform/load:
        etlplus extract in.csv \
        | etlplus transform --operations '{"select":["a"]}'

    Notes
    -----
    - The ``extract`` command always writes JSON to stdout.
    - CSV output is unsupported for this command.
    - Use shell redirection (``>``) or pipelines to persist the output.
    """
    state = _ensure_state(ctx)

    source_type = _optional_choice(
        source_type,
        _SOURCE_CHOICES,
        label='source_type',
    )
    source_format = _optional_choice(
        source_format,
        _FORMAT_CHOICES,
        label='source_format',
    )

    resolved_source = source
    resolved_source_type = source_type or _infer_resource_type_or_exit(
        resolved_source,
    )

    _log_inferred_resource(
        state,
        role='source',
        value=resolved_source,
        resource_type=resolved_source_type,
    )

    format_kwargs = _format_namespace_kwargs(
        format_value=source_format,
        default='json',
    )
    ns = _stateful_namespace(
        state,
        command='extract',
        source_type=resolved_source_type,
        source=resolved_source,
        **format_kwargs,
    )
    return int(cmd_extract(ns))


@app.command('list')
def list_cmd(
    ctx: typer.Context,
    config: PipelineConfigOption,
    jobs: bool = typer.Option(
        False,
        '--jobs',
        help='List available job names and exit',
    ),
    pipelines: bool = typer.Option(
        False,
        '--pipelines',
        help='List ETL pipelines',
    ),
    sources: bool = typer.Option(
        False,
        '--sources',
        help='List data sources',
    ),
    summary: bool = typer.Option(
        False,
        '--summary',
        help='Show pipeline summary (name, version, sources, targets, jobs)',
    ),
    targets: bool = typer.Option(
        False,
        '--targets',
        help='List data targets',
    ),
    transforms: bool = typer.Option(
        False,
        '--transforms',
        help='List data transforms',
    ),
) -> int:
    """
    Print ETL entities from a pipeline YAML configuration.

    Parameters
    ----------
    ctx : typer.Context
        Typer execution context provided to the command.
    config : PipelineConfigOption
        Path to pipeline YAML configuration file.
    jobs : bool, optional
        If True, list available job names and exit.
    pipelines : bool, optional
        If True, list ETL pipelines.
    sources : bool, optional
        If True, list data sources.
    summary : bool, optional
        If True, show pipeline summary (name, version, sources, targets, jobs).
    targets : bool, optional
        If True, list data targets.
    transforms : bool, optional
        If True, list data transforms.

    Returns
    -------
    int
        Zero on success.
    """
    state = _ensure_state(ctx)
    ns = _stateful_namespace(
        state,
        command='list',
        config=config,
        summary=summary,
        pipelines=pipelines,
        jobs=jobs,
        sources=sources,
        targets=targets,
        transforms=transforms,
    )
    return int(cmd_list(ns))


@app.command('load')
def load_cmd(
    ctx: typer.Context,
    target: TargetInputArg,
    source_format: StdinFormatOption | None = None,
    target_format: TargetFormatOption | None = None,
    target_type: TargetOverrideOption | None = None,
) -> int:
    """
    Load data into a file, database, or REST API.

    Parameters
    ----------
    ctx : typer.Context
        Typer execution context provided to the command.
    target : TargetInputArg
        Load destination (file path, URL/DSN, or ``-`` for stdout).
    source_format : StdinFormatOption | None, optional
        Hint for parsing stdin payloads (json or csv).
    target_format : TargetFormatOption | None, optional
        Payload format when not a file target (or when TARGET is ``-``).
    target_type : TargetOverrideOption | None, optional
        Override the inferred target type.

    Returns
    -------
    int
        Zero on success.

    Examples
    --------
    - Pipe into a file:
        etlplus extract in.csv \
        | etlplus transform --operations '{"select":["a"]}' \
        | etlplus load --to file out.json
    - Read from stdin and write to a file:
        etlplus load out.json
    - Write to stdout:
        etlplus load --to file -

    Notes
    -----
    - The ``load`` command reads JSON from stdin.
    - CSV input is unsupported unless ``--source-format csv`` is provided.
    - Convert upstream before piping into ``load`` when working with other
        formats.
    """
    state = _ensure_state(ctx)

    source_format = _optional_choice(
        source_format,
        _FORMAT_CHOICES,
        label='source_format',
    )
    target_type = _optional_choice(
        target_type,
        _SOURCE_CHOICES,
        label='target_type',
    )
    target_format = _optional_choice(
        target_format,
        _FORMAT_CHOICES,
        label='target_format',
    )

    resolved_target = target
    resolved_target_type = target_type or _infer_resource_type_or_exit(
        resolved_target,
    )

    resolved_source_value = '-'
    resolved_source_type = _infer_resource_type_soft(resolved_source_value)

    _log_inferred_resource(
        state,
        role='source',
        value=resolved_source_value,
        resource_type=resolved_source_type,
    )
    _log_inferred_resource(
        state,
        role='target',
        value=resolved_target,
        resource_type=resolved_target_type,
    )

    format_kwargs = _format_namespace_kwargs(
        format_value=target_format,
        default='json',
    )
    ns = _stateful_namespace(
        state,
        command='load',
        source=resolved_source_value,
        source_format=source_format,
        target_type=resolved_target_type,
        target=resolved_target,
        **format_kwargs,
    )
    return int(cmd_load(ns))


@app.command('pipeline')
def pipeline_cmd(
    ctx: typer.Context,
    config: PipelineConfigOption,
    job: str | None = typer.Option(
        None,
        '--job',
        metavar='JOB',
        help='Run a specific job by name',
    ),
    jobs: bool = typer.Option(
        False,
        '--jobs',
        help='List available job names and exit',
    ),
    pipeline: str | None = typer.Option(
        None,
        '--pipeline',
        help='Run a specific pipeline by name',
    ),
) -> int:
    """
    Deprecated wrapper to inspect or run a pipeline YAML configuration.

    Parameters
    ----------
    ctx : typer.Context
        Typer execution context provided to the command.
    config : PipelineConfigOption
        Path to pipeline YAML configuration file.
    job : str | None, optional
        Name of a specific job to run.
    jobs : bool, optional
        If True, list available job names and exit.
    pipeline : str | None, optional
        Name of a specific pipeline to run.

    Returns
    -------
    int
        Zero on success.
    """
    state = _ensure_state(ctx)
    run_target = job or pipeline
    ns = _stateful_namespace(
        state,
        command='pipeline',
        config=config,
        list=jobs,
        run=run_target,
    )
    return int(cmd_pipeline(ns))


@app.command('render')
def render_cmd(
    ctx: typer.Context,
    config: RenderConfigOption = None,
    spec: RenderSpecOption = None,
    table: RenderTableOption = None,
    template: RenderTemplateOption = 'ddl',
    template_path: RenderTemplatePathOption = None,
    output: RenderOutputOption = None,
) -> int:
    """
    Render SQL DDL from table schemas defined in YAML/JSON configs.

    Parameters
    ----------
    ctx : typer.Context
        Typer execution context provided to the command.
    config : RenderConfigOption, optional
        Pipeline YAML containing ``table_schemas`` entries.
    spec : RenderSpecOption, optional
        Standalone table spec file (.yml/.yaml/.json).
    table : RenderTableOption, optional
        Filter to a single table name within the available specs.
    template : RenderTemplateOption, optional
        Built-in template key or template file path.
    template_path : RenderTemplatePathOption, optional
        Explicit template file path to render with.
    output : RenderOutputOption, optional
        Path to write SQL to (stdout when omitted).

    Returns
    -------
    int
        Zero on success.
    """
    state = _ensure_state(ctx)
    ns = _stateful_namespace(
        state,
        command='render',
        config=config,
        spec=spec,
        table=table,
        template=template,
        template_path=template_path,
        output=output,
    )
    return int(cmd_render(ns))


@app.command('run')
def run_cmd(
    ctx: typer.Context,
    config: PipelineConfigOption,
    job: str | None = typer.Option(
        None,
        '-j',
        '--job',
        help='Name of the job to run',
    ),
    pipeline: str | None = typer.Option(
        None,
        '-p',
        '--pipeline',
        help='Name of the pipeline to run',
    ),
) -> int:
    """
    Execute an ETL job or pipeline from a YAML configuration.

    Parameters
    ----------
    ctx : typer.Context
        Typer execution context provided to the command.
    config : PipelineConfigOption
        Path to pipeline YAML configuration file.
    job : str | None, optional
        Name of the job to run.
    pipeline : str | None, optional
        Name of the pipeline to run.

    Returns
    -------
    int
        Zero on success.
    """
    state = _ensure_state(ctx)
    ns = _stateful_namespace(
        state,
        command='run',
        config=config,
        job=job,
        pipeline=pipeline,
    )
    return int(cmd_run(ns))


@app.command('transform')
def transform_cmd(
    ctx: typer.Context,
    operations: OperationsJSONOption = '{}',
    source: StreamingSourceArg = '-',
    source_format: SourceFormatOption | None = None,
    source_type: SourceOverrideOption | None = None,
    target: TargetPathOption | None = None,
    target_format: TargetFormatOption | None = None,
    target_type: TargetOverrideOption | None = None,
) -> int:
    """
    Transform records using JSON-described operations.

    Parameters
    ----------
    ctx : typer.Context
        Typer execution context provided to the command.
    operations : OperationsJSONOption, optional
        Transformation operations as a JSON string.
    source : StreamingSourceArg, optional
        Data source (file path or ``-`` for stdin).
    source_format : SourceFormatOption | None, optional
        Input payload format when not a file (or when SOURCE is -).
    source_type : SourceOverrideOption | None, optional
        Override the inferred source type.
    target : TargetPathOption | None, optional
        Optional output path. Use ``-`` for stdout.
    target_format : TargetFormatOption | None, optional
        Output payload format when not a file target (or when OUTPUT is -).
        Accepts ``--target-format``.
    target_type : TargetOverrideOption | None, optional
        Override the inferred target type.

    Returns
    -------
    int
        Zero on success.

    Examples
    --------
    - Transform data from a file and write to another file:
        etlplus transform --from file in.json \
        --operations '{"select": ["id", "name"]}' \
        --to file out.json
    - Transform data from stdin and write to stdout:
        cat in.json \
        | etlplus transform \
        --operations '{"filter": {"field": "age", "gt": 30}}'
    - Transform data from a file and write to stdout:
        etlplus transform --from file in.csv \
        --source-format csv \
        --operations '{"select": ["id", "email"]}'
    - Transform data from stdin and write to a file:
        cat in.json \
        | etlplus transform --operations '{"sort": ["-created_at"]}' \
        --to file out.json

    Notes
    -----
    - The ``transform`` command reads JSON from stdin when SOURCE is ``-``.
    - CSV input is unsupported for this command.
    - Convert upstream before piping into ``transform``.
    """
    state = _ensure_state(ctx)

    source_format = _optional_choice(
        source_format,
        _FORMAT_CHOICES,
        label='source_format',
    )
    source_type = _optional_choice(
        source_type,
        _SOURCE_CHOICES,
        label='source_type',
    )
    target_format = _optional_choice(
        target_format,
        _FORMAT_CHOICES,
        label='target_format',
    )
    target_format_kwargs = _format_namespace_kwargs(
        format_value=target_format,
        default='json',
    )
    target_type = _optional_choice(
        target_type,
        _SOURCE_CHOICES,
        label='target_type',
    )

    resolved_source_type = source_type or _infer_resource_type_soft(source)
    resolved_source_value = source if source is not None else '-'
    resolved_target_value = target if target is not None else '-'

    if resolved_source_type is not None:
        resolved_source_type = _validate_choice(
            resolved_source_type,
            _SOURCE_CHOICES,
            label='source_type',
        )

    resolved_target_type = _resolve_resource_type(
        explicit_type=None,
        override_type=target_type,
        value=resolved_target_value,
        label='target_type',
    )

    _log_inferred_resource(
        state,
        role='source',
        value=resolved_source_value,
        resource_type=resolved_source_type,
    )
    _log_inferred_resource(
        state,
        role='target',
        value=resolved_target_value,
        resource_type=resolved_target_type,
    )

    ns = _stateful_namespace(
        state,
        command='transform',
        source=resolved_source_value,
        source_type=resolved_source_type,
        operations=json_type(operations),
        target=resolved_target_value,
        source_format=source_format,
        target_type=resolved_target_type,
        target_format=target_format_kwargs['format'],
        **target_format_kwargs,
    )
    return int(cmd_transform(ns))


@app.command('validate')
def validate_cmd(
    ctx: typer.Context,
    rules: RulesJSONOption = '{}',
    source: StreamingSourceArg = '-',
    source_format: SourceFormatOption | None = None,
    source_type: SourceOverrideOption | None = None,
    target: TargetPathOption | None = None,
) -> int:
    """
    Validate data against JSON-described rules.

    Parameters
    ----------
    ctx : typer.Context
        Typer execution context provided to the command.
    rules : RulesJSONOption, optional
        Validation rules as a JSON string.
    source : StreamingSourceArg, optional
        Data source (file path or ``-`` for stdin).
    source_format : SourceFormatOption | None, optional
        Optional stdin format hint (JSON or CSV) when SOURCE is ``-``.
    source_type : SourceOverrideOption | None, optional
        Override the inferred source type when heuristics fail.
    target : TargetPathOption | None, optional
        Optional output path. Use ``-`` for stdout.

    Returns
    -------
    int
        Zero on success.
    """
    source_format = _optional_choice(
        source_format,
        _FORMAT_CHOICES,
        label='source_format',
    )
    source_type = _optional_choice(
        source_type,
        _SOURCE_CHOICES,
        label='source_type',
    )
    source_format_kwargs = _format_namespace_kwargs(
        format_value=source_format,
        default='json',
    )

    state = _ensure_state(ctx)
    resolved_source_type = source_type or _infer_resource_type_soft(source)

    _log_inferred_resource(
        state,
        role='source',
        value=source,
        resource_type=resolved_source_type,
    )

    ns = _stateful_namespace(
        state,
        command='validate',
        source=source,
        source_type=resolved_source_type,
        rules=json_type(rules),  # convert CLI string to dict
        target=target,
        source_format=source_format,
        **source_format_kwargs,
    )
    return int(cmd_validate(ns))
