# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from typing import Self, cast

from keystoneauth1 import adapter

from openstack import _log
from openstack.baremetal.v1 import _common
from openstack import exceptions
from openstack import resource
from openstack import utils


_logger = _log.setup_logging('openstack')


class Introspection(resource.Resource):
    resources_key = 'introspection'
    base_path = '/introspection'

    # capabilities
    allow_create = True
    allow_fetch = True
    allow_commit = False
    allow_delete = True
    allow_list = True

    create_opts = resource.CreateOpts(requires_id=True)
    create_returns_body = False

    #: Timestamp at which the introspection was finished.
    finished_at = resource.Body('finished_at')
    #: The last error message (if any).
    error = resource.Body('error')
    #: The UUID of the introspection (matches the node UUID).
    id = resource.Body('uuid', alternate_id=True)
    #: Whether introspection is finished.
    is_finished = resource.Body('finished', type=bool)
    #: A list of relative links, including the self and bookmark links.
    links = resource.Body('links', type=list)
    #: Timestamp at which the introspection was started.
    started_at = resource.Body('started_at')
    #: The current introspection state.
    state = resource.Body('state')

    def abort(self, session: adapter.Adapter) -> None:
        """Abort introspection.

        :param session: The session to use for making this request.
        """
        if self.is_finished:
            return

        session = self._get_session(session)

        version = self._get_microversion(session)
        request = self._prepare_request(requires_id=True)
        request.url = utils.urljoin(request.url, 'abort')
        response = session.post(
            request.url,
            headers=request.headers,
            microversion=version,
            retriable_status_codes=_common.RETRIABLE_STATUS_CODES,
        )
        msg = f"Failed to abort introspection for node {self.id}"
        exceptions.raise_from_response(response, error_message=msg)

    def get_data(
        self,
        session: adapter.Adapter,
        processed: bool = True,
    ) -> dict[str, object]:
        """Get introspection data.

        Note that the introspection data format is not stable and can vary
        from environment to environment.

        :param session: The session to use for making this request.
        :param processed: Whether to fetch the final processed data (the
            default) or the raw unprocessed data as received from the ramdisk.
        :returns: introspection data from the most recent successful run.
        """
        session = self._get_session(session)

        version = self._get_microversion(session) if processed else '1.17'
        request = self._prepare_request(requires_id=True)
        request.url = utils.urljoin(request.url, 'data')
        if not processed:
            request.url = utils.urljoin(request.url, 'unprocessed')
        response = session.get(
            request.url, headers=request.headers, microversion=version
        )
        msg = f"Failed to fetch introspection data for node {self.id}"
        exceptions.raise_from_response(response, error_message=msg)
        return cast(dict[str, object], response.json())

    def wait(
        self,
        session: adapter.Adapter,
        timeout: int | float | None = None,
        ignore_error: bool = False,
    ) -> Self:
        """Wait for the node to reach the expected state.

        :param session: The session to use for making this request.
        :param timeout: How much (in seconds) to wait for the introspection.
            The value of ``None`` (the default) means no client-side timeout.
        :param ignore_error: If ``True``, this call will raise an exception
            if the introspection reaches the ``error`` state. Otherwise the
            error state is considered successful and the call returns.
        :return: This :class:`Introspection` instance.
        :raises: :class:`~openstack.exceptions.ResourceFailure` if
            introspection fails and ``ignore_error`` is ``False``.
        :raises: :class:`~openstack.exceptions.ResourceTimeout` on timeout.
        """
        if self._check_state(ignore_error):
            return self

        for count in utils.iterate_timeout(
            timeout, f"Timeout waiting for introspection on node {self.id}"
        ):
            self.fetch(session)
            if self._check_state(ignore_error):
                return self

            _logger.debug(
                'Still waiting for introspection of node %(node)s, '
                'the current state is "%(state)s"',
                {'node': self.id, 'state': self.state},
            )

        assert False, 'unreachable'

    def _check_state(self, ignore_error: bool) -> bool:
        if self.state == 'error' and not ignore_error:
            raise exceptions.ResourceFailure(
                f"Introspection of node {self.id} failed: {self.error}"
            )
        return cast(bool, self.is_finished)
