"""Router validation workflow. Used in a nightly schedule."""

import json

from orchestrator.targets import Target
from orchestrator.utils.errors import ProcessFailureError
from orchestrator.utils.json import json_dumps
from orchestrator.workflow import StepList, begin, conditional, done, step, workflow
from orchestrator.workflows.steps import resync, store_process_subscription, unsync
from orchestrator.workflows.utils import wrap_modify_initial_input_form

from gso.products.product_types.iptrunk import Iptrunk
from gso.services import infoblox
from gso.services.lso_client import anonymous_lso_interaction, execute_playbook
from gso.services.netbox_client import NetboxClient
from gso.utils.helpers import get_router_vendor
from gso.utils.shared_enums import Vendor


@step("Validate IP trunk configuration")
def validate_router_config(subscription: Iptrunk, callback_route: str) -> None:
    """Run an Ansible playbook that validates the configuration that is present on an active IP trunk."""
    extra_vars = {"wfo_trunk_json": json.loads(json_dumps(subscription)), "verb": "validate"}

    execute_playbook(
        playbook_name="base_config.yaml",
        callback_route=callback_route,
        inventory=f"{subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn}\n"
        f"{subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn}\n",
        extra_vars=extra_vars,
    )


@step("Verify IPAM resources for LAG interfaces")
def verify_ipam_records(subscription: Iptrunk) -> None:
    """Validate the :term:`IPAM` resources for the :term:`LAG` interfaces.

    Raises an :class:`orchestrator.utils.errors.ProcessFailureError` if :term:`IPAM` is configured incorrectly.
    """
    ipam_errors = []
    ipam_v4_network = infoblox.find_network_by_cidr(subscription.iptrunk.iptrunk_ipv4_network)
    ipam_v6_network = infoblox.find_network_by_cidr(subscription.iptrunk.iptrunk_ipv6_network)

    if not ipam_v4_network or not ipam_v6_network:
        ipam_errors.append(
            "Missing IP trunk IPAM records, found the following instead.\n"
            f"IPv4 expected '{subscription.iptrunk.iptrunk_ipv4_network}', actual: '{ipam_v4_network}'\n"
            f"IPv6 expected '{subscription.iptrunk.iptrunk_ipv6_network}', actual: '{ipam_v6_network}'"
        )

    for index, side in enumerate(subscription.iptrunk.iptrunk_sides):
        lag_fqdn = f"{side.iptrunk_side_ae_iface}-0.{side.iptrunk_side_node.router_fqdn}"
        side_v4 = subscription.iptrunk.iptrunk_ipv4_network[index]
        side_v6 = subscription.iptrunk.iptrunk_ipv6_network[index + 1]
        #  Validate IPv4 address allocation
        record = infoblox.find_host_by_fqdn(lag_fqdn)
        if not record:
            ipam_errors.append(f"No IPv4 host record found with FQDN '{lag_fqdn}'")
        else:
            #  Allocation inside IPv4 network must be correct
            if str(side_v4) != record.ipv4addr:
                ipam_errors.append(
                    f"Incorrectly allocated host record for FQDN '{lag_fqdn}'.\n"
                    f"Expected '{side_v4}', actual: '{record.ipv4addr}'"
                )

            #  Allocated host record needs to be set correctly
            if record.comment != str(subscription.subscription_id):
                ipam_errors.append(
                    f"Incorrect host record found for '{lag_fqdn}' at '{side_v4}'. Comment should have been equal "
                    f"to subscription ID '{subscription.subscription_id}'."
                )

        #  Validate IPv6 address allocation
        record = infoblox.find_v6_host_by_fqdn(lag_fqdn)
        if not record:
            ipam_errors.append(f"No IPv6 host record found with FQDN '{lag_fqdn}'")
        else:
            #  Allocation inside IPv6 network must be correct
            if str(side_v6) != record.ipv6addr:
                ipam_errors.append(
                    f"Incorrectly allocated host record for FQDN '{lag_fqdn}'.\n"
                    f"Expected '{side_v6}', actual: '{record.ipv6addr}'"
                )

            #  Allocated host record needs to be set correctly
            if record.comment != str(subscription.subscription_id):
                ipam_errors.append(
                    f"Incorrect host record found for '{lag_fqdn}' at '{side_v6}'. Comment should have been equal "
                    f"to subscription ID '{subscription.subscription_id}'."
                )

    if ipam_errors:
        raise ProcessFailureError(message="IPAM misconfiguration(s) found", details=str(ipam_errors))


@step("Verify NetBox entries")
def verify_netbox_entries(subscription: Iptrunk) -> None:
    """Validate required entries for an IP trunk in NetBox."""
    nbclient = NetboxClient()
    netbox_errors = []
    for side in subscription.iptrunk.iptrunk_sides:
        if get_router_vendor(side.iptrunk_side_node.owner_subscription_id) == Vendor.NOKIA:
            #  Raises en exception when not found.
            interface = nbclient.get_interface_by_name_and_device(
                side.iptrunk_side_ae_iface, side.iptrunk_side_node.router_fqdn
            )
            if interface.description != str(subscription.subscription_id):
                netbox_errors.append(
                    f"Incorrect description for '{side.iptrunk_side_ae_iface}', expected "
                    f"'{subscription.subscription_id}' but got '{interface.description}'"
                )
            if not interface.enabled:
                netbox_errors.append(f"NetBox interface '{side.iptrunk_side_ae_iface}' is not enabled.")
            for member in side.iptrunk_side_ae_members:
                interface = nbclient.get_interface_by_name_and_device(
                    member.interface_name, side.iptrunk_side_node.router_fqdn
                )
                if interface.description != str(subscription.subscription_id):
                    netbox_errors.append(
                        f"Incorrect description for '{member.interface_name}', expected "
                        f"'{subscription.subscription_id}' but got '{interface.description}'"
                    )
                if not interface.enabled:
                    netbox_errors.append(f"NetBox interface '{side.iptrunk_side_ae_iface}' is not enabled.")

    if netbox_errors:
        raise ProcessFailureError(message="NetBox misconfiguration(s) found", details=str(netbox_errors))


@step("Verify configuration of IPtrunk")
def verify_iptrunk_config(subscription: Iptrunk, callback_route: str) -> None:
    """Check for configuration drift on the relevant routers."""
    execute_playbook(
        playbook_name="iptrunks.yaml",
        callback_route=callback_route,
        inventory=f"{subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn}\n"
        f"{subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn}\n",
        extra_vars={
            "wfo_trunk_json": json.loads(json_dumps(subscription)),
            "verb": "deploy",
            "dry_run": "true",
            "config_object": "trunk_interface",
            "is_verification_workflow": "true",
        },
    )


@step("Check ISIS configuration")
def check_ip_trunk_isis(subscription: Iptrunk, callback_route: str) -> None:
    """Run an Ansible playbook to check for any :term:`ISIS` configuration drift."""
    execute_playbook(
        playbook_name="iptrunks.yaml",
        callback_route=callback_route,
        inventory=f"{subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn}\n"
        f"{subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn}\n",
        extra_vars={
            "wfo_trunk_json": json.loads(json_dumps(subscription)),
            "verb": "deploy",
            "dry_run": "true",
            "config_object": "isis_interface",
            "is_verification_workflow": "true",
        },
    )


@step("Verify TWAMP configuration")
def verify_twamp_config(subscription: Iptrunk, callback_route: str) -> None:
    """Check for configuration drift of TWAMP."""
    execute_playbook(
        playbook_name="deploy_twamp.yaml",
        callback_route=callback_route,
        inventory=f"{subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn}\n"
        f"{subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn}\n",
        extra_vars={
            "subscription": json.loads(json_dumps(subscription)),
            "verb": "deploy",
            "dry_run": "true",
            "is_verification_workflow": "true",
        },
    )


@workflow(
    "Validate IP trunk configuration", target=Target.SYSTEM, initial_input_form=wrap_modify_initial_input_form(None)
)
def validate_iptrunk() -> StepList:
    """Validate an existing, active IP Trunk subscription.

    * Verify that the :term:`LAG` interfaces are correctly configured in :term:`IPAM`.
    * Check correct configuration of interfaces in NetBox.
    * Verify the configuration on both sides of the trunk is intact.
    * Check the ISIS metric of the trunk.
    * Verify that TWAMP configuration is correct.

    If a trunk has a Juniper router on both sides, it is considered legacy and does not require validation.
    """
    skip_legacy_trunks = conditional(
        lambda state: all(
            side.iptrunk_side_node.vendor == Vendor.JUNIPER
            for side in Iptrunk.from_subscription(state["subscription_id"]).iptrunk.iptrunk_sides
        )
    )

    return (
        begin
        >> store_process_subscription(Target.SYSTEM)
        >> skip_legacy_trunks(done)
        >> unsync
        >> verify_ipam_records
        >> verify_netbox_entries
        >> anonymous_lso_interaction(verify_iptrunk_config)
        >> anonymous_lso_interaction(check_ip_trunk_isis)
        >> anonymous_lso_interaction(verify_twamp_config)
        >> resync
        >> done
    )