# -*- coding: utf-8 -*-
"""Resolvedor DNS alternativo conservador para o OnePlayVIP.

Baseado no dns.py enviado pelo usuário, mas adaptado para não instalar patch global
automaticamente no import. O patch só entra quando a configuração do addon permitir.
"""
import socket
import struct
import random
import logging
import sys
import json
import time
import os
import ssl
import http.client
import threading

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
profile = TRANSLATE_(ADDON_.getAddonInfo('profile'))
try:
    if not os.path.exists(profile):
        os.makedirs(profile)
except Exception:
    pass
CACHE_FILE = os.path.join(profile, 'dns_cache.json')

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

# RFC 8484 via POST. A conexão usa IP bootstrap, mas mantém SNI e validação do
# certificado pelo hostname; a URL do provedor nunca é trocada por IP.
_DOH_ENDPOINTS = (
    {
        'host': 'cloudflare-dns.com',
        'path': '/dns-query',
        'ips': ('1.1.1.1', '1.0.0.1'),
    },
    {
        'host': 'dns.google',
        'path': '/dns-query',
        'ips': ('8.8.8.8', '8.8.4.4'),
    },
)


def _safe_log(message, level=None):
    try:
        xbmc.log('[plugin.video.oneplayvip][dns] %s' % message, level if level is not None else xbmc.LOGDEBUG)
    except Exception:
        try:
            logging.debug(message)
        except Exception:
            pass


class customdns:
    def __init__(self, cache_file=CACHE_FILE, cache_ttl=3600, mode_logger=False):
        # DNS neutros primeiro; DNS filtrantes ficam no fim para reduzir falso bloqueio.
        self.dns_server = [
            '1.1.1.1',        # Cloudflare
            '1.0.0.1',        # Cloudflare
            '8.8.8.8',        # Google DNS
            '8.8.4.4',        # Google DNS
            '208.67.222.222', # OpenDNS
            '208.67.220.220', # OpenDNS
            '94.140.14.140',  # AdGuard
            '94.140.14.141',  # AdGuard
        ]
        self.original_getaddrinfo = _DNS_PATCH_STATE.get('original_getaddrinfo', socket.getaddrinfo)
        self.cache_file = cache_file
        self.cache_ttl = max(60, int(cache_ttl or 1800))
        self.query_timeout = 1.2
        self.max_servers_per_lookup = 4
        self.doh_timeout = 2.5
        self.doh_total_timeout = 3.5
        self.cache_max_size = 1000
        self.negative_ttl = 45
        self.negative_cache_max_size = 1000
        self.negative_cache = {}
        self._cache_lock = threading.RLock()
        self.cache = self._load_cache()
        self.mode_logger = bool(mode_logger)
        self.stats = {
            'cache_hits': 0,
            'udp_hits': 0,
            'doh_hits': 0,
            'system_fallbacks': 0,
        }

    def _load_cache(self):
        try:
            if os.path.exists(self.cache_file):
                with open(self.cache_file, 'r') as f:
                    cache = json.load(f)
                current_time = time.time()
                clean = {}
                for domain, data in cache.items():
                    try:
                        domain_clean = self._normalize_domain(domain)
                        ip = data.get('ip')
                        if domain_clean and self.is_valid_ipv4(ip) and float(data.get('expires', 0)) > current_time:
                            clean[domain_clean] = data
                    except Exception:
                        pass
                if len(clean) > self.cache_max_size:
                    clean = dict(sorted(
                        clean.items(),
                        key=lambda item: float((item[1] or {}).get('expires') or 0),
                        reverse=True,
                    )[:self.cache_max_size])
                return clean
        except Exception as e:
            _safe_log('Erro ao carregar cache DNS: %s' % e, xbmc.LOGDEBUG)
        return {}

    def _save_cache(self):
        try:
            folder = os.path.dirname(self.cache_file)
            if folder and not os.path.isdir(folder):
                os.makedirs(folder, exist_ok=True)
            tmp_path = self.cache_file + '.tmp'
            with self._cache_lock:
                with open(tmp_path, 'w') as f:
                    json.dump(self.cache, f, indent=2)
                os.replace(tmp_path, self.cache_file)
        except Exception as e:
            _safe_log('Erro ao salvar cache DNS: %s' % e, xbmc.LOGDEBUG)

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

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

    def _normalize_domain(self, domain):
        try:
            value = str(domain or '').strip().strip('.').lower()
            if not value or len(value) > 253 or value.startswith('.') or '..' in value:
                return ''
            labels = value.split('.')
            if any((not label) or len(label.encode('idna')) > 63 for label in labels):
                return ''
            return value
        except Exception:
            return ''

    def _build_dns_query(self, domain):
        transaction_id = random.randint(0, 65535)
        flags = 0x0100
        header = struct.pack('>HHHHHH', transaction_id, flags, 1, 0, 0, 0)
        labels = []
        for part in domain.split('.'):
            encoded = part.encode('idna')
            labels.append(bytes([len(encoded)]) + encoded)
        qname = b''.join(labels) + b'\x00'
        return transaction_id, header + qname + struct.pack('>HH', 1, 1)  # A / IN

    def _skip_name(self, data, offset):
        steps = 0
        while offset < len(data) and steps < 128:
            steps += 1
            length = data[offset]
            if length == 0:
                return offset + 1
            if (length & 0xC0) == 0xC0:
                return offset + 2
            if length & 0xC0:
                return None
            offset += 1 + length
        return None

    def _parse_dns_response_record(self, data, expected_transaction_id=None):
        try:
            if not data or len(data) < 12:
                return None
            transaction_id, flags, question_count, answer_count = struct.unpack('>HHHH', data[:8])
            if expected_transaction_id is not None and transaction_id != expected_transaction_id:
                return None
            # QR=1 e RCODE=0: somente resposta DNS válida/no error.
            if not (flags & 0x8000) or (flags & 0x000F):
                return None
            offset = 12
            for _ in range(question_count):
                offset = self._skip_name(data, offset)
                if offset is None or offset + 4 > len(data):
                    return None
                offset += 4
            for _ in range(answer_count):
                offset = self._skip_name(data, offset)
                if offset is None or offset + 10 > len(data):
                    return None
                rtype, rclass, ttl, rdlength = struct.unpack('>HHIH', data[offset:offset + 10])
                offset += 10
                if offset + rdlength > len(data):
                    return None
                if rtype == 1 and rclass == 1 and rdlength == 4:
                    ip = '.'.join(map(str, struct.unpack('>BBBB', data[offset:offset + 4])))
                    return ip, max(1, int(ttl or self.cache_ttl))
                offset += rdlength
        except Exception as e:
            if self.mode_logger:
                _safe_log('Erro ao interpretar resposta DNS: %s' % e, xbmc.LOGDEBUG)
        return None

    def _parse_dns_response(self, data, expected_transaction_id=None):
        record = self._parse_dns_response_record(data, expected_transaction_id)
        return record[0] if record else None

    def _store_cache(self, domain, ip, ttl=None, source='dns'):
        if not domain or not self.is_valid_ipv4(ip):
            return False
        try:
            ttl = int(ttl or self.cache_ttl)
        except Exception:
            ttl = self.cache_ttl
        ttl = max(60, min(self.cache_ttl, ttl))
        with self._cache_lock:
            if domain not in self.cache and len(self.cache) >= self.cache_max_size:
                try:
                    oldest = min(
                        self.cache.items(),
                        key=lambda item: float((item[1] or {}).get('expires') or 0)
                    )[0]
                    self.cache.pop(oldest, None)
                except Exception:
                    self.cache.clear()
            self.cache[domain] = {
                'ip': ip,
                'expires': time.time() + ttl,
                'source': str(source or 'dns')[:16],
            }
            self._save_cache()
        return True

    def _negative_cache_hit(self, domain):
        now = time.time()
        with self._cache_lock:
            expires = float(self.negative_cache.get(domain, 0) or 0)
            if expires > now:
                return True
            self.negative_cache.pop(domain, None)
        return False

    def _mark_negative(self, domain):
        if not domain:
            return
        now = time.time()
        with self._cache_lock:
            expired = [key for key, value in self.negative_cache.items() if float(value or 0) <= now]
            for key in expired:
                self.negative_cache.pop(key, None)
            if domain not in self.negative_cache and len(self.negative_cache) >= self.negative_cache_max_size:
                try:
                    oldest = min(self.negative_cache, key=self.negative_cache.get)
                    self.negative_cache.pop(oldest, None)
                except Exception:
                    self.negative_cache.clear()
            self.negative_cache[domain] = now + self.negative_ttl

    def _doh_post(self, endpoint, query, deadline=None):
        host = str((endpoint or {}).get('host') or '').strip()
        path = str((endpoint or {}).get('path') or '/dns-query').strip() or '/dns-query'
        ips = (endpoint or {}).get('ips') or ()
        if not host or not isinstance(query, bytes):
            return None
        request_head = (
            'POST {path} HTTP/1.1\r\n'
            'Host: {host}\r\n'
            'Accept: application/dns-message\r\n'
            'Content-Type: application/dns-message\r\n'
            'Content-Length: {length}\r\n'
            'User-Agent: OnePlayVIP-DNS/1.0\r\n'
            'Connection: close\r\n\r\n'
        ).format(path=path, host=host, length=len(query)).encode('ascii')

        if deadline is None:
            deadline = time.monotonic() + self.doh_total_timeout
        for ip in ips:
            raw_sock = None
            tls_sock = None
            try:
                remaining = deadline - time.monotonic()
                if remaining <= 0:
                    break
                timeout = max(0.2, min(self.doh_timeout, remaining))
                raw_sock = socket.create_connection((ip, 443), timeout=timeout)
                context = ssl.create_default_context()
                tls_sock = context.wrap_socket(raw_sock, server_hostname=host)
                raw_sock = None
                tls_sock.settimeout(max(0.2, min(self.doh_timeout, deadline - time.monotonic())))
                tls_sock.sendall(request_head + query)
                response = http.client.HTTPResponse(tls_sock)
                response.begin()
                if int(response.status or 0) != 200:
                    response.read(1024)
                    continue
                content_type = str(response.getheader('Content-Type') or '').lower()
                if 'application/dns-message' not in content_type:
                    response.read(1024)
                    continue
                payload = response.read(65536)
                if 12 <= len(payload) <= 65535:
                    return payload
            except Exception as exc:
                if self.mode_logger:
                    _safe_log('DoH indisponível via %s: %s' % (host, exc), xbmc.LOGDEBUG)
            finally:
                try:
                    if tls_sock is not None:
                        tls_sock.close()
                except Exception:
                    pass
                try:
                    if raw_sock is not None:
                        raw_sock.close()
                except Exception:
                    pass
        return None

    def _resolve_doh(self, domain):
        domain_clean = self._normalize_domain(domain)
        if not domain_clean:
            return None
        try:
            transaction_id, query = self._build_dns_query(domain_clean)
        except Exception:
            return None
        deadline = time.monotonic() + self.doh_total_timeout
        for endpoint in _DOH_ENDPOINTS:
            if time.monotonic() >= deadline:
                break
            payload = self._doh_post(endpoint, query, deadline=deadline)
            record = self._parse_dns_response_record(payload, transaction_id) if payload else None
            if record:
                ip, ttl = record
                self._store_cache(domain_clean, ip, ttl, source='doh')
                self.stats['doh_hits'] += 1
                if self.mode_logger:
                    _safe_log('DNS over HTTPS: %s resolvido com segurança.' % domain_clean, xbmc.LOGDEBUG)
                return ip
        return None

    def get_stats(self):
        with self._cache_lock:
            data = dict(self.stats)
            data['cache_size'] = len(self.cache)
            data['negative_cache_size'] = len([
                value for value in self.negative_cache.values() if value > time.time()
            ])
        return data

    def resolve(self, domain, dns_custom):
        domain_clean = self._normalize_domain(domain)
        if not domain_clean or domain_clean in ('localhost',):
            return None
        now = time.time()
        if self._negative_cache_hit(domain_clean):
            return None
        with self._cache_lock:
            if domain_clean in self.cache:
                try:
                    data = self.cache[domain_clean]
                    if float(data.get('expires', 0)) > now and data.get('ip'):
                        self.stats['cache_hits'] += 1
                        return data.get('ip')
                    del self.cache[domain_clean]
                except Exception:
                    pass
        s = None
        try:
            transaction_id, 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(self.query_timeout)
            s.sendto(query, addr)
            data, _addr = s.recvfrom(4096)
            record = self._parse_dns_response_record(data, expected_transaction_id=transaction_id)
            if record:
                ip, ttl = record
                self._store_cache(domain_clean, ip, ttl, source='udp')
                self.stats['udp_hits'] += 1
                if self.mode_logger:
                    _safe_log('DNS alternativo: %s -> %s via %s' % (domain_clean, ip, dns_custom), xbmc.LOGDEBUG)
                return ip
        except Exception as e:
            if self.mode_logger:
                _safe_log('Falha DNS %s via %s: %s' % (domain_clean, dns_custom, e), xbmc.LOGDEBUG)
        finally:
            try:
                if s:
                    s.close()
            except Exception:
                pass
        return None

    def _resolver(self, host, port, family=0, socktype=0, proto=0, flags=0):
        try:
            host_text = str(host or '').strip()
            if not host_text:
                return self.original_getaddrinfo(host, port, family, socktype, proto, flags)
            if self.is_valid_ipv4(host_text) or self.is_valid_ipv6(host_text):
                return self.original_getaddrinfo(host, port, family, socktype, proto, flags)
            if host_text.lower() in ('localhost',) or host_text.endswith('.local'):
                return self.original_getaddrinfo(host, port, family, socktype, proto, flags)
            # Para IPv6 explícito e Unix sockets, preserve a resolução do sistema.
            if family not in (0, getattr(socket, 'AF_UNSPEC', 0), socket.AF_INET):
                return self.original_getaddrinfo(host, port, family, socktype, proto, flags)
            for dns_server in self.dns_server[:self.max_servers_per_lookup]:
                ip = self.resolve(host_text, dns_server)
                if ip:
                    return [(socket.AF_INET, socktype or socket.SOCK_STREAM, proto or 6, '', (ip, port))]
            # Se DNS UDP estiver bloqueado/filtrado, tenta RFC 8484 com TLS e
            # certificado validados. A URL original continua com hostname/SNI.
            ip = self._resolve_doh(host_text)
            if ip:
                return [(socket.AF_INET, socktype or socket.SOCK_STREAM, proto or 6, '', (ip, port))]
            self._mark_negative(self._normalize_domain(host_text))
        except Exception as e:
            if self.mode_logger:
                _safe_log('Erro no resolvedor para %s: %s' % (host, e), xbmc.LOGDEBUG)
        self.stats['system_fallbacks'] += 1
        return self.original_getaddrinfo(host, port, family, socktype, proto, flags)



def install(cache_ttl=3600, mode_logger=False):
    """Instala o resolvedor alternativo no runtime Python atual."""
    try:
        if _DNS_PATCH_STATE.get('installed'):
            return True
        _DNS_PATCH_STATE['original_getaddrinfo'] = socket.getaddrinfo
        resolver = customdns(cache_file=CACHE_FILE, cache_ttl=cache_ttl, mode_logger=mode_logger)
        _DNS_PATCH_STATE['resolver'] = resolver
        socket.getaddrinfo = resolver._resolver
        _DNS_PATCH_STATE['installed'] = True
        if mode_logger:
            _safe_log('Resolvedor DNS alternativo ativado.', xbmc.LOGDEBUG)
        return True
    except Exception as e:
        _safe_log('Falha ao ativar resolvedor DNS alternativo: %s' % e, xbmc.LOGERROR)
        return False


def uninstall():
    """Restaura socket.getaddrinfo quando o recurso estiver desligado."""
    try:
        if _DNS_PATCH_STATE.get('installed'):
            socket.getaddrinfo = _DNS_PATCH_STATE.get('original_getaddrinfo', socket.getaddrinfo)
            _DNS_PATCH_STATE['installed'] = False
            _DNS_PATCH_STATE['resolver'] = None
        return True
    except Exception as e:
        _safe_log('Falha ao desativar resolvedor DNS alternativo: %s' % e, xbmc.LOGDEBUG)
        return False


def is_installed():
    return bool(_DNS_PATCH_STATE.get('installed'))
