import logging

from routers.router import Router, RouterType

from .adapter_factory import get_alias_adapter
from .loader import (
    find_alias_by_name,
    get_exclusions_for_client,
    get_ignore_differences_for_client,
    get_reference_aliases_for_client,
)
from .types import Alias

logger = logging.getLogger(__name__)


def _show_differences(client_alias: Alias, router: Router, reference_alias: Alias, output_buffer: list[str]) -> None:
    """
    Show the differences between two aliases where the alias name matches.
    :param client_alias: Alias object from the client
    :param router: Router object
    :param reference_alias: Reference Alias object
    :param output_buffer: List to store output strings
    """
    assert router.type in (
        RouterType.PFSENSE_V1,
        RouterType.PFSENSE_V2,
        RouterType.OPNSENSE,
    ), "Unsupported router type."

    if router.type in (RouterType.PFSENSE_V1, RouterType.PFSENSE_V2):
        comparison_keys = ["name", "alias_type", "description", "content", "detail"]
    else:
        comparison_keys = ["name", "alias_type", "description", "content"]

    output_buffer.append(f'--> Comparing alias "{client_alias.name}" on {router.name} with reference')
    for key in comparison_keys:
        client_value = getattr(client_alias, key)
        reference_value = getattr(reference_alias, key)
        if client_value != reference_value:
            output_buffer.append(f"  {key}: '{client_value}' -> '{reference_value}'")


def print_differing_client_aliases(router: Router, ignore_exclusions: bool = False, show_missing: bool = True) -> None:
    """
    Print the differences between the client aliases on the router and the reference aliases.
    :param router: Router object
    :param ignore_exclusions: Show all missing aliases even if they are in the exclusions list
    :param show_missing: Printing missing aliases (default: True)
    """
    output_buffer: list[str] = []
    adapter = get_alias_adapter(router)
    reference_aliases = get_reference_aliases_for_client(router.name)
    exclusions = get_exclusions_for_client(router.name)
    ignore_differences = get_ignore_differences_for_client(router.name)

    firewall_aliases = adapter.get_all_aliases()
    for reference_alias in reference_aliases:
        router_alias = find_alias_by_name(reference_alias.name, firewall_aliases)
        if router_alias is None:
            if show_missing and (ignore_exclusions or reference_alias.name not in exclusions):
                output_buffer.append(f"Alias with name {reference_alias.name} doesn't exist on {router.name}!")
        elif not router_alias.equals(reference_alias, router) and router_alias.name not in ignore_differences:
            _show_differences(router_alias, router, reference_alias, output_buffer)

    if output_buffer:
        print("\n".join(output_buffer))
    else:
        print(f"No differences found between client aliases and reference aliases on {router}.")


def print_missing_reference_aliases(router: Router, assume_updated: bool = True) -> None:
    """
    Print all aliases from the router that are not in the reference list.
    :param router: Router object
    :param assume_updated: If True, the function will assume that the router aliases have been updated
    and that the reference list is outdated. If False, the function will assume that the reference list is up-to-date.
    """
    output_buffer: list[str] = []
    adapter = get_alias_adapter(router)
    reference_aliases = get_reference_aliases_for_client(router.name)
    exclusions = get_exclusions_for_client(router.name)
    firewall_aliases = adapter.get_all_aliases()

    for router_alias in firewall_aliases:
        reference_alias = find_alias_by_name(router_alias.name, reference_aliases)
        if reference_alias is None:
            output_buffer.append(f"Missing on {router.name}: {router_alias}")
        elif not router_alias.equals(reference_alias, router):
            if assume_updated:
                output_buffer.append(f"Updated on {router.name}: {router_alias}")
            else:
                _show_differences(router_alias, router, reference_alias, output_buffer)
        if router_alias.name in exclusions:
            output_buffer.append(f"Excluded on {router.name}: {router_alias.name}")

    if output_buffer:
        print("\n".join(output_buffer))
    else:
        print(f"No missing or updated aliases found in reference list for {router}.")


def update_differences_on_client(router: Router, no_create: bool = False) -> None:
    """
    Update aliases on the client where differences are found.
    :param router: Router object
    :param no_create: If True, will not create aliases that do not exist on the client
    """
    adapter = get_alias_adapter(router)
    reference_aliases = get_reference_aliases_for_client(router.name)
    exclusions = get_exclusions_for_client(router.name)
    ignore_differences = get_ignore_differences_for_client(router.name)

    firewall_aliases = adapter.get_all_aliases()
    for reference_alias in reference_aliases:
        router_alias = find_alias_by_name(reference_alias.name, firewall_aliases)
        if (
            router_alias
            and not router_alias.equals(reference_alias, router)
            and router_alias.name not in ignore_differences
        ):
            logger.info(f"Updating alias with name {reference_alias.name}")
            logger.info(adapter.update_alias(reference_alias, apply=False))
        if router_alias is None and reference_alias.name not in exclusions:
            if no_create:
                logger.info(
                    f"Alias with name {reference_alias.name} doesn't exist on {router.name}. Skipping creation."
                )
            else:
                logger.info(f"Alias with name {reference_alias.name} doesn't exist on {router.name}. Creating:")
                logger.info(adapter.create_alias(reference_alias, apply=False))
    # Apply changes all at once at the end
    logger.info(adapter.apply())


def create_new_on_client(router: Router) -> None:
    """
    Create all aliases on the router that are not in the reference list (excluding exclusions).
    This function will not update existing aliases.
    :param router: Router object
    """
    adapter = get_alias_adapter(router)
    reference_aliases = get_reference_aliases_for_client(router.name)
    exclusions = get_exclusions_for_client(router.name)
    firewall_aliases = adapter.get_all_aliases()

    for reference_alias in reference_aliases:
        router_alias = find_alias_by_name(reference_alias.name, firewall_aliases)
        if router_alias is None and reference_alias.name not in exclusions:
            logger.info(f"Alias with name {reference_alias.name} doesn't exist on {router.name}. Creating:")
            logger.info(adapter.create_alias(reference_alias, apply=False))
    # Apply changes all at once at the end
    logger.info(adapter.apply())


def delete_missing_reference_aliases(router: Router) -> None:
    """
    Delete all aliases on the router that are not in the reference list.
    Also delete aliases that are on the routers exclusions list.
    :param router: Router object
    """
    adapter = get_alias_adapter(router)
    reference_aliases = get_reference_aliases_for_client(router.name)
    exclusions = get_exclusions_for_client(router.name)
    firewall_aliases = adapter.get_all_aliases()

    for router_alias in firewall_aliases:
        reference_alias = find_alias_by_name(router_alias.name, reference_aliases)
        if reference_alias is None or router_alias.name in exclusions:
            logger.info(f"Deleting alias with name {router_alias.name}")
            logger.info(adapter.delete_alias(router_alias.name, apply=False))
    # Apply changes all at once at the end
    logger.info(adapter.apply())


def rename_alias_on_client(router: Router, old_name: str, new_name: str) -> None:
    """
    Rename an alias on the client.
    :param router: Router object
    :param old_name: Current name of the alias
    :param new_name: New name for the alias
    """
    adapter = get_alias_adapter(router)
    firewall_aliases = adapter.get_all_aliases()
    old_alias = find_alias_by_name(old_name, firewall_aliases)
    if old_alias is None:
        logger.info(f"Alias with name {old_name} doesn't exist on {router.name}. Skipping rename.")
        return
    logger.info(f"Renaming alias with name {old_name} to {new_name}")
    logger.info(adapter.rename_alias(old_alias, new_name))
