from asyncio import gather
from asyncio import sleep as async_sleep
from typing import ClassVar, List, Literal, cast

from ..common import (
    DisplayFormatSchemes,
    Endpoints,
    ErrorResponse,
    LabelFormats,
    LabelLayouts,
    ShipStationClient,
    ShipStationConnection,
)
from ._types import (
    ChargeEvents,
    Label,
    LabelListResponse,
    LabelShipment,
    LabelStatuses,
    LabelVoidResponse,
    TrackingInformation,
)


class LabelPortal(ShipStationClient):
    # Polling defaults for batch label processing
    _BATCH_POLL_INTERVAL: ClassVar[float] = 2.0  # seconds between polls
    _BATCH_POLL_TIMEOUT: ClassVar[float] = 20.0  # max seconds to wait

    @classmethod
    async def where(
        cls: type["LabelPortal"],
        connection: ShipStationConnection,
        label_status: LabelStatuses | None = None,
        service_code: str | None = None,
        carrier_id: str | None = None,
        tracking_number: str | None = None,
        batch_id: str | None = None,
        rate_id: str | None = None,
        shipment_id: str | None = None,
        warehouse_id: str | None = None,
        created_at_start: str | None = None,
        created_at_end: str | None = None,
        page: int = 1,
        page_size: int = 25,
        sort_dir: Literal["asc", "desc"] = "desc",
        sort_by: Literal["created_at", "modified_at"] = "created_at",
        identity: bool = False,
    ) -> tuple[int, LabelListResponse | ErrorResponse]:
        """
        This method returns a list of labels that you've created. You can optionally filter the results as well as control their sort order and the number of results returned at a time.

        By default all labels are returned 25 at a time, starting with the most recently created ones. You can combine multiple filter options to narrow-down the results. For example, if you only want your UPS labels for your east coast warehouse you could query by both warehouse_id and carrier_id.

        Args:
            label_status (LabelStatuses | None, optional): Filter results by label status. Defaults to None.
            service_code (str | None, optional): Filter results by service code. Defaults to None.
            carrier_id (str | None, optional): Filter results by carrier ID. Defaults to None.
            tracking_number (str | None, optional): Filter results by tracking number. Defaults to None.
            batch_id (str | None, optional): Filter results by batch ID. Defaults to None.
            rate_id (str | None, optional): Filter results by rate ID. Defaults to None.
            shipment_id (str | None, optional): Filter results by shipment ID. Defaults to None.
            warehouse_id (str | None, optional): Filter results by warehouse ID. Defaults to None.
            created_at_start (str | None, optional): Filter results by creation date start (ISO 8601 format). Defaults to None.
            created_at_end (str | None, optional): Filter results by creation date end (ISO 8601 format). Defaults to None.
            page (int, optional): The page number of results to return. Defaults to 1.
            page_size (int, optional): The number of results to return per page. Defaults to 25.
            sort_dir (Literal["asc", "desc"], optional): The direction to sort the results. Defaults to "desc".
            sort_by (Literal["created_at", "modified_at"], optional): The field to sort the results by. Defaults to "created_at".

        Returns:
            tuple[int, LabelListResponse | ErrorResponse]: A tuple containing the HTTP status code and either
        """
        params = {
            "label_status": label_status,
            "service_code": service_code,
            "carrier_id": carrier_id,
            "tracking_number": tracking_number,
            "batch_id": batch_id,
            "rate_id": rate_id,
            "shipment_id": shipment_id,
            "warehouse_id": warehouse_id,
            "created_at_start": created_at_start,
            "created_at_end": created_at_end,
            "page": page,
            "page_size": page_size,
            "sort_dir": sort_dir,
            "sort_by": sort_by,
        }

        params = {k: v for k, v in params.items() if v is not None}

        endpoint = f"{connection.v2_endpoint}/{Endpoints.LABELS.value}"

        try:
            res = await connection.request("GET", endpoint, params=params)  # type: ignore[arg-type]

            return cls.validate_response(
                res,
                (200,),
                LabelListResponse,
                identity=identity,
            )
        except Exception as e:
            return cls.parse_unknown_exception(e)

    @classmethod
    async def list_for_shipments(
        cls: type["LabelPortal"],
        connection: ShipStationConnection,
        shipment_ids: List[str],
        identity: bool = False,
    ) -> tuple[int, LabelListResponse, list[ErrorResponse]]:

        all_labels: List[Label] = []
        errors: List[ErrorResponse] = []
        try:
            labels = await gather(
                *[
                    cls.where(
                        connection,
                        shipment_id=shipment_id,
                        page_size=50,  # Assuming a shipment won't have more than 50 labels
                    )
                    for shipment_id in shipment_ids
                ]
            )

            for stat, label_list in labels:
                if stat in (200, 201, 207):
                    all_labels.extend(label_list.get("labels", []))
                else:
                    errors.append(cast(ErrorResponse, label_list))

            status = 200
        except Exception as e:
            status, error = cls.parse_unknown_exception(e)
            errors.append(error)

        return (
            status,
            cast(
                LabelListResponse,
                {
                    "labels": all_labels,
                    "pagination": {
                        "page": 1,
                        "page_size": len(all_labels),
                        "total_pages": 1,
                        "total_items": len(all_labels),
                    },
                },
            ),
            errors,
        )

    @classmethod
    async def poll_label_until_ready(
        cls: type["LabelPortal"],
        connection: ShipStationConnection,
        label_id: str,
        timeout: float | None = None,
        interval: float | None = None,
        identity: bool = False,
    ) -> tuple[bool, Label | ErrorResponse]:
        """
        This method is a helper function that polls the list endpoint for a batch of labels until they are all ready. This is useful for the purchase endpoint which can return a 207 status code if some labels in the batch are ready while others are still being processed.

        Returns:
            tuple[int, LabelListResponse | ErrorResponse]: A tuple containing the HTTP status code and either a LabelListResponse with all labels in the batch or an ErrorResponse if an error occurs.
        """

        _timeout = timeout or cls._BATCH_POLL_TIMEOUT
        _interval = interval or cls._BATCH_POLL_INTERVAL

        terminal = ("completed", "error", "voided")

        elapsed = 0.0
        while elapsed < _timeout:
            stat, label = await cls.get_by_id(connection, label_id=label_id)

            if stat not in (200, 201, 207):
                return False, label

            if isinstance(label, dict) and label.get("status") in terminal:
                return True, cast(Label, label)

            await async_sleep(_interval)
            elapsed += _interval

        # Final attempt after timeout
        stat, label = await cls.get_by_id(connection, label_id=label_id)
        if (
            stat in (200, 201)
            and isinstance(label, dict)
            and label.get("status") in terminal
        ):
            return True, cast(Label, label)
        return False, label

    @classmethod
    async def poll_labels_until_ready(
        cls: type["LabelPortal"],
        connection: ShipStationConnection,
        label_ids: List[str],
        timeout: float | None = None,
        interval: float | None = None,
        identity: bool = False,
    ) -> tuple[bool, List[Label] | List[ErrorResponse]]:

        labels: list[Label] = []
        label_states = await gather(
            *[
                cls.poll_label_until_ready(
                    connection,
                    label_id=label_id,
                    timeout=timeout,
                    interval=interval,
                    identity=identity,
                )
                for label_id in label_ids
            ]
        )

        errors: List[tuple[bool, ErrorResponse]] = []
        for ready, label in label_states:
            if not ready:
                errlabel = cast(ErrorResponse, label)
                errors.append(
                    (
                        False,
                        errlabel,
                    )
                )

            labels.append(cast(Label, label))

        return (True, labels) if not errors else (False, [err for _, err in errors])

    @classmethod
    async def purchase(
        cls: type["LabelPortal"],
        connection: ShipStationConnection,
        shipment: LabelShipment,
        charge_event: ChargeEvents,
        outbound_label_id: str,
        validate_address: Literal[
            "no_validation", "validate_only", "validate_and_clean"
        ] = "no_validation",
        label_download_type: Literal["url", "inline"] = "url",
        is_return_label: bool = False,
        rma_number: str | None = None,
        ship_to_service_point_id: str | None = None,
        ship_from_service_point_id: str | None = None,
        label_format: LabelFormats = "pdf",
        display_scheme: DisplayFormatSchemes = "label",
        label_layout: LabelLayouts = "4x6",
        label_image_id: str | None = None,
        test_label: bool = False,
        identity: bool = False,
    ) -> tuple[int, ErrorResponse | Label]:
        """
        This method allows you to purchase a shipping and print a label for a given shipment. You can specify various options such as the charge event, label format, and whether it's a return label.

        Args:
            shipment (Shipment): The shipment details for which the label is to be purchased.
            charge_event (ChargeEvents): The event that will trigger the charge for the label.
            outbound_label_id (str): The ID of the outbound label.
            validate_address (Literal["no_validation", "validate_only", "validate_and_clean"], optional): Address validation option. Defaults to "no_validation".
            label_download_type (Literal["url", "inline"], optional): The format in which the label will be downloaded. Defaults to "url".
            is_return_label (bool, optional): Indicates if the label is a return label. Defaults to False.
            rma_number (str | None, optional): The RMA number for return labels. Defaults to None.
            ship_to_service_point_id (str | None, optional): Service point ID for shipping to. Defaults to None.
            ship_from_service_point_id (str | None, optional): Service point ID for shipping from. Defaults to None.
            label_format (LabelFormats, optional): The format of the label. Defaults to "pdf".
            display_scheme (DisplayFormatSchemes, optional): The display scheme for the label. Defaults to "label".
            label_layout (LabelLayouts, optional): The layout of the label. Defaults to "4x6".
            label_image_id (str | None, optional): The image ID for the label. Defaults to None.
            test_label (bool, optional): Indicates if this is a test label. Defaults to False.

        Returns:
            tuple[int, ErrorResponse | Label]: A tuple containing the HTTP status code and either an ErrorResponse or the purchased Label.
        """
        payload = {
            "shipment": shipment,
            "charge_event": charge_event,
            "outbound_label_id": outbound_label_id,
            "validate_address": validate_address,
            "label_download_type": label_download_type,
            "is_return_label": is_return_label,
            "rma_number": rma_number,
            "ship_to_service_point_id": ship_to_service_point_id,
            "ship_from_service_point_id": ship_from_service_point_id,
            "label_format": label_format,
            "display_scheme": display_scheme,
            "label_layout": label_layout,
            "label_image_id": label_image_id,
            "test_label": test_label,
        }

        payload = {k: v for k, v in payload.items() if v is not None}

        endpoint = f"{connection.v2_endpoint}/{Endpoints.LABELS.value}"

        try:
            res = await connection.request("POST", endpoint, json=payload)  # type: ignore[arg-type]

            return cls.validate_response(
                res,
                (200,),
                Label,
                identity=identity,
            )
        except Exception as e:
            return cls.parse_unknown_exception(e)

    @classmethod
    async def purchase_with_rate_id(
        cls: type["LabelPortal"],
        connection: ShipStationConnection,
        rate_id: str,
        validate_address: Literal[
            "no_validation", "validate_only", "validate_and_clean"
        ] = "no_validation",
        label_layout: LabelLayouts = "4x6",
        label_format: LabelFormats = "pdf",
        label_download_type: Literal["url", "inline"] = "url",
        display_scheme: DisplayFormatSchemes = "label",
        identity: bool = False,
    ) -> tuple[int, ErrorResponse | Label]:
        """
        When retrieving rates for shipments using the /rates endpoint, the returned information contains a rate_id property that can be used to generate a label without having to refill in the shipment information repeatedly

        Args:
            rate_id (str): The ID of the rate to be used for purchasing the label.
            validate_address (Literal["no_validation", "validate_only", "validate_and_clean"], optional): Address validation option. Defaults to "no_validation".
            label_layout (LabelLayouts, optional): The layout of the label. Defaults to "4x6".
            label_format (LabelFormats, optional): The format of the label. Defaults to "pdf".
            label_download_type (Literal["url", "inline"], optional): The format in which the label will be downloaded. Defaults to "url".
            display_scheme (DisplayFormatSchemes, optional): The display scheme for the label. Defaults to "label".

        Returns:
            tuple[int, ErrorResponse | Label]: A tuple containing the HTTP status code and either an ErrorResponse or the purchased Label.
        """
        payload = {
            "validate_address": validate_address,
            "label_layout": label_layout,
            "label_format": label_format,
            "label_download_type": label_download_type,
            "display_scheme": display_scheme,
        }

        endpoint = f"{connection.v2_endpoint}/{Endpoints.LABELS.value}/rates/{rate_id}"

        try:
            res = await connection.request("POST", endpoint, json=payload)  # type: ignore[arg-type]

            return cls.validate_response(
                res,
                (200,),
                Label,
                identity=identity,
            )
        except Exception as e:
            return cls.parse_unknown_exception(e)

    @classmethod
    async def purchase_with_shipment_id(
        cls: type["LabelPortal"],
        connection: ShipStationConnection,
        shipment_id: str,
        validate_address: Literal[
            "no_validation", "validate_only", "validate_and_clean"
        ] = "no_validation",
        label_layout: LabelLayouts = "4x6",
        label_format: LabelFormats = "pdf",
        label_download_type: Literal["url", "inline"] = "url",
        display_scheme: DisplayFormatSchemes = "label",
        identity: bool = False,
    ) -> tuple[int, ErrorResponse | Label]:
        """
        Purchase a label using a shipment ID that has already been created with the desired address and package info.

        Args:
            shipment_id (str): The ID of the shipment to be used for purchasing the label.
            validate_address (Literal["no_validation", "validate_only", "validate_and_clean"], optional): Address validation option. Defaults to "no_validation".
            label_layout (LabelLayouts, optional): The layout of the label. Defaults to "4x6".
            label_format (LabelFormats, optional): The format of the label. Defaults to "pdf".
            label_download_type (Literal["url", "inline"], optional): The format in which the label will be downloaded. Defaults to "url".
            display_scheme (DisplayFormatSchemes, optional): The display scheme for the label. Defaults to "label".

        Returns:
            tuple[int, ErrorResponse | Label]: A tuple containing the HTTP status code and either an ErrorResponse or the purchased Label.
        """
        payload = {
            "validate_address": validate_address,
            "label_layout": label_layout,
            "label_format": label_format,
            "label_download_type": label_download_type,
            "display_scheme": display_scheme,
        }

        endpoint = (
            f"{connection.v2_endpoint}/{Endpoints.LABELS.value}/shipment/{shipment_id}"
        )

        try:
            res = await connection.request("POST", endpoint, json=payload)  # type: ignore[arg-type]

            return cls.validate_response(
                res,
                (200,),
                Label,
                identity=identity,
            )
        except Exception as e:
            return cls.parse_unknown_exception(e)

    @classmethod
    async def get_by_id(
        cls: type["LabelPortal"],
        connection: ShipStationConnection,
        label_id: str,
        label_download_type: Literal["url", "inline"] = "url",
        identity: bool = False,
    ) -> tuple[int, ErrorResponse | Label]:
        """
        This method retrieves the details of a specific label using its unique ID.

        Args:
            label_id (str): The unique identifier of the label to be retrieved.
            label_download_type (Literal["url", "inline"], optional): The format in which the label was downloaded. Defaults to "url".
        Returns:
            tuple[int, ErrorResponse | Label]: A tuple containing the HTTP status code and either an ErrorResponse or the requested Label.
        """
        endpoint = f"{connection.v2_endpoint}/{Endpoints.LABELS.value}/{label_id}?label_download_type={label_download_type}"

        try:
            res = await connection.request("GET", endpoint)

            return cls.validate_response(
                res,
                (200,),
                Label,
                identity=identity,
            )
        except Exception as e:
            return cls.parse_unknown_exception(e)

    @classmethod
    async def create_return_label(
        cls: type["LabelPortal"],
        connection: ShipStationConnection,
        label_id: str,
        charge_event: ChargeEvents,
        label_layout: LabelLayouts = "4x6",
        label_format: LabelFormats = "pdf",
        label_download_type: Literal["url", "inline"] = "url",
        display_scheme: DisplayFormatSchemes = "label",
        label_image_id: str | None = None,
        identity: bool = False,
    ) -> tuple[int, ErrorResponse | Label]:
        """
        Create a return label for a previously created outbound label.
        The return label will automatically swap the ship to and ship from addresses from the original label.

        Args:
            label_id (str): The ID of the outbound label for which the return label is to be created.
            charge_event (ChargeEvents): The event that will trigger the charge for the return label.
            label_layout (LabelLayouts, optional): The layout of the label. Defaults to "4x6".
            label_format (LabelFormats, optional): The format of the label. Defaults to "pdf".
            label_download_type (Literal["url", "inline"], optional): The format in which the label will be downloaded. Defaults to "url".
            display_scheme (DisplayFormatSchemes, optional): The display scheme for the label. Defaults to "label".
            label_image_id (str | None, optional): The image ID for the label. Defaults to None.

        Returns:
            tuple[int, ErrorResponse | Label]: A tuple containing the HTTP status code and either an ErrorResponse or the created return Label.
        """

        payload = {
            "charge_event": charge_event,
            "label_layout": label_layout,
            "label_format": label_format,
            "label_download_type": label_download_type,
            "display_scheme": display_scheme,
            "label_image_id": label_image_id,
        }

        endpoint = (
            f"{connection.v2_endpoint}/{Endpoints.LABELS.value}/{label_id}/return"
        )

        try:
            res = await connection.request("POST", endpoint, json=payload)  # type: ignore[arg-type]

            return cls.validate_response(
                res,
                (200,),
                Label,
                identity=identity,
            )
        except Exception as e:
            return cls.parse_unknown_exception(e)

    @classmethod
    async def get_tracking_information(
        cls: type["LabelPortal"],
        connection: ShipStationConnection,
        label_id: str,
        identity: bool = False,
    ) -> tuple[int, ErrorResponse | TrackingInformation]:
        endpoint = f"{connection.v2_endpoint}/{Endpoints.LABELS.value}/{label_id}/track"

        try:
            res = await connection.request("GET", endpoint)

            return cls.validate_response(
                res,
                (200,),
                TrackingInformation,
                identity=identity,
            )
        except Exception as e:
            return cls.parse_unknown_exception(e)

    @classmethod
    async def void_label(
        cls: type["LabelPortal"],
        connection: ShipStationConnection,
        label_id: str,
        identity: bool = False,
    ) -> tuple[int, ErrorResponse | LabelVoidResponse]:
        endpoint = f"{connection.v2_endpoint}/{Endpoints.LABELS.value}/{label_id}/void"

        try:
            res = await connection.request("PUT", endpoint)

            return cls.validate_response(
                res,
                (200,),
                LabelVoidResponse,
                identity=identity,
            )
        except Exception as e:
            return cls.parse_unknown_exception(e)
