from datetime import datetime
from pathlib import Path

from qgis.core import Qgis
from qgis.PyQt.QtGui import QIcon

from oslandia.constants import (
    GITLAB_PLUGINS_REMOTE,
    PRIVATE_PROJECTS_REMOTE,
    PUBLIC_PROJECTS_REMOTE,
    UTM_CONTENT_SHOWCASE,
    VALID_EXTENSIONS,
)
from oslandia.datamodels.mdl_projects_browser import ProjectObj, ProjectsObj
from oslandia.gui.showcase.task_download_showcase_assets import ProjectAsset
from oslandia.toolbelt import PlgLogger
from oslandia.toolbelt.cache_manager import CacheManager
from oslandia.toolbelt.file_stats import is_file_older_than
from oslandia.toolbelt.network_manager import NetworkRequestsManager


class ShowcaseManager:
    def __init__(self):
        """Initialization."""
        self.cache_manager = CacheManager(".oslandia", "cache")
        self.network_manager = NetworkRequestsManager()
        self.log = PlgLogger().log

        # cache path json
        self.PUBLIC_PROJECTS_LOCAL = Path(
            self.cache_manager.get_cache_path / "showcase/public_plugins.json"
        )
        self.PRIVATE_PROJECTS_LOCAL = Path(
            self.cache_manager.get_cache_path / "showcase/private_plugins.json"
        )
        self.GITLAB_PLUGINS_LOCAL = Path(
            self.cache_manager.get_cache_path / "showcase/gitlab_plugins.json"
        )

    def update_json_projects(self):
        """Update the list of projects in the cache manager if they are more than a week old."""
        if not self.cache_manager.ensure_cache_dir_exists():
            self.cache_manager.create_cache_dir()

        self.download_if_needed(self.PUBLIC_PROJECTS_LOCAL, PUBLIC_PROJECTS_REMOTE, 30)
        self.download_if_needed(
            self.PRIVATE_PROJECTS_LOCAL, PRIVATE_PROJECTS_REMOTE, 30
        )
        self.download_if_needed(self.GITLAB_PLUGINS_LOCAL, GITLAB_PLUGINS_REMOTE, 30)

    @property
    def json_exists(self):
        """Check if json files exists."""

        if not Path(self.PUBLIC_PROJECTS_LOCAL).exists():
            return False

        if not Path(self.GITLAB_PLUGINS_LOCAL).exists():
            return False

        if not Path(self.PRIVATE_PROJECTS_LOCAL).exists():
            return False

        return True

    def force_update_json_projects(self):
        """Update the list of projects in the cache manager without regard to date."""
        if not self.cache_manager.ensure_cache_dir_exists():
            self.cache_manager.create_cache_dir()

        # Directly download without checking the file age
        self.network_manager.download_file_to(
            PUBLIC_PROJECTS_REMOTE,
            self.PUBLIC_PROJECTS_LOCAL,
            caller=UTM_CONTENT_SHOWCASE,
        )
        self.network_manager.download_file_to(
            PRIVATE_PROJECTS_REMOTE,
            self.PRIVATE_PROJECTS_LOCAL,
            caller=UTM_CONTENT_SHOWCASE,
        )
        self.network_manager.download_file_to(
            GITLAB_PLUGINS_REMOTE,
            self.GITLAB_PLUGINS_LOCAL,
            caller=UTM_CONTENT_SHOWCASE,
        )

    def download_if_needed(self, file_path: Path, url: str, days: int = 7) -> None:
        """Check if a download is required. A download is required if the file does not
        exist or is more than x days old (defined by days).

        :param file_path: Path file
        :type file_path: Path
        :param url: URL to download
        :type url: str
        :param days: File expiration time, defaults to 7
        :type days: int, optional
        """
        if is_file_older_than(
            local_file_path=file_path,
            expiration_rotating_hours=days * 24,
        ):
            self.network_manager.download_file_to(
                url, file_path, caller=UTM_CONTENT_SHOWCASE
            )
        else:
            self.log(
                message=f"File {file_path} is up to date",
                log_level=Qgis.MessageLevel.Info,
            )

    def get_last_modification_date(self, file_path: Path) -> datetime:
        """Return the last modification date of a file.

        :param file_path: Path to the file
        :type file_path: Path
        :return: The datetime of the last modification
        :rtype: datetime
        :raises FileNotFoundError: If the file does not exist
        """
        if not file_path.exists():
            raise FileNotFoundError("The specified file does not exist.")
        return datetime.fromtimestamp(file_path.stat().st_mtime)

    @property
    def get_date_json_projects_update(self) -> str:
        """Check the last modification date of a specific file.

        :param file_name: Name of the file to check
        :type file_name: str
        :return: Last modification date and time as a formatted string
        :rtype: str
        :raises FileNotFoundError: If the file does not exist
        """
        file_path = Path(self.PUBLIC_PROJECTS_LOCAL)
        last_modification_date = self.get_last_modification_date(file_path)
        return last_modification_date.strftime("%Y-%m-%d %H:%M:%S")

    def _download_project_assets_to_cache(
        self, attribute_name: str, subfolder: str
    ) -> None:
        """Download specified project assets to cache.

        :param attribute_name: Name of attribut in ProjectObj
        :type attribute_name: str
        :param subfolder: Name of subfolder witch contains images
        :type subfolder: str
        """
        projects_obj = ProjectsObj(self.cache_manager.get_cache_path)

        for project in projects_obj.get_projects():
            if attribute_name == "screenshot":
                project_subfolder = subfolder + f"/{project.name}"
            else:
                project_subfolder = subfolder

            asset_urls = getattr(project, attribute_name, None)
            if not asset_urls:
                continue

            # logo is a str, screenshot is a list: normalize to list
            if isinstance(asset_urls, str):
                asset_urls = [asset_urls]

            folder_path = Path(self.cache_manager.get_cache_path / project_subfolder)
            expected_files = []

            for index, asset_url in enumerate(asset_urls):
                extension = next(
                    (ext for ext in VALID_EXTENSIONS if ext in asset_url), None
                )
                if not extension:
                    continue
                base_name = project.name.lower().replace(" ", "_")
                if attribute_name == "screenshot":
                    filename = f"{base_name}_{index}{extension}"
                else:
                    filename = base_name + extension
                expected_files.append(filename)
                file_path = folder_path / filename
                if not file_path.exists():
                    self.download_if_needed(file_path=file_path, url=asset_url, days=30)

    def download_projects_icons_to_cache(self) -> None:
        """Download projects icons to cache."""
        self._download_project_assets_to_cache(
            attribute_name="logo",
            subfolder="icons",
        )

    def download_projects_images_to_cache(self) -> None:
        """Download projects images to cache."""
        self._download_project_assets_to_cache(
            attribute_name="screenshot",
            subfolder="images",
        )

    def get_missing_assets(
        self,
        projects: list[ProjectObj],
        attribute_name: str,
        subfolder: str,
        days: int = 30,
    ) -> list[ProjectAsset]:
        """Build the list of assets that need to be downloaded: those not yet
        in cache, or older than `days` days. Downloads nothing itself, meant
        to be used ahead of a ProjectAssetsDownloadTask.

        :param projects: list of projects to consider
        :type projects: list[ProjectObj]
        :param attribute_name: "logo" or "screenshot"
        :type attribute_name: str
        :param subfolder: cache subfolder ("icons" or "images")
        :type subfolder: str
        :param days: max age in days before re-downloading, defaults to 30
        :type days: int, optional
        :return: list of assets to download
        :rtype: list[ProjectAsset]
        """
        assets = []
        for project in projects:
            if attribute_name == "screenshot":
                project_subfolder = subfolder + f"/{project.name}"
            else:
                project_subfolder = subfolder

            asset_urls = getattr(project, attribute_name, None)
            if not asset_urls:
                continue

            # logo is a str, screenshot is a list: normalize to list
            if isinstance(asset_urls, str):
                asset_urls = [asset_urls]

            folder_path = Path(self.cache_manager.get_cache_path / project_subfolder)
            base_name = project.name.lower().replace(" ", "_")

            for index, asset_url in enumerate(asset_urls):
                extension = next(
                    (ext for ext in VALID_EXTENSIONS if ext in asset_url), None
                )
                if not extension:
                    continue

                if attribute_name == "screenshot":
                    filename = f"{base_name}_{index}{extension}"
                else:
                    filename = base_name + extension

                dest_path = folder_path / filename

                needs_download = is_file_older_than(
                    local_file_path=dest_path,
                    expiration_rotating_hours=days * 24,
                )

                if needs_download:
                    assets.append(
                        ProjectAsset(
                            project_name=project.name,
                            attribute_name=attribute_name,
                            url=asset_url,
                            dest_path=dest_path,
                        )
                    )
                else:
                    self.log(
                        message=f"File {dest_path} is up to date",
                        log_level=Qgis.MessageLevel.Info,
                    )
        return assets

    def get_project_icon_path(self, project_name: str, logo: str) -> Path | None:
        """Build the expected local path for a project's logo, whether the
        file already exists or not.

        :param project_name: Name of the project
        :type project_name: str
        :param logo: Path or name of the logo file
        :type logo: str
        :return: expected Path, or None if the extension is not valid
        :rtype: Path | None
        """
        extension = next((ext for ext in VALID_EXTENSIONS if ext in logo), None)
        if extension is None:
            return None
        icon_file = project_name.lower().replace(" ", "_") + extension
        return Path(self.cache_manager.get_cache_path / "icons" / icon_file)

    def get_project_icon(self, project_name: str, logo: str) -> QIcon | None:
        """Generate an icon for the project based on its logo path.

        :param project_name: Name of the project
        :type project_name: str
        :param logo: Path or name of the logo file
        :type logo: str
        :return: QIcon if logo extension is valid, otherwise None
        :rtype: Optional[QIcon]
        """
        icon_path = self.get_project_icon_path(project_name, logo)
        if icon_path is None:
            return None
        return QIcon(str(icon_path))

    def get_image_by_project_name(self, project_name: str) -> list[Path]:
        """Returns a list of screenshots and illustrations for a given project name.

        :param project_name: Project name from ProjectObj
        :type project_name: str
        :return: List of image paths
        :rtype: list
        """
        folder = Path(self.cache_manager.get_cache_path / "images" / project_name)
        if not folder.exists():
            return []
        return sorted(f for f in folder.iterdir() if f.is_file())
