#!/usr/bin/env python3

import tldextract
import urllib.request
import re
from pathlib import Path
import json
import os
import shutil
import subprocess
import sys

rusDomainsInsideOut='Russia/inside'
rusDomainsInsideSrcSingle='src/Russia-domains-inside-single.lst'
rusDomainsInsideCategories='Categories'
rusDomainsInsideServices='Services'
rusDomainsOutsideSrc='src/Russia-domains-outside.lst'
rusDomainsOutsideOut='Russia/outside'
uaDomainsSrc='src/Ukraine-domains-inside.lst'
uaDomainsOut='Ukraine/inside'
SUBNET_SERVICES = [
    'discord', 'meta', 'twitter', 'telegram',
    'cloudflare', 'hetzner', 'ovh', 'digitalocean',
    'cloudfront', 'roblox', 'google_meet',
]
ExcludeServices = {"telegram.lst", "cloudflare.lst", "google_ai.lst", "google_play.lst", 'hetzner.lst', 'ovh.lst', 'digitalocean.lst', 'cloudfront.lst', 'hodca.lst', 'roblox.lst', 'google_meet.lst'}

def collect_files(src):
    files = []
    for dir_path in src:
        path = Path(dir_path)
        if path.is_dir():
            files.extend(f for f in path.glob('*') if f.name not in ExcludeServices)
        elif path.is_file() and path.name not in ExcludeServices:
            files.append(path)
    return files

def collect_domains(src, dot_prefix=True):
    domains = set()
    for f in collect_files(src):
        if not f.is_file():
            continue
        with open(f) as infile:
            for line in infile:
                ext = tldextract.extract(line.rstrip())
                if not ext.suffix:
                    continue
                if re.search(r'[^а-я\-]', ext.domain):
                    domains.add(ext.fqdn)
                elif not ext.domain:
                    prefix = '.' if dot_prefix else ''
                    domains.add(prefix + ext.suffix)
    return domains

def raw(src, out):
    domains = sorted(collect_domains(src))

    with open(f'{out}-raw.lst', 'w') as file:
        for name in domains:
            file.write(f'{name}\n')

def dnsmasq(src, out, remove={'google.com'}):
    domains = sorted(collect_domains(src) - remove)

    with open(f'{out}-dnsmasq-nfset.lst', 'w') as file:
        for name in domains:
            file.write(f'nftset=/{name}/4#inet#fw4#vpn_domains\n')

    with open(f'{out}-dnsmasq-ipset.lst', 'w') as file:
        for name in domains:
            file.write(f'ipset=/{name}/vpn_domains\n')

def clashx(src, out, remove={'google.com'}):
    domains = sorted(collect_domains(src) - remove)

    with open(f'{out}-clashx.lst', 'w') as file:
        for name in domains:
            file.write(f'DOMAIN-SUFFIX,{name}\n')

def kvas(src, out, remove={'google.com'}):
    domains = sorted(collect_domains(src, dot_prefix=False) - remove)

    with open(f'{out}-kvas.lst', 'w') as file:
        for name in domains:
            file.write(f'{name}\n')

def mikrotik_fwd(src, out, remove={'google.com'}):
    domains = sorted(collect_domains(src) - remove)

    with open(f'{out}-mikrotik-fwd.lst', 'w') as file:
        for name in domains:
            if name.startswith('.'):
                file.write(f'/ip dns static add name=*.{name[1:]} type=FWD address-list=allow-domains forward-to=localhost\n')
            else:
                file.write(f'/ip dns static add name={name} type=FWD address-list=allow-domains match-subdomain=yes forward-to=localhost\n')

def lines_from_file(filepath):
    if not os.path.exists(filepath):
        print(f"Warning: input file not found: {filepath}", file=sys.stderr)
        return []
    with open(filepath, 'r', encoding='utf-8') as f:
        return [line.strip() for line in f if line.strip()]

def compile_mrs(domains, name, mrs_dir='MRS', behavior='domain'):
    os.makedirs(mrs_dir, exist_ok=True)

    txt_path = os.path.join(mrs_dir, f"{name}.txt")
    mrs_path = os.path.join(mrs_dir, f"{name}.mrs")

    with open(txt_path, 'w', encoding='utf-8') as f:
        for d in domains:
            f.write(f"{d}\n")

    try:
        subprocess.run(
            ["mihomo", "convert-ruleset", behavior, "text", txt_path, mrs_path], check=True
        )
        print(f"Compiled: {mrs_path}")
    except subprocess.CalledProcessError as e:
        print(f"Compile error {txt_path}: {e}")
        sys.exit(1)

def compile_srs(data, name, json_dir='JSON', srs_dir='SRS'):
    os.makedirs(json_dir, exist_ok=True)
    os.makedirs(srs_dir, exist_ok=True)

    json_path = os.path.join(json_dir, f"{name}.json")
    srs_path = os.path.join(srs_dir, f"{name}.srs")

    with open(json_path, 'w', encoding='utf-8') as f:
        json.dump(data, f, indent=4)

    try:
        subprocess.run(
            ["sing-box", "rule-set", "compile", json_path, "-o", srs_path], check=True
        )
        print(f"Compiled: {srs_path}")
    except subprocess.CalledProcessError as e:
        print(f"Compile error {json_path}: {e}")
        sys.exit(1)

def srs_rule(name, rules):
    compile_srs({"version": 3, "rules": rules}, name)

def generate_srs_for_categories(directories):
    exclude = {"meta", "twitter", "discord", "telegram", "hetzner", "ovh", "digitalocean", "cloudfront", "roblox", "google_meet"}

    for directory in directories:
        for filename in os.listdir(directory):
            if any(keyword in filename for keyword in exclude):
                continue
            file_path = os.path.join(directory, filename)
            if os.path.isfile(file_path):
                domains = lines_from_file(file_path)
                name = os.path.splitext(filename)[0]
                srs_rule(name, [{"domain_suffix": domains}])


def prepare_dat_domains(domains, output_name, dirs=None):
    output_lists_directory = 'geosite_data'
    os.makedirs(output_lists_directory, exist_ok=True)

    domain_attrs = {domain: [] for domain in domains}

    for directory in (dirs or []):
        if not os.path.isdir(directory):
            continue
        for filename in os.listdir(directory):
            file_path = os.path.join(directory, filename)
            if not os.path.isfile(file_path):
                continue

            attribute = os.path.splitext(filename)[0].replace('_', '-')

            with open(file_path, 'r', encoding='utf-8') as f:
                for line in f:
                    domain = line.strip()
                    if not domain:
                        continue
                    if domain in domain_attrs:
                        domain_attrs[domain].append(f" @{attribute}")

    output_file_path = os.path.join(output_lists_directory, output_name)
    with open(output_file_path, 'w', encoding='utf-8') as out_f:
        for domain, attrs in domain_attrs.items():
            line = domain + "".join(attrs)
            out_f.write(f"{line}\n")

def prepare_dat_combined(dirs):
    output_lists_directory = 'geosite_data'
    os.makedirs(output_lists_directory, exist_ok=True)

    for directory in dirs:
        if not os.path.isdir(directory):
            continue

        for filename in os.listdir(directory):
            source_path = os.path.join(directory, filename)
            if not os.path.isfile(source_path):
                continue

            new_name = os.path.splitext(filename)[0].replace('_', '-')
            destination_path = os.path.join(output_lists_directory, new_name)

            shutil.copyfile(source_path, destination_path)
 
def parse_geosite_line(line):
    from proto import geosite_pb2

    parts = line.split()
    raw_domain = parts[0]
    attrs = [p.lstrip('@') for p in parts[1:] if p.startswith('@')]

    if raw_domain.startswith('full:'):
        domain_type = geosite_pb2.Domain.Full
        value = raw_domain[5:]
    elif raw_domain.startswith('regexp:'):
        domain_type = geosite_pb2.Domain.Regex
        value = raw_domain[7:]
    elif raw_domain.startswith('keyword:'):
        domain_type = geosite_pb2.Domain.Plain
        value = raw_domain[8:]
    else:
        domain_type = geosite_pb2.Domain.RootDomain
        value = raw_domain.lstrip('.')

    return domain_type, value, attrs

def generate_dat_domains(data_path='geosite_data', output_name='geosite.dat', output_directory='DAT'):
    from proto import geosite_pb2

    os.makedirs(output_directory, exist_ok=True)

    geo_site_list = geosite_pb2.GeoSiteList()

    for filename in sorted(os.listdir(data_path)):
        file_path = os.path.join(data_path, filename)
        if not os.path.isfile(file_path):
            continue

        geo_site = geo_site_list.entry.add()
        geo_site.country_code = filename.upper()

        with open(file_path, 'r', encoding='utf-8') as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith('#'):
                    continue

                domain_type, value, attrs = parse_geosite_line(line)

                domain = geo_site.domain.add()
                domain.type = domain_type
                domain.value = value

                for attr in attrs:
                    attribute = domain.attribute.add()
                    attribute.key = attr
                    attribute.bool_value = True

    output_path = os.path.join(output_directory, output_name)
    with open(output_path, 'wb') as f:
        f.write(geo_site_list.SerializeToString())

    print(f"Compiled .dat file: {output_path}")

if __name__ == '__main__':
    # Russia inside
    Path("Russia").mkdir(parents=True, exist_ok=True)

    removeDomains = {'google.com', 'googletagmanager.com', 'github.com', 'githubusercontent.com', 'githubcopilot.com', 'microsoft.com', 'cloudflare-dns.com', 'parsec.app' }
    removeDomainsMikrotik = {'google.com', 'googletagmanager.com', 'github.com', 'githubusercontent.com', 'githubcopilot.com', 'microsoft.com', 'cloudflare-dns.com', 'parsec.app', 'showip.net' }
    removeDomainsKvas = {'google.com', 'googletagmanager.com', 'github.com', 'githubusercontent.com', 'githubcopilot.com', 'microsoft.com', 'cloudflare-dns.com', 'parsec.app', 't.co', 'ua' }
    
    inside_lists = [rusDomainsInsideCategories, rusDomainsInsideServices]

    raw(inside_lists, rusDomainsInsideOut)
    dnsmasq(inside_lists, rusDomainsInsideOut, removeDomains)
    clashx(inside_lists, rusDomainsInsideOut, removeDomains)
    kvas(inside_lists, rusDomainsInsideOut, removeDomainsKvas)
    mikrotik_fwd(inside_lists, rusDomainsInsideOut, removeDomainsMikrotik)

    # Russia outside
    outside_lists = [rusDomainsOutsideSrc]

    raw(outside_lists, rusDomainsOutsideOut)
    dnsmasq(outside_lists, rusDomainsOutsideOut)
    clashx(outside_lists, rusDomainsOutsideOut)
    kvas(outside_lists, rusDomainsOutsideOut)
    mikrotik_fwd(outside_lists, rusDomainsOutsideOut)

    # Ukraine
    Path("Ukraine").mkdir(parents=True, exist_ok=True)

    with urllib.request.urlopen("https://raw.githubusercontent.com/uablacklist/uablacklist.net/master/gen/scripts/domains.json") as resp:
        uablacklist_data = json.loads(resp.read().decode())
    with open("uablacklist-domains.lst", 'w') as f:
        for domain in uablacklist_data.keys():
            f.write(f"{domain}\n")

    urllib.request.urlretrieve("https://raw.githubusercontent.com/zhovner/zaborona_help/master/config/domainsdb.txt", "zaboronahelp-domains.lst")

    ua_lists = ['uablacklist-domains.lst', 'zaboronahelp-domains.lst', uaDomainsSrc]
    
    raw(ua_lists, uaDomainsOut)
    dnsmasq(ua_lists, uaDomainsOut)
    clashx(ua_lists, uaDomainsOut)
    kvas(ua_lists, uaDomainsOut)
    mikrotik_fwd(ua_lists, uaDomainsOut)

    for temp_file in ['uablacklist-domains.lst', 'zaboronahelp-domains.lst']:
        Path(temp_file).unlink()

    # Sing-box ruleset main
    russia_inside = lines_from_file('Russia/inside-raw.lst')
    russia_outside = lines_from_file('Russia/outside-raw.lst')
    ukraine_inside = lines_from_file('Ukraine/inside-raw.lst')
    srs_rule('russia_inside', [{"domain_suffix": russia_inside}])
    srs_rule('russia_outside', [{"domain_suffix": russia_outside}])
    srs_rule('ukraine_inside', [{"domain_suffix": ukraine_inside}])

    # Sing-box categories
    directories = ['Categories', 'Services']
    generate_srs_for_categories(directories)

    # Sing-box subnets + domains
    for service in SUBNET_SERVICES:
        if service == 'discord':
            continue
        subnets = lines_from_file(f'Subnets/IPv4/{service}.lst')
        domains = lines_from_file(f'Services/{service}.lst')
        srs_rule(service, [{"domain_suffix": domains, "ip_cidr": subnets}])

    # Discord (domains + UDP subnets on high ports)
    discord_subnets = lines_from_file('Subnets/IPv4/discord.lst')
    discord_domains = lines_from_file('Services/discord.lst')
    discord_cf_subnets = ["104.16.0.0/12"]
    srs_rule('discord', [
        {"domain_suffix": discord_domains},
        {"network": ["udp"], "ip_cidr": discord_subnets, "port_range": ["50000:65535"]},
        {"network": ["udp"], "ip_cidr": discord_cf_subnets, "port_range": ["19000:20000"]},
    ])

    # Mihomo main
    to_mrs = lambda domains: [f'+.{d.lstrip(".")}' for d in domains]
    mrs_russia_inside = to_mrs(russia_inside)
    mrs_russia_outside = to_mrs(russia_outside)
    mrs_ukraine_inside = to_mrs(ukraine_inside)
    compile_mrs(mrs_russia_inside, 'russia_inside_domain')
    compile_mrs(mrs_russia_outside, 'russia_outside_domain')
    compile_mrs(mrs_ukraine_inside, 'ukraine_inside_domain')

    # Mihomo categories
    for directory in ['Categories', 'Services']:
        for filename in os.listdir(directory):
            file_path = os.path.join(directory, filename)
            if os.path.isfile(file_path):
                domains = to_mrs(lines_from_file(file_path))
                name = os.path.splitext(filename)[0]
                compile_mrs(domains, f'{name}_domain')

    # Mihomo subnets
    for service in SUBNET_SERVICES:
        subnets = lines_from_file(f'Subnets/IPv4/{service}.lst')
        compile_mrs(subnets, f'{service}_ipcidr', behavior='ipcidr')

    # Xray domains
    prepare_dat_domains(russia_inside, 'russia-inside', directories)
    prepare_dat_domains(russia_outside, 'russia-outside')
    prepare_dat_domains(ukraine_inside, 'ukraine-inside')
    prepare_dat_combined(directories)
    generate_dat_domains()
