""" Billing information, subscriptions, and plan information. """

import stripe

from flask import request
from app import billing
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action,
                           related_user_resource, internal_only, require_user_admin, show_if, 
                           path_param, require_scope, abort)
from endpoints.exception import Unauthorized, NotFound
from endpoints.api.subscribe import subscribe, subscription_view
from auth.permissions import AdministerOrganizationPermission
from auth.auth_context import get_authenticated_user
from auth import scopes
from data import model
from data.billing import PLANS, get_plan

import features
import uuid
import json

def get_namespace_plan(namespace):
  """ Returns the plan of the given namespace. """
  namespace_user = model.user.get_namespace_user(namespace)
  if namespace_user is None:
    return None

  if not namespace_user.stripe_id:
    return None

  # Ask Stripe for the subscribed plan.
  # TODO: Can we cache this or make it faster somehow?
  try:
    cus = billing.Customer.retrieve(namespace_user.stripe_id)
  except stripe.APIConnectionError:
    abort(503, message='Cannot contact Stripe')

  if not cus.subscription:
    return None

  return get_plan(cus.subscription.plan.id)


def lookup_allowed_private_repos(namespace):
  """ Returns false if the given namespace has used its allotment of private repositories. """
  current_plan = get_namespace_plan(namespace)
  if current_plan is None:
    return False

  # Find the number of private repositories used by the namespace and compare it to the
  # plan subscribed.
  private_repos = model.user.get_private_repo_count(namespace)

  return private_repos < current_plan['privateRepos']


def carderror_response(e):
  return {'carderror': e.message}, 402


def get_card(user):
  card_info = {
    'is_valid': False
  }

  if user.stripe_id:
    try:
      cus = billing.Customer.retrieve(user.stripe_id)
    except stripe.APIConnectionError as e:
      abort(503, message='Cannot contact Stripe')

    if cus and cus.default_card:
      # Find the default card.
      default_card = None
      for card in cus.cards.data:
        if card.id == cus.default_card:
          default_card = card
          break

      if default_card:
        card_info = {
          'owner': default_card.name,
          'type': default_card.type,
          'last4': default_card.last4,
          'exp_month': default_card.exp_month,
          'exp_year': default_card.exp_year
        }

  return {'card': card_info}


def set_card(user, token):
  if user.stripe_id:
    try:
      cus = billing.Customer.retrieve(user.stripe_id)
    except stripe.APIConnectionError as e:
      abort(503, message='Cannot contact Stripe')

    if cus:
      try:
        cus.card = token
        cus.save()
      except stripe.CardError as exc:
        return carderror_response(exc)
      except stripe.InvalidRequestError as exc:
        return carderror_response(exc)
      except stripe.APIConnectionError as e:
        return carderror_response(e)

  return get_card(user)


def get_invoices(customer_id):
  def invoice_view(i):
    return {
      'id': i.id,
      'date': i.date,
      'period_start': i.period_start,
      'period_end': i.period_end,
      'paid': i.paid,
      'amount_due': i.amount_due,
      'next_payment_attempt': i.next_payment_attempt,
      'attempted': i.attempted,
      'closed': i.closed,
      'total': i.total,
      'plan': i.lines.data[0].plan.id if i.lines.data[0].plan else None
    }

  try:
    invoices = billing.Invoice.all(customer=customer_id, count=12)
  except stripe.APIConnectionError as e:
    abort(503, message='Cannot contact Stripe')

  return {
    'invoices': [invoice_view(i) for i in invoices.data]
  }


def get_invoice_fields(user):
  try:
    cus = billing.Customer.retrieve(user.stripe_id)
  except stripe.APIConnectionError:
    abort(503, message='Cannot contact Stripe')

  if not 'metadata' in cus:
    cus.metadata = {}

  return json.loads(cus.metadata.get('invoice_fields') or '[]'), cus


def create_billing_invoice_field(user, title, value):
  new_field = {
    'uuid': str(uuid.uuid4()).split('-')[0],
    'title': title,
    'value': value
  }

  invoice_fields, cus = get_invoice_fields(user)
  invoice_fields.append(new_field)

  if not 'metadata' in cus:
    cus.metadata = {}

  cus.metadata['invoice_fields'] = json.dumps(invoice_fields)
  cus.save()
  return new_field


def delete_billing_invoice_field(user, field_uuid):
  invoice_fields, cus = get_invoice_fields(user)
  invoice_fields = [field for field in invoice_fields if not field['uuid'] == field_uuid]

  if not 'metadata' in cus:
    cus.metadata = {}

  cus.metadata['invoice_fields'] = json.dumps(invoice_fields)
  cus.save()
  return True


@resource('/v1/plans/')
@show_if(features.BILLING)
class ListPlans(ApiResource):
  """ Resource for listing the available plans. """
  @nickname('listPlans')
  def get(self):
    """ List the avaialble plans. """
    return {
      'plans': PLANS,
    }


@resource('/v1/user/card')
@internal_only
@show_if(features.BILLING)
class UserCard(ApiResource):
  """ Resource for managing a user's credit card. """
  schemas = {
    'UserCard': {
      'id': 'UserCard',
      'type': 'object',
      'description': 'Description of a user card',
      'required': [
        'token',
      ],
      'properties': {
        'token': {
          'type': 'string',
          'description': 'Stripe token that is generated by stripe checkout.js',
        },
      },
    },
  }

  @require_user_admin
  @nickname('getUserCard')
  def get(self):
    """ Get the user's credit card. """
    user = get_authenticated_user()
    return get_card(user)

  @require_user_admin
  @nickname('setUserCard')
  @validate_json_request('UserCard')
  def post(self):
    """ Update the user's credit card. """
    user = get_authenticated_user()
    token = request.get_json()['token']
    response = set_card(user, token)
    log_action('account_change_cc', user.username)
    return response


@resource('/v1/organization/<orgname>/card')
@path_param('orgname', 'The name of the organization')
@internal_only
@related_user_resource(UserCard)
@show_if(features.BILLING)
class OrganizationCard(ApiResource):
  """ Resource for managing an organization's credit card. """
  schemas = {
    'OrgCard': {
      'id': 'OrgCard',
      'type': 'object',
      'description': 'Description of a user card',
      'required': [
        'token',
      ],
      'properties': {
        'token': {
          'type': 'string',
          'description': 'Stripe token that is generated by stripe checkout.js',
        },
      },
    },
  }

  @require_scope(scopes.ORG_ADMIN)
  @nickname('getOrgCard')
  def get(self, orgname):
    """ Get the organization's credit card. """
    permission = AdministerOrganizationPermission(orgname)
    if permission.can():
      organization = model.organization.get_organization(orgname)
      return get_card(organization)

    raise Unauthorized()

  @nickname('setOrgCard')
  @validate_json_request('OrgCard')
  def post(self, orgname):
    """ Update the orgnaization's credit card. """
    permission = AdministerOrganizationPermission(orgname)
    if permission.can():
      organization = model.organization.get_organization(orgname)
      token = request.get_json()['token']
      response = set_card(organization, token)
      log_action('account_change_cc', orgname)
      return response

    raise Unauthorized()


@resource('/v1/user/plan')
@internal_only
@show_if(features.BILLING)
class UserPlan(ApiResource):
  """ Resource for managing a user's subscription. """
  schemas = {
    'UserSubscription': {
      'id': 'UserSubscription',
      'type': 'object',
      'description': 'Description of a user card',
      'required': [
        'plan',
      ],
      'properties': {
        'token': {
          'type': 'string',
          'description': 'Stripe token that is generated by stripe checkout.js',
        },
        'plan': {
          'type': 'string',
          'description': 'Plan name to which the user wants to subscribe',
        },
      },
    },
  }

  @require_user_admin
  @nickname('updateUserSubscription')
  @validate_json_request('UserSubscription')
  def put(self):
    """ Create or update the user's subscription. """
    request_data = request.get_json()
    plan = request_data['plan']
    token = request_data['token'] if 'token' in request_data else None
    user = get_authenticated_user()
    return subscribe(user, plan, token, False)  # Business features not required

  @require_user_admin
  @nickname('getUserSubscription')
  def get(self):
    """ Fetch any existing subscription for the user. """
    cus = None
    user = get_authenticated_user()
    private_repos = model.user.get_private_repo_count(user.username)

    if user.stripe_id:
      try:
        cus = billing.Customer.retrieve(user.stripe_id)
      except stripe.APIConnectionError as e:
        abort(503, message='Cannot contact Stripe')

      if cus.subscription:
        return subscription_view(cus.subscription, private_repos)

    return {
      'hasSubscription': False,
      'isExistingCustomer': cus is not None,
      'plan': 'free',
      'usedPrivateRepos': private_repos,
    }


@resource('/v1/organization/<orgname>/plan')
@path_param('orgname', 'The name of the organization')
@internal_only
@related_user_resource(UserPlan)
@show_if(features.BILLING)
class OrganizationPlan(ApiResource):
  """ Resource for managing a org's subscription. """
  schemas = {
    'OrgSubscription': {
      'id': 'OrgSubscription',
      'type': 'object',
      'description': 'Description of a user card',
      'required': [
        'plan',
      ],
      'properties': {
        'token': {
          'type': 'string',
          'description': 'Stripe token that is generated by stripe checkout.js',
        },
        'plan': {
          'type': 'string',
          'description': 'Plan name to which the user wants to subscribe',
        },
      },
    },
  }

  @require_scope(scopes.ORG_ADMIN)
  @nickname('updateOrgSubscription')
  @validate_json_request('OrgSubscription')
  def put(self, orgname):
    """ Create or update the org's subscription. """
    permission = AdministerOrganizationPermission(orgname)
    if permission.can():
      request_data = request.get_json()
      plan = request_data['plan']
      token = request_data['token'] if 'token' in request_data else None
      organization = model.organization.get_organization(orgname)
      return subscribe(organization, plan, token, True)  # Business plan required

    raise Unauthorized()

  @require_scope(scopes.ORG_ADMIN)
  @nickname('getOrgSubscription')
  def get(self, orgname):
    """ Fetch any existing subscription for the org. """
    cus = None
    permission = AdministerOrganizationPermission(orgname)
    if permission.can():
      private_repos = model.user.get_private_repo_count(orgname)
      organization = model.organization.get_organization(orgname)
      if organization.stripe_id:
        try:
          cus = billing.Customer.retrieve(organization.stripe_id)
        except stripe.APIConnectionError as e:
          abort(503, message='Cannot contact Stripe')

        if cus.subscription:
          return subscription_view(cus.subscription, private_repos)

      return {
        'hasSubscription': False,
        'isExistingCustomer': cus is not None,
        'plan': 'free',
        'usedPrivateRepos': private_repos,
      }

    raise Unauthorized()


@resource('/v1/user/invoices')
@internal_only
@show_if(features.BILLING)
class UserInvoiceList(ApiResource):
  """ Resource for listing a user's invoices. """
  @require_user_admin
  @nickname('listUserInvoices')
  def get(self):
    """ List the invoices for the current user. """
    user = get_authenticated_user()
    if not user.stripe_id:
      raise NotFound()

    return get_invoices(user.stripe_id)


@resource('/v1/organization/<orgname>/invoices')
@path_param('orgname', 'The name of the organization')
@related_user_resource(UserInvoiceList)
@show_if(features.BILLING)
class OrganizationInvoiceList(ApiResource):
  """ Resource for listing an orgnaization's invoices. """
  @require_scope(scopes.ORG_ADMIN)
  @nickname('listOrgInvoices')
  def get(self, orgname):
    """ List the invoices for the specified orgnaization. """
    permission = AdministerOrganizationPermission(orgname)
    if permission.can():
      organization = model.organization.get_organization(orgname)
      if not organization.stripe_id:
        raise NotFound()

      return get_invoices(organization.stripe_id)

    raise Unauthorized()


@resource('/v1/user/invoice/fields')
@internal_only
@show_if(features.BILLING)
class UserInvoiceFieldList(ApiResource):
  """ Resource for listing and creating a user's custom invoice fields. """
  schemas = {
    'InvoiceField': {
      'id': 'InvoiceField',
      'type': 'object',
      'description': 'Description of an invoice field',
      'required': [
        'title', 'value'
      ],
      'properties': {
        'title': {
          'type': 'string',
          'description': 'The title of the field being added',
        },
        'value': {
          'type': 'string',
          'description': 'The value of the field being added',
        },
      },
    },
  }

  @require_user_admin
  @nickname('listUserInvoiceFields')
  def get(self):
    """ List the invoice fields for the current user. """
    user = get_authenticated_user()
    if not user.stripe_id:
      raise NotFound()

    return {'fields': get_invoice_fields(user)[0]}

  @require_user_admin
  @nickname('createUserInvoiceField')
  @validate_json_request('InvoiceField')
  def post(self):
    """ Creates a new invoice field. """
    user = get_authenticated_user()
    if not user.stripe_id:
      raise NotFound()

    data = request.get_json()
    created_field = create_billing_invoice_field(user, data['title'], data['value'])
    return created_field


@resource('/v1/user/invoice/field/<field_uuid>')
@internal_only
@show_if(features.BILLING)
class UserInvoiceField(ApiResource):
  """ Resource for deleting a user's custom invoice fields. """
  @require_user_admin
  @nickname('deleteUserInvoiceField')
  def delete(self, field_uuid):
    """ Deletes the invoice field for the current user. """
    user = get_authenticated_user()
    if not user.stripe_id:
      raise NotFound()

    result = delete_billing_invoice_field(user, field_uuid)
    if not result:
      abort(404)

    return 'Okay', 201


@resource('/v1/organization/<orgname>/invoice/fields')
@path_param('orgname', 'The name of the organization')
@related_user_resource(UserInvoiceFieldList)
@internal_only
@show_if(features.BILLING)
class OrganizationInvoiceFieldList(ApiResource):
  """ Resource for listing and creating an organization's custom invoice fields. """
  schemas = {
    'InvoiceField': {
      'id': 'InvoiceField',
      'type': 'object',
      'description': 'Description of an invoice field',
      'required': [
        'title', 'value'
      ],
      'properties': {
        'title': {
          'type': 'string',
          'description': 'The title of the field being added',
        },
        'value': {
          'type': 'string',
          'description': 'The value of the field being added',
        },
      },
    },
  }

  @require_scope(scopes.ORG_ADMIN)
  @nickname('listOrgInvoiceFields')
  def get(self, orgname):
    """ List the invoice fields for the organization. """
    permission = AdministerOrganizationPermission(orgname)
    if permission.can():
      organization = model.organization.get_organization(orgname)
      if not organization.stripe_id:
        raise NotFound()

      return {'fields': get_invoice_fields(organization)[0]}

    abort(403)

  @require_scope(scopes.ORG_ADMIN)
  @nickname('createOrgInvoiceField')
  @validate_json_request('InvoiceField')
  def post(self, orgname):
    """ Creates a new invoice field. """
    permission = AdministerOrganizationPermission(orgname)
    if permission.can():
      organization = model.organization.get_organization(orgname)
      if not organization.stripe_id:
        raise NotFound()

      data = request.get_json()
      created_field = create_billing_invoice_field(organization, data['title'], data['value'])
      return created_field

    abort(403)


@resource('/v1/organization/<orgname>/invoice/field/<field_uuid>')
@path_param('orgname', 'The name of the organization')
@related_user_resource(UserInvoiceField)
@internal_only
@show_if(features.BILLING)
class OrganizationInvoiceField(ApiResource):
  """ Resource for deleting an organization's custom invoice fields. """
  @require_scope(scopes.ORG_ADMIN)
  @nickname('deleteOrgInvoiceField')
  def delete(self, orgname, field_uuid):
    """ Deletes the invoice field for the current user. """
    permission = AdministerOrganizationPermission(orgname)
    if permission.can():
      organization = model.organization.get_organization(orgname)
      if not organization.stripe_id:
        raise NotFound()

      result = delete_billing_invoice_field(organization, field_uuid)
      if not result:
        abort(404)

      return 'Okay', 201

    abort(403)