#!/usr/bin/env python
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai

from __future__ import (unicode_literals, division, absolute_import, print_function)

import dateutil.parser
import dateutil.tz
import json
import re

from calibre.utils.date import parse_only_date

from calibre_plugins.overdrive_link.book import LibraryBook
from calibre_plugins.overdrive_link.formats import (FORMAT_ADOBE_EPUB, FORMAT_MP3)
from calibre_plugins.overdrive_link.language import LANGUAGE_CODE
from calibre_plugins.overdrive_link.library import SearchableLibrary
from calibre_plugins.overdrive_link.net import open_url
from calibre_plugins.overdrive_link.author_prep import normalize_author
from calibre_plugins.overdrive_link.title_prep import normalize_title
from calibre_plugins.overdrive_link.parseweb import LibraryError

from .python_transition import (IS_PYTHON2)
if IS_PYTHON2:
    from .python_transition import (http, repr, str, urllib)
else:
    import http.cookiejar
    import urllib.parse


__license__ = 'GPL v3'
__copyright__ = '2012-2025, John Howell <jhowell@acm.org>'


CLOUD_LIBRARY_FORMAT_NAMES = {
    'digital': FORMAT_ADOBE_EPUB,
    'audio': FORMAT_MP3,
    }


class CloudLibrary(SearchableLibrary):
    id = '3m'                   # Original name was 3M Cloud Library
    name = 'CloudLibrary'

    ebook_formats_supported = {FORMAT_ADOBE_EPUB}
    audiobook_formats_supported = {FORMAT_MP3}

    formats_supported = ebook_formats_supported | audiobook_formats_supported

    supports_recommendation = True
    sign_in_affects_get_current_availability = True     # can determine wait for books on hold

    @staticmethod
    def validate_library_id(library_id, migrate=True, config=None):
        if (':' in library_id) or ('/' in library_id):
            purl = urllib.parse.urlparse(library_id)
            m = re.match(r'^/library/([^/]+)/', purl.path)
            if not m:
                raise ValueError('cloudLibrary library id cannot be extracted from url: "%s"' % library_id)
            library_id = m.group(1)

        if not re.match(r'^([0-9a-zA-Z_]+)$', library_id):
            raise ValueError('cloudLibrary library id must be alphanumeric: "%s"' % library_id)

        return library_id

    @staticmethod
    def validate_book_id(book_id, library_id):
        if not re.match(r'^([0-9a-z]+)$', book_id):
            raise ValueError('cloudLibrary book id must be alphanumeric: "%s"' % book_id)

        return book_id

    @staticmethod
    def book_url(library_id, book_id):
        return 'http://ebook.yourcloudlibrary.com/library/%s/detail/%s' % (library_id, book_id)

    def __init__(self):
        self.cookiejar = http.cookiejar.CookieJar()

        self.search_audiobooks_allowed = False

        self.holds_checked = False
        self.holds = {}
        self.search_keys = []

    def sign_in(self, use_credentials):
        self.signin_required = True     # prevent access after failure to initialize
        self.cookiejar.clear()

        self.library_url = 'https://ebook.yourcloudlibrary.com/library/%s/Featured' % self.library_id

        response = open_url(self.log, self.library_url, cookiejar=self.cookiejar, expect_errors=[500])

        if response.is_httperror_exception or "Unable to load the page" in response.data_string or response.url.endswith('yourcloudlibrary.com/'):
            raise Exception('Library id "%s" is unknown at cloudLibrary' % self.library_id)

        result = self.open_cloud_library_json_url("https://ebook.yourcloudlibrary.com/library/%s/search?query=&_data=root" % self.library_id)
        config = result.get("config", {})

        self.search_keys = config.get("cloudLibraryConfiguration", {}).get("searchSources", {}).get("search", [])

        if "com.bookpac.archive.search.contenttype.audiobook" in self.search_keys:
            self.search_audiobooks_allowed = True

        if self.card_number and use_credentials:
            self.signin_required = True

            url = 'https://ebook.yourcloudlibrary.com/?_data=root'

            data = {}
            data["action"] = "login"
            data["barcode"] = self.card_number
            data["library"] = self.library_id
            data["pin"] = self.card_pin

            #self.log.info('Post %s' % url)
            result = self.open_cloud_library_json_url(url, data=urllib.parse.urlencode(data), log_request=False, return_errors=True)

            if "message" in result:
                raise LibraryError('Sign in failed: %s' % result["message"])

            self.log.info('Sign in successful')
            self.signed_in = True
        else:
            self.signin_required = False

    def find_books(self, books, search_author, search_title, keyword_search):
        '''
        Search cloudLibrary for books that match an author/title (or subsets thereof) and
        return the found matches with cloudLibrary identifiers.
        books = Set of Books to be updated

        /library/OCLS/search?title=handmaid&authors=atwood&series=&isbn=&category=&format=digital&available=Any&language=eng&sort=&
        orderBy=relevence&owned=yes&_data=routes%2Flibrary.%24name.search
        '''

        find_recommendable = self.config.check_recommendable    # suggestions seems to be allowed for all libraries
        search_ebook = len(self.config.search_formats & self.ebook_formats_supported) > 0
        search_audiobook = len(self.config.search_formats & self.audiobook_formats_supported) > 0

        if search_ebook and search_audiobook:
            if self.search_audiobooks_allowed:
                search_format = ''
                desired_formats = self.formats_supported
            else:
                search_format = 'digital'
                desired_formats = self.ebook_formats_supported
        elif search_ebook:
            search_format = 'digital'
            desired_formats = self.ebook_formats_supported
        elif search_audiobook and self.search_audiobooks_allowed:
            search_format = 'audio'
            desired_formats = self.audiobook_formats_supported
        else:
            return False    # no desired formats available for search

        page_num = 1
        has_more = True
        results_processed = 0

        while (has_more):
            data = {}

            if keyword_search:
                data["query"] = search_title
            else:
                data["title"] = search_title
                data["authors"] = search_author

            data["series"] = ""
            data["isbn"] = ""
            data["category"] = ""

            data["format"] = search_format
            data["available"] = "any" if find_recommendable else "Any"   # "true" for only show available now

            language_code = LANGUAGE_CODE.get(self.config.search_language, "")
            if ("language.%s" % language_code) in self.search_keys:
                data["language"] = language_code        # eg: "eng"
            else:
                data["language"] = ""                   # any

            data["sort"] = ""
            data["orderBy"] = "relevence"
            data["owned"] = "any" if find_recommendable else "yes"
            data["_data"] = "routes/library.$name.search"

            if page_num != 1:
                data["segment"] = str(page_num)

            result = self.open_cloud_library_json_url(
                "https://ebook.yourcloudlibrary.com/library/%s/search?%s" % (self.library_id, urllib.parse.urlencode(data)))

            #self.log.info("result: %s" % repr(result))
            search_results = result.get("results", {}).get("search", {})
            total_items = search_results.get("totalItems", 0)
            num_pages = search_results.get("totalSegments", 0)

            for item in search_results.get("items", []):
                document_id = item["documentId"]

                title = normalize_title(item.get("title", ''))
                subtitle = item.get("subtitle", '')
                if subtitle:
                    title = '%s: %s' % (title, subtitle)

                authors = []
                for a in item.get("authors", []):
                    author = normalize_author(a, unreverse=True)
                    if author not in authors:
                        authors.append(author)

                publisher = item.get('publisherName', '')

                if 'datePublished' in item:
                    pubdate = dateutil.parser.parse(str(item['datePublished'])).replace(tzinfo=dateutil.tz.tzutc())  # ISO 8601 format
                elif 'yearPublished' in item:
                    pubdate = parse_only_date(str(item['yearPublished']), assume_utc=True)
                else:
                    pubdate = None

                isbn = item.get('isbn', '')

                format = item.get('format', '')
                if format in CLOUD_LIBRARY_FORMAT_NAMES:
                    formats = set([CLOUD_LIBRARY_FORMAT_NAMES[format]])
                else:
                    self.log.error('Unexpected format: %s' % format)
                    continue

                available = item.get("totalCopies", None) not in [None, 0]
                recommendable = find_recommendable and not available

                lbook = LibraryBook(
                        authors=authors, title=title,
                        publisher=publisher, pubdate=pubdate, isbn=isbn, formats=formats,
                        available=available, recommendable=recommendable,
                        lib=self, book_id=document_id, search_author=search_author,
                        allow_get_from_cache=False)  # prevent old cached results from being used

                if formats.isdisjoint(desired_formats):
                    self.log.info('Ignoring wrong format: %s' % repr(lbook))
                elif not (available or find_recommendable):
                    self.log.info('Ignoring unavailable book: %s' % repr(lbook))
                else:
                    self.log.info('Found: %s' % repr(lbook))
                    books.add(lbook)

                results_processed += 1

            if page_num >= num_pages:
                has_more = False

                if results_processed != total_items:
                    self.log.warn("Found %d items but totalItems is %d" % (results_processed, total_items))
                    self.log.info("search_results: %s" % repr(search_results))
            else:
                page_num += 1

        return False

    def get_current_book_availability(self, book_id):
        self.check_current_holds()
        library_copies = 1
        available_copies = 0
        availability_date = None
        number_waiting_overall = 0
        is_on_hold = book_id in self.holds

        if is_on_hold:
            self.log.info('Using availability date from %s holds list for %s' % (self.name, book_id))
            availability_date = self.holds[book_id]

        else:
            result = self.open_cloud_library_json_url(
                "https://ebook.yourcloudlibrary.com/library/%s/detail/%s?%s" % (
                    self.library_id, book_id, urllib.parse.urlencode({"_data": "routes/library.$name.detail.$id"})),
                return_errors=True)

            error_message = result.get('message')
            if error_message:
                self.log.info('Book not available: %s' % error_message)
                return False

            book = result.get("book", {})

            item_id = book.get("itemId", "?")
            if item_id != book_id:
                self.log.warn('Book itemId incorrect: %s' % item_id)
                return False

            # could also check "status":"CAN_LOAN" if available and "status":"CAN_HOLD" if not
            available_copies = book.get("currentlyAvailable", 1 if book.get("canBorrow", False) else 0)

            if not available_copies:
                date = book.get("availabilityDate") or book.get("holdAvailableDate")    # unknown which is best
                if date:
                    availability_date = dateutil.parser.parse(date).replace(tzinfo=dateutil.tz.tzutc())
                    number_waiting_overall = 1      # wait not provided, guess
                else:
                    library_copies = 0  # unknown wait

        # estimate availability
        wait_weeks = self.calculate_wait_weeks(
                library_copies=library_copies, available_copies=available_copies,
                number_waiting_overall=number_waiting_overall, availability_date=availability_date)

        return (wait_weeks, is_on_hold, book_id)

    def check_current_holds(self):
        if self.holds_checked or not self.signed_in:
            return

        self.holds_checked = True

        try:
            result = self.open_cloud_library_json_url('https://ebook.yourcloudlibrary.com/library/%s/mybooks/holds?%s' % (
                self.library_id, urllib.parse.urlencode({"_data": "routes/library.$name.mybooks.holds"})),
                data=urllib.parse.urlencode({"format": ""}))

            for item in result.get("patronItems", []):
                book_id = item["itemId"]
                date = item.get("availabilityDate") or item.get("holdAvailableDate")    # unknown which is best
                if date:
                    self.holds[book_id] = dateutil.parser.parse(date).replace(tzinfo=dateutil.tz.tzutc())

        except Exception as e:
            self.log.exception('', e)

    def open_cloud_library_json_url(self, url, **kwargs):
        return_errors = kwargs.pop('return_errors', False)

        kwargs['cookiejar'] = self.cookiejar
        kwargs['referer'] = self.library_url
        kwargs['addheaders'] = [("Accept", "*/*")]
        kwargs['retry_on_internal_error'] = False
        kwargs['expect_errors'] = [401, 500]

        response = open_url(self.log, url, **kwargs)

        if response.is_httperror_exception:
            error = json.loads(response.response_data)    # some API errors returned this way

            if return_errors:
                if 'message' not in error:
                    error = {'message': 'Unknown error'}

                return error

            raise LibraryError('HTTP error %d: %s' % (response.code, error['message']))

        return json.loads(response.data_string)
