"""Parser for the AdmInfo XML format returned by management jobs such as mng.GetUserList."""

from __future__ import annotations

import xml.etree.ElementTree as ET
from dataclasses import dataclass, field
from datetime import datetime, timezone


@dataclass(frozen=True)
class ECMUser:
    """A user account returned by ``mng.GetUserList``.

    Attributes:
        id: Numeric user ID assigned by the server.
        username: Short internal user name (``benutzer`` attribute).
        login_name: Login credential name used for authentication.
        display_name: Full display name of the user.
        email: E-mail address of the user.
        guid: Globally unique identifier of the user account.
        profile_id: ID of the user profile; ``-1`` = no profile, ``0`` = user
            profile, ``>0`` = ID of the assigned profile.
        locked: ``True`` if the account is locked and the user cannot log in.
        remark: Optional remark from the user configuration.
        valid_from: Datetime from which the account is valid; ``None`` if not set.
        valid_to: Datetime until which the account is valid; ``None`` if not set.
        groups: List of group names the user belongs to.  Populated only when
            ``users`` is called with ``extended_info=True``.
    """

    id: int
    username: str
    login_name: str
    display_name: str
    email: str
    guid: str
    profile_id: int
    locked: bool
    remark: str
    valid_from: datetime | None
    valid_to: datetime | None
    groups: list[str] = field(default_factory=list)


@dataclass(frozen=True)  # pylint: disable=too-many-instance-attributes
class ECMUserAttributes:
    """Detailed attributes of a single user account returned by ``mng.GetUserAttributes``.

    Attributes:
        id: Numeric user ID assigned by the server.
        username: Short internal user name (``benutzer`` attribute).
        login_name: Login credential name used for authentication.
        display_name: Full display name of the user.
        email: E-mail address of the user.
        guid: Globally unique identifier of the user account.
        profile_id: ID of the user profile; ``-1`` = no profile.
        locked: ``True`` if the account is locked and the user cannot log in.
        remark: Optional remark from the user configuration.
        account_type: Account type identifier.
        flags: Bit-field of user flags.
        lang_id: Language ID assigned to the user; ``0`` = server default.
        login_count: Number of times the user has logged in.
        login_station: Name of the station from the last login.
        login_time: Timestamp of the last login; ``None`` if the user has never logged in.
        last_modified: Timestamp of the last account modification; ``None`` if never modified.
        mfa_flag: Multi-factor authentication flag.
        never_expire: ``True`` if the account never expires regardless of ``valid_to``.
        change_pwd: ``True`` if the user is required to change their password at next login.
        pwd_changed: Timestamp of the last password change; ``None`` if the password has never been changed.
        server_id: ID of the home server for this account.
        station: Default station name assigned to the user.
        supervisor_id: User ID of the supervisor; ``-1`` if no supervisor is assigned.
        valid_from: Datetime from which the account is valid; ``None`` if not set.
        valid_to: Datetime until which the account is valid; ``None`` if not set.
    """

    id: int
    username: str
    login_name: str
    display_name: str
    email: str
    guid: str
    profile_id: int
    locked: bool
    remark: str
    account_type: int
    flags: int
    lang_id: int
    login_count: int
    login_station: str
    login_time: datetime | None
    last_modified: datetime | None
    mfa_flag: int
    never_expire: bool
    change_pwd: bool
    pwd_changed: datetime | None
    server_id: int
    station: str
    supervisor_id: int
    valid_from: datetime | None
    valid_to: datetime | None


@dataclass(frozen=True)
class ECMGroup:
    """A user group returned by ``mng.GetUserGroups``.

    Attributes:
        id: Numeric group ID assigned by the server.
        name: Name of the group.
        guid: Globally unique identifier of the group.
        profile_user_id: ID of the profile user assigned to the group;
            ``-1`` if no profile user is assigned.
        description: Optional description of the group.
    """

    id: int
    name: str
    guid: str
    profile_user_id: int
    description: str


class ECMAdmInfoParser:
    """Parser for the ``<AdmInfo>`` XML format.

    Used by management jobs such as ``mng.GetUserList`` and ``mng.GetUserGroups``.
    Accepts either a UTF-8 encoded byte string or a plain string.

    Args:
        xml: The raw XML payload — either ``bytes`` (UTF-8) or ``str``.
        extended_info: When ``True``, the parser also reads ``bemerkung``,
            ``validfrom``, ``validto`` and ``<Groups>`` child elements from each
            ``<User>`` node.  Pass the same value that was used for the
            ``ExtendedInfo`` server parameter.
    """

    def __init__(self, xml: str | bytes, *, extended_info: bool = False) -> None:
        if isinstance(xml, bytes):
            xml = xml.decode("utf-8")
        self._node = ET.fromstring(xml)
        self._extended_info = extended_info

    @staticmethod
    def _parse_datetime(value: str | None) -> datetime | None:
        if not value:
            return None
        return datetime.strptime(value, "%Y/%m/%d %H:%M:%S")

    @staticmethod
    def _parse_unix_timestamp(value: str | None) -> datetime | None:
        if not value:
            return None
        try:
            ts = int(value)
        except ValueError:
            return None
        return datetime.fromtimestamp(ts, tz=timezone.utc) if ts else None

    def users(self) -> list[ECMUser]:
        """Parse and return all ``<User>`` entries from the XML.

        Returns:
            A list of :class:`ECMUser` instances.  Returns an empty list when
            no users are present in the response.
        """
        result = []
        for node in self._node.findall("Users/User"):
            groups = [g.get("name", "") for g in node.findall("Groups/Group")] if self._extended_info else []
            result.append(
                ECMUser(
                    id=int(node.get("id", "-1")),
                    username=node.get("benutzer", ""),
                    login_name=node.get("loginname") or node.get("loginName", ""),
                    display_name=node.get("name", ""),
                    email=node.get("osemail", ""),
                    guid=node.get("osguid", ""),
                    profile_id=int(node.get("profil", "-1")),
                    locked=node.get("locked", "0") == "1",
                    remark=node.get("bemerkung", "") if self._extended_info else "",
                    valid_from=self._parse_datetime(node.get("validfrom")) if self._extended_info else None,
                    valid_to=self._parse_datetime(node.get("validto")) if self._extended_info else None,
                    groups=groups,
                )
            )
        return result

    def user_attributes(self) -> ECMUserAttributes | None:
        """Parse and return the single ``<User>`` entry from a ``mng.GetUserAttributes`` response.

        Returns:
            An :class:`ECMUserAttributes` instance, or ``None`` when the XML contains
            no ``<User>`` node.
        """
        node = self._node.find("Users/User")
        if node is None:
            return None
        return ECMUserAttributes(
            id=int(node.get("id", "-1")),
            username=node.get("benutzer", ""),
            login_name=node.get("loginname") or node.get("loginName", ""),
            display_name=node.get("name", ""),
            email=node.get("osemail", ""),
            guid=node.get("osguid", ""),
            profile_id=int(node.get("profil", "-1")),
            locked=node.get("locked", "0") == "1",
            remark=node.get("bemerkung", ""),
            account_type=int(node.get("account_type", "0")),
            flags=int(node.get("flags", "0")),
            lang_id=int(node.get("langid", "0")),
            login_count=int(node.get("logincount", "0")),
            login_station=node.get("loginstation", ""),
            login_time=self._parse_unix_timestamp(node.get("logintime")),
            last_modified=self._parse_unix_timestamp(node.get("geaendert")),
            mfa_flag=int(node.get("mfauthflag", "0")),
            never_expire=node.get("never_expire", "0") == "1",
            change_pwd=node.get("changepwd", "0") == "1",
            pwd_changed=self._parse_unix_timestamp(node.get("pwd_changed")),
            server_id=int(node.get("server_id", "0")),
            station=node.get("station", ""),
            supervisor_id=int(node.get("supervisor", "-1")),
            valid_from=self._parse_datetime(node.get("validfrom")),
            valid_to=self._parse_datetime(node.get("validto")),
        )

    def groups(self) -> list[ECMGroup]:
        """Parse and return all ``<Group>`` entries from the XML.

        Returns:
            A list of :class:`ECMGroup` instances.  Returns an empty list when
            no groups are present in the response.
        """
        return [
            ECMGroup(
                id=int(node.get("id", "-1")),
                name=node.get("name", ""),
                guid=node.get("osguid", ""),
                profile_user_id=int(node.get("profil", "-1")),
                description=node.get("description", ""),
            )
            for node in self._node.findall("Groups/Group")
        ]

    def group_attributes(self) -> ECMGroup | None:
        """Parse and return the single ``<Group>`` entry from a ``mng.GetGroupAttributes`` response.

        Returns:
            An :class:`ECMGroup` instance, or ``None`` when the XML contains no ``<Group>`` node.
        """
        groups = self.groups()
        return groups[0] if groups else None
