import logging
import os
import re
import socket
import time
import copy

from dns.exception import Timeout
from dns.resolver import NXDOMAIN
from dns.resolver import NoAnswer
from dns.resolver import Resolver

from tsproxy.common import FIFOCache, MyThreadPoolExecutor, lookup_conf_file
from tsproxy import common

logger = logging.getLogger(__name__)

cn_ip_list = []
cn_ip_file_mod = 0
cn_ip_update = 0

apnic_file = None

APNIC_LATEST = 'apnic-latest'

local_dns_query = False

resolver = Resolver('/dev/null')
resolver.nameservers.clear()
resolver.nameservers.append('208.67.220.220')
resolver.nameservers.append('208.67.222.222')
resolver.lifetime = 2
resolver.port = 443

cn_addr_cache = {}
dns_cache = FIFOCache()

ip_regex = re.compile(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')

ipv6_regex = re.compile(r'(\A([0-9a-f]{1,4}:){1,1}(:[0-9a-f]{1,4}){1,6}\Z)|'
                        r'(\A([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}\Z)|'
                        r'(\A([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}\Z)|'
                        r'(\A([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}\Z)|'
                        r'(\A([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}\Z)|'
                        r'(\A([0-9a-f]{1,4}:){1,6}(:[0-9a-f]{1,4}){1,1}\Z)|'
                        r'(\A(([0-9a-f]{1,4}:){1,7}|:):\Z)|(\A:(:[0-9a-f]{1,4})'
                        r'{1,7}\Z)|(\A((([0-9a-f]{1,4}:){6})(25[0-5]|2[0-4]\d|[0-1]'
                        r'?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})\Z)|'
                        r'(\A(([0-9a-f]{1,4}:){5}[0-9a-f]{1,4}:(25[0-5]|2[0-4]\d|'
                        r'[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})\Z)|'
                        r'(\A([0-9a-f]{1,4}:){5}:[0-9a-f]{1,4}:(25[0-5]|2[0-4]\d|'
                        r'[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)|'
                        r'(\A([0-9a-f]{1,4}:){1,1}(:[0-9a-f]{1,4}){1,4}:(25[0-5]|'
                        r'2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d))'
                        r'{3}\Z)|(\A([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,3}:'
                        r'(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?'
                        r'\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4})'
                        r'{1,2}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|'
                        r'[0-1]?\d?\d)){3}\Z)|(\A([0-9a-f]{1,4}:){1,4}(:[0-9a-f]'
                        r'{1,4}){1,1}:(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|'
                        r'2[0-4]\d|[0-1]?\d?\d)){3}\Z)|(\A(([0-9a-f]{1,4}:){1,5}|:):'
                        r'(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?'
                        r'\d?\d)){3}\Z)|(\A:(:[0-9a-f]{1,4}){1,5}:(25[0-5]|2[0-4]\d|'
                        r'[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\Z)')

local_ip_list = (
    ('127.0.0.0', 0xff000000),
    ('10.0.0.0', 0xff000000),
    ('192.168.0.0', 0xffff0000),
    ('172.16.0.0', 0xffff0000),
    ('172.17.0.0', 0xffff0000),
    ('172.18.0.0', 0xffff0000),
    ('172.19.0.0', 0xffff0000),
    ('172.20.0.0', 0xffff0000),
    ('172.21.0.0', 0xffff0000),
    ('172.22.0.0', 0xffff0000),
    ('172.23.0.0', 0xffff0000),
    ('172.24.0.0', 0xffff0000),
    ('172.25.0.0', 0xffff0000),
    ('172.26.0.0', 0xffff0000),
    ('172.27.0.0', 0xffff0000),
    ('172.28.0.0', 0xffff0000),
    ('172.29.0.0', 0xffff0000),
    ('172.30.0.0', 0xffff0000),
    ('172.31.0.0', 0xffff0000)
)

local_ip_mask_list = None

_cn_domain_list = {
    'localhost',
    '.cn',
    '.baidu.com',
    '.jd.com',
    '.360buyimg.com',
    '.taobao.com',
    '.alicdn.com',
    '.tanx.com',
    '.bdimg.com',
    '.qq.com'
}

_foreign_domain_list = {
    'google.com',
    'facebook.com',
    'twitter.com',
    'tumblr.com'
}

_hosts = {}

cn_domain_list = set()
foreign_domain_list = set()

cn_domain_file = None
cn_domain_update = 0
cn_domain_file_mod = 0

hosts_update_time = 0
hosts_file_mod = 0
hosts_file = '/etc/hosts'
if 'WINDIR' in os.environ:
    hosts_file = os.environ['WINDIR'] + '/system32/drivers/etc/hosts'

# rlock = threading.RLock()

dns_executor = MyThreadPoolExecutor(max_workers=os.cpu_count(), pool_name='DnsWorker', order_by_func=True)


async def update_apnic_latest(raise_on_fail=False, loop=None):
    from pyclda.aio_downloader import AioDownloader
    global apnic_file
    if apnic_file is None:
        apnic_file = lookup_conf_file(APNIC_LATEST)
    is_exist = os.path.isfile(apnic_file)
    expired_time = common.apnic_expired_days * 24 * 3600
    if not is_exist or (time.time() - os.stat(apnic_file).st_mtime) >= expired_time:
        # not found or expired
        tmp_filename = apnic_file + '.downloading'
        done, new_file, _ = await AioDownloader(common.apnic_latest_url, out_file=tmp_filename, loop=loop).download()
        if done:
            if is_exist:
                # backup old file
                bak_filename = '%s.%s' % (apnic_file, common.str_datetime(os.stat(apnic_file).st_mtime, fmt='%Y-%m-%d'))
                os.rename(apnic_file, bak_filename)
            os.rename(new_file, apnic_file)
            if not is_exist:
                apnic_file = lookup_conf_file(APNIC_LATEST)
            load_cn_list()
            return expired_time
        elif not is_exist and raise_on_fail:
            raise Exception('download %s from %s fail, pls check the log' % (APNIC_LATEST, common.apnic_latest_url))
        else:
            logger.warning('download %s from %s fail, pls check the log' % (APNIC_LATEST, common.apnic_latest_url))
            if is_exist:
                load_cn_list()
            return 60
    else:
        load_cn_list()
        return os.stat(apnic_file).st_mtime + expired_time - time.time()


def update_hosts():
    global hosts_update_time
    global hosts_file_mod
    global hosts_file

    if (time.time() - hosts_update_time) < 1:
        return
    hosts_update_time = time.time()

    try:
        mtime = os.stat(hosts_file).st_mtime
        if hosts_file_mod < mtime:
            # with rlock:
            if hosts_file_mod >= mtime:
                return
            _hosts.clear()
            with open(hosts_file, 'r') as f:
                for line in f.readlines():
                    line = line.strip()
                    parts = line.split()
                    if line.startswith("#"):
                        continue
                    if len(parts) >= 2:
                        ip = parts[0]
                        if is_ipv4(ip):
                            for i in range(1, len(parts)):
                                hostname = parts[i]
                                if hostname and hostname.startswith('#'):
                                    break
                                if hostname:
                                    _hosts[hostname] = ip
            hosts_file_mod = mtime
            logger.info('%s reloaded', hosts_file)
    except BaseException:
        hosts_update_time = time.time() + 10 * 60
        _hosts['localhost'] = '127.0.0.1'


def update_cn_domain():
    global cn_domain_update
    global cn_domain_file_mod
    global cn_domain_file
    global _cn_domain_list
    global cn_domain_list

    if (time.time() - cn_domain_update) < 1:
        return
    cn_domain_update = time.time()
    if cn_domain_file is None:
        cn_domain_file = lookup_conf_file('cn_domain.conf')
    try:
        mtime = os.stat(cn_domain_file).st_mtime
        if cn_domain_file_mod < mtime:
            # with rlock:
            if cn_domain_file_mod >= mtime:
                return
            cn_domain_list.clear()
            cn_domain_list.update(_cn_domain_list)
            foreign_domain_list.clear()
            foreign_domain_list.update(_foreign_domain_list)
            with open(cn_domain_file, 'r') as f:
                while True:
                    d = f.readline()
                    if not d:
                        break
                    d = d.strip()
                    if d and not d.startswith("#"):
                        if d.startswith("-"):
                            foreign_domain_list.add(d[1:])
                        else:
                            cn_domain_list.add(d)
            cn_domain_file_mod = mtime
            logger.info('%s reloaded', cn_domain_file)
    except FileNotFoundError:
        cn_domain_update = time.time() + 60
        logger.debug('china domain file: %s not found', cn_domain_file)


def is_foreign_domain(addr):
    update_cn_domain()
    for domain in foreign_domain_list:
        if addr.endswith(domain):
            logger.log(5, '%s match foreign_domain %s', addr, domain)