#!/usr/bin/env python3

# Libervia Tor models
# Copyright (C) 2009-2026 Jérôme Poisson (goffi@goffi.org)

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from enum import StrEnum, auto
from typing import TYPE_CHECKING, Self

from pydantic import BaseModel, Field

from libervia.backend.core import exceptions
from libervia.backend.core.log import getLogger

log = getLogger(__name__)

if TYPE_CHECKING:
    from libervia.backend.core.core_types import SatXMPPClient, SatXMPPEntity


class TorMode(StrEnum):
    #: Tor is completely disabled.
    DISABLED = auto()
    #: Tor is enable for all connections, but WebRTC is disabled (to avoid leaking IP).
    ENABLED_NO_WEBRTC = auto()


class Relay(BaseModel):
    id: str = Field(description="Hex-encoded router identity fingerprint")
    name: str = Field(description="Router nickname or name")
    ip: str = Field(description="IP address")
    flags: list = Field(description='Router flags (e.g., "Guard", "Fast")')
    bandwidth: int | None = Field(
        default=None, description="Reported bandwidth in bytes/sec"
    )
    country: str = Field(description="Country code (ISO 3166-1 alpha-2)")
    details_url: str = Field(
        default_factory=lambda data: f"https://metrics.torproject.org/rs.html#details/{data['id']}",
        description="URL of metrics website with more details on the relay.",
    )


class Circuit(BaseModel):
    id: int = Field(description="Unique identifier for the circuit")
    state: str = Field(
        description="Current state of the circuit (e.g., 'BUILT', 'FAILED')"
    )
    purpose: str = Field(
        description="Purpose of the circuit (e.g., 'GENERAL', 'HS_CLIENT_INTRO')"
    )
    path: list[Relay] = Field(description="List of relays in the circuit path")


class Traffic(BaseModel):
    read: int = Field(description="Bytes read (downloaded) since Tor started")
    written: int = Field(description="Bytes written (uploaded) since Tor started")


class TorData(BaseModel):
    mode: TorMode = Field(description="In which mode Tor active")
    version: str = Field(description="Tor version")
    uptime: int = Field(description="Uptime of the Tor daemon in seconds")
    traffic: Traffic = Field(description="Global traffic statistics")
    circuit: Circuit | None = Field(
        default=None, description="Circuit used for XMPP connection"
    )

    @classmethod
    async def from_client(cls, client: "SatXMPPClient") -> Self:
        """Built an instance from ``client.tor``.

        @return: Instance of TorData
        """
        if client.tor is None:
            raise exceptions.FeatureNotFound("Tor is not used for this profile.")
        host = client.jid.host

        # Initialize with basic version info
        infos = {
            "mode": client.tor_mode,
            "version": client.tor.version,
            "uptime": None,
            "traffic": None,
            "circuit": None,
        }

        state = await client.tor.create_state()

        for stream in state.streams.values():
            if (
                stream.target_host == host
                and stream.target_port == client.port
                and stream.circuit is not None
                and stream.state == "SUCCEEDED"
            ):
                circuit = stream.circuit
                # We have the active stream.
                router_infos = []
                for router in circuit.path:
                    try:
                        location = await router.get_location()
                        country = location.countrycode
                    except Exception:
                        log.exception("Can't get location.")
                        country = "UNKNOWN"

                    router_info = Relay(
                        id=router.id_hex.lstrip("$"),
                        name=router.name,
                        ip=router.ip,
                        flags=router.flags,
                        bandwidth=getattr(router, "bandwidth", None),
                        country=country,
                    )
                    router_infos.append(router_info)

                circuit_info = Circuit(
                    id=circuit.id,
                    state=circuit.state,
                    purpose=circuit.purpose,
                    path=router_infos,
                )
                infos["circuit"] = circuit_info

        try:
            infos_dict = await client.tor.protocol.get_info(
                "uptime", "traffic/read", "traffic/written"
            )
        except Exception:
            log.exception("Can't get Tor infos.")
            raise exceptions.DataError("Tor infos not available.")
        else:
            if "uptime" in infos_dict:
                infos["uptime"] = infos_dict["uptime"]
            if "traffic/read" in infos_dict and "traffic/written" in infos_dict:
                infos["traffic"] = Traffic(
                    read=infos_dict["traffic/read"].strip(),
                    written=infos_dict["traffic/written"].strip(),
                )

        return cls(**infos)
