"""Agent CLI run pipe command - execute a pipeline by pipe code or bundle path."""

from __future__ import annotations

import asyncio
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 agent_error, agent_success
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.cli.method_resolver import resolve_pipe_from_exports
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_pipe_cmd(
    ctx: typer.Context,
    target: Annotated[
        str | None,
        typer.Argument(help="Pipe code or bundle file path (auto-detected)"),
    ] = None,
    pipe: Annotated[
        str | None,
        typer.Option("--pipe", help="Pipe code to run"),
    ] = None,
    bundle: Annotated[
        str | None,
        typer.Option("--bundle", help="Bundle file path (.mthds)"),
    ] = 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,
) -> None:
    """Execute a pipeline by pipe code or bundle path and output JSON results.

    Outputs JSON to stdout on success, JSON to stderr on error with exit code 1.
    Graph visualizations are generated by default; use --no-graph to disable.

    Examples:
        pipelex-agent run pipe pipelex-wip/pipeline_01/
        pipelex-agent run pipe my_bundle.mthds --pipe my_pipe
        pipelex-agent run pipe my_pipe --dry-run --mock-inputs
        pipelex-agent run pipe pipelex-wip/pipeline_01/ --no-graph
    """
    # Validate that at least one target is provided
    provided_options = sum([target is not None, pipe is not None, bundle is not None])
    if provided_options == 0:
        agent_error("No pipe code or bundle file specified", "ArgumentError")

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

    # Determine pipe_code and bundle_path from arguments
    pipe_code: str | None = None
    bundle_path: str | None = None
    auto_inputs_path: str | None = None

    if target:
        target_path = Path(target)
        if target_path.is_dir():
            # Directory mode: auto-detect bundle, inputs, and library dir
            if bundle:
                agent_error("Cannot use --bundle when passing a pipeline directory as target", "ArgumentError")

            # Find .mthds: try default name first, then fall back to single .mthds
            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 '{target}'", "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 '{target}' ({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]

            # Consume --pipe if provided
            if pipe:
                pipe_code = pipe
                pipe = None  # prevent double-assignment below

        elif is_pipelex_file(target_path):
            bundle_path = target
            if bundle:
                agent_error("Cannot use --bundle if already passing a bundle file as positional argument", "ArgumentError")
        else:
            pipe_code = target
            if pipe:
                agent_error("Cannot use --pipe if already passing a pipe code as positional argument", "ArgumentError")

    if bundle:
        bundle_path = bundle

    if pipe:
        pipe_code = pipe

    if not pipe_code and not bundle_path:
        agent_error("No pipe code or bundle file specified", "ArgumentError")

    # Check installed methods' exports for additional library dirs
    if pipe_code and not bundle_path:
        export_dirs = resolve_pipe_from_exports(pipe_code)
        if export_dirs:
            if library_dir is None:
                library_dir = export_dirs
            else:
                library_dir = [*export_dirs, *library_dir]

    # Load MTHDS content from bundle if provided
    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
    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_content=mthds_content,
                        inputs=pipeline_inputs,
                        with_memory=with_memory,
                    )
                )
                agent_success(result)

            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:
                agent_error(str(exc), type(exc).__name__, cause=exc)

        case RunnerType.PIPELEX:
            make_pipelex_for_agent_cli(log_level=ctx.obj["log_level"])

            try:
                result = asyncio.run(
                    run_pipeline_core(
                        pipe_code=pipe_code,  # type: ignore[arg-type]
                        mthds_content=mthds_content,
                        bundle_uri=bundle_path,
                        inputs=pipeline_inputs,
                        dry_run=dry_run,
                        mock_inputs=mock_inputs,
                        library_dirs=library_dir,
                        graph=graph,
                        with_memory=with_memory,
                    )
                )
                agent_success(result)

            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:
                agent_error(str(exc), type(exc).__name__, cause=exc)

            finally:
                Pipelex.teardown_if_needed()
