from datetime import datetime
from enum import Enum

from prometeo import exceptions, base_client, base_session, utils
from .models import (
    CFDIBill,
    CFDIDownloadItem,
    DownloadRequest as DownloadRequestModel,
    PdfFile,
    DownloadFile,
    AcknowledgementResult as AcknowledgementResultModel,
)


PRODUCTION_URL = "https://fiscal.prometeoapi.net"
SANDBOX_URL = "https://fiscal.sandbox.prometeoapi.com"


class LoginScope(Enum):
    CFDI = "cfdi"
    SIAT = "siat"


class DownloadAction(Enum):
    BULK_DOWNLOAD = "bulk_download"
    METADATA_DOWNLOAD = "metadata_download"
    PDF_EXPORT = "pdf_export"
    LIST = "list"


class BillStatus(Enum):
    ANY = "any"
    CANCELLED = "cancelled"
    VALID = "valid"


class Motive(Enum):
    ALL = "all"
    AF = "af"
    DE = "de"
    CO = "co"
    FC = "fc"
    MONTHLY = "monthly"


class DocumentType(Enum):
    ALL = "all"
    CT = "ct"
    B = "b"
    PL = "pl"
    XF = "xf"
    XC = "xc"


class Status(Enum):
    ALL = "all"
    RECEIVED = "received"
    ACCEPTED = "accepted"
    REJECTED = "rejected"


class SendType(Enum):
    ALL = "all"
    N = "n"
    C = "c"


class Session(base_session.BaseSession):
    @utils.adapt_async_sync
    async def logout(self):
        """
        Logs out of SAT. You won't be able to use this session after logout.
        """
        await self._client.logout(self._session_key)

    @utils.adapt_async_sync
    async def get_emitted_bills(self, date_start, date_end, status):
        """
        List all emitted bills in a range of dates.

        :param date_start: Start date to filter
        :type date_start: :class:`~datetime.datetime`

        :param date_end: End date to filter
        :type date_end: :class:`~datetime.datetime`

        :param status: Status of the bills
        :type status: :class:`BillStatus`

        :rtype: List of :class:`~prometeo.sat.models.CFDIBill`
        """
        return await self._client.get_emitted(
            self._session_key, date_start, date_end, status, DownloadAction.LIST
        )

    @utils.adapt_async_sync
    async def download_emitted_bills(self, date_start, date_end, status):
        """
        Creates a request to download all the emitted bills in a range of dates.

        :param date_start: Start date to filter
        :type date_start: :class:`~datetime.datetime`

        :param date_end: End date to filter
        :type date_end: :class:`~datetime.datetime`

        :param status: Status of the bills
        :type status: :class:`BillStatus`

        :rtype: List of :class:`~DownloadRequest`
        """
        requests = await self._client.get_emitted(
            self._session_key,
            date_start,
            date_end,
            status,
            DownloadAction.BULK_DOWNLOAD,
        )
        return [
            DownloadRequest(
                self._client,
                self._session_key,
                request.request_id,
            )
            for request in requests
        ]

    @utils.adapt_async_sync
    async def get_received_bills(self, year, month, status):
        """
        List all received bills in a range of dates.

        :param year: Year of the received bills
        :type year: int

        :param month: Month of the received bills
        :type month: int

        :param status: Status of the bills
        :type status: :class:`BillStatus`

        :rtype: List of :class:`~prometeo.sat.models.CFDIBill`
        """
        return await self._client.get_received(
            self._session_key, year, month, status, DownloadAction.LIST
        )

    @utils.adapt_async_sync
    async def download_received_bills(self, year, month, status):
        """
        Creates a request to download all the received bills in a range of dates.

        :param date_start: Start date to filter
        :type date_start: :class:`~datetime.datetime`

        :param date_end: End date to filter
        :type date_end: :class:`~datetime.datetime`

        :param status: Status of the bills
        :type status: :class:`BillStatus`

        :rtype: List of :class:`~DownloadRequest`
        """
        requests = await self._client.get_received(
            self._session_key, year, month, status, DownloadAction.BULK_DOWNLOAD
        )
        return [
            DownloadRequest(
                self._client,
                self._session_key,
                request.request_id,
            )
            for request in requests
        ]

    @utils.adapt_async_sync
    async def get_downloads(self):
        """
        Gets a list of available downloads

        :rtype: List of :class:`DownloadRequest`
        """
        requests = await self._client.get_downloads(self._session_key)
        return [
            DownloadRequest(
                self._client,
                self._session_key,
                request.request_id,
            )
            for request in requests
        ]

    @utils.adapt_async_sync
    async def get_acknowledgements(
        self, year, month_start, month_end, motive, document_type, status, send_type
    ):
        """
        Gets a list of acknowledgements for a range of dates

        :param year: The year of the acknowledgements
        :type year: int

        :param month_start: Start month to filter
        :type month_start: int

        :param month_end: End month to filter
        :type month_end: int

        :param motive: Motive
        :type motive: :class:`Motive`

        :param document_type: Document type
        :type document_type: :class:`DocumentType`

        :param status: Status
        :type status: :class:`Status`

        :param send_type: Send type
        :type send_type: :class:`SendType`

        :rtype: List of :class:`AcknowledgementResult`
        """
        acks = await self._client.get_acknowledgements(
            self._session_key,
            year,
            month_start,
            month_end,
            motive,
            document_type,
            status,
            send_type,
        )
        return [
            AcknowledgementResult(self._client, self._session_key, ack) for ack in acks
        ]


class SatAPIClient(base_client.BaseClient):
    """
    API Client for SAT API
    """

    ENVIRONMENTS = {
        "production": PRODUCTION_URL,
        "sandbox": SANDBOX_URL,
    }

    session_class = Session

    @utils.adapt_async_sync
    async def login(self, rfc, password, scope):
        """
        Log in to SAT

        :param rfc: RFC of the person to log in
        :type rfc: str

        :param password: Password used to login, also known as CIEC
        :type password: str

        :param scope: Depending on the type of information to query, use
                      ``LoginScope.CFDI`` to download bill xmls or
                      ``LoginScope.SIAT`` to download acknowledgements.
        :type scope: :class:`LoginScope`

        :rtype: :class:`Session`
        """
        response = await self.call_api(
            "POST",
            "/login/",
            data={
                "provider": "sat",
                "rfc": rfc,
                "password": password,
                "scope": scope.value,
            },
        )
        if response["status"] == "logged_in":
            return Session(self, response["status"], response["session_key"])
        elif response["status"] == "wrong_credentials":
            raise exceptions.WrongCredentialsError(response["message"])
        else:
            raise exceptions.ClientError(response["message"])

    @utils.adapt_async_sync
    async def logout(self, session_key):
        await self.call_api(
            "GET",
            "/logout/",
            params={
                "session_key": session_key,
            },
        )

    def _handle_bill_parsing(self, action, data):
        if action == DownloadAction.LIST:
            return [
                CFDIBill(
                    id=bill["id"],
                    emitter_rfc=bill["emitter_rfc"],
                    emitter_reason=bill["emitter_reason"],
                    receiver_rfc=bill["receiver_rfc"],
                    receiver_reason=bill["receiver_reason"],
                    emitted_date=datetime.strptime(
                        bill["emitted_date"], "%Y-%m-%dT%H:%M:%S"
                    ),
                    certification_date=datetime.strptime(
                        bill["certification_date"], "%Y-%m-%dT%H:%M:%S"
                    ),
                    certification_pac=bill["certification_pac"],
                    total_value=bill["total_value"],
                    effect=bill["effect"],
                    status=BillStatus(bill["status"]),
                )
                for bill in data
            ]
        elif action == DownloadAction.BULK_DOWNLOAD:
            return [
                DownloadRequestModel(request_id=request["request_id"])
                for request in data
            ]
        elif action == DownloadAction.METADATA_DOWNLOAD:
            return [
                DownloadRequestModel(request_id=request["request_id"])
                for request in data
            ]
        elif action == DownloadAction.PDF_EXPORT:
            return [PdfFile(pdf_url=download["pdf_url"]) for download in data]

    @utils.adapt_async_sync
    async def get_emitted(self, session_key, date_start, date_end, status, action):
        data = await self.call_api(
            "GET",
            "/cfdi/emitted/",
            params={
                "session_key": session_key,
                "date_start": date_start.strftime("%d/%m/%Y"),
                "date_end": date_end.strftime("%d/%m/%Y"),
                "status": status.value,
                "action": action.value,
            },
        )
        return self._handle_bill_parsing(action, data["emitted"])

    @utils.adapt_async_sync
    async def download_emitted(self, session_key, bill_id):
        data = await self.call_api(
            "GET",
            "/cfdi/emitted/{}/".format(bill_id),
            params={
                "session_key": session_key,
            },
        )
        return DownloadFile(**data["download"])

    @utils.adapt_async_sync
    async def get_received(self, session_key, year, month, status, action):
        data = await self.call_api(
            "GET",
            "/cfdi/received/",
            params={
                "session_key": session_key,
                "year": year,
                "month": month,
                "status": status.value,
                "action": action.value,
            },
        )
        return self._handle_bill_parsing(action, data["received"])

    @utils.adapt_async_sync
    async def download_received(self, session_key, bill_id):
        data = await self.call_api(
            "GET",
            "/cfdi/received/{}/".format(bill_id),
            params={
                "session_key": session_key,
            },
        )
        return DownloadFile(**data["download"])

    @utils.adapt_async_sync
    async def get_downloads(self, session_key):
        data = await self.call_api(
            "GET",
            "/cfdi/download/",
            params={
                "session_key": session_key,
            },
        )
        return [CFDIDownloadItem(**item) for item in data["downloads"]]

    @utils.adapt_async_sync
    async def get_download(self, session_key, request_id):
        data = await self.call_api(
            "GET",
            "/cfdi/download/{}/".format(request_id),
            params={
                "session_key": session_key,
            },
        )
        return DownloadFile(**data["download"])

    @utils.adapt_async_sync
    async def get_acknowledgements(
        self,
        session_key,
        year,
        month_start,
        month_end,
        motive,
        document_type,
        status,
        send_type,
    ):
        data = await self.call_api(
            "GET",
            "/ccee/acknowledgment/",
            params={
                "session_key": session_key,
                "year": year,
                "month_start": month_start,
                "month_end": month_end,
                "motive": motive.value,
                "document_type": document_type.value,
                "status": status.value,
                "send_type": send_type.value,
            },
        )
        return [AcknowledgementResultModel(**result) for result in data["results"]]

    @utils.adapt_async_sync
    async def download_acknowledgement(self, session_key, ack_id):
        data = await self.call_api(
            "GET",
            "/ccee/acknowledgment/{}/".format(ack_id),
            params={
                "session_key": session_key,
            },
        )
        return DownloadFile(**data["download"])


class AcknowledgementResult(object):
    """
    Info on an acknowledgement, returned by :meth:`Session.get_acknowledgements`
    """

    def __init__(self, client, session_key, data):
        self._client = client
        self._session_key = session_key
        self.id = data.id
        self.period = data.period
        self.motive = data.motive
        self.document_type = data.document_type
        self.send_type = data.send_type
        self.file_name = data.file_name
        self.reception_date = data.reception_date
        self.status = data.status

    @utils.adapt_async_sync
    async def get_download(self):
        """
        Download the acknowledgement data

        :rtype: :class:`Download`
        """
        download = await self._client.download_acknowledgement(
            self._session_key, self.id
        )
        return base_client.Download(self._client, download.download_url)


class DownloadRequest(object):
    """
    A request for bulk downloading of bills.
    """

    def __init__(self, client, session_key, request_id):
        self._download = None
        self._client = client
        self._session_key = session_key
        self.request_id = request_id

    @utils.adapt_async_sync
    async def get_download(self):
        """
        Download the generated zip file with all the xmls

        :rtype: :class:`Download`
        """
        if self._download is None:
            download = await self._client.get_download(
                self._session_key, self.request_id
            )
            self._download = base_client.Download(self._client, download.download_url)
        return self._download

    @utils.adapt_async_sync
    async def is_ready(self):
        """
        Check if the request is ready to download.

        :rtype: bool
        """
        try:
            await self.get_download()
            return True
        except exceptions.NotFoundError:
            return False
