import logging
from datetime import datetime, timedelta, timezone
from typing import List, Dict, Any
from uuid import UUID
from aiohttp import ClientSession, ClientResponseError, ClientTimeout
from pydantic import parse_obj_as
from . import settings
from .schemas import (
    IntegrationInformation,
    OAuthToken,
    TIntegrationInformation,
    DeviceState,
    OutboundConfiguration
)

logger = logging.getLogger(__name__)
logger.setLevel(settings.LOG_LEVEL)


class PortalApi:
    def __init__(self):
        self.client_id = settings.KEYCLOAK_CLIENT_ID
        self.client_secret = settings.KEYCLOAK_CLIENT_SECRET
        self.integrations_endpoint = (
            f"{settings.PORTAL_API_ENDPOINT}/integrations/inbound/configurations"
        )
        self.device_states_endpoint = (
            f"{settings.PORTAL_API_ENDPOINT}/devices/states"
        )
        self.devices_endpoint = f"{settings.PORTAL_API_ENDPOINT}/devices"

        self.oauth_token_url = settings.OAUTH_TOKEN_URL
        self.audience = settings.KEYCLOAK_AUDIENCE
        self.portal_api_endpoint = settings.PORTAL_API_ENDPOINT

        self.cached_token = None
        self.cached_token_expires_at = datetime.min.replace(tzinfo=timezone.utc)

    async def get_access_token(self, session: ClientSession) -> OAuthToken:

        if self.cached_token and self.cached_token_expires_at > datetime.now(
            tz=timezone.utc
        ):
            return self.cached_token

        logger.debug(
            f"get_access_token from {self.oauth_token_url} using client_id: {self.client_id}"
        )
        payload = {
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "audience": self.audience,
            "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket",
            "scope": "openid",
        }

        response = await session.post(
            self.oauth_token_url, data=payload, ssl=settings.CDIP_ADMIN_SSL_VERIFY
        )

        response.raise_for_status()
        token = await response.json()
        token = OAuthToken.parse_obj(token)
        self.cached_token_expires_at = datetime.now(tz=timezone.utc) + timedelta(
            seconds=token.expires_in - 15
        )  # fudge factor
        self.cached_token = token
        return token

    async def get_auth_header(self, session: ClientSession) -> dict:
        token_object = await self.get_access_token(session)
        return {
            "authorization": f"{token_object.token_type} {token_object.access_token}"
        }

    async def get_destinations(self, *args, integration_id: str = None, device_id: str = None):

        async with ClientSession() as session:

            headers = await self.get_auth_header(session)
    
            url = f'{self.portal_api_endpoint}/integrations/outbound/configurations'
            response = await session.get(url, params={'inbound_id': integration_id, 'device_id': device_id}, headers=headers)

            if response.ok:

                data = await response.json()

                return [OutboundConfiguration.parse_obj(item) for item in data]

            return []

    async def get_authorized_integrations(
        self,
        session: ClientSession,
        t_int_info: TIntegrationInformation = IntegrationInformation,
    ) -> List[IntegrationInformation]:
        logger.debug(
            f"get_authorized_integrations for : {settings.KEYCLOAK_CLIENT_ID}"
        )
        headers = await self.get_auth_header(session)

        logger.debug(f"url: {self.integrations_endpoint}")
        response = await session.get(
            url=self.integrations_endpoint,
            headers=headers,
            ssl=settings.CDIP_ADMIN_SSL_VERIFY,
        )
        response.raise_for_status()
        json_response = await response.json()

        if isinstance(json_response, dict):
            json_response = [json_response]

        logger.debug(
            f"Got {len(json_response)} integrations for {settings.KEYCLOAK_CLIENT_ID}"
        )
        return [t_int_info.parse_obj(r) for r in json_response]

    async def update_state(
        self, session: ClientSession, integration_info: IntegrationInformation
    ):
        async with ClientSession() as sess:
            headers = await self.get_auth_header(sess)

            response = await sess.put(
                url=f"{self.integrations_endpoint}/{integration_info.id}",
                headers=headers,
                json=dict(state=integration_info.state),
                ssl=settings.CDIP_ADMIN_SSL_VERIFY,
            )
            logger.info(f"update integration state resp: {response.status}")
            response.raise_for_status()

            return await self.update_states_with_dict(
                sess, integration_info.id, integration_info.device_states
            )

    async def fetch_device_states(self, session: ClientSession, inbound_id: UUID):
        try:
            headers = await self.get_auth_header(session)

            # This ought to be quick so just do it straight away.
            response = await session.get(
                url=f"{self.device_states_endpoint}/",
                params={"inbound_config_id": str(inbound_id)},
                headers=headers,
                timeout=ClientTimeout(sock_connect=3.1, sock_read=10),
                verify=settings.CDIP_ADMIN_SSL_VERIFY,
            )
            if response.status == 200:
                result = response.json()
        except ClientResponseError as ex:
            logger.exception("Failed to get devices for iic: {inbound_id}")
        else:
            states_received = parse_obj_as(List[DeviceState], result)
            # todo: cleanup after all functions have their device state migrated over
            states_asdict = {}
            for s in states_received:
                if isinstance(s.state, dict) and "value" in s.state:
                    states_asdict[s.device_external_id] = s.state.get("value")
                else:
                    states_asdict[s.device_external_id] = s.state
            return states_asdict
            # return {s.device_external_id: s.state for s in states_received}

    async def ensure_device(
        self, session: ClientSession, inbound_id: UUID, external_id: str
    ):
        # Post device ID and Integration ID combination to ensure it exists
        # in the Portal's database and is also in the Integration's default
        # device group.
        headers = await self.get_auth_header(session)
        payload = {"external_id": external_id, "inbound_configuration": inbound_id}
        response = await session.post(
            url=self.devices_endpoint,
            json=payload,
            headers=headers,
            ssl=settings.CDIP_ADMIN_SSL_VERIFY,
        )
        resp = await response.json()
        # print(resp)
        if response.ok:
            return resp
        else:
            logger.error(
                "Failed to post device to portal.",
                extra={**payload, "response": str(response)},
            )

        return None

    async def update_states_with_dict(
        self, session: ClientSession, inbound_id: UUID, states_dict: Dict[str, Any]
    ):
        headers = await self.get_auth_header(session)
        response = await session.post(
            url=f"{self.device_states_endpoint}/update/{inbound_id}",
            headers=headers,
            json=states_dict,
            ssl=settings.CDIP_ADMIN_SSL_VERIFY,
        )
        response.raise_for_status()
        text = await response.text()
        logger.info(f"update device_states resp: {response.status}")
        return text

    async def _get(self, session: ClientSession, url: str, params=None):
        headers = await self.get_auth_header(session)
        response = await session.get(
            url=url,
            headers=headers,
            params=params,
            ssl=settings.CDIP_ADMIN_SSL_VERIFY,
        )
        response.raise_for_status()
        return await response.json()

    async def get_bridge_integration(self, session: ClientSession, bridge_id: str):
        return await self._get(
            session=session,
            url=f"{settings.PORTAL_API_ENDPOINT}/integrations/bridges/{bridge_id}",
        )

    async def get_inbound_integration(self, session: ClientSession, integration_id: str):
        return await self._get(
            session=session,
            url=f"{settings.PORTAL_API_ENDPOINT}/integrations/inbound/configurations/{integration_id}",
        )

    async def get_outbound_integration(self, session: ClientSession, integration_id: str):
        return await self._get(
            session=session,
            url=f"{settings.PORTAL_API_ENDPOINT}/integrations/outbound/configurations/{integration_id}",
        )

    async def get_outbound_integration_list(self, session: ClientSession, **query_params):
        return await self._get(
            session=session,
            url=f"{settings.PORTAL_API_ENDPOINT}/integrations/outbound/configurations",
            params=query_params
        )
