"""Models and functions used for YesWeHack platform data access."""
from json import JSONDecodeError
from typing import (
    Any,
    Dict,
    List,
    Optional,
    cast,
)
from urllib.parse import urlsplit

import requests
from yeswehack.api import Attachment as YesWeHackRawApiAttachment
from yeswehack.api import Log as YesWeHackRawApiLog
from yeswehack.api import Report as YesWeHackRawApiReport
from yeswehack.api import YesWeHack as YesWeHackRawApiClient
from yeswehack.exceptions import APIError as YesWeHackRawAPiError

from ywh2bt.core.api.client import TestableApiClient
from ywh2bt.core.api.mapping import (
    MappingContext,
    map_raw_attachment,
    map_raw_logs,
    map_raw_report,
)
from ywh2bt.core.api.models.report import (
    Attachment,
    Log,
    Report,
)
from ywh2bt.core.configuration.yeswehack import YesWeHackConfiguration
from ywh2bt.core.exceptions import CoreException
from ywh2bt.version import __VERSION__


class YesWeHackApiClientError(CoreException):
    """A YesWeHack API client error."""


class YesWeHackApiClient(TestableApiClient):
    """A YesWeHack API client."""

    _configuration: YesWeHackConfiguration
    _raw_client: YesWeHackRawApiClient
    _yeswehack_domain: str
    _logged_in: bool

    def __init__(
        self,
        configuration: YesWeHackConfiguration,
    ):
        """
        Initialize self.

        Args:
            configuration: a YesWeHack configuration
        """
        self._configuration = configuration
        self._raw_client = self._build_raw_client()
        self._logged_in = False
        self._yeswehack_domain = self._extract_yeswehack_domain(
            url=cast(str, self._configuration.api_url),
        )

    def test(
        self,
    ) -> None:
        """Test the client."""
        self._ensure_login()

    def _build_raw_client(
        self,
    ) -> YesWeHackRawApiClient:
        configuration = self._configuration
        try:
            client = YesWeHackRawApiClient(
                api_url=self._normalize_api_url(
                    api_url=cast(str, configuration.api_url),
                ),
                pat=configuration.pat,
                verify=configuration.verify,
                lazy=True,
                headers={
                    "User-Agent": f"ywh2bt/{__VERSION__}",
                },
            )
        except (YesWeHackRawAPiError, requests.RequestException) as e:
            raise YesWeHackApiClientError("Unable to initialize YesWeHack API client") from e
        return client

    def _normalize_api_url(
        self,
        api_url: str,
    ) -> str:
        return api_url[:-1] if api_url[-1] == "/" else api_url

    def _extract_yeswehack_domain(
        self,
        url: str,
    ) -> str:
        result = urlsplit(url)
        # only keep 'xxx.yyy.zzz' from 'www.xxx.yyy.zzz'
        _, domain = result.netloc.split(".", 1)
        return domain

    def _ensure_login(
        self,
    ) -> None:
        if not self._logged_in:
            try:
                success = self._raw_client.login()
            except (YesWeHackRawAPiError, requests.RequestException) as e:
                raise YesWeHackApiClientError("Unable to log in with YesWeHack API client") from e
            self._logged_in = success

    def get_program_reports(
        self,
        slug: str,
        filters: Optional[Dict[str, Any]] = None,
    ) -> List[Report]:
        """
        Get reports for the program.

        Args:
            slug: a program slug
            filters: a report filter

        Returns:
            the list of reports

        Raises:
            YesWeHackApiClientError: if an error occurred when getting the reports
        """
        self._ensure_login()
        try:
            raw_reports = self._raw_client.get_reports(
                program=slug,
                filters=filters or {},
                lazy=True,
            )
        except (YesWeHackRawAPiError, YesWeHackApiClientError, requests.RequestException) as e:
            raise YesWeHackApiClientError(f"Unable to get reports for program {slug}") from e
        return self._get_detailed_reports(
            raw_reports=raw_reports,
        )

    def _get_detailed_reports(
        self,
        raw_reports: List[YesWeHackRawApiReport],
    ) -> List[Report]:
        return [
            self._get_detailed_report(
                report_id=raw_report.id,
            )
            for raw_report in raw_reports
        ]

    def _get_detailed_report(
        self,
        report_id: int,
    ) -> Report:
        try:
            raw_report = self._raw_client.get_report(
                report=report_id,
                lazy=True,
            )
        except (YesWeHackRawAPiError, requests.RequestException, TypeError) as e:
            raise YesWeHackApiClientError(f"Unable to get report #{report_id} details") from e
        return self._map_raw_report(
            raw_report=raw_report,
        )

    def _get_mapping_context(
        self,
    ) -> MappingContext:
        return MappingContext(
            yeswehack_domain=self._yeswehack_domain,
        )

    def _map_raw_report(
        self,
        raw_report: YesWeHackRawApiReport,
    ) -> Report:
        try:
            return map_raw_report(
                context=self._get_mapping_context(),
                raw_report=raw_report,
            )
        except TypeError as e:
            raise YesWeHackApiClientError(f"Unable to map report #{raw_report.id}") from e

    def _map_raw_attachment(
        self,
        raw_attachment: YesWeHackRawApiAttachment,
    ) -> Attachment:
        try:
            return map_raw_attachment(
                context=self._get_mapping_context(),
                raw_attachment=raw_attachment,
            )
        except TypeError as e:
            raise YesWeHackApiClientError(f"Unable to map attachment #{raw_attachment}") from e

    def _map_raw_logs(
        self,
        raw_logs: List[YesWeHackRawApiLog],
    ) -> List[Log]:
        try:
            return map_raw_logs(
                context=self._get_mapping_context(),
                raw_logs=raw_logs,
            )
        except TypeError as e:
            raise YesWeHackApiClientError(f"Unable to map logs #{raw_logs}") from e

    def put_report_tracking_status(
        self,
        report: Report,
        status: str,
        tracker_name: str,
        issue_id: str,
        issue_url: str,
        comment: str,
    ) -> None:
        """
        Update a report tracking status with information about a tracked issue.

        Args:
            report: a report
            status: a status (T, AFI, ...)
            tracker_name: a tracker name
            issue_id: an id of an issue from a tracker
            issue_url: an URL of an issue from a tracker
            comment: a comment

        Raises:
            YesWeHackApiClientError: if an error occurred when updating the status
        """
        self._ensure_login()
        try:
            response: requests.Response = report.raw_report.put_tracking_status(
                tracking_status=status,
                tracker_name=tracker_name,
                tracker_id=issue_id,
                tracker_url=issue_url,
                message=comment,
            )
        except (YesWeHackRawAPiError, requests.RequestException) as api_error:
            raise YesWeHackApiClientError(f"Unable to update report #{report.report_id} tracking status") from api_error
        try:
            data = response.json()
        except JSONDecodeError as decode_error:
            raise YesWeHackApiClientError(
                f"Unable to parse response after updating report #{report.report_id} tracking status",
            ) from decode_error
        if not isinstance(data, dict):
            raise YesWeHackApiClientError(
                (
                    f"Expecting {dict} from response to report #{report.report_id} tracking status update ; "
                    + f"got {type(data)}",
                ),
            )
        if not response.ok or "errors" in data:
            message = data["message"] if "message" in data and data["message"] else "[no error message]"
            raise YesWeHackApiClientError(
                f"Unable to update report #{report.report_id} tracking status: {message}",
            )

    def post_report_tracker_update(
        self,
        report: Report,
        tracker_name: str,
        issue_id: str,
        issue_url: str,
        token: str,
        comment: str,
    ) -> None:
        """
        Send a report tracker update with information about a tracked issue.

        Args:
            report: a report
            tracker_name: a tracker name
            issue_id: an id of an issue from a tracker
            issue_url: an URL of an issue from a tracker
            token: a token containing the state of the issue
            comment: a comment

        Raises:
            YesWeHackApiClientError: if an error occurred when sending the update
        """
        self._ensure_login()
        try:
            response = report.raw_report.post_tracker_update(
                tracker_name=tracker_name,
                tracker_id=issue_id,
                tracker_url=issue_url,
                token=token,
                message=comment,
            )
        except (YesWeHackRawAPiError, requests.RequestException) as api_error:
            raise YesWeHackApiClientError(f"Unable to send report #{report.report_id} tracker update") from api_error
        try:
            data = response.json()
        except JSONDecodeError as decode_error:
            raise YesWeHackApiClientError(
                f"Unable to parse response after report #{report.report_id} tracker update",
            ) from decode_error
        if not isinstance(data, dict):
            raise YesWeHackApiClientError(
                (
                    f"Expecting {dict} from response to report #{report.report_id} tracker update ; "
                    + f"got {type(data)}",
                ),
            )
        if not response.ok or "errors" in data:
            message = data["message"] if "message" in data and data["message"] else "[no error message]"
            raise YesWeHackApiClientError(
                f"Unable to update report #{report.report_id} tracker: {message}",
            )

    def post_report_attachment(
        self,
        report: Report,
        filename: str,
        file_content: bytes,
        file_type: Optional[str] = None,
    ) -> Attachment:
        """
        Send an attachment to a report.

        Args:
            report: a report
            filename: a file name
            file_content: file content
            file_type: an optional file type (mime)

        Returns:
            the attachment

        Raises:
            YesWeHackApiClientError: if an error occurred when sending the attachment
        """
        self._ensure_login()
        try:
            attachment = report.raw_report.post_attachment(
                filename=filename,
                file_content=file_content,
                file_type=file_type,
            )
        except (YesWeHackRawAPiError, requests.RequestException) as api_error:
            raise YesWeHackApiClientError(
                f"Unable to send attachment {filename} to report #{report.report_id}",
            ) from api_error
        return self._map_raw_attachment(
            raw_attachment=attachment,
        )

    def post_report_tracker_message(
        self,
        report: Report,
        tracker_name: str,
        issue_id: str,
        issue_url: str,
        comment: str,
        attachments: Optional[List[str]] = None,
    ) -> None:
        """
        Send a report tracker message with information about a tracked issue.

        Args:
            report: a report
            tracker_name: a tracker name
            issue_id: an id of an issue from a tracker
            issue_url: an URL of an issue from a tracker
            comment: a comment
            attachments: a list of attachment identifiers

        Raises:
            YesWeHackApiClientError: if an error occurred when sending the message
        """
        self._ensure_login()
        try:
            report.raw_report.post_tracker_message(
                tracker_name=tracker_name,
                tracker_id=issue_id,
                tracker_url=issue_url,
                message=comment,
                attachments=attachments,
            )
        except (YesWeHackRawAPiError, requests.RequestException) as api_error:
            raise YesWeHackApiClientError(f"Unable to send report #{report.report_id} tracker message") from api_error

    def put_status(
        self,
        report: Report,
        status: str,
        comment: str,
        attachments: Optional[List[str]] = None,
    ) -> List[Log]:
        """
        Update a report status.

        Args:
            report: a report
            status: a status
            comment: a comment
            attachments: a list of attachment identifiers

        Returns:
            a list of logs

        Raises:
            YesWeHackApiClientError: if an error occurred when updating the status
        """
        self._ensure_login()
        try:
            raw_logs = report.raw_report.put_status(
                status=status,
                message=comment,
                attachments=attachments,
            )
        except (YesWeHackRawAPiError, requests.RequestException) as api_error:
            raise YesWeHackApiClientError(f"Unable to update report #{report.report_id} status") from api_error
        return self._map_raw_logs(
            raw_logs=raw_logs,
        )

    def put_ask_for_fix_verification_status(
        self,
        report: Report,
        status: str,
        comment: str,
        attachments: Optional[List[str]] = None,
    ) -> List[Log]:
        """
        Update a report status.

        Args:
            report: a report
            status: a status
            comment: a comment
            attachments: a list of attachment identifiers

        Returns:
            a list of logs

        Raises:
            YesWeHackApiClientError: if an error occurred when updating the status
        """
        self._ensure_login()
        try:
            raw_logs = report.raw_report.put_ask_for_fix_verification_status(
                status=status,
                message=comment,
                attachments=attachments,
            )
        except (YesWeHackRawAPiError, requests.RequestException) as api_error:
            raise YesWeHackApiClientError(
                f"Unable to update report #{report.report_id} ask for fix verification status"
            ) from api_error
        return self._map_raw_logs(
            raw_logs=raw_logs,
        )
