import traceback
from binascii import hexlify
from functools import wraps
from os import urandom

import pendulum
import requests
import rollbar
from flask import abort as flask_abort
from flask import current_app, jsonify, make_response, render_template_string, request, session
from flask_login import current_user, login_user, logout_user
from werkzeug.exceptions import HTTPException
from sqlalchemy.exc import DBAPIError

from app.models import Agency, BriefUser, User, db
from app.tasks.email import send_email
from app.authentication import get_api_key_from_request
from app.api.business.errors import DBError
from dmutils.csrf import get_csrf_token
from dmutils.email import (ONE_DAY_IN_SECONDS, EmailError,
                           parse_fernet_timestamp)


def allow_api_key_auth(func):
    @wraps(func)
    def decorated_view(*args, **kwargs):
        request_key = get_api_key_from_request(request)
        if request_key:
            from app.api import load_user
            from app.api.services import api_key_service
            key = api_key_service.get_key(request_key)
            if not key:
                return flask_abort(403, 'Invalid API key - revoked or non existent')
            user = load_user(key.user_id)
            login_user(user)
            current_app.logger.info('login.api_key.success: {user}', extra={'user': key.user.name})
        return func(*args, **kwargs)
    return decorated_view


def require_api_key_auth(func):
    @wraps(func)
    def decorated_view(*args, **kwargs):
        request_key = get_api_key_from_request(request)
        if request_key:
            from app.api import load_user
            from app.api.services import api_key_service
            key = api_key_service.get_key(request_key)
            if not key:
                return flask_abort(403, 'Invalid API key - revoked or non existent')
            user = load_user(key.user_id)
            login_user(user)
            current_app.logger.info('login.api_key.success: {user}', extra={'user': key.user.name})
            return func(*args, **kwargs)
        return flask_abort(403, 'Must authenticate using API key authentication')
    return decorated_view


def exception_logger(func):
    @wraps(func)
    def decorated_view(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except HTTPException as e:
            return e.get_response()
        except Exception as e:
            rollbar.report_exc_info()
            print('\033[38;5;196m ERROR: {}'.format(e))
            traceback.print_exc()
            print('\033[0m')
            return flask_abort(500)
    return decorated_view


def role_required(*roles):
    def role_decorator(func):
        @wraps(func)
        def decorated_view(*args, **kwargs):
            if not any(current_user.has_role(role) for role in roles):
                return jsonify(message="One of [{}] roles required".format(", ".join(roles))), 403
            else:
                if not current_user.has_role('admin'):
                    return jsonify(message="This service is not available"), 403
                else:
                    return func(*args, **kwargs)

        return decorated_view

    return role_decorator


def permissions_required(*permissions):
    def permissions_decorator(func):
        @wraps(func)
        def decorated_view(*args, **kwargs):
            if not any(current_user.has_permission(p) for p in permissions):
                return jsonify(message="One of [{}] permissions required".format(", ".join(permissions))), 403
            return func(*args, **kwargs)

        return decorated_view

    return permissions_decorator


def must_be_in_team_check(func):
    @wraps(func)
    def decorated_view(*args, **kwargs):
        from app.api.services import agency_service
        must_join_team = False
        try:
            must_join_team = agency_service.agency_requires_team_membership(current_user.agency_id)
            if must_join_team and not current_user.is_part_of_team():
                return jsonify(message='You must join a team to perform this action'), 403
        except Exception as e:
            pass
        return func(*args, **kwargs)
    return decorated_view


def is_current_supplier(func):
    @wraps(func)
    def decorated_view(code, *args, **kwargs):
        if current_user.role == 'supplier' and current_user.supplier_code != code:
            return jsonify(message="Unauthorised to view supplier"), 403
        return func(code, *args, **kwargs)
    return decorated_view


def is_current_user_in_brief(func):
    @wraps(func)
    def decorated_view(*args, **kwargs):
        if kwargs is not None:
            brief_id = kwargs.get('brief_id', None)

            if not brief_id:
                not_found('Invalid opportunity {}'.format(brief_id))

            users = db.session.query(BriefUser.user_id)\
                .filter(BriefUser.brief_id == brief_id).all()

            if not users:
                not_found('No users for opportunity {}'.format(brief_id))

            valid_users = [user for user in users if current_user.id in user]

            if not valid_users:
                forbidden(message='User is not associated with opportunity {}'.format(brief_id))
        return func(*args, **kwargs)
    return decorated_view


# returns an ascii hex encoded random value
def generate_random_token(length=32):
    return hexlify(urandom(length)).decode()


def user_info(user):
    from app.api.services import agency_service
    try:
        user_type = user.role
    except AttributeError:
        user_type = 'anonymous'

    try:
        email_address = user.email_address
    except AttributeError:
        email_address = None

    try:
        supplier_code = user.supplier_code
    except AttributeError:
        supplier_code = None

    try:
        notification_count = user.notification_count
    except AttributeError:
        notification_count = None

    try:
        framework = user.frameworks[0].framework.slug if user.frameworks else 'digital-marketplace'
    except AttributeError:
        framework = None

    try:
        is_authenticated = user.is_authenticated
    except AttributeError:
        is_authenticated = False

    try:
        teams = user.teams
    except AttributeError:
        teams = []

    try:
        is_part_of_team = user.is_part_of_team()
    except AttributeError:
        is_part_of_team = False

    try:
        is_team_lead = user.is_team_lead()
    except AttributeError:
        is_team_lead = False

    domains = None
    must_join_team = None
    try:
        agency_id = user.agency_id
        if agency_id:
            agency = agency_service.get_agency(agency_id)
            if agency:
                domains = [x['domain'] for x in agency.get('domains', [])]
                must_join_team = agency.get('must_join_team', None)
    except AttributeError:
        agency_id = None

    prevent_login = user_type is not 'admin'

    if prevent_login:
        if is_authenticated:
            if current_app.config['REDIS_SESSIONS']:
                session.destroy()
            logout_user()

        return {
            "isAuthenticated": False,
            "userType": "anonymous",
            "supplierCode": None,
            "emailAddress": None,
            "csrfToken": get_csrf_token(),
            "framework": None,
            "notificationCount": 0,
            "teams": [],
            "mustJoinTeam": False,
            "isPartOfTeam": False,
            "isTeamLead": False,
            "agencyId": None,
            "agencyDomains": None
        }
    else:
        return {
            "isAuthenticated": is_authenticated,
            "userType": user_type,
            "supplierCode": supplier_code,
            "emailAddress": email_address,
            "csrfToken": get_csrf_token(),
            "framework": framework,
            "notificationCount": notification_count,
            "teams": teams,
            "mustJoinTeam": must_join_team,
            "isPartOfTeam": is_part_of_team,
            "isTeamLead": is_team_lead,
            "agencyId": agency_id,
            "agencyDomains": domains
        }


def notify_team(subject, body, more_info_url=None):
    """
    Generic routine for making simple notifications to the Marketplace team.

    Notification messages should be very simple so that they're compatible with a variety of backends.
    """
    # ensure strings can be encoded as ascii only
    body = body.encode("ascii", "ignore").decode('ascii')
    subject = subject.encode("ascii", "ignore").decode('ascii')

    if current_app.config.get('DM_TEAM_EMAIL', None):
        email_body = render_template_string(
            '<p>{{ body }}</p>{% if more_info_url %}<a href="{{ more_info_url }}">More info</a>{% endif %}',
            body=body, more_info_url=more_info_url
        )
        try:
            send_email(
                current_app.config['DM_TEAM_EMAIL'],
                email_body,
                subject,
                current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
                current_app.config['DM_GENERIC_ADMIN_NAME'],
            )
        except EmailError as error:
            try:
                msg = error.message
            except AttributeError:
                msg = str(error)
            rollbar.report_exc_info()
            current_app.logger.error('Failed to send notification email: {}'.format(msg))


def get_root_url(framework_slug):
    return current_app.config['APP_ROOT'].get(framework_slug)


def abort(message):
    if isinstance(message, str):
        current_app.logger.error(message)
    return flask_abort(make_response(jsonify(message=message), 400))


def server_error(message):
    if isinstance(message, str):
        current_app.logger.error(message)
    return flask_abort(make_response(jsonify(message=message), 500))


def forbidden(message):
    return flask_abort(make_response(jsonify(message=message), 403))


def not_found(message):
    return flask_abort(make_response(jsonify(message=message), 404))


def parse_date(dt):
    try:
        return pendulum.parse(dt).date()
    except ValueError as e:
        abort(message=str(e))


def get_email_domain(email_address):
    return email_address.split('@')[-1]


def is_valid_email(email_address):
    return '@' in email_address and email_address.count('@') == 1


def prepare_specialist_responses(brief, responses):
    candidates = []

    for response in responses:
        essential_responses = list(zip(
            brief.data.get('essentialRequirements', []),
            response.data.get('essentialRequirements', [])
        ))

        nice_to_have_responses = list(zip(
            brief.data.get('niceToHaveRequirements', []),
            response.data.get('niceToHaveRequirements', [])
        ))

        candidates.append({
            'essential_responses': essential_responses,
            'name': response.data.get('specialistName', 'Unknown'),
            'nice_to_have_responses': nice_to_have_responses,
            'seller': response.supplier.name
        })

    return candidates


def state_to_long_name(state):
    states = {
        'act': 'Australian Capital Territory',
        'nsw': 'New South Wales',
        'nt': 'Northern Territory',
        'qld': 'Queensland',
        'tas': 'Tasmania',
        'sa': 'South Australia',
        'wa': 'Western Australia',
        'vic': 'Victoria'
    }
    return states.get(state, 'Unknown')


class ServiceException(Exception):
    def __init__(self, msg):
        self.msg = msg


class Service(object):
    __model__ = None

    def _isinstance(self, model, raise_error=True):
        rv = isinstance(model, self.__model__)
        if not rv and raise_error:
            raise ValueError('%s is not of type %s' % (model, self.__model__))
        return rv

    def _preprocess_params(self, kwargs):
        return kwargs

    def save(self, model, do_commit=True):
        self._isinstance(model)
        self.add_to_session(model)
        if do_commit:
            self.commit_changes()
        return model

    def all(self):
        return self.__model__.query.all()

    def get(self, id):
        return self.__model__.query.get(id)

    def get_all(self, *ids):
        return self.__model__.query.filter(self.__model__.id.in_(ids)).all()

    def filter(self, *args):
        return self.__model__.query.filter(*args)

    def find(self, **kwargs):
        return self.__model__.query.filter_by(**kwargs)

    def first(self, **kwargs):
        return self.find(**kwargs).first()

    def new(self, **kwargs):
        return self.__model__(**self._preprocess_params(kwargs))

    def create(self, **kwargs):
        return self.save(self.new(**kwargs))

    def update(self, model, **kwargs):
        self._isinstance(model)
        for k, v in self._preprocess_params(kwargs).items():
            setattr(model, k, v)
        self.save(model)
        return model

    def delete(self, model, do_commit=True):
        self._isinstance(model)
        db.session.delete(model)
        if do_commit:
            self.commit_changes()

    def add_to_session(self, model):
        self._isinstance(model)
        db.session.add(model)

    def commit_changes(self):
        try:
            db.session.commit()
        except DBAPIError as e:
            db.session.rollback()
            raise DBError(str(e))
