#!/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 re
import json

from calibre.utils.date import parse_only_date

from calibre_plugins.overdrive_link.numbers import value_unit
from calibre_plugins.overdrive_link.formats import (
    FORMAT_ADOBE_EPUB, FORMAT_ADOBE_PDF, FORMAT_BOUNDLESS_READER, FORMAT_BOUNDLESS_AUDIO)
from calibre_plugins.overdrive_link.book import (LibraryBook, InfoBook)
from calibre_plugins.overdrive_link.library import SearchableLibrary
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.net import (open_url, hostname_from_url)
from calibre_plugins.overdrive_link.parseweb import LibraryError
from calibre_plugins.overdrive_link.language import LANGUAGE_NAME

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


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


FORMAT_OF_NAME = {
    'AXS': FORMAT_BOUNDLESS_READER,
    'EPB': FORMAT_ADOBE_EPUB,
    'FND': FORMAT_BOUNDLESS_AUDIO,
    'PDF': FORMAT_ADOBE_PDF,
    'XPS': FORMAT_BOUNDLESS_READER,
    }


def library_host(library_id):
    if '.' in library_id:
        return library_id

    return '%s.boundless.baker-taylor.com' % library_id


class Boundless(SearchableLibrary):
    id = 'bl'
    name = 'Boundless'
    formats_supported = {FORMAT_ADOBE_EPUB, FORMAT_ADOBE_PDF,
                         FORMAT_BOUNDLESS_READER, FORMAT_BOUNDLESS_AUDIO}

    ebook_formats_supported = {FORMAT_ADOBE_EPUB, FORMAT_ADOBE_PDF, FORMAT_BOUNDLESS_READER}
    audiobook_formats_supported = {FORMAT_BOUNDLESS_AUDIO}

    @staticmethod
    def validate_library_id(library_id, migrate=True, config=None):
        if (':' in library_id) or ('/' in library_id):
            library_id = hostname_from_url(library_id)

        if library_id.lower().endswith('.boundless.baker-taylor.com'):
            library_id = library_id[:-len('.boundless.baker-taylor.com')]    # strip suffix

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

        return library_id.lower()

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

        return book_id

    @staticmethod
    def book_url(library_id, book_id):
        return 'http://%s/ng/view/library/title/%s' % (library_host(library_id), book_id)

    def find_books(self, books, search_author, search_title, keyword_search):
        '''
        Search Boundless for books that match an author/title (or subsets thereof).
        '''

        for book_format in ['EBT', 'ABT']:      # EBT=ebook, ABT=audiobook
            if book_format == 'EBT' and len(self.config.search_formats & self.ebook_formats_supported) == 0:
                continue    # not searching for this type
            if book_format == 'ABT' and len(self.config.search_formats & self.audiobook_formats_supported) == 0:
                continue    # not searching for this type

            page_num = 1
            total_pages = 1
            total_results = 0
            results_processed = 0
            RESULTS_PER_PAGE = 10

            MAX_RESULTS_ALLOWED = 500

            while (page_num <= total_pages):
                # GET /Search/GetListContent?view=false&page=1&pageSize=10&term=Rachel%20Aaron&searchby=author&addeddate=&format=ABT&agelevel=All&
                # availability=&collections=&releasedate=&language=&axisattribute=&sortby=Relevancy&profiletype= HTTP/1.1
                data = {}
                data['view'] = 'false'
                data['page'] = '%d' % page_num
                data['pageSize'] = '%d' % RESULTS_PER_PAGE
                data['term'] = ' '.join([search_author, search_title]).strip()
                data['searchBy'] = 'author' if not search_title else 'all'
                data['format'] = book_format
                data['agelevel'] = 'ALL'
                data['availability'] = ''
                data['collections'] = ''
                data['releasedate'] = ''
                data['language'] = ''
                data['axisattribute'] = ''
                data['sortby'] = 'Relevancy'
                data['profiletype'] = ''

                response = open_url(self.log, 'http://%s/Search/GetListContent?%s' % (library_host(self.library_id), urllib.parse.urlencode(data)))

                results = json.loads(response.data_string)      # Parse the json results

                new_total_results = results["TotalItems"] or 0
                if total_results and (new_total_results != total_results):
                    raise LibraryError('Total results changed from %d to %d' % (total_results, new_total_results))

                total_results = new_total_results
                total_pages = ((total_results - 1) // RESULTS_PER_PAGE) + 1  # floor division
                self.log.info('Response: page %d of %d. %d total results' % (page_num, total_pages, total_results))

                if total_results > MAX_RESULTS_ALLOWED:
                    return True

                for book in (results["Items"] or []):
                    book_id = book["ItemId"]
                    title = normalize_title(book["Title"].replace('<b>', '').replace('</b>', ''))
                    authors = [normalize_author(a, unreverse=True) for a in re.split(r'[;#]', book["Author"])]
                    isbn = book["ISBN"]
                    available = not book["IsRecommendable"]

                    lbook = LibraryBook(
                        authors=authors, title=title, isbn=isbn,
                        available=available, lib=self, book_id=book_id, search_author=search_author)

                    if not available:
                        self.log.info('Ignoring unavailable: %s' % repr(lbook))
                    else:
                        self.log.info('Found: %s' % repr(lbook))
                        books.add(lbook)

                    results_processed += 1

                if results_processed >= total_results:
                    break

                page_num += 1

            if results_processed != total_results:
                raise LibraryError('Expected %s but found %d' % (value_unit(total_results, 'result'), results_processed))

        return False

    def get_book_info(self, book_id, cache):
        # https://queens.boundless.baker-taylor.com/Title/TitleDetails?itemId=0019176077&ISBN=9780544798861&isRecommendable=false&collectionType=Default&view=false
        # https://queens.boundless.baker-taylor.com/Title/TitleDetails?itemId=0019176077
        data = {}
        data['itemId'] = book_id
        response = open_url(self.log, 'http://%s/Title/TitleDetails?%s' % (library_host(self.library_id), urllib.parse.urlencode(data)))

        if "404 - Title Not Found" in response.data_string:
            self.log.info('Book not valid or no longer available')
            return InfoBook(lib=self, book_id=book_id)

        results = json.loads(response.data_string)      # Parse the json results
        book_model = results.get("bookModel", {})

        authors = [normalize_author(a, unreverse=True) for a in book_model["author"]]
        title = normalize_title(book_model["title"].replace('<b>', '').replace('</b>', ''))

        subtitle = book_model.get("subTitle")
        if subtitle:
            title = '%s: %s' % (title, subtitle)

        publisher = book_model.get("publisher", "")

        publication_date = book_model.get("publicationDate")
        pubdate = parse_only_date(publication_date, assume_utc=True) if publication_date else None

        language_code = book_model.get("language", "").lower()
        if language_code in LANGUAGE_NAME:
            language = LANGUAGE_NAME[language_code]
        else:
            self.log.warn('Unknown Boundless language code %s' % language_code)
            language = ''

        isbn = ''
        availability = results.get("titleAvailabilityInfo", {}).get("availability", {})

        book_formats = set(availability.get("availableFormats", "").split(","))

        book_format = book_model.get("format", "")
        if book_format:
            book_formats.add(book_format)

        formats = set()
        for book_format in list(book_formats):
            if book_format in FORMAT_OF_NAME:
                formats.add(FORMAT_OF_NAME[book_format])
            else:
                self.log.warn('Unknown Boundless format %s' % book_format)

        for action in availability.get("actions", []):
            if action.get("isbn"):
                isbn = action['isbn']
                break

        return InfoBook(
            authors=authors, title=title, isbn=isbn, publisher=publisher, pubdate=pubdate,
            language=language, formats=formats, lib=self, book_id=book_id)

    def get_current_book_availability(self, book_id):
        data = {}
        data['itemId'] = book_id
        response = open_url(self.log, 'http://%s/Title/TitleDetails?%s' % (library_host(self.library_id), urllib.parse.urlencode(data)))

        if "404 - Title Not Found" in response.data_string:
            self.log.info('Book not valid or no longer available')
            return False

        results = json.loads(response.data_string)      # Parse the json results
        availability = results.get("titleAvailabilityInfo", {}).get("availability", {})

        library_copies = 0
        available_copies = 0
        number_waiting_overall = 0

        if availability["availableCopiesSpecified"]:
            available_copies = availability["availableCopies"]

        if availability["belongsToLibrarySpecified"] and availability["belongsToLibrary"] and availability["totalQuantitySpecified"]:
            library_copies = availability["totalQuantity"]

        if availability["numOfPatronsOnHoldSpecified"]:
            number_waiting_overall = availability["numOfPatronsOnHold"]

        if not library_copies:
            self.log.info('Book no longer available')
            return False

        # not signed in so cannot determine if book is checked out or already on hold by this user

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