import hashlib
import json
import logging
import pickle
import re
import socket
import ssl

from .compat import get_charset
from .compat import HTTPError
from .compat import OrderedDict
from .compat import Request
from .compat import urlencode
from .compat import URLError
from .compat import urlopen

from micawber.exceptions import InvalidResponseException
from micawber.exceptions import ProviderException
from micawber.exceptions import ProviderNotFoundException
from micawber.parsers import extract
from micawber.parsers import extract_html
from micawber.parsers import parse_html
from micawber.parsers import parse_text
from micawber.parsers import parse_text_full


logger = logging.getLogger(__name__)


class Provider(object):
    def __init__(self, endpoint, timeout=3.0, user_agent=None, **kwargs):
        self.endpoint = endpoint
        self.socket_timeout = timeout
        self.user_agent = user_agent or 'python-micawber'
        self.base_params = {'format': 'json'}
        self.base_params.update(kwargs)

    def fetch(self, url):
        req = Request(url, headers={'User-Agent': self.user_agent})
        try:
            resp = fetch(req, self.socket_timeout)
        except URLError:
            return False
        except HTTPError:
            return False
        except socket.timeout:
            return False
        except ssl.SSLError:
            return False
        return resp

    def encode_params(self, url, **extra_params):
        params = dict(self.base_params)
        params.update(extra_params)
        params['url'] = url
        return urlencode(sorted(params.items()))

    def request(self, url, **extra_params):
        encoded_params = self.encode_params(url, **extra_params)

        endpoint_url = self.endpoint
        if '?' in endpoint_url:
            endpoint_url = '%s&%s' % (endpoint_url.rstrip('&'), encoded_params)
        else:
            endpoint_url = '%s?%s' % (endpoint_url, encoded_params)

        response = self.fetch(endpoint_url)
        if response:
            return self.handle_response(response, url)
        else:
            raise ProviderException('Error fetching "%s"' % endpoint_url)

    def handle_response(self, response, url):
        try:
            json_data = json.loads(response)
        except ValueError as exc:
            try:
                msg = exc.message
            except AttributeError:
                msg = exc.args[0]
            raise InvalidResponseException(msg)

        if 'url' not in json_data:
            json_data['url'] = url
        if 'title' not in json_data:
            json_data['title'] = json_data['url']

        return json_data


def make_key(*args, **kwargs):
    return hashlib.md5(pickle.dumps((args, kwargs))).hexdigest()


def url_cache(fn):
    def inner(self, url, **params):
        if self.cache is not None:
            key = make_key(url, params)
            data = self.cache.get(key)
            if not data:
                data = fn(self, url, **params)
                self.cache.set(key, data)
            return data
        return fn(self, url, **params)
    return inner


def fetch(request, timeout=None):
    urlopen_params = {}
    if timeout:
        urlopen_params['timeout'] = timeout
    resp = urlopen(request, **urlopen_params)
    if resp.code < 200 or resp.code >= 300:
        return False

    # by RFC, default HTTP charset is ISO-8859-1
    charset = get_charset(resp) or 'iso-8859-1'

    content = resp.read().decode(charset)
    resp.close()
    return content


def fetch_cache(cache, url, refresh=False, timeout=None):
    contents = None
    if cache is not None and not refresh:
        contents = cache.get('micawber.%s' % url)
    if contents is None:
        contents = fetch(url, timeout=timeout)
        if cache is not None:
            cache.set('micawber.%s' % url, contents)
    return contents


class ProviderRegistry(object):
    def __init__(self, cache=None):
        self._registry = OrderedDict()
        self.cache = cache

    def register(self, regex, provider):
        self._registry[regex] = provider

    def unregister(self, regex):
        del self._registry[regex]

    def __iter__(self):
        return iter(reversed(list(self._registry.items())))

    def provider_for_url(self, url):
        for regex, provider in self:
            if re.match(regex, url):
                return provider

    @url_cache
    def request(self, url, **params):
        provider = self.provider_for_url(url)
        if provider:
            return provider.request(url, **params)
        raise ProviderNotFoundException('Provider not found for "%s"' % url)

    def parse_text(self, text, **kwargs):
        return parse_text(text, self, **kwargs)

    def parse_text_full(self, text, **kwargs):
        return parse_text_full(text, self, **kwargs)

    def parse_html(self, html, **kwargs):
        return parse_html(html, self, **kwargs)

    def extract(self, text, **kwargs):
        return extract(text, self, **kwargs)

    def extract_html(self, html, **kwargs):
        return extract_html(html, self, **kwargs)


def bootstrap_basic(cache=None, registry=None):
    # complements of oembed.com#section7
    pr = registry or ProviderRegistry(cache)

    # c
    pr.register(r'http://chirb\.it/\S+', Provider('http://chirb.it/oembed.json'))
    pr.register(r'https?://www\.circuitlab\.com/circuit/\S+', Provider('https://www.circuitlab.com/circuit/oembed'))

    # d
    pr.register(r'https?://(?:www\.)?dailymotion\.com/\S+', Provider('http://www.dailymotion.com/services/oembed'))

    # f
    pr.register(r'https?://\S*?flickr\.com/\S+', Provider('https://www.flickr.com/services/oembed/'))
    pr.register(r'https?://flic\.kr/\S*', Provider('https://www.flickr.com/services/oembed/'))
    pr.register(r'https?://(?:www\.)?funnyordie\.com/videos/\S+', Provider('http://www.funnyordie.com/oembed'))

    # g
    # 2020-11-04: removed GitHub gist, as it seems to be unsupported now.
    #pr.register(r'https?://gist\.github\.com/\S*', Provider('https://github.com/api/oembed'))

    # h
    pr.register(r'http://(?:www\.)hulu\.com/watch/\S+', Provider('http://www.hulu.com/api/oembed.json'))

    # i
    pr.register(r'https?://\S*imgur\.com/\S+', Provider('https://api.imgur.com/oembed')),
    pr.register(r'https?://(www\.)?instagr(\.am|am\.com)/p/\S+', Provider('http://api.instagram.com/oembed'))

    # m
    pr.register(r'http://www\.mobypicture\.com/user/\S*?/view/\S*', Provider('http://api.mobypicture.com/oEmbed'))
    pr.register(r'http://moby\.to/\S*', Provider('http://api.mobypicture.com/oEmbed'))

    # p
    pr.register(r'http://i\S*\.photobucket\.com/albums/\S+', Provider('http://photobucket.com/oembed'))
    pr.register(r'http://gi\S*\.photobucket\.com/groups/\S+', Provider('http://photobucket.com/oembed'))
    pr.register(r'http://www\.polleverywhere\.com/(polls|multiple_choice_polls|free_text_polls)/\S+', Provider('http://www.polleverywhere.com/services/oembed/'))
    pr.register(r'https?://(.+\.)?polldaddy\.com/\S*', Provider('http://polldaddy.com/oembed/'))

    # s
    pr.register(r'https?://(?:www\.)?slideshare\.net/[^\/]+/\S+', Provider('http://www.slideshare.net/api/oembed/2'))
    pr.register(r'https?://slidesha\.re/\S*', Provider('http://www.slideshare.net/api/oembed/2'))
    pr.register(r'http://\S*\.smugmug\.com/\S*', Provider('http://api.smugmug.com/services/oembed/'))
    pr.register(r'https://\S*?soundcloud\.com/\S+', Provider('http://soundcloud.com/oembed'))
    pr.register(r'https?://speakerdeck\.com/\S*', Provider('https://speakerdeck.com/oembed.json')),
    pr.register(r'https?://(?:www\.)?scribd\.com/\S*', Provider('http://www.scribd.com/services/oembed'))

    # t
    pr.register(r'https?://(www\.)tiktok\.com/\S+', Provider('https://www.tiktok.com/oembed'))
    pr.register(r'https?://(www\.)?twitter\.com/\S+/status(es)?/\S+', Provider('https://publish.twitter.com/oembed'))

    # v
    pr.register(r'http://(?:player\.)?vimeo\.com/\S+', Provider('http://vimeo.com/api/oembed.json'))
    pr.register(r'https://(?:player\.)?vimeo\.com/\S+', Provider('https://vimeo.com/api/oembed.json'))

    # w
    pr.register(r'http://\S+\.wordpress\.com/\S+', Provider('http://public-api.wordpress.com/oembed/'))
    pr.register(r'https?://wordpress\.tv/\S+', Provider('http://wordpress.tv/oembed/'))

    # y
    pr.register(r'http://(\S*\.)?youtu(\.be/|be\.com/watch)\S+', Provider('https://www.youtube.com/oembed'))
    pr.register(r'https://(\S*\.)?youtu(\.be/|be\.com/watch)\S+', Provider('https://www.youtube.com/oembed?scheme=https&'))

    return pr


def bootstrap_embedly(cache=None, registry=None, refresh=False, **params):
    endpoint = 'http://api.embed.ly/1/oembed'
    schema_url = 'http://api.embed.ly/1/services/python'

    pr = registry or ProviderRegistry(cache)

    # fetch the schema
    contents = fetch_cache(cache, schema_url, refresh=refresh)
    json_data = json.loads(contents)

    for provider_meta in json_data:
        for regex in provider_meta['regex']:
            pr.register(regex, Provider(endpoint, **params))
    return pr


def bootstrap_noembed(cache=None, registry=None, refresh=False, **params):
    endpoint = 'http://noembed.com/embed'
    schema_url = 'http://noembed.com/providers'

    pr = registry or ProviderRegistry(cache)

    # fetch the schema
    contents = fetch_cache(cache, schema_url, refresh=refresh)
    json_data = json.loads(contents)

    for provider_meta in json_data:
        for regex in provider_meta['patterns']:
            pr.register(regex, Provider(endpoint, **params))
    return pr


def bootstrap_oembed(cache=None, registry=None, refresh=False, **params):
    schema_url = 'https://oembed.com/providers.json'
    pr = registry or ProviderRegistry(cache)

    # Fetch schema.
    contents = fetch_cache(cache, schema_url, refresh=refresh)
    json_data = json.loads(contents)

    for item in json_data:
        for endpoint in reversed(item['endpoints']):
            # Possibly this provider only supports discovery via <link> tags,
            # which is not supported by micawber.
            if 'schemes' not in endpoint:
                continue

            # Consists of one or more schemes, a destination URL and optionally
            # a format, e.g. "json".
            url = endpoint['url']
            if '{format}' in url:
                url = url.replace('{format}', 'json')

            provider = Provider(url, **params)
            for scheme in endpoint['schemes']:
                # If a question-mark is being used, it is for the query-string
                # and should be treated as a literal.
                scheme = scheme.replace('?', r'\?')

                # Transform the raw pattern into a reasonable regex. Match one
                # or more of any character that is not a slash, whitespace, or
                # a parameter used for separating querystring/url params.
                pattern = scheme.replace('*', r'[^\/\s\?&]+?')
                try:
                    re.compile(pattern)
                except re.error:
                    logger.exception('oembed.com provider %s regex could not '
                                     'be compiled: %s', url, pattern)
                    continue

                pr.register(pattern, provider)

    # Currently oembed.com does not provide patterns for YouTube, so we'll add
    # these ourselves.
    pr.register(r'http://(\S*\.)?youtu(\.be/|be\.com/watch)\S+',
                Provider('https://www.youtube.com/oembed'))
    pr.register(r'https://(\S*\.)?youtu(\.be/|be\.com/watch)\S+',
                Provider('https://www.youtube.com/oembed?scheme=https&'))

    return pr
