import json
import logging
from decimal import Decimal
from math import modf

import stripe
from django.conf import settings
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.utils import timezone

from vendor.config import (DEFAULT_CURRENCY, StripeConnectAccountConfig,
                           VendorSiteCommissionConfig)
from vendor.integrations import StripeIntegration
from vendor.models import (CustomerProfile, Invoice, Offer, Payment, Receipt,
                           Subscription)
from vendor.models.choice import (Country, InvoiceStatus, PurchaseStatus,
                                  SubscriptionStatus, TermDetailUnits,
                                  TermType)
from vendor.processors.base import PaymentProcessorBase

logger = logging.getLogger(__name__)


class StripeQueryBuilder:
    """
    Query builder that adheres to Stripe search rules found here https://stripe.com/docs/search

    Ex:
    To generate a query like 'name:"Johns Offer" AND metadata["site"]:"site4"'

    query_builder = StripeQueryBuilder()

    name_clause = query_builder.make_clause_template()
    name_clause['field'] = 'name'
    name_clause['value'] = 'Johns Offer'
    name_clause['operator'] = query_builder.EXACT_MATCH
    name_clause['next_operator'] = query_builder.AND

    metadata_clause = query_builder.make_clause_template()
    metadata_clause['field'] = 'metadata'
    metadata_clause['key'] = 'site'
    metadata_clause['value'] = 'site4'
    metadata_clause['operator'] = query_builder.EXACT_MATCH

    query = query_builder.build_search_query(processor.stripe.Product, [name_clause, metadata_clause])


    """

    # Search syntax
    EXACT_MATCH = ':'
    AND = 'AND'
    OR = 'OR'
    EXCLUDE = '-'
    NULL = 'NULL'
    SUBSTRING_MATCH = '~'
    GREATER_THAN = '>'
    LESS_THAN = '<'
    EQUALS = '='
    GREATER_THAN_OR_EQUAL_TO = '>='
    LESS_THAN_OR_EQUAL_TO = '<='

    # Valid fields
    VALID_FIELDS = {
        'charge': [
            'amount',
            'billing_details.address.postal_code',
            'created',
            'currency',
            'customer',
            'disputed',
            'metadata',
            'payment_method_details.card.last4',
            'payment_method_details.card.exp_month',
            'payment_method_details.card.exp_year',
            'payment_method_details.card.brand',
            'payment_method_details.card.fingerprint',
            'refunded',
            'status'
        ],
        'customer': [
            'created',
            'email',
            'metadata',
            'name',
            'phone'
        ],
        'invoice': [
            'created',
            'currency',
            'customer',
            'metadata',
            'number',
            'receipt_number',
            'subscription',
            'total'
        ],
        'paymentintent': [
            'amount',
            'created',
            'currency',
            'customer',
            'metadata',
            'status',
        ],
        'price': [
            'active',
            'lookup_key',
            'currency',
            'product',
            'metadata',
            'type',
        ],
        'product': [
            'active',
            'description',
            'name',
            'shippable',
            'metadata',
            'url',
        ],
        'subscription': [
            'created',
            'metadata',
            'status',
        ],
    }

    def make_clause_template(self, field=None, operator=None, key=None, value=None, next_operator=None):
        return {
            'field': field,
            'operator': operator,
            'key': key,
            'value': value,
            'next_operator': next_operator
        }

    def is_valid_field(self, stripe_object_class, field):
        return field in self.VALID_FIELDS[stripe_object_class.__name__.lower()]

    def search_clause_checks_pass(self, clause_obj):
        """
        All checks should be added here to make sure the caller isnt missing required params
        """
        if clause_obj.get('field', None) == 'metadata':
            if not clause_obj.get('key', None):
                logger.error(f'StripeQueryBuilder.search_clause_checks_pass: metadata searches need a key field')
                return False

        # TODO add more checks

        return True

    def build_search_query(self, stripe_object_class, search_clauses):
        query = ""

        if not isinstance(search_clauses, list):
            logger.error(f'Passed in params {search_clauses} is not a list of search clauses (dicts)')
            return query

        if not len(search_clauses) > 0:
            logger.error(f'Passed in search clauses {search_clauses} cannot be empty')
            return query

        for query_obj in search_clauses:
            field = query_obj.get('field', None)
            operator = query_obj.get('operator', None)
            key = query_obj.get('key', None)
            value = query_obj.get('value', None)
            next_operator = query_obj.get('next_operator', None)

            if not self.search_clause_checks_pass(query_obj):
                logger.error(f'StripeQueryBuilder.build_search_query: search clause {query_obj} is not valid')
                return query

            if self.is_valid_field(stripe_object_class, field):
                if not key:
                    # not metadata
                    if isinstance(value, str):
                        query += f'{field}{operator}"{value}"'
                    else:
                        query += f'{field}{operator}{value}'
                else:
                    # is metadata
                    if isinstance(value, str):
                        query += f'{field}["{key}"]{operator}"{value}"'
                    else:
                        query += f'{field}["{key}"]{operator}{value}'

                if next_operator:
                    # space, AND, OR
                    query += f' {next_operator} '

            else:
                logger.error(f'StripeQueryBuilder.build_search_query: {field} is not valid for {stripe_object_class}')
                return query

        return query


class StripeProcessor(PaymentProcessorBase):
    """ 
    Implementation of Stripe SDK
    https://self.stripe.com/docs/api/authentication?lang=python
    """

    TRANSACTION_SUCCESS_MESSAGE = 'message'
    TRANSACTION_SUCCESS_CODE = 'code'
    TRANSACTION_FAIL_MESSAGE = 'error_text'
    TRANSACTION_FAIL_CODE = 'error_code'
    TRANSACTION_RESPONSE_CODE = 'response_code'
    source = None
    products_mapping = {}

    def processor_setup(self, site, source=None):
        self.credentials = StripeIntegration(site)
        self.source = source
        self.site = site
        self.stripe = stripe
        self.query_builder = StripeQueryBuilder()

        if self.credentials.instance:
            self.stripe.api_key = self.credentials.instance.private_key
        elif settings.STRIPE_SECRET_KEY:
            self.stripe.api_key = settings.STRIPE_SECRET_KEY
        else:
            logger.error("StripeProcessor missing keys in settings: STRIPE_PUBLIC_KEY")
            raise ValueError("StripeProcessor missing keys in settings: STRIPE_PUBLIC_KEY")

        # TODO handle this better, but needed to get passed this error message
        # Search is not supported on api version 2016-07-06. Update your API version, or set the API Version of this request to 2020-08-27 or greater.
        # https://stripe.com/docs/libraries/set-version
        self.stripe.api_version = '2023-08-16'

    def validate_invoice_customer_in_stripe(self):
        if not self.invoice.profile.meta.get('stripe_id'):
            self.create_stripe_customers([self.invoice.profile])
        
        stripe_customer = self.stripe_get_object(self.stripe.Customer, self.invoice.profile.meta.get('stripe_id'))

        if not stripe_customer:
            self.create_stripe_customers([self.invoice.profile])

    def validate_invoice_offer_in_stripe(self):
        offer_to_create = []
        for order_item in self.invoice.get_one_time_transaction_order_items():
            if not order_item.offer.meta.get('stripe'):
                offer_to_create.append(order_item.offer)

        self.create_offers(offer_to_create)

    def validate_invoice_subscriptions_in_stripe(self):
        subscriptions_to_create = []
        for order_item in self.invoice.get_recurring_order_items():
            if not order_item.offer.meta.get('stripe'):
                subscriptions_to_create.append(order_item.offer)

        self.create_offers(subscriptions_to_create)

    ##########
    # Parsers
    ##########
    def parse_response(self, response_data):
        """
        Processes the transaction response
        """
        ...

    ##########
    # Stripe utils
    ##########
    def stripe_call(self, *args):
        func, func_args = args
        self.transaction_succeeded = False

        try:
            if isinstance(func_args, str):
                self.transaction_response = func(func_args)
            else:
                self.transaction_response = func(**func_args)

        except (self.stripe.error.CardError, self.stripe.error.RateLimitError,
                self.stripe.error.InvalidRequestError, self.stripe.error.AuthenticationError,
                self.stripe.error.APIConnectionError, self.stripe.error.StripeError,
                Exception) as e:

            user_message = ""
            if hasattr(e, 'user_message'):
                user_message = e.user_message
            raw = {
                'exception': f"{e}\n{func}\n{func_args}:user_message: {user_message}"
            }
            errors = {
                'error': f"{e}"
            }
            self.transaction_info = self.get_transaction_info(raw=raw, errors=errors)
            logger.error(self.transaction_info)
            return None

        self.transaction_succeeded = True
        self.transaction_info = self.get_transaction_info(raw=f"{func} - {func_args} {json.dumps(self.transaction_response)}", data=self.transaction_response.data if 'data' in self.transaction_response else "")

        return self.transaction_response

    def convert_decimal_to_integer(self, decimal):
        if decimal == 0:
            return 0
            
        fraction_part, whole_part = modf(decimal)
                
        whole_str = str(whole_part).split('.')[0]
        fraction_str = str(fraction_part).split('.')[1][:2]

        if len(fraction_str) < 2:
            fraction_str = "0" + fraction_str

        stripe_amount = int("".join([whole_str, fraction_str]))
            
        return stripe_amount

    def convert_integer_to_decimal(self, integer):
        if not integer:
            return 0
        
        return Decimal(f"{str(integer)[:-2]}.{str(integer)[-2:]}")
    
    def get_stripe_connect_account(self):
        stripe_connect = StripeConnectAccountConfig(self.site)

        if stripe_connect.instance:
            return stripe_connect.get_key_value('stripe_connect_account')

        return None

    def get_application_fee_percent(self):
        vendor_site_commission = VendorSiteCommissionConfig(self.site)

        if vendor_site_commission.instance:
            return vendor_site_commission.get_key_value('commission')

        return None

    def get_application_fee_amount(self):
        vendor_site_commission = VendorSiteCommissionConfig(self.site)

        if vendor_site_commission.instance:
            return self.convert_decimal_to_integer((vendor_site_commission.get_key_value('commission')*self.invoice.total)/100)

        return None

    def get_invoice_status(self, stripe_status):
        if stripe_status in ["draft", ]:
            return InvoiceStatus.CART
        elif stripe_status in ["paid", "uncollectible", "open", "void"]:
            return InvoiceStatus.COMPLETE

    def get_payment_status(self, stripe_status, is_refund):
        if is_refund:
            return PurchaseStatus.REFUNDED
        
        if stripe_status == "pending":
            return PurchaseStatus.QUEUED
        elif stripe_status == "succeeded":
            return PurchaseStatus.SETTLED
        elif stripe_status == "failed":
            return PurchaseStatus.DECLINED

    def get_subscription_status(self, stripe_status):
        if stripe_status in ["active", "trialing"]:
            return SubscriptionStatus.ACTIVE
        elif stripe_status == "paused":
            return SubscriptionStatus.PAUSED
        elif stripe_status == "canceled":
            return SubscriptionStatus.CANCELED
        elif stripe_status in ["past_due", "unpaid", "incomplete"]:
            return SubscriptionStatus.SUSPENDED
        elif stripe_status == "incomplete_expired":
            return SubscriptionStatus.EXPIRED

    ##########
    # CRUD Stripe Object
    ##########
    def stripe_create_object(self, stripe_object_class, object_data):
        stripe_object = self.stripe_call(stripe_object_class.create, object_data)

        return stripe_object

    def stripe_query_object(self, stripe_object_class, query, limit=100, page=None):
        if isinstance(query, dict):
            query_data = query
            query_data['limit'] = limit
            query_data['page'] = page

        elif isinstance(query, str):
            query_data = {
                'query': query,
                'limit': limit,
                'page': page
            }

        else:
            logger.error(f'stripe_query_object: {query} is invalid')
            return None

        query_result = self.stripe_call(stripe_object_class.search, query_data)

        return query_result

    def stripe_delete_object(self, stripe_object_class, object_id):
        delete_result = self.stripe_call(stripe_object_class.delete, object_id)

        return delete_result

    def stripe_get_object(self, stripe_object_class, object_id, expand=None):
        if expand:
            # some fields are expandable https://stripe.com/docs/api/expanding_objects
            object_id = {
                'id': object_id,
                'expand': expand
            }
        stripe_object = self.stripe_call(stripe_object_class.retrieve, object_id)

        return stripe_object

    def stripe_update_object(self, stripe_object_class, object_id, object_data):
        object_data['sid'] = object_id
        stripe_object = self.stripe_call(stripe_object_class.modify, object_data)

        if self.transaction_succeeded:
            self.transaction_succeeded = False
            return stripe_object

        return None

    def stripe_list_objects(self, stripe_object_class, limit=100, starting_after=None):
        object_data = {
            'limit': limit,
            'starting_after': starting_after
        }
        stripe_objects = self.stripe_call(stripe_object_class.list, object_data)

        return stripe_objects

    def get_all_stripe_list_objects(self, stripe_object):
        """
        Get entire list of any stripe object with .list() method.
        Will make multiple stripe calls until the list is exhausted
        """
        object_list = []
        starting_after = None
        while True:
            objs = self.stripe_list_objects(stripe_object, limit=100, starting_after=starting_after)
            object_list.extend(objs)
            if objs['has_more']:
                starting_after = objs['data'][-1]['id']
            else:
                break

        return object_list

    def get_all_stripe_search_objects(self, stripe_object, query, limit=100):
        """
        Get entire list of any stripe object with .search() method.
        Will make multiple stripe calls until the list is exhausted
        """
        object_list = []
        page = None

        while True:
            objs = self.stripe_query_object(stripe_object, query=query, limit=limit, page=page)
            object_list.extend(objs['data'])
            if objs['has_more']:
                page = objs['next_page']
            else:
                break

        return object_list
        
    def stripe_update_object_metadata(self, stripe_object_class, object_id, metadata):
        object_data = {}
        object_data['sid'] = object_id
        object_data['metadata'] = metadata

        stripe_object = self.stripe_call(stripe_object_class.modify, object_data)

        if self.transaction_succeeded:
            self.transaction_succeeded = False
            return stripe_object

        return None

    # Stripe Object Builders
    ##########
    def build_transfer_data(self):
        return {
            'destination': self.get_stripe_connect_account(),
            # Not required if you are using the application_fee parameter
            # 'amount_percent': 100 - self.get_application_fee_percent()
        }

    def build_customer(self, customer_profile):
        return {
            'name': f"{customer_profile.user.first_name} {customer_profile.user.last_name}",
            'email': customer_profile.user.email,
            'metadata': {'site': customer_profile.site}
        }
    
    def build_product(self, offer):
        return {
            'name': offer.name,
            'metadata': {'site': offer.site.domain, 'pk': offer.pk}

        }
   
    def build_price(self, price, currency):
        if 'stripe' not in price.offer.meta or 'product_id' not in price.offer.meta['stripe']:
            raise TypeError(f"Price cannot be created without a product_id on price.offer.meta['stripe'] field offer: {price.offer}")

        price_data = {
            'active': True,
            'product': price.offer.meta['stripe']['product_id'],
            'currency': currency,
            'unit_amount': self.convert_decimal_to_integer(price.cost),
            'metadata': {
                'site': price.offer.site.domain,
                'pk': price.pk
            }
        }
        
        if price.offer.terms < TermType.PERPETUAL:
            price_data['recurring'] = {
                'interval': 'month' if price.offer.term_details['term_units'] == TermDetailUnits.MONTH else 'year',
                'interval_count': price.offer.term_details['period_length'],
                'usage_type': 'licensed'
            }
        
        return price_data
    
    def is_card_valid(self):
        # TODO: see how stripe has check for 
        return True

    def build_coupon(self, offer, currency):
        coupon_data = {
            'name': offer.name,
            'currency': currency,
            'amount_off': self.convert_decimal_to_integer(offer.discount(currency)),
            'metadata': {'site': offer.site.domain}
        }

        if offer.terms < TermType.PERPETUAL:
            coupon_data['amount_off'] = self.convert_decimal_to_integer(offer.get_trial_discount())
            coupon_data['duration'] = 'once' if offer.term_details['trial_occurrences'] <= 1 else 'repeating'
            coupon_data['duration_in_months'] = offer.term_details['trial_occurrences']
        
        return coupon_data

    def build_payment_method(self):
        return {
            'type': 'card',
            'card': {
                'number': self.payment_info.cleaned_data.get('card_number'),
                'exp_month': self.payment_info.cleaned_data.get('expire_month'),
                'exp_year': self.payment_info.cleaned_data.get('expire_year'),
                'cvc': self.payment_info.cleaned_data.get('cvv_number'),
            },
            'billing_details': {
                'address': {
                    'line1': self.billing_address.cleaned_data.get('address_1', None),
                    'line2': self.billing_address.cleaned_data.get('address_2', None),
                    'city': self.billing_address.cleaned_data.get("locality", ""),
                    'state': self.billing_address.cleaned_data.get("state", ""),
                    'country': Country.names[Country.values.index(int(self.billing_address.cleaned_data.get("country")))],
                    'postal_code': self.billing_address.cleaned_data.get("postal_code")
                },
                'name': self.payment_info.cleaned_data.get('full_name', None),
                'email': self.invoice.profile.user.email
            }
        }

    def build_payment_intent(self, amount, currency=DEFAULT_CURRENCY):
        return {
            'amount': amount,
            'currency': currency,
            'customer': self.invoice.profile.meta['stripe_id'],
            'application_fee_percent': self.get_application_fee_amount(),
            'transfer_data': self.get_stripe_connect_account(),
            'on_behalf_of': self.get_stripe_connect_account()
        }

    def build_setup_intent(self, payment_method_id):
        return {
            'customer': self.invoice.profile.meta['stripe_id'],
            'confirm': True,
            'payment_method_types': ['card'],
            'payment_method': payment_method_id,
            'metadata': {'site': self.invoice.site},
            'on_behalf_of': self.get_stripe_connect_account()
        }
    
    def build_subscription(self, subscription, payment_method_id):
        price = subscription.offer.get_current_price_instance()
        return {
            'customer': self.invoice.profile.meta['stripe_id'],
            'promotion_code': self.invoice.coupon_code.first().meta['stripe_id'] if self.invoice.coupon_code.count() else None,
            'items': [{'price': subscription.offer.meta['stripe']['prices'][str(price.pk)]}],
            'default_payment_method': payment_method_id,
            'metadata': {'site': self.invoice.site},
            'trial_period_days': subscription.offer.get_trial_days(),
            'application_fee_percent': self.get_application_fee_percent(),
            'transfer_data': self.build_transfer_data(),
            'on_behalf_of': self.get_stripe_connect_account()
        }

    def build_invoice_line_item(self, order_item, invoice_id):
        line_item = {
            'customer': self.invoice.profile.meta.get('stripe_id'),
            'invoice': invoice_id,
            'price': order_item.offer.meta['stripe'].get('price_id'),
        }

        if order_item.offer.has_trial() or order_item.offer.has_valid_billing_start_date() or order_item.offer.discount():
            line_item['discounts'] = [{'coupon': order_item.offer.meta['stripe'].get('coupon_id')}]

        return line_item

    def build_invoice(self, currency=DEFAULT_CURRENCY):
        return {
            'customer': self.invoice.profile.meta.get('stripe_id'),
            'currency': currency,
        }

    ##########
    # Customers
    ##########
    def get_stripe_customers(self, site, expand=None):
        """
        Returns all Stripe created customers for this site
        """

        clause = self.query_builder.make_clause_template(
            field='metadata',
            key='site',
            value=site.domain,
            operator=self.query_builder.EXACT_MATCH
        )

        query = self.query_builder.build_search_query(self.stripe.Customer, [clause])

        customer_search = self.get_all_stripe_search_objects(self.stripe.Customer, {'query': query})

        return customer_search
        
    def get_customer_profile(self, stripe_customer, site=None):
        """Get's CustomerProfile if found.
        Try's to get the CustomerProfile based on the stripe_customer.email and site.
        If found and the CustomerProfile instance does not have the associated stripe_customer.id it addes it addes it to the meta field.

        Args:
            site: Site instance
            stripe_customer: Stripe Customer Object

        Returns:
            CustomerProfile Instance or None
        """
        customer_profile = None
        if not site:
            site = self.site

        try:
            customer_profile = CustomerProfile.objects.get(site=site, user__email=stripe_customer.email)
        except ObjectDoesNotExist:
            logger.error(f"get_customer_profile CustomerProfile Not Found email:{stripe_customer.email} site: {site.domain}")
        except MultipleObjectsReturned:
            logger.error(f"get_customer_profile Multiple CustomerProfiles returned for email:{stripe_customer.email} site: {site.domain}")
        except Exception as exce:
            logger.error(f"get_customer_profile Exception: {exce} for email:{stripe_customer.email} site: {site.domain} ")

        if customer_profile and 'stripe_id' not in customer_profile.meta:
            customer_profile.meta.update({'stripe_id': stripe_customer.id})
            customer_profile.save()

        return customer_profile

    def get_customer_profile_and_stripe_customer(self, stripe_customer_id):
        """Gets CustomerProfile and Stripe Customer using Stripe Customer ID
        
            Args:
                stripe_customer_id: Stripe Customer ID
            
            Returns:
                CustomerProfile Instance or None
                Stripe Customer Object or None
        """
        stripe_customer = None
        customer_profile = None
        
        stripe_customer = self.stripe_get_object(stripe_customer_id)
        if not stripe_customer:
            logger.info(f"Stripe Customer was not found: {stripe_customer_id}")
            return customer_profile, stripe_customer

        customer_profile = self.get_customer_profile(stripe_customer)
        if not customer_profile:
            logger.info(f"Customer Profile not found for stripe_customer {stripe_customer.id} site: {self.site}")
            return customer_profile, stripe_customer

        return customer_profile, stripe_customer
    
    def get_vendor_customers_in_stripe(self, customer_email_list, site):
        """
        Returns all vendor customers who have been created as Stripe customers
        """
        users = CustomerProfile.objects.filter(
            user__email__iregex=r'(' + '|'.join(customer_email_list) + ')', # iregex used for case insensitive list match
            site=site
        ).select_related('user')

        return users

    def get_vendor_customers_not_in_stripe(self, customer_email_list, site):
        """
        Returns all vendor customers who have not been created as Stripe customers
        """
        users = CustomerProfile.objects.filter(
            site=site
        ).exclude(
            user__email__iregex=r'(' + '|'.join(customer_email_list) + ')',  # iregex used for case insensitive list match
        ).select_related('user')

        return users

    def create_stripe_customers(self, customers):
        for profile in customers:
            profile_data = self.build_customer(profile)
            new_stripe_customer = self.stripe_create_object(self.stripe.Customer, profile_data)

            if new_stripe_customer:
                profile.meta.update({'stripe_id': new_stripe_customer['id']})
                profile.save()
                logger.info(f"create_stripe_customers: Stripe Customer created: {new_stripe_customer['id']} from profile: {profile} site: {profile.site}")

    def update_stripe_customers(self, customers):
        for profile in customers:
            customer_id = profile.meta.get('stripe_id')
            profile_data = self.build_customer(profile)
            existing_stripe_customer = self.stripe_update_object(self.stripe.Customer, customer_id, profile_data)

            if existing_stripe_customer:
                profile.meta.update({'stripe_id': existing_stripe_customer['id']})
                profile.save()
                logger.info(f"update_stripe_customers: Stipe Customer updated: {existing_stripe_customer['id']} from profile: {profile} site: {profile.site}")
            else:
                self.create_stripe_customers([profile])
    
    def get_customer_payment_methods(self, customer_id):
        payment_methods = self.stripe_call(self.stripe.Customer.list_payment_methods, customer_id)

        if not payment_methods.data:
            return None

        return payment_methods.data

    def get_customer_email(self, customer_id):
        customer = self.stripe_get_object(self.stripe.Customer, customer_id)

        if customer:
            return customer['email']
        return None

    def get_customer_id_for_expiring_cards(self, month):
        # month is passed as YYYY-M

        customers = self.get_stripe_customers(self.site)
        sources = [
            (customer['id'], customer['email'], customer['default_source'])
            for customer in customers if customer.get('default_source')
        ]

        expired_cards_emails = []
        for customer_id, customer_email, source_id in sources:
            if 'card' in source_id:
                stripe_card = self.stripe_call(
                    self.stripe.Customer.retrieve_source,
                    {'id': customer_id, 'nested_id': source_id}
                )
                if stripe_card:
                    if stripe_card['exp_year'] and stripe_card['exp_month']:
                        exp_date = f"{stripe_card['exp_year']}-{stripe_card['exp_month']}"
                        exp_date_obj = timezone.datetime.strptime(exp_date, "%Y-%m")
                        current_date_obj = timezone.datetime.strptime(month, "%Y-%m")
                        date_diff = (exp_date_obj - current_date_obj).days
                        if 0 < date_diff <= 60:
                            expired_cards_emails.append(customer_email)

        return expired_cards_emails

    def get_stripe_customer_from_email(self, email):
        query_builder = StripeQueryBuilder()

        email_clause = query_builder.make_clause_template(
            field='email',
            value=email,
            operator=query_builder.EXACT_MATCH
        )
        
        query = query_builder.build_search_query(self.stripe.Customer, [email_clause])
        query_result = self.stripe_query_object(self.stripe.Customer, query)

        if len(query_result['data']) == 1:
            return query_result['data'][0]
        elif len(query_result['data']) > 1:
            logger.info(f"Multiple stripe customer found for email: {email} on site: {self.site}")
        else:
            logger.info(f"No Stripe Customer Found for email: {email} on site: {self.site}")
        
        return None

    ##########
    # Offers/Products
    ##########
    def get_stripe_product(self, site, pk):
        pk_clause = self.query_builder.make_clause_template(
            field='metadata',
            key='pk',
            value=str(pk),
            operator=self.query_builder.EXACT_MATCH,
            next_operator=self.query_builder.AND
        )
        site_clause = self.query_builder.make_clause_template(
            field='metadata',
            key='site',
            value=site.domain,
            operator=self.query_builder.EXACT_MATCH
        )

        query = self.query_builder.build_search_query(self.stripe.Product, [pk_clause, site_clause])
        search_data = self.stripe_query_object(self.stripe.Product, {'query': query})

        if search_data:
            return search_data.data[0]
        
        return None

    def does_product_exist(self, name, metadata):
        name_clause = self.query_builder.make_clause_template(
            field='name',
            value=name,
            operator=self.query_builder.EXACT_MATCH,
            next_operator=self.query_builder.AND
        )

        metadata_clause = self.query_builder.make_clause_template(
            field='metadata',
            key=metadata['key'],
            value=metadata['value'],
            operator=self.query_builder.EXACT_MATCH
        )

        query = self.query_builder.build_search_query(self.stripe.Product, [name_clause, metadata_clause])

        search_data = self.stripe_query_object(self.stripe.Product, {'query': query})

        if search_data:
            if search_data.get('data', False):
                return True

        return False

    def get_site_stripe_products(self, site):
        """
        Returns all Stripe created Products
        """

        clause = self.query_builder.make_clause_template(
            field='metadata',
            key='site',
            value=site.domain,
            operator=self.query_builder.EXACT_MATCH
        )

        query = self.query_builder.build_search_query(self.stripe.Product, [clause])

        product_search = self.get_all_stripe_search_objects(self.stripe.Product, {'query': query})

        return product_search

    def get_vendor_offers_in_stripe(self, offer_pk_list, site):
        offers = Offer.objects.filter(site=site, pk__in=offer_pk_list).exclude(is_promotional=True)
        return offers

    def get_vendor_offers_not_in_stripe(self, offer_pk_list, site):
        offers = Offer.objects.filter(site=site).exclude(pk__in=offer_pk_list).exclude(is_promotional=True)
        return offers

    def create_stripe_product(self, offer):
        product_data = self.build_product(offer)

        stripe_product = self.stripe_create_object(self.stripe.Product, product_data)

        if not stripe_product:
            logger.error(f"create_stripe_product error: {self.transaction_info}")
            return None
        
        if 'stripe' in offer.meta:
            offer.meta['stripe']['product_id'] = stripe_product['id']
        else:
            offer.meta['stripe'] = {'product_id': stripe_product['id']}

        offer.save()
        logger.info(f"create_stripe_product Stripe Product created: {stripe_product.id} from offer {offer} site {offer.site}")
        return stripe_product
    
    def create_offers(self, offers):
        for offer in offers:
            # build product first, since product_id is needed to build price later
            self.create_stripe_product(offer)

            # TODO: Need to explore what is the best way to upload prices in each currency
            # currently we will only support the default currency (DEFAULT_CURRENCY) se
            # on the vendor.config.py file
            # for currency in AVAILABLE_CURRENCIES:
            #     ...
            self.create_stripe_product_prices(offer)

    def sync_offer(self, offer):
        product_id = offer.meta['stripe']['product_id']
        product_data = self.build_product(offer)
        stripe_product = self.stripe_update_object(self.stripe.Product, product_id, product_data)
        
        if not stripe_product:
            self.create_offers([offer])
        
        if 'stripe' in offer.meta:
            offer.meta['stripe']['product_id'] = stripe_product['id']
        else:
            offer.meta['stripe'] = {'product_id': stripe_product['id']}
        
        offer.save()
        return stripe_product

    def update_offers(self, offers):
        for offer in offers:
            self.sync_offer(offer)
            # Handle Price
            # TODO: Need to explore what is the best way to upload prices in each currency
            # currently we will only support the default currency (DEFAULT_CURRENCY) se
            # on the vendor.config.py file
            # for currency in AVAILABLE_CURRENCIES:
            #     ...
            self.sync_offer_prices(offer)

    def get_product_id_with_name(self, name, metadata):
        name_clause = self.query_builder.make_clause_template(
            field='name',
            value=name,
            operator=self.query_builder.EXACT_MATCH,
            next_operator=self.query_builder.AND
        )
        metadata_clause = self.query_builder.make_clause_template(
            field='metadata',
            key=metadata['key'],
            value=metadata['value'],
            operator=self.query_builder.EXACT_MATCH
        )

        query = self.query_builder.build_search_query(self.stripe.Product, [name_clause, metadata_clause])
        search_data = self.stripe_query_object(self.stripe.Product, {'query': query})

        if search_data:
            return search_data['data'][0]['id']
        return None
  
    def get_offer_from_stripe_product(self, stripe_product):
        offer = None

        if "pk" not in stripe_product.metadata:
            logger.error(f"get_offer_from_stripe_product stripe_product id: ({stripe_product.id}, {stripe_product.name}) does not have a pk in metadata")
            return offer
        
        try:
            offer = Offer.objects.get(pk=int(stripe_product.metadata["pk"]))
        except ObjectDoesNotExist:
            logger.error(f"get_offer_from_stripe_product Offer Does Not Exists for stripe_product: ({stripe_product.id}, {stripe_product.name}) pk: {stripe_product.metadata['pk']}")
        except MultipleObjectsReturned:
            logger.error(f"get_offer_from_stripe_product Multiple Offers Found for stripe_product: ({stripe_product.id}, {stripe_product.name}) pk: {stripe_product.metadata['pk']}")
        except Exception as exce:
            logger.error(f"get_offer_from_stripe_product Exception {exce} for stripe_product: ({stripe_product.id}, {stripe_product.name}) pk: {stripe_product.metadata['pk']}")
        
        return offer

    ##########
    # Prices
    ##########
    def does_stripe_and_vendor_price_match(self, stripe_price, vendor_price):
        if not stripe_price.unit_amount == vendor_price['unit_amount']:
            return False
        
        if 'recurring' in vendor_price:
            if 'recurring' not in stripe_price:
                return False

            if not stripe_price.recurring['interval'] == vendor_price['recurring']['interval']:
                return False
            
            if not stripe_price.recurring['interval_count'] == vendor_price['recurring']['interval_count']:
                return False
        
        return True

    def get_price_id_with_product(self, product):
        price = self.stripe_get_object(self.stripe.Price, {'id': product})

        if price:
            return price['id']

        return None
    
    def get_stripe_prices_for_product(self, product):
        product_clause = self.query_builder.make_clause_template(
            field='product',
            value=product,
            operator=self.query_builder.EXACT_MATCH
        )

        query = self.query_builder.build_search_query(self.stripe.Price, [product_clause])

        search_data = self.get_all_stripe_search_objects(self.stripe.Price, {'query': query})

        return search_data

    def does_price_exist(self, product, metadata):
        product_clause = self.query_builder.make_clause_template(
            field='product',
            value=product,
            operator=self.query_builder.EXACT_MATCH,
            next_operator=self.query_builder.AND
        )
        metadata_clause = self.query_builder.make_clause_template(
            field='metadata',
            key=metadata['key'],
            value=metadata['value'],
            operator=self.query_builder.EXACT_MATCH
        )

        query = self.query_builder.build_search_query(self.stripe.Price, [product_clause, metadata_clause])

        search_data = self.stripe_query_object(self.stripe.Price, {'query': query})

        if search_data:
            if search_data.get('data', False):
                return True

        return False

    def create_price_with_product(self, product):
        price_data = {
            'currency': self.invoice.currency,
            'product': product
        }

        price = self.stripe_create_object(self.stripe.Price, price_data)

        if price:
            return price['id']

        return None

    def create_stripe_product_prices(self, offer):
        for price in offer.prices.all():
            price_data = self.build_price(price, DEFAULT_CURRENCY)
            stripe_price = self.stripe_create_object(self.stripe.Price, price_data)
            
            if not stripe_price:
                logger.error(f"create_stripe_product_prices price not created: {self.transaction_info}")
                return None
            
            if 'stripe' not in offer.meta:
                offer.meta = {'stripe': {'prices': {price.pk: stripe_price.id}}}
            elif 'prices' in offer.meta['stripe']:
                offer.meta['stripe']['prices'].update({price.pk: stripe_price.id})
            else:
                offer.meta['stripe'].update({'prices': {price.pk: stripe_price.id}})

            offer.save()
            logger.info(f"create_stripe_product_prices: Stripe Price created: {stripe_price.id} from offer {offer} site {offer.site}")

    def sync_offer_prices(self, offer):
        stripe_product_prices = self.get_stripe_prices_for_product(offer.meta['stripe']['product_id'])

        if not stripe_product_prices:
            self.create_stripe_product_prices(offer)
            return None
        
        current_price = offer.get_current_price_instance()
        
        for stripe_price in stripe_product_prices:
            try:
                price = offer.prices.get(pk=stripe_price.metadata['pk'])
            except ObjectDoesNotExist:
                self.stripe_delete_object(self.stripe.Price, stripe_price.id)
            
            price_data = self.build_price(price, DEFAULT_CURRENCY) | {'active': False}
            if current_price == price:
                price_data['active'] = True
            
            price_data.pop('unit_amount', None)
            price_data.pop('product', None)
            price_data.pop('currency', None)
            price_data.pop('recurring', None)

            self.stripe_update_object(self.stripe.Price, stripe_price.id, price_data)

            if 'stripe' not in offer.meta:
                offer.meta = {'stripe': {'prices': {price.pk: stripe_price.id}}}
            elif 'prices' in offer.meta['stripe']:
                offer.meta['stripe']['prices'].update({price.pk: stripe_price.id})
            else:
                offer.meta['stripe'].update({'prices': {price.pk: stripe_price.id}})
            offer.save()
            
            logger.info(f"sync_offer_prices: Stripe Price Updated: ({stripe_price.id}, {price.pk})")

    ##########
    # Coupons
    ##########
    def match_coupons(self, coupons, offer):
        matches = []
        amount_off = self.convert_decimal_to_integer(offer.discount())
        duration = offer.get_trial_duration_in_months()
        for coupon in coupons:
            if coupon['metadata']['site'] == offer.site.domain and coupon['amount_off'] == amount_off\
                    and coupon['duration_in_months'] == duration:
                matches.append(coupon)
        return matches

    def get_coupons(self):
        """
        Returns all stripe Coupons

        This is needed since there is no search method on Coupon.
        """

        return self.get_all_stripe_list_objects(self.stripe.Coupon) or []

    ##########
    # Invoice
    ##########
    def get_subscription_invoices(self, subscription_id):
        subscription_clause = self.query_builder.make_clause_template(
            field='subscription',
            value=subscription_id,
            operator=self.query_builder.EXACT_MATCH
        )

        query = self.query_builder.build_search_query(self.stripe.Invoice, [subscription_clause, ])

        invoices = self.stripe_query_object(self.stripe.Invoice, {'query': query})

        return invoices.data

    def get_or_create_invoice_from_stripe_invoice(self, stripe_invoice, offer, customer_profile):
        invoice = None
        created = False

        try:
            invoice, created = Invoice.objects.get_or_create(
                site=customer_profile.site,
                vendor_notes__has_key="stripe_id",
                vendor_notes__stripe_id=stripe_invoice.id,
                profile=customer_profile,
                defaults={
                    "vendor_notes": {'stripe_id': stripe_invoice.id},
                    "status": self.get_invoice_status(stripe_invoice.status),
                    "ordered_date": timezone.datetime.fromtimestamp(stripe_invoice.created, tz=timezone.utc)
                }
            )
        except MultipleObjectsReturned:
            logger.error(f"get_or_create_invoice_from_stripe_invoice Multiple Invoice for id: {stripe_invoice.id} customer_profile: {customer_profile.user.email}")
            invoice = Invoice.objects.filter(
                site=customer_profile.site,
                vendor_notes__has_key="stripe_id",
                vendor_notes__stripe_id=stripe_invoice.id,
                profile=customer_profile,
            ).first()
        except Exception as exce:
            logger.error(f"get_or_create_invoice_from_stripe_invoice Exception {exce} for id: {stripe_invoice.id} customer_profile: {customer_profile.user.email}")

        if created:
            invoice.empty_cart()
            invoice.add_offer(offer)
            invoice.total = self.convert_integer_to_decimal(stripe_invoice.total)
            logger.info(f"get_or_create_invoice_from_stripe_invoice Invoice Created: ({invoice.pk}, {stripe_invoice.id})")
        
        invoice.status = self.get_invoice_status(stripe_invoice.status)
        invoice.save()

        return invoice, created

    def get_offers_from_invoice_line_items(self, stripe_line_items):
        offers = []
        
        for line_item in stripe_line_items:
            stripe_product = self.stripe_get_object(self.stripe.Product, line_item.product)

            offer = self.get_offer_from_stripe_product(stripe_product)
            if offer:
                offers.append(offer)

        return offers
        
    ##########
    # Payments and Receipts
    ##########

    def get_or_create_payment_from_stripe_payment_and_charge(self, invoice, stripe_payment_method, stripe_charge):
        payment = None
        created = False

        try:
            payment, created = Payment.objects.get_or_create(
                transaction=stripe_charge.id,
                defaults={
                    "profile": invoice.profile,
                    "amount": self.convert_integer_to_decimal(stripe_charge.amount_captured),
                    "invoice": invoice,
                    "submitted_date": timezone.datetime.fromtimestamp(stripe_charge.created, tz=timezone.utc),
                    "transaction": stripe_charge.id,
                    "status": self.get_payment_status(stripe_charge.status, stripe_charge.refunded),
                    "success": stripe_charge.paid,
                    "payee_full_name": stripe_payment_method['billing_details']['name'],
                    "result": {
                        'account_number': stripe_payment_method['card']['last4'] if "card" in stripe_payment_method else "",
                        'full_name': stripe_payment_method['billing_details']['name']
                    }
                }
            )
        except MultipleObjectsReturned:
            dup_payments = Payment.objects.filter(transaction=stripe_charge.id)
            payment = dup_payments.first()
            logger.error(f"get_or_create_payment_from_stripe_payment_and_charge Multiple Payments for transaction: {stripe_charge.id} queryresult for payments: {dup_payments}")
        except Exception as exce:
            logger.error(f"get_or_create_payment_from_stripe_payment_and_charge Exception {exce} for stripe_charge id: {stripe_charge.id}")

        if created:
            logger.info(f"get_or_create_payment_from_stripe_payment_and_charge: Payment Created ({payment.pk},{stripe_charge.id})")
            
        return payment, created

    def get_or_create_subscription_receipt_from_stripe_charge(self, invoice, payment, stripe_charge):
        receipt = None
        created = False
        try:
            receipt, created = Receipt.objects.get_or_create(
                profile=invoice.profile,
                transaction=stripe_charge.id,
                defaults={
                    "order_item": invoice.order_items.first(),
                    "start_date": timezone.datetime.fromtimestamp(stripe_charge.created, tz=timezone.utc),
                    "end_date": invoice.order_items.first().offer.get_offer_end_date(start_date=timezone.datetime.fromtimestamp(stripe_charge.created, tz=timezone.utc)),
                    "subscription": payment.subscription,
                    "meta": payment.result | {"payment_amount": str(payment.amount)}
                }
            )
        except MultipleObjectsReturned:
            dup_receipts = Receipt.objects.filter(profile=invoice.profile, transaction=stripe_charge.id)
            receipt = dup_receipts.first()
            logger.info(f"get_or_create_receipt_from_stripe_charge Multiple Receipts for transaction: {stripe_charge.id}, duplicated receipts: {dup_receipts}")
        except Exception as exce:
            logger.error(f"get_or_create_receipt_from_stripe_charge Exception {exce} for stripe_charge id: {stripe_charge.id}, subscriptions: ({payment.subscription.pk},{payment.subscription.gateway_id})")

        if created:
            receipt.products.add(invoice.order_items.first().offer.products.first())
            logger.info(f"get_or_create_receipt_from_stripe_charge Receipt created: ({receipt.pk}, {stripe_charge.id})")

        return receipt, created
    
    def create_single_purchase_receipts(self, invoice, payment, stripe_charge):
        receipt = None
        created = False

        for order_item in invoice.get_one_time_transaction_order_items():
            try:
                receipt, created = Receipt.objects.get_or_create(
                    profile=invoice.profile,
                    transaction=stripe_charge.id,
                    defaults={
                        "order_item": order_item,
                        "start_date": timezone.datetime.fromtimestamp(stripe_charge.created, tz=timezone.utc),
                        "end_date": order_item.offer.get_offer_end_date(start_date=timezone.datetime.fromtimestamp(stripe_charge.created, tz=timezone.utc)),
                        "subscription": payment.subscription,
                        "meta": payment.result | {"payment_amount": str(payment.amount)}
                    }
                )
            except MultipleObjectsReturned:
                dup_receipts = Receipt.objects.filter(profile=invoice.profile, transaction=stripe_charge.id)
                receipt = dup_receipts.first()
                logger.info(f"get_or_create_receipt_from_stripe_charge Multiple Receipts for transaction: {stripe_charge.id}, duplicated receipts: {dup_receipts}")
            except Exception as exce:
                logger.error(f"get_or_create_receipt_from_stripe_charge Exception {exce} for stripe_charge id: {stripe_charge.id}, subscriptions: ({payment.subscription.pk},{payment.subscription.gateway_id})")

            if created:
                receipt.products.add(order_item.offer.products.first())
                logger.info(f"get_or_create_receipt_from_stripe_charge Receipt created: ({receipt.pk}, {stripe_charge.id})")

    ##########
    # Subscriptions
    ##########
    def get_stripe_subscriptions(self, site):
        clause = self.query_builder.make_clause_template(
            field="metadata",
            key="site",
            value=site.domain,
            operator=self.query_builder.EXACT_MATCH
        )

        query = self.query_builder.build_search_query(self.stripe.Subscription, [clause])

        subscription_search = self.get_all_stripe_search_objects(self.stripe.Subscription, {'query': query})

        return subscription_search

    def get_or_create_subscription_from_stripe_subscription(self, customer_profile, stripe_subscription):
        created = False
        subscription = None
        try:
            subscription, created = Subscription.objects.get_or_create(
                profile=customer_profile,
                gateway_id=stripe_subscription.id)
        except MultipleObjectsReturned:
            logger.error(f"get_or_create_subscription_from_stripe_subscription Multiple Subscriptions returned for customer_profile: {customer_profile} subscription: {stripe_subscription.id}")
        except Exception as exce:
            logger.error(f"get_or_create_subscription_from_stripe_subscription Exception {exce} for customer_profile: {customer_profile} subscription {stripe_subscription.id}")
        
        if subscription:
            subscription.status = self.get_subscription_status(stripe_subscription.status)
            subscription.save()
        
        if created:
            logger.info(f"get_or_create_subscription_from_stripe_subscription Subscription Created: ({subscription.pk}, {stripe_subscription.id})")

        return subscription, created
    
    def get_subscription(self, stripe_subscription):
        subscription = None
        try:
            subscription = Subscription.objects.get(gateway_id=stripe_subscription.id)
        except ObjectDoesNotExist:
            logger.error(f"Subscription does not exists for Stripe Subscription: {stripe_subscription.id}")
            return None
        except MultipleObjectsReturned:
            logger.error(f"Multiple Subscriptions found for Stripe Subscription: {stripe_subscription.id}")
        except Exception as exce:
            logger.error(f"Get Subscription Exception: {exce} for Stripe Subscription {stripe_subscription.id}")
        
        return subscription
    ##########
    # Sync Vendor and Stripe
    ##########
    # TODO: function to deprecate. 
    def initialize_products(self, site):
        """
        Grab all subscription offers on invoice and either create or fetch Stripe products.
        Then using those products, create Stripe prices. Add all that to products_mapping

        Will be used in create_subscription
        """
        # Create Customer loop through all site customer and save their id's
        # Create Stripe product with site offers
        ## Create Stripe Price from Offer.Prices
        ## Create Coupons from Offer.term_details. 
        for subscription in self.invoice.get_recurring_order_items():
            product_name = subscription.offer.name
            product_name_full = f'{product_name} - site {self.site.domain}'
            product_details = subscription.offer.term_details
            total = self.to_valid_decimal(subscription.total - subscription.discounts)
            interval = "month" if product_details['term_units'] == TermDetailUnits.MONTH else "year"
            metadata = {
                'key': 'site',
                'value': 'site4',
                'field': 'metadata'
            }
            product_response = self.does_product_exist(product_name_full, metadata=metadata)
            if product_response:
                product_id = self.get_product_id_with_name(product_name_full)

                # Each product needs an attached price obj. Check if one exists for the product
                # and attach it to the mapping or create one and attach
                if product_id:
                    price_response = self.does_price_exist(product_id)
                    if price_response:
                        price_id = self.get_price_id_with_product(product_id)
                    else:
                        price_id = self.create_price_with_product(product_id)

                    self.products_mapping[product_name] = {
                        'product_id': product_id,
                        'price_id': price_id
                    }

            else:
                # product doesnt exist, create it and related pricing obj
                product = self.stripe_call(self.stripe.Product.create, {
                    'name': product_name,
                    'metadata': product_details,
                    'default_price_data': {
                        'currency': self.invoice.currency,
                        'unit_amount_decimal': total,
                        'recurring': {
                            'interval': interval,
                            'interval_count': subscription.offer.get_period_length()
                        }
                    }
                })
                if product:
                    product_id = product['id']

                    # Each product needs an attached price obj. Check if one exists for the product
                    # and attach it to the mapping or create one and attach
                    price_response = self.does_price_exist(product_id)
                    if price_response:
                        price_id = self.get_price_id_with_product(product_id)
                    else:
                        price_id = self.create_price_with_product(product_id)

                    self.products_mapping[product_name] = {
                        'product_id': product_id,
                        'price_id': price_id
                    }

    def sync_customers(self, site):
        logger.info("StripeProcessor sync_customers Started")
        stripe_customers = self.get_stripe_customers(site)
        stripe_customers_emails = [customer_obj['email'] for customer_obj in stripe_customers]

        vendor_customers_in_stripe = self.get_vendor_customers_in_stripe(stripe_customers_emails, site)

        vendor_customers_with_stripe_meta = vendor_customers_in_stripe.filter(meta__has_key='stripe_id')
        vendor_customers_without_stripe_meta = vendor_customers_in_stripe.exclude(meta__has_key='stripe_id')

        vendor_customers_not_in_stripe = self.get_vendor_customers_not_in_stripe(stripe_customers_emails, site)
        vendor_customer_to_create = vendor_customers_not_in_stripe | vendor_customers_without_stripe_meta

        self.create_stripe_customers(vendor_customer_to_create)
        self.update_stripe_customers(vendor_customers_with_stripe_meta)
        logger.info("StripeProcessor sync_customers Finished")

    def sync_offers(self, site):
        logger.info("StripeProcessor sync_offers Started")
        stripe_products = self.get_site_stripe_products(site)
        offer_pk_list = [product['metadata']['pk'] for product in stripe_products]

        offers_in_vendor = self.get_vendor_offers_in_stripe(offer_pk_list, site)

        offers_in_vendor_with_stripe_meta = offers_in_vendor.filter(meta__has_key='stripe')
        offers_in_vendor_without_stripe_meta = offers_in_vendor.exclude(meta__has_key='stripe')
        
        offers_to_create = self.get_vendor_offers_not_in_stripe(offer_pk_list, site)

        for update_offer_meta in offers_in_vendor_without_stripe_meta:
            for stripe_product in stripe_products:
                if stripe_product['metadata']['pk'] == update_offer_meta.pk:
                    update_offer_meta.meta.update({'stripe': {'product_id': stripe_product.id}})
                    update_offer_meta.save()

        self.create_offers(offers_to_create)
        self.update_offers(offers_in_vendor_with_stripe_meta | offers_in_vendor_without_stripe_meta)
        logger.info("StripeProcessor sync_offers Finished")

    def sync_stripe_subscription(self, site, stripe_subscription):
        stripe_customer = self.stripe_get_object(self.stripe.Customer, stripe_subscription.customer)
        customer_profile = self.get_customer_profile(stripe_customer, site)
        
        if customer_profile:
            subscription, created = self.get_or_create_subscription_from_stripe_subscription(customer_profile, stripe_subscription)

            stripe_product = self.stripe_get_object(self.stripe.Product, stripe_subscription.plan.product)
            offer = self.get_offer_from_stripe_product(stripe_product)
            
            if offer:
                stripe_sub_invoices = self.get_subscription_invoices(stripe_subscription.id)
                
                for stripe_invoice in stripe_sub_invoices:
                    invoice, created = self.get_or_create_invoice_from_stripe_invoice(stripe_invoice, offer, customer_profile)
                        
                    if stripe_invoice.charge:
                        stripe_charge = self.stripe_get_object(self.stripe.Charge, stripe_invoice.charge)
                        stripe_payment_method = self.stripe_get_object(self.stripe.PaymentMethod, stripe_charge.payment_method)
                        
                        payment, created = self.get_or_create_payment_from_stripe_payment_and_charge(invoice, stripe_payment_method, stripe_charge)
                        if created:
                            payment.subscription = subscription
                            payment.save()

                        if payment.status == PurchaseStatus.SETTLED:
                            receipt, created = self.get_or_create_subscription_receipt_from_stripe_charge(invoice, payment, stripe_charge)

    def sync_stripe_subscriptions(self, site):
        logger.info("sync_stripe_subscriptions Started")
        stripe_subscriptions = self.get_stripe_subscriptions(site)

        for stripe_subscription in stripe_subscriptions:
            logger.info(f"sync_stripe_subscriptions: syncing subscription: {stripe_subscription.id}")
            self.sync_stripe_subscription(site, stripe_subscription)

        logger.info("sync_stripe_subscriptions Ended")

    def sync_stripe_vendor_objects(self, site):
        """
        Sync up all the CustomerProfiles, Offers, Prices, and Coupons for all of the sites
        """
        logger.info("StripeProcessor sync_stripe_vendor_objects Started")
        self.sync_customers(site)
        self.sync_offers(site)
        self.sync_stripe_subscriptions(site)
        logger.info("StripeProcessor sync_stripe_vendor_objects Finished")

    ##########
    # Stripe Transactions
    ##########
    def create_setup_intent(self, setup_intent_data):
        setup_intent = self.stripe_create_object(self.stripe.SetupIntent, setup_intent_data)

        return setup_intent

    def create_payment_intent(self, payment_intent_data):
        # Will return client secret value to be returned to the front end to continue processing payment

        #intent = self.stripe_call(self.stripe.PaymentIntent.create, {
        #    'customer': customer['id'],
        #    'setup_future_usage': 'off_session',
        #    'amount': self.invoice.get_one_time_transaction_total(),
        #    'currency': self.invoice.currency,
        #    'automatic_payment_methods': {
        #        'enabled': True
        #    }
        #})

        payment_intent = self.stripe_create_object(self.stripe.PaymentIntent, payment_intent_data)

        return payment_intent

    def set_stripe_payment_source(self):
        """
        This is needed for the charge api due to this error message:
        'You cannot create a charge with a PaymentMethod. Use the Payment Intents API instead'
        """
        if not self.source:
            if self.payment_info.is_valid():
                card_number = self.payment_info.cleaned_data.get('card_number')
                exp_month = self.payment_info.cleaned_data.get('expire_month')
                exp_year = self.payment_info.cleaned_data.get('expire_year')
                cvc = self.payment_info.cleaned_data.get('cvc_number')
                card = {
                    'number': card_number,
                    'exp_month': exp_month,
                    'exp_year': exp_year,
                    'cvc': cvc
                 }
                card = self.stripe_create_object(self.stripe.Token, {'card':card})
                if card:
                    self.source = card['id']

    def create_payment_method(self, payment_method_data):
        payment_method = self.stripe_create_object(self.stripe.PaymentMethod, payment_method_data)

        return payment_method

    def create_charge(self):
        charge_data = {
            'amount': self.to_stripe_valid_unit(self.invoice.get_one_time_transaction_total()),
            'currency': self.invoice.currency,
            'source': self.source,
        }
        charge = self.stripe_create_object(self.stripe.Charge, charge_data)
        if charge:
            return charge
        return None

    ##########
    # Base Processor Transaction Implementations
    ##########
    def pre_authorization(self):
        """
        Called before the authorization begins.
        """
        self.validate_invoice_customer_in_stripe()
        self.validate_invoice_offer_in_stripe()
        self.validate_invoice_subscriptions_in_stripe()
        self.transaction_succeeded = False

    def process_payment(self):
        invoice_data = self.build_invoice()
        stripe_invoice = self.stripe_create_object(self.stripe.Invoice, invoice_data)
        if not stripe_invoice:
            return None

        payment_method_data = self.build_payment_method()
        stripe_payment_method = self.stripe_create_object(self.stripe.PaymentMethod, payment_method_data)
        if not stripe_payment_method:
            return None

        self.stripe_call(stripe_payment_method.attach, {'customer': self.invoice.profile.meta.get('stripe_id')})
        if not self.transaction_succeeded:
            return None
        
        self.invoice.vendor_notes['stripe_id'] = stripe_invoice.id
        self.invoice.save()
        
        stripe_line_items = []
        for order_item in self.invoice.get_one_time_transaction_order_items():
            line_item_data = self.build_invoice_line_item(order_item, stripe_invoice.id)
            stripe_line_item = self.stripe_create_object(self.stripe.InvoiceItem, line_item_data)
            stripe_line_items.append(stripe_line_item)
        
        stripe_invoice.lines = stripe_line_items

        amount = self.convert_decimal_to_integer(self.invoice.get_one_time_transaction_total())
        payment_intent_data = self.build_payment_intent(amount)
        stripe_payment_intent = self.stripe_create_object(self.stripe.PaymentIntent, payment_intent_data)

        if not stripe_payment_intent:
            return None

        self.stripe_call(stripe_invoice.pay, {"payment_method": stripe_payment_method.id})

        if self.transaction_succeeded:
            self.transaction_id = stripe_invoice.payment_intent

    def subscription_payment(self, subscription):
        payment_method_data = self.build_payment_method()
        stripe_payment_method = self.stripe_create_object(self.stripe.PaymentMethod, payment_method_data)
        if not stripe_payment_method:
            return None

        setup_intent_object = self.build_setup_intent(stripe_payment_method.id)
        if not setup_intent_object:
            return None

        stripe_setup_intent = self.stripe_create_object(self.stripe.SetupIntent, setup_intent_object)
        if not stripe_setup_intent:
            return None

        subscription_obj = self.build_subscription(subscription, stripe_payment_method.id)
        stripe_subscription = self.stripe_create_object(self.stripe.Subscription, subscription_obj)
        
        if not stripe_subscription or stripe_subscription.status == 'incomplete':
            self.transaction_succeeded = False
            return None

        if self.invoice.vendor_notes is None:
            self.invoice.vendor_notes = {}

        self.invoice.vendor_notes['stripe_id'] = stripe_subscription.latest_invoice
        self.invoice.save()

        self.subscription_id = stripe_subscription.id

    def charge_customer_profile(self):
        invoice_data = self.build_invoice()
        stripe_invoice = self.stripe_create_object(self.stripe.Invoice, invoice_data)
        if not stripe_invoice:
            return None

        stripe_customer_payment_methods = self.get_customer_payment_methods(self.invoice.profile.meta['stripe_id'])
        if not stripe_customer_payment_methods:
            return None
        
        self.invoice.vendor_notes['stripe_id'] = stripe_invoice.id
        self.invoice.save()
        
        stripe_line_items = []
        for order_item in self.invoice.get_one_time_transaction_order_items():
            line_item_data = self.build_invoice_line_item(order_item, stripe_invoice.id)
            stripe_line_item = self.stripe_create_object(self.stripe.InvoiceItem, line_item_data)
            stripe_line_items.append(stripe_line_item)
        
        stripe_invoice.lines = stripe_line_items

        amount = self.convert_decimal_to_integer(self.invoice.get_one_time_transaction_total())
        payment_intent_data = self.build_payment_intent(amount)
        stripe_payment_intent = self.stripe_create_object(self.stripe.PaymentIntent, payment_intent_data)

        if not stripe_payment_intent:
            return None

        self.stripe_call(stripe_invoice.pay, {"payment_method": stripe_customer_payment_methods[0].id})

        if self.transaction_succeeded:
            self.transaction_id = stripe_invoice.payment_intent

    def subscription_cancel(self, subscription):
        super().subscription_cancel(subscription)
        self.stripe_delete_object(self.stripe.Subscription, subscription.gateway_id)

    def subscription_update_payment(self, subscription):
        """
        Updates the credit card information for the subscription in stripe
        and updates the subscription model in vendor.
        """

        stripe_subscription_object = self.stripe_get_object(self.stripe.Subscription, subscription.gateway_id)
        if not stripe_subscription_object:
            return None

        current_payment_method = self.stripe.Customer.retrieve_payment_method(
            stripe_subscription_object.customer,
            stripe_subscription_object.default_payment_method
        )

        payment_method_data = {
            'type': 'card',
            'card': {
                'number': self.payment_info.cleaned_data.get('card_number'),
                'exp_month': self.payment_info.cleaned_data.get('expire_month'),
                'exp_year': self.payment_info.cleaned_data.get('expire_year'),
                'cvc': self.payment_info.cleaned_data.get('cvv_number'),
            },
            'billing_details': {
                'name': self.payment_info.cleaned_data.get('full_name', None),
                'email': subscription.profile.user.email
            }
        }
        # keep previous address
        if current_payment_method:
            payment_method_data['billing_details']['address'] = current_payment_method.billing_details.get("address")

        # create payment method using new card
        stripe_payment_method = self.stripe_create_object(self.stripe.PaymentMethod, payment_method_data)
        if not stripe_payment_method:
            return None

        setup_intent_object = {
            'customer': subscription.profile.meta['stripe_id'],
            'confirm': True,
            'payment_method_types': ['card'],
            'payment_method': stripe_payment_method.id,
            'metadata': {'site': subscription.profile.site}
        }

        # validate
        stripe_setup_intent = self.stripe_create_object(self.stripe.SetupIntent, setup_intent_object)
        if not stripe_setup_intent:
            return None

        # update subscription
        self.stripe.Subscription.modify(
            subscription.gateway_id,
            default_payment_method=stripe_payment_method
        )

        # save payment info to subscription model
        payment_info = {}
        account_number = payment_method_data.get("card", {}).get("number", "")[-4:]
        account_type = stripe_payment_method.card.get("brand", "")

        if account_number:
            payment_info['account_number'] = account_number

        if account_type:
            payment_info['account_type'] = account_type

        subscription.save_payment_info(payment_info)
