"""
DNS管理 — CloudFlare + 阿里云 + DNSPod + GoDaddy
"""
import requests
import hashlib
import hmac
import time
import urllib.parse
import base64
import json
from typing import List, Optional, Callable

from core.models import DNSAccount, DomainInfo


class DNSProviderBase:
    def __init__(self, account: DNSAccount):
        self.account = account

    def get_domains(self) -> List[DomainInfo]:
        raise NotImplementedError

    def add_record(self, domain: str, record_type: str, name: str,
                   content: str, ttl: int = 120, priority: int = None) -> bool:
        raise NotImplementedError

    def update_record(self, domain: str, record_id: str, record_type: str,
                      name: str, content: str, ttl: int = 120, priority: int = None) -> bool:
        """更新已有记录 — 子类可覆盖"""
        raise NotImplementedError

    def delete_record(self, domain: str, record_id: str) -> bool:
        raise NotImplementedError

    def get_records(self, domain: str) -> list:
        raise NotImplementedError

    def delete_zone(self, domain: str) -> bool:
        raise NotImplementedError

    def _normalize_name(self, domain: str, name: str) -> str:
        """子类可覆盖: 记录名规范化 (CF 用完整域名, Aliyun/DNSPod 用 @ 等相对名)"""
        return name

    def upsert_record(self, domain: str, record_type: str, name: str,
                      content: str, ttl: int = 120, priority: int = None,
                      log_cb=None) -> str:
        """幂等添加/更新记录：先查询同name+type记录，已存在则更新，否则添加。
        TXT 记录按内容前缀(v=spf1/v=DKIM1/v=DMARC1)匹配, 避免误改站点验证等其他TXT;
        同名同前缀多条时更新第一条并删除其余重复。
        返回 'added' / 'updated' / 'skipped' / 'failed'"""
        try:
            full_name = self._normalize_name(domain, name)
            records = self.get_records(domain)
            key = None
            if record_type == 'TXT':
                key = (content or '').split(';', 1)[0].split(' ', 1)[0].strip().lower()
            matches = []
            for r in records:
                rtype = r.get('type', r.get('Type', ''))
                rname = r.get('name', r.get('RR', r.get('Name', '')))
                rcontent = r.get('content', r.get('Value', r.get('Data', '')))
                rid = r.get('id', r.get('RecordId', r.get('record_id', '')))
                if rtype != record_type:
                    continue
                if rname != full_name and rname != name:
                    continue
                if key:
                    rkey = (str(rcontent or '')).replace('"', '').split(';', 1)[0].split(' ', 1)[0].strip().lower()
                    if rkey != key:
                        continue
                matches.append({'id': str(rid) if rid else '', 'content': rcontent})
            norm = (content or '').replace('"', '')
            same = [m for m in matches if (str(m['content'] or '')).replace('"', '') == norm]
            if same:
                keep_id = same[0]['id']
                for m in matches:
                    if m['id'] and m['id'] != keep_id:
                        try:
                            self.delete_record(domain, m['id'])
                            if log_cb:
                                log_cb('info', f'[{domain}] {record_type} {name} 删除重复记录')
                        except Exception:
                            pass
                if log_cb:
                    log_cb('info', f'[{domain}] {record_type} {name} 已存在且内容一致，跳过')
                return 'skipped'
            updated = False
            if matches:
                first = matches[0]
                if first['id']:
                    try:
                        ok = self.update_record(domain, first['id'], record_type, full_name, content, ttl, priority)
                        updated = bool(ok)
                    except Exception as e:
                        if log_cb:
                            log_cb('error', f'[{domain}] 更新失败{e}，尝试删除后添加')
                        try:
                            self.delete_record(domain, first['id'])
                        except Exception:
                            pass
                for m in matches[1:]:
                    if m['id']:
                        try:
                            self.delete_record(domain, m['id'])
                            if log_cb:
                                log_cb('info', f'[{domain}] {record_type} {name} 删除重复记录')
                        except Exception:
                            pass
            if updated:
                if log_cb:
                    log_cb('info', f'[{domain}] {record_type} {name} 已更新')
                return 'updated'
            ok = self.add_record(domain, record_type, full_name, content, ttl, priority)
            if ok:
                if log_cb:
                    log_cb('info', f'[{domain}] {record_type} {name} 已添加')
                return 'added'
            return 'failed'
        except Exception as e:
            if log_cb:
                log_cb('error', f'[{domain}] upsert异常: {e}')
            return 'failed'


class CloudFlareDNS(DNSProviderBase):
    BASE = 'https://api.cloudflare.com/client/v4'

    def _normalize_name(self, domain: str, name: str) -> str:
        """CF API 要求完整域名: '@' -> 域本身, '_dmarc' -> '_dmarc.域名'"""
        name = (name or '').strip()
        if name == '@':
            return domain
        if name == domain or name.endswith('.' + domain):
            return name
        return name + '.' + domain

    def _auth_headers(self):
        """根据 account 字段自动识别认证方式:
        - Bearer Token 模式 (新, 用户用 API Token) — api_key 形如 "cfat_xxx" 或 40位 hex token
        - Global API Key 模式 (旧, 必须配合 api_email) — 用 X-Auth-Email + X-Auth-Key
        """
        api_key = (self.account.api_key or '').strip()
        api_email = (self.account.api_email or '').strip()
        # Cloudflare API Token 常见前缀: cfat_ / cf_ / cfut_; 或长度>=40 且无邮箱
        # Global API Key 是 37 位 hex (必须有邮箱)
        is_token = (api_key.startswith('cfat_') or api_key.startswith('cf_')
                    or api_key.startswith('cfut_') or len(api_key) >= 40
                    or not api_email)
        if is_token:
            return {
                'Authorization': 'Bearer ' + api_key,
                'Content-Type': 'application/json',
            }
        # Global Key (旧式) — 必须有 email 才能用
        if api_email and api_key:
            return {
                'X-Auth-Email': api_email,
                'X-Auth-Key': api_key,
                'Content-Type': 'application/json',
            }
        # fallback: 没邮箱时也用 Bearer, 万一是没前缀的 token
        return {
            'Authorization': 'Bearer ' + api_key,
            'Content-Type': 'application/json',
        }

    def _headers(self):
        # 兼容旧调用方(老方法名)
        return self._auth_headers()

    def get_domains(self) -> List[DomainInfo]:
        try:
            resp = requests.get(f'{self.BASE}/zones', headers=self._auth_headers(),
                               params={'per_page': 50}, timeout=30)
            data = resp.json()
        except Exception as _e:
            return []
        domains = []
        if data.get('success'):
            for zone in data.get('result', []):
                d = DomainInfo(
                    domain=zone['name'],
                    account_id=self.account.id,
                    provider='cloudflare',
                    zone_id=zone['id'],
                    record_count=0,
                    status='active' if zone.get('status') == 'active' else 'inactive'
                )
                domains.append(d)
        return domains

    def _get_zone_id(self, domain: str) -> str:
        try:
            resp = requests.get(f'{self.BASE}/zones', headers=self._auth_headers(),
                               params={'name': domain}, timeout=30)
            data = resp.json()
            if data.get('success') and data.get('result'):
                return data['result'][0]['id']
        except Exception:
            pass
        return ''

    def get_records(self, domain: str) -> list:
        zone_id = self._get_zone_id(domain)
        if not zone_id:
            return []
        all_records = []
        page = 1
        while True:
            resp = requests.get(f'{self.BASE}/zones/{zone_id}/dns_records',
                                headers=self._headers(),
                                params={'per_page': 100, 'page': page}, timeout=30)
            try:
                data = resp.json()
            except Exception:
                break
            if not data.get('success'):
                break
            all_records.extend(data.get('result', []))
            info = data.get('result_info', {})
            if page >= int(info.get('total_pages', 1) or 1):
                break
            page += 1
        return all_records

    @staticmethod
    def _format_content(record_type: str, content: str) -> str:
        """CF TXT 超过255字符必须按255分段并每段加双引号, 否则 API 报错"""
        if record_type == 'TXT' and content and len(content) > 255:
            parts = [content[i:i + 255] for i in range(0, len(content), 255)]
            return ''.join('"' + p + '"' for p in parts)
        return content

    def add_record(self, domain: str, record_type: str, name: str,
                   content: str, ttl: int = 120, priority: int = None) -> bool:
        zone_id = self._get_zone_id(domain)
        if not zone_id:
            return False
        payload = {'type': record_type, 'name': name,
                   'content': self._format_content(record_type, content), 'ttl': ttl}
        if priority is not None:
            payload['priority'] = priority
        resp = requests.post(f'{self.BASE}/zones/{zone_id}/dns_records',
                             headers=self._headers(), json=payload)
        return resp.json().get('success', False)

    def update_record(self, domain: str, record_id: str, record_type: str,
                      name: str, content: str, ttl: int = 120, priority: int = None) -> bool:
        zone_id = self._get_zone_id(domain)
        if not zone_id:
            return False
        payload = {'type': record_type, 'name': name,
                   'content': self._format_content(record_type, content), 'ttl': ttl}
        if priority is not None:
            payload['priority'] = priority
        resp = requests.put(f'{self.BASE}/zones/{zone_id}/dns_records/{record_id}',
                           headers=self._headers(), json=payload)
        return resp.json().get('success', False)

    def delete_record(self, domain: str, record_id: str) -> bool:
        zone_id = self._get_zone_id(domain)
        if not zone_id:
            return False
        resp = requests.delete(f'{self.BASE}/zones/{zone_id}/dns_records/{record_id}',
                              headers=self._headers())
        return resp.json().get('success', False)

    def delete_zone(self, domain: str) -> bool:
        zone_id = self._get_zone_id(domain)
        if not zone_id:
            return False
        resp = requests.delete(f'{self.BASE}/zones/{zone_id}', headers=self._headers())
        return resp.json().get('success', False)


class AliyunDNS(DNSProviderBase):
    BASE = 'https://alidns.aliyuncs.com/'

    def _sign(self, params: dict) -> str:
        sorted_params = sorted(params.items())
        query = '&'.join([f"{self._percent_encode(k)}={self._percent_encode(v)}" for k, v in sorted_params])
        string_to_sign = 'GET&' + self._percent_encode('/') + '&' + self._percent_encode(query)
        hmac_obj = hmac.new((self.account.access_key_secret + '&').encode(),
                           string_to_sign.encode(), hashlib.sha1)
        return base64.b64encode(hmac_obj.digest()).decode()

    def _percent_encode(self, s: str) -> str:
        return urllib.parse.quote(str(s), safe='-_.~')

    def _request(self, params: dict) -> dict:
        common = {
            'Format': 'JSON',
            'Version': '2015-01-09',
            'AccessKeyId': self.account.access_key_id,
            'SignatureMethod': 'HMAC-SHA1',
            'Timestamp': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
            'SignatureVersion': '1.0',
            'SignatureNonce': str(int(time.time() * 1000)),
        }
        common.update(params)
        common['Signature'] = self._sign(common)
        resp = requests.get(self.BASE, params=common)
        return resp.json()

    def get_domains(self) -> List[DomainInfo]:
        data = self._request({'Action': 'DescribeDomains'})
        domains = []
        for d in data.get('Domains', {}).get('Domain', []):
            domains.append(DomainInfo(
                domain=d['DomainName'],
                account_id=self.account.id,
                provider='aliyun',
                zone_id=d.get('DomainId', ''),
                status='active'
            ))
        return domains

    def add_record(self, domain: str, record_type: str, name: str,
                   content: str, ttl: int = 120, priority: int = None) -> bool:
        params = {
            'Action': 'AddDomainRecord',
            'DomainName': domain,
            'RR': name if name != '@' else '@',
            'Type': record_type,
            'Value': content,
            'TTL': ttl,
        }
        data = self._request(params)
        return 'RecordId' in data

    def update_record(self, domain: str, record_id: str, record_type: str,
                      name: str, content: str, ttl: int = 120, priority: int = None) -> bool:
        params = {
            'Action': 'UpdateDomainRecord',
            'RecordId': record_id,
            'RR': name if name != '@' else '@',
            'Type': record_type,
            'Value': content,
            'TTL': ttl,
        }
        data = self._request(params)
        return 'RecordId' in data

    def get_records(self, domain: str) -> list:
        data = self._request({'Action': 'DescribeDomainRecords', 'DomainName': domain})
        return data.get('DomainRecords', {}).get('Record', [])

    def delete_zone(self, domain: str) -> bool:
        return False


class DNSPodDNS(DNSProviderBase):
    BASE = 'https://dnsapi.cn/'

    def _params(self, extra: dict) -> dict:
        params = {
            'login_token': f'{self.account.api_email},{self.account.api_key}',
            'format': 'json',
        }
        params.update(extra)
        return params

    def get_domains(self) -> List[DomainInfo]:
        resp = requests.post(self.BASE + 'Domain.List', data=self._params({}))
        data = resp.json()
        domains = []
        for d in data.get('domains', []):
            domains.append(DomainInfo(
                domain=d['name'],
                account_id=self.account.id,
                provider='dnspod',
                zone_id=str(d.get('id', '')),
                status='active'
            ))
        return domains

    def add_record(self, domain: str, record_type: str, name: str,
                   content: str, ttl: int = 120, priority: int = None) -> bool:
        params = self._params({
            'domain': domain,
            'record_type': record_type,
            'sub_domain': name,
            'value': content,
            'ttl': ttl,
        })
        if record_type == 'MX' and priority is not None:
            params['mx'] = priority
        resp = requests.post(self.BASE + 'Record.Create', data=params)
        return resp.json().get('status', {}).get('code') == '1'

    def update_record(self, domain: str, record_id: str, record_type: str,
                      name: str, content: str, ttl: int = 120, priority: int = None) -> bool:
        params = self._params({
            'domain': domain,
            'record_id': record_id,
            'record_type': record_type,
            'sub_domain': name,
            'value': content,
            'ttl': ttl,
        })
        if record_type == 'MX' and priority is not None:
            params['mx'] = priority
        resp = requests.post(self.BASE + 'Record.Modify', data=params)
        return resp.json().get('status', {}).get('code') == '1'

    def delete_record(self, domain: str, record_id: str) -> bool:
        resp = requests.post(self.BASE + 'Record.Remove',
                           data=self._params({'domain': domain, 'record_id': record_id}))
        return resp.json().get('status', {}).get('code') == '1'

    def get_records(self, domain: str) -> list:
        resp = requests.post(self.BASE + 'Record.List',
                           data=self._params({'domain': domain}))
        return resp.json().get('records', [])

    def delete_zone(self, domain: str) -> bool:
        resp = requests.post(self.BASE + 'Domain.Remove',
                           data=self._params({'domain': domain}))
        return resp.json().get('status', {}).get('code') == '1'


class GoDaddyDNS(DNSProviderBase):
    BASE = 'https://api.godaddy.com/v1'

    def _headers(self):
        return {
            'Authorization': f'sso-key {self.account.api_email}:{self.account.api_key}',
            'Content-Type': 'application/json',
        }

    def get_domains(self) -> List[DomainInfo]:
        resp = requests.get(f'{self.BASE}/domains', headers=self._headers())
        domains = []
        if resp.status_code == 200:
            for d in resp.json():
                domains.append(DomainInfo(
                    domain=d.get('domain', ''),
                    account_id=self.account.id,
                    provider='godaddy',
                    zone_id=d.get('domainId', ''),
                    status='active'
                ))
        return domains

    def get_records(self, domain: str) -> list:
        resp = requests.get(f'{self.BASE}/domains/{domain}/records', headers=self._headers())
        if resp.status_code == 200:
            return resp.json()
        return []

    def add_record(self, domain: str, record_type: str, name: str,
                   content: str, ttl: int = 120, priority: int = None) -> bool:
        records = [{
            'type': record_type,
            'name': name,
            'data': content,
            'ttl': ttl,
        }]
        if priority is not None:
            records[0]['priority'] = priority
        resp = requests.patch(f'{self.BASE}/domains/{domain}/records',
                            headers=self._headers(), json=records)
        return resp.status_code in (200, 204)

    def update_record(self, domain: str, record_id: str, record_type: str,
                      name: str, content: str, ttl: int = 120, priority: int = None) -> bool:
        # GoDaddy 用 PATCH 同类型同名记录替换
        return self.add_record(domain, record_type, name, content, ttl, priority)

    def delete_record(self, domain: str, record_id: str) -> bool:
        resp = requests.delete(f'{self.BASE}/domains/{domain}/records',
                              headers=self._headers(),
                              params={'type': record_id.split(':')[0] if ':' in record_id else 'TXT',
                                     'name': record_id.split(':')[1] if ':' in record_id else '@'})
        return resp.status_code in (200, 204)

    def delete_zone(self, domain: str) -> bool:
        return False


def create_dns_provider(account: DNSAccount) -> Optional[DNSProviderBase]:
    providers = {
        'cloudflare': CloudFlareDNS,
        'aliyun': AliyunDNS,
        'dnspod': DNSPodDNS,
        'godaddy': GoDaddyDNS,
    }
    cls = providers.get(account.provider)
    if cls:
        return cls(account)
    return None


class DNSManager:
    """DNS统一管理器"""

    def __init__(self, log_callback: Callable[[str, str], None] = None):
        self.log_callback = log_callback
        self.accounts: List[DNSAccount] = []

    def log(self, level, msg):
        if self.log_callback:
            self.log_callback(level, msg)

    def add_account(self, account: DNSAccount):
        self.accounts.append(account)

    def get_all_domains(self) -> List[DomainInfo]:
        all_domains = []
        for account in self.accounts:
            if not account.enable:
                continue
            try:
                provider = create_dns_provider(account)
                if provider:
                    self.log('info', f'从 {account.provider} ({account.label}) 读取域名...')
                    domains = provider.get_domains()
                    all_domains.extend(domains)
                    self.log('success', f'{account.label}: {len(domains)}个域名')
            except Exception as e:
                self.log('error', f'{account.label} 读取失败: {e}')
        return all_domains

    def batch_configure(self, domains: List[DomainInfo], server_ip: str,
                        spf_policy: str = '~all', dmarc_policy: str = 'none',
                        dkim_record: str = None) -> dict:
        results = {'success': 0, 'fail': 0}
        for d in domains:
            account = next((a for a in self.accounts if a.id == d.account_id), None)
            if not account:
                continue
            try:
                provider = create_dns_provider(account)
                if not provider:
                    continue

                provider.upsert_record(d.domain, 'A', 'mail', server_ip, log_cb=self.log)
                provider.upsert_record(d.domain, 'MX', '@', f'mail.{d.domain}', priority=10, log_cb=self.log)
                provider.upsert_record(d.domain, 'TXT', '@', f'v=spf1 ip4:{server_ip} {spf_policy}', log_cb=self.log)
                provider.upsert_record(d.domain, 'TXT', '_dmarc', f'v=DMARC1; p={dmarc_policy}; sp={dmarc_policy};', log_cb=self.log)
                if dkim_record:
                    provider.upsert_record(d.domain, 'TXT', 'default._domainkey', dkim_record, log_cb=self.log)

                results['success'] += 1
                self.log('success', f'[{d.domain}] DNS配置成功')
            except Exception as e:
                results['fail'] += 1
                self.log('error', f'[{d.domain}] DNS配置失败: {e}')
        return results

    def batch_clean(self, domains: List[DomainInfo]) -> dict:
        results = {'success': 0, 'fail': 0}
        for d in domains:
            account = next((a for a in self.accounts if a.id == d.account_id), None)
            if not account:
                continue
            try:
                provider = create_dns_provider(account)
                if not provider:
                    continue
                records = provider.get_records(d.domain)
                for r in records:
                    rtype = r.get('type', r.get('Type', ''))
                    if rtype in ('MX', 'TXT') and ('spf' in str(r).lower() or 'dkim' in str(r).lower() or 'dmarc' in str(r).lower() or rtype == 'MX'):
                        rid = r.get('id', r.get('RecordId', ''))
                        if rid:
                            provider.delete_record(d.domain, str(rid))
                results['success'] += 1
                self.log('success', f'[{d.domain}] DNS清理成功')
            except Exception as e:
                results['fail'] += 1
                self.log('error', f'[{d.domain}] DNS清理失败: {e}')
        return results
