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

_license__   = 'GPL v3'
__docformat__ = 'restructuredtext en'


import codecs
import os, traceback, zipfile

try:
    from PyQt5.Qt import QToolButton, QUrl
except ImportError:
    from PyQt4.Qt import QToolButton, QUrl

from calibre.gui2 import open_url, question_dialog
from calibre.gui2.actions import InterfaceAction
from calibre.utils.config import config_dir
from calibre.ptempfile import (PersistentTemporaryDirectory,
                               PersistentTemporaryFile, remove_dir)

from calibre.ebooks.metadata.meta import get_metadata

from calibre_plugins.obok_dedrm.dialogs import (SelectionDialog, DecryptAddProgressDialog,
                                                AddEpubFormatsProgressDialog, ResultsSummaryDialog)
from calibre_plugins.obok_dedrm.config import plugin_prefs as cfg
from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME, PLUGIN_SAFE_NAME,
                                PLUGIN_VERSION, PLUGIN_DESCRIPTION, HELPFILE_NAME)
from calibre_plugins.obok_dedrm.utilities import (
                            get_icon, set_plugin_icon_resources, showErrorDlg, format_plural,
                            debug_print
                            )

from calibre_plugins.obok_dedrm.obok.obok import KoboLibrary
from calibre_plugins.obok_dedrm.obok.legacy_obok import legacy_obok

PLUGIN_ICONS = ['images/obok.png']

try:
    debug_print("obok::action_err.py - loading translations")
    load_translations()
except NameError:
    debug_print("obok::action_err.py - exception when loading translations")
    pass # load_translations() added in calibre 1.9

class InterfacePluginAction(InterfaceAction):
    name = PLUGIN_NAME
    action_spec = (PLUGIN_NAME, None,
            _(PLUGIN_DESCRIPTION), None)
    popup_type = QToolButton.InstantPopup
    action_type = 'current'

    def genesis(self):
        icon_resources = self.load_resources(PLUGIN_ICONS)
        set_plugin_icon_resources(PLUGIN_NAME, icon_resources)

        self.qaction.setIcon(get_icon(PLUGIN_ICONS[0]))
        self.qaction.triggered.connect(self.launchObok)
        self.gui.keyboard.finalize()

    def launchObok(self):
        '''
        Main processing/distribution method
        '''
        self.count = 0
        self.books_to_add = []
        self.formats_to_add = []
        self.add_books_cancelled = False
        self.decryption_errors = []
        self.userkeys = []
        self.duplicate_book_list = []
        self.no_home_for_book = []
        self.ids_of_new_books = []
        self.successful_format_adds =[]
        self.add_formats_cancelled = False
        self.tdir = PersistentTemporaryDirectory('_obok', prefix='')
        self.db = self.gui.current_db.new_api
        self.current_idx = self.gui.library_view.currentIndex()

        print ('Running {}'.format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
        #
        # search for connected device in case serials are saved
        tmpserials = cfg['kobo_serials']
        device_path = None
        try:
            device = self.parent().device_manager.connected_device
            if (device):
                device_path = device._main_prefix
                debug_print("get_device_settings - device_path=", device_path)
            else:
                debug_print("didn't find device")
        except:
            debug_print("Exception getting device path. Probably not an E-Ink Kobo device")

        # Get the Kobo Library object (obok v3.01)
        self.library = KoboLibrary(tmpserials, device_path, cfg['kobo_directory'])
        debug_print ("got kobodir %s" % self.library.kobodir)
        if (self.library.kobodir == ''):
            # linux and no device connected, but could be extended
            # to the case where on Windows/Mac the prog is not installed
            msg = _('<p>Could not find Kobo Library\n<p>Windows/Mac: do you have Kobo Desktop installed?\n<p>Windows/Mac/Linux: In case you have an Kobo eInk device, connect the device.')
            showErrorDlg(msg, None)
            return


        # Get a list of Kobo titles
        books = self.build_book_list()
        if len(books) < 1:
            msg = _('<p>No books found in Kobo Library\nAre you sure it\'s installed/configured/synchronized?')
            showErrorDlg(msg, None)
            return

        # Check to see if a key can be retrieved using the legacy obok method.
        legacy_key = legacy_obok().get_legacy_cookie_id
        if legacy_key is not None:
            print (_('Legacy key found: '), legacy_key.encode('hex_codec'))
            self.userkeys.append(legacy_key)
        # Add userkeys found through the normal obok method to the list to try.
        try:
            candidate_keys = self.library.userkeys
        except:
            print (_('Trouble retrieving keys with newer obok method.'))
            traceback.print_exc()
        else:
            if len(candidate_keys):
                self.userkeys.extend(candidate_keys)
                print (_('Found {0} possible keys to try.').format(len(self.userkeys)))
        if not len(self.userkeys):
            msg = _('<p>No userkeys found to decrypt books with. No point in proceeding.')
            showErrorDlg(msg, None)
            return

        # Launch the Dialog so the user can select titles.
        dlg = SelectionDialog(self.gui, self, books)
        if dlg.exec_():
            books_to_import = dlg.getBooks()
            self.count = len(books_to_import)
            debug_print("InterfacePluginAction::launchObok - number of books to decrypt: %d" % self.count)
            # Feed the titles, the callback function (self.get_decrypted_kobo_books)
            # and the Kobo library object to the ProgressDialog dispatcher.
            d = DecryptAddProgressDialog(self.gui, books_to_import, self.get_decrypted_kobo_books, self.library, 'kobo',
                               status_msg_type='Kobo books', action_type=('Decrypting', 'Decryption'))
            # Canceled the decryption process; clean up and exit.
            if d.wasCanceled():
                print (_('{} - Decryption canceled by user.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
                self.library.close()
                remove_dir(self.tdir)
                return
        else:
            # Canceled the selection process; clean up and exit.
            self.library.close()
            remove_dir(self.tdir)
            return
        # Close Kobo Library object
        self.library.close()

        # If we have decrypted books to work with, feed the list of decrypted books details
        # and the callback function (self.add_new_books) to the ProgressDialog dispatcher.
        if len(self.books_to_add):
            d = DecryptAddProgressDialog(self.gui, self.books_to_add, self.add_new_books, self.db, 'calibre',
                               status_msg_type='new calibre books', action_type=('Adding','Addition'))
            # Canceled the "add new books to calibre" process;
            # show the results of what got added before cancellation.
            if d.wasCanceled():
                print (_('{} - "Add books" canceled by user.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
                self.add_books_cancelled = True
                print (_('{} - wrapping up results.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
                self.wrap_up_results()
                remove_dir(self.tdir)
                return
        # If books couldn't be added because of duplicate entries in calibre, ask
        # if we should try to add the decrypted epubs to existing calibre library entries.
        if len(self.duplicate_book_list):
            if cfg['finding_homes_for_formats'] == 'Always':
                self.process_epub_formats()
            elif cfg['finding_homes_for_formats'] == 'Never':
                self.no_home_for_book.extend([entry[0] for entry in self.duplicate_book_list])
            else:
                if self.ask_about_inserting_epubs():
                    # Find homes for the epub decrypted formats in existing calibre library entries.
                    self.process_epub_formats()
                else:
                    print (_('{} - User opted not to try to insert EPUB formats').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
                    self.no_home_for_book.extend([entry[0] for entry in self.duplicate_book_list])

        print (_('{} - wrapping up results.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
        self.wrap_up_results()
        remove_dir(self.tdir)
        return

    def show_help(self):
        '''
        Extract on demand the help file resource
        '''
        def get_help_file_resource():
            # We will write the help file out every time, in case the user upgrades the plugin zip
            # and there is a newer help file contained within it.
            file_path = os.path.join(config_dir, 'plugins', HELPFILE_NAME)
            file_data = self.load_resources(HELPFILE_NAME)[HELPFILE_NAME].decode('utf-8')
            with open(file_path,'w') as f:
                f.write(file_data)
            return file_path
        url = 'file:///' + get_help_file_resource()
        open_url(QUrl(url))

    def build_book_list(self):
        '''
        Connect to Kobo db and get titles.
        '''
        return self.library.books

    def get_decrypted_kobo_books(self, book):
        '''
        This method is a call-back function used by DecryptAddProgressDialog in dialogs.py to decrypt Kobo books

        :param book: A KoboBook object that is to be decrypted.
        '''
        print (_('{0} - Decrypting {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, book.title))
        decrypted = self.decryptBook(book)
        if decrypted['success']:
            # Build a list of calibre "book maps" for calibre's add_book function.
            mi = get_metadata(decrypted['fileobj'], 'epub')
            bookmap = {'EPUB':decrypted['fileobj'].name}
            self.books_to_add.append((mi, bookmap))
        else:
            # Book is probably still encrypted.
            print (_('{0} - Couldn\'t decrypt {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, book.title))
            self.decryption_errors.append((book.title, _('decryption errors')))
            return False
        return True

    def add_new_books(self, books_to_add):
        '''
        This method is a call-back function used by DecryptAddProgressDialog in dialogs.py to add books to calibre
        (It's set up to handle multiple books, but will only be fed books one at a time by DecryptAddProgressDialog)

        :param books_to_add: List of calibre bookmaps (created in get_decrypted_kobo_books)
        '''

        cfg_add_duplicates = (cfg['finding_homes_for_formats'] == 'Add new entry')

        added = self.db.add_books(books_to_add, add_duplicates=cfg_add_duplicates, run_hooks=False)
        if len(added[0]):
            # Record the id(s) that got added
            for id in added[0]:
                print (_('{0} - Added {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, books_to_add[0][0].title))
                self.ids_of_new_books.append((id, books_to_add[0][0]))
        if len(added[1]):
            # Build a list of details about the books that didn't get added because duplicate were detected.
            for mi, map in added[1]:
                print (_('{0} - {1} already exists. Will try to add format later.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, mi.title))
                self.duplicate_book_list.append((mi, map['EPUB'], _('duplicate detected')))
            return False
        return True

    def add_epub_format(self, book_id, mi, path):
        '''
        This method is a call-back function used by AddEpubFormatsProgressDialog in dialogs.py

        :param book_id: calibre ID of the book to add the encrypted epub to.
        :param mi: calibre metadata object
        :param path: path to the decrypted epub (temp file)
        '''
        if self.db.add_format(book_id, 'EPUB', path, replace=False, run_hooks=False):
            self.successful_format_adds.append((book_id, mi))
            print (_('{0} - Successfully added EPUB format to existing {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, mi.title))
            return True
        # we really shouldn't get here.
        print (_('{0} - Error adding EPUB format to existing {1}. This really shouldn\'t happen.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, mi.title))
        self.no_home_for_book.append(mi)
        return False

    def process_epub_formats(self):
        '''
        Ask the user if they want to try to find homes for those books that already had an entry in calibre
        '''
        for book in self.duplicate_book_list:
            mi, tmp_file = book[0], book[1]
            dup_ids = self.db.find_identical_books(mi)
            home_id = self.find_a_home(dup_ids)
            if home_id is not None:
                # Found an epub-free duplicate to add the epub to.
                # build a list for the add_epub_format method to use.
                self.formats_to_add.append((home_id, mi, tmp_file))
            else:
                self.no_home_for_book.append(mi)
        # If we found homes for decrypted epubs in existing calibre entries, feed the list of decrypted book
        # details and the callback function (self.add_epub_format) to the ProgressDialog dispatcher.
        if self.formats_to_add:
            d = AddEpubFormatsProgressDialog(self.gui, self.formats_to_add, self.add_epub_format)
            if d.wasCanceled():
                print (_('{} - "Insert formats" canceled by user.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
                self.add_formats_cancelled = True
                return
            #return
        return

    def wrap_up_results(self):
        '''
        Present the results
        '''
        caption = PLUGIN_NAME + ' v' + PLUGIN_VERSION
        # Refresh the gui and highlight new entries/modified entries.
        if len(self.ids_of_new_books) or len(self.successful_format_adds):
            self.refresh_gui_lib()

        msg, log = self.build_report()

        sd = ResultsSummaryDialog(self.gui, caption, msg, log)
        sd.exec_()
        return

    def ask_about_inserting_epubs(self):
        '''
        Build question dialog with details about kobo books
        that couldn't be added to calibre as new books.
        '''
        ''' Terisa: Improve the message
        '''
        caption = PLUGIN_NAME + ' v' + PLUGIN_VERSION
        plural = format_plural(len(self.ids_of_new_books))
        det_msg = ''
        if self.count > 1:
            msg = _('<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> ').format(len(self.ids_of_new_books), len(self.duplicate_book_list), plural)
            msg += _('not added because books with the same title/author were detected.<br /><br />Would you like to try and add the EPUB format{0}').format(plural)
            msg += _(' to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be overwritten.')
            for entry in self.duplicate_book_list:
                det_msg += _('{0} -- not added because of {1} in your library.\n\n').format(entry[0].title, entry[2])
        else:
            msg = _('<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />').format(self.duplicate_book_list[0][0].title, self.duplicate_book_list[0][2])
            msg += _('Would you like to try and add the EPUB format to an available calibre duplicate?<br /><br />')
            msg += _('NOTE: no pre-existing EPUB will be overwritten.')

        return question_dialog(self.gui, caption, msg, det_msg)

    def find_a_home(self, ids):
        '''
        Find the ID of the first EPUB-Free duplicate available

        :param ids: List of calibre IDs that might serve as a home.
        '''
        for id in ids:
            # Find the first entry that matches the incoming book that doesn't have an EPUB format.
            if not self.db.has_format(id, 'EPUB'):
                return id
                break
        return None

    def refresh_gui_lib(self):
        '''
        Update the GUI; highlight the books that were added/modified
        '''
        if self.current_idx.isValid():
            self.gui.library_view.model().current_changed(self.current_idx, self.current_idx)
        new_entries = [id for id, mi in self.ids_of_new_books]
        if new_entries:
            self.gui.library_view.model().db.data.books_added(new_entries)
            self.gui.library_view.model().books_added(len(new_entries))
        new_entries.extend([id for id, mi in self.successful_format_adds])
        self.gui.db_images.reset()
        self.gui.tags_view.recount()
        self.gui.library_view.model().set_highlight_only(True)
        self.gui.library_view.select_rows(new_entries)
        return

    def decryptBook(self, book):
        '''
        Decrypt Kobo book

        :param book: obok file object
        '''
        result = {}
        result['success'] = False
        result['fileobj'] = None

        zin = zipfile.ZipFile(book.filename, 'r')
        #print ('Kobo library filename: {0}'.format(book.filename))
        for userkey in self.userkeys:
            print (_('Trying key: '), codecs.encode(userkey, 'hex'))
            try:
                fileout = PersistentTemporaryFile('.epub', dir=self.tdir)
                #print ('Temp file: {0}'.format(fileout.name))
                # modify the output file to be compressed by default
                zout = zipfile.ZipFile(fileout.name, "w", zipfile.ZIP_DEFLATED)
                # ensure that the mimetype file is the first written to the epub container
                # and is stored with no compression
                members = zin.namelist();
                try:
                    members.remove('mimetype')
                except Exception:
                    pass
                zout.writestr('mimetype', 'application/epub+zip', zipfile.ZIP_STORED)
                # end of mimetype mod
                for filename in members:
                    contents = zin.read(filename)
                    if filename in book.encryptedfiles:
                        file = book.encryptedfiles[filename]
                        contents = file.decrypt(userkey, contents)
                        # Parse failures mean the key is probably wrong.
                        file.check(contents)
                    zout.writestr(filename, contents)
                zout.close()
                zin.close()
                result['success'] = True
                result['fileobj'] = fileout
                print ('Success!')
                return result
            except ValueError:
                print (_('Decryption failed, trying next key.'))
                zout.close()
                continue
            except Exception:
                print (_('Unknown Error decrypting, trying next key..'))
                zout.close()
                continue
        result['fileobj'] = book.filename
        zin.close()
        return result

    def build_report(self):
        log = ''
        processed = len(self.ids_of_new_books) + len(self.successful_format_adds)

        if processed == self.count:
            if self.count > 1:
                msg = _('<p>All selected Kobo books added as new calibre books or inserted into existing calibre ebooks.<br /><br />No issues.')
            else:
                # Single book ... don't get fancy.
                title = self.ids_of_new_books[0][1].title if self.ids_of_new_books else self.successful_format_adds[0][1].title
                msg = _('<p>{0} successfully added.').format(title)
            return (msg, log)
        else:
            if self.count != 1:
                msg = _('<p>Not all selected Kobo books made it into calibre.<br /><br />View report for details.')
                log += _('<p><b>Total attempted:</b> {}</p>\n').format(self.count)
                log += _('<p><b>Decryption errors:</b> {}</p>\n').format(len(self.decryption_errors))
                if self.decryption_errors:
                    log += '<ul>\n'
                    for title, reason in self.decryption_errors:
                        log += '<li>{}</li>\n'.format(title)
                    log += '</ul>\n'
                log += _('<p><b>New Books created:</b> {}</p>\n').format(len(self.ids_of_new_books))
                if self.ids_of_new_books:
                    log += '<ul>\n'
                    for id, mi in self.ids_of_new_books:
                        log += '<li>{}</li>\n'.format(mi.title)
                    log += '</ul>\n'
                if self.add_books_cancelled:
                    log += _('<p><b>Duplicates that weren\'t added:</b> {}</p>\n').format(len(self.duplicate_book_list))
                    if self.duplicate_book_list:
                        log += '<ul>\n'
                        for book in self.duplicate_book_list:
                            log += '<li>{}</li>\n'.format(book[0].title)
                        log += '</ul>\n'
                    cancelled_count = self.count - (len(self.decryption_errors) + len(self.ids_of_new_books) + len(self.duplicate_book_list))
                    if cancelled_count > 0:
                        log += _('<p><b>Book imports cancelled by user:</b> {}</p>\n').format(cancelled_count)
                    return (msg, log)
                log += _('<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n').format(len(self.successful_format_adds))
                if self.successful_format_adds:
                    log += '<ul>\n'
                    for id, mi in self.successful_format_adds:
                        log += '<li>{}</li>\n'.format(mi.title)
                    log += '</ul>\n'
                log += _('<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n').format(len(self.no_home_for_book))
                log += _('(Either because the user <i>chose</i> not to insert them, or because all duplicates already had an EPUB format)')
                if self.no_home_for_book:
                    log += '<ul>\n'
                    for mi in self.no_home_for_book:
                        log += '<li>{}</li>\n'.format(mi.title)
                    log += '</ul>\n'
                if self.add_formats_cancelled:
                    cancelled_count = self.count - (len(self.decryption_errors) + len(self.ids_of_new_books) + len(self.successful_format_adds) + len(self.no_home_for_book))
                    if cancelled_count > 0:
                        log += _('<p><b>Format imports cancelled by user:</b> {}</p>\n').format(cancelled_count)
                return (msg, log)
            else:

                # Single book ... don't get fancy.
                if self.ids_of_new_books:
                    title = self.ids_of_new_books[0][1].title
                elif self.successful_format_adds:
                    title = self.successful_format_adds[0][1].title
                elif self.no_home_for_book:
                    title = self.no_home_for_book[0].title
                elif self.decryption_errors:
                    title = self.decryption_errors[0][0]
                else:
                    title = _('Unknown Book Title')
                if self.decryption_errors:
                    reason = _('it couldn\'t be decrypted.')
                elif self.no_home_for_book:
                    reason = _('user CHOSE not to insert the new EPUB format, or all existing calibre entries HAD an EPUB format already.')
                else:
                    reason = _('of unknown reasons. Gosh I\'m embarrassed!')
                msg = _('<p>{0} not added because {1}').format(title, reason)
                return (msg, log)

