import logging
import stripe
import json

from flask import request
from flask.ext.login import logout_user
from flask.ext.principal import identity_changed, AnonymousIdentity

from app import app
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
                           log_action, internal_only, NotFound, require_user_admin,
                           InvalidToken, require_scope, format_date)
from endpoints.api.subscribe import subscribe
from endpoints.common import common_login
from data import model
from data.plans import get_plan
from auth.permissions import (AdministerOrganizationPermission, CreateRepositoryPermission,
                              UserAdminPermission, UserReadPermission)
from auth.auth_context import get_authenticated_user
from auth import scopes
from util.gravatar import compute_hash
from util.email import (send_confirmation_email, send_recovery_email,
                        send_change_email)


logger = logging.getLogger(__name__)


def user_view(user):
  def org_view(o):
    admin_org = AdministerOrganizationPermission(o.username)
    return {
      'name': o.username,
      'gravatar': compute_hash(o.email),
      'is_org_admin': admin_org.can(),
      'can_create_repo': admin_org.can() or CreateRepositoryPermission(o.username).can(),
      'preferred_namespace': not (o.stripe_id is None)
    }

  organizations = model.get_user_organizations(user.username)

  def login_view(login):
    return {
      'service': login.service.name,
      'service_identifier': login.service_ident,
    }

  logins = model.list_federated_logins(user)

  user_response = {
    'verified': user.verified,
    'anonymous': False,
    'username': user.username,
    'email': user.email,
    'gravatar': compute_hash(user.email),
  }

  user_admin = UserAdminPermission(user.username)
  if user_admin.can():
    user_response.update({
      'organizations': [org_view(o) for o in organizations],
      'logins': [login_view(login) for login in logins],
      'can_create_repo': True,
      'invoice_email': user.invoice_email,
      'preferred_namespace': not (user.stripe_id is None),
    })

  return user_response


def notification_view(notification):
  return {
    'organization': notification.target.username if notification.target.organization else None,
    'kind': notification.kind.name,
    'created': format_date(notification.created),
    'metadata': json.loads(notification.metadata_json),
  }


@resource('/v1/user/')
class User(ApiResource):
  """ Operations related to users. """
  schemas = {
    'NewUser': {
      'id': 'NewUser',
      'type': 'object',
      'description': 'Fields which must be specified for a new user.',
      'required': [
        'username',
        'password',
        'email',
      ],
      'properties': {
        'username': {
          'type': 'string',
          'description': 'The user\'s username',
        },
        'password': {
          'type': 'string',
          'description': 'The user\'s password',
        },
        'email': {
          'type': 'string',
          'description': 'The user\'s email address',
        },
      }
    },
    'UpdateUser': {
      'id': 'UpdateUser',
      'type': 'object',
      'description': 'Fields which can be updated in a user.',
      'properties': {
        'password': {
          'type': 'string',
          'description': 'The user\'s password',
        },
        'invoice_email': {
          'type': 'boolean',
          'description': 'Whether the user desires to receive an invoice email.',
        },
        'email': {
          'type': 'string',
          'description': 'The user\'s email address',
        },
      },
    },
  }

  @require_scope(scopes.READ_USER)
  @nickname('getLoggedInUser')
  def get(self):
    """ Get user information for the authenticated user. """
    user = get_authenticated_user()
    if user is None or user.organization or not UserReadPermission(user.username).can():
      raise InvalidToken("Requires authentication", payload={'session_required': False})

    return user_view(user)

  @require_user_admin
  @nickname('changeUserDetails')
  @internal_only
  @validate_json_request('UpdateUser')
  def put(self):
    """ Update a users details such as password or email. """
    user = get_authenticated_user()
    user_data = request.get_json()

    try:
      if 'password' in user_data:
        logger.debug('Changing password for user: %s', user.username)
        log_action('account_change_password', user.username)
        model.change_password(user, user_data['password'])

      if 'invoice_email' in user_data:
        logger.debug('Changing invoice_email for user: %s', user.username)
        model.change_invoice_email(user, user_data['invoice_email'])

      if 'email' in user_data and user_data['email'] != user.email:
        new_email = user_data['email']
        if model.find_user_by_email(new_email):
          # Email already used.
          raise request_error(message='E-mail address already used')
        
        logger.debug('Sending email to change email address for user: %s',
                     user.username)
        code = model.create_confirm_email_code(user, new_email=new_email)
        send_change_email(user.username, user_data['email'], code.code)
        
    except model.InvalidPasswordException, ex:
      raise request_error(exception=ex)

    return user_view(user)

  @nickname('createNewUser')
  @internal_only
  @validate_json_request('NewUser')
  def post(self):
    """ Create a new user. """
    user_data = request.get_json()

    existing_user = model.get_user(user_data['username'])
    if existing_user:
      raise request_error(message='The username already exists')

    try:
      new_user = model.create_user(user_data['username'], user_data['password'],
                                   user_data['email'])
      code = model.create_confirm_email_code(new_user)
      send_confirmation_email(new_user.username, new_user.email, code.code)
      return 'Created', 201
    except model.DataModelException as ex:
      raise request_error(exception=ex)

@resource('/v1/user/private')
@internal_only
class PrivateRepositories(ApiResource):
  """ Operations dealing with the available count of private repositories. """
  @require_user_admin
  @nickname('getUserPrivateAllowed')
  def get(self):
    """ Get the number of private repos this user has, and whether they are allowed to create more.
    """
    user = get_authenticated_user()
    private_repos = model.get_private_repo_count(user.username)
    repos_allowed = 0

    if user.stripe_id:
      cus = stripe.Customer.retrieve(user.stripe_id)
      if cus.subscription:
        plan = get_plan(cus.subscription.plan.id)
        if plan:
          repos_allowed = plan['privateRepos']
      
    return {
      'privateCount': private_repos,
      'privateAllowed': (private_repos < repos_allowed)
    }


def conduct_signin(username_or_email, password):
  needs_email_verification = False
  invalid_credentials = False

  verified = model.verify_user(username_or_email, password)
  if verified:
    if common_login(verified):
      return {'success': True}
    else:
      needs_email_verification = True

  else:
    invalid_credentials = True

  return {
    'needsEmailVerification': needs_email_verification,
    'invalidCredentials': invalid_credentials,
  }, 403


@resource('/v1/user/convert')
@internal_only
class ConvertToOrganization(ApiResource):
  """ Operations for converting a user to an organization. """
  schemas = {
    'ConvertUser': {
      'id': 'ConvertUser',
      'type': 'object',
      'description': 'Information required to convert a user to an organization.',
      'required': [
        'adminUser',
        'adminPassword',
        'plan',
      ],
      'properties': {
        'adminUser': {
          'type': 'string',
          'description': 'The user who will become an org admin\'s username',
        },
        'adminPassword': {
          'type': 'string',
          'description': 'The user who will become an org admin\'s password',
        },
        'plan': {
          'type': 'string',
          'description': 'The plan to which the organizatino should be subscribed',
        },
      },
    },
  }

  @require_user_admin
  @nickname('convertUserToOrganization')
  @validate_json_request('ConvertUser')
  def post(self):
    """ Convert the user to an organization. """
    user = get_authenticated_user()
    convert_data = request.get_json()

    # Ensure that the new admin user is the not user being converted.
    admin_username = convert_data['adminUser']
    if admin_username == user.username:
      raise request_error(reason='invaliduser',
                           message='The admin user is not valid')

    # Ensure that the sign in credentials work.
    admin_password = convert_data['adminPassword']
    if not model.verify_user(admin_username, admin_password):
      raise request_error(reason='invaliduser',
                           message='The admin user credentials are not valid')

    # Subscribe the organization to the new plan.
    plan = convert_data['plan']
    subscribe(user, plan, None, True)  # Require business plans

    # Convert the user to an organization.
    model.convert_user_to_organization(user, model.get_user(admin_username))
    log_action('account_convert', user.username)

    # And finally login with the admin credentials.
    return conduct_signin(admin_username, admin_password)


@resource('/v1/signin')
@internal_only
class Signin(ApiResource):
  """ Operations for signing in the user. """
  schemas = {
    'SigninUser': {
      'id': 'SigninUser',
      'type': 'object',
      'description': 'Information required to sign in a user.',
      'required': [
        'username',
        'password',
      ],
      'properties': {
        'username': {
          'type': 'string',
          'description': 'The user\'s username',
        },
        'password': {
          'type': 'string',
          'description': 'The user\'s password',
        },
      },
    },
  }

  @nickname('signinUser')
  @validate_json_request('SigninUser')
  def post(self):
    """ Sign in the user with the specified credentials. """
    signin_data = request.get_json()
    if not signin_data:
      raise NotFound()

    username = signin_data['username']
    password = signin_data['password']

    return conduct_signin(username, password)


@resource('/v1/signout')
@internal_only
class Signout(ApiResource):
  """ Resource for signing out users. """
  @nickname('logout')
  def post(self):
    """ Request that the current user be signed out. """
    logout_user()
    identity_changed.send(app, identity=AnonymousIdentity())
    return {'success': True}


@resource("/v1/recovery")
@internal_only
class Recovery(ApiResource):
  """ Resource for requesting a password recovery email. """
  schemas = {
    'RequestRecovery': {
      'id': 'RequestRecovery',
      'type': 'object',
      'description': 'Information required to sign in a user.',
      'required': [
        'email',
      ],
      'properties': {
        'email': {
          'type': 'string',
          'description': 'The user\'s email address',
        },
      },
    },
  }

  @nickname('requestRecoveryEmail')
  @validate_json_request('RequestRecovery')
  def post(self):
    """ Request a password recovery email."""
    email = request.get_json()['email']
    code = model.create_reset_password_email_code(email)
    send_recovery_email(email, code.code)
    return 'Created', 201


@resource('/v1/user/notifications')
@internal_only
class UserNotificationList(ApiResource):
  @require_user_admin
  @nickname('listUserNotifications')
  def get(self):
    notifications = model.list_notifications(get_authenticated_user())
    return {
      'notifications': [notification_view(notification) for notification in notifications]
    }


def authorization_view(access_token):
  oauth_app = access_token.application
  return {
    'application': {
      'name': oauth_app.name,
      'description': oauth_app.description,
      'url': oauth_app.application_uri,
      'gravatar': compute_hash(oauth_app.gravatar_email or oauth_app.organization.email),
      'organization': {
        'name': oauth_app.organization.username,
        'gravatar': compute_hash(oauth_app.organization.email)
      }
    },
    'scopes': scopes.get_scope_information(access_token.scope),
    'uuid': access_token.uuid
  }

@resource('/v1/user/authorizations')
@internal_only
class UserAuthorizationList(ApiResource):
  @require_user_admin
  @nickname('listUserAuthorizations')
  def get(self):
    access_tokens = model.oauth.list_access_tokens_for_user(get_authenticated_user())

    return {
      'authorizations': [authorization_view(token) for token in access_tokens]
    }


@resource('/v1/user/authorizations/<access_token_uuid>')
@internal_only
class UserAuthorization(ApiResource):
  @require_user_admin
  @nickname('getUserAuthorization')
  def get(self, access_token_uuid):
    access_token = model.oauth.lookup_access_token_for_user(get_authenticated_user(),
                                                            access_token_uuid)
    if not access_token:
      raise NotFound()

    return authorization_view(access_token)

  @require_user_admin
  @nickname('deleteUserAuthorization')
  def delete(self, access_token_uuid):
    access_token = model.oauth.lookup_access_token_for_user(get_authenticated_user(),
                                                            access_token_uuid)
    if not access_token:
      raise NotFound()

    access_token.delete_instance(recursive=True, delete_nullable=True)
    return 'Deleted', 204