#!/usr/bin/env python3
"""Генератор ников для ботов — порт nickgen.html (логика 1:1).

Режимы:
  python nickgen.py --print 30 [--seed 42]   напечатать N ников
  python nickgen.py [--seed 42]              dry-run: показать, что изменится
  python nickgen.py --apply [--seed 42]      переписать имена в сидах:
      server/game/data/FakePlayerProgression.xml   (<character name="...">)
      server/db_installer/sql/spp_clan_seed.sql    (characters.char_name)

Соответствие: character id=N в XML <-> charId 270001000+N в SQL, имена ставятся
из одного списка, так что XML и БД остаются согласованы. После --apply ники в
работающей БД не изменятся (INSERT IGNORE) — нужен reset-bots.ps1 и пересборка
образа.
"""
import argparse
import random
import re
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent
XML_PATH = ROOT / 'server' / 'game' / 'data' / 'FakePlayerProgression.xml'
SQL_PATH = ROOT / 'server' / 'db_installer' / 'sql' / 'spp_clan_seed.sql'
CHAR_ID_BASE = 270001000

# --- Пулы слов (как в nickgen.html) ------------------------------------------
RU_ADJ = ['злой', 'добрый', 'старый', 'дикий', 'мелкий', 'жирный', 'хитрый', 'бешеный', 'мамкин', 'просто',
          'лысый', 'грозный', 'тихий', 'сонный', 'голодный', 'кривой', 'ржавый', 'бодрый', 'унылый', 'модный',
          'четкий', 'дерзкий', 'борзый', 'лютый', 'шустрый', 'реальный', 'угарный', 'наглый']
RU_NOUN = ['гном', 'пес', 'кот', 'волк', 'сухарь', 'травка', 'пельмень', 'колбаса', 'мармелад', 'чума',
           'ведьмак', 'сквозняк', 'холодок', 'бомж', 'топор', 'кабан', 'хомяк', 'огурец', 'батон', 'чеснок',
           'валенок', 'тапок', 'кефир', 'компот', 'барсук', 'ежик', 'мухомор', 'лопата', 'кирпич', 'шашлык',
           'беляш', 'танкист', 'дед', 'сосед', 'медоед', 'каратель', 'монах',
           'гопник', 'чебурек', 'шаурма', 'семки', 'пацан', 'браток', 'кент', 'батя', 'гараж', 'подъезд',
           'чушпан', 'задрот', 'нуб', 'читер', 'красавчик', 'нагибатор', 'убиватор', 'терминатор']
RU_NAME = ['олег', 'вася', 'дашка', 'ленка', 'федор', 'санек', 'толик', 'серега', 'витек', 'петруха',
           'колян', 'димон', 'настя', 'машка', 'гриша', 'степан', 'юрец', 'макс',
           'жека', 'вован', 'толян', 'саня', 'диман', 'леха', 'андрюха', 'кирюха', 'паша', 'стас',
           'руслан', 'тимур', 'натаха', 'светка', 'танюха', 'ирка', 'вовчик', 'петрович', 'палыч']
RU_SURNAME = ['пупкин', 'сидоров', 'петров', 'иванов', 'смирнов', 'кузнецов', 'козлов', 'волков', 'зайцев', 'медведев']
PREFIXES = ['MC', 'Dj', 'Mr']
EN_ADJ = ['Dark', 'Shadow', 'Storm', 'Night', 'Blood', 'Ghost', 'Crazy', 'Old', 'Swift', 'Silent',
          'Grim', 'Mad', 'Evil', 'Frost', 'Iron', 'Black', 'Wild', 'Lone', 'Sweet', 'True']
EN_NOUN = ['Slayer', 'Reaper', 'Hunter', 'Killer', 'Knight', 'Rogue', 'Monk', 'Rider', 'Arrow', 'Blade',
           'Master', 'Lord', 'King', 'Sniper', 'Mage', 'Angel', 'Demon', 'Raven', 'Wolf', 'Dragon',
           'Fire', 'Ice', 'Death', 'Star', 'Dream', 'Soul', 'Fang', 'Claw', 'Bishop', 'Titan']

# --- Транслит: кириллица -> латиница -----------------------------------------
TRANSLIT = {'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'e', 'ж': 'zh', 'з': 'z',
            'и': 'i', 'й': 'j', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r',
            'с': 's', 'т': 't', 'у': 'u', 'ф': 'f', 'х': 'h', 'ц': 'c', 'ч': 'ch', 'ш': 'sh', 'щ': 'sch',
            'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya'}

# --- Псевдокириллица: латинские двойники (KpucuK, TpaBka, IIeJIbMeHb) ---------
# None = нет похожего символа, берётся транслит буквы.
FAKE_CYR = {'а': 'a', 'б': '6', 'в': 'B', 'г': 'r', 'д': None, 'е': 'e', 'ё': 'e', 'ж': None, 'з': '3',
            'и': 'u', 'й': 'u', 'к': 'K', 'л': 'JI', 'м': 'M', 'н': 'H', 'о': 'o', 'п': 'II', 'р': 'p',
            'с': 'c', 'т': 'T', 'у': 'y', 'ф': None, 'х': 'x', 'ц': None, 'ч': '4', 'ш': 'LLI', 'щ': None,
            'ъ': '', 'ы': 'bI', 'ь': 'b', 'э': '3', 'ю': 'IO', 'я': '9I'}

# --- l33t для латинских слов (Ki4iga, OlegBI4) --------------------------------
LEET = {'e': '3', 'o': '0', 'a': '4', 'i': '1', 's': '5'}

VALID = re.compile(r'^[A-Za-z0-9]{3,16}$')
RU_RE = re.compile(r'[а-яё]')


class NickGen:
    def __init__(self, seed=None, translit=True, fake_cyr=True, leet=True, digits=True, wrap=True,
                 weights=(25, 55, 20)):
        self.r = random.Random(seed)
        self.translit = translit
        self.fake_cyr = fake_cyr
        self.leet = leet
        self.digits = digits
        self.wrap = wrap
        self.weights = weights

    def chance(self, pct):
        return self.r.randrange(100) < pct

    def pick(self, arr):
        return arr[self.r.randrange(len(arr))]

    def translit_word(self, w):
        out = ''
        for ch in w:
            t = TRANSLIT.get(ch, ch)
            if ch == 'й' and self.chance(50):
                t = 'y'
            if ch == 'в' and self.chance(15):
                t = 'w'  # Wowan-стиль
            if ch == 'х':
                t = 'x' if self.chance(40) else ('kh' if self.chance(30) else 'h')  # Xyligan-стиль
            out += t
        return out[:1].upper() + out[1:]

    def fake_cyr_word(self, w):
        out = ''
        for ch in w:
            f = FAKE_CYR.get(ch, ch)
            out += f if f is not None else TRANSLIT.get(ch, ch)
        return out

    def leet_word(self, w):
        out, used = '', 0
        for ch in w:
            l = LEET.get(ch.lower())
            if l and used < 2 and self.chance(45):
                out += l
                used += 1
            else:
                out += ch
        return out

    def build_parts(self, lang, n):
        """-> список пар (слово, is_surname)."""
        w = lambda x: (x, False)
        s = lambda x: (x, True)
        if lang == 'ru':
            if n == 1:
                v = self.r.randrange(100)
                if v < 45:
                    return [w(self.pick(RU_NOUN))]
                if v < 80:
                    return [w(self.pick(RU_NAME))]
                return [s(self.pick(RU_SURNAME))]
            if n == 2:
                v = self.r.randrange(4)
                if v == 0:
                    return [w(self.pick(RU_ADJ)), w(self.pick(RU_NOUN))]
                if v == 1:
                    return [w(self.pick(RU_ADJ)), w(self.pick(RU_NAME))]
                if v == 2:
                    return [w(self.pick(RU_NOUN)), w(self.pick(RU_NOUN))]
                return [w(self.pick(RU_NAME)), s(self.pick(RU_SURNAME))]  # VasyaPupkin
            if self.chance(50):
                return [w(self.pick(RU_ADJ)), w(self.pick(RU_NOUN)), w(self.pick(RU_NOUN))]
            return [w(self.pick(RU_ADJ)), w(self.pick(RU_NAME)), s(self.pick(RU_SURNAME))]
        if n == 1:
            return [w(self.pick(EN_NOUN))]
        if n == 2:
            return [w(self.pick(EN_ADJ)), w(self.pick(EN_NOUN))]
        first = self.pick(EN_ADJ)
        second = self.pick([x for x in EN_ADJ if x != first]) if self.chance(50) else self.pick(EN_NOUN)
        return [w(first), w(second), w(self.pick(EN_NOUN))]

    def generate_one(self):
        total = max(sum(self.weights), 1)
        roll = self.r.randrange(total)
        n = 1 if roll < self.weights[0] else 2 if roll < self.weights[0] + self.weights[1] else 3

        lang = 'ru' if self.chance(70) else 'en'
        parts = self.build_parts(lang, n)
        # редкая смесь языков в 2-3-частных никах
        if n > 1 and self.chance(15):
            parts[-1] = (self.pick(EN_NOUN), False) if lang == 'ru' else (self.translit_word(self.pick(RU_NOUN)), False)

        modes = ([['translit'], []][not self.translit]) + ([['fakeCyr'], []][not self.fake_cyr])
        ru_mode = self.pick(modes) if modes else 'translit'

        rendered = []
        for w, is_surname in parts:
            if RU_RE.search(w):
                if ru_mode == 'fakeCyr':
                    rendered.append(self.fake_cyr_word(w))
                else:
                    t = self.translit_word(w)
                    # фамилии на -ov: SmirnOFF / Smirnoff
                    if is_surname and t.lower().endswith('ov') and self.chance(50):
                        t = t[:-2] + ('OFF' if self.chance(40) else 'off')
                    rendered.append(t)
            else:
                rendered.append(self.leet_word(w) if self.leet and self.chance(20) else w)
        nick = ''.join(rendered)

        # декорации (стиль сида: dashka, ZloyPES, HolodOK, OldRogue38, MCVovan95, xXDarkSlayerXx)
        if ru_mode == 'translit':
            if self.chance(12):
                nick = nick.lower()
            elif self.chance(12) and len(nick) > 4:
                k = 2 + self.r.randrange(2)
                nick = nick[:-k] + nick[-k:].upper()
        if self.digits and self.chance(28) and len(nick) <= 14:
            v = self.r.randrange(100)
            if v < 45:
                suf = str(1 + self.r.randrange(9)) if self.chance(50) else str(10 + self.r.randrange(90))
            elif v < 80:
                suf = '%02d' % ((85 + self.r.randrange(23)) % 100)  # год рождения 85..07
            else:
                suf = str(1985 + self.r.randrange(23))              # 1985..2007
            if len(nick) + len(suf) <= 16:
                nick += suf
        if self.chance(4) and len(nick) <= 13:
            nick += 'RUS'
        prefixed = False
        if self.chance(6) and len(nick) <= 14:
            nick = self.pick(PREFIXES) + nick
            prefixed = True
        if self.wrap and not prefixed and self.chance(4) and len(nick) <= 12:
            nick = 'xX' + nick + 'Xx'
        return nick

    def batch(self, count, taken=None):
        taken = taken if taken is not None else set()
        out = []
        guard = count * 200
        while len(out) < count and guard > 0:
            guard -= 1
            nick = self.generate_one()
            if VALID.match(nick) and nick.lower() not in taken:
                taken.add(nick.lower())
                out.append(nick)
        if len(out) < count:
            raise RuntimeError(f'Не хватило уникальных имён: {len(out)}/{count}. Расширь пулы слов.')
        return out


# --- Перезапись сидов ---------------------------------------------------------
XML_RE = re.compile(r'(<character id="(\d+)" name=")([^"]*)(")')
SQL_RE = re.compile(r"(VALUES \((270\d{6}), 'sppbots', ')([^']*)(')")


def rewrite(xml_path, sql_path, names_by_id, apply_changes):
    xml_text = xml_path.read_text(encoding='utf-8')
    sql_text = sql_path.read_text(encoding='utf-8')

    def xml_sub(m):
        return m.group(1) + names_by_id[int(m.group(2))] + m.group(4)

    def sql_sub(m):
        return m.group(1) + names_by_id[int(m.group(2)) - CHAR_ID_BASE] + m.group(4)

    new_xml, n_xml = XML_RE.subn(xml_sub, xml_text)
    new_sql, n_sql = SQL_RE.subn(sql_sub, sql_text)
    if n_xml != len(names_by_id) or n_sql != len(names_by_id):
        raise RuntimeError(f'Несогласованность: XML={n_xml}, SQL={n_sql}, имён={len(names_by_id)}')
    if apply_changes:
        xml_path.write_text(new_xml, encoding='utf-8', newline='')
        sql_path.write_text(new_sql, encoding='utf-8', newline='')
    return n_xml, n_sql


def main():
    p = argparse.ArgumentParser(description='Генератор ников для ботов')
    p.add_argument('--print', dest='print_n', type=int, metavar='N', help='напечатать N ников и выйти')
    p.add_argument('--apply', action='store_true', help='переписать имена в XML и SQL (по умолчанию dry-run)')
    p.add_argument('--seed', type=int, help='сид для воспроизводимости (по умолчанию случайный)')
    p.add_argument('--xml', type=Path, default=XML_PATH)
    p.add_argument('--sql', type=Path, default=SQL_PATH)
    a = p.parse_args()

    seed = a.seed if a.seed is not None else random.randrange(1 << 31)
    gen = NickGen(seed=seed)
    print(f'seed = {seed}')

    if a.print_n:
        for nick in gen.batch(a.print_n):
            print(nick)
        return

    ids = [int(m.group(2)) for m in XML_RE.finditer(a.xml.read_text(encoding='utf-8'))]
    names = gen.batch(len(ids))
    names_by_id = dict(zip(ids, names))
    n_xml, n_sql = rewrite(a.xml, a.sql, names_by_id, a.apply)
    sample = ', '.join(names[:10])
    if a.apply:
        print(f'Переписано имён: XML={n_xml}, SQL={n_sql}. Первые: {sample}')
        print('В работающей БД имена не изменятся (INSERT IGNORE) — нужен reset-bots.ps1 и пересборка образа.')
    else:
        print(f'Dry-run: заменил бы XML={n_xml}, SQL={n_sql} имён. Первые: {sample}')
        print('Запусти с --apply чтобы записать.')


if __name__ == '__main__':
    sys.exit(main())
