import logging
import re
import itertools
import validators
import asyncio
import dns.resolver

from collections import OrderedDict
from transliterate import translit
from pydantic.dataclasses import dataclass

from sqlalchemy import and_
from typing import Optional, List, Any

from intranet.domenator.src.db import MailUserDomainStatus, MailUserDomain
from intranet.domenator.src.logic.exceptions.mail_user_domain import MailUserDomainsMultipleNotAllowed, MailUserDomainNotRegistered
from intranet.domenator.src.logic.utils import chunks, timeit
from intranet.domenator.src.logic.dns_resolver import DNSResolver
from intranet.domenator.src.logic.connect import get_domains_in_connect
from intranet.domenator.src.utils.tasks import is_active_by_uid
from intranet.domenator.src.domenator_logging.logger import get_logger
from intranet.domenator.src.settings import config
from intranet.domenator.src.logic.blackbox import get_used_domains

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


@dataclass
class UserSuggestEmail:
    domain: str
    login: str = 'me'
    email: str = ''
    validated_domain: Optional[str] = ''
    valid: bool = False

    def __post_init__(self):
        self.validated_domain = get_valid_domain(self.domain)
        if self.validated_domain:
            self.valid = True
            self.email = f'{self.login}@{self.validated_domain}'

    def __hash__(self):
        return hash(self.email)

    def __lt__(self, other):
        return len(self.email) < len(other.email)


DOMAINS_TOP_SUFFIX = ['1', 'pro', '7', 'top', 'work', 'design', 'photo']
TOP_LOGINS = ['info', 'mail', 'i', 'my', 'ya', 'inbox', 'iam', 'sales', 'support', 'contact', 'director', 'art', 'boss', 'ceo']


MIN_NUMBER_OF_CHARS_IN_DOMAIN_NAME = 6  # minimum 3 letters before .ru
MAX_NUMBER_OF_CHARS_IN_DOMAIN_NAME = 63
MAX_NUMBER_OF_CHARS_IN_LOGIN = 30


def get_valid_domain(candidate: str, tld='ru') -> Optional[str]:
    """
    A domain name can be from 3 up to 63 characters (letters, numbers or combination) long.
    The only symbol character domain names can include is a hyphen (-)
    although the domain name cannot start or end with a hyphen nor have consecutive hyphens.

    """

    candidate = candidate.lower()

    # first let's try to transliterate
    # raises LanguageDetectionError when only latin letters are in the string
    # so we just ignore all errors from transliterate
    # and replace all non-letters and non-digits later
    try:
        candidate = translit(candidate, reversed=True, strict=True).lower()
    except Exception:
        pass

    try:
        candidate = re.sub(r'[^a-z0-9-]*', '', candidate)

        candidate = re.sub(r'--+', '-', candidate)
        candidate = re.sub(r'^-*', '', candidate)
        candidate = re.sub(r'-*$', '', candidate)
        candidate = f'{candidate[:MAX_NUMBER_OF_CHARS_IN_DOMAIN_NAME]}.{tld}'

        assert len(candidate) >= MIN_NUMBER_OF_CHARS_IN_DOMAIN_NAME

        if validators.domain(candidate):
            return candidate

        return None
    except Exception:
        return None


def get_valid_login(login: str) -> Optional[str]:
    """
    Логин может быть длиной до 40 символов и состоять только из символов [a-zA-Z0-9._-]
    """
    if not login:
        return

    # first let's try to transliterate
    # raises LanguageDetectionError when only latin letters are in the string
    # so we just ignore all errors from transliterate
    # and replace all non-letters and non-digits later
    try:
        login = translit(login, reversed=True, strict=True)
    except Exception:
        pass

    login = re.sub(r'[^a-zA-Z0-9.-]*', '', login)
    all_checks_ok = False
    while not all_checks_ok:
        prev_login = login
        for prefix in DISALLOWED_PREFIXES:
            if login.startswith(prefix):
                login = login[len(prefix):]
        login = re.sub(r'^[0-9.-]*', '', login)
        login = re.sub(r'--+', '-', login)
        login = re.sub(r'\.\.+', '.', login)
        login = re.sub(r'\.-', '-', login)
        login = re.sub(r'-\.', '-', login)
        login = re.sub(r'[-.]*$', '', login)
        if prev_login == login:
            all_checks_ok = True
    login = login[:MAX_NUMBER_OF_CHARS_IN_LOGIN]
    return login


def unique_and_valid(iterable):
    return list(OrderedDict.fromkeys([item for item in iterable if item.valid]))


DNS_RECORD_TYPES = ('NS', 'A')


@timeit
async def filter_domain_candidates(user, candidates: List[Any], limit: int):

    resolver = DNSResolver()

    async def is_not_registered(domain: str) -> Optional[str]:
        for record_type in DNS_RECORD_TYPES:
            try:
                await resolver.resolve(domain, record_type, raise_on_no_answer=False)
                return
            except dns.resolver.NXDOMAIN:
                default_log.info(f'domain: {domain}, rtype: {record_type} does not exists')
            except Exception as e:
                default_log.error(f'domain: {domain}, rtype: {record_type}, error: {e}')

        default_log.info(f'domain: {domain} is free')
        return domain

    # check domain cache
    unique_domains = set(candidate.validated_domain for candidate in candidates)
    free_domains = unique_domains - config.domain_cache
    default_log.info(f'DNS cache hit: {len(unique_domains-free_domains)}; miss: {len(free_domains)}')

    connect_domains = set()
    if is_active_by_uid(user['uid'], percentage=config.connect_check_domain_percent):
        try:
            default_log.info(free_domains)
            if is_active_by_uid(user['uid'], percentage=config.connect_check_domain_in_bb_percent):
                connect_domains = await get_used_domains(free_domains)
            else:
                connect_domains = await get_domains_in_connect(free_domains)

            default_log.info(f'Connect domains hit: {len(connect_domains)}; miss: {len(free_domains-connect_domains)}')
        except Exception:
            default_log.warning('Connect get_domains error, skip check', exc_info=True)
        free_domains -= connect_domains

    candidates = [candidate for candidate in candidates if candidate.validated_domain in free_domains]
    validated_domains = list(OrderedDict.fromkeys([candidate.validated_domain for candidate in candidates]).keys())
    results = []
    if limit:
        for chunk in chunks(validated_domains, max(limit * 2, 4)):
            free_domains = await asyncio.gather(*[
                is_not_registered(domain) for domain in chunk
            ])
            results += [candidate for candidate in candidates if candidate.validated_domain in free_domains]
            if len(results) >= limit:
                break
    else:
        free_domains = await asyncio.gather(*[
            is_not_registered(domain) for domain in validated_domains
        ])
        results += [candidate for candidate in candidates if candidate.validated_domain in free_domains]

    if limit:
        results = results[:limit]
    return results


ACTIVE_MAILUSER_DOMAIN_STATUSES = {
    MailUserDomainStatus.wait_dns_entries,
    MailUserDomainStatus.pending_registrar,
    MailUserDomainStatus.registered,
    MailUserDomainStatus.probation
}

ACTIVE_MAILUSER_DOMAIN_STATUSES_STR = {
    status.value for status in ACTIVE_MAILUSER_DOMAIN_STATUSES
}


async def check_user_can_register_or_throw(uid: str, domain: str):
    active_domains = await MailUserDomain.query.where(and_(
        MailUserDomain.status.in_(ACTIVE_MAILUSER_DOMAIN_STATUSES_STR),
        MailUserDomain.uid == uid
    )).gino.first()

    if active_domains is not None:
        raise MailUserDomainsMultipleNotAllowed()


# https://a.yandex-team.ru/arc_vcs/passport/python/core/types/login/login.py?rev=r8066461#L28-35
DISALLOWED_PREFIXES = {
    'yandex-team', 'yndx',  # TEST_YANDEX_LOGIN_PREFIXES
    'uid-', 'uid.',   # SOCIAL_LOGIN_PREFIX
    'phne-', 'phne.',  # PHONISH_LOGIN_PREFIX
    'nphne-', 'nphne.',  # NEOPHONISH_LOGIN_PREFIX
    'yambot-', 'yambot.',  # YAMBOT_LOGIN_PREFIX
    'kolonkish-', 'kolonkish.',  # TEST_FRODO_LOGIN_PREFIXES
    'frodo-spam', 'frodo-pdd-spam', 'frodo-change-pass',  # TEST_FRODO_LOGIN_PREFIXES
    'kid-', 'kid.',  # KIDDISH_LOGIN_PREFIX
}


def check_login_allowed_or_throw(login: str):
    for prefix in DISALLOWED_PREFIXES:
        if login.startswith(prefix):
            raise MailUserDomainNotRegistered()


def get_candidates(user, selected_login, domain_base) -> List[UserSuggestEmail]:
    first_name = user['first_name'] or ''
    last_name = user['last_name'] or ''
    user_login = user['login'] or ''

    suggested_domains = []
    if last_name:
        if first_name:
            suggested_domains = [
                f'{first_name[0]}{last_name}',
                f'{first_name}-{last_name}',
                f'{first_name}{last_name}',
                user_login,
            ]
        else:
            suggested_domains = [
                last_name,
                user_login
            ]
        suggested_domains += [f'{last_name}-{suffix}' for suffix in DOMAINS_TOP_SUFFIX]
    suggested_domains += [f'{user_login}-{suffix}' for suffix in DOMAINS_TOP_SUFFIX]
    valid_first_name_login = get_valid_login(first_name) or 'me'
    candidates = [
        UserSuggestEmail(domain=last_name),
        UserSuggestEmail(login=valid_first_name_login, domain=last_name),
        UserSuggestEmail(domain=user_login),
    ]

    special_words_candidates = [
        UserSuggestEmail(login=login, domain=domain)
        for login, domain in itertools.product(TOP_LOGINS, suggested_domains)
    ]
    candidates += special_words_candidates

    if domain_base:
        candidates = [UserSuggestEmail(domain_base)] + [
            UserSuggestEmail(login=candidate.login, domain=f'{domain_base}-{candidate.domain}')
            for candidate in candidates
        ]

    if selected_login:
        candidates = [
            UserSuggestEmail(login=selected_login, domain=candidate.domain)
            for candidate in candidates
        ]

    candidates = unique_and_valid(candidates)

    candidates.sort()
    return candidates
