import asyncio
import os
import time

import RNS


class RNProbeHandler:
    DEFAULT_PROBE_SIZE = 16
    DEFAULT_TIMEOUT = 12

    def __init__(self, reticulum_instance, identity):
        self.reticulum = reticulum_instance
        self.identity = identity

    async def probe_destination(
        self,
        destination_hash: bytes,
        full_name: str,
        size: int = DEFAULT_PROBE_SIZE,
        timeout: float | None = None,
        wait: float = 0,
        probes: int = 1,
    ):
        try:
            app_name, aspects = RNS.Destination.app_and_aspects_from_name(full_name)
        except Exception as e:
            msg = f"Invalid destination name: {e}"
            raise ValueError(msg)

        if not RNS.Transport.has_path(destination_hash):
            RNS.Transport.request_path(destination_hash)

        timeout_after = time.time() + (
            timeout
            or self.DEFAULT_TIMEOUT
            + self.reticulum.get_first_hop_timeout(destination_hash)
        )
        while (
            not RNS.Transport.has_path(destination_hash) and time.time() < timeout_after
        ):
            await asyncio.sleep(0.1)

        if not RNS.Transport.has_path(destination_hash):
            msg = "Path request timed out"
            raise TimeoutError(msg)

        server_identity = RNS.Identity.recall(destination_hash)
        request_destination = RNS.Destination(
            server_identity,
            RNS.Destination.OUT,
            RNS.Destination.SINGLE,
            app_name,
            *aspects,
        )

        results = []
        sent = 0

        while probes > 0:
            if sent > 0:
                await asyncio.sleep(wait)

            try:
                probe = RNS.Packet(request_destination, os.urandom(size))
                probe.pack()
            except OSError:
                msg = f"Probe packet size of {len(probe.raw)} bytes exceeds MTU of {RNS.Reticulum.MTU} bytes"
                raise ValueError(msg)

            receipt = probe.send()
            sent += 1

            next_hop = self.reticulum.get_next_hop(destination_hash)
            via_str = f" via {RNS.prettyhexrep(next_hop)}" if next_hop else ""
            if_name = self.reticulum.get_next_hop_if_name(destination_hash)
            if_str = f" on {if_name}" if if_name and if_name != "None" else ""

            timeout_after = time.time() + (
                timeout
                or self.DEFAULT_TIMEOUT
                + self.reticulum.get_first_hop_timeout(destination_hash)
            )
            while (
                receipt.status == RNS.PacketReceipt.SENT and time.time() < timeout_after
            ):
                await asyncio.sleep(0.1)

            result: dict = {
                "probe_number": sent,
                "size": size,
                "destination": RNS.prettyhexrep(destination_hash),
                "via": via_str,
                "interface": if_str,
                "status": "timeout",
            }

            if time.time() > timeout_after:
                result["status"] = "timeout"
            elif receipt.status == RNS.PacketReceipt.DELIVERED:
                hops = RNS.Transport.hops_to(destination_hash)
                rtt = receipt.get_rtt()

                if rtt >= 1:
                    rtt_str = f"{round(rtt, 3)} seconds"
                else:
                    rtt_str = f"{round(rtt * 1000, 3)} milliseconds"

                reception_stats = {}
                if self.reticulum.is_connected_to_shared_instance:
                    reception_rssi = self.reticulum.get_packet_rssi(
                        receipt.proof_packet.packet_hash,
                    )
                    reception_snr = self.reticulum.get_packet_snr(
                        receipt.proof_packet.packet_hash,
                    )
                    reception_q = self.reticulum.get_packet_q(
                        receipt.proof_packet.packet_hash,
                    )

                    if reception_rssi is not None:
                        reception_stats["rssi"] = reception_rssi
                    if reception_snr is not None:
                        reception_stats["snr"] = reception_snr
                    if reception_q is not None:
                        reception_stats["quality"] = reception_q
                elif receipt.proof_packet:
                    if receipt.proof_packet.rssi is not None:
                        reception_stats["rssi"] = receipt.proof_packet.rssi
                    if receipt.proof_packet.snr is not None:
                        reception_stats["snr"] = receipt.proof_packet.snr

                result.update(
                    {
                        "status": "delivered",
                        "hops": hops,
                        "rtt": rtt,
                        "rtt_string": rtt_str,
                        "reception_stats": reception_stats,
                    },
                )
            else:
                result["status"] = "failed"

            results.append(result)
            probes -= 1

        return {
            "results": results,
            "sent": sent,
            "delivered": sum(1 for r in results if r["status"] == "delivered"),
            "timeouts": sum(1 for r in results if r["status"] == "timeout"),
            "failed": sum(1 for r in results if r["status"] == "failed"),
        }
