from recurly import recurly_logging as logging
import sys
import re
from datetime import datetime
import six
from six.moves.urllib.parse import urljoin
from six import iteritems
from defusedxml import ElementTree
from xml.etree import ElementTree as ElementTreeBuilder

import recurly
from recurly.errors import *
from recurly.resource import Resource, Money, PageError, Page

"""

Recurly's Python client library is an interface to its REST API.

Please see the Recurly API documentation for more information:

https://dev.recurly.com/docs/getting-started

"""

__version__ = '2.9.10'
__python_version__ = '.'.join(map(str, sys.version_info[:3]))

cached_rate_limits = {
        'limit': None,
        'remaining': None,
        'resets_at': None,
        'cached_at': None,
        }

VALID_DOMAINS = ('.recurly.com',)
"""A tuple of whitelisted domains that this client can connect to."""

USER_AGENT = 'recurly-python/%s; python %s; %s' % (recurly.__version__, recurly.__python_version__, recurly.resource.ssl.OPENSSL_VERSION)

BASE_URI = 'https://%s.recurly.com/v2/'
"""The API endpoint to send requests to."""

SUBDOMAIN = 'api'
"""The subdomain of the site authenticating API requests."""

API_KEY = None
"""The API key to use when authenticating API requests."""

API_VERSION = '2.22'
"""The API version to use when making API requests."""

CA_CERTS_FILE = None
"""A file contianing a set of concatenated certificate authority certs
for validating the server against."""

DEFAULT_CURRENCY = 'USD'
"""The currency to use creating `Money` instances when one is not specified."""

SOCKET_TIMEOUT_SECONDS = None
"""The number of seconds after which to timeout requests to the Recurly API.
If unspecified, the global default timeout is used."""

def urljoin(url1, url2):
    if url1.endswith('/'):
        url1 = url1[0:-1]
    if not url2.startswith('/'):
        url2 = '/' + url2
    return url1 + url2

def base_uri():
    if SUBDOMAIN is None:
        raise ValueError('recurly.SUBDOMAIN not set')

    return BASE_URI % SUBDOMAIN

def api_version():
    return API_VERSION

def cache_rate_limit_headers(resp_headers):
    try:
        recurly.cached_rate_limits = {
                'cached_at': datetime.utcnow(),
                'limit': int(resp_headers['X-RateLimit-Limit']),
                'remaining': int(resp_headers['X-RateLimit-Remaining']),
                'resets_at': datetime.utcfromtimestamp(int(resp_headers['X-RateLimit-Reset']))
                }
    except:
        log = logging.getLogger('recurly.cached_rate_limits')
        log.info('Failed to parse rate limits from header')

class Address(Resource):

    nodename = 'address'

    attributes = (
        'first_name',
        'last_name',
        'name_on_account',
        'company',
        'address1',
        'address2',
        'city',
        'state',
        'zip',
        'country',
        'phone',
    )

class AccountBalance(Resource):

    """The Balance on an account"""

    nodename = 'account_balance'

    attributes = (
        'balance_in_cents',
        'past_due',
    )

class AccountAcquisition(Resource):

    """Account acquisition data https://dev.recurly.com/docs/create-account-acquisition"""

    nodename = 'account_acquisition'

    attributes = (
        'cost_in_cents',
        'currency',
        'channel',
        'subchannel',
        'campaign',
        'created_at',
        'updated_at',
    )

class CustomField(Resource):

    """A field to store extra data on the account or subscription."""

    nodename = 'custom_field'

    attributes = (
        'name',
        'value',
    )

    def to_element(self, root_name=None):
        # Include the field name/value pair when the value changed
        if 'value' in self.__dict__:
            try:
                self.name = self.name # forces name into __dict__
            except AttributeError:
                pass

        return super(CustomField, self).to_element(root_name)

class Account(Resource):

    """A customer account."""

    member_path = 'accounts/%s'
    collection_path = 'accounts'

    nodename = 'account'

    attributes = (
        'account_code',
        'parent_account_code',
        'username',
        'email',
        'first_name',
        'last_name',
        'company_name',
        'vat_number',
        'tax_exempt',
        'exemption_certificate',
        'entity_use_code',
        'accept_language',
        'cc_emails',
        'account_balance',
        'created_at',
        'updated_at',
        'shipping_addresses',
        'account_acquisition',
        'has_live_subscription',
        'has_active_subscription',
        'has_future_subscription',
        'has_canceled_subscription',
        'has_paused_subscription',
        'has_past_due_invoice',
        'preferred_locale',
        'custom_fields',
        'transaction_type',
    )

    _classes_for_nodename = { 'address': Address, 'custom_field': CustomField }

    sensitive_attributes = ('number', 'verification_value',)

    def to_element(self, root_name=None):
        elem = super(Account, self).to_element(root_name)

        # Make sure the account code is always included in a serialization.
        if 'account_code' not in self.__dict__:  # not already included
            try:
                account_code = self.account_code
            except AttributeError:
                pass
            else:
                elem.append(self.element_for_value('account_code', account_code))
        if 'billing_info' in self.__dict__:
            elem.append(self.billing_info.to_element())
        if 'address' in self.__dict__:
            elem.append(self.address.to_element())

        return elem

    @classmethod
    def all_active(cls, **kwargs):
        """Return a `Page` of active customer accounts.

        This is a convenience method for `Account.all(state='active')`.

        """
        return cls.all(state='active', **kwargs)

    @classmethod
    def all_closed(cls, **kwargs):
        """Return a `Page` of closed customer accounts.

        This is a convenience method for `Account.all(state='closed')`.

        """
        return cls.all(state='closed', **kwargs)

    @classmethod
    def all_past_due(cls, **kwargs):
        """Return a `Page` of past-due customer accounts.

        This is a convenience method for `Account.all(state='past_due').

        """
        return cls.all(state='past_due', **kwargs)

    @classmethod
    def all_subscribers(cls, **kwargs):
        """Return a `Page` of customer accounts that are subscribers.

        This is a convenience method for `Account.all(state='subscriber').

        """
        return cls.all(state='subscriber', **kwargs)

    @classmethod
    def all_non_subscribers(cls, **kwargs):
        """Return a `Page` of customer accounts that are not subscribers.

        This is a convenience method for `Account.all(state='non_subscriber').

        """
        return cls.all(state='non_subscriber', **kwargs)

    def __getattr__(self, name):
        if name == 'billing_info':
            try:
                billing_info_url = self._elem.find('billing_info').attrib['href']
            except (AttributeError, KeyError):
                raise AttributeError(name)
            resp, elem = BillingInfo.element_for_url(billing_info_url)
            return BillingInfo.from_element(elem)
        try:
            return super(Account, self).__getattr__(name)
        except AttributeError:
            if name == 'address':
                self.address = Address()
                return self.address
            else:
              raise AttributeError(name)

    def charge(self, charge):
        """Charge (or credit) this account with the given `Adjustment`."""
        url = urljoin(self._url, '/adjustments')
        return charge.post(url)

    def invoice(self, **kwargs):
        """Create an invoice for any outstanding adjustments this account has."""
        url = urljoin(self._url, '/invoices')

        if kwargs:
            response = self.http_request(url, 'POST', Invoice(**kwargs), {'Content-Type':
                'application/xml; charset=utf-8'})
        else:
            response = self.http_request(url, 'POST')

        if response.status != 201:
            self.raise_http_error(response)

        response_xml = response.read()
        logging.getLogger('recurly.http.response').debug(response_xml)
        elem = ElementTree.fromstring(response_xml)

        invoice_collection = InvoiceCollection.from_element(elem)
        return invoice_collection

    def build_invoice(self):
        """Preview an invoice for any outstanding adjustments this account has."""
        url = urljoin(self._url, '/invoices/preview')

        response = self.http_request(url, 'POST')
        if response.status != 200:
            self.raise_http_error(response)

        response_xml = response.read()
        logging.getLogger('recurly.http.response').debug(response_xml)
        elem = ElementTree.fromstring(response_xml)

        invoice_collection = InvoiceCollection.from_element(elem)
        return invoice_collection

    def notes(self):
        """Fetch Notes for this account."""
        url = urljoin(self._url, '/notes')
        return Note.paginated(url)

    def redemption(self):
      try:
        return self.redemptions()[0]
      except AttributeError:
        raise AttributeError("redemption")

    def reopen(self):
        """Reopen a closed account."""
        url = urljoin(self._url, '/reopen')
        response = self.http_request(url, 'PUT')
        if response.status != 200:
            self.raise_http_error(response)

        response_xml = response.read()
        logging.getLogger('recurly.http.response').debug(response_xml)
        self.update_from_element(ElementTree.fromstring(response_xml))

    def subscribe(self, subscription):
        """Create the given `Subscription` for this existing account."""
        url = urljoin(self._url, '/subscriptions')
        return subscription.post(url)

    def update_billing_info(self, billing_info):
        """Change this account's billing information to the given `BillingInfo`."""
        url = urljoin(self._url, '/billing_info')
        response = billing_info.http_request(url, 'PUT', billing_info,
            {'Content-Type': 'application/xml; charset=utf-8'})
        if response.status == 200:
            pass
        elif response.status == 201:
            billing_info._url = response.getheader('Location')
        else:
            billing_info.raise_http_error(response)

        response_xml = response.read()
        logging.getLogger('recurly.http.response').debug(response_xml)
        billing_info.update_from_element(ElementTree.fromstring(response_xml))

    def create_shipping_address(self, shipping_address):
        """Creates a shipping address on an existing account. If you are
        creating an account, you can embed the shipping addresses with the
        request"""
        url = urljoin(self._url, '/shipping_addresses')
        return shipping_address.post(url)

class BillingInfo(Resource):

    """A set of billing information for an account."""

    nodename = 'billing_info'

    attributes = (
        'type',
        'name_on_account',
        'first_name',
        'last_name',
        'number',
        'verification_value',
        'year',
        'month',
        'start_month',
        'start_year',
        'issue_number',
        'company',
        'address1',
        'address2',
        'city',
        'state',
        'zip',
        'country',
        'phone',
        'vat_number',
        'ip_address',
        'ip_address_country',
        'card_type',
        'first_six',
        'last_four',
        'paypal_billing_agreement_id',
        'amazon_billing_agreement_id',
        'amazon_region',
        'token_id',
        'account_type',
        'routing_number',
        'account_number',
        'currency',
        'updated_at',
        'external_hpp_type',
        'gateway_token',
        'gateway_code',
        'three_d_secure_action_result_token_id',
        'transaction_type',
    )
    sensitive_attributes = ('number', 'verification_value', 'account_number')
    xml_attribute_attributes = ('type',)

class ShippingAddress(Resource):

    """Shipping Address information"""

    nodename = 'shipping_address'

    attributes = (
        'address1',
        'address2',
        'city',
        'company',
        'country',
        'email',
        'first_name',
        'id',
        'last_name',
        'nickname',
        'phone',
        'state',
        'zip',
    )

class Delivery(Resource):

    """Delivery information for use with a Gift Card"""

    nodename = 'delivery'

    attributes = (
        'address',
        'deliver_at',
        'email_address',
        'first_name',
        'gifter_name',
        'last_name',
        'method',
        'personal_message',
    )

# This is used internally for proper XML generation
class _RecipientAccount(Account):
    nodename = 'recipient_account'

class GiftCard(Resource):

    """A Gift Card for a customer to purchase or apply to a subscription or account."""

    member_path= 'gift_cards/%s'
    collection_path = 'gift_cards'

    nodename = 'gift_card'

    attributes = (
        'balance_in_cents',
        'canceled_at',
        'created_at',
        'currency',
        'delivery',
        'gifter_account',
        'id',
        'invoice',
        'product_code',
        'recipient_account',
        'redeemed_at',
        'redemption_code',
        'updated_at',
        'unit_amount_in_cents',
        'billing_info',
    )
    _classes_for_nodename = {'recipient_account': Account,'gifter_account':
            Account, 'delivery': Delivery}

    def preview(self):
        """Preview the purchase of this gift card"""

        if hasattr(self, '_url'):
            url = self._url + '/preview'
            return self.post(url)
        else:
            url = urljoin(recurly.base_uri(), self.collection_path + '/preview')
            return self.post(url)

    def redeem(self, account_code):
        """Redeem this gift card on the specified account code"""

        redemption_path = '%s/redeem' % (self.redemption_code)

        if hasattr(self, '_url'):
            url = urljoin(self._url, '/redeem')
        else:
            url = urljoin(recurly.base_uri(), self.collection_path + '/' + redemption_path)

        recipient_account = _RecipientAccount(account_code=account_code)
        return self.post(url, recipient_account)

    def to_element(self, root_name=None):
        elem = super(GiftCard, self).to_element(root_name)

        # Make sure the redemption code is always included in a serialization.
        if 'redemption_code' not in self.__dict__:  # not already included
            try:
                redemption_code = self.redemption_code
            except AttributeError:
                pass
            else:
                elem.append(self.element_for_value('redemption_code',
                    redemption_code))

        return elem

class Coupon(Resource):

    """A coupon for a customer to apply to their account."""

    member_path = 'coupons/%s'
    collection_path = 'coupons'

    nodename = 'coupon'

    attributes = (
        'coupon_code',
        'name',
        'discount_type',
        'discount_percent',
        'discount_in_cents',
        'redeem_by_date',
        'invoice_description',
        'single_use',
        'applies_for_months',
        'duration',
        'temporal_unit',
        'temporal_amount',
        'max_redemptions',
        'applies_to_all_plans',
        'applies_to_non_plan_charges',
        'redemption_resource',
        'created_at',
        'updated_at',
        'deleted_at',
        'plan_codes',
        'hosted_description',
        'max_redemptions_per_account',
        'coupon_type',
        'unique_code_template',
        'unique_coupon_codes',
        'free_trial_unit',
        'free_trial_amount',
        'id',
    )

    @classmethod
    def value_for_element(cls, elem):
        if elem is None or elem.tag != 'plan_codes' or elem.attrib.get('type') != 'array':
            return super(Coupon, cls).value_for_element(elem)

        return [code_elem.text for code_elem in elem]

    @classmethod
    def element_for_value(cls, attrname, value):
        if attrname != 'plan_codes':
            return super(Coupon, cls).element_for_value(attrname, value)

        elem = ElementTreeBuilder.Element(attrname)
        elem.attrib['type'] = 'array'
        for code in value:
            code_el = ElementTreeBuilder.Element('plan_code')
            code_el.text = code
            elem.append(code_el)

        return elem

    @classmethod
    def all_redeemable(cls, **kwargs):
        """Return a `Page` of redeemable coupons.

        This is a convenience method for `Coupon.all(state='redeemable')`.

        """
        return cls.all(state='redeemable', **kwargs)

    @classmethod
    def all_expired(cls, **kwargs):
        """Return a `Page` of expired coupons.

        This is a convenience method for `Coupon.all(state='expired')`.

        """
        return cls.all(state='expired', **kwargs)

    @classmethod
    def all_maxed_out(cls, **kwargs):
        """Return a `Page` of coupons that have been used the maximum
        number of times.

        This is a convenience method for `Coupon.all(state='maxed_out')`.

        """
        return cls.all(state='maxed_out', **kwargs)

    def has_unlimited_redemptions_per_account(self):
        return self.max_redemptions_per_account == None

    def generate(self, amount):
        elem = ElementTreeBuilder.Element(self.nodename)
        elem.append(Resource.element_for_value('number_of_unique_codes', amount))

        url = urljoin(self._url, '/generate')
        body = ElementTree.tostring(elem, encoding='UTF-8')

        response = self.http_request(url, 'POST', body, { 'Content-Type':
            'application/xml; charset=utf-8' })

        if response.status not in (200, 201, 204):
            self.raise_http_error(response)

        return Page.page_for_url(response.getheader('Location'))

    def restore(self):
        url = urljoin(self._url, '/restore')
        self.put(url)

class Redemption(Resource):

    """A particular application of a coupon to a customer account."""

    nodename = 'redemption'

    attributes = (
        'account_code',
        'single_use',
        'total_discounted_in_cents',
        'subscription_uuid',
        'currency',
        'created_at',
        'updated_at',
    )

class TaxDetail(Resource):

    """A charge's tax breakdown"""

    nodename = 'taxdetail'
    inherits_currency = True

    attributes = (
        'name',
        'type',
        'tax_rate',
        'tax_in_cents',
    )

class Adjustment(Resource):

    """A charge or credit applied (or to be applied) to an account's invoice."""

    nodename = 'adjustment'
    member_path = 'adjustments/%s'

    attributes = (
        'uuid',
        'description',
        'accounting_code',
        'product_code',
        'quantity',
        'unit_amount_in_cents',
        'discount_in_cents',
        'tax_in_cents',
        'tax_type',
        'tax_region',
        'tax_rate',
        'total_in_cents',
        'currency',
        'tax_exempt',
        'tax_code',
        'tax_details',
        'start_date',
        'end_date',
        'created_at',
        'updated_at',
        'type',
        'revenue_schedule_type',
        'shipping_address',
        'shipping_address_id',
    )
    xml_attribute_attributes = ('type',)
    _classes_for_nodename = {'tax_detail': TaxDetail, 'shipping_address':
                             ShippingAddress}

    # This can be removed when the `original_adjustment_uuid` is moved to a link
    def __getattr__(self, name):
        if name == 'original_adjustment':
            try:
                uuid = super(Adjustment, self).__getattr__('original_adjustment_uuid')
            except (AttributeError):
                return super(Adjustment, self).__getattr__(name)

            return lambda: Adjustment.get(uuid)
        else:
            return super(Adjustment, self).__getattr__(name)

class Invoice(Resource):

    """A payable charge to an account for the customer's charges and
    subscriptions."""

    member_path = 'invoices/%s'
    collection_path = 'invoices'

    nodename = 'invoice'

    attributes = (
        'uuid',
        'state',
        'invoice_number',
        'invoice_number_prefix',
        'po_number',
        'vat_number',
        'tax_in_cents',
        'tax_type',
        'tax_rate',
        'total_in_cents',
        'currency',
        'created_at',
        'updated_at',
        'line_items',
        'transactions',
        'terms_and_conditions',
        'customer_notes',
        'vat_reverse_charge_notes', # Only shows if reverse charge invoice
        'address',
        'closed_at',
        'collection_method',
        'net_terms',
        'attempt_next_collection_at',
        'recovery_reason',
        'balance_in_cents',
        'subtotal_before_discount_in_cents',
        'subtotal_in_cents',
        'discount_in_cents',
        'due_on',
        'type',
        'origin',
        'credit_customer_notes',
        'gateway_code',
        'billing_info',
    )

    blacklist_attributes = (
        'currency',
    )

    def invoice_number_with_prefix(self):
        return '%s%s' % (self.invoice_number_prefix, self.invoice_number)

    @classmethod
    def all_pending(cls, **kwargs):
        """Return a `Page` of pending invoices.

        This is a convenience method for `Invoice.all(state='pending')`.

        """
        return cls.all(state='pending', **kwargs)

    @classmethod
    def all_paid(cls, **kwargs):
        """Return a `Page` of paid invoices.

        This is a convenience method for `Invoice.all(state='paid')`.

        """
        return cls.all(state='paid', **kwargs)

    @classmethod
    def all_failed(cls, **kwargs):
        """Return a `Page` of failed invoices.

        This is a convenience method for `Invoice.all(state='failed')`.

        """
        return cls.all(state='failed', **kwargs)

    @classmethod
    def all_past_due(cls, **kwargs):
        """Return a `Page` of past-due invoices.

        This is a convenience method for `Invoice.all(state='past_due')`.

        """
        return cls.all(state='past_due', **kwargs)

    @classmethod
    def pdf(cls, uuid):
        """Return a PDF of the invoice identified by the UUID

        This is a raw string, which can be written to a file with:
        `
            with open('invoice.pdf', 'w') as invoice_file:
                invoice_file.write(recurly.Invoice.pdf(uuid))
        `

        """
        url = urljoin(base_uri(), cls.member_path % (uuid,))
        pdf_response = cls.http_request(url, headers={'Accept': 'application/pdf'})
        return pdf_response.read()

    def refund_amount(self, amount_in_cents, refund_options = {}):
        # For backwards compatibility
        # TODO the consequent branch of this conditional should eventually be removed
        # and we should document that as a breaking change in the changelog.
        # The same change should be applied to the refund() method
        if (isinstance(refund_options, six.string_types)):
            refund_options = { 'refund_method': refund_options }
        else:
            if 'refund_method' not in refund_options:
                refund_options = { 'refund_method': 'credit_first' }

        amount_element = self._refund_open_amount_xml(amount_in_cents,
                                                     refund_options)
        return self._create_refund_invoice(amount_element)

    def refund(self, adjustments, refund_options = {}):
       # For backwards compatibility
       # TODO the consequent branch of this conditional should eventually be removed
       # and we should document that as a breaking change in the changelog.
       # The same change should be applied to the refund_amount() method
       if (isinstance(refund_options, six.string_types)):
           refund_options = { 'refund_method': refund_options }
       else:
           if 'refund_method' not in refund_options:
               refund_options = { 'refund_method': 'credit_first' }

       adjustments_element = self._refund_line_items_xml(adjustments,
                                                          refund_options)
       return self._create_refund_invoice(adjustments_element)

    def _refund_open_amount_xml(self, amount_in_cents, refund_options):
        elem = ElementTreeBuilder.Element(self.nodename)
        elem.append(Resource.element_for_value('amount_in_cents',
            amount_in_cents))

        # Need to sort the keys for tests to pass in python 2 and 3
        # Can remove `sorted` when we drop python 2 support
        for k, v in sorted(iteritems(refund_options)):
            elem.append(Resource.element_for_value(k, v))
        return elem

    def _refund_line_items_xml(self, line_items, refund_options):
        elem = ElementTreeBuilder.Element(self.nodename)

        line_items_elem = ElementTreeBuilder.Element('line_items')

        for item in line_items:
            adj_elem = ElementTreeBuilder.Element('adjustment')
            adj_elem.append(Resource.element_for_value('uuid',
                item['adjustment'].uuid))
            adj_elem.append(Resource.element_for_value('quantity',
            item['quantity']))
            adj_elem.append(Resource.element_for_value('prorate', item['prorate']))
            line_items_elem.append(adj_elem)

        elem.append(line_items_elem)

        # Need to sort the keys for tests to pass in python 2 and 3
        # Can remove `sorted` when we drop python 2 support
        for k, v in sorted(iteritems(refund_options)):
            elem.append(Resource.element_for_value(k, v))

        return elem

    def mark_failed(self):
        url = urljoin(self._url, '/mark_failed')

        collection = InvoiceCollection()
        response = self.http_request(url, 'PUT')
        if response.status != 200:
            self.raise_http_error(response)
        response_xml = response.read()
        logging.getLogger('recurly.http.response').debug(response_xml)
        collection.update_from_element(ElementTree.fromstring(response_xml))

        return collection

    def _create_refund_invoice(self, element):
        url = urljoin(self._url, '/refund')
        body = ElementTree.tostring(element, encoding='UTF-8')

        refund_invoice = Invoice()
        refund_invoice.post(url, body)

        return refund_invoice

    def redemption(self):
      try:
        return self.redemptions()[0]
      except AttributeError:
        raise AttributeError("redemption")

    def enter_offline_payment(self, transaction):
        """
        Records an offline (external) payment on the invoice.
        Pass in a Transaction object to set the details of the created
        transaction. The attributes available to set are
        (payment_method, collected_at, amount_in_cents, description)

        Returns:
            Transaction: The created transaction
        """
        url = urljoin(self._url, '/transactions')
        transaction.post(url)
        return transaction

    def save(self):
        if hasattr(self, '_url'):
            super(Invoice, self).save()
        else:
            raise BadRequestError("New invoices cannot be created using Invoice#save")

class InvoiceCollection(Resource):

    """A collection of invoices resulting from some action. Includes
    a charge invoice and a list of credit invoices.
    """

    nodename = 'invoice_collection'
    attributes = (
        'charge_invoice',
        'credit_invoices',
    )
    _classes_for_nodename = {
        'charge_invoice': Invoice,
        'credit_invoice': Invoice,
    }

class Purchase(Resource):

    """
    A purchase allows the programmer to invoice multiple subscriptions,
    adjustments, coupon codes, and gift cards all in one call.
    """

    collection_path = 'purchases'
    nodename = 'purchase'

    attributes = (
        'account',
        'adjustments',
        'currency',
        'po_number',
        'net_terms',
        'gift_card',
        'coupon_codes',
        'subscriptions',
        'customer_notes',
        'terms_and_conditions',
        'vat_reverse_charge_notes',
        'shipping_address_id',
        'shipping_fees',
        'gateway_code',
        'collection_method',
        'transaction_type',
    )

    def invoice(self):
        """
        Will invoice the purchase object and run all validations and transactions.

        Returns:
            InvoiceCollection: The generated collection of invoices
        """
        return self.__invoice(self.collection_path)

    def preview(self):
        """
        Will create a preview invoice for the purchase. It runs all the
        validations but not the transactions.

        Returns:
            InvoiceCollection: The preview of collection of invoices
        """
        return self.__invoice(self.collection_path + '/preview')

    def authorize(self):
        """
        Will generate an authorized invoice for the purchase. Runs validations
        but does not run any transactions. This endpoint will create a
        pending purchase that can be activated at a later time once payment
        has been completed on an external source (e.g. Adyen's Hosted
        Payment Pages).

        Returns:
            InvoiceCollection: The authorized collection of invoices
        """
        return self.__invoice(self.collection_path + '/authorize')

    def pending(self):
        """
        Use for Adyen HPP transaction requests. Runs validations
        but does not run any transactions.

        Returns:
            InvoiceCollection: The pending collection of invoices
        """
        return self.__invoice(self.collection_path + '/pending')

    def __invoice(self, url):
        # We must null out currency in subscriptions and adjustments
        # TODO we should deprecate and remove default currency support
        def filter_currency(resources):
            for resource in resources:
                resource.attributes = tuple([a for a in resource.attributes if
                                             a != 'currency'])
        try:
            filter_currency(self.adjustments)
        except AttributeError:
            pass
        try:
            filter_currency(self.subscriptions)
        except AttributeError:
            pass

        url = urljoin(recurly.base_uri(), url)
        response = self.http_request(url, 'POST', self)
        if response.status not in (200, 201):
            self.raise_http_error(response)
        response_xml = response.read()
        logging.getLogger('recurly.http.response').debug(response_xml)
        elem = ElementTree.fromstring(response_xml)
        invoice_collection = InvoiceCollection.from_element(elem)
        return invoice_collection

class ShippingFee(Resource):

    """A one time shipping fee on a Purchase"""

    nodename = 'shipping_fee'

    attributes = (
        'shipping_method_code',
        'shipping_amount_in_cents',
        'shipping_address',
        'shipping_address_id',
    )

class ShippingMethod(Resource):

    """A shipping method available on the site"""

    member_path = 'shipping_methods/%s'
    collection_path = 'shipping_methods'

    nodename = 'shipping_method'

    attributes = (
      'code',
      'name',
      'accounting_code',
      'tax_code',
      'created_at',
      'updated_at',
    )

class Subscription(Resource):

    """A customer account's subscription to your service."""

    member_path = 'subscriptions/%s'
    collection_path = 'subscriptions'

    nodename = 'subscription'

    attributes = (
        'uuid',
        'state',
        'plan_code',
        'coupon_code',
        'coupon_codes',
        'quantity',
        'activated_at',
        'updated_at',
        'canceled_at',
        'starts_at',
        'expires_at',
        'current_period_started_at',
        'current_period_ends_at',
        'trial_started_at',
        'trial_ends_at',
        'unit_amount_in_cents',
        'tax_in_cents',
        'tax_type',
        'tax_rate',
        'total_billing_cycles',
        'remaining_billing_cycles',
        'timeframe',
        'currency',
        'subscription_add_ons',
        'account',
        'pending_subscription',
        'net_terms',
        'collection_method',
        'po_number',
        'first_renewal_date',
        'bulk',
        'terms_and_conditions',
        'customer_notes',
        'vat_reverse_charge_notes',
        'bank_account_authorized_at',
        'redemptions',
        'revenue_schedule_type',
        'gift_card',
        'shipping_address',
        'shipping_address_id',
        'shipping_method_code',
        'shipping_amount_in_cents',
        'started_with_gift',
        'converted_at',
        'no_billing_info_reason',
        'imported_trial',
        'remaining_pause_cycles',
        'paused_at',
        'auto_renew',
        'renewal_billing_cycles',
        'first_billing_date',
        'first_bill_date',
        'next_bill_date',
        'current_term_started_at',
        'current_term_ends_at',
        'custom_fields',
        'gateway_code',
        'transaction_type',
        'billing_info',
    )
    sensitive_attributes = ('number', 'verification_value', 'bulk')

    def preview(self):
        if hasattr(self, '_url'):
            url = self._url + '/preview'
            return self.post(url)
        else:
            url = urljoin(recurly.base_uri(), self.collection_path + '/preview')
            return self.post(url)

    def update_notes(self, **kwargs):
        """
        Updates the notes on the subscription without generating a change
        This endpoint also allows you to update custom fields:

            `
                sub.custom_fields[0].value = 'A new value'
                sub.update_notes()
            `
        """
        for key, val in iteritems(kwargs):
            setattr(self, key, val)
        url = urljoin(self._url, '/notes')
        self.put(url)

    def pause(self, remaining_pause_cycles):
        """Pause a subscription"""
        url = urljoin(self._url, '/pause')
        elem = ElementTreeBuilder.Element(self.nodename)
        elem.append(Resource.element_for_value('remaining_pause_cycles',
                                               remaining_pause_cycles))
        body = ElementTree.tostring(elem, encoding='UTF-8')

        response = self.http_request(url, 'PUT', body, { 'Content-Type':
            'application/xml; charset=utf-8' })

        if response.status not in (200, 201, 204):
            self.raise_http_error(response)

        self.update_from_element(ElementTree.fromstring(response.read()))

    def resume(self):
        """Resume a subscription"""
        url = urljoin(self._url, '/resume')
        self.put(url)

    def _update(self):
        if not hasattr(self, 'timeframe'):
            self.timeframe = 'now'
        return super(Subscription, self)._update()

    def __getpath__(self, name):
        if name == 'plan_code':
            return 'plan/plan_code'
        else:
            return name

    def create_usage(self, sub_add_on, usage):
        """Record the usage on the given subscription add on and update the
        usage object with returned xml"""
        url = urljoin(self._url, '/add_ons/%s/usage' % (sub_add_on.add_on_code,))
        return usage.post(url)

class TransactionBillingInfo(recurly.Resource):
    node_name = 'billing_info'
    attributes = (
        'first_name',
        'last_name',
        'address1',
        'address2',
        'city',
        'state',
        'country',
        'zip',
        'phone',
        'vat_number',
        'first_six',
        'last_four',
        'card_type',
        'month',
        'year',
        'transaction_uuid',
    )


class TransactionAccount(recurly.Resource):
    node_name = 'account'
    attributes = (
        'first_name',
        'last_name',
        'company',
        'email',
        'account_code',
    )
    _classes_for_nodename = {'billing_info': TransactionBillingInfo}

class TransactionDetails(recurly.Resource):
    node_name = 'details'
    attributes = ('account')
    _classes_for_nodename = {'account': TransactionAccount}


class TransactionError(recurly.Resource):
    node_name = 'transaction_error'
    attributes = (
        'id',
        'merchant_message',
        'error_caterogy',
        'customer_message',
        'error_code',
        'gateway_error_code',
    )

class TransactionFraudInfo(recurly.Resource):
    node_name = 'fraud'
    attributes = (
        'score',
        'decision',
    )

class Transaction(Resource):

    """An immediate one-time charge made to a customer's account."""

    member_path = 'transactions/%s'
    collection_path = 'transactions'

    nodename = 'transaction'

    attributes = (
        'uuid',
        'action',
        'account',
        'currency',
        'amount_in_cents',
        'tax_in_cents',
        'status',
        'reference',
        'test',
        'voidable',
        'description',
        'refundable',
        'cvv_result',
        'avs_result',
        'avs_result_street',
        'avs_result_postal',
        'created_at',
        'updated_at',
        'details',
        'transaction_error',
        'type',
        'ip_address',
        'tax_exempt',
        'tax_code',
        'accounting_code',
        'fraud',
        'original_transaction',
        'gateway_type',
        'origin',
        'message',
        'approval_code',
    )
    xml_attribute_attributes = ('type',)
    sensitive_attributes = ('number', 'verification_value',)
    _classes_for_nodename = {
        'details': TransactionDetails,
        'fraud': TransactionFraudInfo,
        'transaction_error': TransactionError
    }

    def _handle_refund_accepted(self, response):
        if response.status != 202:
            self.raise_http_error(response)

        self._refund_transaction_url = response.getheader('Location')
        return self

    def get_refund_transaction(self):
        """Retrieve the refund transaction for this transaction, immediately
        after refunding.

        After calling `refund()` to refund a transaction, call this method to
        retrieve the new transaction representing the refund.

        """
        try:
            url = self._refund_transaction_url
        except AttributeError:
            raise ValueError("No refund transaction is available for this transaction")

        resp, elem = self.element_for_url(url)
        value = self.value_for_element(elem)
        return value

    def refund(self, **kwargs):
        """Refund this transaction.

        Calling this method returns the refunded transaction (that is,
        ``self``) if the refund was successful, or raises a `ResponseError` if
        an error occurred requesting the refund. After a successful call to
        `refund()`, to retrieve the new transaction representing the refund,
        use the `get_refund_transaction()` method.

        """
        # Find the URL and method to refund the transaction.
        try:
            selfnode = self._elem
        except AttributeError:
            raise AttributeError('refund')
        url, method = None, None
        for anchor_elem in selfnode.findall('a'):
            if anchor_elem.attrib.get('name') == 'refund':
                url = anchor_elem.attrib['href']
                method = anchor_elem.attrib['method'].upper()
        if url is None or method is None:
            raise AttributeError("refund")  # should do something more specific probably

        actionator = self._make_actionator(url, method, extra_handler=self._handle_refund_accepted)
        return actionator(**kwargs)


Transaction._classes_for_nodename['transaction'] = Transaction


class Plan(Resource):

    """A service level for your service to which a customer account
    can subscribe."""

    member_path = 'plans/%s'
    collection_path = 'plans'

    nodename = 'plan'

    attributes = (
        'plan_code',
        'name',
        'description',
        'success_url',
        'cancel_url',
        'display_donation_amounts',
        'display_quantity',
        'display_phone_number',
        'bypass_hosted_confirmation',
        'unit_name',
        'payment_page_tos_link',
        'plan_interval_length',
        'plan_interval_unit',
        'trial_interval_length',
        'trial_interval_unit',
        'accounting_code',
        'setup_fee_accounting_code',
        'created_at',
        'updated_at',
        'tax_exempt',
        'tax_code',
        'unit_amount_in_cents',
        'setup_fee_in_cents',
        'total_billing_cycles',
        'revenue_schedule_type',
        'setup_fee_revenue_schedule_type',
        'trial_requires_billing_info',
        'auto_renew',
    )

    def get_add_on(self, add_on_code):
        """Return the `AddOn` for this plan with the given add-on code."""
        url = urljoin(self._url, '/add_ons/%s' % (add_on_code,))
        resp, elem = AddOn.element_for_url(url)
        return AddOn.from_element(elem)

    def create_add_on(self, add_on):
        """Make the given `AddOn` available to subscribers on this plan."""
        url = urljoin(self._url, '/add_ons')
        return add_on.post(url)

class Usage(Resource):

    """A recording of usage agains a measured unit"""

    nodename = 'usage'
    collection_path = 'usages'

    attributes = (
        'measured_unit',
        'amount',
        'merchant_tag',
        'recording_timestamp',
        'usage_timestamp',
        'usage_type',
        'unit_amount_in_cents',
        'usage_percentage',
        'billed_at',
        'created_at',
        'updated_at',
    )

class MeasuredUnit(Resource):

    """A unit of measurement for usage based billing"""

    nodename = 'measured_unit'
    member_path = 'measured_units/%s'
    collection_path = 'measured_units'

    attributes = (
        'id',
        'name',
        'display_name',
        'description',
        'created_at',
        'updated_at',
    )

class AddOn(Resource):

    """An additional benefit a customer subscribed to a particular plan
    can also subscribe to."""

    nodename = 'add_on'

    attributes = (
        'add_on_code',
        'name',
        'display_quantity_on_hosted_page',
        'display_quantity',
        'default_quantity',
        'accounting_code',
        'unit_amount_in_cents',
        'measured_unit_id',
        'usage_type',
        'usage_percentage',
        'add_on_type',
        'tax_code',
        'revenue_schedule_type',
        'optional',
        'created_at',
        'updated_at',
    )

class SubscriptionAddOn(Resource):

    """A plan add-on as added to a customer's subscription.

    Use these instead of `AddOn` instances when specifying a
    `Subscription` instance's `subscription_add_ons` attribute.

    """

    nodename = 'subscription_add_on'
    inherits_currency = True

    attributes = (
        'add_on_code',
        'quantity',
        'unit_amount_in_cents',
        'address',
    )

class Note(Resource):

    """A customer account's notes."""

    nodename = 'note'
    collection_path = 'notes'

    attributes = (
        'message',
        'created_at',
    )

    @classmethod
    def from_element(cls, elem):
        new_note = Note()
        for child_el in elem:
            if not child_el.tag:
                continue
            setattr(new_note, child_el.tag, child_el.text)
        return new_note

class CreditPayment(Resource):

    """A payment from credit"""

    nodename = 'credit_payment'
    collection_path = 'credit_payments'
    member_path = 'credit_payments/%s'

    attributes = (
        'uuid',
        'unit_amount_in_cents',
        'currency',
        'action',
        'created_at',
        'updated_at',
        'voided_at',
    )


class ExportDate(Resource):
    nodename = 'export_date'
    collection_path = 'export_dates'

    attributes = (
        'date',
        'export_files'
    )

    def files(self, date):
        """
        Fetch files for a given date.
        :param date: The date to fetch the export files for
        :return: A list of exported files for that given date or an empty list if not file exists for that date
        """
        url = urljoin(recurly.base_uri() + self.collection_path, '/%s/export_files' % date)
        return ExportDateFile.paginated(url)


class ExportDateFile(Resource):
    nodename = 'export_file'
    collection_path = 'export_files'

    attributes = (
        'name',
        'md5sum',
        'expires_at',
        'download_url'
    )

    def download_information(self):
        """
        Download an export file
        :return: The download information of the file that includes the download URL as well as the expiry time of the
            download URL
        """
        _response, element = ExportDateFile.element_for_url(self._url)
        return ExportDateFile.from_element(element)


Resource._learn_nodenames(locals().values())


def objects_for_push_notification(notification):
    """Decode a push notification with the given body XML.

    Returns a dictionary containing the constituent objects of the push
    notification. The kind of push notification is given in the ``"type"``
    member of the returned dictionary.

    """
    notification_el = ElementTree.fromstring(notification)
    objects = {'type': notification_el.tag}
    for child_el in notification_el:
        tag = child_el.tag
        res = Resource.value_for_element(child_el)
        objects[tag] = res
    return objects
