import datetime as dt
import importlib.metadata
import logging
from typing import Any, Dict, List, Optional, Union

import requests

import mediacloud
import mediacloud.error

logger = logging.getLogger(__name__)

# Identify the version of this package that's running
try:
    VERSION = "v" + importlib.metadata.version('mediacloud')
except importlib.metadata.PackageNotFoundError:
    VERSION = "dev"


class BaseApi:

    # Default applied to all queries made to main server. You can alter this on
    # your instance if you want to bail out more quickly, or know you have longer
    # running queries
    TIMEOUT_SECS = 60

    BASE_API_URL = "https://search.mediacloud.org/api/"

    USER_AGENT_STRING = f"mediacloud {VERSION}"

    def __init__(self, auth_token: Optional[str] = None):
        if not auth_token:
            raise mediacloud.error.MCException("No api key set - nothing will work without this")
        # Specify the auth_token to use for all future requests
        self._auth_token = auth_token
        # better performance to put all HTTP through this one object
        self._session = requests.Session()
        self._session.headers.update({'Authorization': f'Token {self._auth_token}'})
        self._session.headers.update({'Accept': 'application/json'})
        self._session.headers.update({"User-Agent": self.USER_AGENT_STRING})

    def user_profile(self) -> Dict:
        # :return: basic info about the current user, including their roles
        return self._query('auth/profile')

    def version(self) -> Dict:
        """
        returns dict with (at least):
        GIT_REV, now (float epoch time), version
        """
        return self._query('version')

    def _query(self, endpoint: str, params: Optional[Dict] = None, method: str = 'GET') -> Dict:
        """
        Centralize making the actual queries here for easy maintenance and testing of HTTP comms
        """
        endpoint_url = self.BASE_API_URL + endpoint
        if method == 'GET':
            r = self._session.get(endpoint_url, params=params, timeout=self.TIMEOUT_SECS)
        elif method == 'POST':
            r = self._session.post(endpoint_url, json=params, timeout=self.TIMEOUT_SECS)
        else:
            raise RuntimeError(f"Unsupported method of '{method}'")
        if r.status_code != 200:
            raise RuntimeError(f"API Server Error {r.status_code}. Params: {params}")
        return r.json()


class DirectoryApi(BaseApi):

    PLATFORM_ONLINE_NEWS = "online_news"
    PLATFORM_YOUTUBE = "youtube"
    PLATFORM_TWITTER = "twitter"
    PLATFORM_REDDIT = "reddit"

    def collection(self, collection_id: int):

        return self._query(f'sources/collections/{collection_id}/', None)

    def collection_list(self, platform: Optional[str] = None, name: Optional[str] = None,
                        limit: Optional[int] = 0, offset: Optional[int] = 0, source_id: Optional[int] = None) -> Dict:
        params: Dict[Any, Any] = dict(limit=limit, offset=offset)
        if name:
            params['name'] = name
        if platform:
            params['platform'] = platform
        if source_id:
            params['source_id'] = source_id
        return self._query('sources/collections/', params)

    def source(self, source_id: int):
        return self._query(f'sources/sources/{source_id}/', None)

    def source_list(self, platform: Optional[str] = None, name: Optional[str] = None,
                    collection_id: Optional[int] = None,
                    limit: Optional[int] = 0, offset: Optional[int] = 0) -> Dict:
        params: Dict[Any, Any] = dict(limit=limit, offset=offset)
        if collection_id:
            params['collection_id'] = collection_id
        if name:
            params['name'] = name
        if platform:
            params['platform'] = platform
        return self._query('sources/sources/', params)

    def feed_list(self, source_id: Optional[int] = None,
                  modified_since: Optional[Union[dt.datetime, int, float]] = None,
                  modified_before: Optional[Union[dt.datetime, int, float]] = None,
                  limit: Optional[int] = 0, offset: Optional[int] = 0, return_details: bool = False) -> Dict:
        params: Dict[Any, Any] = dict(limit=limit, offset=offset)
        if source_id:
            params['source_id'] = source_id

        def epoch_param(t, param):
            if t is None:
                return        # parameter not set
            if isinstance(t, dt.datetime):
                params[param] = t.timestamp()  # get epoch time
            elif isinstance(t, (int, float)):
                params[param] = t
            else:
                raise ValueError(param)

        epoch_param(modified_since, 'modified_since')
        epoch_param(modified_before, 'modified_before')

        if return_details:
            return {'results': self._query('sources/feeds/details/', params)['feeds']}

        return self._query('sources/feeds/', params)


class SearchApi(BaseApi):
    PROVIDER = "onlinenews-mediacloud"

    def _prep_default_params(self, query: str, start_date: dt.date, end_date: dt.date,
                             collection_ids: Optional[List[int]] = [], source_ids: Optional[List[int]] = [],
                             platform: Optional[str] = None):
        params: Dict[Any, Any] = dict(start=start_date.isoformat(), end=end_date.isoformat(), q=query,
                                      platform=(platform or self.PROVIDER))
        if len(source_ids):
            params['ss'] = ",".join([str(sid) for sid in source_ids]),
        if len(collection_ids):
            params['cs'] = ",".join([str(cid) for cid in collection_ids]),
        return params

    def story_count(self, query: str, start_date: dt.date, end_date: dt.date, collection_ids: Optional[List[int]] = [],
                    source_ids: Optional[List[int]] = [], platform: Optional[str] = None) -> Dict:
        params = self._prep_default_params(query, start_date, end_date, collection_ids, source_ids, platform)
        results = self._query('search/total-count', params)
        return results['count']

    def story_count_over_time(self, query: str, start_date: dt.date, end_date: dt.date,
                              collection_ids: Optional[List[int]] = [], source_ids: Optional[List[int]] = [],
                              platform: Optional[str] = None) -> List[Dict]:
        params = self._prep_default_params(query, start_date, end_date, collection_ids, source_ids, platform)
        results = self._query('search/count-over-time', params)
        for d in results['count_over_time']['counts']:
            d['date'] = dt.date.fromisoformat(d['date'][:10])
        return results['count_over_time']['counts']

    def story_list(self, query: str, start_date: dt.date, end_date: dt.date, collection_ids: Optional[List[int]] = [],
                   source_ids: Optional[List[int]] = [], platform: Optional[str] = None,
                   expanded: Optional[bool] = None, pagination_token: Optional[str] = None,
                   sort_order: Optional[str] = None,
                   page_size: Optional[int] = None) -> tuple[List[Dict], Optional[str]]:
        params = self._prep_default_params(query, start_date, end_date, collection_ids, source_ids, platform)
        if expanded:
            params['expanded'] = 1
        if pagination_token:
            params['pagination_token'] = pagination_token
        if sort_order:
            params['sort_order'] = sort_order
        if page_size:
            params['page_size'] = page_size
        results = self._query('search/story-list', params)
        self._dates_str2objects(results['stories'])
        return results['stories'], results['pagination_token']

    def _dates_str2objects(self, stories: List[Dict]):
        # _in place_ translation from ES date str to python data/datetime objects to save memory
        for s in stories:
            s['publish_date'] = dt.date.fromisoformat(s['publish_date'][:10]) if s['publish_date'] else None
            s['indexed_date'] = dt.datetime.fromisoformat(s['indexed_date']) if s['indexed_date'] else None

    def story_sample(self, query: str, start_date: dt.date, end_date: dt.date, collection_ids: Optional[List[int]] = [],
                     source_ids: Optional[List[int]] = [], platform: Optional[str] = None,
                     limit: Optional[int] = None, expanded=False) -> List[Dict]:
        params = self._prep_default_params(query, start_date, end_date, collection_ids, source_ids, platform)
        if limit:
            params['limit'] = limit
        fields = ['indexed_date', 'publish_date', 'id', 'language', 'media_name', 'media_url', 'title', 'url']
        if expanded:  # STILL UNSUPPORTED: admins can query full text if they choose to
            fields.append('text')
        params['fields'] = fields  # gets passed down to ES in MC client
        results = self._query('search/sample', params)
        self._dates_str2objects(results['sample'])
        return results['sample']

    def story(self, story_id: str) -> Dict:
        params = dict(storyId=story_id, platform=self.PROVIDER)
        results = self._query('search/story', params)
        return results['story']

    def words(self, query: str, start_date: dt.date, end_date: dt.date, collection_ids: Optional[List[int]] = [],
              source_ids: Optional[List[int]] = [], platform: Optional[str] = None,
              limit: Optional[int] = None) -> List[Dict]:
        params = self._prep_default_params(query, start_date, end_date, collection_ids, source_ids, platform)
        if limit:
            params['limit'] = limit
        results = self._query('search/words', params)
        return results['words']

    def sources(self, query: str, start_date: dt.date, end_date: dt.date, collection_ids: Optional[List[int]] = [],
                source_ids: Optional[List[int]] = [], platform: Optional[str] = None,
                limit: Optional[int] = None) -> List[Dict]:
        params = self._prep_default_params(query, start_date, end_date, collection_ids, source_ids, platform)
        if limit:
            params['limit'] = limit
        results = self._query('search/sources', params)
        return results['sources']

    def languages(self, query: str, start_date: dt.date, end_date: dt.date, collection_ids: Optional[List[int]] = [],
                  source_ids: Optional[List[int]] = [], platform: Optional[str] = None,
                  limit: Optional[int] = None) -> List[Dict]:
        params = self._prep_default_params(query, start_date, end_date, collection_ids, source_ids, platform)
        if limit:
            params['limit'] = limit
        results = self._query('search/languages', params)
        return results['languages']
