# Copyright © 2025 Mathieu Pasquet
# This file is part of slixmpp
# See the file LICENSE for copying permission.
import json
import logging
from datetime import datetime
from typing import Optional, Union
from urllib.parse import urlparse

from slixmpp import JID
from slixmpp.plugins import BasePlugin, xep_0082
from dataclasses import dataclass

log = logging.getLogger(__name__)


class AiohttpNotFound(Exception):
    pass


@dataclass
class ExternalStatus:
    planned: Optional[bool]
    beginning: datetime
    expected_end: Optional[datetime]
    message: Optional[dict[str, str]]


class XEP_0455(BasePlugin):
    """
    XEP-0455: Service Outage Status
    """
    name = 'xep_0455'
    description = 'XEP-0455: Service Outage Status'
    dependencies = {'xep_0128', 'xep_0030', 'xep_0082'}
    namespace = 'urn:xmpp:sos:0'

    async def get_external_status_addresses(self, domain: Optional[JID] = None,
                                            **iqkwargs) -> list[str]:
        """Return the list of external status addresses for this domain.

        :param domain: Domain to disco to find a service.
        """
        if domain is None:
            domain = JID(self.xmpp.boundjid.host)

        uris = []
        results = await self.xmpp['xep_0030'].get_info(jid=domain,
            **iqkwargs
        )

        disco = results.get_plugin('disco_info', check=True)
        if disco is None or 'forms' not in disco:
            return uris
        forms = disco['forms']
        if not forms:
            return uris
        field = 'external-status-addresses'
        for form in forms:
            values = form.get_values()
            if values.get('FORM_TYPE') == [self.namespace]:
                uris.extend(values.get(field, []))
        return uris

    @classmethod
    async def fetch_status(cls, addresses: Union[str, list[str]]) -> dict:
        """
        Get the external status from a list of addresses.
        Only works with http/https for now and stops on the first status
        fetched successfully.

        :param addresses: address or list of addresses to fetch the status from.
        """
        try:
            from aiohttp import ClientSession
        except ImportError:
            raise AiohttpNotFound("aiohttp was not found, unable to download statuses")

        if not isinstance(addresses, list):
            addresses = [addresses]

        async with ClientSession(headers={'User-Agent': 'slixmpp'}) as session:
            for address in addresses:
                scheme, *_ = urlparse(address)
                if not scheme in ('http', 'https'):
                    continue
                response = await session.get(address, timeout=60)
                if response.status >= 400:
                    log.debug(f'Server "{address}" answered with code {response.status}')
                    continue
                text = await response.text()
                status = cls._parse_status(text)
                if status is not None:
                    return status

    @staticmethod
    def _parse_status(raw: str) -> Optional[ExternalStatus]:
        """
        Parse a status json payload. Return None if the required (beginning)
        field is not found or not parseable.

        Returns a dataclass with the fields.
        """
        try:
            payload = json.loads(raw)
        except json.JSONDecodeError:
            log.error('Unable to parse the external status: {text}')
            return None
        if not payload:
            return None
        beginning_raw = payload.get('beginning')
        try:
            beginning = xep_0082.parse(beginning_raw)
        except (ValueError, TypeError):
            log.error(f'Bad value for beginning: "{beginning_raw}"')
            return None

        expected_end_raw = payload.get('expected_end')
        expected_end = None
        try:
            expected_end = xep_0082.parse(expected_end_raw)
        except (ValueError, TypeError):
            log.error(f'Bad value for expected end: "{expected_end_raw}"')

        planned_raw = payload.get('planned')
        planned = planned_raw if isinstance(planned_raw, bool) else None

        message_raw = payload.get('message')
        message = None
        if isinstance(message_raw, dict):
            message = {}
            for key, value in message_raw.items():
                if isinstance(key, str) and isinstance(value, str):
                    message[key] = value
        return ExternalStatus(planned, beginning, expected_end, message)
