# Copyright 2017 LasLabs Inc.
# Copyright 2018 ACSONE SA/NV.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).

import json
import logging
import os

from odoo import _, api, exceptions, models, tools
from odoo.modules.module import get_module_path

from ..addon_hash import addon_hash

PARAM_INSTALLED_CHECKSUMS = "module_auto_update.installed_checksums"
PARAM_EXCLUDE_PATTERNS = "module_auto_update.exclude_patterns"
DEFAULT_EXCLUDE_PATTERNS = "*.pyc,*.pyo,i18n/*.pot,i18n_extra/*.pot,static/*"

_logger = logging.getLogger(__name__)


class FailedUpgradeError(exceptions.UserError):
    pass


class IncompleteUpgradeError(exceptions.UserError):
    pass


def ensure_module_state(env, modules, state):
    # read module states, bypassing any Odoo cache
    if not modules:
        return
    env.cr.execute(
        "SELECT name FROM ir_module_module " "WHERE id IN %s AND state != %s",
        (tuple(modules.ids), state),
    )
    names = [r[0] for r in env.cr.fetchall()]
    if names:
        raise FailedUpgradeError(
            "The following modules should be in state '%s' "
            "at this stage: %s. Bailing out for safety."
            % (
                state,
                ",".join(names),
            ),
        )


class Module(models.Model):
    _inherit = "ir.module.module"

    def _get_checksum_dir(self):
        self.ensure_one()

        exclude_patterns = self.env["ir.config_parameter"].get_param(
            PARAM_EXCLUDE_PATTERNS,
            DEFAULT_EXCLUDE_PATTERNS,
        )
        exclude_patterns = [p.strip() for p in exclude_patterns.split(",")]
        keep_langs = self.env["res.lang"].search([]).mapped("code")

        module_path = get_module_path(self.name)
        if module_path and os.path.isdir(module_path):
            checksum_dir = addon_hash(
                module_path,
                exclude_patterns,
                keep_langs,
            )
        else:
            checksum_dir = False

        return checksum_dir

    @api.model
    def _get_saved_checksums(self):
        Icp = self.env["ir.config_parameter"]
        return json.loads(Icp.get_param(PARAM_INSTALLED_CHECKSUMS, "{}"))

    @api.model
    def _save_checksums(self, checksums):
        Icp = self.env["ir.config_parameter"]
        Icp.set_param(PARAM_INSTALLED_CHECKSUMS, json.dumps(checksums))
        Icp.flush_model()

    @api.model
    def _save_installed_checksums(self):
        checksums = {}
        installed_modules = self.search([("state", "=", "installed")])
        for module in installed_modules:
            checksums[module.name] = module._get_checksum_dir()
        self._save_checksums(checksums)

    @api.model
    def _get_modules_partially_installed(self):
        return self.search([("state", "in", ("to install", "to remove", "to upgrade"))])

    @api.model
    def _get_modules_with_changed_checksum(self):
        saved_checksums = self._get_saved_checksums()
        installed_modules = self.search([("state", "=", "installed")])
        return installed_modules.filtered(
            lambda r: r._get_checksum_dir() != saved_checksums.get(r.name),
        )

    @api.model
    def upgrade_changed_checksum(self, overwrite_existing_translations=False):
        """Run an upgrade of the database, upgrading only changed modules.

        Installed modules for which the checksum has changed since the
        last successful run of this method are marked "to upgrade",
        then the normal Odoo scheduled upgrade process
        is launched.

        If there is no module with a changed checksum, and no module in state
        other than installed, uninstalled, uninstallable, this method does
        nothing, otherwise the normal Odoo upgrade process is launched.

        After a successful upgrade, the checksums of installed modules are
        saved.

        In case of error during the upgrade, an exception is raised.
        If any module remains to upgrade or to uninstall after the upgrade
        process, an exception is raised as well.

        Note: this method commits the current transaction at each important
        step, it is therefore not intended to be run as part of a
        larger transaction.
        """
        _logger.info(
            "Checksum upgrade starting (i18n-overwrite=%s)...",
            overwrite_existing_translations,
        )

        tools.config[
            "overwrite_existing_translations"
        ] = overwrite_existing_translations

        _logger.info("Updating modules list...")
        self.update_list()
        changed_modules = self._get_modules_with_changed_checksum()
        if not changed_modules and not self._get_modules_partially_installed():
            _logger.info(
                "No checksum change detected in installed modules "
                "and all modules installed, nothing to do."
            )

            return {
                "type": "ir.actions.client",
                "tag": "display_notification",
                "params": {
                    "message": _(
                        "No checksum change detected in installed modules "
                        "and all modules installed, nothing to do."
                    ),
                    "type": "success",
                    "sticky": False,
                },
            }

        _logger.info(
            "Marking the following modules to upgrade, "
            "for their checksums changed: %s...",
            ",".join(changed_modules.mapped("name")),
        )
        changed_modules.button_upgrade()
        self.env.cr.commit()  # pylint: disable=invalid-commit
        # in rare situations, button_upgrade may fail without
        # exception, this would lead to corruption because
        # no upgrade would be performed and save_installed_checksums
        # would update cheksums for modules that have not been upgraded
        ensure_module_state(self.env, changed_modules, "to upgrade")

        _logger.info("Upgrading...")
        self.env["base.module.upgrade"].upgrade_module()
        self.env.cr.commit()  # pylint: disable=invalid-commit

        _logger.info("Upgrade successful, updating checksums...")
        self._save_installed_checksums()
        self.env.cr.commit()  # pylint: disable=invalid-commit

        partial_modules = self._get_modules_partially_installed()
        if partial_modules:
            raise IncompleteUpgradeError(
                "Checksum upgrade successful "
                "but incomplete for the following modules: %s"
                % ",".join(partial_modules.mapped("name"))
            )

        _logger.info("Checksum upgrade complete.")
        return {
            "type": "ir.actions.client",
            "tag": "display_notification",
            "params": {
                "message": _("Checksum upgrade complete."),
                "type": "success",
                "sticky": False,
            },
        }
