"""Agent CLI run bundle command - execute a pipeline from a bundle file or directory."""

from __future__ import annotations

import asyncio
import functools
from pathlib import Path
from typing import Annotated, Any

import typer
from mthds.runners.types import RunnerType

from pipelex.builder.conventions import DEFAULT_BUNDLE_FILE_NAME, DEFAULT_INPUTS_FILE_NAME
from pipelex.cli.agent_cli.commands.agent_cli_factory import make_pipelex_for_agent_cli
from pipelex.cli.agent_cli.commands.agent_output import CliOutputFormat, agent_error, agent_success_formatted, set_agent_cli_error_format
from pipelex.cli.agent_cli.commands.run._output_helpers import format_run_markdown
from pipelex.cli.agent_cli.commands.run._run_core import run_pipeline_core
from pipelex.cli.agent_cli.commands.run._run_core_api import run_pipeline_core_api
from pipelex.cli.agent_cli.commands.run.stdin_resolver import parse_cli_inputs
from pipelex.core.interpreter.exceptions import MthdsDecodeError, PipelexInterpreterError
from pipelex.core.interpreter.helpers import MTHDS_EXTENSION, is_pipelex_file
from pipelex.core.interpreter.interpreter import PipelexInterpreter
from pipelex.core.pipes.exceptions import PipeOperatorModelChoiceError
from pipelex.pipe_operators.exceptions import PipeOperatorModelAvailabilityError
from pipelex.pipelex import Pipelex
from pipelex.pipeline.exceptions import PipelineExecutionError


def run_bundle_cmd(
    ctx: typer.Context,
    path: Annotated[
        str,
        typer.Argument(help="Path to a .mthds bundle file or a pipeline directory"),
    ],
    pipe: Annotated[
        str | None,
        typer.Option("--pipe", help="Pipe code to run (overrides bundle's main_pipe)"),
    ] = None,
    inputs: Annotated[
        str | None,
        typer.Option("--inputs", "-i", help="Path to JSON file with inputs or inline JSON"),
    ] = None,
    dry_run: Annotated[
        bool,
        typer.Option("--dry-run", help="Run pipeline in dry mode (no actual inference calls)"),
    ] = False,
    mock_inputs: Annotated[
        bool,
        typer.Option("--mock-inputs", help="Generate mock data for missing required inputs (requires --dry-run)"),
    ] = False,
    graph: Annotated[
        bool,
        typer.Option("--graph/--no-graph", help="Generate execution graph visualizations (saved alongside output)"),
    ] = True,
    library_dir: Annotated[
        list[str] | None,
        typer.Option("--library-dir", "-L", help="Directory to search for pipe definitions (.mthds files)"),
    ] = None,
    with_memory: Annotated[
        bool,
        typer.Option("--with-memory", help="Include full working memory in output (for piping to another method)"),
    ] = False,
    output_format: Annotated[
        CliOutputFormat,
        typer.Option("--format", help="Success output format: markdown (default) or json (structured)"),
    ] = CliOutputFormat.MARKDOWN,
    error_format: Annotated[
        CliOutputFormat | None,
        typer.Option("--error-format", help="Error output format (defaults to --format value): markdown or json"),
    ] = None,
) -> None:
    """Execute a pipeline from a bundle file (.mthds) or pipeline directory.

    Default output is markdown; use --format json for structured JSON.
    Results go to stdout on success, errors to stderr with exit code 1.
    Graph visualizations are generated by default; use --no-graph to disable.

    Examples:
        pipelex-agent run bundle mthds-wip/pipeline_01/
        pipelex-agent run bundle my_bundle.mthds --pipe my_pipe
        pipelex-agent run bundle pipeline_01/ --dry-run --mock-inputs
        pipelex-agent run bundle pipeline_01/ --format json
    """
    set_agent_cli_error_format(error_format or output_format)

    # Validate --mock-inputs requires --dry-run
    if mock_inputs and not dry_run:
        agent_error("--mock-inputs requires --dry-run", "ArgumentError")

    pipe_code: str | None = pipe
    bundle_path: str | None = None
    auto_inputs_path: str | None = None
    target_path = Path(path)

    if target_path.is_dir():
        # Directory mode: auto-detect bundle, inputs, and library dir
        bundle_file = target_path / DEFAULT_BUNDLE_FILE_NAME
        if bundle_file.is_file():
            bundle_path = str(bundle_file)
        else:
            mthds_files = list(target_path.glob(f"*{MTHDS_EXTENSION}"))
            if len(mthds_files) == 0:
                agent_error(f"No .mthds bundle file found in directory '{path}'", "FileNotFoundError")
            if len(mthds_files) > 1:
                mthds_names = ", ".join(mthds_file.name for mthds_file in mthds_files)
                agent_error(
                    f"Multiple .mthds files found in '{path}' ({mthds_names}) and no '{DEFAULT_BUNDLE_FILE_NAME}'. "
                    f"Pass the .mthds file directly instead.",
                    "ArgumentError",
                )
            bundle_path = str(mthds_files[0])

        # Auto-detect inputs file (used as low-priority fallback)
        inputs_file = target_path / DEFAULT_INPUTS_FILE_NAME
        auto_inputs_path = str(inputs_file) if inputs_file.is_file() else None

        # Add directory as library dir (prepend to user-supplied list)
        target_dir_str = str(target_path)
        if library_dir is None:
            library_dir = [target_dir_str]
        elif target_dir_str not in library_dir:
            library_dir = [target_dir_str, *library_dir]

    elif is_pipelex_file(target_path):
        bundle_path = path
    else:
        agent_error(
            f"'{path}' is not a .mthds file or directory. Use 'run pipe <code>' for pipe codes, or 'run bundle <path>' for .mthds files/directories.",
            "ArgumentError",
        )

    # Load MTHDS content from bundle
    mthds_content: str | None = None
    if bundle_path:
        try:
            mthds_content = Path(bundle_path).read_text(encoding="utf-8")
            if not pipe_code:
                bundle_blueprint = PipelexInterpreter.make_pipelex_bundle_blueprint(mthds_content=mthds_content)
                main_pipe_code = bundle_blueprint.main_pipe
                if not main_pipe_code:
                    agent_error(
                        f"Bundle '{bundle_path}' does not declare a main_pipe. Specify a pipe code with --pipe.",
                        "BundleError",
                    )
                pipe_code = main_pipe_code
        except FileNotFoundError as exc:
            agent_error(f"Bundle file not found: {bundle_path}", "FileNotFoundError", cause=exc)
        except (OSError, UnicodeDecodeError) as exc:
            agent_error(f"Failed to read bundle file '{bundle_path}': {exc}", type(exc).__name__, cause=exc)
        except (PipelexInterpreterError, MthdsDecodeError) as exc:
            agent_error(f"Failed to parse bundle '{bundle_path}': {exc}", type(exc).__name__, cause=exc)

    # Load inputs: --inputs flag takes priority, then stdin fallback, then auto-detected
    pipeline_inputs: dict[str, Any] | None = parse_cli_inputs(inputs_arg=inputs, stdin_fallback=True, auto_inputs_path=auto_inputs_path)

    runner_type: RunnerType = ctx.obj["runner"]

    match runner_type:
        case RunnerType.API:
            # Validate unsupported flags for API runner
            if dry_run:
                agent_error("--dry-run is not supported with --runner api", "ArgumentError")
            if mock_inputs:
                agent_error("--mock-inputs is not supported with --runner api", "ArgumentError")

            from mthds.client.exceptions import ClientAuthenticationError, PipelineRequestError  # noqa: PLC0415

            try:
                result = asyncio.run(
                    run_pipeline_core_api(
                        pipe_code=pipe_code,  # type: ignore[arg-type]
                        mthds_contents=[mthds_content] if mthds_content else None,
                        inputs=pipeline_inputs,
                        with_memory=with_memory,
                    )
                )
                agent_success_formatted(result, functools.partial(format_run_markdown, with_memory=with_memory), output_format)

            except ClientAuthenticationError as exc:
                agent_error(str(exc), "ClientAuthenticationError", cause=exc)

            except PipelineRequestError as exc:
                agent_error(str(exc), "PipelineRequestError", cause=exc)

            except Exception as exc:  # noqa: BLE001
                # Agent CLI command boundary: agent_error() (NoReturn) converts any unexpected failure into the structured error payload.
                agent_error(str(exc), type(exc).__name__, cause=exc)

        case RunnerType.PIPELEX:
            make_pipelex_for_agent_cli(log_level=ctx.obj["log_level"], needs_inference=not dry_run, needs_model_specs=True)

            try:
                result = asyncio.run(
                    run_pipeline_core(
                        pipe_code=pipe_code,  # type: ignore[arg-type]
                        mthds_contents=[mthds_content] if mthds_content else None,
                        bundle_uris=[bundle_path] if bundle_path else None,
                        inputs=pipeline_inputs,
                        dry_run=dry_run,
                        mock_inputs=mock_inputs,
                        library_dirs=library_dir,
                        graph=graph,
                        with_memory=with_memory,
                    )
                )
                agent_success_formatted(result, functools.partial(format_run_markdown, with_memory=with_memory), output_format)

            except PipelineExecutionError as exc:
                extra_fields: dict[str, Any] = {
                    "pipe_code": exc.pipe_code,
                    "pipe_stack": exc.pipe_stack,
                }
                if exc.__cause__:
                    extra_fields["cause_type"] = type(exc.__cause__).__name__
                    extra_fields["cause_message"] = str(exc.__cause__)
                agent_error(exc.message, "PipelineExecutionError", cause=exc, **extra_fields)

            except PipeOperatorModelChoiceError as exc:
                agent_error(
                    exc.message,
                    "PipeOperatorModelChoiceError",
                    cause=exc,
                    pipe_code=exc.pipe_code,
                    model_type=str(exc.model_type),
                    model_choice=str(exc.model_choice),
                )

            except PipeOperatorModelAvailabilityError as exc:
                availability_extra: dict[str, Any] = {
                    "pipe_code": exc.pipe_code,
                    "model_handle": exc.model_handle,
                }
                if exc.fallback_list:
                    availability_extra["fallback_list"] = exc.fallback_list
                if exc.pipe_stack:
                    availability_extra["pipe_stack"] = exc.pipe_stack
                agent_error(exc.message, "PipeOperatorModelAvailabilityError", cause=exc, **availability_extra)

            except Exception as exc:  # noqa: BLE001
                # Agent CLI command boundary: agent_error() (NoReturn) converts any unexpected failure into the structured error payload.
                agent_error(str(exc), type(exc).__name__, cause=exc)

            finally:
                Pipelex.teardown_if_needed()
