# -*- coding: utf-8 -*-

import socket
import time
import logging
import traceback

from google.protobuf.message import DecodeError

# for version >= 2.3
from google.protobuf.internal.decoder import _DecodeVarint32 as Decoder

import rtyserver_pb2 as rtyserver
import errors
import message

log = logging.getLogger('utils')


SEND_TYPES = {
    'modify': rtyserver.TMessage.MODIFY_DOCUMENT,
    'add': rtyserver.TMessage.ADD_DOCUMENT,
    'delete': rtyserver.TMessage.DELETE_DOCUMENT,
}

MIME_TYPES = [
    'text/html',
    'text/rtf',
    'application/msword',
    'application/x-shockwave-flash',
    'application/vnd.ms-excel',
    'application/vnd.ms-powerpoint',
    'text/xml',
    'application/pdf',
    'text/plain',
    'audio/mpeg',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    'application/vnd.openxmlformats-officedocument.presentationml.presentation',
    'application/vnd.oasis.opendocument.text',
    'application/vnd.oasis.opendocument.presentation',
    'application/vnd.oasis.opendocument.spreadsheet',
    'application/vnd.oasis.opendocument.graphics',
]

ATTRIBUTE_TYPES = {
    'integer': rtyserver.TMessage.TDocument.TAttribute.INTEGER_ATTRIBUTE,
    'literal': rtyserver.TMessage.TDocument.TAttribute.LITERAL_ATTRIBUTE,
}

MESSAGE_ID = 1


def decode_varint(input_data):
    try:
        length, position = Decoder(input_data, 0)
        return position, length
    except DecodeError:
        return None, None


def check_response(response):
    if response:
        if response.Status == rtyserver.TReply.OK:
            if response.MessageId != MESSAGE_ID:
                raise errors.IndexerError('failed indexing message with response.status=%s,'
                    ' response.message_id=%s and response %s' % (response.Status, response.MessageId, response.StatusMessage))
            return
        elif response.Status == rtyserver.TReply.NOTNOW:
            raise errors.IndexerError('failed indexing message with response.status=NOTNOW,'
                ' rtyserver is busy')
        elif response.Status == rtyserver.TReply.INCORRECT_DOCUMENT:
            raise errors.IncorrectDocumentError('failed indexing message,'
                ' because message is bad: not document or invalid document, bad service or not keyprefix')
        elif response.Status == rtyserver.TReply.SEND_FAILED:
            raise errors.IndexerError('failed indexing message'
                ' with response.status=SEND_FAILED, because failed to send message to rtyserver ')
        elif response.Status == rtyserver.TReply.READ_FAILED:
            raise errors.IndexerError('failed indexing message with'
                ' response.status=READ_FAILED, beacuse unable to read response from rtyserver')
        elif response.Status == rtyserver.TReply.STORE_FAILED:
            raise errors.IndexerError('failed indexing message with'
                ' response.status=STORE_FAILED, because, failed to send message to rtyserver')
        elif response.Status == rtyserver.TReply.INTERNAL_ERROR:
            raise errors.InternalError('message has not been indexed,'
                ' so that there was an internal error on the rtyserver side')
        else:
            raise errors.IndexerError('failed indexing message with response.status=%s' % (response.Status,))
    else:
        raise errors.IndexerError('failed indexing message, because empty response')


def send_message(sock, message, with_response=True):
    if with_response:
        sock.sendall(message)

        byte_stream = sock.recv(1024)
        response = rtyserver.TReply()

        if not byte_stream:
            raise errors.ClosedConnection('Empty response, connection is closed.')

        start, length = decode_varint(byte_stream)
        response.ParseFromString(byte_stream[start: start + length])

        check_response(response)
    else:
        sock.sendall(message)


def _get_sock(host, port, timeout):
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        sock.connect((host, port))
        return sock
    except Exception:
        if sock:
            sock.close()
        return None


def get_sock(host, port, timeout, retry_count=None):
    if retry_count:
        for i in xrange(retry_count):
            sock = _get_sock(host, port, timeout)
            if sock is not None:
                return sock
            else:
                log.warn('could not connect to %s:%s, sleeping 1 second', host, port)
                time.sleep(1)
        else:
            sock = _get_sock(host, port, timeout)
            if sock is not None:
                return sock
            else:
                raise errors.FailedToConnectDispatcher('could not connect to %s:%s' % (host, port))
    else:
        while True:
            sock = _get_sock(host, port, timeout)
            if sock is not None:
                return sock
            else:
                log.warn('could not connect to %s:%s, sleeping 1 second', host, port)
                time.sleep(1)


def check_param(param_name, param_value, param_type, required=False):
    if param_value:
        try:
            return param_type(param_value)
        except (TypeError, ValueError):
            raise errors.HttpIndexerProxyError(
                400,
                '400 Bad request',
                'Argument %s must be an %s, not %s.' %
                (param_name, param_type.__name__, type(param_value).__name__))
    elif required:
        raise errors.HttpIndexerProxyError(
            400,
            '400 Bad request',
            'Argument %s is required.' %
            (param_name,))

    return param_value


def check_attribute_param(attribute_param_name, param_name, param_value):
    if param_value is None:
        raise errors.HttpIndexerProxyError(
            400,
            '400 Bad request',
            'Argument %s in %s is required.' %
            (param_name, attribute_param_name,))

    if not isinstance(param_value, unicode):
        try:
            param_value = unicode(param_value)
        except UnicodeDecodeError:
            raise errors.HttpIndexerProxyError(
                400,
                '400 Bad request',
                'Argument %s in %s must be an %s, not %s.' %
                (param_name, attribute_param_name, unicode, type(param_value).__name__))

    if not param_value:
        raise errors.HttpIndexerProxyError(
            400,
            '400 Bad request',
            'Argument %s in %s is required.' %
            (param_name, attribute_param_name,))
    return param_value


def check_special_params(param_name, param_value, special_params, required=False):
    if param_value and param_value not in special_params:
        raise errors.HttpIndexerProxyError(400,
            '400 Bad Request',
            'Argument %s must be one of %s.' % (param_name, special_params))
    elif (not param_value or param_value not in special_params) and required:
        raise errors.HttpIndexerProxyError(
            400,
            '400 Bad Request',
            'Argument %s is required.' %
            (param_name,))

    return param_value


def create_attributes(attributes_name, attributes, with_type=True):
    try:
        res_attributes = []
        for attr in attributes:
            name = check_attribute_param(attributes_name, 'name',  attr.get('name'))
            value = check_attribute_param(attributes_name, 'value', attr.get('value'))

            type = None
            if with_type:
                _type = check_special_params('type in %s' % attributes_name, attr.get('type'), ATTRIBUTE_TYPES.keys(), required=True)
                type = ATTRIBUTE_TYPES.get(_type)

            res_attributes.append(message.Attribute(name=name, value=value, type=type))

        return res_attributes
    except errors.HttpIndexerProxyError:
        raise
    except Exception:
        log.error(traceback.format_exc())
        raise errors.HttpIndexerProxyError(
            400,
            '400 Bad Request',
            'Argument %s=%s is bad. Must be list [{"name": "some_name", "value": "some_value"}].' %
            (attributes_name, attributes))


def parse_data(data, document):
    # @send_type: unicode, utils.SEND_TYPES; required - тип отправки сообщения на идексацию
    send_type = check_special_params('send_type', data.get('send_type'), SEND_TYPES.keys(), required=True)

    # @url: unicode; required - произвольный уникальный строковой идентификатор с семантикой Url.
    # Используется для отождествления документов при выполнении команд MODIFY_DOCUMENTS и DELETE_DOCUMENTS.
    # Внутренние сервисные процедуры индексатора также испольуют факт уникальности Url
    url = check_param('url', data.get('url'), unicode, required=True)

    # @keyprefix: int - префикс всех ключей в индексе
    keyprefix = check_param('keyprefix', data.get('keyprefix'), int)

    # @mime_type: unicode, utils.MIME_TYPES - тип содержимого документа отправленого в секции document
    mime_type = check_special_params('mime_type', data.get('mime_type'), MIME_TYPES, required=True)

    # @charset: unicode- кодировка документа
    charset = check_param('charset', data.get('charset'), unicode, required=True)
    try:
        if not isinstance(document, unicode):
            document = unicode(document, charset)
    except:
        log.error(traceback.format_exc())
        raise errors.HttpIndexerProxyError(
            400,
            '400 Bad request',
            'Bad charset %s for document.' % charset)

    # @language: unicode - язык документа
    language = check_param('language', data.get('language'), unicode)

    # @language2: unicode - вторичный язык документа
    language2 = check_param('language2', data.get('language2'), unicode)

    # @modification_timestamp: int - время последней модификации документа
    modification_timestamp = check_param('modification_timestamp', data.get('modification_timestamp'), int)

    # @search_attributes: list - список поисковых атрибутов
    # [{'name': attribute name, 'value': attribute value, 'type': attribute type}, ...]
    # @name: unicode, @value: unicode, @type: 'literal' or 'integer'
    search_attributes = check_param('search_attributes', data.get('search_attributes', []), list)
    search_attributes = create_attributes('search_attributes', search_attributes)

    # @grouping_attributes: list - список группировочных аттрибутов
    # [{'name': attribute name, 'value': attribute value, 'type': attribute type}, ...]
    # @name: unicode, @value: unicode, @type: 'literal' or 'integer'
    grouping_attributes = check_param('grouping_attributes', data.get('grouping_attributes', []), list)
    grouping_attributes = create_attributes('grouping_attributes', grouping_attributes)

    # @document_attributes: list - список документных аттрибутов
    # [{'name': attribute name, 'value': attribute value}, ...]
    # @name: unicode, @value: unicode
    document_attributes = check_param('document_attributes', data.get('document_attributes', []), list)
    document_attributes = create_attributes('document_attributes', document_attributes, with_type=False)

    # @factors: list - список факторов
    # [{'name': attribute name, 'value': attribute value, 'type': attribute type}, ...]
    # @name: unicode, @value: unicode, @type: 'literal' or 'integer'
    factors = check_param('factors', data.get('factors', []), list)
    factors = create_attributes('factors', factors)

    m = message.Message(
        send_type=send_type,
        url=url,
        document=document,
        keyprefix=keyprefix,
        mime_type=mime_type,
        charset=charset,
        language=language,
        language2=language2,
        modification_timestamp=modification_timestamp,
        search_attributes=search_attributes,
        grouping_attributes=grouping_attributes,
        document_attributes=document_attributes,
        factors=factors,
    )

    return m


def parse_searchmap(f):
    services_name = []
    for line in f:
        line = line.strip()

        # берём только незакомментированные строчки
        if line and line[0] != '#':
            space_index = line.find(' ')
            if space_index != -1:
                services_name.append(line[:space_index])

    return services_name
