"""Data structures for the isard SAML authentication library."""

from __future__ import annotations

import uuid
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from typing import Optional


def _utcnow() -> datetime:
    return datetime.now(timezone.utc)


# ---------------------------------------------------------------------------
# User information extracted from a SAML assertion
# ---------------------------------------------------------------------------


@dataclass
class UserInfo:
    username: str
    display_name: str
    email: str
    given_name: Optional[str] = None
    family_name: Optional[str] = None
    organization: Optional[str] = None
    role: Optional[str] = None
    # All raw SAML attributes keyed by attribute Name
    saml_attributes: dict[str, list[str]] = field(default_factory=dict)

    def full_name(self) -> str:
        if self.given_name and self.family_name:
            return f"{self.given_name} {self.family_name}"
        if self.given_name:
            return self.given_name
        if self.family_name:
            return self.family_name
        return self.display_name


# ---------------------------------------------------------------------------
# Authenticated session
# ---------------------------------------------------------------------------


@dataclass
class AuthSession:
    user_info: UserInfo
    expires_at: datetime
    session_id: str = field(default_factory=lambda: str(uuid.uuid4()))
    access_token: Optional[str] = None
    refresh_token: Optional[str] = None
    # Raw cookie dict (name → value) captured after ACS submission
    cookies: dict[str, str] = field(default_factory=dict)
    attributes: dict[str, str] = field(default_factory=dict)
    created_at: datetime = field(default_factory=_utcnow)
    last_refreshed_at: Optional[datetime] = None

    def is_valid(self) -> bool:
        return _utcnow() < self.expires_at

    def should_refresh(self, refresh_threshold: timedelta) -> bool:
        return self.is_valid() and _utcnow() >= (self.expires_at - refresh_threshold)

    def time_until_expiry(self) -> timedelta:
        delta = self.expires_at - _utcnow()
        return delta if delta.total_seconds() > 0 else timedelta(0)


# ---------------------------------------------------------------------------
# SAML protocol structures
# ---------------------------------------------------------------------------


@dataclass
class SamlStatus:
    status_code: str
    sub_status_code: Optional[str] = None
    status_message: Optional[str] = None

    _SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"

    def is_success(self) -> bool:
        return self.status_code == self._SUCCESS


@dataclass
class SamlAssertion:
    id: str
    issue_instant: datetime
    issuer: str
    subject: str
    name_id_format: Optional[str] = None
    not_before: Optional[datetime] = None
    not_on_or_after: Optional[datetime] = None
    session_index: Optional[str] = None
    session_not_on_or_after: Optional[datetime] = None
    authn_context_class_ref: Optional[str] = None
    authn_instant: Optional[datetime] = None
    attributes: dict[str, list[str]] = field(default_factory=dict)
    audience_restrictions: list[str] = field(default_factory=list)
    signature_verified: bool = False

    def is_valid(self, clock_skew: timedelta = timedelta(minutes=5)) -> bool:
        now = _utcnow()
        if self.not_before is not None and now < (self.not_before - clock_skew):
            return False
        if self.not_on_or_after is not None and now >= (
            self.not_on_or_after + clock_skew
        ):
            return False
        return True

    # ------------------------------------------------------------------
    # Map Azure URN-based SAML attributes → UserInfo
    # Mirrors SamlAssertion::to_user_info() in Rust
    # ------------------------------------------------------------------
    def to_user_info(self) -> UserInfo:
        NS = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/"

        def first(attr_names: list[str]) -> Optional[str]:
            for name in attr_names:
                vals = self.attributes.get(name)
                if vals:
                    return vals[0]
            return None

        username = (
            first(
                [
                    f"{NS}userPrincipalName",
                    f"{NS}upn",
                    "upn",
                ]
            )
            or self.subject
        )

        display_name = (
            first(
                [
                    f"{NS}DisplayName",
                    f"{NS}name",
                    "displayName",
                    "cn",
                ]
            )
            or username
        )

        email = (
            first(
                [
                    f"{NS}emailaddress",
                    f"{NS}EmailAddress",
                    "mail",
                    "emailAddress",
                ]
            )
            or username
        )

        given_name = first([f"{NS}givenname", "givenName"])
        family_name = first([f"{NS}surname", "sn", "surname"])
        organization = first([f"{NS}CompanyName", "o", "organization"])
        role = first([f"{NS}Department", "title", "role"])

        return UserInfo(
            username=username,
            display_name=display_name,
            email=email,
            given_name=given_name,
            family_name=family_name,
            organization=organization,
            role=role,
            saml_attributes=self.attributes,
        )


@dataclass
class SamlResponse:
    id: str
    issue_instant: datetime
    status: SamlStatus
    issuer: str
    in_response_to: Optional[str] = None
    assertion: Optional[SamlAssertion] = None
    destination: Optional[str] = None


# ---------------------------------------------------------------------------
# IsardVDI session (JWT-based, separate from the Gencat SAML session)
# ---------------------------------------------------------------------------


@dataclass
class IsardVdiSession:
    """Holds the JWT token returned by IsardVDI's /authentication/login endpoint."""

    token: str
    username: str
    # IsardVDI JWTs don't always carry an explicit expiry; default to 8 hours.
    expires_at: datetime = field(
        default_factory=lambda: datetime.now(timezone.utc) + timedelta(hours=8)
    )

    def is_valid(self) -> bool:
        return datetime.now(timezone.utc) < self.expires_at


# ---------------------------------------------------------------------------
# IsardVDI desktop
# ---------------------------------------------------------------------------


@dataclass
class Desktop:
    id: str
    name: str
    state: str
    ip: Optional[str] = None
    description: Optional[str] = None
    os: Optional[str] = None
    image: Optional[str] = None
    template_id: Optional[str] = None
    vcpus: Optional[int] = None
    memory_mb: Optional[int] = None

    def is_started(self) -> bool:
        return self.state == "Started"

    @classmethod
    def from_dict(cls, data: dict) -> "Desktop":
        return cls(
            id=data["id"],
            name=data["name"],
            state=data.get("state", "Unknown"),
            ip=data.get("ip"),
            description=data.get("description"),
            os=data.get("os"),
            image=data.get("image"),
            template_id=data.get("template"),
        )


@dataclass
class Template:
    """Represents an IsardVDI template/image available for creating desktops."""

    id: str
    name: str
    description: Optional[str] = None
    os: Optional[str] = None
    category: Optional[str] = None
    category_name: Optional[str] = None
    enabled: bool = True
    vcpus: Optional[int] = None
    memory_mb: Optional[int] = None
    disk_size_gb: Optional[int] = None

    @classmethod
    def from_dict(cls, data: dict) -> "Template":
        """Create Template from IsardVDI API response."""
        # Extract hardware info if available
        hardware = data.get("create_dict", {}).get("hardware", {})
        memory_kib = hardware.get("memory")
        memory_mb = int(memory_kib // 1024) if memory_kib else None

        # Extract disk info if available
        disk_size_gb = None
        disks = hardware.get("disks", [])
        if disks and isinstance(disks, list) and len(disks) > 0:
            disk_size_bytes = disks[0].get("size")
            if disk_size_bytes:
                disk_size_gb = int(disk_size_bytes // (1024**3))

        return cls(
            id=data["id"],
            name=data["name"],
            description=data.get("description"),
            os=data.get("os"),
            category=data.get("category"),
            category_name=data.get("category_name"),
            enabled=data.get("enabled", True),
            vcpus=hardware.get("vcpus"),
            memory_mb=memory_mb,
            disk_size_gb=disk_size_gb,
        )


# ---------------------------------------------------------------------------
# IsardVDI bastion (SSH jump-host) configuration
# ---------------------------------------------------------------------------


@dataclass
class BastionSsh:
    """SSH sub-configuration of a :class:`BastionTarget`."""

    enabled: bool = False
    port: int = 22
    authorized_keys: list[str] = field(default_factory=list)

    @classmethod
    def from_dict(cls, data: dict) -> "BastionSsh":
        return cls(
            enabled=bool(data.get("enabled", False)),
            port=int(data.get("port", 22)),
            authorized_keys=[k for k in data.get("authorized_keys", []) if k],
        )


@dataclass
class BastionHttp:
    """HTTP sub-configuration of a :class:`BastionTarget`."""

    enabled: bool = False
    http_port: int = 80
    https_port: int = 443
    proxy_protocol: bool = False

    @classmethod
    def from_dict(cls, data: dict) -> "BastionHttp":
        return cls(
            enabled=bool(data.get("enabled", False)),
            http_port=int(data.get("http_port", 80)),
            https_port=int(data.get("https_port", 443)),
            proxy_protocol=bool(data.get("proxy_protocol", False)),
        )


@dataclass
class BastionTarget:
    """Bastion configuration for a single IsardVDI desktop.

    The ``id`` field is the SSH username used to connect via the bastion:

        ssh <id>@<isardvdi_host> [-p <port>]
    """

    id: str
    desktop_id: str
    user_id: str
    ssh: BastionSsh = field(default_factory=BastionSsh)
    http: BastionHttp = field(default_factory=BastionHttp)
    domains: list[str] = field(default_factory=list)

    @classmethod
    def from_dict(cls, data: dict) -> "BastionTarget":
        return cls(
            id=data["id"],
            desktop_id=data["desktop_id"],
            user_id=data["user_id"],
            ssh=BastionSsh.from_dict(data.get("ssh", {})),
            http=BastionHttp.from_dict(data.get("http", {})),
            domains=list(data.get("domains", [])),
        )


# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

_DEFAULT_USER_AGENTS = [
    (
        "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
        "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
    ),
    (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
        "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
    ),
    (
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
        "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
    ),
]


@dataclass
class GencatConfig:
    base_url: str = "https://elmeuescriptori.gestioeducativa.gencat.cat"
    sp_entity_id: str = (
        "https://elmeuescriptori.gestioeducativa.gencat.cat"
        "/authentication/saml/metadata"
    )
    sp_acs_url: str = (
        "https://elmeuescriptori.gestioeducativa.gencat.cat/authentication/saml/acs"
    )
    sp_sls_url: str = (
        "https://elmeuescriptori.gestioeducativa.gencat.cat/authentication/saml/slo"
    )
    refresh_threshold: timedelta = field(default_factory=lambda: timedelta(minutes=2))
    max_retries: int = 3
    request_timeout: timedelta = field(default_factory=lambda: timedelta(seconds=30))
    request_delay: timedelta = field(
        default_factory=lambda: timedelta(milliseconds=500)
    )
    clock_skew_tolerance: timedelta = field(
        default_factory=lambda: timedelta(minutes=5)
    )
    user_agents: list[str] = field(default_factory=lambda: list(_DEFAULT_USER_AGENTS))
