import csv
import itertools
from datetime import date
from functools import partial, wraps
import json
from io import StringIO, BytesIO
import logging
import math
import types
from typing import Callable, List
import xlsxwriter

from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse, HttpResponseForbidden, StreamingHttpResponse
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.utils.encoding import smart_text
from django.utils.translation import override
from django.db.models import QuerySet

from django_yauth.authentication_mechanisms.tvm.request import TvmServiceRequest

from staff.reports.objects.base import Report

from staff.lib.auth import auth_mechanisms as auth
from staff.lib.forms import errors as form_errors
from staff.lib.json import JSONEncoder, dumps as custom_dumps
from staff.lib.middleware import AccessMiddleware


logger = logging.getLogger(__name__)


def use_request_lang(view):
    @wraps(view)
    def wrapper(request, *args, **kwargs):
        lang = request.GET.get('lang', request.POST.get('lang'))
        if lang and lang in [l[0] for l in settings.LANGUAGES]:
            with override(lang):
                return view(request, *args, **kwargs)
        else:
            return view(request, *args, **kwargs)
    return wrapper


class FrontJSONEncoder(JSONEncoder):

    def iterencode(self, o, _one_shot=False):
        # Replace `null` with empty string to make our frontedner happy
        return (
            '""' if chunk == 'null' else chunk
            for chunk in super(FrontJSONEncoder, self).iterencode(o, _one_shot)
        )


front_dumps = partial(json.dumps, cls=FrontJSONEncoder)


def _wrap_in_callback(request, data):
    GET = request.GET
    json_callback = GET.get('jsonp') or GET.get('callback') or 'jsonp'
    data = '{0}({1});'.format(json_callback, data)
    return data


ITER_TYPES = [type(col) for col in [dict().keys(), dict().items(), dict().values()]]
ITER_TYPES.append(list)
ITER_TYPES.append(dict)
ITER_TYPES = tuple(ITER_TYPES)


def make_json_response(request, data, status=200, is_jsonp=False, dumps=custom_dumps):
    if isinstance(data, ITER_TYPES):
        data = dumps(data)

    data = smart_text(data)

    if is_jsonp:
        data = _wrap_in_callback(request, data)

    return HttpResponse(
        data,
        content_type='application/json; charset=utf-8',
        status=status,
    )


def responding_json(view, is_jsonp=False, dumps=custom_dumps):
    """
    Декоратор для обертки респонза в JSON

    @return: HttpResponse
    """

    @wraps(view)
    def wrapper(request, *args, **kwargs):
        data = view(request, *args, **kwargs)

        if isinstance(data, (HttpResponse, StreamingHttpResponse)):
            return data

        status = 200
        if isinstance(data, tuple):
            data, status = data

        return make_json_response(request, data, status, is_jsonp)

    return wrapper


responding_jsonp = partial(responding_json, is_jsonp=True)
front_json = partial(responding_json, dumps=front_dumps)


def make_csv_response(filename, data, separator=',', encoding='utf-8', bom=None, status=200):
    responding_data = data

    if isinstance(data, (list, types.GeneratorType, itertools.chain)):
        def ___streaming_generator():
            if bom is not None:
                yield bom

            batch_size = 100
            row_index = 0
            stream = StringIO()
            w = csv.writer(stream, delimiter=separator)

            for row in data:
                w.writerow([smart_text(d, encoding=encoding) for d in row])
                row_index += 1

                if row_index == batch_size:
                    row_index = 0
                    d = stream.getvalue()
                    stream.close()
                    stream = StringIO()
                    w = csv.writer(stream, delimiter=separator)
                    yield d

            d = stream.getvalue()
            stream.close()
            yield d

        responding_data = ___streaming_generator()

    response = StreamingHttpResponse(
        responding_data,
        content_type='text/csv; charset=%s' % encoding,
        status=status,
    )

    today = date.today().isoformat()
    response["Content-Disposition"] = (
        "attachment;filename=%s_%s.csv" % (filename, today)
    )

    return response


def responding_csv(filename='untitled', separator=',', encoding='utf-8', bom=None):
    def decorator(view):
        """Decorator for wrapping response to csv"""

        @wraps(view)
        def wrapper(request, *args, **kwargs):
            data = view(request, *args, **kwargs)

            if isinstance(data, HttpResponse):
                return data

            status = 200
            if isinstance(data, tuple):
                data, status = data

            return make_csv_response(filename, data, separator, encoding, bom, status)

        return wrapper
    return decorator


def make_xlsx_response(file_name, prefix, data, status=200):
    stream = BytesIO()
    with xlsxwriter.Workbook(stream) as wb:
        for sheet_name, sheet_data in data:
            sheet_outline_settings = None
            freeze_panes = None

            if isinstance(sheet_name, Report.Sheet):
                sheet = sheet_name
                sheet_name = sheet.sheet_name
                sheet_outline_settings = sheet.sheet_outline_settings
                freeze_panes = sheet.freeze_panes

            ws = wb.add_worksheet(name=sheet_name)

            if sheet_outline_settings:
                ws.outline_settings(**sheet_outline_settings)

            if freeze_panes:
                ws.freeze_panes(**freeze_panes)

            for i, row in enumerate(sheet_data):
                if isinstance(row, Report.Row):
                    cell_format = wb.add_format(row.cell_format) if row.cell_format else None
                    ws.set_row(i, None, cell_format=cell_format, options=row.row_options)
                    row = row.cells

                for j, cell_data in enumerate(row):
                    if isinstance(cell_data, Report.Column):
                        cell_format = wb.add_format(cell_data.cell_format) if cell_data.cell_format else None
                        ws.write(i, j, cell_data.name, cell_format)
                        ws.set_column(j, j, width=cell_data.width, options=cell_data.column_options)
                    else:
                        ws.write(i, j, cell_data)

    data = stream.getvalue()
    response = HttpResponse(
        data,
        content_type=(
            'application/vnd'
            '.openxmlformats-officedocument'
            '.spreadsheetml.sheet; charset=utf-8'
        ),
        status=status,
    )

    file_name = file_name if file_name else '{}_{}'.format(prefix, date.today().isoformat())

    response['Content-Disposition'] = 'attachment; filename="%s.xlsx"' % file_name
    response['Content-Length'] = len(data)
    return response


def responding_xlsx(prefix=None):
    """
    Декоратор для обертки респонза в xls

    @return: HttpResponse
    """

    def decorator(view):

        @wraps(view)
        def wrapper(request, *args, **kwargs):
            data = view(request, *args, **kwargs)

            if isinstance(data, HttpResponse):
                return data

            status = 200
            file_name = None

            if isinstance(data, tuple):
                data, status, file_name = data

            return make_xlsx_response(
                file_name,
                prefix,
                data,
                status,
            )

        return wrapper

    return decorator


def to_json(view):
    """
    Декоратор для обертки JSON

    @return: str
    """

    @wraps(view)
    def wrapper(request, *args, **kwargs):
        return custom_dumps(view(request, *args, **kwargs))
    return wrapper


def require_permission(permission, exception=PermissionDenied):
    def decorator(view_func):
        def wrapper(request, *args, **kwargs):
            user = request.user
            if not user.has_perm(permission):
                raise exception()
            return view_func(request, *args, **kwargs)
        return wrapper
    return decorator


def render_to(template=None, content_type=None):
    """
    Decorator for Django views that sends returned dict to render_to_response
    function.

    Template name can be decorator parameter or TEMPLATE item in returned
    dictionary.  RequestContext always added as context instance.
    If view doesn't return dict then decorator simply returns output.

    Parameters:
     - template: template name to use
     - content_type: content type to send in response headers

    Examples:
    # 1. Template name in decorator parameters

    @render_to('template.html')
    def foo(request):
        bar = Bar.object.all()
        return {'bar': bar}

    # equals to
    def foo(request):
        bar = Bar.object.all()
        return render_to_response('template.html',
                                  {'bar': bar},
                                  context_instance=RequestContext(request))


    # 2. Template name as TEMPLATE item value in return dictionary.
         if TEMPLATE is given then its value will have higher priority
         than render_to argument.

    @render_to()
    def foo(request, category):
        template_name = '%s.html' % category
        return {'bar': bar, 'TEMPLATE': template_name}

    #equals to
    def foo(request, category):
        template_name = '%s.html' % category
        return render_to_response(template_name,
                                  {'bar': bar},
                                  context_instance=RequestContext(request))

    """
    def renderer(function):
        @wraps(function)
        def wrapper(request, *args, **kwargs):
            output = function(request, *args, **kwargs)
            if not isinstance(output, dict):
                return output
            tmpl = output.pop('TEMPLATE', template)
            return render_to_response(
                tmpl,
                context_instance=RequestContext(request, output),
                content_type=content_type
            )
        return wrapper
    return renderer


class Paginator(object):

    def __init__(self, page, limit):
        self.page = page
        self.limit = limit
        self.offset = (self.page - 1) * self.limit
        self.result = None
        self.total = None

    def as_dict(self):
        return {
            'page': self.page,
            'limit': self.limit,
            'result': self.result,
            'total': self.total,
            'pages': int(math.ceil(self.total / self.limit))
        }

    def cut_iterable(self, iterable):
        return iterable[self.offset: self.offset + self.limit]

    def set_total(self, total):
        self.total = total

    def set_result(self, result):
        self.result = result

    def update_by_queryset(self, queryset: QuerySet):
        self.set_total(queryset.count())
        chunk = self.cut_iterable(queryset)
        self.set_result(result=list(chunk))


def paginated(*fn, **options):
    """
    Декоратор, добавляющий пагинацию к списку объектов, возращаемых view.
    Формат пагинации как в static-api (staff-api, plan-api).
    Обернутая функция получает ключевой аргумент `paginator`, у которого она
    берет атрибуты `page` и `limit`, полученные из query-параметров,
    и должна проставить `result` (список объектов с нужным сдвигом,
    ограниченных limit) и `total` — общее число объектов.

    Можно использовать в форме
    >>> @paginated
    >>> def my_view(request, paginator):
    >>> ...

    тогда, если limit не передан в query-параметрах по умолчанию берется 100

    или
    >>> @paginated(default_limit=10)
    >>> def my_view(request, paginator):
    >>> ...

    тогда limit по умолчанию берется из параметра декоратора.
    """
    default_limit = options.pop('default_limit', 100)

    def try_parse_int(request, key, default):
        try:
            result = int(request.GET.get(key, default))
            return result if result > 0 else default
        except ValueError:
            return default

    def deco(view):
        @wraps(view)
        def wrapper(request, *args, **kwargs):
            page = try_parse_int(request, '_page', 1)
            limit = try_parse_int(request, '_limit', default_limit)
            kwargs['paginator'] = Paginator(page, limit)
            response = view(request, *args, **kwargs)
            if isinstance(response, Paginator):
                return response.as_dict()
            return response
        return wrapper

    if len(fn) == 1 and len(options) == 0 and callable(fn[0]):
        return deco(fn[0])
    else:
        return deco


def consuming_json(*fn, **kwargs):
    for_methods = kwargs.get('for_methods', ('POST'))

    def decorator(view):
        @wraps(view)
        def wrapper(request, *args, **kwargs):
            if request.method not in for_methods:
                kwargs['json_data'] = None
                return view(request, *args, **kwargs)

            try:
                data = json.loads(request.body)
            except ValueError:
                logger.exception('Wrong JSON: %s', request.body)
                return form_errors.invalid_json_error(request.body), 400

            kwargs['json_data'] = data

            return view(request, *args, **kwargs)

        return wrapper

    if len(fn) == 1 and callable(fn[0]):
        return decorator(fn[0])

    return decorator


def _check_service_id(yauser, available_service_ids):
    return isinstance(yauser, TvmServiceRequest) and yauser.service_ticket.src in available_service_ids


def _is_tvm_request(request):
    return request.auth_mechanism == auth.TVM


def _get_service_tvm_ids(tvm_services):
    service_tvm_ids = {settings.TVM_APPLICATIONS.get(service_name) for service_name in tvm_services}
    if None in service_tvm_ids:
        logger.warning('TVM client "%s" missing. Check settings.TVM_APPLICATIONS', str(tvm_services))
    return service_tvm_ids


def auth_by_tvm_only(tvm_services: List[str]) -> Callable:
    service_tvm_ids = _get_service_tvm_ids(tvm_services)

    def decorator(view):
        @wraps(view)
        def wrapper(request, *args, **kwargs):
            if _check_service_id(request.yauser, service_tvm_ids) or getattr(request.user, 'is_superuser', False):
                return view(request, *args, **kwargs)
            return HttpResponseForbidden()
        AccessMiddleware.views_available_by_tvm.add(view.__name__)
        return wrapper

    return decorator


def available_by_tvm(tvm_services: List[str]) -> Callable:
    service_tvm_ids = _get_service_tvm_ids(tvm_services)

    def decorator(view):
        @wraps(view)
        def wrapper(request, *args, **kwargs):
            if not _is_tvm_request(request):
                return view(request, *args, **kwargs)

            if _check_service_id(request.yauser, service_tvm_ids) or getattr(request.user, 'is_superuser', False):
                return view(request, *args, **kwargs)

            return HttpResponseForbidden()

        AccessMiddleware.views_available_by_tvm.add(view.__name__)
        return wrapper

    return decorator


def available_for_external(view_or_permission):

    def decorator(view_):
        perm_name = view_or_permission
        AccessMiddleware.views_available_for_external[view_.__name__] = perm_name
        return view_

    if callable(view_or_permission):
        view = view_or_permission
        AccessMiddleware.views_available_for_external[view.__name__] = None
        return view
    else:
        return decorator


def forbidden_for_robots(view):
    AccessMiddleware.views_forbidden_for_robots.add(view.__name__)
    return view


def available_by_center_token(view):
    AccessMiddleware.views_available_by_center_token.add(view.__name__)
    return view
