import datetime
import logging
from typing import Optional

import pytz
from django.conf import settings
from django.db import models, transaction, connection
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from yql.api.v1.client import YqlClient

from idm.utils import chunkify

log = logging.getLogger(__name__)


class AppMetrica(models.Model):
    application_id: int = models.CharField(_('ID приложения'), max_length=64, db_index=True)
    name: str = models.CharField(_('Имя приложения'), max_length=1024, null=True, db_index=True)
    update_time: datetime.datetime = models.DateTimeField()

    INSERT_CHUNK_SIZE = 100
    UPDATE_TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
    RECENT_CHANGES_QUERY = '''
    USE hahn;

    $parse_update_time = DateTime::Parse("{update_time_format}");

    SELECT id, name, update_time
    FROM `home/metrika/export/applications`
    WHERE
        DateTime::MakeDatetime($parse_update_time(update_time)) > (
            CurrentUtcDatetime() - DateTime::IntervalFromSeconds({seconds})
    );
    '''
    FULL_TABLE_QUERY = '''
    USE hahn;

    SELECT id, name, update_time
    FROM `home/metrika/export/applications`
    '''

    class Meta:
        verbose_name = _('Приложения Метрики')
        verbose_name_plural = _('Приложения Метрики')

    @classmethod
    @transaction.atomic
    def sync_from_yt(cls, full: bool = False, time_offset: int = 24 * 60 * 60):
        if full:
            query = cls.FULL_TABLE_QUERY.format(update_time_format=cls.UPDATE_TIME_FORMAT)
            log.info('Start sync whole YT table of Metrika apps')
            with connection.cursor() as cursor:
                cursor.execute(f'TRUNCATE TABLE {AppMetrica._meta.db_table} ')
        else:
            assert isinstance(time_offset, int) and time_offset > 0
            query = cls.RECENT_CHANGES_QUERY.format(update_time_format=cls.UPDATE_TIME_FORMAT, seconds=time_offset)
            log.info(f'Start sync changes of table of Metrika apps '
                     f'since {(timezone.now() - datetime.timedelta(seconds=time_offset)).isoformat()}')

        yql_client = YqlClient(token=settings.IDM_ROBOT_YQL_OAUTH_TOKEN)
        request = yql_client.query(query, syntax_version=1)
        request.run()
        app_table, *_ = request.get_results()
        for app_chunk in chunkify(app_table.get_iterator(), chunk_size=cls.INSERT_CHUNK_SIZE):
            apps = {}
            for id, name, update_time in app_chunk:
                try:
                    update_time = datetime.datetime.strptime(update_time, cls.UPDATE_TIME_FORMAT) \
                        .replace(tzinfo=pytz.UTC)
                except ValueError:
                    log.info(f'Field update_time has invalid format {update_time}')
                    continue

                application_id = str(id)

                if isinstance(name, bytes):
                    try:
                        name = name.decode()
                    except UnicodeDecodeError:
                        name = None
                if isinstance(name, str) and not name.isprintable():
                    log.warning(f'Invalid characters in name {name} of app (id: {application_id}). Skip.')
                    continue
                elif name is not None:
                    name = str(name)

                apps[application_id] = cls(application_id=application_id, name=name, update_time=update_time)

            if not full:
                irrelevant_apps = set()
                for application_id, app_name in AppMetrica.objects.filter(
                        application_id__in=set(apps)).values_list('application_id', 'name'):
                    if app_name != apps[application_id].name:
                        irrelevant_apps.add(application_id)  # изменившиеся удаляем
                    else:
                        del apps[application_id]  # неизменные не перезаписываем
                cls.objects.filter(application_id__in=irrelevant_apps).all().delete()

            cls.objects.bulk_create(apps.values())
            log.debug(f'Bulk of {len(apps)} apps write successfully')
        log.info('Sync Metrika apps finished correctly')

    @classmethod
    def get_app_name(cls, application_id: int) -> Optional[str]:
        if app := AppMetrica.objects.filter(application_id=application_id).first():
            return app.name
