import json
import time
from typing import Any

import click
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text

import asyncmq
from asyncmq.backends.base import DelayedInfo, RepeatableInfo  # Import for type hints
from asyncmq.cli.utils import QUEUES_LOGO, get_centered_logo, get_print_banner, run_cmd

console = Console()


@click.group(name="queue", invoke_without_command=True)
@click.pass_context
def queue_app(ctx: click.Context) -> None:
    """
    Manages AsyncMQ queues.

    This is the main command group for queue-related actions accessible via
    the command line interface. It provides subcommands for tasks such as
    listing available queues, pausing and resuming queue processing, and
    displaying detailed information about a specific queue or its jobs
    (delayed, repeatable). If no subcommand is specified when the `queue`
    group is invoked, it prints a custom help message followed by the standard
    Click help for the available subcommands.

    Args:
        ctx: The Click context object. This object is automatically passed
             by the Click library and contains information about the current
             invocation, including the invoked subcommand.
    """
    # Check if any subcommand was explicitly invoked by the user.
    if ctx.invoked_subcommand is None:
        # If no subcommand was invoked, print the custom help message
        # and then the standard help text generated by Click for this group.
        _print_queue_help()
        click.echo(ctx.get_help())


def _print_queue_help() -> None:
    """
    Prints a custom formatted help message for the queue command group.

    This function generates a visually appealing help message using the
    Rich library. It includes the AsyncMQ logo, a descriptive header, a
    brief explanation of the command group's purpose, and examples of
    common usage. The entire message is presented within a styled Rich Panel.
    """
    # Create a Rich Text object to build the formatted output string.
    text = Text()
    # Add the centered AsyncMQ logo using a utility function, applying bold cyan styling.
    text.append(get_centered_logo(), style="bold cyan")
    # Add a header for the queue commands, also with bold cyan styling, followed by newlines.
    text.append("📦 Queue Commands\n\n", style="bold cyan")
    # Add a descriptive sentence about managing queues, styled in white.
    text.append("Manage AsyncMQ queues: pause, resume, list jobs.\n\n", style="white")
    # Add a section header for examples, styled in bold yellow.
    text.append("Examples:\n", style="bold yellow")
    # Add example command lines.
    text.append("  asyncmq queue list\n")
    text.append("  asyncmq queue pause myqueue\n")
    text.append("  asyncmq queue resume myqueue\n")
    # Print the constructed Rich Text object within a styled Panel.
    console.print(Panel(text, title="Queue CLI", border_style="cyan"))


@queue_app.command("list")
def list_queues() -> None:
    """
    Lists all available queues in the current backend.

    This command interacts with the configured AsyncMQ backend to retrieve
    a list of all queue names that the backend is currently managing or
    is aware of. It then prints these queue names to the console. The
    ability to list queues depends on whether the specific backend
    implementation supports this functionality (i.e., exposes a `list_queues`
    method).
    """
    # Get the currently configured backend instance from monkay.settings.
    backend = asyncmq.monkay.settings.backend
    # Print a banner for the queue listing operation.
    get_print_banner(QUEUES_LOGO, title="AsyncMQ Queues")
    # Print a message indicating that the backend is being queried for queues.
    console.print("[bold green]Fetching queues...[/bold green]")

    # Check if the backend instance has a 'list_queues' method.
    if hasattr(backend, "list_queues"):
        # If the method exists, call it asynchronously using anyio.run
        # and store the returned list of queue names.
        queues = run_cmd(backend.list_queues)
        # Check if the returned list of queues is not empty.
        if queues:
            # If queues were found, iterate through the list and print each queue name
            # prefixed with a bullet point.
            for queue in queues:
                console.print(f"• {queue}")
        else:
            # If the list of queues is empty, print a message indicating no queues were found.
            console.print("[yellow]No queues found.[/yellow]")
    else:
        # If the backend does not have a 'list_queues' method, print a message
        # indicating that queue listing is not supported for this backend type.
        console.print("[yellow]Queue listing not supported for this backend.[/yellow]")


@queue_app.command("pause")
@click.argument("queue")
def pause_queue(queue: str) -> None:
    """
    Pauses processing for a specific queue.

    This command instructs the configured backend to pause the queue with the
    given name. When a queue is paused, workers should typically stop
    dequeueing new jobs from it. The exact behavior depends on the backend's
    implementation of the `pause_queue` method.

    Args:
        queue: The name of the queue to pause. This argument is required
               and is passed from the command line.
    """
    # Get the currently configured backend instance from monkay.settings.
    backend = asyncmq.monkay.settings.backend
    # Call the backend's pause_queue method asynchronously using anyio.run,
    # passing the queue name as an argument.
    run_cmd(backend.pause_queue, queue)

    # Print a banner for the queue operation.
    get_print_banner(QUEUES_LOGO, title="AsyncMQ Queues")
    # Print a confirmation message indicating that the queue has been paused.
    console.print(f"[bold red]Paused queue '{queue}'.[/bold red]")


@queue_app.command("resume")
@click.argument("queue")
def resume_queue(queue: str) -> None:
    """
    Resumes processing for a specific queue.

    This command instructs the configured backend to resume the queue with the
    given name, allowing workers to begin picking up jobs from it again if
    it was previously paused. The exact behavior depends on the backend's
    implementation of the `resume_queue` method.

    Args:
        queue: The name of the queue to resume. This argument is required
               and is passed from the command line.
    """
    # Get the currently configured backend instance from monkay.settings.
    backend = asyncmq.monkay.settings.backend
    # Call the backend's resume_queue method asynchronously using anyio.run,
    # passing the queue name as an argument.
    run_cmd(backend.resume_queue, queue)

    # Print a banner for the queue operation.
    get_print_banner(QUEUES_LOGO, title="AsyncMQ Queues")
    # Print a confirmation message indicating that the queue has been resumed.
    console.print(f"[bold green]Resumed queue '{queue}'.[/bold green]")


@queue_app.command("info")
@click.argument("queue")
def info_queue(queue: str) -> None:
    """
    Shows detailed information about a specific queue.

    This command retrieves and displays various statistics and status
    information for the specified queue. This includes whether the queue
    is currently paused and the number of jobs in different states (waiting,
    delayed, dead-letter queue). The availability and accuracy of this
    information depend on the capabilities and implementation of the
    configured backend.

    Args:
        queue: The name of the queue to get information about. This argument
               is required and is passed from the command line.
    """
    # Get the currently configured backend instance from monkay.settings.
    backend = asyncmq.monkay.settings.backend

    # Print a banner for the queue information operation.
    get_print_banner(QUEUES_LOGO, title="AsyncMQ Queues")
    # Print a message indicating that data fetching is in progress for the specified queue.
    console.print(f"[cyan]Fetching info about queue '{queue}'...[/cyan]\n")

    # Fetch the paused status of the queue asynchronously using anyio.run.
    # This calls the backend's is_queue_paused method.
    paused = run_cmd(backend.is_queue_paused, queue)

    # Initialize job counts to zero. These will be updated if the backend
    # exposes the necessary attributes (e.g., for InMemoryBackend).
    waiting_jobs = 0
    delayed_jobs = 0
    dlq_jobs = 0

    # Check if the backend instance has a 'queues' attribute (common for
    # backends that maintain an in-memory list of waiting jobs, like InMemoryBackend).
    if hasattr(backend, "queues"):
        # If the attribute exists, get the list for the specific queue name
        # (defaulting to an empty list if not found) and get its length.
        waiting_jobs = len(backend.queues.get(queue, []))
    # Check if the backend instance has a 'delayed' attribute (common for
    # backends that maintain an in-memory list of delayed jobs).
    if hasattr(backend, "delayed"):
        # If the attribute exists, get the list for the specific queue name
        # (defaulting to an empty list if not found) and get its length.
        delayed_jobs = len(backend.delayed.get(queue, []))
    # Check if the backend instance has a 'dlqs' attribute (common for
    # backends that maintain an in-memory list of dead-letter queue jobs).
    if hasattr(backend, "dlqs"):
        # If the attribute exists, get the list for the specific queue name
        # (defaulting to an empty list if not found) and get its length.
        dlq_jobs = len(backend.dlqs.get(queue, []))

    # Build a nice Rich table to display the fetched queue information.
    table = Table(
        title=f"Queue '{queue}' Info",  # Title of the table.
        show_header=True,  # Display column headers.
        header_style="bold magenta",  # Styling for the headers.
    )
    # Add columns to the table with specified styling and wrapping behavior.
    table.add_column("Property", style="cyan", no_wrap=True)
    table.add_column("Value", style="green")

    # Add rows to the table with the fetched information.
    table.add_row("Paused", "Yes" if paused else "No")  # Display "Yes" or "No" for paused status.
    table.add_row("Waiting Jobs", str(waiting_jobs))  # Display the count of waiting jobs.
    table.add_row("Delayed Jobs", str(delayed_jobs))  # Display the count of delayed jobs.
    table.add_row("DLQ Jobs", str(dlq_jobs))  # Display the count of DLQ jobs.

    # Print the completed Rich table to the console.
    console.print(table)


@queue_app.command("list-delayed")
@click.argument("queue")
def cli_list_delayed(queue: str) -> None:
    """
    Lists all currently delayed jobs for a specific queue.

    This command retrieves the list of delayed jobs from the specified queue
    using the backend's `list_delayed` method and displays them in a formatted
    table, including the job ID, scheduled run time, and payload.

    Args:
        queue: The name of the queue to list delayed jobs from.
    """
    from asyncmq.queues import Queue

    # Print a banner for the list delayed jobs operation.
    get_print_banner(QUEUES_LOGO, title="AsyncMQ List Delayed Jobs")
    # Create a Queue instance for the specified queue name.
    q = Queue(queue)
    # Call the queue's list_delayed method asynchronously using anyio.run
    # and store the returned list of DelayedInfo dataclass instances.
    jobs: list[DelayedInfo] = run_cmd(q.list_delayed)  # type: ignore

    # Create a Rich Table to display the delayed job information.
    table = Table(show_header=True, header_style="bold magenta")
    # Add columns to the table.
    table.add_column("Job ID", style="dim", width=12)
    table.add_column("Run At", style="green")  # Column for the scheduled run time.
    table.add_column("Payload", overflow="fold")  # Column for the job payload, allowing wrapping.

    # Iterate through the list of DelayedInfo instances.
    for job in jobs:
        # Format the 'run_at' timestamp into a human-readable string.
        human_run_at = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(job.run_at))
        # Serialize the job payload dictionary to a JSON string for display.
        payload_json = json.dumps(job.payload, ensure_ascii=False)
        # Add a row to the table with the job's information.
        table.add_row(
            job.job_id,  # Job ID from the DelayedInfo instance.
            human_run_at,  # Formatted run_at timestamp.
            payload_json,  # JSON string of the job payload.
        )
    # Print the completed Rich Table to the console.
    console.print(table)


@queue_app.command("remove-delayed")
@click.argument("queue")
@click.argument("job_id")
def cli_remove_delayed(queue: str, job_id: str | int) -> None:
    """
    Removes a specific job from the delayed queue.

    This command calls the backend's `remove_delayed` method to cancel a
    job that is currently scheduled for future execution. It provides
    feedback indicating whether the job was successfully found and removed.

    Args:
        queue: The name of the queue the delayed job belongs to.
        job_id: The unique identifier of the delayed job to remove. Can be
                a string or an integer depending on how job IDs are managed.
    """
    from asyncmq.queues import Queue

    # Print a banner for the remove delayed job operation.
    get_print_banner(QUEUES_LOGO, title="AsyncMQ Removed Delayed Jobs")

    # Create a Queue instance for the specified queue name.
    q = Queue(queue)
    # Call the queue's remove_delayed method asynchronously using anyio.run,
    # passing the job ID. The method returns True if the job was removed, False otherwise.
    ok = run_cmd(q.remove_delayed, job_id)

    # Check the boolean result returned by remove_delayed.
    if ok:
        # If True, print a success message with a check mark emoji and the job ID.
        console.print(f":white_check_mark: Removed delayed job [bold]{job_id}[/]")
    else:
        # If False, print a failure message with a cross mark emoji and the job ID.
        console.print(f":cross_mark: No delayed job found with ID [bold]{job_id}[/]")


@queue_app.command("list-repeatables")
@click.argument("queue")
def cli_list_repeatables(queue: str) -> None:
    """
    Lists all currently defined repeatable jobs for a specific queue.

    This command retrieves the list of repeatable job definitions from the
    specified queue using the backend's `list_repeatables` method and displays
    them in a formatted table. The table includes the job definition (as JSON),
    the next scheduled run time, and the current status (active or paused).

    Args:
        queue: The name of the queue to list repeatable jobs from.
    """
    from asyncmq.queues import Queue

    # Print a banner for the list repeatable jobs operation.
    get_print_banner(QUEUES_LOGO, title="AsyncMQ List Repeatable Jobs")
    # Create a Queue instance for the specified queue name.
    q = Queue(queue)
    # Call the queue's list_repeatables method asynchronously using anyio.run
    # and store the returned list of RepeatableInfo dataclass instances.
    rpts: list[RepeatableInfo] = run_cmd(q.list_repeatables)

    # Create a Rich Table to display the repeatable job information.
    table = Table(show_header=True, header_style="bold magenta")
    # Add columns to the table.
    table.add_column("Job Definition", overflow="fold")  # Column for the job definition JSON.
    table.add_column("Next Run", style="green", width=20)  # Column for the next scheduled run time.
    table.add_column("Status", style="cyan", width=8)  # Column for the job status (paused/active).

    # Iterate through the list of RepeatableInfo instances.
    for rpt in rpts:
        # Serialize the job definition dictionary to a JSON string for display.
        job_def_json = json.dumps(rpt.job_def, ensure_ascii=False)
        # Format the 'next_run' timestamp into a human-readable string.
        human_next_run = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(rpt.next_run))
        # Determine the status string based on the 'paused' flag.
        status = "[yellow]paused[/]" if rpt.paused else "[green]active[/]"
        # Add a row to the table with the repeatable job's information.
        table.add_row(
            job_def_json,  # JSON string of the job definition.
            human_next_run,  # Formatted next_run timestamp.
            status,  # Status string.
        )
    # Print the completed Rich Table to the console.
    console.print(table)


@queue_app.command("pause-repeatable")
@click.argument("queue")
@click.argument("job_def_json")
def cli_pause_repeatable(queue: str, job_def_json: str | Any) -> None:
    """
    Pauses a specific repeatable job definition.

    This command takes the queue name and the JSON string representation of
    the repeatable job definition as arguments. It deserializes the JSON,
    calls the backend's `pause_repeatable` method to mark the job as paused,
    and prints a confirmation message.

    Args:
        queue: The name of the queue the repeatable job belongs to.
        job_def_json: A JSON string representing the repeatable job definition.
                      This string must exactly match the definition stored.
                      It's typed as `str | Any` to allow Click's argument
                      handling, but is expected to be a string that can be
                      deserialized to a dictionary.
    """
    from asyncmq.queues import Queue

    # Print a banner for the pause repeatable job operation.
    get_print_banner(QUEUES_LOGO, title="AsyncMQ Pause Repeatable Jobs")
    # Create a Queue instance for the specified queue name.
    q = Queue(queue)
    # Deserialize the input JSON string into a Python dictionary.
    job_def = json.loads(job_def_json)
    # Call the queue's pause_repeatable method asynchronously using anyio.run,
    # passing the job definition dictionary.
    run_cmd(q.pause_repeatable, job_def)
    # Print a confirmation message with a pause emoji and the job definition.
    console.print(f":pause_button: Paused repeatable [bold]{job_def}[/]")


@queue_app.command("resume-repeatable")
@click.argument("queue")
@click.argument("job_def_json")
def cli_resume_repeatable(queue: str, job_def_json: str | Any) -> None:
    """
    Resumes a specific repeatable job definition.

    This command takes the queue name and the JSON string representation of
    the repeatable job definition as arguments. It deserializes the JSON,
    calls the backend's `resume_repeatable` method to un-pause the job and
    compute its next run time, and prints a confirmation message including
    the next scheduled run time.

    Args:
        queue: The name of the queue the repeatable job belongs to.
        job_def_json: A JSON string representing the repeatable job definition.
                      This string must exactly match the definition stored.
                      It's typed as `str | Any` to allow Click's argument
                      handling, but is expected to be a string that can be
                      deserialized to a dictionary.
    """
    from asyncmq.queues import Queue

    # Print a banner for the resume repeatable job operation.
    get_print_banner(QUEUES_LOGO, title="AsyncMQ Resume Repeatable Jobs")
    # Create a Queue instance for the specified queue name.
    q = Queue(queue)
    # Deserialize the input JSON string into a Python dictionary.
    job_def = json.loads(job_def_json)
    # Call the queue's resume_repeatable method asynchronously using anyio.run,
    # passing the job definition dictionary. This method returns the newly
    # computed next run timestamp.
    next_run = run_cmd(q.resume_repeatable, job_def)
    # Format the returned timestamp into a human-readable string.
    human = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(next_run))
    # Print a confirmation message with a forward arrow emoji, the job definition,
    # and the human-readable next run time.
    console.print(f":arrow_forward: Resumed repeatable [bold]{job_def}[/], next run at [bold]{human}[/]")
