# -*- coding: utf-8 -*-
# Copyright (c) 2018  Red Hat, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Written by Jan Kaluza <jkaluza@redhat.com>

import time
import os
import shutil
from datetime import timedelta
from flufl.lock import Lock

from odcs.common.types import PungiSourceType, COMPOSE_STATES
from odcs.server import conf, log
from odcs.server.utils import makedirs


class KojiTagCache(object):
    """
    Class caching old composes of KOJI_TAG source_type generated by Pungi.

    The goal of this caching is to let Pungi reuse old repodata when running
    createrepo_c and make composes faster.
    Cached composes are stored in the "conf.targetdir/koji_tag_cache" in
    directories named according to Koji tag from which the compose is
    generated, compose flags, sigkeys and arches. These are basically all the
    ODCS compose attributes which influences the repodata.
    """

    def __init__(self, compose):
        self.cache_dir = os.path.join(compose.target_dir, "koji_tag_cache")
        makedirs(self.cache_dir)

    @classmethod
    def remove_old_koji_tag_cache_data(klass):
        """
        Removes the old unused Koji tag cache directories which has not been
        used for more than `conf.koji_tag_cache_cleanup_timeout` days.
        """
        # The koji_tag_cache_cleanup_timeout is in days, so convert it to
        # seconds.
        older_than_seconds = conf.koji_tag_cache_cleanup_timeout * 24 * 3600
        threshold = time.time() - older_than_seconds

        for target_dir in [conf.target_dir] + list(conf.extra_target_dirs.values()):
            cache_dir = os.path.join(target_dir, "koji_tag_cache")
            if not os.path.exists(cache_dir):
                continue

            for cached_dir in os.listdir(cache_dir):
                path = os.path.join(cache_dir, cached_dir)
                try:
                    mtime = os.path.getmtime(path)
                except OSError:
                    # Directory might have been removed in meantime by some
                    # 3rd party process.
                    log.exception(
                        "Old koji tag cache directory %s removed while checking "
                        "Koji cache." % path
                    )
                    continue

                if mtime > threshold:
                    # Koji tag directory is not old enough to be removed.
                    continue

                log.info("Removing old Koji tag cache data in %s." % path)
                shutil.rmtree(path)

    def cached_compose_dir(self, compose):
        """
        Returns the full path to caching directory belonging to `compose`.
        The returned full path is based on Koji tag, flags, sigkeys and arches
        attributes of `compose`.

        :param models.Compose compose: Compose to generate the cache path from.
        :rtype: str
        :return: Full path to caching directory belonging to compose.
        """
        koji_tag = compose.source
        flags = str(compose.flags)
        if compose.sigkeys:
            sigkeys = "-".join(sorted(compose.sigkeys.split(" ")))
        else:
            sigkeys = ""
        if compose.arches:
            arches = "-".join(sorted(compose.arches.split(" ")))
        else:
            arches = ""
        dir_name = "-".join([koji_tag, flags, sigkeys, arches])
        return os.path.join(self.cache_dir, dir_name)

    def is_cached(self, compose):
        """
        Returns True if there is old compose generated from the same Koji tag
        which can be reused when generating this `compose`.

        :param models.Compose compose: Compose to check.
        :rtype: bool
        :return: Returns True when there exists cached compose for input
            compose.
        """
        if compose.source_type != PungiSourceType.KOJI_TAG:
            return False

        return os.path.exists(self.cached_compose_dir(compose))

    def reuse_cached(self, compose):
        """
        Configures the Cache directory so the Pungi will find the old compose
        defined by KojiTagCache.cached_compose_dir when building the `compose`.

        :param models.Compose compose: Compose which will reuse the older
            cached compose.
        """

        cached_compose_dir = self.cached_compose_dir(compose)
        log.info("Reusing repodata from old cached compose %s", cached_compose_dir)

        # Create the lock. The rmtree and copytree on same fs should not take more
        # than 3 minutes really.
        lock = Lock(cached_compose_dir + ".lock")
        lock.lifetime = timedelta(minutes=3)

        try:
            lock.lock()

            compose_dir_name = "odcs-%d-1-19700101.n.0" % compose.id
            compose_dir = os.path.join(self.cache_dir, compose_dir_name)

            if os.path.exists(compose_dir):
                shutil.rmtree(compose_dir)
            shutil.copytree(cached_compose_dir, compose_dir, symlinks=True)
        finally:
            if lock.is_locked:
                lock.unlock()

    def cleanup_reused(self, compose):
        """
        Cleans-up the configuration done by `reuse_cached`. Should be called
        once the image depending on cached image is built.
        """
        compose_dir_name = "odcs-%d-1-19700101.n.0" % compose.id
        compose_dir = os.path.join(self.cache_dir, compose_dir_name)

        if os.path.exists(compose_dir):
            log.info("Removing cached reused compose %s" % compose_dir)
            shutil.rmtree(compose_dir)

    def update_cache(self, compose):
        """
        Updates the cache with newly built version of `compose`.

        :param models.Compose compose: Compose to update the cache from.
        """
        if (
            compose.source_type != PungiSourceType.KOJI_TAG
            or compose.state != COMPOSE_STATES["done"]
        ):
            log.info("Not caching the compose %s.", compose)
            return

        log.info("Caching the compose %s", compose)
        cached_compose_dir = self.cached_compose_dir(compose)
        compose_dir = os.path.realpath(compose.toplevel_dir)

        # Create the lock. The rmtree and copytree on same fs should not take more
        # than 3 minutes really.
        lock = Lock(cached_compose_dir + ".lock")
        lock.lifetime = timedelta(minutes=3)

        try:
            lock.lock()
            if os.path.exists(cached_compose_dir):
                shutil.rmtree(cached_compose_dir)
            shutil.copytree(compose_dir, cached_compose_dir, symlinks=True)
        finally:
            if lock.is_locked:
                lock.unlock()
