"""
Collector plugin to copy artifacts using the Nexus API
"""

import json
import os
import re

from pathlib import Path
from time import sleep
from typing import Any, Dict, Optional
from urllib.parse import urljoin, urlparse

import requests

from packageurl import PackageURL  # type: ignore
from requests.auth import HTTPBasicAuth

from hoppr import __version__
from hoppr.base_plugins.collector import SerialCollectorPlugin
from hoppr.base_plugins.hoppr import hoppr_rerunner
from hoppr.context import Context
from hoppr.hoppr_types.cred_object import CredObject
from hoppr.net import download_file
from hoppr.result import Result


class CollectNexusSearch(SerialCollectorPlugin):
    """
    Class to copy artifacts using the Nexus API
    """

    def get_version(self) -> str:  # pylint: disable=duplicate-code
        return __version__

    def __init__(self, context: Context, config: Optional[Dict] = None) -> None:
        super().__init__(context=context, config=config)
        if self.config is not None:
            if "purl_types" in self.config:
                self.supported_purl_types = self.config["purl_types"]

    def supports_purl_type(self, purl_type: str) -> bool:
        """
        Indicates whether or not this particular plug-in supports components of the specified
        purl type.

        Unless specified by the user, the Nexus search collector supports all types EXCEPT those
        listed below
        """

        if self.supported_purl_types:
            return super().supports_purl_type(purl_type)

        return purl_type not in ["git", "github", "gitlab"]

    @hoppr_rerunner
    def collect(self, comp: Any, repo_url: str, creds: CredObject = None) -> Result:
        """
        Collect artifact from Nexus
        """

        auth: Optional[HTTPBasicAuth] = None
        if creds is not None:
            auth = HTTPBasicAuth(username=creds.username, password=creds.password)

        if not CollectNexusSearch.is_nexus_instance(repo_url, auth):
            return Result.fail(f"{repo_url} is not a Nexus instance")

        purl = PackageURL.from_string(comp.purl)

        nexus_result = self.check_purl_specified_url(purl, repo_url)
        if not nexus_result.is_success():
            return nexus_result

        source_urls = CollectNexusSearch.get_download_urls(purl, repo_url, auth)
        if len(source_urls) == 0:
            msg = f"No artifacts found in Nexus instance {repo_url} for purl {comp.purl}"
            self.get_logger().error(msg, indent_level=2)
            return Result.fail(msg)

        target_dir = self._directory_for_nexus(purl, source_urls[0])

        for source_url in source_urls:
            self.get_logger().info(
                msg=f"Collecting from {source_url}",
                indent_level=2,
            )

            file_name = Path(urlparse(source_url).path).parts[-1]

            response = download_file(source_url, os.path.join(target_dir, file_name), creds)
            nexus_result = Result.from_http_response(response)

            self.get_logger().info(f"Download Result: {nexus_result}", indent_level=3)

            if not nexus_result.is_success():
                return nexus_result

        return Result.success()

    def _directory_for_nexus(self, purl: PackageURL, url: str) -> Path:
        repo_match = re.search(r"(.*?/repository/.*?)(/.*)?/(.*)", url)
        if repo_match:
            nexus_repo = repo_match.group(1)
            path = repo_match.group(2)
            if path is not None:
                path = path[1:]

        subdir = None
        match purl.type:
            case "docker" | "generic" | "maven" | "rpm":
                subdir = path
            case "helm" | "pypi":
                subdir = f"{purl.name}_{purl.version}"

        target_dir = self.directory_for(purl.type, nexus_repo, subdir=subdir)

        return target_dir

    @staticmethod
    def is_nexus_instance(repo_url: str, auth: Optional[HTTPBasicAuth] = None) -> bool:
        """
        Checks whether or not the repo_url refers to a Nexus instance
        """

        test_url = urljoin(repo_url, "service/rest/v1/status")

        for attempt in range(3):
            if attempt > 0:
                sleep(5)

            response = requests.get(
                test_url,
                auth=auth,
                allow_redirects=True,
                stream=True,
                verify=True,
                timeout=60,
            )
            if response.status_code < 300:
                return True
            if response.status_code < 500:
                return False

        return False

    @staticmethod
    def get_download_urls(purl: PackageURL, repo_url: str, auth: Optional[HTTPBasicAuth] = None) -> list[str]:
        """
        Retrieves all urls to be retrieved from Nexus for this component
        """
        search_url = urljoin(repo_url, "service/rest/v1/search/assets")

        additional_search_params: list[dict] = [{}]
        nexus_format = purl.type
        match purl.type:
            case "generic":
                nexus_format = "raw"
            case "maven":
                nexus_format = "maven2"
                additional_search_params = [
                    {"maven.extension": "jar", "maven.classifier": ""},
                    {"maven.extension": "jar", "maven.classifier": "sources"},
                    {"maven.extension": "pom"},
                ]
            case "rpm" | "yum":
                nexus_format = "yum"
                if purl.qualifiers.get("arch") is not None:
                    additional_search_params = [{"yum.architecture": purl.qualifiers.get('arch')}]

        base_params = {"sort": "version", "name": purl.name, "format": nexus_format}

        if purl.version is not None:
            base_params["version"] = purl.version

        url_list = []
        for extra_search_parms in additional_search_params:
            response = requests.get(
                search_url,
                auth=auth,
                allow_redirects=True,
                stream=True,
                verify=True,
                timeout=60,
                params=base_params | extra_search_parms,
            )
            if response.status_code < 300:
                search_result = json.loads(response.content)
                if len(search_result["items"]) > 0:
                    url_list.append(search_result["items"][0]["downloadUrl"])

        return url_list
