import logging
import uuid
from datetime import timedelta
from typing import List, Optional

import stripe
from django.apps import apps
from django.db import IntegrityError, models, transaction
from django.utils import dateformat, timezone
from django.utils.encoding import smart_str
from stripe.api_resources.abstract.api_resource import APIResource
from stripe.error import InvalidRequestError

from djstripe.utils import get_friendly_currency_amount

from ..fields import JSONField, StripeDateTimeField, StripeForeignKey, StripeIdField
from ..managers import StripeModelManager
from ..settings import djstripe_settings

logger = logging.getLogger(__name__)


class StripeBaseModel(models.Model):
    stripe_class: Optional[APIResource] = None

    djstripe_created = models.DateTimeField(auto_now_add=True, editable=False)
    djstripe_updated = models.DateTimeField(auto_now=True, editable=False)

    class Meta:
        abstract = True

    @classmethod
    def api_list(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs):
        """
        Call the stripe API's list operation for this model.

        :param api_key: The api key to use for this request. \
            Defaults to djstripe_settings.STRIPE_SECRET_KEY.
        :type api_key: string

        See Stripe documentation for accepted kwargs for each object.

        :returns: an iterator over all items in the query
        """

        return cls.stripe_class.list(api_key=api_key, **kwargs).auto_paging_iter()


class StripeModel(StripeBaseModel):
    # This must be defined in descendants of this model/mixin
    # e.g. Event, Charge, Customer, etc.
    expand_fields: List[str] = []
    stripe_dashboard_item_name = ""

    objects = models.Manager()
    stripe_objects = StripeModelManager()

    djstripe_id = models.BigAutoField(
        verbose_name="ID", serialize=False, primary_key=True
    )
    id = StripeIdField(unique=True)

    djstripe_owner_account: Optional[StripeForeignKey] = StripeForeignKey(
        "djstripe.Account",
        on_delete=models.CASCADE,
        to_field="id",
        null=True,
        blank=True,
        help_text="The Stripe Account this object belongs to.",
    )

    livemode = models.BooleanField(
        null=True,
        default=None,
        blank=True,
        help_text="Null here indicates that the livemode status is unknown or was "
        "previously unrecorded. Otherwise, this field indicates whether this record "
        "comes from Stripe test mode or live mode operation.",
    )
    created = StripeDateTimeField(
        null=True,
        blank=True,
        help_text="The datetime this object was created in stripe.",
    )
    metadata = JSONField(
        null=True,
        blank=True,
        help_text="A set of key/value pairs that you can attach to an object. "
        "It can be useful for storing additional information about an object in "
        "a structured format.",
    )
    description = models.TextField(
        null=True, blank=True, help_text="A description of this object."
    )

    class Meta:
        abstract = True
        get_latest_by = "created"

    def _get_base_stripe_dashboard_url(self):
        owner_path_prefix = (
            (self.djstripe_owner_account.id + "/")
            if self.djstripe_owner_account
            else ""
        )
        return "https://dashboard.stripe.com/{}{}".format(
            owner_path_prefix, "test/" if not self.livemode else ""
        )

    def get_stripe_dashboard_url(self) -> str:
        """Get the stripe dashboard url for this object."""
        if not self.stripe_dashboard_item_name or not self.id:
            return ""
        else:
            return "{base_url}{item}/{id}".format(
                base_url=self._get_base_stripe_dashboard_url(),
                item=self.stripe_dashboard_item_name,
                id=self.id,
            )

    @property
    def human_readable_amount(self) -> str:
        return get_friendly_currency_amount(self.amount, self.currency)

    @property
    def default_api_key(self) -> str:
        # If the class is abstract (StripeModel), fall back to default key.
        if not self._meta.abstract:
            if self.djstripe_owner_account:
                return self.djstripe_owner_account.get_default_api_key()
        return djstripe_settings.get_default_api_key(self.livemode)

    def _get_stripe_account_id(self, api_key=None) -> Optional[str]:
        """
        Call the stripe API's retrieve operation for this model.

        :param api_key: The api key to use for this request. \
            Defaults to djstripe_settings.STRIPE_SECRET_KEY.
        :type api_key: string
        :param stripe_account: The optional connected account \
            for which this request is being made.
        :type stripe_account: string
        """
        api_key = api_key or self.default_api_key

        try:
            djstripe_owner_account = self.djstripe_owner_account
            if djstripe_owner_account is not None:
                return djstripe_owner_account.id
        except (AttributeError, KeyError, ValueError):
            pass

        # Get reverse foreign key relations to Account in case we ne
ed to
        # retrieve ourselves using that Account ID.
        reverse_account_relatio
ns = (
            field
            for field in self._meta.get_fields(include_parents=True)
            if field.is_relation and field.one_to_many
            # Avoid circular import problems by using the app registry to
            # get the model class rather than a direct import.
            and field.related_model
            is apps.get_model(app_label="djstripe", model_name="account")
        )

        # Handle case where we have a reverse relation to Account and should pass
        # that account ID to the retrieve call.
        for field in reverse_account_relations:
            # Grab the related object, using the first one we find.
            reverse_lookup_attr = field.get_accessor_name()
            account = getattr(self, reverse_lookup_attr).first()

            if account is not None:
                return account.id

        return None

    def api_retrieve(self, api_key=None, stripe_account=None):
        """
        Call the stripe API's retrieve operation for this model.

        :param api_key: The api key to use for this request. \
            Defaults to djstripe_settings.STRIPE_SECRET_KEY.
        :type api_key: string
        :param stripe_account: The optional connected account \
            for which this request is being made.
        :type stripe_account: string
        """
        # Prefer passed in stripe_account if set.
        if not stripe_account:
            stripe_account = self._get_stripe_account_id(api_key)

        return self.stripe_class.retrieve(
            id=self.id,
            api_key=api_key or self.default_api_key,
            expand=self.expand_fields,
            stripe_account=stripe_account,
        )

    @classmethod
    def _api_create(cls, api_key=djstripe_settings.STRIPE_SECRET_KEY, **kwargs):
        """
        Call the stripe API's create operation for this model.

        :param api_key: The api key to use for this request. \
            Defaults to djstripe_settings.STRIPE_SECRET_KEY.
        :type api_key: string
        """

        return cls.stripe_class.create(api_key=api_key, **kwargs)

    def _api_delete(self, api_key=None, stripe_account=None, **kwargs):
        """
        Call the stripe API's delete operation for this model

        :param api_key: The api key to use for this request. \
            Defaults to djstripe_settings.STRIPE_SECRET_KEY.
        :type api_key: string
        :param stripe_account: The optional connected account \
            for which this request is being made.
        :type stripe_account: string
        """
        api_key = api_key or self.default_api_key
        # Prefer passed in stripe_account if set.
        if not stripe_account:
            stripe_account = self._get_stripe_account_id(api_key)

        return self.api_retrieve(api_key=api_key, stripe_account=stripe_account).delete(
            **kwargs
        )

    def