# -*- coding: utf-8 -*-
import socket
import struct
import random
import logging
import sys
import json
import time
import os
import io
try:
    from kodi_six import xbmc, xbmcaddon, xbmcvfs
except ImportError:
    import xbmc
    import xbmcaddon
    import xbmcvfs
PY2 = sys.version_info[0] == 2
ADDON_ = xbmcaddon.Addon()
TRANSLATE_ = xbmc.translatePath if PY2 else xbmcvfs.translatePath
try:
    from resources.lib.common import ensure_dir, debug_enabled, log_debug, log_exception, log
except Exception:
    debug_enabled = lambda addon=None: False
    log_debug = None
    log_exception = None
    log = None
    def ensure_dir(path):
        try:
            if path and not os.path.exists(path):
                os.makedirs(path)
        except Exception:
            pass
        return path


def _dns_debug(message):
    try:
        if log_debug is not None:
            log_debug(message, component='DNS')
    except Exception:
        pass


def _dns_exception(context, exc):
    try:
        if log_exception is not None:
            log_exception('DNS', context, exc)
            return
    except Exception:
        pass
    try:
        if log is not None:
            log('[DNS] {}: {}'.format(context, exc.__class__.__name__), level=getattr(xbmc, 'LOGWARNING', 2))
    except Exception:
        pass
profile = ensure_dir(TRANSLATE_(ADDON_.getAddonInfo('profile')))
CACHE_FILE = os.path.join(profile, 'dns_cache.json')
_CACHE_LOCK_SUFFIX = '.lock'
_CACHE_LOCK_STALE_SECS = 6.0
_CACHE_LOCK_WAIT_SECS = 0.42
_CACHE_REPLACE_ATTEMPTS = 5


_DNS_PATCH_STATE = {'installed': False, 'original_getaddrinfo': socket.getaddrinfo, 'resolver': None, 'instance': None}


class customdns:
    def __init__(self, cache_file=CACHE_FILE, cache_ttl=3600):
        self.dns_server = [
            '94.140.14.140', # adguard
            '94.140.14.141', # adguard
            '208.67.222.222',# OpenDNS
            '208.67.220.220',# OpenDNS
            '1.1.1.1',       # Cloudflare
            '8.8.8.8'        # Google DNS
        ]
        self.original_getaddrinfo = _DNS_PATCH_STATE.get('original_getaddrinfo', socket.getaddrinfo)
        self.cache_file = cache_file
        self.cache_ttl = cache_ttl  # Tempo de expiração em segundos
        self.cache = self._load_cache()
        self.debug_mode = bool(debug_enabled())
        self.mode_logger = self.debug_mode
        _dns_debug('Override DNS iniciado; servidores configurados={}.'.format(len(self.dns_server)))

        # Override DNS only once per runtime. Guardamos a função exata para
        # poder restaurar com segurança se o usuário desligar a opção depois.
        if not _DNS_PATCH_STATE.get('installed'):
            resolver = self._resolver
            _DNS_PATCH_STATE['original_getaddrinfo'] = socket.getaddrinfo
            _DNS_PATCH_STATE['resolver'] = resolver
            _DNS_PATCH_STATE['instance'] = self
            socket.getaddrinfo = resolver
            _DNS_PATCH_STATE['installed'] = True

    def _read_cache_file(self, report_error=False):
        """Lê somente entradas DNS ainda válidas sem tocar no resolvedor."""
        try:
            if not os.path.exists(self.cache_file):
                return {}
            with io.open(self.cache_file, 'r', encoding='utf-8') as f:
                cache = json.load(f)
            if not isinstance(cache, dict):
                return {}
            now = time.time()
            return {
                domain: data for domain, data in cache.items()
                if isinstance(data, dict) and float(data.get('expires', 0) or 0) > now
            }
        except Exception as exc:
            if report_error:
                _dns_exception('Erro ao carregar cache DNS', exc)
            return {}

    def _load_cache(self):
        """Carrega o cache DNS sem quebrar em arquivo parcial/corrompido."""
        return self._read_cache_file(report_error=True)

    def _acquire_cache_lock(self):
        """Lock curto via arquivo para serializar invokers Kodi independentes.

        Cada clique do plugin pode rodar em outro interpretador Python. Um lock
        de threading não cobre esse caso no Windows; O_EXCL cobre todos os
        invokers locais sem bloquear a navegação por mais de alguns centésimos.
        """
        lock_file = self.cache_file + _CACHE_LOCK_SUFFIX
        deadline = time.monotonic() + _CACHE_LOCK_WAIT_SECS
        while time.monotonic() < deadline:
            try:
                ensure_dir(os.path.dirname(lock_file))
                fd = os.open(lock_file, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
                try:
                    os.write(fd, str(os.getpid() if hasattr(os, 'getpid') else 0).encode('ascii'))
                finally:
                    os.close(fd)
                return lock_file
            except OSError:
                # Lock abandonado por crash/interrupção: remove apenas quando
                # comprovadamente antigo, sem tocar em escrita em andamento.
                try:
                    age = time.time() - os.path.getmtime(lock_file)
                    if age > _CACHE_LOCK_STALE_SECS:
                        os.remove(lock_file)
                        continue
                except Exception:
                    pass
                time.sleep(0.035 + random.random() * 0.020)
        return ''

    def _release_cache_lock(self, lock_file):
        try:
            if lock_file and os.path.exists(lock_file):
                os.remove(lock_file)
        except Exception:
            pass

    def _replace_cache_file(self, temp_file):
        """Troca atômica com pequenas tentativas contra lock momentâneo Windows."""
        last_exc = None
        for attempt in range(_CACHE_REPLACE_ATTEMPTS):
            try:
                if hasattr(os, 'replace'):
                    os.replace(temp_file, self.cache_file)
                else:
                    if os.path.exists(self.cache_file):
                        os.remove(self.cache_file)
                    os.rename(temp_file, self.cache_file)
                return True
            except OSError as exc:
                last_exc = exc
                if attempt + 1 < _CACHE_REPLACE_ATTEMPTS:
                    time.sleep(0.035 * (attempt + 1))
        if last_exc is not None:
            raise last_exc
        return False

    def _save_cache(self):
        """Salva cache DNS sem corrida entre invokers e sem travar rede.

        Se outro invoker estiver gravando ou o Windows mantiver o arquivo em
        uso por poucos milissegundos, a memória atual continua válida e a
        persistência é apenas adiada. DNS nunca deixa de responder por causa do
        arquivo de cache.
        """
        lock_file = ''
        temp_file = '{}.tmp.{}.{}.{}'.format(
            self.cache_file,
            os.getpid() if hasattr(os, 'getpid') else 0,
            int(time.time() * 1000),
            random.randint(0, 999999)
        )
        try:
            ensure_dir(os.path.dirname(self.cache_file))
            lock_file = self._acquire_cache_lock()
            if not lock_file:
                if self.mode_logger:
                    _dns_debug('Gravação do cache DNS adiada: outro invoker está atualizando o arquivo.')
                return False
            # Mescla conteúdo recém-gravado por outro invoker para não perder
            # domínios resolvidos em paralelo. A expiração maior vence.
            disk_cache = self._read_cache_file(report_error=False)
            merged = dict(disk_cache)
            for domain, data in (self.cache or {}).items():
                if not isinstance(data, dict):
                    continue
                previous = merged.get(domain) or {}
                if float(data.get('expires', 0) or 0) >= float(previous.get('expires', 0) or 0):
                    merged[domain] = data
            self.cache = merged
            with io.open(temp_file, 'w', encoding='utf-8') as f:
                json.dump(merged, f, ensure_ascii=False, separators=(',', ':'))
            self._replace_cache_file(temp_file)
            return True
        except Exception as exc:
            _dns_exception('Erro ao salvar cache DNS', exc)
            return False
        finally:
            try:
                if os.path.exists(temp_file):
                    os.remove(temp_file)
            except Exception:
                pass
            self._release_cache_lock(lock_file)

    def is_valid_ipv4(self, ip):
        try:
            socket.inet_aton(ip)
            return True
        except socket.error:
            return False

    def is_valid_ipv6(self, ip):
        try:
            if hasattr(socket, 'inet_pton'):
                socket.inet_pton(socket.AF_INET6, ip)
                return True
            return False
        except socket.error:
            return False

    def _build_dns_query(self, domain):
        transaction_id = random.randint(0, 65535)
        flags = 0x0100
        questions = 1
        header = struct.pack('>HHHHHH', transaction_id, flags, questions, 0, 0, 0)

        if PY2:
            qname = b''.join(chr(len(part)) + part for part in domain.split('.')) + b'\x00'
        else:
            qname = b''.join(bytes([len(part)]) + part.encode() for part in domain.split('.')) + b'\x00'

        qtype = 1  # A record
        qclass = 1  # IN
        question = qname + struct.pack('>HH', qtype, qclass)
        return header + question

    def _parse_dns_response(self, data):
        answer_count = struct.unpack(">H", data[6:8])[0]
        offset = 12
        while data[offset] != 0:
            offset += 1
        offset += 5  # null + qtype + qclass

        for _ in range(answer_count):
            offset += 2  # name (pointer)
            rtype, rclass, ttl, rdlength = struct.unpack(">HHIH", data[offset:offset+10])
            offset += 10
            if rtype == 1 and rdlength == 4:  # A record
                ip_parts = struct.unpack(">BBBB", data[offset:offset+4])
                return ".".join(map(str, ip_parts))
            offset += rdlength
        return None

    def resolve(self, domain, dns_custom):
        # Verifica o cache
        if domain in self.cache:
            if self.cache[domain]['expires'] > time.time():
                if self.mode_logger:
                    _dns_debug('Cache DNS utilizado para {}.'.format(domain))
                return self.cache[domain]['ip']
            else:
                # Remove entrada expirada
                del self.cache[domain]
                self._save_cache()

        try:
            domain_clean = domain.strip('.')
            if self.mode_logger:
                _dns_debug('Resolvendo {} via DNS alternativo.'.format(domain_clean))

            query = self._build_dns_query(domain_clean)

            if self.is_valid_ipv6(dns_custom):
                s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
                addr = (dns_custom, 53, 0, 0)
            else:
                s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                addr = (dns_custom, 53)

            s.settimeout(3)
            s.sendto(query, addr)
            data, _ = s.recvfrom(512)
            s.close()

            ip = self._parse_dns_response(data)
            if ip:
                # Salva no cache com timestamp de expiração
                self.cache[domain] = {
                    'ip': ip,
                    'expires': time.time() + self.cache_ttl
                }
                self._save_cache()
                if self.mode_logger:
                    _dns_debug('DNS alternativo respondeu para {}.'.format(domain))
                return ip
        except Exception as e:
            if self.mode_logger:
                _dns_exception('Erro ao resolver {} via DNS alternativo'.format(domain), e)
        return None

    def _resolver(self, host, port, *args, **kwargs):
        """Resolver DNS que respeita a intenção do socket chamador.

        Mantém o resolvedor nativo para IPv6 explícito, localhost, .local e
        tipos de socket não convencionais. Assim não interfere em descoberta
        local e APIs nativas em Android, iOS/tvOS, Windows, Linux e ARM.
        """
        try:
            family = args[0] if len(args) > 0 else kwargs.get('family', 0)
            socktype = args[1] if len(args) > 1 else kwargs.get('type', 0)
            proto = args[2] if len(args) > 2 else kwargs.get('proto', 0)
            flags = args[3] if len(args) > 3 else kwargs.get('flags', 0)
            host_text = host.decode('utf-8', 'ignore') if isinstance(host, bytes) else str(host or '').strip()
            lowered = host_text.lower().rstrip('.')
            ai_numeric = getattr(socket, 'AI_NUMERICHOST', 0)
            if (not host_text or lowered in ('localhost', 'localhost.localdomain') or
                    lowered.endswith('.local') or lowered.endswith('.lan') or
                    family == getattr(socket, 'AF_INET6', 10) or
                    (flags and ai_numeric and (flags & ai_numeric))):
                return self.original_getaddrinfo(host, port, *args, **kwargs)
            if self.is_valid_ipv4(host_text) or self.is_valid_ipv6(host_text):
                return self.original_getaddrinfo(host, port, *args, **kwargs)
            if socktype not in (0, socket.SOCK_STREAM, socket.SOCK_DGRAM):
                return self.original_getaddrinfo(host, port, *args, **kwargs)
            for dns_server in self.dns_server:
                ip = self.resolve(host_text, dns_server)
                if ip:
                    resolved_type = socktype or socket.SOCK_STREAM
                    resolved_proto = proto or (socket.IPPROTO_UDP if resolved_type == socket.SOCK_DGRAM else socket.IPPROTO_TCP)
                    return [(socket.AF_INET, resolved_type, resolved_proto, '', (ip, port))]
            if self.mode_logger:
                _dns_debug('DNS alternativo sem resposta para {}; usando resolvedor do sistema.'.format(host_text))
        except Exception as e:
            if self.mode_logger:
                _dns_exception('Erro no resolvedor alternativo', e)
        return self.original_getaddrinfo(host, port, *args, **kwargs)


def _override_enabled(addon=None):
    try:
        addon = addon or xbmcaddon.Addon()
        return (addon.getSetting('proxy_dns_override') or 'false').strip().lower() == 'true'
    except Exception:
        return False


def disable_customdns():
    """Restaura o resolvedor nativo somente se o OnePlay ainda for o dono.

    Não desfaz um monkeypatch diferente que tenha sido instalado depois por
    outro addon/processo durante a mesma sessão Kodi.
    """
    try:
        if not _DNS_PATCH_STATE.get('installed'):
            return False
        resolver = _DNS_PATCH_STATE.get('resolver')
        original = _DNS_PATCH_STATE.get('original_getaddrinfo')
        if resolver is not None and socket.getaddrinfo is resolver and original is not None:
            socket.getaddrinfo = original
        _DNS_PATCH_STATE['installed'] = False
        _DNS_PATCH_STATE['resolver'] = None
        _DNS_PATCH_STATE['instance'] = None
        return True
    except Exception:
        return False


def apply_configured_override(addon=None):
    """Aplica ou remove o DNS alternativo conforme a configuração explícita.

    Retorna True somente quando o override OnePlay permanece ativo. Com a
    opção desligada, a rede do Kodi segue usando o DNS normal do sistema.
    """
    if not _override_enabled(addon=addon):
        disable_customdns()
        _dns_debug('DNS alternativo desligado; resolvedor do sistema preservado.')
        return False
    try:
        if not _DNS_PATCH_STATE.get('installed'):
            customdns()
        active = bool(_DNS_PATCH_STATE.get('installed'))
        _dns_debug('DNS alternativo ativo={}.'.format('sim' if active else 'não'))
        return active
    except Exception as exc:
        _dns_exception('Falha ao aplicar DNS alternativo', exc)
        return False
