
import asyncio
import re
import json
from typing import Dict, List, Optional, cast

import aiohttp
from tenacity import (
    retry,
    wait_exponential,
    stop_after_delay,
    retry_if_exception_type,
)


from .api import WebApi
from .models import LiveStatus, RoomInfo, UserInfo
from .typing import QualityNumber, StreamFormat, ResponseData
from .exceptions import (
    LiveRoomHidden, LiveRoomLocked, LiveRoomEncrypted, NoStreamUrlAvailable
)


__all__ = 'Live',


_INFO_PATTERN = re.compile(
    rb'<script>\s*window\.__NEPTUNE_IS_MY_WAIFU__\s*=\s*(\{.*?\})\s*</script>'
)
_LIVE_STATUS_PATTERN = re.compile(rb'"live_status"\s*:\s*(\d)')


class Live:
    def __init__(
        self, room_id: int, user_agent: str = '', cookie: str = ''
    ) -> None:
        self._room_id = room_id
        self._user_agent = user_agent
        self._cookie = cookie
        self._html_page_url = f'https://live.bilibili.com/{room_id}'

        self._room_info: RoomInfo
        self._user_info: UserInfo

    @property
    def user_agent(self) -> str:
        return self._user_agent

    @user_agent.setter
    def user_agent(self, value: str) -> None:
        self._user_agent = value

    @property
    def cookie(self) -> str:
        return self._cookie

    @cookie.setter
    def cookie(self, value: str) -> None:
        self._cookie = value

    @property
    def headers(self) -> Dict[str, str]:
        return {
            'Referer': 'https://live.bilibili.com/',
            'Connection': 'Keep-Alive',
            'User-Agent': self._user_agent,
            'cookie': self._cookie,
        }

    @property
    def session(self) -> aiohttp.ClientSession:
        return self._session

    @property
    def room_id(self) -> int:
        return self._room_id

    @property
    def room_info(self) -> RoomInfo:
        return self._room_info

    @property
    def user_info(self) -> UserInfo:
        return self._user_info

    async def init(self) -> None:
        self._session = aiohttp.ClientSession(
            headers=self.headers,
            raise_for_status=True,
            trust_env=True,
        )
        self._api = WebApi(self._session)

        self._room_info = await self.get_room_info()
        self._user_info = await self.get_user_info(self._room_info.uid)

    async def deinit(self) -> None:
        await self._session.close()

    async def get_live_status(self) -> LiveStatus:
        try:
            # frequent requests will be intercepted by the server's firewall!
            live_status = await self._get_live_status_via_api()
        except Exception:
            # more cpu consumption
            live_status = await self._get_live_status_via_html_page()

        return LiveStatus(live_status)

    def is_living(self) -> bool:
        return self._room_info.live_status == LiveStatus.LIVE

    async def update_info(self) -> None:
        await asyncio.wait([self.update_user_info(), self.update_room_info()])

    @retry(
        reraise=True,
        retry=retry_if_exception_type((
            asyncio.TimeoutError, aiohttp.ClientError,
        )),
        wait=wait_exponential(max=10),
        stop=stop_after_delay(60),
    )
    async def update_user_info(self) -> None:
        self._user_info = await self.get_user_info(self._room_info.uid)

    @retry(
        reraise=True,
        retry=retry_if_exception_type((
            asyncio.TimeoutError, aiohttp.ClientError,
        )),
        wait=wait_exponential(max=10),
        stop=stop_after_delay(60),
    )
    async def update_room_info(self) -> None:
        self._room_info = await self.get_room_info()

    async def get_room_info(self) -> RoomInfo:
        try:
            # frequent requests will be intercepted by the server's firewall!
            room_info_data = await self._get_room_info_via_api()
        except Exception:
            # more cpu consumption
            room_info_data = await self._get_room_info_via_html_page()

        return RoomInfo.from_data(room_info_data)

    async def get_user_info(self, uid: int) -> UserInfo:
        user_info_data = await self._api.get_user_info(uid)
        return UserInfo.from_data(user_info_data)

    async def get_server_timestamp(self) -> int:
        # the timestamp on the server at the moment in seconds
        return await self._api.get_timestamp()

    async def get_live_stream_url(
        self, qn: QualityNumber = 10000, format: StreamFormat = 'flv'
    ) -> Optional[str]:
        try:
            data = await self._api.get_room_play_info(self._room_id, qn)
        except Exception:
            # fallback to the html page global info
            data = await self._get_room_play_info_via_html_page()
            self._check_room_play_info(data)
            stream = data['playurl_info']['playurl']['stream']
            if stream[0]['format'][0]['codec'][0]['current_qn'] != qn:
                raise
        else:
            self._check_room_play_info(data)

        streams = list(filter(
            lambda s: s['format'][0]['format_name'] == format,
            data['playurl_info']['playurl']['stream']
        ))
        codec = streams[0]['format'][0]['codec'][0]
        accept_qn = cast(List[QualityNumber], codec['accept_qn'])

        if qn not in accept_qn:
            return None

        assert codec['current_qn'] == qn
        url_info = codec['url_info'][0]

        return url_info['host'] + codec['base_url'] + url_info['extra']

    def _check_room_play_info(self, data: ResponseData) -> None:
        if data['is_hidden']:
            raise LiveRoomHidden()
        if data['is_locked']:
            raise LiveRoomLocked()
        if data['encrypted'] and not data['pwd_verified']:
            raise LiveRoomEncrypted()
        try:
            data['playurl_info']['playurl']['stream'][0]
        except Exception:
            raise NoStreamUrlAvailable()

    async def _get_live_status_via_api(self) -> int:
        room_info_data = await self._get_room_info_via_api()
        return int(room_info_data['live_status'])

    async def _get_room_info_via_api(self) -> ResponseData:
        try:
            room_info_data = await self._api.get_info(self._room_id)
        except Exception:
            info_data = await self._api.get_info_by_room(self._room_id)
            room_info_data = info_data['room_info']

        return room_info_data

    async def _get_live_status_via_html_page(self) -> int:
        async with self._session.get(self._html_page_url) as response:
            data = await response.read()

        m = _LIVE_STATUS_PATTERN.search(data)
        assert m is not None, data

        return int(m.group(1))

    async def _get_room_info_via_html_page(self) -> ResponseData:
        info_res = await self._get_room_info_res_via_html_page()
        return info_res['room_info']

    async def _get_room_play_info_via_html_page(self) -> ResponseData:
        return await self._get_room_init_res_via_html_page()

    async def _get_room_info_res_via_html_page(self) -> ResponseData:
        info = await self._get_info_via_html_page()
        if info['roomInfoRes']['code'] != 0:
            raise ValueError(f"Invaild roomInfoRes: {info['roomInfoRes']}")
        return info['roomInfoRes']['data']

    async def _get_room_init_res_via_html_page(self) -> ResponseData:
        info = await self._get_info_via_html_page()
        if info['roomInitRes']['code'] != 0:
            raise ValueError(f"Invaild roomInitRes: {info['roomInitRes']}")
        return info['roomInitRes']['data']

    async def _get_info_via_html_page(self) -> ResponseData:
        async with self._session.get(self._html_page_url) as response:
            data = await response.read()

        m = _INFO_PATTERN.search(data)
        assert m is not None, data

        string = m.group(1).decode(encoding='utf8')
        return json.loads(string)
