import os
import tarfile

from gettext import gettext

from asgiref.sync import sync_to_async

from pulpcore.plugin.models import Artifact, Repository, ProgressReport
from pulpcore.plugin.stages import (
    ArtifactSaver,
    ContentSaver,
    DeclarativeVersion,
    QueryExistingArtifacts,
    QueryExistingContents,
    ResolveContentFutures,
    Stage,
)

from pulp_ostree.app.models import (
    OstreeCommit,
    OstreeConfig,
    OstreeObjectType,
)
from pulp_ostree.app.tasks.utils import get_checksum_filepath
from pulp_ostree.app.tasks.stages import OstreeAssociateContent, DeclarativeContentCreatorMixin

import gi

gi.require_version("OSTree", "1.0")
from gi.repository import Gio, GLib, OSTree  # noqa: E402: module level not at top of file


def import_all_refs_and_commits(artifact_pk, repository_pk, repository_name):
    """Import all ref and commits to an OSTree repository.

    Args:
        artifact_pk (str): The PK of an artifact that identifies a tarball.
        repository_pk (str): The repository PK.
        repository_name (str): The name of an OSTree repository (e.g., "repo").

    Raises:
        ValueError: If an OSTree repository could not be properly parsed or the specified ref
            does not exist.
    """
    tarball_artifact = Artifact.objects.get(pk=artifact_pk)
    repository = Repository.objects.get(pk=repository_pk)
    first_stage = OstreeImportAllRefsFirstStage(tarball_artifact, repository_name)
    dv = OstreeImportDeclarativeVersion(first_stage, repository)
    return dv.create()


def import_child_commits(artifact_pk, repository_pk, repository_name, ref):
    """Import child commits to a specific ref.

    Args:
        artifact_pk (str): The PK of an artifact that identifies a tarball.
        repository_pk (str): The repository PK.
        repository_name (str): The name of an OSTree repository (e.g., "repo").
        ref (str): The name of a ref object that points to the last commit.

    Raises:
        ValueError: If an OSTree repository could not be properly parsed or the specified ref
            does not exist.
    """
    tarball_artifact = Artifact.objects.get(pk=artifact_pk)
    repository = Repository.objects.get(pk=repository_pk)
    first_stage = OstreeImportSingleRefFirstStage(tarball_artifact, repository_name, ref)
    dv = OstreeImportDeclarativeVersion(first_stage, repository)
    return dv.create()


class OstreeSingleRefParserMixin:
    """A mixin class that allows stages to share the same methods for parsing OSTree data."""

    async def parse_ref(self, name, ref_commit_checksum, has_referenced_parent=False):
        """Parse a single ref object with associated commits and other objects."""
        _, ref_commit, _ = self.repo.load_commit(ref_commit_checksum)
        relative_path = get_checksum_filepath(
            ref_commit_checksum, OstreeObjectType.OSTREE_OBJECT_TYPE_COMMIT
        )
        ref_path = os.path.join("refs/", "heads/", name)

        parent_checksum = OSTree.commit_get_parent(ref_commit)
        if not parent_checksum:
            # there are not any parent commits, return and continue parsing the next ref
            commit = OstreeCommit(checksum=ref_commit_checksum)
            commit_dc = self.create_dc(relative_path, commit)
            await self.put(commit_dc)

            await self.submit_related_objects(commit_dc)

            self.init_ref_object(name, ref_path, commit_dc)

            return

        checksum = ref_commit_checksum
        ref_commit = OstreeCommit(checksum=checksum)
        ref_commit_dc = self.create_dc(relative_path, ref_commit)
        self.commit_dcs.append(ref_commit_dc)

        self.init_ref_object(name, ref_path, ref_commit_dc)

        try:
            _, parent_commit, _ = self.repo.load_commit(parent_checksum)
        except GLib.Error:
            if has_referenced_parent:
                # the associated parent commit may not be present in the parsed tarball
                # and this state is still considered valid
                return parent_checksum, ref_commit_dc
            else:
                raise ValueError(
                    gettext("The parent commit '{}' could not be loaded").format(parent_checksum)
                )

        return await self.load_next_commits(parent_commit, parent_checksum, has_referenced_parent)

    async def load_next_commits(self, parent_commit, checksum, has_referenced_parent=False):
        """Queue next parent commits if exist."""
        relative_path = get_checksum_filepath(checksum, OstreeObjectType.OSTREE_OBJECT_TYPE_COMMIT)

        parent_checksum = OSTree.commit_get_parent(parent_commit)

        while parent_checksum:
            commit = OstreeCommit(checksum=checksum)
            commit_dc = self.create_dc(relative_path, commit)
            self.commit_dcs.append(commit_dc)

            checksum = parent_checksum
            relative_path = get_checksum_filepath(
                checksum, OstreeObjectType.OSTREE_OBJECT_TYPE_COMMIT
            )
            try:
                _, parent_commit, _ = self.repo.load_commit(parent_checksum)
            except GLib.Error:
                if has_referenced_parent:
                    # the associated parent commit may not be present in the parsed tarball
                    # and this state is still considered valid
                    return parent_checksum, commit_dc
                else:
                    raise ValueError(
                        gettext("The parent commit '{}' could not be loaded").format(
                            parent_checksum
                        )
                    )
            parent_checksum = OSTree.commit_get_parent(parent_commit)

        commit = OstreeCommit(checksum=checksum)
        commit_dc = self.create_dc(relative_path, commit)
        self.commit_dcs.append(commit_dc)

        await self.put(commit_dc)
        await self.submit_related_objects(commit_dc)

        await self.submit_previous_commits_and_related_objects()


class OstreeImportStage(Stage):
    """A stage generalizing the common methods for initializing an OSTree repository."""

    def __init__(self, repo_name):
        """Initialize class variables that are common for tasks that import OSTree content."""
        super().__init__()

        self.repo_name = repo_name
        self.repo = None
        self.repo_path = None

        self.commit_dcs = []
        self.refs_dcs = []

    def init_repository(self):
        """Initialize new OSTree repository objects."""
        self.repo_path = os.path.join(os.getcwd(), self.repo_name)

        self.repo = OSTree.Repo.new(Gio.File.new_for_path(self.repo_path))

        try:
            self.repo.open()
        except GLib.Error:
            raise ValueError(
                gettext("An invalid path to the repository provided: {}").format(self.repo_name)
            )


class OstreeImportSingleRefFirstStage(
    DeclarativeContentCreatorMixin, OstreeSingleRefParserMixin, OstreeImportStage
):
    """A first stage of the OSTree importing pipeline that appends child commits to a repository."""

    def __init__(self, tarball_artifact, repo_name, ref):
        """Initialize class variables used for parsing OSTree objects."""
        super().__init__(repo_name)
        self.tarball_artifact = tarball_artifact
        self.ref = ref

        self.create_object_dc_func = self.create_dc

    async def run(self):
        """Create OSTree content units and associate them with the parent commit."""
        with tarfile.open(self.tarball_artifact.file.path) as tar:
            async with ProgressReport(
                message="Adding the child commits", code="adding.commits", total=1
            ) as pb:
                tar.extractall(path=os.getcwd())
                self.init_repository()

                last_commit_dc = None
                _, refs = self.repo.list_refs()
                for name, ref_commit_checksum in refs.items():
                    if self.ref == name:
                        parent_checksum, last_commit_dc = await self.parse_ref(
                            name, ref_commit_checksum, has_referenced_parent=True
                        )
                        break

                if last_commit_dc is None:
                    raise ValueError(
                        gettext("An invalid ref name in the repository was specified: {}").format(
                            self.ref
                        )
                    )

                try:
                    parent_commit = await sync_to_async(OstreeCommit.objects.get)(
                        checksum=parent_checksum
                    )
                except OstreeCommit.DoesNotExist:
                    pass
                else:
                    last_commit_dc.extra_data["parent_commit"] = parent_commit
                    await self.put(last_commit_dc)
                    await self.submit_related_objects(last_commit_dc)

                await self.submit_previous_commits_and_related_objects()

                await pb.aincrement()

            await self.submit_ref_objects()


class OstreeImportAllRefsFirstStage(
    DeclarativeContentCreatorMixin, OstreeSingleRefParserMixin, OstreeImportStage
):
    """A first stage of the OSTree importing pipeline that handles creation of content units."""

    def __init__(self, tarball_artifact, repo_name):
        """Initialize class variables used for parsing OSTree objects."""
        super().__init__(repo_name)
        self.tarball_artifact = tarball_artifact

        self.create_object_dc_func = self.create_dc

    async def run(self):
        """Create OSTree content units and declare relations between them."""
        with tarfile.open(self.tarball_artifact.file.path) as tar:
            async with ProgressReport(
                message="Committing the tarball", code="committing.tarball", total=1
            ) as pb:
                tar.extractall(path=os.getcwd())
                self.init_repository()

                await self.submit_metafile_object("config", OstreeConfig())

                _, refs = self.repo.list_refs()
                for name, ref_commit_checksum in refs.items():
                    await self.parse_ref(name, ref_commit_checksum)

                await pb.aincrement()

            await self.submit_ref_objects()


class OstreeImportDeclarativeVersion(DeclarativeVersion):
    """A customized DeclarativeVersion class that creates a pipeline for the OSTree import."""

    def pipeline_stages(self, new_version):
        """Build a list of stages."""
        pipeline = [
            self.first_stage,
            QueryExistingArtifacts(),
            ArtifactSaver(),
            QueryExistingContents(),
            ContentSaver(),
            ResolveContentFutures(),
            OstreeAssociateContent(),
        ]

        return pipeline
