"""
Sends out ARP spoofing packets for devices in the Device table.

"""

import asyncio
import logging
import random
import time
import traceback
from typing import Any

from scapy.layers.l2 import ARP, Ether
from scapy.sendrecv import sendp

from model.database_manager import DatabaseClient
import shared.config as config
from shared.networking_helpers import get_arp_table, get_default_route

logger = logging.getLogger(__name__)


async def spoof_internet_traffic(db_client: DatabaseClient) -> None:
    """
    Sends out ARP spoofing packets between inspected devices and the gateway.

    """

    arp_spoof = config.get("arp_spoof")
    spoof_mode = config.get("spoof_mode", "continuous")
    opcode = config.get("arp_spoof_op", 2)  # ARP reply by default
    if not arp_spoof:
        return

    start_spoof_ts: float = time.time()
    spoof_interval = config.get(
        "spoof_interval", 10
    )  # seconds between spoofing packets
    spoof_duration = config.get("spoof_duration", 60)  # duration to spoof
    spoof_probability = config.get(
        "spoof_probability", 0.1
    )  # for a given spoofing window, the probability of spoofing the device

    # Get all inspected devices
    inspected_device_list: list[dict[str, Any]] = []
    devices_req = db_client.send_request("get_devices")
    devices_resp = db_client.wait_for_response(devices_req)
    if not devices_resp.success:
        logger.error(f"Error getting devices: {devices_resp.error}")
        return
    if devices_resp.data is None:
        logger.error("Devices query returned None")
        return

    for device in devices_resp.data:
        if not device.get("is_arp_spoofed") or device.get("ip_addr") is None:
            continue
        if spoof_mode == "continuous":
            inspected_device_list.append(device)
        elif spoof_mode == "intermittent":
            if random.random() < spoof_probability:
                # Randomly decide whether to spoof this device
                inspected_device_list.append(device)
        else:
            logger.warning(
                "Unexpected value of spoof_mode setting {spoof_mode}. Refusing to arp spoof"
            )
            return

    route = get_default_route()
    gateway_ip_addr = route.gateway_ip
    gateway_mac_addr = route.gateway_mac
    host_active_interface = route.interface
    host_mac_addr = route.host_mac

    # Get the gateway's IP and MAC addresses
    if gateway_mac_addr is None:
        logger.warning(
            f"Gateway (ip: {gateway_ip_addr}) MAC address not found in ARP cache. Cannot spoof internet traffic yet."
        )
        return

    logger.info(f"Spoofing internet traffic for {len(inspected_device_list)} devices")

    while True:
        if time.time() - start_spoof_ts > spoof_duration:
            logger.debug(f"Stopping ARP spoofing after {spoof_duration} seconds.")
            break

        arp_table = get_arp_table()

        # Send ARP spoofing packets for each inspected device
        for device in inspected_device_list:
            # Make sure that the device is in the ARP cache; if not, skip
            if arp_table.get(device["ip_addr"]) is None:
                continue
            try:
                send_spoofed_arp(
                    device["mac_addr"],
                    device["ip_addr"],
                    gateway_mac_addr,
                    gateway_ip_addr,
                    host_mac_addr,
                    host_active_interface,
                    opcode,
                )
            except Exception:
                logger.error(
                    f"Error spoofing {device['mac_addr']}, {device['ip_addr']} <-> {gateway_mac_addr}, {gateway_ip_addr}"
                )
                logger.debug(traceback.format_exc())

        await asyncio.sleep(spoof_interval)


def send_spoofed_arp(
    victim_mac_addr: str,
    victim_ip_addr: str,
    gateway_mac_addr: str,
    gateway_ip_addr: str,
    host_mac_addr: str,
    host_active_interface: str,
    opcode: int = 2,
) -> None:
    """
    Sends out bidirectional ARP spoofing packets between the victim and the gateway.

    """

    if victim_ip_addr == gateway_ip_addr:
        return

    # logger.debug(
    #     f"spoofing {victim_mac_addr} ({victim_ip_addr}) <-> {gateway_mac_addr} ({gateway_ip_addr})"
    # )

    # Send ARP spoof request to gateway, so that the gateway thinks that Inspector's host is the victim.

    dest_arp = ARP()
    dest_arp.op = opcode
    dest_arp.psrc = victim_ip_addr
    dest_arp.hwsrc = host_mac_addr
    dest_arp.pdst = gateway_ip_addr
    dest_arp.hwdst = gateway_mac_addr

    # use layer 2 sendp function so we can set iface ourselves
    # https://0xbharath.github.io/art-of-packet-crafting-with-scapy/scapy/sending_recieving/index.html#sendp
    _ = sendp(Ether() / dest_arp, iface=host_active_interface, verbose=0)
    # logger.debug(Ether() / dest_arp)

    # Send ARP spoof request to victim, so that the victim thinks that Inspector's host is the gateway.

    victim_arp = ARP()
    victim_arp.op = opcode
    victim_arp.psrc = gateway_ip_addr
    victim_arp.hwsrc = host_mac_addr
    victim_arp.pdst = victim_ip_addr
    victim_arp.hwdst = victim_mac_addr

    # use layer 2 sendp function so we can set iface ourselves
    # https://0xbharath.github.io/art-of-packet-crafting-with-scapy/scapy/sending_recieving/index.html#sendp
    _ = sendp(Ether() / victim_arp, iface=host_active_interface, verbose=0)
    # logger.debug(Ether() / victim_arp)


def reset_arp_tables() -> None:
    pass


async def spoof_internet_traffic_loop(db_client: DatabaseClient) -> None:
    while True:
        try:
            await spoof_internet_traffic(db_client)
        except SystemExit:
            logger.info("SystemExit received, exiting ARP spoofing loop")
            return
        except Exception as e:
            logger.error(f"Error spoofing internet traffic: {e}")
            logger.debug(traceback.format_exc())
        await asyncio.sleep(5)
