import logging
logger = logging.getLogger("mlbstreamer")
import os
import re
import base64
import binascii
import json
import sqlite3
import pickle
import functools
import random
import string
from contextlib import contextmanager

import six
from six.moves.http_cookiejar import LWPCookieJar, Cookie
from six import StringIO
import requests
from requests_toolbelt.utils import dump
import lxml
import lxml, lxml.etree
import yaml
from orderedattrdict import AttrDict
import orderedattrdict.yamlutils
from orderedattrdict.yamlutils import AttrDictYAMLLoader
import pytz
from datetime import datetime, timedelta
import dateutil.parser

from . import config
from . import state
from .state import memo
from .exceptions import *

USER_AGENT = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:56.0) "
              "Gecko/20100101 Firefox/56.0.4")

# Default cache duration to 60 seconds
CACHE_DURATION_SHORT = 60 # 60 seconds
CACHE_DURATION_MEDIUM = 60*60*24 # 1 day
CACHE_DURATION_LONG = 60*60*24*30  # 30 days
CACHE_DURATION_DEFAULT = CACHE_DURATION_SHORT

CACHE_FILE=os.path.join(config.CONFIG_DIR, "cache.sqlite")

def gen_random_string(n):
    return ''.join(
        random.choice(
            string.ascii_uppercase + string.digits
        ) for _ in range(64)
    )


class Media(AttrDict):
    pass


class Stream(AttrDict):
    pass

class StreamSession(object):
    """
    Top-level stream session interface

    Individual stream providers can be implemented by inheriting from this class
    and implementing methods for login flow, getting streams, etc.
    """


    # SESSION_FILE=os.path.join(config.CONFIG_DIR, "session")

    HEADERS = {
        "User-agent": USER_AGENT
    }

    def __init__(
            self,
            username, password,
            proxies=None,
            no_cache=False,
            *args, **kwargs
    ):

        self.session = requests.Session()
        self.cookies = LWPCookieJar()
        if not os.path.exists(self.COOKIES_FILE):
            self.cookies.save(self.COOKIES_FILE)
        self.cookies.load(self.COOKIES_FILE, ignore_discard=True)
        self.session.headers = self.HEADERS
        self._state = AttrDict([
            ("username", username),
            ("password", password),
            ("proxies", proxies)
        ])
        self.no_cache = no_cache
        self._cache_responses = False
        if not os.path.exists(CACHE_FILE):
            self.cache_setup(CACHE_FILE)
        self.conn = sqlite3.connect(CACHE_FILE,
                                    detect_types = sqlite3.PARSE_DECLTYPES)
        self.cursor = self.conn.cursor()
        self.cache_purge()
        # if not self.logged_in:
        self.login()
        # logger.debug("already logged in")
            # return



    @classmethod
    def session_type(cls):
        return cls.__name__.replace("StreamSession", "").lower()

    @classmethod
    def _COOKIES_FILE(cls):
        return os.path.join(config.CONFIG_DIR, f"{cls.session_type()}.cookies")

    @property
    def COOKIES_FILE(self):
        return self._COOKIES_FILE()

    @classmethod
    def _SESSION_FILE(cls):
        return os.path.join(config.CONFIG_DIR, f"{cls.session_type()}.session")

    @property
    def SESSION_FILE(self):
        return self._SESSION_FILE()

    @classmethod
    def new(cls, **kwargs):
        try:
            return cls.load(**kwargs)
        except FileNotFoundError:
            logger.trace(f"creating new session: {kwargs}")
            provider = config.settings.profile.providers.get(cls.session_type())
            return cls(username=provider.username,
                       password=provider.password,
                       **kwargs)

    @property
    def cookies(self):
        return self.session.cookies

    @cookies.setter
    def cookies(self, value):
        self.session.cookies = value

    @classmethod
    def destroy(cls):
        if os.path.exists(cls.COOKIES_FILE):
            os.remove(cls.COOKIES_FILE)
        if os.path.exists(cls.SESSION_FILE):
            os.remove(cls.SESSION_FILE)

    @classmethod
    def load(cls, *args, **kwargs):
        state = yaml.load(open(cls._SESSION_FILE()), Loader=AttrDictYAMLLoader)
        logger.trace(f"load: {cls.__name__}, {state}")
        return cls(**state)

    def save(self):
        logger.trace(f"load: {self.__class__.__name__}, {self._state}")
        with open(self.SESSION_FILE, 'w') as outfile:
            yaml.dump(self._state, outfile, default_flow_style=False)
        self.cookies.save(self.COOKIES_FILE)


    def get_cookie(self, name):
        return requests.utils.dict_from_cookiejar(self.cookies).get(name)

    def __getattr__(self, attr):
        if attr in ["delete", "get", "head", "options", "post", "put", "patch"]:
            # return getattr(self.session, attr)
            session_method = getattr(self.session, attr)
            return functools.partial(self.request, session_method)
        raise AttributeError(attr)

    def request(self, method, url, *args, **kwargs):

        response = None
        use_cache = not self.no_cache and self._cache_responses
        if use_cache:
            logger.debug("getting cached response fsesor %s" %(url))
            self.cursor.execute(
                "SELECT response, last_seen "
                "FROM response_cache "
                "WHERE url = ?",
                (url,)
            )
            try:
                (pickled_response, last_seen) = self.cursor.fetchone()
                td = datetime.now() - last_seen
                if td.seconds >= self._cache_responses:
                    logger.debug("cache expired for %s" %(url))
                else:
                    response = pickle.loads(pickled_response)
                    logger.debug("using cached response for %s" %(url))
            except TypeError:
                logger.debug("no cached response for %s" %(url))

        # if not response:
        #     response = method(url, *args, **kwargs)
        #     logger.trace(dump.dump_all(response).decode("utf-8"))
        if use_cache:
            pickled_response = pickle.dumps(response)
            sql="""INSERT OR REPLACE
            INTO response_cache (url, response, last_seen)
            VALUES (?, ?, ?)"""
            self.cursor.execute(
                sql,
                (url, pickled_response, datetime.now())
            )
            self.conn.commit()

        return response

    @property
    def username(self):
        return self._state.username

    @property
    def password(self):
        return self._state.password

    @property
    def proxies(self):
        return self._state.proxies

    @property
    def headers(self):
        return []

    @proxies.setter
    def proxies(self, value):
        # Override proxy environment variables if proxies are defined on session
        if value is not None:
            self.session.trust_env = (len(value) == 0)
        self._state.proxies = value
        self.session.proxies.update(value)


    @contextmanager
    def cache_responses(self, duration=CACHE_DURATION_DEFAULT):
        self._cache_responses = duration
        try:
            yield
        finally:
            self._cache_responses = False

    def cache_responses_short(self):
        return self.cache_responses(CACHE_DURATION_SHORT)

    def cache_responses_medium(self):
        return self.cache_responses(CACHE_DURATION_MEDIUM)

    def cache_responses_long(self):
        return self.cache_responses(CACHE_DURATION_LONG)

    def cache_setup(self, dbfile):

        conn = sqlite3.connect(dbfile)
        c = conn.cursor()
        c.execute('''
        CREATE TABLE response_cache
        (url TEXT,
        response TEXT,
        last_seen TIMESTAMP DEFAULT (datetime('now','localtime')),
        PRIMARY KEY (url))''');
        conn.commit()
        c.close()

    def cache_purge(self, days=CACHE_DURATION_LONG):

        self.cursor.execute(
            "DELETE "
            "FROM response_cache "
            "WHERE last_seen < datetime('now', '-%d days')" %(days)
        )

class BAMStreamSessionMixin(object):
    """
    StreamSession subclass for BAMTech Media stream providers, which currently
    includes MLB.tv and NHL.tv
    """
    sport_id = 1 # FIXME

    @memo(region="short")
    def schedule(
            self,
            # sport_id=None,
            start=None,
            end=None,
            game_type=None,
            team_id=None,
            game_id=None,
    ):

        logger.debug(
            "getting schedule: %s, %s, %s, %s, %s, %s" %(
                self.sport_id,
                start,
                end,
                game_type,
                team_id,
                game_id
            )
        )
        url = self.SCHEDULE_TEMPLATE.format(
            sport_id = self.sport_id,
            start = start.strftime("%Y-%m-%d") if start else "",
            end = end.strftime("%Y-%m-%d") if end else "",
            game_type = game_type if game_type else "",
            team_id = team_id if team_id else "",
            game_id = game_id if game_id else ""
        )
        with self.cache_responses_short():
            return self.session.get(url).json()

    @memo(region="short")
    def get_epgs(self, game_id, title=None):

        schedule = self.schedule(game_id=game_id)
        try:
            # Get last date for games that have been rescheduled to a later date
            game = schedule["dates"][-1]["games"][0]
        except KeyError:
            logger.debug("no game data")
            return
        epgs = game["content"]["media"]["epg"]

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

        return [ e for e in epgs if (not title) or title == e["title"] ]

    def get_media(self,
                  game_id,
                  media_id=None,
                  title=None,
                  preferred_stream=None,
                  call_letters=None):

        logger.debug(f"geting media for game {game_id} ({media_id}, {title}, {call_letters})")

        epgs = self.get_epgs(game_id, title)
        for epg in epgs:
            for item in epg["items"]:
                if (not preferred_stream
                    or (item.get("mediaFeedType", "").lower() == preferred_stream)
                ) and (
                    not call_letters
                    or (item.get("callLetters", "").lower() == call_letters)
                ) and (
                    not media_id
                    or (item.get("mediaId", "").lower() == media_id)
                ):
                    logger.debug("found preferred stream")
                    yield Media(item)
            else:
                if len(epg["items"]):
                    logger.debug("using non-preferred stream")
                    yield Media(epg["items"][0])
        # raise StopIteration



class MLBStreamSession(BAMStreamSessionMixin, StreamSession):

    SCHEDULE_TEMPLATE = (
        "http://statsapi.mlb.com/api/v1/schedule"
        "?sportId={sport_id}&startDate={start}&endDate={end}"
        "&gameType={game_type}&gamePk={game_id}"
        "&teamId={team_id}"
        "&hydrate=linescore,team,game(content(summary,media(epg)),tickets)"
    )

    PLATFORM = "macintosh"

    BAM_SDK_VERSION = "3.4"

    MLB_API_KEY_URL = "https://www.mlb.com/tv/g490865/"

    API_KEY_RE = re.compile(r'"apiKey":"([^"]+)"')

    CLIENT_API_KEY_RE = re.compile(r'"clientApiKey":"([^"]+)"')

    OKTA_CLIENT_ID_RE = re.compile("""production:{clientId:"([^"]+)",""")

    MLB_OKTA_URL = "https://www.mlbstatic.com/mlb.com/vendor/mlb-okta/mlb-okta.js"

    AUTHN_URL = "https://ids.mlb.com/api/v1/authn"

    AUTHZ_URL = "https://ids.mlb.com/oauth2/aus1m088yK07noBfh356/v1/authorize"

    BAM_DEVICES_URL = "https://us.edge.bamgrid.com/devices"

    BAM_SESSION_URL = "https://us.edge.bamgrid.com/session"

    BAM_TOKEN_URL = "https://us.edge.bamgrid.com/token"

    BAM_ENTITLEMENT_URL = "https://media-entitlement.mlb.com/api/v3/jwt"

    GAME_CONTENT_URL_TEMPLATE="http://statsapi.mlb.com/api/v1/game/{game_id}/content"

    STREAM_URL_TEMPLATE="https://edge.svcs.mlb.com/media/{media_id}/scenarios/browser~csai"

    AIRINGS_URL_TEMPLATE=(
        "https://search-api-mlbtv.mlb.com/svc/search/v2/graphql/persisted/query/"
        "core/Airings?variables={{%22partnerProgramIds%22%3A[%22{game_id}%22]}}"
    )

    RESOLUTIONS = AttrDict([
        ("720p", "720p_alt"),
        ("720p@30", "720p"),
        ("540p", "540p"),
        ("504p", "504p"),
        ("360p", "360p"),
        ("288p", "288p"),
        ("224p", "224p")
    ])

    def __init__(
            self,
            username, password,
            api_key=None,
            client_api_key=None,
            okta_client_id=None,
            session_token=None,
            access_token=None,
            access_token_expiry=None,
            *args, **kwargs
    ):
        super(MLBStreamSession, self).__init__(
            username, password,
            *args, **kwargs
        )
        self._state.api_key = api_key
        self._state.client_api_key = client_api_key
        self._state.okta_client_id = okta_client_id
        self._state.session_token = session_token
        self._state.access_token = access_token
        self._state.access_token_expiry = access_token_expiry


    def login(self):

        AUTHN_PARAMS = {
            "username": self.username,
            "password": self.password,
            "options": {
                "multiOptionalFactorEnroll": False,
                "warnBeforePasswordExpired": True
            }
        }
        authn_response = self.session.post(
            self.AUTHN_URL, json=AUTHN_PARAMS
        ).json()
        self.session_token = authn_response["sessionToken"]

        # logger.debug("logged in: %s" %(self.ipid))
        self.save()

    @property
    def headers(self):

        return {
            "Authorization": self.access_token
        }


    @property
    def ipid(self):
        return self.get_cookie("ipid")

    @property
    def fingerprint(self):
        return self.get_cookie("fprt")

    @property
    def api_key(self):

        if not self._state.get("api_key"):
            self.update_api_keys()
        return self._state.api_key

    @property
    def client_api_key(self):

        if not self._state.get("client_api_key"):
            self.update_api_keys()
        return self._state.client_api_key

    @property
    def okta_client_id(self):

        if not self._state.get("okta_client_id"):
            self.update_api_keys()
        return self._state.okta_client_id

    def update_api_keys(self):

        logger.debug("updating MLB api keys")
        content = self.session.get(self.MLB_API_KEY_URL).text
        parser = lxml.etree.HTMLParser()
        data = lxml.etree.parse(StringIO(content), parser)

        scripts = data.xpath(".//script")
        for script in scripts:
            if script.text and "apiKey" in script.text:
                self._state.api_key = self.API_KEY_RE.search(script.text).groups()[0]
            if script.text and "clientApiKey" in script.text:
                self._state.client_api_key = self.CLIENT_API_KEY_RE.search(script.text).groups()[0]

        logger.debug("updating Okta api keys")
        content = self.session.get(self.MLB_OKTA_URL).text
        self._state.okta_client_id = self.OKTA_CLIENT_ID_RE.search(content).groups()[0]
        self.save()

    @property
    def session_token(self):
        if not self._state.session_token:
            self.login()
        if not self._state.session_token:
            raise Exception("no session token")
        return self._state.session_token

    @session_token.setter
    def session_token(self, value):
        self._state.session_token = value

    @property
    def access_token_expiry(self):

        if self._state.access_token_expiry:
            return dateutil.parser.parse(self._state.access_token_expiry)

    @access_token_expiry.setter
    def access_token_expiry(self, val):
        if val:
            self._state.access_token_expiry = val.isoformat()

    @property
    def access_token(self):
        if not self._state.access_token or not self.access_token_expiry or \
                self.access_token_expiry < datetime.now(tz=pytz.UTC):
            try:
                self.refresh_access_token()
            except requests.exceptions.HTTPError:
                # Clear token and then try to get a new access_token
                self.refresh_access_token(clear_token=True)

        logger.debug("access_token: %s" %(self._state.access_token))
        return self._state.access_token

    def refresh_access_token(self, clear_token=False):
        logger.debug("refreshing access token")

        if clear_token:
            self.session_token = None

        # ----------------------------------------------------------------------
        # Okta authentication -- used to get media entitlement later
        # ----------------------------------------------------------------------
        STATE = gen_random_string(64)
        NONCE = gen_random_string(64)

        AUTHZ_PARAMS = {
            "client_id": self.okta_client_id,
            "redirect_uri": "https://www.mlb.com/login",
            "response_type": "id_token token",
            "response_mode": "okta_post_message",
            "state": STATE,
            "nonce": NONCE,
            "prompt": "none",
            "sessionToken": self.session_token,
            "scope": "openid email"
        }
        authz_response = self.session.get(self.AUTHZ_URL, params=AUTHZ_PARAMS)
        authz_content = authz_response.text

        for line in authz_content.split("\n"):
            if "data.access_token" in line:
                OKTA_ACCESS_TOKEN = line.split("'")[1].encode('utf-8').decode('unicode_escape')
                break
        else:
            raise Exception(authz_content)

        # ----------------------------------------------------------------------
        # Get device assertion - used to get device token
        # ----------------------------------------------------------------------
        DEVICES_HEADERS = {
            "Authorization": "Bearer %s" % (self.client_api_key),
            "Origin": "https://www.mlb.com",
        }

        DEVICES_PARAMS = {
            "applicationRuntime": "firefox",
            "attributes": {},
            "deviceFamily": "browser",
            "deviceProfile": "macosx"
        }

        devices_response = self.session.post(
            self.BAM_DEVICES_URL,
            headers=DEVICES_HEADERS, json=DEVICES_PARAMS
        ).json()

        DEVICES_ASSERTION=devices_response["assertion"]

        # ----------------------------------------------------------------------
        # Get device token
        # ----------------------------------------------------------------------

        TOKEN_PARAMS = {
            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
            "latitude": "0",
            "longitude": "0",
            "platform": "browser",
            "subject_token": DEVICES_ASSERTION,
            "subject_token_type": "urn:bamtech:params:oauth:token-type:device"
        }
        token_response = self.session.post(
            self.BAM_TOKEN_URL, headers=DEVICES_HEADERS, data=TOKEN_PARAMS
        ).json()


        DEVICE_ACCESS_TOKEN = token_response["access_token"]
        DEVICE_REFRESH_TOKEN = token_response["refresh_token"]

        # ----------------------------------------------------------------------
        # Create session -- needed for device ID, which is used for entitlement
        # ----------------------------------------------------------------------
        SESSION_HEADERS = {
            "Authorization": DEVICE_ACCESS_TOKEN,
            "User-agent": USER_AGENT,
            "Origin": "https://www.mlb.com",
            "Accept": "application/vnd.session-service+json; version=1",
            "Accept-Encoding": "gzip, deflate, br",
            "Accept-Language": "en-US,en;q=0.5",
            "x-bamsdk-version": self.BAM_SDK_VERSION,
            "x-bamsdk-platform": self.PLATFORM,
            "Content-type": "application/json",
            "TE": "Trailers"
        }
        session_response = self.session.get(
            self.BAM_SESSION_URL,
            headers=SESSION_HEADERS
        ).json()
        DEVICE_ID = session_response["device"]["id"]

        # ----------------------------------------------------------------------
        # Get entitlement token
        # ----------------------------------------------------------------------
        ENTITLEMENT_PARAMS={
            "os": self.PLATFORM,
            "did": DEVICE_ID,
            "appname": "mlbtv_web"
        }

        ENTITLEMENT_HEADERS = {
            "Authorization": "Bearer %s" % (OKTA_ACCESS_TOKEN),
            "Origin": "https://www.mlb.com",
            "x-api-key": self.api_key

        }
        entitlement_response = self.session.get(
            self.BAM_ENTITLEMENT_URL,
            headers=ENTITLEMENT_HEADERS,
            params=ENTITLEMENT_PARAMS
        )

        ENTITLEMENT_TOKEN = entitlement_response.content

        # ----------------------------------------------------------------------
        # Finally (whew!) get access token using entitlement token
        # ----------------------------------------------------------------------
        headers = {
            "Authorization": "Bearer %s" % (self.client_api_key),
            "User-agent": USER_AGENT,
            "Accept": "application/vnd.media-service+json; version=1",
            "x-bamsdk-version": self.BAM_SDK_VERSION,
            "x-bamsdk-platform": self.PLATFORM,
            "origin": "https://www.mlb.com"
        }
        data = {
            "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
            "platform": "browser",
            "subject_token": ENTITLEMENT_TOKEN,
            "subject_token_type": "urn:bamtech:params:oauth:token-type:account"
        }
        response = self.session.post(
            self.BAM_TOKEN_URL,
            data=data,
            headers=headers
        )
        # from requests_toolbelt.utils import dump
        # print(dump.dump_all(response).decode("utf-8"))
        response.raise_for_status()
        token_response = response.json()

        self.access_token_expiry = datetime.now(tz=pytz.UTC) + \
                       timedelta(seconds=token_response["expires_in"])
        self._state.access_token = token_response["access_token"]
        self.save()

    def content(self, game_id):

        return self.session.get(
            self.GAME_CONTENT_URL_TEMPLATE.format(game_id=game_id)).json()

    # def feed(self, game_id):

    #     return self.session.get(GAME_FEED_URL.format(game_id=game_id)).json()

    @memo(region="long")
    def teams(self, sport_code="mlb", season=None):

        if sport_code != "mlb":
            media_title = "MiLBTV"
            raise MLBPlayException("Sorry, MiLB.tv streams are not yet supported")

        sports_url = (
            "http://statsapi.mlb.com/api/v1/sports"
        )
        with state.session.cache_responses_long():
            sports = self.session.get(sports_url).json()

        sport = next(s for s in sports["sports"] if s["code"] == sport_code)

        # season = game_date.year
        teams_url = (
            "http://statsapi.mlb.com/api/v1/teams"
            "?sportId={sport}&{season}".format(
                sport=sport["id"],
                season=season if season else ""
            )
        )

        # raise Exception(self.session.get(teams_url).json())
        with state.session.cache_responses_long():
            teams = AttrDict(
                (team["abbreviation"].lower(), team["id"])
                for team in sorted(self.session.get(teams_url).json()["teams"],
                                   key=lambda t: t["fileCode"])
            )

        return teams

    def airings(self, game_id):

        airings_url = self.AIRINGS_URL_TEMPLATE.format(game_id = game_id)
        airings = self.session.get(
            airings_url
        ).json()["data"]["Airings"]
        return airings


    def media_timestamps(self, game_id, media_id):

        try:
            airing = next(a for a in self.airings(game_id)
                          if a["mediaId"] == media_id)
        except StopIteration:
            raise StreamSessionException("No airing for media %s" %(media_id))

        start_timestamps = []
        try:
            start_time = next(
                    t["startDatetime"] for t in
                    next(m for m in airing["milestones"]
                     if m["milestoneType"] == "BROADCAST_START"
                    )["milestoneTime"]
                if t["type"] == "absolute"
                )

        except StopIteration:
            # Some streams don't have a "BROADCAST_START" milestone.  We need
            # something, so we use the scheduled game start time, which is
            # probably wrong.
            start_time = airing["startDate"]

        start_timestamps.append(
            ("S", start_time)
        )

        try:
            start_offset = next(
                t["start"] for t in
                next(m for m in airing["milestones"]
                     if m["milestoneType"] == "BROADCAST_START"
                )["milestoneTime"]
                if t["type"] == "offset"
            )
        except StopIteration:
            # Same as above.  Missing BROADCAST_START milestone means we
            # probably don't get accurate offsets for inning milestones.
            start_offset = 0

        start_timestamps.append(
            ("SO", start_offset)
        )

        timestamps = AttrDict(start_timestamps)
        timestamps.update(AttrDict([
            (
            "%s%s" %(
                "T"
                if next(
                        k for k in m["keywords"]
                        if k["type"] == "top"
                )["value"] == "true"
                else "B",
                int(
                    next(
                        k for k in m["keywords"] if k["type"] == "inning"
                    )["value"]
                )),
            next(t["start"]
                      for t in m["milestoneTime"]
                      if t["type"] == "offset"
                 )
            )
                 for m in airing["milestones"]
                 if m["milestoneType"] == "INNING_START"
        ]))
        return timestamps

    def get_stream(self, media):

        media_id = media.get("mediaId", media.get("guid"))

        headers={
            "Authorization": self.access_token,
            "User-agent": USER_AGENT,
            "Accept": "application/vnd.media-service+json; version=1",
            "x-bamsdk-version": "3.0",
            "x-bamsdk-platform": self.PLATFORM,
            "origin": "https://www.mlb.com"
        }
        stream_url = self.STREAM_URL_TEMPLATE.format(media_id=media_id)
        logger.info("getting stream %s" %(stream_url))
        stream = self.session.get(
            stream_url,
            headers=headers
        ).json()
        logger.debug("stream response: %s" %(stream))
        if "errors" in stream and len(stream["errors"]):
            return None
        stream = Stream(stream)
        stream.url = stream["stream"]["complete"]
        return stream



class NHLStreamSession(BAMStreamSessionMixin, StreamSession):

    AUTH = b"web_nhl-v1.0.0:2d1d846ea3b194a18ef40ac9fbce97e3"

    SCHEDULE_TEMPLATE = (
        "https://statsapi.web.nhl.com/api/v1/schedule"
        "?sportId={sport_id}&startDate={start}&endDate={end}"
        "&gameType={game_type}&gamePk={game_id}"
        "&teamId={team_id}"
        "&hydrate=linescore,team,game(content(summary,media(epg)),tickets)"
    )

    RESOLUTIONS = AttrDict([
        ("720p", "720p"),
        ("540p", "540p"),
        ("504p", "504p"),
        ("360p", "360p"),
        ("288p", "288p"),
        ("216p", "216p")
    ])

    def __init__(
            self,
            username, password,
            session_key=None,
            *args, **kwargs
    ):
        super(NHLStreamSession, self).__init__(
            username, password,
            *args, **kwargs
        )
        self.session_key = session_key


    def login(self):

        if self.logged_in:
            logger.info("already logged in")
            return

        auth = base64.b64encode(self.AUTH).decode("utf-8")

        token_url = "https://user.svc.nhl.com/oauth/token?grant_type=client_credentials"

        headers = {
            "Authorization": f"Basic {auth}",
            # "Referer": "https://www.nhl.com/login/freeGame?forwardUrl=https%3A%2F%2Fwww.nhl.com%2Ftv%2F2018020013%2F221-2000552%2F61332703",
            "Accept": "application/json, text/javascript, */*; q=0.01",
            "Accept-Language": "en-US,en;q=0.5",
            "Accept-Encoding": "gzip, deflate, br",
            "Origin": "https://www.nhl.com"
        }

        res = self.session.post(token_url, headers=headers)
        self.session_token = json.loads(res.text)["access_token"]

        login_url="https://gateway.web.nhl.com/ws/subscription/flow/nhlPurchase.login"

        auth = base64.b64encode(b"web_nhl-v1.0.0:2d1d846ea3b194a18ef40ac9fbce97e3")

        params = {
            "nhlCredentials":  {
                "email": self.username,
                "password": self.password
            }
        }

        headers = {
            "Authorization": self.session_token,
            "Origin": "https://www.nhl.com",
            # "Referer": "https://www.nhl.com/login/freeGame?forwardUrl=https%3A%2F%2Fwww.nhl.com%2Ftv%2F2018020013%2F221-2000552%2F61332703",
        }

        res = self.session.post(
            login_url,
            json=params,
            headers=headers
        )
        self.save()
        print(res.status_code)
        return (res.status_code == 200)


    @property
    def logged_in(self):

        logged_in_url = "https://account.nhl.com/ui/AccountProfile"
        content = self.session.get(logged_in_url).text
        # FIXME: this is gross
        if '"NHL Account - Profile"' in content:
            return True
        return False

    @property
    def session_key(self):
        return self._state.session_key

    @session_key.setter
    def session_key(self, value):
        self._state.session_key = value

    @property
    def token(self):
        return self._state.token

    @token.setter
    def token(self, value):
        self._state.token = value


    @memo(region="long")
    def teams(self, sport_code="mlb", season=None):

        teams_url = (
            "https://statsapi.web.nhl.com/api/v1/teams"
            "?{season}".format(
                season=season if season else ""
            )
        )

        # raise Exception(self.session.get(teams_url).json())
        with state.session.cache_responses_long():
            teams = AttrDict(
                (team["abbreviation"].lower(), team["id"])
                for team in sorted(self.session.get(teams_url).json()["teams"],
                                   key=lambda t: t["abbreviation"])
            )

        return teams


    def get_stream(self, media):

        url = "https://mf.svc.nhl.com/ws/media/mf/v2.4/stream"

        event_id = media["eventId"]
        if not self.session_key:
            logger.info("getting session key")


            params = {
                "eventId": event_id,
                "format": "json",
                "platform": "WEB_MEDIAPLAYER",
                "subject": "NHLTV",
                "_": "1538708097285"
            }

            res = self.session.get(
                url,
                params=params
            )
            j = res.json()
            logger.trace(json.dumps(j, sort_keys=True,
                             indent=4, separators=(',', ': ')))

            self.session_key = j["session_key"]
            self.save()

        params = {
            "contentId": media["mediaPlaybackId"],
            "playbackScenario": "HTTP_CLOUD_WIRED_WEB",
            "sessionKey": self.session_key,
            "auth": "response",
            "platform": "WEB_MEDIAPLAYER",
            "_": "1538708097285"
        }
        res = self.session.get(
            url,
            params=params
        )
        j = res.json()
        logger.trace(json.dumps(j, sort_keys=True,
                                   indent=4, separators=(',', ': ')))

        try:
            media_auth = next(x["attributeValue"]
                              for x in j["session_info"]["sessionAttributes"]
                              if x["attributeName"] == "mediaAuth_v2")
        except KeyError:
            raise StreamSessionException(f"No stream found for event {event_id}")

        self.cookies.set_cookie(
            Cookie(0, 'mediaAuth_v2', media_auth,
                   '80', '80', '.nhl.com',
                   None, None, '/', True, False, 4102444800, None, None, None, {}),
        )

        stream = Stream(j["user_verified_event"][0]["user_verified_content"][0]["user_verified_media_item"][0])

        return stream


def new(provider, *args, **kwargs):
    session_class = globals().get(f"{provider.upper()}StreamSession")
    return session_class.new(*args, **kwargs)

PROVIDERS_RE = re.compile(r"(.+)StreamSession$")
PROVIDERS = [ k.replace("StreamSession", "").lower()
              for k in globals() if PROVIDERS_RE.search(k) ]


def main():

    from . import state
    from . import utils
    import argparse

    global options

    parser = argparse.ArgumentParser()
    group = parser.add_mutually_exclusive_group()
    group.add_argument("-v", "--verbose", action="count", default=0,
                        help="verbose logging")
    group.add_argument("-q", "--quiet", action="count", default=0,
                        help="quiet logging")
    options, args = parser.parse_known_args()

    utils.setup_logging(options.verbose - options.quiet)

    # state.session = MLBStreamSession.new()
    # raise Exception(state.session.token)
    raise Exception(PROVIDERS)

    # state.session = NHLStreamSession.new()
    # raise Exception(state.session.session_key)


    # schedule = state.session.schedule(game_id=2018020020)
    # media = self.session.get_epgs(game_id=2018020020)
    # print(json.dumps(list(media), sort_keys=True,
    #                  indent=4, separators=(',', ': ')))


if __name__ == "__main__":
    main()



__all__ = ["MLBStreamSession", "StreamSessionException"]