import glob
import urllib.parse
from collections.abc import Sequence
from functools import cached_property
from shlex import quote
from typing import ClassVar, Optional

import tmt.utils
from tmt.container import container, simple_field
from tmt.guest import Guest, TransferOptions
from tmt.package_managers._rpm import RpmVersion
from tmt.steps.prepare.artifact.providers import (
    ArtifactInfo,
    ArtifactProvider,
    ArtifactProviderId,
    DownloadError,
    provides_artifact_provider,
)
from tmt.utils import ShellScript


@provides_artifact_provider("file")
@container
class PackageAsFileArtifactProvider(ArtifactProvider):
    """
    Provider for preparing artifacts from local or remote package files.

    This provider can handle:
    - Glob patterns matching multiple package files.
    - Local package files specified by absolute or relative paths.
    - Remote package files accessible via URLs.
    - Directories containing package files (all packages in the directory are included).

    Example usage:

    .. code-block:: yaml

        prepare:
          - summary: package files
            how: artifact
            stage: prepare
            provide:
              - file:/tmp/*.rpm                    # Local glob
              - file:/build/specific.rpm           # Single file
              - file:https://example.com/pkg.rpm   # Remote URL
              - file:/path/to/packages/            # Directory
    """

    SUPPORTED_PREFIX: ClassVar[str] = "file"

    _source: str = simple_field(init=False)
    _is_url: bool = simple_field(init=False)

    def __post_init__(self) -> None:
        super().__post_init__()
        source = self.raw_id[len(f"{self.SUPPORTED_PREFIX}:") :]
        parsed = urllib.parse.urlparse(source)
        self._source = source
        self._is_url = parsed.scheme in ("http", "https")

    @classmethod
    def _extract_provider_id(cls, raw_id: str) -> ArtifactProviderId:
        if not raw_id.startswith(f"{cls.SUPPORTED_PREFIX}:"):
            raise ValueError(f"Unsupported provider id: '{raw_id}'.")
        return ArtifactProviderId(raw_id)

    def make_rpm_artifact(self, path: str) -> ArtifactInfo:
        return ArtifactInfo(
            version=RpmVersion.from_filename(tmt.utils.Path(path).name),
            location=path,
            provider=self,
        )

    @cached_property
    def artifacts(self) -> Sequence[ArtifactInfo]:
        artifacts: list[ArtifactInfo] = []
        seen_ids: set[str] = set()

        def add(info: ArtifactInfo) -> None:
            if info.id not in seen_ids:
                artifacts.append(info)
                seen_ids.add(info.id)
            else:
                self.logger.warning(
                    f"Duplicate artifact '{info.id}' found; ignoring duplicate entry."
                )

        if self._is_url:
            add(self.make_rpm_artifact(self._source))
        # Everything else is treated as a glob pattern
        elif matched_files := glob.glob(self._source):
            for matched_file in sorted(matched_files):
                f = tmt.utils.Path(matched_file)
                if f.is_dir():  # find all .rpm files within it
                    for rpm_file in sorted(f.glob("*.rpm")):
                        add(self.make_rpm_artifact(str(rpm_file)))
                elif f.is_file():
                    add(self.make_rpm_artifact(str(f)))
        else:
            self.logger.warning(f"No files matched pattern: '{self._source}'.")

        if not artifacts:
            self.logger.warning(f"No artifacts found for source: '{self._source}'.")

        return artifacts

    def _download_artifact(
        self, artifact: ArtifactInfo, guest: Guest, destination: tmt.utils.Path
    ) -> None:
        try:
            if self._is_url:  # Remote file, download it
                guest.download(artifact.location, destination)
            else:  # Local file, push it to the guest
                # When pushing a single file, use recursive=False. The default recursive=True
                # treats the source as a directory (appending "/."), which only works for
                # directories and fails when pushing individual files.
                guest.push(
                    tmt.utils.Path(artifact.location),
                    destination,
                    options=TransferOptions(recursive=False),
                )
            self.logger.info(f"Successfully downloaded: '{artifact.id}'.")
        except Exception as error:
            raise DownloadError(f"Failed to download '{artifact}'.") from error

    def contribute_to_shared_repo(
        self,
        guest: Guest,
        source_path: tmt.utils.Path,
        shared_repo_dir: tmt.utils.Path,
        exclude_patterns: Optional[list[tmt.utils.Pattern[str]]] = None,
    ) -> None:
        guest.execute(
            ShellScript(f"cp {quote(str(source_path))}/*.rpm {quote(str(shared_repo_dir))}")
        )
        self.logger.info(f"Contributed artifacts from '{source_path}' to '{shared_repo_dir}'.")
