# -*- coding: UTF-8 -*-

__revision__ = '$Id$'

# Copyright (c) 2005-2009 Vasco Nunes, Piotr Ożarowski
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA

# You may use and distribute this software under the terms of the
# GNU General Public License, version 2 or later

import datetime
import logging
import os.path
import zipfile
from io import StringIO
from shutil import rmtree, move
from tempfile import mkdtemp

from gi.repository import Gtk
from sqlalchemy import create_engine

import config
import gutils
import db
import sql
from initialize import dictionaries, people_treeview
from gutils import read_file
from gettext import gettext as _

try:
    pass
except Exception:
    pass

log = logging.getLogger('Griffith')


def create(self):
    """perform a compressed griffith database/posters/preferences backup"""
    # if self.db.session.bind.engine.name != 'sqlite':
    #     gutils.error(_("Backup function is available only for SQLite engine for now"), self.widgets['window'])
    #     return False

    default_name = "%s_backup_%s.zip" % (
        self.config.get('name', 'griffith', section='database'),
        datetime.date.isoformat(datetime.datetime.now()))
    filename = gutils.file_chooser(
        _("Save Griffith backup"),
        action=Gtk.FileChooserAction.SAVE, name=default_name,
        buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE,
                 Gtk.ResponseType.OK))

    if filename and filename[0]:
        proceed = True
        zipfilename = filename[0]
        log.debug('Backup filename: %s', zipfilename)
        if os.path.isfile(zipfilename):
            if not gutils.question(_("File exists. Do you want to overwrite it?"), window=self.widgets['window']):
                proceed = False

        if proceed:
            try:
                if zipfile.zlib is not None:
                    log.debug('Creating zip file with compression')
                    mzip = zipfile.ZipFile(zipfilename, 'w', zipfile.ZIP_DEFLATED)
                else:
                    log.debug('Creating zip file without compression')
                    mzip = zipfile.ZipFile(zipfilename, 'w')
            except Exception:
                gutils.error(_("Error creating backup"), self.widgets['window'])
                return False
            log.debug('Preparing data and saving it to the zip archive')
            if self.db.session.bind.engine.name == 'sqlite':
                mzip.write(os.path.join(self.locations['home'], 'griffith.cfg').encode('utf-8'), arcname='griffith.cfg')
                db_file_name = "%s.db" % self.config.get('name', 'griffith', section='database')
                file_path = os.path.join(self.locations['home'], db_file_name).encode('utf-8')
                mzip.write(file_path, arcname=db_file_name)
            else:
                tmp_engine = None
                try:
                    tmp_dir = mkdtemp()
                    tmp_config_file = os.path.join(tmp_dir, 'griffith.cfg')
                    self.config.save(tmp_config_file)
                    tmp_config = config.Config(file=tmp_config_file)
                    tmp_config.set('type', 'sqlite', section='database')
                    tmp_config.set('file', 'griffith.db', section='database')
                    tmp_config.set('name', 'griffith', section='database')
                    tmp_config.save()
                    mzip.write(tmp_config._file, arcname='griffith.cfg')

                    tmp_file = os.path.join(tmp_dir, 'griffith.db')
                    tmp_engine = create_engine("sqlite:///%s" % tmp_file)
                    db.metadata.create_all(bind=tmp_engine)

                    # SQLite doesn't care about foreign keys much so we can just copy the data
                    with self.db.engine.connect() as src_conn, tmp_engine.begin() as tmp_conn:
                        for table in db.metadata.sorted_tables:
                            if table.name in ('posters', 'filters'):
                                continue  # see below
                            result = src_conn.execute(table.select())
                            data = result.fetchall()
                            if data:
                                tmp_conn.execute(table.insert(), data)

                    # posters
                    with self.db.engine.connect() as src_conn, tmp_engine.begin() as tmp_conn:
                        result = src_conn.execute(db.metadata.tables['posters'].select())
                        for poster in result:
                            tmp_conn.execute(
                                db.metadata.tables['posters'].insert(),
                                {
                                    'md5sum': poster.md5sum,
                                    'data': StringIO(poster.data).read(),
                                    },
                                )

                    mzip.write(tmp_file, arcname='griffith.db')
                finally:
                    # disposing the temporary db connection before rmtree and in finally block to avoid locked db file
                    if tmp_engine:
                        tmp_engine.dispose()
                    rmtree(tmp_dir)
            gutils.info(_("Backup has been created"), self.widgets['window'])


@gutils.popup_message(_('Restoring database...'))
def copy_db(src_engine, dst_engine):
    log.debug('replacing old database with new one')
    db.metadata.drop_all(dst_engine)  # remove all previous data
    db.metadata.create_all(dst_engine)  # create table stucture

    # posters
    with src_engine.connect() as src_conn, dst_engine.begin() as dst_conn:
        result = src_conn.execute(db.metadata.tables['posters'].select())
        for poster in result:
            dst_conn.execute(
                db.metadata.tables['posters'].insert(),
                {'md5sum': poster.md5sum, 'data': poster.data},
                )

    with src_engine.connect() as src_conn, dst_engine.begin() as dst_conn:
        for table in db.metadata.sorted_tables:
            if table.name in ('posters',):
                continue  # see above
            log.debug('... processing %s table', table)
            result = src_conn.execute(table.select())
            data = [dict((col.key, row[col.name]) for col in table.c) for row in result]
            if data:
                log.debug('inserting new data...')
                insertcmd = table.insert()
                # insert in steps of 100 items because otherwise there is an error with mysql
                # I tried to insert more than 800 movies at ones: OperationalError
                for partition in range(0, len(data), 10):
                    dst_conn.execute(insertcmd, data[partition:partition + 10])

                if dst_engine.name in ('postgres', 'postgresql'):
                    # update current value of sequences
                    primary_column_name = list(table.primary_key.keys())[0]
                    if primary_column_name.endswith('_id'):
                        currval = max(row[primary_column_name] for row in data)
                        query = "SELECT setval('%s_%s_seq', %s)" % (table.name, primary_column_name, currval)
                        log.debug('updating sequence: %s', query)
                        try:
                            dst_conn.exec_driver_sql(query)
                        except Exception as e:
                            e = getattr(e, 'message', e)
                            log.error('... cannot update sequence: %s', e)


def merge_db(src_db, dst_db):  # FIXME
    merged = 0
    dst_db.session.rollback()  # cancel all pending operations
    src_session = src_db.Session()  # create new session
    dst_session = dst_db.Session()  # create new session
    src_session.query(db.Movie).count()
    for movie in src_session.query(db.Movie).all():
        if dst_session.query(db.Movie).filter_by(o_title=movie.o_title).first() is not None:
            continue
        t_movies = {}
        for column in list(movie.mapper.c.keys()):
            t_movies[column] = eval("movie.%s" % column)

        # replace number with new one
        t_movies["number"] = gutils.find_next_available(dst_db)

        # don't restore volume/collection/tag/language/loan data (it's dangerous)
        t_movies.pop('movie_id')
        t_movies.pop('loaned')
        t_movies.pop('volume_id')
        t_movies.pop('collection_id')

        if dst_db.add_movie(t_movies):  # FIXME
            print(t_movies)

        if movie.image is not None:
            # FIXME: where does "self" and "tmp_dir" come from
            dest_file = os.path.join(self.locations['posters'], movie.image + '.jpg')
            if not os.path.isfile(dest_file):
                src_file = os.path.join(tmp_dir, movie.image + '.jpg')
                if os.path.isfile(src_file):
                    move(src_file, dest_file)
        merged += 1
    return merged


def restore(self, merge=False):
    """
    Merge database from:
    * compressed backup (*.zip)
    * SQLite2 *.gri file
    * SQLite3 *.db file
    """
    # let user select a backup file
    filename, path = gutils.file_chooser(
        _("Restore Griffith backup"),
        action=Gtk.FileChooserAction.OPEN, backup=True,
        buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                 Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
    if not filename:
        log.debug('no file selected')
        return False

    try:
        tmp_db = None
        tmp_dir = mkdtemp()
        os.mkdir(os.path.join(tmp_dir, 'posters'))
        print(filename)
        if filename.lower().endswith('.zip'):
            try:
                zip_file = zipfile.ZipFile(filename, 'r')
            except Exception as ex:
                gutils.error(_(f"Can't read backup file, {type(ex).__name__}: "
                               f"{ex}"), self.widgets['window'])
                return False

            old_config_file = False
            # unpack files to temporary directory
            for file_path in zip_file.namelist():
                file_name = os.path.split(file_path)[-1]
                if not os.path.isdir(file_name):
                    if not file_name:
                        log.debug('skipping %s', file_path)
                        continue

                    if 'posters' in file_path:
                        new_file = os.path.join(tmp_dir, 'posters', file_name)
                    else:
                        new_file = os.path.join(tmp_dir, file_name)
                    if file_name.endswith('.conf'):
                        old_config_file = new_file
                    with open(new_file, 'wb') as outfile:
                        outfile.write(zip_file.read(file_path))
            zip_file.close()

            # restore config file (new one will be created if old config
            # format is detected)
            tmp_config = config.Config(file=os.path.join(tmp_dir, 'griffith.cfg'))
            if old_config_file:
                log.info('Old config file detected. Please note that it will '
                         'not be used.')
                old_config_raw_data = read_file(old_config_file)
                if old_config_raw_data.find('griffith.gri') >= -1:
                    tmp_config.set('file', 'griffith.gri', section='database')

            # update filename var. to point to the unpacked database
            filename = os.path.join(tmp_dir, tmp_config.get('name', 'griffith', section='database') + '.db')
        else:  # not a zip file? prepare a fake config file then
            tmp_config = config.Config(file=os.path.join(tmp_dir, 'griffith.cfg'))
            tmp_config.set('type', 'sqlite', section='database')
            tmp_config.set('file', 'griffith.db', section='database')

        # prepare temporary GriffithSQL instance
        locations = {'home': tmp_dir}
        # check if file needs conversion
        if filename.lower().endswith('.gri'):
            from .dbupgrade import convert_from_old_db
            tmp_db = convert_from_old_db(tmp_config, filename, os.path.join(tmp_dir, 'griffith.db'), locations)
            if not tmp_db:
                log.info("MERGE: Can't convert database, aborting.")
                return False
        else:
            tmp_db = sql.GriffithSQL(tmp_config, tmp_dir, fallback=False)

        if merge:
            merge_db(tmp_db, self.db)
        else:
            self.db.session.rollback()  # cancel all pending operations
            copy_db(tmp_db.session.bind, self.db.session.bind)
            # update old database section with current config values
            # (important while restoring to external databases)
            for key in ('name', 'passwd', 'host', 'user', 'file', 'type', 'port'):
                tmp_config.set(key, self.config.get(key, section='database'), section='database')
            tmp_config._file = self.config._file
            self.config = tmp_config
            self.config.save()
            dictionaries(self)
            people_treeview(self)
            # let's refresh the treeview
            self.clear_details()
            self.populate_treeview()
        # gutils.info(_("Databases merged!\n\nProcessed movies: %s\n"
        #               "Merged movies: %s"%(movies, merged)),
        #             self.widgets['window'])
        gutils.info(_("Backup restored"), self.widgets['window'])
    except Exception:
        log.exception('')
        raise
    finally:
        # disposing the temporary db connection before rmtree and
        # in finally block to avoid locked db file
        if tmp_db:
            tmp_db.dispose()
        log.debug('temporary directory no logger needed, removing %s', tmp_dir)
        rmtree(tmp_dir)
