#    Copyright 2025 Genesis Corporation.
#
#    All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.
from __future__ import annotations

import json
import os
import pathlib
import shutil

from packaging import version

from exordos import constants as c
from exordos import logger as logger_base
from exordos.builder import base as builder_base
from exordos.repo import base
from exordos.repo.utils import get_published


class FSRepoDriver(base.AbstractRepoDriver):
    def __init__(
        self,
        path: str,
        name: str = "fs_repo",
        logger: logger_base.AbstractLogger = logger_base.ClickLogger(),
    ):
        self._repo_path = path
        self._logger = logger
        self._name = name

    @property
    def name(self) -> str:
        return self._name

    def _push_directory(
        self, element: builder_base.ElementInventory, dst: str, name: str
    ) -> None:
        if artifacts := getattr(element, name):
            os.makedirs(os.path.join(dst, name), exist_ok=True)
            for artifact in artifacts:
                artifact_name = os.path.basename(artifact)
                shutil.copyfile(artifact, os.path.join(dst, name, artifact_name))
                self._logger.info(f"Pushed {artifact_name} to {dst}")

            self._logger.info(f"Finished pushing {name}")

    @property
    def elements_path(self) -> str:
        return os.path.join(self._repo_path, c.ELEMENT_REPO_PATH)

    def elements_inventory_path(self, element: builder_base.ElementInventory) -> str:
        """Get the base path for elements in the repository."""
        return os.path.join(
            self.elements_path, element.name, element.version, "inventory.json"
        )

    def elements_inventory_path_latest(
        self, element: builder_base.ElementInventory
    ) -> str:
        """Get the base path for elements in the repository as latest."""
        return os.path.join(
            self.elements_path, element.name, "latest", "inventory.json"
        )

    def init_repo(self) -> None:
        """Initialize the repo."""
        elements_path = self.elements_path
        meta_path = os.path.join(self._repo_path, "exordos-repo-meta.json")

        # Check if repo is already initialized
        if os.path.exists(elements_path) or os.path.exists(meta_path):
            raise base.RepoAlreadyExistsError(
                f"Repo {self._repo_path} already initialized."
            )
        os.makedirs(self.elements_path, exist_ok=True)

        meta = base.RepoMetaV1()
        with open(meta_path, "w") as f:
            json.dump(meta.to_dict(), f, indent=2)

    def delete_repo(self) -> None:
        """Delete the repo."""
        elements_path = self.elements_path
        meta_path = os.path.join(self._repo_path, "exordos-repo-meta.json")

        if os.path.exists(elements_path):
            os.rmdir(elements_path)

        if os.path.exists(meta_path):
            os.remove(meta_path)

    def push(
        self, element: builder_base.ElementInventory, latest: bool = False
    ) -> None:
        """Push the element to the repo."""
        element_path = os.path.join(self.elements_path, element.name, element.version)
        if os.path.exists(element_path):
            raise base.ElementAlreadyExistsError(
                f"Element {element.name} version {element.version} already exists."
            )

        os.makedirs(element_path, exist_ok=True)

        # Push all artifacts
        for name in builder_base.ElementInventory.categories():
            self._push_directory(element, element_path, name)

        # Push inventory
        spec = element.to_dict()
        for category in element.categories():
            spec[category] = [
                os.path.basename(artifact) for artifact in getattr(element, category)
            ]

        spec["published"] = get_published()

        with open(self.elements_inventory_path(element), "w") as f:
            json.dump(spec, f, indent=2)

        if latest and not version.parse(element.version).is_prerelease:
            # Push latest
            latest_element_path = os.path.join(
                self.elements_path, element.name, "latest"
            )

            shutil.rmtree(latest_element_path, ignore_errors=True)
            os.makedirs(latest_element_path, exist_ok=True)

            # Push all artifacts to latest
            for name in builder_base.ElementInventory.categories():
                self._push_directory(element, latest_element_path, name)

            with open(self.elements_inventory_path_latest(element), "w") as f:
                json.dump(spec, f, indent=2)

    def pull(self, element: builder_base.ElementInventory, dst_path: str) -> None:
        """Pull the element from the repo."""
        # Pull all artifacts
        shutil.copytree(
            os.path.join(self.elements_path, element.name, element.version),
            dst_path,
            dirs_exist_ok=True,
        )

        # Pull inventory
        with open(self.elements_inventory_path(element), "r") as f:
            spec = json.load(f)

        inventory_path = os.path.join(dst_path, "inventory.json")
        with open(inventory_path, "w") as f:
            json.dump(spec, f, indent=2)

        self._logger.info(f"Pulled {element.name} version {element.version}")

    def remove(self, element: builder_base.ElementInventory) -> None:
        """Remove the element from the repo."""
        element_path = os.path.join(self.elements_path, element.name, element.version)
        if os.path.exists(element_path):
            shutil.rmtree(element_path)
        self._logger.info(f"Removed {element.name} version {element.version}")

    def list(self) -> dict[str, list[str]]:
        """List the elements in the repo."""
        try:
            return {
                name: os.listdir(os.path.join(self.elements_path, name))
                for name in os.listdir(self.elements_path)
            }
        except FileNotFoundError:
            raise base.RepoNotFoundError(f"Repo {self._repo_path} not found.")

    def inventories(self) -> dict:
        """Return the repo inventory."""
        with open(pathlib.Path(self.elements_path) / "inventory.json") as f:
            return json.load(f)["elements"]

    def inventory(
        self, element_name: str, element_version: str | None = None
    ) -> builder_base.ElementInventory:
        inventories = self.inventories()
        versions: dict = inventories.get(element_name)

        if not versions:
            raise ValueError(f"No `{element_name}` element found")

        # Get any version of element in the inventory
        if element_version is None:
            version, inventory_dict = next(iter(versions.items()))
            inventory_dir = pathlib.Path(self.elements_path) / element_name / version
            inventory = builder_base.ElementInventory.from_dict(inventory_dict)
            return inventory.replace_with_abspath(inventory_dir)

        # Get the particular version of the element
        inventory_dict = versions.get(element_version)
        if inventory_dict is None:
            raise ValueError(
                f"No `{element_name}` element with version `{element_version}` found"
            )

        inventory_dir = (
            pathlib.Path(self.elements_path) / element_name / element_version
        )
        inventory = builder_base.ElementInventory.from_dict(inventory_dict)
        return inventory.replace_with_abspath(inventory_dir)
