import json
import logging
from asyncpg.pgproto.pgproto import timedelta
from datetime import datetime, date
from operator import and_
from sqlalchemy import desc

import dns.resolver
import asyncio
from typing import Set

from intranet.domenator.src.db.event.models import Event as EventModel
from intranet.domenator.src.worker.utils import periodic_job
from intranet.domenator.src.logbroker import LogbrokerClient
from intranet.domenator.src.db.domain.models import MailUserDomain, MailUserDomainStatus
from intranet.domenator.src.settings import config
from intranet.domenator.src.logic.mailsettings import get_mailsettings_client
from intranet.domenator.src.logic.passport import get_passport_client
from intranet.domenator.src.logic.regru import get_regru_client
from intranet.domenator.src.logic.fouras import get_fouras_client
from intranet.domenator.src.logic.gendarme import get_gendarme_client
from intranet.domenator.src.logic.sender import send_registered_domain_mail
from intranet.domenator.src.domenator_logging.logger import get_logger
from async_clients.exceptions.passport import PassportException

from intranet.domenator.src.api.routes.domain import error_log

log = logging.getLogger(__name__)
default_log = get_logger(log_name='dom_default')


def chunks(lst, chunk_size):
    for i in range(0, len(lst), chunk_size):
        yield lst[i:i + chunk_size]


@periodic_job(
    predicate=config.enable_logbroker_task,
)
async def push_events_to_logbroker(ctx) -> None:
    default_log.info('Start push_events_to_logbroker task')
    logbroker: LogbrokerClient = ctx['logbroker']
    while True:
        events = await EventModel.query.where(
            EventModel.is_processed == False  # noqa
        ).order_by(EventModel.created_at).limit(500).gino.all()
        if not events:
            break

        event: EventModel
        for event in events:
            json_data = {
                'name': event.name,
                'created_at': event.created_at.isoformat(),
                'data': event.data,
            }
            logbroker.write(
                json.dumps(json_data).encode()
            )
            await event.update(is_processed=True).apply()

DKIM_SELECTOR = 'mail'


async def add_dkim_entries(domains: Set[str]):
    fouras = await get_fouras_client()
    gendarme = await get_gendarme_client()
    regru_client = await get_regru_client()

    for domain in domains:
        try:
            dkim_public_key = await fouras.get_or_gen_domain_key(domain, selector=DKIM_SELECTOR)
            assert dkim_public_key
            txt_resp = await regru_client.zone_add_txt(domain, subdomain=f'{DKIM_SELECTOR}._domainkey', text=dkim_public_key)
            assert 'answer' in txt_resp, f'failed to add dkim entries: {txt_resp} for domain {domain}'
            await gendarme.recheck(domain)
            log.debug(f'dkim for {domain} added successfully')
        except Exception as exc:
            log.error(f'failed to generate dkim for {domain}, error {exc}', exc_info=True)


async def send_success_mails_from_records(records):
    for record in records:
        try:
            await send_registered_domain_mail(to_yandex_puid=record.uid, email=f'{record.login}@{record.domain}')
        except Exception:
            log.error('failed to send successful registration email for uid %s and domain %s',
                      record.uid, record.domain)


EXPECTED_NS_RECORDS = [f'{ns}.' for ns in config.regru_config['nss'].values()] + [
    # temporarily allow reg.ru ns
    # https://st.yandex-team.ru/CHEMODAN-76454
    'ns1.reg.ru.',
    'ns2.reg.ru.'
]

DOMAINS_PER_LOOP_LIMIT = 300
YANDEX_SPF = 'v=spf1 redirect=_spf.yandex.net'


@periodic_job(
    predicate=config.enable_expired_at,
    minute=set(range(5, 55, 10)),
    timeout=500
)
async def sync_expired_at_task(ctx) -> None:
    await asyncio.sleep(1)
    await sync_expired_at()


async def sync_expired_at() -> None:
    regru_client = await get_regru_client()

    domains = await MailUserDomain.query.where(
        and_(
            and_(
                MailUserDomain.service_id != None, # noqa
                MailUserDomain.status != MailUserDomainStatus.failed
            ),
            MailUserDomain.expired_at == None, # noqa
        )
    ).limit(50).gino.all()

    default_log.info(f'Found {[x.domain for x in domains]} domains')
    if len(domains) == 0:
        return

    domains_info_resp = []
    for chunk_domains in chunks(domains, 10):
        resp = await regru_client.get_info([x.service_id for x in chunk_domains])
        default_log.info(f'Regru response {resp}')
        assert resp['result'] == 'success'
        domains_info_resp.extend(resp['answer']['services'])

    domains_info = {x['service_id']: x for x in domains_info_resp}
    for d in domains:
        domain_info = domains_info.get(d.service_id)

        expired_at = None
        if domain_info is None:
            error_log.error(f'Domain {d.domain} not found in reg ru')
            continue
        else:
            if domain_info['expiration_date'] == '0000-00-00':
                error_log.error(f'Regru expiration_date is zero for {d.domain} domain')
                continue
            if domain_info['state'] != 'A':
                error_log.error(f'Regru state is not active for {d.domain} domain')
                continue
            if expired_at is None:
                expired_at = datetime.strptime(domain_info['expiration_date'], '%Y-%m-%d')

        default_log.info(f'Update expired_at for {d.domain} to {expired_at}')
        await d.update(expired_at=expired_at).apply()


@periodic_job(
    predicate=config.enable_renew_domains,
    minute=set(range(30, 60, 60)),
    timeout=500
)
async def renew_domains_task(ctx) -> None:
    await asyncio.sleep(1)
    await renew_domains()


async def renew_domains():
    regru_client = await get_regru_client()

    domains = await MailUserDomain.query.where(
        and_(
            MailUserDomain.service_id != None, # noqa
                and_(
                MailUserDomain.expired_at != None, # noqa
                and_(
                    MailUserDomain.expired_at > date.min,
                    MailUserDomain.expired_at < datetime.now() + timedelta(days=config.days_for_prolong)
                )
            )
        )
    ).limit(100).gino.all()

    for d in domains:
        default_log.info(f'Try prolong domain {d.domain}')

        renew_resp = await regru_client.renew(d.service_id)
        default_log.info(f'Regru renew response: {renew_resp}')

        await d.update(expired_at=None, updated_at=datetime.now()).apply()


@periodic_job(
    predicate=config.enable_domain_sync,
    minute=set(range(0, 60, 10)),
    timeout=500
)
async def sync_user_domain_status_task(ctx) -> None:
    await asyncio.sleep(1)
    await sync_user_domain_status()


async def sync_user_domain_status() -> None:
    default_log.info('Start sync_user_domain_status task')
    domains_waiting_dns = await MailUserDomain.query.where(
        MailUserDomain.status == MailUserDomainStatus.wait_dns_entries
    ).order_by(desc(MailUserDomain.updated_at)).limit(DOMAINS_PER_LOOP_LIMIT).gino.all()

    if domains_waiting_dns is None:
        return

    mailsettings_client = await get_mailsettings_client()
    passport_client = await get_passport_client()
    regru_client = await get_regru_client()
    records_to_update = []
    for record in domains_waiting_dns:
        default_log.info(f'checking domain {record.domain}')
        if record.uid in config.users_with_emulation:
            records_to_update.append(record.id)
            continue
        try:
            answers = dns.resolver.query(record.domain, 'NS')

            entries = ''
            for rdata in answers:
                entry = rdata.to_text()
                entries += f' | {entry}'
                if entry in EXPECTED_NS_RECORDS:
                    default_log.info(f'domain {record.domain} check success')

                    pdd_alias = f'{record.login}@{record.domain}'
                    try:
                        await passport_client.add_pdd_alias(record.uid, pdd_alias)
                    except PassportException as exc:
                        default_log.info(f'error on add pdd alias {record.domain}: {str(exc)}')

                        if str(exc) == 'alias.exists':
                            pass
                        elif str(exc) == 'account.not_subscribed':
                            await record.update(status=MailUserDomainStatus.cancelled_by_user).apply()
                            break
                        elif str(exc) == 'domain.not_found':
                            await passport_client.domain_add(record.domain, config.mail_domains_shared_admin_uid)
                            break
                        else:
                            raise exc

                    default_log.info(f'domain {record.domain} pdd alias success')
                    try:
                        await mailsettings_client.update_profile(record.uid, {
                            'default_email': pdd_alias
                        })
                    except Exception as exc:
                        await passport_client.delete_pdd_alias(record.uid)
                        default_log.info(f'reverted alias for {record.uid}')
                        raise exc

                    records_to_update.append(record.id)
                    break
            else:
                default_log.info(f'domain {record.domain} uid {record.uid} check failed, entries found: {entries}')
        except dns.resolver.NXDOMAIN as exc:
            default_log.error(f'domain {record.domain} check failed, no entries found: {exc}')
        except Exception as exc:
            default_log.error(f'domain {record.domain}, uid {record.uid} check failed with error {exc}', exc_info=True)

    if len(records_to_update) > 0:
        records = await MailUserDomain.query.where(
            MailUserDomain.id.in_(records_to_update)
        ).limit(DOMAINS_PER_LOOP_LIMIT).gino.all()
        if records is None:
            default_log.info('no domains after query to update')
            return
        domains = set([record.domain for record in records if record.uid not in config.users_with_emulation])
        for chunk_domains in chunks(list(domains), 30):
            # add mx entries
            mx_resp = await regru_client.zone_add_mx(chunk_domains)
            assert 'answer' in mx_resp, f'failed to add mx entries: {mx_resp}'
            default_log.info(f'domains {chunk_domains} mx added successfully')

            # add SPF
            txt_resp = await regru_client.zone_add_txt(chunk_domains, text=YANDEX_SPF)
            assert 'answer' in txt_resp, f'failed to add txt entries: {txt_resp}'
            default_log.info(f'domains {chunk_domains} txt added successfully')

            await add_dkim_entries(chunk_domains)

        for record in records:
            default_log.info(f'User {record.uid} add beautiful address {record.login}@{record.domain}')

        # FIXME: MailUserDomainStatus.update(...)
        # triggers readonly transaction error for some reason
        tasks = [record.update(
            status=MailUserDomainStatus.registered.value).apply() for record in records]
        await asyncio.gather(*tasks)

        await send_success_mails_from_records(records)

        default_log.info(f'domains {domains} updated successfully')

# @periodic_job()
# async def test_periodic_task_1(ctx) -> None:
#     default_log.info('Start test_periodic_task_1')
#     await asyncio.sleep(1)
#
#
# @periodic_job()
# async def test_periodic_task_2(ctx) -> None:
#     default_log.info('Start test_periodic_task_2')
