"""Pipeline CLI command group for Oscura.

Provides command-line interface for pipeline operations including execution,
validation, and handler inspection.

Example:
    $ oscura pipeline run analysis.yaml
    $ oscura pipeline run analysis.yaml --var input_file=trace.wfm --var baud_rate=115200
    $ oscura pipeline validate analysis.yaml
    $ oscura pipeline handlers
    $ oscura pipeline handler-info decoder.uart
"""

from __future__ import annotations

import json
import logging
import sys
from typing import TYPE_CHECKING, Any

import click

from oscura.pipeline import (
    Pipeline,
    PipelineExecutionError,
    PipelineValidationError,
    register_all_handlers,
)
from oscura.pipeline.handlers import get_all_handlers, get_handler, list_handler_types

if TYPE_CHECKING:
    from collections.abc import Callable

logger = logging.getLogger("oscura.cli.pipeline")


def _parse_variables(var_tuples: tuple[str, ...]) -> dict[str, Any]:
    """Parse --var key=value arguments into dictionary.

    Args:
        var_tuples: Tuple of "key=value" strings

    Returns:
        Dictionary of parsed variables

    Raises:
        click.BadParameter: If variable format is invalid

    Example:
        >>> _parse_variables(("input_file=trace.wfm", "baud_rate=115200"))
        {"input_file": "trace.wfm", "baud_rate": 115200}
    """
    variables: dict[str, Any] = {}
    for var_str in var_tuples:
        if "=" not in var_str:
            raise click.BadParameter(f"Invalid variable format: '{var_str}'. Expected 'key=value'.")

        key, value = var_str.split("=", 1)
        key = key.strip()
        value = value.strip()

        # Try to parse as int, float, bool, or keep as string
        parsed_value: Any
        if value.lower() in ("true", "yes", "on"):
            parsed_value = True
        elif value.lower() in ("false", "no", "off"):
            parsed_value = False
        elif value.isdigit():
            parsed_value = int(value)
        else:
            try:
                parsed_value = float(value)
            except ValueError:
                parsed_value = value

        variables[key] = parsed_value

    return variables


def _create_progress_callback(verbose: bool) -> Callable[[str, int], None]:
    """Create progress callback for pipeline execution.

    Args:
        verbose: Whether to show detailed progress

    Returns:
        Progress callback function
    """

    def progress_callback(step_name: str, percent: int) -> None:
        """Report pipeline execution progress.

        Args:
            step_name: Name of current step
            percent: Completion percentage (0-100)
        """
        if verbose:
            click.echo(f"[{percent:3d}%] Executing step: {step_name}")

    return progress_callback


def _format_handler_info(handler_type: str, handler_func: Callable[..., Any]) -> dict[str, Any]:
    """Extract handler information from function.

    Args:
        handler_type: Handler type identifier (e.g., "input.file")
        handler_func: Handler function

    Returns:
        Dictionary with handler metadata
    """
    # Parse docstring to extract metadata
    docstring = handler_func.__doc__ or ""
    lines = docstring.strip().split("\n")

    # Extract description (first line or paragraph)
    description = ""
    params_section = []
    outputs_section = []
    current_section = None

    for line in lines:
        line = line.strip()
        if line.lower().startswith("parameters:"):
            current_section = "params"
            continue
        elif line.lower().startswith("outputs:"):
            current_section = "outputs"
            continue
        elif line.lower().startswith("inputs:"):
            current_section = "inputs"
            continue

        if current_section == "params" and line:
            params_section.append(line)
        elif current_section == "outputs" and line:
            outputs_section.append(line)
        elif not description and line and current_section is None:
            description = line

    # Determine category from handler type
    category = handler_type.split(".")[0] if "." in handler_type else "unknown"

    result: dict[str, Any] = {
        "type": handler_type,
        "category": category,
        "description": description,
        "parameters": params_section,
        "outputs": outputs_section,
    }
    return result


@click.group()
def pipeline() -> None:
    """Pipeline operations for declarative workflows.

    Execute, validate, and inspect YAML-based analysis pipelines with
    60+ built-in handlers for common operations.

    Example:
        oscura pipeline run analysis.yaml
        oscura pipeline validate analysis.yaml
        oscura pipeline handlers
    """


@pipeline.command()
@click.argument("yaml_file", type=click.Path(exists=True))
@click.option(
    "--var",
    multiple=True,
    help="Set template variable (format: key=value). Can be used multiple times.",
)
@click.option(
    "--dry-run",
    is_flag=True,
    help="Validate pipeline without executing.",
)
@click.option(
    "--json",
    "json_output",
    is_flag=True,
    help="Output results as JSON.",
)
@click.option(
    "--verbose",
    "-v",
    is_flag=True,
    help="Enable verbose logging and progress reporting.",
)
@click.pass_context
def run(
    ctx: click.Context,
    yaml_file: str,
    var: tuple[str, ...],
    dry_run: bool,
    json_output: bool,
    verbose: bool,
) -> None:
    """Execute a pipeline from YAML file.

    Loads and executes the specified pipeline with optional template variables.
    Displays progress during execution and reports results.

    Args:
        ctx: Click context object.
        yaml_file: Path to pipeline YAML file.
        var: Template variables (key=value pairs).
        dry_run: If True, validate only without executing.
        json_output: If True, output results as JSON.
        verbose: If True, enable detailed progress reporting.

    Example:
        oscura pipeline run analysis.yaml
        oscura pipeline run analysis.yaml --var input_file=trace.wfm --var baud_rate=115200
        oscura pipeline run analysis.yaml --dry-run
        oscura pipeline run analysis.yaml --json
    """
    # Set logging level
    if verbose:
        logging.basicConfig(level=logging.INFO)
        logger.setLevel(logging.INFO)

    # Parse variables
    variables = _parse_variables(var)

    try:
        # Load pipeline
        if verbose:
            click.echo(f"Loading pipeline: {yaml_file}")
        pipeline_obj = Pipeline.load(yaml_file, variables=variables)

        # Register all handlers
        register_all_handlers(pipeline_obj)

        # Dry-run mode: validate only
        if dry_run:
            if verbose:
                click.echo("Validating pipeline (dry-run mode)...")
            # Validation happens during load
            if json_output:
                result = {
                    "status": "valid",
                    "pipeline": pipeline_obj.definition.name,
                    "steps": len(pipeline_obj.definition.steps),
                }
                click.echo(json.dumps(result, indent=2))
            else:
                click.echo(f"Pipeline '{pipeline_obj.definition.name}' is valid.")
                click.echo(f"Steps: {len(pipeline_obj.definition.steps)}")
            return

        # Execute pipeline
        if verbose:
            click.echo(f"Executing pipeline: {pipeline_obj.definition.name}")
            pipeline_obj.on_progress(_create_progress_callback(verbose))

        results = pipeline_obj.execute()

        # Display results
        if json_output:
            output = {
                "status": "success" if results.success else "error",
                "pipeline": results.pipeline_name,
                "steps_executed": len(results.step_results),
                "outputs": {
                    key: str(value)[:100]
                    if not isinstance(value, (int, float, bool, str))
                    else value
                    for key, value in results.outputs.items()
                },
            }
            if results.error:
                output["error"] = results.error
            click.echo(json.dumps(output, indent=2))
        else:
            click.echo(f"\nPipeline '{results.pipeline_name}' completed successfully.")
            click.echo(f"Steps executed: {len(results.step_results)}")
            if results.outputs:
                click.echo("\nOutputs:")
                for key, value in results.outputs.items():
                    value_str = str(value)
                    if len(value_str) > 80:
                        value_str = value_str[:77] + "..."
                    click.echo(f"  {key}: {value_str}")

    except PipelineValidationError as e:
        if json_output:
            error: dict[str, Any] = {
                "status": "validation_error",
                "message": str(e),
            }
            if e.step_name is not None:
                error["step"] = e.step_name
            if e.suggestion is not None:
                error["suggestion"] = e.suggestion
            click.echo(json.dumps(error, indent=2))
        else:
            click.echo(f"Validation error: {e}", err=True)
            if e.step_name:
                click.echo(f"  Step: {e.step_name}", err=True)
            if e.suggestion:
                click.echo(f"  Suggestion: {e.suggestion}", err=True)
        sys.exit(1)

    except PipelineExecutionError as e:
        if json_output:
            error = {
                "status": "execution_error",
                "message": str(e),
                "step": e.step_name,
            }
            if e.traceback_str:
                error["traceback"] = e.traceback_str
            click.echo(json.dumps(error, indent=2))
        else:
            click.echo(f"Execution error: {e}", err=True)
            if e.step_name:
                click.echo(f"  Step: {e.step_name}", err=True)
            if e.traceback_str and verbose:
                click.echo(f"\nTraceback:\n{e.traceback_str}", err=True)
        sys.exit(1)

    except FileNotFoundError as e:
        if json_output:
            error = {"status": "file_not_found", "message": str(e)}
            click.echo(json.dumps(error, indent=2))
        else:
            click.echo(f"File not found: {e}", err=True)
        sys.exit(1)

    except Exception as e:
        if json_output:
            error = {"status": "error", "message": str(e), "type": type(e).__name__}
            click.echo(json.dumps(error, indent=2))
        else:
            click.echo(f"Unexpected error: {e}", err=True)
            if verbose:
                import traceback

                click.echo(traceback.format_exc(), err=True)
        sys.exit(1)


@pipeline.command()
@click.argument("yaml_file", type=click.Path(exists=True))
@click.option(
    "--var",
    multiple=True,
    help="Set template variable (format: key=value). Can be used multiple times.",
)
@click.option(
    "--json",
    "json_output",
    is_flag=True,
    help="Output results as JSON.",
)
def validate(
    yaml_file: str,
    var: tuple[str, ...],
    json_output: bool,
) -> None:
    """Validate pipeline without executing (dry-run).

    Checks pipeline syntax, step dependencies, and handler availability
    without executing any steps.

    Args:
        yaml_file: Path to pipeline YAML file.
        var: Template variables (key=value pairs).
        json_output: If True, output results as JSON.

    Example:
        oscura pipeline validate analysis.yaml
        oscura pipeline validate analysis.yaml --var input_file=trace.wfm
        oscura pipeline validate analysis.yaml --json
    """
    # Parse variables
    variables = _parse_variables(var)

    try:
        # Load pipeline (validation happens during load)
        pipeline_obj = Pipeline.load(yaml_file, variables=variables)

        # Register handlers to check availability
        register_all_handlers(pipeline_obj)

        # Check all steps have handlers
        missing_handlers = []
        for step in pipeline_obj.definition.steps:
            if get_handler(step.type) is None:
                missing_handlers.append(step.type)

        if missing_handlers:
            if json_output:
                error_data: dict[str, Any] = {
                    "status": "validation_error",
                    "message": f"Missing handlers for step types: {', '.join(missing_handlers)}",
                    "missing_handlers": missing_handlers,
                }
                click.echo(json.dumps(error_data, indent=2))
            else:
                click.echo("Validation error: Missing handlers", err=True)
                click.echo(f"  Missing: {', '.join(missing_handlers)}", err=True)
                click.echo("\nRun 'oscura pipeline handlers' to see available handlers.", err=True)
            sys.exit(1)

        # Success
        if json_output:
            result = {
                "status": "valid",
                "pipeline": pipeline_obj.definition.name,
                "version": pipeline_obj.definition.version,
                "steps": len(pipeline_obj.definition.steps),
                "description": pipeline_obj.definition.description,
            }
            click.echo(json.dumps(result, indent=2))
        else:
            click.echo(f"Pipeline '{pipeline_obj.definition.name}' is valid.")
            click.echo(f"  Version: {pipeline_obj.definition.version}")
            click.echo(f"  Steps: {len(pipeline_obj.definition.steps)}")
            if pipeline_obj.definition.description:
                click.echo(f"  Description: {pipeline_obj.definition.description}")

    except PipelineValidationError as e:
        if json_output:
            error_info: dict[str, Any] = {
                "status": "validation_error",
                "message": str(e),
            }
            if e.step_name is not None:
                error_info["step"] = e.step_name
            if e.suggestion is not None:
                error_info["suggestion"] = e.suggestion
            click.echo(json.dumps(error_info, indent=2))
        else:
            click.echo(f"Validation error: {e}", err=True)
            if e.step_name:
                click.echo(f"  Step: {e.step_name}", err=True)
            if e.suggestion:
                click.echo(f"  Suggestion: {e.suggestion}", err=True)
        sys.exit(1)

    except FileNotFoundError as e:
        if json_output:
            error_msg: dict[str, Any] = {"status": "file_not_found", "message": str(e)}
            click.echo(json.dumps(error_msg, indent=2))
        else:
            click.echo(f"File not found: {e}", err=True)
        sys.exit(1)

    except Exception as e:
        if json_output:
            error_obj: dict[str, Any] = {
                "status": "error",
                "message": str(e),
                "type": type(e).__name__,
            }
            click.echo(json.dumps(error_obj, indent=2))
        else:
            click.echo(f"Unexpected error: {e}", err=True)
        sys.exit(1)


@pipeline.command()
@click.option(
    "--json",
    "json_output",
    is_flag=True,
    help="Output results as JSON.",
)
def handlers(json_output: bool) -> None:
    """List all registered pipeline handlers.

    Displays all available handler types organized by category
    (input, decoder, analysis, filter, transform, output).

    Args:
        json_output: If True, output results as JSON.

    Example:
        oscura pipeline handlers
        oscura pipeline handlers --json
    """
    # Get all handlers
    all_handlers = get_all_handlers()

    if not all_handlers:
        if json_output:
            click.echo(json.dumps({"handlers": [], "total": 0}))
        else:
            click.echo("No handlers registered.")
        return

    # Group by category
    categories: dict[str, list[str]] = {}
    for handler_type in sorted(all_handlers.keys()):
        category = handler_type.split(".")[0] if "." in handler_type else "other"
        if category not in categories:
            categories[category] = []
        categories[category].append(handler_type)

    # Output results
    if json_output:
        result = {
            "handlers": sorted(all_handlers.keys()),
            "categories": categories,
            "total": len(all_handlers),
            "by_category": {cat: len(handlers) for cat, handlers in categories.items()},
        }
        click.echo(json.dumps(result, indent=2))
    else:
        click.echo(f"Registered Pipeline Handlers ({len(all_handlers)} total):\n")
        for category in sorted(categories.keys()):
            handlers = categories[category]
            click.echo(f"{category.upper()} ({len(handlers)}):")
            for handler_type in handlers:
                click.echo(f"  - {handler_type}")
            click.echo()


@pipeline.command()
@click.argument("handler_type")
@click.option(
    "--json",
    "json_output",
    is_flag=True,
    help="Output results as JSON.",
)
def handler_info(handler_type: str, json_output: bool) -> None:
    """Show detailed information about a handler.

    Displays handler documentation, parameters, inputs, and outputs.

    Args:
        handler_type: Handler type identifier (e.g., "decoder.uart").
        json_output: If True, output results as JSON.

    Example:
        oscura pipeline handler-info decoder.uart
        oscura pipeline handler-info input.file --json
    """
    handler = get_handler(handler_type)

    if handler is None:
        if json_output:
            error = {
                "status": "not_found",
                "message": f"Handler '{handler_type}' not found",
                "available": list_handler_types(),
            }
            click.echo(json.dumps(error, indent=2))
        else:
            click.echo(f"Handler '{handler_type}' not found.", err=True)
            click.echo("\nAvailable handlers:", err=True)
            for h in list_handler_types()[:5]:
                click.echo(f"  - {h}", err=True)
            if len(list_handler_types()) > 5:
                click.echo(f"  ... and {len(list_handler_types()) - 5} more", err=True)
            click.echo("\nRun 'oscura pipeline handlers' for full list.", err=True)
        sys.exit(1)

    # Extract handler information
    info = _format_handler_info(handler_type, handler)

    if json_output:
        click.echo(json.dumps(info, indent=2))
    else:
        click.echo(f"Handler: {info['type']}")
        click.echo(f"Category: {info['category']}")
        if info["description"]:
            click.echo(f"\nDescription:\n  {info['description']}")

        if info["parameters"]:
            click.echo("\nParameters:")
            for param in info["parameters"]:
                click.echo(f"  {param}")

        if info["outputs"]:
            click.echo("\nOutputs:")
            for output in info["outputs"]:
                click.echo(f"  {output}")
