import os
import re
from logging import Logger
from typing import override

from pydantic.dataclasses import dataclass

from test_automation_suite.checks.check import Check
from test_automation_suite.core.context import Context
from test_automation_suite.core.task import Step, StepResult
from test_automation_suite.utility.cmd import run_cmd, run_cmd_t


@dataclass
class WifiCredentials:
    ssid: str
    password: str


class CheckNetwork(Check):
    @override
    def steps(self) -> list[Step]:
        return [
            *super().steps(),
            self.check_lan,
            self.check_wifi,
            self.check_bluetooth,
        ]

    @staticmethod
    def remove_connection(con: str, logger: Logger) -> bool:
        """Remove all connections named "con"

        This ensures creating the connection later in the test does not cause unnecessary errors.
        Because nmcli deletes all connections with this name, no additional logic is required.
        """
        logger.info('removing all connections with name "%s"', con)
        return run_cmd(f"nmcli c delete '{con}'").success

    def check_internet_connection(self, logger: Logger) -> bool:
        target: str = "139.178.84.217"  # "kernel.org"
        timeout: str = "30"
        count: str = "5"
        logger.info("checking internet connectivity by pinging %s", target)
        cmd = run_cmd("ping " + target + " -W " + timeout + " -c " + count)
        if not cmd.success:
            logger.error("Checking network failed: %s", cmd.stderr)
            return False
        return True

    def check_wifi(self, _context: Context, logger: Logger) -> StepResult:  # noqa: PLR0911
        self.deactivate_all_connections(logger)

        credentials = self.retrieve_wifi_info()
        if not credentials:
            logger.error("No Wifi credentials given")
            return StepResult(success=False)

        _, error = run_cmd_t("nmcli r wifi on")
        if error:
            logger.error("Failed to turn radio on: %s", error)
            return StepResult(success=False)
        logger.info("parse device name for wifi device")
        device_result, error = run_cmd_t("nmcli -t d | grep ':wifi:'")
        if error:
            logger.error("Failed to find wifi device: %s", error)
            return StepResult(success=False)
        ifacename = device_result.split(":")[0]
        logger.info("ifacename found: %s", ifacename)
        con_name = "wifitest"
        self.remove_connection(con_name, logger)
        logger.info("create new wifi connection %s", con_name)
        cmd = f"nmcli connection add type wifi ifname '{ifacename}' ssid '{credentials.ssid}' con-name '{con_name}' +802-11-wireless-security.key-mgmt WPA-PSK +802-11-wireless-security.psk '{credentials.password}'"  # noqa: E501
        _, error = run_cmd_t(cmd)
        if error:
            logger.error("Failed to add new wifi connection: %s", error)
            return StepResult(success=False)
        logger.info("connect to new wifi connection")
        _, error = run_cmd_t(f"nmcli -t c up '{con_name}'")
        if error:
            logger.error("Failed to activate wifi connection: %s", error)
            return StepResult(success=False)
        if self.check_internet_connection(logger):
            return StepResult(success=True)
        return StepResult(success=False)

    def deactivate_all_connections(self, logger: Logger) -> bool:
        logger.info("deactivating all connections")
        result, error = run_cmd_t('nmcli -t | grep " connected"')
        if error:
            logger.error("Could not parse active connections: %s", error)
            return False
        all_connections = result.split("\n")
        for con in all_connections:
            if con:
                s = re.search("to\\s.*", con)
                assert s is not None, "regex didn't capture any connection"
                c = s.group()
                c = c[3:]
                result, error = run_cmd_t(f'nmcli c down "{c}"')
                if error:
                    # Do not stop execution if this command fails, since the error is often
                    # irrelevant. The error is logged anyway, so if it causes issues (e.g.
                    # the wired connection was not disconnected and the Wi-Fi test ran
                    # incorrectly), this will still be visible in the log file.
                    logger.error("Could not disconnect: %s", error)
        return True

    def check_lan(self, _context: Context, logger: Logger) -> StepResult:
        logger.info("checking lan connection")
        self.deactivate_all_connections(logger)
        if not self.reconnect_lan(logger):
            return StepResult(success=False)
        if not self.check_internet_connection(logger):
            return StepResult(success=False)
        return StepResult(success=False)

    def reconnect_lan(self, logger: Logger) -> bool:
        logger.info("checking for ethernet devices")
        result, error = run_cmd_t('nmcli -t d | grep "ethernet"')
        if error:
            logger.error("Failed to find ethernet device: %s", error)
            return False
        interface = result.split(":")[0]
        logger.info("interface found: %s", interface)
        con_name = result.split(":")[3].strip()
        self.remove_connection(con_name, logger)
        cmd = f"nmcli -t c add type ethernet ifname '{interface}' con-name '{con_name}' ipv4.method auto"  # noqa: E501
        result, error = run_cmd_t(cmd)
        if error:
            logger.error("Failed to add ethernet connection: %s", error)
            return False
        cmd = f"nmcli connection up '{con_name}'"
        result, error = run_cmd_t(cmd)
        if error:
            logger.error("Failed to activate LAN connection: %s", error)
            return False
        return True

    def check_bluetooth(self, _context: Context, logger: Logger) -> StepResult:
        logger.info("checking bluetooth")
        # TODO: maybe we could build our own little bluetooth AP with an esp32 and
        #   hardcode mac address? => just scan and see if you can see bt devices, no
        #   connection testing for now

        # turn bluetooth power on for the radio to work
        result, error = run_cmd_t("bluetoothctl power on")
        if error:
            logger.error("Failed to turn bt radio on: %s", error)
            return StepResult(success=False)
        # for non interactive mode you need to specify a timeout, or otherwise it will
        # abort immediately again, so scan for 30s
        result, error = run_cmd_t("bluetoothctl -t 30 scan on")
        if error:
            logger.error("Failed to scan bt devices: %s", error)
            return StepResult(success=False)
        # look at result of scan, if at least one "device" is being displayed with a mac
        # address, bt should be fine.
        if "Device" in result:
            logger.info("At least one bt device was found")
            return StepResult(success=True)

        logger.error("No bt was found during scan. (Are there any?)")
        return StepResult(success=False)

    def retrieve_wifi_info(self) -> WifiCredentials | None:
        try:
            wifi_ssid = os.environ["TAS_WIFI_SSID"]
            wifi_passcode = os.environ["TAS_WIFI_PASS"]
        except KeyError:
            return None
        else:
            return WifiCredentials(wifi_ssid, wifi_passcode)
