# SPDX-License-Identifier: MPL-2.0
# Copyright (c) 2025 Daniel Schmidt

"""CLI entry point for nac-test."""

import logging
from datetime import datetime
from pathlib import Path
from typing import Annotated

import typer

import nac_test
from nac_test.cli.diagnostic import diagnostic_callback
from nac_test.cli.ui import display_aci_defaults_banner
from nac_test.cli.validators import validate_aci_defaults
from nac_test.combined_orchestrator import CombinedOrchestrator
from nac_test.core.constants import (
    CONSOLE_TIME_FORMAT,
    DEBUG_MODE,
    EXIT_ERROR,
    EXIT_INTERRUPTED,
    EXIT_INVALID_ARGS,
)
from nac_test.data_merger import DataMerger
from nac_test.utils.formatting import format_duration
from nac_test.utils.logging import (
    DEFAULT_LOGLEVEL,
    LogLevel,
    configure_logging,
)
from nac_test.utils.platform import check_and_exit_if_unsupported_macos_python

# Pretty exceptions are verbose but helpful for debugging.
# Enable them when NAC_TEST_DEBUG=true, disable for cleaner output otherwise.
app = typer.Typer(add_completion=False, pretty_exceptions_enable=DEBUG_MODE)

logger = logging.getLogger(__name__)


def version_callback(value: bool) -> None:
    """Print version and exit when --version is passed."""
    if value:
        typer.echo(f"nac-test, version {nac_test.__version__}")
        raise typer.Exit()


# Named "LoglevelOption" (not "Loglevel") to avoid confusion with the LogLevel enum type
LoglevelOption = Annotated[
    LogLevel | None,
    typer.Option(
        "--loglevel",
        "-l",
        help=f"Log level. Default: {DEFAULT_LOGLEVEL.value} (or DEBUG if --verbose is set).",
        envvar="NAC_TEST_LOGLEVEL",
        is_eager=True,
    ),
]

DeprecatedVerbosity = Annotated[
    LogLevel | None,
    typer.Option(
        "--verbosity",
        "-v",
        hidden=True,
        is_eager=True,
    ),
]


Data = Annotated[
    list[Path],
    typer.Option(
        "-d",
        "--data",
        exists=True,
        dir_okay=True,
        file_okay=True,
        help="Path to data YAML files.",
        envvar="NAC_TEST_DATA",
    ),
]


Templates = Annotated[
    Path,
    typer.Option(
        "-t",
        "--templates",
        exists=True,
        dir_okay=True,
        file_okay=False,
        help="Path to test templates.",
        envvar="NAC_TEST_TEMPLATES",
    ),
]


Filters = Annotated[
    Path | None,
    typer.Option(
        "-f",
        "--filters",
        exists=True,
        dir_okay=True,
        file_okay=False,
        help="Path to Jinja filters.",
        envvar="NAC_TEST_FILTERS",
    ),
]


Tests = Annotated[
    Path | None,
    typer.Option(
        "--tests",
        exists=True,
        dir_okay=True,
        file_okay=False,
        help="Path to Jinja tests.",
        envvar="NAC_TEST_TESTS",
    ),
]


Output = Annotated[
    Path,
    typer.Option(
        "-o",
        "--output",
        exists=False,
        dir_okay=True,
        file_okay=False,
        help="Path to output directory.",
        envvar="NAC_TEST_OUTPUT",
    ),
]


Include = Annotated[
    list[str] | None,
    typer.Option(
        "-i",
        "--include",
        help="Selects the test cases by tag (include).",
        envvar="NAC_TEST_INCLUDE",
    ),
]


Exclude = Annotated[
    list[str] | None,
    typer.Option(
        "-e",
        "--exclude",
        help="Selects the test cases by tag (exclude).",
        envvar="NAC_TEST_EXCLUDE",
    ),
]


MergedDataFilename = Annotated[
    str,
    typer.Option(
        "-m",
        "--merged-data-filename",
        help="Filename for the merged data model YAML file.",
    ),
]


RenderOnly = Annotated[
    bool,
    typer.Option(
        "--render-only",
        help="Only render tests without executing them.",
        envvar="NAC_TEST_RENDER_ONLY",
    ),
]


DryRun = Annotated[
    bool,
    typer.Option(
        "--dry-run",
        help="Dry run mode: validates test structure without execution.",
        envvar="NAC_TEST_DRY_RUN",
    ),
]


Processes = Annotated[
    int | None,
    typer.Option(
        "--processes",
        help="Number of parallel processes for test execution (pabot --processes option), default is max(2, cpu count).",
        envvar="NAC_TEST_PROCESSES",
    ),
]


PyATS = Annotated[
    bool,
    typer.Option(
        "--pyats",
        help="[DEV ONLY] Run only PyATS tests (skips Robot Framework). Use for faster development cycles.",
        envvar="NAC_TEST_PYATS",
    ),
]


Robot = Annotated[
    bool,
    typer.Option(
        "--robot",
        help="[DEV ONLY] Run only Robot Framework tests (skips PyATS). Use for faster development cycles.",
        envvar="NAC_TEST_ROBOT",
    ),
]


MaxParallelDevices = Annotated[
    int,
    typer.Option(
        "--max-parallel-devices",
        help="Maximum number of devices to test in parallel for SSH/D2D tests. If not specified, automatically calculated based on system resources. Use this to set a lower limit if needed.",
        envvar="NAC_TEST_MAX_PARALLEL_DEVICES",
        min=1,
        max=500,
    ),
]


MinimalReports = Annotated[
    bool,
    typer.Option(
        "--minimal-reports",
        help="Only include detailed command outputs for failed/errored tests in HTML reports (80-95%% artifact size reduction).",
        envvar="NAC_TEST_MINIMAL_REPORTS",
    ),
]


Version = Annotated[
    bool,
    typer.Option(
        "--version",
        callback=version_callback,
        help="Display version number.",
        is_eager=True,
    ),
]


Diagnostic = Annotated[
    bool,
    typer.Option(
        "--diagnostic",
        callback=diagnostic_callback,
        is_eager=True,
        help="Wrap execution with diagnostic collection. Produces a zip with system info, logs, and artifacts.",
    ),
]


Verbose = Annotated[
    bool,
    typer.Option(
        "--verbose",
        help="Enable verbose mode: enables verbose output for nac-test, Robot and PyATS execution.",
        envvar="NAC_TEST_VERBOSE",
    ),
]


Testbed = Annotated[
    Path | None,
    typer.Option(
        "--testbed",
        exists=True,
        dir_okay=False,
        file_okay=True,
        help="Path to custom PyATS testbed YAML. Devices in this file will override auto-discovered device connections and can include additional helper devices (e.g., jump hosts).",
        envvar="NAC_TEST_TESTBED",
    ),
]


@app.command(
    context_settings={"ignore_unknown_options": True, "allow_extra_args": True}
)
def main(
    ctx: typer.Context,
    data: Data,
    templates: Templates,
    output: Output,
    filters: Filters = None,
    tests: Tests = None,
    include: Include = None,
    exclude: Exclude = None,
    render_only: RenderOnly = False,
    dry_run: DryRun = False,
    processes: Processes = None,
    pyats: PyATS = False,
    robot: Robot = False,
    max_parallel_devices: MaxParallelDevices | None = None,
    minimal_reports: MinimalReports = False,
    testbed: Testbed = None,
    loglevel: LoglevelOption = None,
    verbosity: DeprecatedVerbosity = None,
    version: Version = False,  # noqa: ARG001
    diagnostic: Diagnostic = False,  # noqa: ARG001
    verbose: Verbose = False,
    merged_data_filename: MergedDataFilename = "merged_data_model_test_variables.yaml",
) -> None:
    """A CLI tool to render and execute Robot Framework and PyATS tests using Jinja templating.

    Additional Robot Framework options can be passed at the end of the command to
    further control test execution (e.g., --variable, --listener, --loglevel).
    These are appended to the pabot invocation. Pabot-specific options and test
    files/directories are not supported and will result in an error.
    """

    # Handle deprecated --verbosity option
    if verbosity is not None:
        typer.echo(
            typer.style(
                "Warning: --verbosity is deprecated, use --loglevel instead.",
                fg=typer.colors.YELLOW,
            ),
            err=True,
        )
        if loglevel is None:
            loglevel = verbosity

    # Resolve loglevel: explicit > verbose-implied > default
    if loglevel is not None:
        effective_loglevel = loglevel
    elif verbose:
        effective_loglevel = LogLevel.DEBUG
    else:
        effective_loglevel = DEFAULT_LOGLEVEL
    configure_logging(effective_loglevel)

    check_and_exit_if_unsupported_macos_python()

    # Validate development flag combinations
    if pyats and robot:
        typer.echo(
            typer.style(
                "Error: Cannot use both --pyats and --robot flags simultaneously.",
                fg=typer.colors.RED,
            )
        )
        typer.echo(
            "Use one development flag at a time, or neither for combined execution."
        )
        raise typer.Exit(EXIT_INVALID_ARGS)

    # Create output directory and shared merged data file (SOT)
    output.mkdir(parents=True, exist_ok=True)

    # Validate ACI defaults before expensive merge operation
    # This catches the common mistake of forgetting -d ./defaults/
    if not validate_aci_defaults(data):
        typer.echo("")
        display_aci_defaults_banner()
        typer.echo("")
        raise typer.Exit(1)

    # Merge data files with timing
    start_time = datetime.now()
    start_timestamp = start_time.strftime(CONSOLE_TIME_FORMAT)
    typer.echo(f"\n\n[{start_timestamp}] 📄 Merging data model files...")

    merged_data = DataMerger.merge_data_files(data)
    DataMerger.write_merged_data_model(merged_data, output, merged_data_filename)

    end_time = datetime.now()
    end_timestamp = end_time.strftime(CONSOLE_TIME_FORMAT)
    duration = (end_time - start_time).total_seconds()
    typer.echo(
        f"[{end_timestamp}] ✅ Data model merging completed ({format_duration(duration)})"
    )

    # CombinedOrchestrator - handles both dev and production modes (uses pre-created merged data)
    orchestrator = CombinedOrchestrator(
        data_paths=data,
        templates_dir=templates,
        custom_testbed_path=testbed,
        output_dir=output,
        merged_data_filename=merged_data_filename,
        filters_path=filters,
        tests_path=tests,
        include_tags=include,
        exclude_tags=exclude,
        render_only=render_only,
        dry_run=dry_run,
        processes=processes,
        extra_args=ctx.args,
        max_parallel_devices=max_parallel_devices,
        minimal_reports=minimal_reports,
        loglevel=effective_loglevel,
        dev_pyats_only=pyats,
        dev_robot_only=robot,
        verbose=verbose,
    )

    # Track total runtime for benchmarking
    runtime_start = datetime.now()

    try:
        stats = orchestrator.run_tests()
    except KeyboardInterrupt:
        # Handle Ctrl+C interruption gracefully
        typer.echo(
            typer.style(
                "\n⚠️  Test execution was interrupted by user (Ctrl+C)",
                fg=typer.colors.YELLOW,
            )
        )
        # Exit with code 253 following Robot Framework convention
        raise typer.Exit(EXIT_INTERRUPTED) from None
    except Exception as e:
        # Infrastructure errors (template rendering, controller detection, etc.)
        typer.echo(f"Error during execution: {e}")
        # Progressive disclosure: clean output for customers, full context for developers
        if DEBUG_MODE:
            raise typer.Exit(EXIT_ERROR) from e  # Developer: full exception context
        raise typer.Exit(EXIT_ERROR) from None  # Customer: clean output

    # Display total runtime before exit
    runtime_end = datetime.now()
    total_runtime = (runtime_end - runtime_start).total_seconds()

    typer.echo(f"\nTotal runtime: {format_duration(total_runtime)}")

    if render_only:
        if stats.has_errors:
            reason = stats.robot.reason if stats.robot and stats.robot.reason else None
            if reason:
                typer.echo(f"\n❌ Template rendering failed: {reason}", err=True)
            else:
                typer.echo("\n❌ Template rendering failed", err=True)
            raise typer.Exit(stats.exit_code)
        typer.echo("\n✅ Templates rendered successfully (render-only mode)")
        raise typer.Exit(0)

    if stats.pre_flight_failure is not None:
        raise typer.Exit(stats.exit_code)

    if stats.has_errors:
        error_list = "; ".join(stats.errors)
        typer.echo(f"\n❌ Execution errors: {error_list}", err=True)
        raise typer.Exit(stats.exit_code)

    if stats.has_failures:
        typer.echo(
            f"\n❌ Tests failed: {stats.failed} out of {stats.total} tests", err=True
        )
        raise typer.Exit(stats.exit_code)

    if stats.is_empty:
        typer.echo("\n⚠️  No tests were executed", err=True)
        raise typer.Exit(stats.exit_code)

    typer.echo(f"\n✅ All tests passed: {stats.passed} out of {stats.total} tests")
    raise typer.Exit(0)
