from utils import get_all_available_services, ifind_similar_names_in_list
from config import (
    get_logger,
    get_rich_console,
    set_log_level_at_runtime,
    clear_relations_cache,
)
from custom_nodes import *  # noqa
from aws import BalconyAWS

import typer
import jmespath
from typing import Optional, List
from rich.columns import Columns
from rich.panel import Panel
import logging
import boto3

console = get_rich_console()
logger = get_logger(__name__)
session = boto3.session.Session()
service_factory = BalconyAWS(session)
app = typer.Typer(no_args_is_help=True)


@app.callback()
def _main_app_callback(
    debug: bool = typer.Option(False, "--debug", "-d", help="Enable debug messages."),
):
    if debug:
        set_log_level_at_runtime(logging.DEBUG)


def _get_available_service_node_names():
    return get_all_available_services(session)


def generate_service_node_completion_items():
    service_names = _get_available_service_node_names(session)
    return service_names


def _complete_service_name(incomplete: str):
    service_names = _get_available_service_node_names()
    if not incomplete:
        for name in service_names:
            yield name
    else:
        for name in service_names:
            if name.startswith(incomplete):
                yield name


def _complete_resource_node_name(ctx: typer.Context, incomplete: str) -> List[str]:
    service = ctx.params.get("service", False)
    if not service:
        return []
    service_node = service_factory.get_service_node(service)
    if not service_node:
        return []
    resource_nodes = service_node.get_resource_nodes()
    if not resource_nodes:
        return []

    resource_node_names = []

    for _rn in resource_nodes:
        _rn_name = _rn.name
        if incomplete:
            if _rn_name.lower().startswith(incomplete.lower()):
                resource_node_names.append(_rn_name)
        else:
            resource_node_names.append(_rn_name)
    return resource_node_names


def _complete_operation_type(ctx: typer.Context) -> List[str]:
    service = ctx.params.get("service", False)
    resource_node = ctx.params.get("resource_node", False)
    if not service or not resource_node:
        return []
    service_node = service_factory.get_service_node(service)
    if not service_node:
        return []
    resource_node_obj = service_node.get_resource_node_by_name(resource_node)
    if not resource_node_obj:
        return []
    operation_types_and_names = resource_node_obj.get_operation_types_and_names()
    return list(operation_types_and_names.keys())


def _list_service_or_resource(
    service: Optional[str] = typer.Argument(
        None,
        show_default=False,
        help="The AWS service name",
        autocompletion=_complete_service_name,
    ),
    resource_node: Optional[str] = typer.Argument(
        None,
        show_default=False,
        help="The AWS Resource Node",
        autocompletion=_complete_resource_node_name,
    ),
) -> None:

    available_service_names = _get_available_service_node_names()
    if not service and not resource_node:
        # nothing is given
        console.print(Columns(available_service_names, equal=True, expand=True))
        return available_service_names

    if service and not resource_node:
        # we only have service name

        if service not in available_service_names:
            similar_service_names = ifind_similar_names_in_list(
                service, available_service_names
            )
            if similar_service_names:
                console.print("[yellow]Found Similar Service Names:")
                console.print(Columns(similar_service_names, equal=True, expand=True))
                return
            else:
                raise typer.Exit(
                    f"Invalid service name: {service}. Please pick a proper one."
                )

        service_node = service_factory.get_service_node(service)
        resource_nodes = service_node.get_resource_nodes()

        resource_node_names = []
        for _rn in resource_nodes:
            _rn_name = _rn.name
            if len(_rn.get_all_required_parameter_names()) >= 2:
                _rn_name = f"[bold red]{_rn_name}[/]"
            elif len(_rn.get_all_required_parameter_names()) >= 1:
                _rn_name = f"[bold green]{_rn_name}[/]"
            resource_node_names.append(_rn_name)

        resource_node_name_as_columns = Columns(
            resource_node_names, equal=True, expand=True
        )
        console.print(resource_node_name_as_columns)
        return resource_node_names

    elif service and resource_node:
        # we got both options filled
        service_node = service_factory.get_service_node(service)
        resource_nodes = service_node.get_resource_nodes()
        resource_node_names = [_rn.name for _rn in resource_nodes]
        resource_node_obj = service_node.get_resource_node_by_name(resource_node)
        if not resource_node_obj:

            similar_resource_names = ifind_similar_names_in_list(
                resource_node, resource_node_names
            )
            if similar_resource_names:
                console.print("[yellow]Found Similar Resource Node Names:")
                console.print(Columns(similar_resource_names, equal=True, expand=True))
                return
            else:
                raise typer.Exit(
                    f"Invalid Resource Node name: [bold]{resource_node}[/] for Service: [bold]{service}[/]."
                )
        operations_panel = service_node._get_operation_details_panel(
            resource_node_obj.name
        )
        console.print(operations_panel)
        return


@app.command("aws", help="List AWS services, Call read-operations, Show documentation")
def aws_main_command(  # noqa
    service: Optional[str] = typer.Argument(
        None,
        show_default=False,
        help="Name of the AWS Service",
        autocompletion=_complete_service_name,
    ),
    resource_node: Optional[str] = typer.Argument(
        None,
        show_default=False,
        help="Name of the AWS Resource Node",
        autocompletion=_complete_resource_node_name,
    ),
    operation: Optional[str] = typer.Argument(
        None,
        show_default=False,
        help="Select a specific operation type. <get, gets, list, describe>",
        autocompletion=_complete_operation_type,
    ),
    patterns: Optional[List[str]] = typer.Option(
        None,
        "--pattern",
        "-p",
        show_default=False,
        help='UNIX pattern matching for generated parameters. Should be quoted. e.g. (-p "*prod-*")',
    ),
    jmespath_selector: Optional[str] = typer.Option(
        None,
        "--jmespath-selector",
        "-js",
        show_default=False,
        help="JMESPath query selector to filter resulted data. Visit for tutorial: https://jmespath.org/tutorial.html",
    ),
    debug: bool = typer.Option(False, "--debug", "-d", help="Enable debug messages."),
    formatter: Optional[str] = typer.Option(
        None,
        "--format",
        "-f",
        show_default=False,
        help="Python f-string expression to generate a line for each item.",
    ),
    list_contents: bool = typer.Option(
        False,
        "--list",
        "-l",
        help="Print the details of Service or Resource. Does not make requests.",
    ),
    paginate: bool = typer.Option(
        False, "--screen", "-s", help="Open the data on a separate paginator on shell."
    ),
):
    if debug:
        set_log_level_at_runtime(logging.DEBUG)
    if list_contents:
        _list_service_or_resource(service, resource_node)
        return

    if not service and not resource_node:
        available_services = _list_service_or_resource(service, resource_node)
        console.print(
            Panel("[bold]Please pick one of the AWS Services", title="[red][bold]ERROR")
        )
        return {"services": available_services}

    if service and not resource_node:
        available_resources = _list_service_or_resource(service, resource_node)
        console.print(
            Panel(
                f"[bold]Please pick one of the Resource Nodes from [green]{service}[/] Service",
                title="[red][bold]ERROR",
            )
        )
        return {"service": service, "resources": available_resources}

    elif service and resource_node:
        service_node = service_factory.get_service_node(service)
        service_reader = service_node.get_service_reader()

        is_operation_selected = operation is not None
        read_data = None
        if not is_operation_selected:
            # read all operations in given resource node
            read_data = service_reader.read_resource_node(
                resource_node, match_patterns=patterns
            )

        else:  # Operation is selected
            resource_node_obj = service_node.get_resource_node_by_name(resource_node)
            types_to_op_names = resource_node_obj.get_operation_types_and_names()
            supported_operation_types = list(types_to_op_names.keys())
            operation_name = types_to_op_names.get(operation, False)
            if not operation_name:
                console.print(
                    f"[red bold]Given {operation} is not supported by {resource_node}. Try: {supported_operation_types}"
                )
                return False
            read_data = service_reader.read_operation(
                resource_node, operation_name, match_patterns=patterns
            )

        if jmespath_selector:

            logger.debug(
                f"Using jmespath selector: {jmespath_selector} to query the returned data."
            )
            read_data = jmespath.search(jmespath_selector, read_data)

        if formatter:
            if read_data:
                for r_data in read_data:
                    try:
                        console.print(formatter.format(**r_data), overflow="ignore")
                    except AttributeError as e:
                        logger.debug(
                            f"Failed to format  [red]{str(e)}[/] with data: {r_data}"
                        )
                return read_data
            else:
                logger.debug(
                    f"[red]No data found.[/] Failed to use --format: '{formatter}'"
                )

        if paginate:
            with console.pager(styles=True):
                console.print_json(data=read_data, default=str)
        else:
            console.print_json(data=read_data, default=str)
        return read_data


@app.command("clear-cache", help="Clear relations json cache")
def clear_cache_command(
    # service: Optional[str] = typer.Argument(None, show_default='all',
    # help='Name of the Service to clear relation caches of', autocompletion=_complete_service_name),
):
    logger.debug("Deleting relations cache for services:")
    deleted_service_caches = clear_relations_cache()
    for deleted_service in deleted_service_caches:
        logger.info(f"[green]Deleted[/] {deleted_service}")


# @app.command('version', help='Show version info' )
# def version_command():
#     from importlib.metadata import version
#     console.print(__package__)
#     version=version(__package__)
#     console.print(version)


def run_app():
    app(prog_name="balcony")


if __name__ == "__main__":
    run_app()
