import sys
import argparse
import importlib
import pkgutil
from hil_testbench.config.task_config import TaskConfig
from hil_testbench.config.config_loader import load_project_config


def main():
    parser = argparse.ArgumentParser(
        description="TaskRunner CLI - Unified Task Execution",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "Examples:\n"
            '  python run_tasks.py iperf --duration 60 --interval 5 --links \'[{"server_host": "server1", "client_host": "client1"}]\'\n'
            "  python run_tasks.py iperf --viz --max-log-size-main 20 --max-log-size-task 10\n"
        ),
    )
    # Task will be specified as a sub-command (e.g., iperf); use --list to show available
    parser.add_argument("--duration", type=str, help="Test duration (seconds)")
    parser.add_argument(
        "--interval", type=str, help="Interval between samples (seconds)"
    )
    # Default visualization on when running in an interactive terminal
    parser.add_argument(
        "--viz",
        action="store_true",
        default=sys.stdout.isatty(),
        help="Enable live metrics visualization (defaults on for interactive terminals)",
    )
    parser.add_argument(
        "--max-log-size-main",
        type=int,
        default=10,
        help="Main log rotation size in MB (rotates to .1, .2, etc when hit)",
    )
    parser.add_argument(
        "--max-log-size-task",
        type=int,
        default=5,
        help="Task log rotation size in MB (rotates to .1, .2, etc when hit)",
    )
    parser.add_argument(
        "--max-log-file-count-main",
        type=int,
        default=10,
        help="Number of rotated main log files to keep (default: 10)",
    )
    parser.add_argument(
        "--max-log-file-count-task",
        type=int,
        default=10,
        help="Number of rotated task log files to keep (default: 10)",
    )
    # Question: Does this apply to parsers/sinks/writers or what?
    parser.add_argument(
        "--max-data-size",
        type=int,
        default=None,
        help="Maximum data size per-task (MB). If omitted, YAML defaults or program default will be used",
    )
    parser.add_argument(
        "--disable-health",
        action="store_true",
        help="Disable runner host health monitoring (local machine)",
    )
    parser.add_argument(
        "--health-interval", type=int, default=600, help="Health monitor interval (sec)"
    )
    parser.add_argument("--config", "-c", help="YAML config file for advanced options")
    parser.add_argument(
        "--describe",
        action="store_true",
        help="Describe available CLI and YAML options without running a task",
    )
    parser.add_argument(
        "--describe-format",
        choices=["text", "json"],
        default="text",
        help="Output format for --describe",
    )

    # Discover available tasks in the `tasks` package
    def discover_tasks():
        return [m.name for m in pkgutil.iter_modules(["tasks"]) if m.name != "__init__"]

    # Expose a quick list of available tasks
    parser.add_argument("--list", action="store_true", help="List available tasks")

    # Build subparsers for each known task to enable per-task help
    # Don't require a subparser; allow `--list` to work standalone
    subparsers = parser.add_subparsers(dest="task", required=False)
    task_names = discover_tasks()
    task_parsers = {}

    def resolve_task_class(module):
        # Prefer explicit 'Task' symbol only if defined in this module
        if hasattr(module, "Task"):
            t = getattr(module, "Task")
            if (
                isinstance(t, type)
                and getattr(t, "__module__", None) == module.__name__
            ):
                return t
        # Fallback: find any class ending with 'Task' defined in this module
        for attr_name in dir(module):
            attr = getattr(module, attr_name)
            if (
                isinstance(attr, type)
                and attr_name.endswith("Task")
                and getattr(attr, "__module__", None) == module.__name__
            ):
                return attr
        # As a last resort, return a class ending with 'Task' even if imported, but not the abstract base
        for attr_name in dir(module):
            attr = getattr(module, attr_name)
            if (
                isinstance(attr, type)
                and attr_name.endswith("Task")
                and attr_name != "Task"
            ):
                return attr
        raise AttributeError("No Task class found in module")

    for tname in task_names:
        try:
            tmod = importlib.import_module(f"tasks.{tname}")
            tcls = resolve_task_class(tmod)
        except (ModuleNotFoundError, AttributeError, ImportError):
            # Skip tasks that fail to import during discovery
            continue
        sp = subparsers.add_parser(tname, help=(tmod.__doc__ or tname))
        # Support both new and legacy CLI arg hooks
        if hasattr(tcls, "cli_args"):
            tcls.cli_args(sp)
        task_parsers[tname] = sp

    # Parse args which now include subparser (task) options
    args = parser.parse_args()

    if args.describe:
        describe_options(parser, task_parsers, args.describe_format)
        sys.exit(0)

    # If no subparser and not listing tasks, show help and exit
    if not args.list and not getattr(args, "task", None):
        parser.print_help()
        sys.exit(1)
    # Load YAML and merge with CLI args to create ready-to-use RunConfig
    yaml_data, run_config, yaml_tasks = load_project_config(
        args.config or "tasks.yaml", cli_args=args
    )

    # Extract YAML defaults for task configuration
    top_defaults = yaml_data.get("defaults", {})
    yaml_task_cfg = yaml_tasks.get(args.task, {})
    if args.list:
        print("Available tasks and flags:")
        for n in sorted(task_names):
            try:
                tmod = importlib.import_module(f"tasks.{n}")
            except (ImportError, ModuleNotFoundError, AttributeError):
                # If task cannot be imported, fall back to showing name only
                print(f"- {n}: (failed to import to load description)")
                continue
            # short description — use module docstring first line
            short = (tmod.__doc__ or "").splitlines()[0].strip()
            print(f"- {n}: {short}")
            sp = task_parsers.get(n)
            if sp:
                # iterate parser actions and show option strings
                help_text = sp.format_help()
                for line in help_text.splitlines():
                    stripped = line.strip()
                    if stripped.startswith("-"):
                        print(f"    {stripped}")
            else:
                print("    (no flags)")
        sys.exit(0)

    # Load the task class
    try:
        task_module = importlib.import_module(f"tasks.{args.task}")
        task_cls = resolve_task_class(task_module)
    except (ModuleNotFoundError, AttributeError):
        print(f"Error: Could not find task '{args.task}' in tasks folder.")
        sys.exit(1)

    # If the task declares CLI args, parse them so we can use typed values
    # Now parse the task-specific options — they are already in `args` due to subparsers
    task_ns = args

    # Build task config from args (RunConfig already has CLI + YAML merged)
    config = TaskConfig.from_args(
        args=args,
        top_defaults=top_defaults,
        task_defaults=yaml_task_cfg,
        run_config=run_config,
        task_ns=task_ns,
    )

    # Instantiate the task class and hand off to the framework executor
    task = task_cls()

    # Use framework executor (task never sees TaskRunner internals)
    from hil_testbench.run.task_runner import (  # hil: allow-lazy - avoid circular import
        TaskRunner,
    )

    executor = TaskRunner()
    # Print a concise startup message; avoid task-specific callout when viz is enabled
    try:
        from hil_testbench.run.task_logger import (  # hil: allow-lazy - avoid circular import
            TaskLogger as _TL,
        )

        _tl = _TL()
        if getattr(config.run_config, "enable_viz", False) or getattr(
            config, "enable_viz", False
        ):
            _tl.console_print("Starting...", level="INFO")
        else:
            _tl.console_print(f"🚀 Running task: {args.task}")
    except Exception:
        # Fallback to plain print if logger isn't available yet
        if getattr(config, "enable_viz", False):
            print("Starting...")
        else:
            print(f"🚀 Running task: {args.task}")
    executor.execute(task, config)


def describe_options(parser: argparse.ArgumentParser, task_parsers, fmt: str) -> None:
    """Print a machine-readable description of CLI + YAML config options.

    - CLI options are taken directly from argparse actions to stay in sync.
    - YAML keys are inferred from dataclasses and task code (static hints).
    """

    def action_info(action: argparse.Action) -> dict:
        flags = action.option_strings or [action.dest]
        default = None if action.default is argparse.SUPPRESS else action.default
        return {
            "flags": flags,
            "dest": action.dest,
            "help": action.help,
            "default": default,
            "type": getattr(action.type, "__name__", None) if action.type else None,
            "choices": action.choices if hasattr(action, "choices") else None,
        }

    def collect_actions(ap: argparse.ArgumentParser):
        return [
            action_info(a)
            for a in ap._actions  # pylint: disable=protected-access
            if a.dest not in ("help", argparse.SUPPRESS)
        ]

    from tools.yaml_options_report import (  # hil: allow-lazy - conditional CLI feature
        build_report,
    )

    report = build_report()
    data = {
        "program": {
            "cli_options": collect_actions(parser),
            "yaml_keys": {
                "root_sections": report.get("root_sections", []),
                "defaults": report.get("task_defaults_keys", []),
                "run_config_keys": report.get("run_config_yaml_keys", []),
                "reserved_task_keys": report.get("task_reserved_keys", []),
                "cli_override_keys": report.get("cli_override_keys", []),
            },
        },
        "tasks": {},
    }

    for name, ap in task_parsers.items():
        task_section = {
            "cli_options": collect_actions(ap),
            "yaml_keys": report.get("tasks", {}).get(name, {}),
        }
        data["tasks"][name] = task_section

    if fmt == "json":
        print(json.dumps(data, indent=2))
    else:
        # Text summary
        print("Program CLI options:")
        for opt in data["program"]["cli_options"]:
            flags = ", ".join(opt["flags"])
            default = (
                f" (default: {opt['default']})"
                if opt["default"] not in (None, argparse.SUPPRESS)
                else ""
            )
            print(f"  {flags}{default} - {opt['help'] or ''}")
        print("\nYAML keys (program-level):")
        print("  defaults:", ", ".join(data["program"]["yaml_keys"]["defaults"]))
        print(
            "  run_config:", ", ".join(data["program"]["yaml_keys"]["run_config_keys"])
        )
        print(
            "  reserved_task_keys:",
            ", ".join(data["program"]["yaml_keys"]["reserved_task_keys"]),
        )
        print(
            "  cli_override_keys:",
            ", ".join(data["program"]["yaml_keys"]["cli_override_keys"]),
        )
        for tname, tdata in data["tasks"].items():
            print(f"\nTask: {tname}")
            if tdata["cli_options"]:
                print("  CLI options:")
                for opt in tdata["cli_options"]:
                    flags = ", ".join(opt["flags"])
                    default = (
                        f" (default: {opt['default']})"
                        if opt["default"] not in (None, argparse.SUPPRESS)
                        else ""
                    )
                    print(f"    {flags}{default} - {opt['help'] or ''}")
            if tdata["yaml_keys"]:
                keys = tdata["yaml_keys"]
                if keys.get("task_params_keys"):
                    print(
                        "  YAML task_params keys:", ", ".join(keys["task_params_keys"])
                    )
                if keys.get("host_param_keys"):
                    print("  YAML host_param keys:", ", ".join(keys["host_param_keys"]))
                if keys.get("nested_keys"):
                    print("  YAML nested keys:", ", ".join(keys["nested_keys"]))


if __name__ == "__main__":
    main()
