from maps.wikimap.stat.libs.gdpr import uid_from_gdpr_str
from maps.wikimap.stat.tasks_payment.tasks_logging.libs.parsing import isotime_normalizer
from nile.api.v1 import extractors, filters
from qb2.api.v1 import (
    extractors as qb2_extractors,
    filters as qb2_filters
)

from cyson import UInt


_OPERATION_ACCEPT = b'accept'
_VIRTUAL_OPERATION_ACCEPT_WITH_EDIT = b'accept-with-edit'
_OPERATION_CHANGE_POSITION = b'change-position'
_OPERATION_CHANGE_TYPE = b'change-type'
_OPERATION_NEED_INFO = b'need-info'
_OPERATION_REJECT = b'reject'

PAID_OPERATIONS = frozenset([
    _OPERATION_ACCEPT,
    _VIRTUAL_OPERATION_ACCEPT_WITH_EDIT,
    _OPERATION_CHANGE_POSITION,
    _OPERATION_CHANGE_TYPE,
    _OPERATION_NEED_INFO,
    _OPERATION_REJECT,
])

_EPOCH_START = b'1970-01-01 00:00:00'
_to_isotime = isotime_normalizer()


def _unwrap_history(records):
    '''
    | history                  | ... |
    |--------------------------+-----|
    | [                        | ... |
    |   {                      |     |
    |     'operation':  bytes, |     |
    |     'modifiedBy': int,   |     |
    |     'modifiedAt': bytes, |     |
    |     ...                  |     |
    |   }                      |     |
    | ]                        |     |

    For each history event produces a line:
    | operation         | modified_by        | modified_at        | ... |
    |-------------------+--------------------+--------------------+-----|
    | history.operation | history.modifiedBy | history.modifiedAt | ... |
    '''
    for record in records:
        for event in record.history:
            yield record.transform(
                'history',  # field to be removed
                operation=event[b'operation'],
                modified_by=UInt(uid_from_gdpr_str(event[b'modifiedBy'])),
                modified_at=_to_isotime(event[b'modifiedAt']),
            )


def _commit_ids_to_commits_created_at(stream, social_commit_event):
    '''
    stream:
    | id  | position | source | type | operation | modified_by | modified_at | commit_ids      |
    |-----+----------+--------+------+-----------+-------------+-------------+-----------------|
    | ... | ...      | ...    | ...  | ...       | ...         | ...         | [commitId, ...] |

    social_commit_event:
    | commit_id | created_at | type | ... |
    |-----------+------------+------+-----|
    | ...       | ...        | ...  | ... |

    result:
    | id  | position | source | type | operation | modified_by | modified_at | commits_created_at |
    |-----+----------+--------+------+-----------+-------------+-------------+--------------------|
    | ... | ...      | ...    | ...  | ...       | ...         | ...         | [created_at, ...]  |

    Note. Only events of type 'edit' are taken into account.
    '''
    def collect_commits_created_at(groups):
        for key, records in groups:
            commits_created_at = []
            position = None
            source = None
            type_val = None
            for record in records:
                # These values must be equal in a group, so select any of them.
                position = record.position
                source = record.source
                type_val = record.type

                commit_created_at = record.get('commit_created_at')
                if commit_created_at:
                    commits_created_at.append(commit_created_at)

            yield key.transform(
                position=position,
                source=source,
                type=type_val,
                commits_created_at=commits_created_at
            )

    social_commit_event = social_commit_event.filter(
        filters.equals('type', b'edit')
    ).project(
        'commit_id',
        commit_created_at=extractors.custom(_to_isotime, 'created_at')
    )

    return stream.project(
        extractors.all(exclude='commit_ids'),
        qb2_extractors.unfold('commit_id', 'commit_ids', default=None)
    ).join(
        social_commit_event,
        type='left',
        by='commit_id',
        assume_unique_right=True
    ).groupby(
        'id', 'operation', 'modified_by', 'modified_at'
    ).reduce(
        collect_commits_created_at
    )


def _is_accept_with_edit(operation, modified_at, prev_op_modified_at, commits_created_at):
    if operation != _OPERATION_ACCEPT:
        return False

    for commit_created_at in commits_created_at:
        if prev_op_modified_at < commit_created_at <= modified_at:
            return True

    return False


def _get_payable_operations_from_paid(stream):
    '''
    Leaves only operations that must be payed: any feedback reopen after it has
    been closed with status other than `need-info` is not paid.

    This function for each id extracts a chain of operations and lefts only its
    max prefix that has form of: [need-info, ...] any_paid_operation.

    stream:
    | id  | modified_at | commits_created_at | operation | <other_stream_columns> |
    |-----+-------------+--------------------+-----------+------------------------|
    | ... | ...         | [...]              | ...       | ...                    |

    result:
    | modified_at | operation | <other_stream_columns> |
    |-------------+-----------+------------------------|
    | ...         | ...       | ...                    |
    '''
    def get_payable_operations_from_paid(groups):
        for _, records in groups:
            prev_op_modified_at = _EPOCH_START
            for record in records:
                assert record.operation in PAID_OPERATIONS, f'Non-paid operation in {record}.'

                is_accept_with_edit = _is_accept_with_edit(
                    record.operation, record.modified_at, prev_op_modified_at, record.commits_created_at
                )

                record = record.transform('commits_created_at')  # remove field
                if (is_accept_with_edit):
                    yield record.transform(operation=_VIRTUAL_OPERATION_ACCEPT_WITH_EDIT)
                else:
                    yield record

                prev_op_modified_at = record.modified_at

                if record.operation != _OPERATION_NEED_INFO:
                    break

    return stream.groupby(
        'id'
    ).sort(
        'modified_at'
    ).reduce(
        get_payable_operations_from_paid
    ).project(
        extractors.all(exclude='id')
    )


def get_paid_operations_at_date(date, feedback, social_commit_event):
    '''
    feedback:
    | id  | position | source | type | history                  | commit_ids      | ... |
    |-----+----------+--------+------+--------------------------+-----------------+-----|
    | ... | ...      | ...    | ...  | [                        | [commitId, ...] | ... |
    |     |          |        |      |   {                      |                 |     |
    |     |          |        |      |     'operation':  bytes, |                 |     |
    |     |          |        |      |     'modifiedBy': int,   |                 |     |
    |     |          |        |      |     'modifiedAt': bytes, |                 |     |
    |     |          |        |      |     ...                  |                 |     |
    |     |          |        |      |   }                      |                 |     |
    |     |          |        |      | ]                        |                 |     |

    social_commit_event:
    | commit_id | created_at | type | ... |
    |-----------+------------+------+-----|
    | ...       | ...        | ...  | ... |

    result:
    | position | source | type          | operation         | modified_by        | modified_at        |
    |----------+--------+---------------+-------------------+--------------------+--------------------|
    | ...      | ...    | feedback.type | history.operation | history.modifiedBy | history.modifiedAt |

    where:
    - `operation` is a paid operation (see `PAID_OPERATIONS`); and
    - `modified_at` datetime in ISO format that belongs to the day defined by `date`.

    Note. Timezone is ignored when `history.modifiedAt` is checked against `date`.
    '''
    date = date.encode()

    return feedback.project(
        'id', 'position', 'source', 'type', 'history', 'commit_ids'
    ).map(
        _unwrap_history
    ).filter(
        qb2_filters.one_of('operation', PAID_OPERATIONS),
    ).call(
        _commit_ids_to_commits_created_at, social_commit_event
    ).call(
        _get_payable_operations_from_paid
    ).filter(
        qb2_filters.startswith('modified_at', date)
    )


def get_task_id(source_value, type_value, operation_value):
    '''
    Combines `source`, `type` and `operation` bytes values into `task_id`.
    '''
    assert operation_value in PAID_OPERATIONS

    return b'/'.join((b'feedback', operation_value, source_value, type_value))
