# -*- coding: utf-8 -*-
"""DokoGen — автоматический подбор угроз безопасности информации (236 приказ).

Логика программы: по характеристикам объекта КИИ (тип, сеть, нарушитель)
подбирает актуальные угрозы из перечня БДУ ФСТЭК (threats_fstec.json).
Если перечень не скачан — использует базовый threats.json.

Правила эвристические; при наличии методического документа (например,
«Методика оценки угроз безопасности информации» ФСТЭК) — адаптируются под него.
"""

import os
import json
import re


def _ubi_num(text):
    """Код УБИ из строки → int: «УБИ.069 Название» → 69. None, если кода нет."""
    m = re.search(r'уби\.?\s*(\d+)', str(text), re.IGNORECASE)
    return int(m.group(1)) if m else None


def sort_threats_by_ubi(items):
    """Сортировка угроз по возрастанию кода УБИ (УБИ.001, УБИ.002, …).

    Строки без кода УБИ уходят в конец (по алфавиту).
    """
    def _key(t):
        n = _ubi_num(t)
        return (0, n) if n is not None else (1, str(t).lower())
    return sorted(items, key=_key)


def load_threats(dict_dir=None):
    """Список угроз: сначала скачанный с ФСТЭК (с кодами УБИ), иначе базовый."""
    if dict_dir is None:
        dict_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dictionaries')
    fstec = os.path.join(dict_dir, 'threats_fstec.json')
    if os.path.exists(fstec):
        try:
            with open(fstec, encoding='utf-8') as f:
                data = json.load(f)
                if data:
                    return data
        except Exception:
            pass
    base = os.path.join(dict_dir, 'threats.json')
    try:
        with open(base, encoding='utf-8') as f:
            return json.load(f)
    except Exception:
        return []


# ---------------------------------------------------------------
# Ключевые слова (в названии угрозы) по признакам объекта
# ---------------------------------------------------------------
# Базовый набор — актуален для любого объекта КИИ
BASE_KEYWORDS = [
    'вредоносн', 'несанкционированн', 'утечк', 'перехват', 'модификац',
    'уничтожен', 'отказ в обслуживании', 'нсд', 'биос', 'пароль',
    'криптографическ', 'шифрован', 'аутентификац',
    # базовые угрозы локальных сетей / клиент-серверных архитектур
    'доступ', 'данн', 'систем', 'конфигурац', 'целостност', 'информац',
    'программн', 'пользовател', 'авторизац', 'сеанс', 'журнал', 'реестр',
    'файл', 'память', 'буфер', 'ресурс', 'сервер', 'клиент', 'учётн',
    'учетн', 'идентификац', 'привилег', 'средств', 'сетев', 'локальн',
]

# Технологии, которые по умолчанию НЕ используются на объекте.
# Если в характеристиках объекта есть явные упоминания — группа возвращается.
TECH_GROUPS = {
    'виртуализац': ['виртуал', 'гипервизор', 'контейнер'],
    'облако': ['облачн', 'облак'],
    'мобильн': ['мобильн', 'смартфон', 'планшет'],
    'грид': ['грид', 'суперкомпьютер'],
    'большие данные': ['больших данных', 'хранилищ'],
    'беспроводн': ['беспроводн', 'wi-fi', 'wifi'],
    'smart-карты': ['smart-карт', 'смарт-карт'],
    'машинное обучение': ['машинного обучения', 'искусственного интеллекта', 'нейросет'],
}

# Объект имеет подключение к сети Интернет / сеть электросвязи
INTERNET_KEYWORDS = [
    'удалённ', 'удаленн', 'сетев', 'фишинг', 'ddos', 'отказ в обслуживании',
    'веб', 'электронн', 'почт', 'беспроводн', 'wi-fi', 'wifi', 'vpn',
    'маршрутизац', 'протокол', 'интернет',
]

# Объект изолирован (нет подключения к Интернету)
ISOLATED_KEYWORDS = [
    'съёмн', 'съемн', 'физическ', 'локальн', 'инсайд', 'носител',
    'биос', 'желез', 'аппаратн',
]

# АСУ / технологический процесс
ASU_KEYWORDS = [
    'технологическ', 'асутп', 'промышленн', 'scada', 'датчик',
    'исполнительн', 'технологическ', 'производств', 'процесс',
]

# ИТКС / сети связи
NETWORK_KEYWORDS = [
    'сеть', 'канал', 'протокол', 'маршрутизац', 'связ', 'передач',
    'коммутац', 'трафик',
]

# Внутренний нарушитель
INSIDER_KEYWORDS = [
    'инсайд', 'сотрудник', 'привилегированн', 'администратор',
    'внутренн', 'персонал',
]

# Внешний нарушитель
EXTERNAL_KEYWORDS = [
    'внешн', 'удалённ', 'удаленн', 'дистанционн',
]


def _is_asu(isys) -> bool:
    t = (getattr(isys, 'object_type', '') or '').lower()
    return 'асу' in t or 'автоматизированн' in t or 'технологическ' in t


def _is_network(isys) -> bool:
    t = (getattr(isys, 'object_type', '') or '').lower()
    return 'телекоммуникац' in t or 'сеть' in t


def _has_internet(isys) -> bool:
    """Есть ли подключение к сети Интернет / внешним сетям."""
    if getattr(isys, 'is_internet', False):
        return True
    desc = ((getattr(isys, 'description', '') or '') + ' ').lower()
    operator = (getattr(isys, 'net_operator', '') or '').lower()
    net_category = (getattr(isys, 'net_category', '') or '').lower()

    # Явные признаки ОТСУТСТВИЯ внешней сети
    if 'отсутств' in operator:                      # оператор «Отсутствует»
        return False
    if 'отсутств' in desc and ('интернет' in desc or 'подключ' in desc):
        return False
    if 'не требуется' in desc and ('доменн' in desc or 'интернет' in desc):
        return False
    if 'изолирован' in desc:
        return False

    # Сеть общего пользования — это и есть внешняя сеть (Интернет)
    if 'общего пользован' in net_category:
        return True

    # Указан реальный оператор/провайдер — внешняя связь есть
    if operator and operator not in ('нет', 'отсутствует', 'не требуется', '-'):
        return True

    # Консервативно: без явных признаков внешней сети считаем, что её нет
    return False


def _is_insider(isys) -> bool:
    a = (getattr(isys, 'attacker_category', '') or '').lower()
    return 'внутрен' in a


def _score(threat_lower, keywords):
    """Сколько ключевых слов встретилось в названии угрозы."""
    return sum(1 for kw in keywords if kw in threat_lower)


def suggest_threats_program(isys, threats=None, limit=None):
    """Подбор угроз по характеристикам объекта КИИ (инвертированная логика).

    По умолчанию актуальны ВСЕ угрозы из перечня (БДУ ФСТЭК).
    Исключаются только заведомо неактуальные для данного объекта:
      - нет сети Интернет → убираем угрозы удалённого доступа, веб, почты,
        беспроводных сетей, DDoS, облачных и т.п.;
      - объект типа АСУ → убираем угрозы BIOS, грид-систем, виртуализации
        (у технологического оборудования их, как правило, нет).
    Такой подход даёт полный список, а не «15 самых похожих».

    Возвращает список строк (с кодами УБИ, если они есть в перечне).
    """
    if threats is None:
        threats = load_threats()
    if not threats:
        return []

    has_internet = _has_internet(isys)
    is_asu = _is_asu(isys)
    is_network = _is_network(isys)

    # --- Технологии, которые явно НЕ используются на объекте ---
    # Если в характеристиках объекта (описание, архитектура, ПО, сфера) нет
    # упоминаний технологии — угрозы этой группы исключаем.
    tech_context = ' '.join([
        str(getattr(isys, 'description', '') or ''),
        str(getattr(isys, 'architecture', '') or ''),
        str(getattr(isys, 'hw_sw', '') or ''),
        str(getattr(isys, 'os_sw', '') or ''),
        str(getattr(isys, 'app_sw', '') or ''),
        str(getattr(isys, 'object_type', '') or ''),
        str(getattr(isys, 'sphere', '') or ''),
        str(getattr(isys, 'net_category', '') or ''),
        str(getattr(isys, 'typical_object', '') or ''),
    ]).lower()

    # --- Слова-исключения (если встретились в названии угрозы — она неактуальна) ---
    exclude = []

    if not has_internet:
        # Объект изолирован: угрозы, реализуемые только через внешние сети
        exclude += [
            'удалённ', 'удаленн', 'дистанционн', 'веб', 'электронн', 'почт',
            'фишинг', 'беспроводн', 'wi-fi', 'wifi', 'интернет', 'ddos', 'vpn',
            'облачн', 'грид', 'внешн',
        ]

    if is_asu:
        # АСУ: нет BIOS/виртуализации/браузера в привычном понимании
        exclude += ['биос', 'грид', 'гипервизор', 'виртуальн', 'браузер']
    else:
        # Не АСУ — исключаем угрозы АСУ ТП / технологических процессов
        exclude += ['технологическ', 'датчик', 'исполнительн', 'асутп', 'scada',
                    'промышленн', 'контроллер', 'числовым программным управлением',
                    'программируемых логических']

    # Технологии, не упомянутые в характеристиках объекта → исключаем
    for tech_name, kws in TECH_GROUPS.items():
        if any(kw in tech_context for kw in kws):
            continue  # технология используется — угрозы оставляем
        # Технология не упомянута — исключаем её угрозы
        for kw in kws:
            exclude.append(kw)

    if not is_network and not is_asu:
        # Обычная ИС: неактуальны чисто сетевые/телеком-угрозы без указания
        # — оставляем всё, кроме специфики каналов связи, если их нет
        pass

    # --- Положительные ключевые слова: чем больше совпало — тем угроза актуальнее ---
    pos = list(BASE_KEYWORDS)
    if has_internet:
        pos += INTERNET_KEYWORDS
    else:
        pos += ISOLATED_KEYWORDS
    if is_asu:
        pos += ASU_KEYWORDS
    if is_network:
        pos += NETWORK_KEYWORDS
    if _is_insider(isys):
        pos += INSIDER_KEYWORDS
    else:
        pos += EXTERNAL_KEYWORDS
    # Убираем дубликаты
    pos = list(dict.fromkeys(pos))

    scored = []
    for t in threats:
        tl = str(t).lower()
        if any(kw in tl for kw in exclude):
            continue
        sc = sum(1 for kw in pos if kw in tl)
        scored.append((sc, t))

    # Сортируем по возрастанию кода УБИ (УБИ.001 → УБИ.211…)
    scored = [t for sc, t in scored if sc > 0]
    result = sort_threats_by_ubi(scored)

    if limit and len(result) > limit:
        result = result[:limit]
    return result


# ---------------------------------------------------------------
# Автозаполнение вкладки «Безопасность» по данным объекта (логическое)
# ---------------------------------------------------------------

# Ключевые слова: угроза → тип инцидента (словарь incidents.json)
INCIDENT_RULES = [
    ('отказ в обслуживании', 'отказ в обслуживании'),
    ('несанкционированн', 'несанкционированный доступ'),
    ('утечк', 'утечка данных (нарушение конфиденциальности)'),
    ('модификац', 'модификация (подмена) данных'),
    ('подмен', 'модификация (подмена) данных'),
    ('функционирован', 'нарушение функционирования технических средств'),
    ('техническ', 'нарушение функционирования технических средств'),
    ('использован', 'несанкционированное использование вычислительных ресурсов объекта'),
]

# Базовый набор орг. мер (всегда актуальны для значимого объекта)
BASE_ORG_MEASURES = [
    'Установление контролируемой зоны',
    'Контроль физического доступа к объекту',
    'Разработка документов (регламентов, инструкций, руководств) по обеспечению безопасности объекта',
    'Назначение ответственных за обеспечение безопасности объекта',
    'Обучение и инструктаж персонала',
    'Регламентация действий при компьютерных инцидентах',
    'Утверждение перечня лиц, имеющих доступ к объекту',
]

# Базовый набор тех. мер — только универсальные, не требующие конкретных СЗИ.
# Меры, зависящие от средств защиты (антивирус, СЗИ от НСД, регистрация событий
# и т.п.), добавляются отдельно по факту наличия СЗИ (DEFENSE_TOOL_MEASURES).
BASE_TECH_MEASURES = [
    'Идентификация и аутентификация пользователей',
    'Управление доступом',
    'Ограничение программной среды',
    'Резервное копирование и восстановление',
    'Защита технических средств и линий связи',
]

# Средства защиты → доп. тех. меры (по факту наличия СЗИ у объекта)
DEFENSE_TOOL_MEASURES = [
    ('межсетев', 'Межсетевое экранирование'),
    ('firewall', 'Межсетевое экранирование'),
    ('сетев', 'Межсетевое экранирование'),
    ('обнаруж', 'Обнаружение вторжений (СОВ)'),
    ('ids', 'Обнаружение вторжений (СОВ)'),
    ('ips', 'Обнаружение вторжений (СОВ)'),
    ('криптограф', 'Криптографическая защита информации (СКЗИ)'),
    ('vipnet', 'Криптографическая защита информации (СКЗИ)'),
    ('континент', 'Криптографическая защита информации (СКЗИ)'),
    ('виртуализ', 'Защита среды виртуализации'),
    ('vmware', 'Защита среды виртуализации'),
    ('hyper-v', 'Защита среды виртуализации'),
    ('антивирус', 'Антивирусная защита'),
    ('kaspersky', 'Антивирусная защита'),
    ('eset', 'Антивирусная защита'),
    ('dr.web', 'Антивирусная защита'),
    ('аваст', 'Антивирусная защита'),
    ('avast', 'Антивирусная защита'),
    ('сзи', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
    ('от нсд', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
    ('dallas', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
    ('secret net', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
    ('аккорд', 'Защита информации от несанкционированного доступа (СЗИ от НСД)'),
    ('siem', 'Регистрация и мониторинг событий безопасности'),
    ('dlp', 'Регистрация и мониторинг событий безопасности'),
    ('мониторинг', 'Регистрация и мониторинг событий безопасности'),
    ('регистрац', 'Регистрация и мониторинг событий безопасности'),
    ('доверенная загрузка', 'Контроль (доверенная) загрузки'),
    ('резервн', 'Резервное копирование и восстановление'),
]


def auto_fill_security(isys, threats=None):
    """Логическое автозаполнение вкладки «Безопасность» по данным объекта КИИ.

    Заполняет (только если поле пустое):
      attacker_category — по наличию интернета (внешний/внутренний нарушитель);
      attacker_level — Н1/Н2 (внутренний — базовые, внешний — базовые повышенные);
      attacker_types — виды нарушителей по категории/интернету;
      threats — подбор программой (скоринг по характеристикам);
      incidents — типы инцидентов, выводимые из подобранных угроз;
      org_measures / tech_measures — базовый набор + по СЗИ объекта.

    Возвращает dict {поле: новое_значение} только для пустых полей.
    """
    from .models import InformationSystem
    res = {}

    has_internet = _has_internet(isys)

    # 1. Категория нарушителя
    if not (getattr(isys, 'attacker_category', '') or '').strip():
        res['attacker_category'] = 'Внешний нарушитель' if has_internet else 'Внутренний нарушитель'

    # 2. Уровень возможностей
    if not (getattr(isys, 'attacker_level', '') or '').strip():
        res['attacker_level'] = 'Н2 — Базовые повышенные возможности' if has_internet else 'Н1 — Базовые возможности'

    # 3. Виды нарушителей
    if not (getattr(isys, 'attacker_types', '') or '').strip():
        if has_internet:
            types = [
                'Отдельные физические лица (хакеры)',
                'Преступные группы (криминальные структуры)',
                'Конкурирующие организации',
                'Авторизованные пользователи систем и сетей',
                'Системные администраторы и администраторы безопасности',
                'Бывшие (уволенные) работники (пользователи)',
            ]
        else:
            types = [
                'Авторизованные пользователи систем и сетей',
                'Системные администраторы и администраторы безопасности',
                'Бывшие (уволенные) работники (пользователи)',
                'Лица, обеспечивающие функционирование систем и сетей или обеспечивающих систем оператора (администрация, охрана, уборщики и др.)',
            ]
        res['attacker_types'] = '\n'.join(types)

    # 4. Угрозы — подбор программой
    if not (getattr(isys, 'threats', '') or '').strip():
        prog = suggest_threats_program(isys, threats=threats)
        if prog:
            res['threats'] = '\n'.join(prog)

    # 5. Инциденты — из подобранных угроз
    if not (getattr(isys, 'incidents', '') or '').strip():
        threats_text = res.get('threats') or (getattr(isys, 'threats', '') or '')
        if threats_text:
            incidents = []
            low = threats_text.lower()
            for kw, incident in INCIDENT_RULES:
                if kw in low and incident not in incidents:
                    incidents.append(incident)
            if incidents:
                res['incidents'] = '\n'.join(incidents)

    # 6. Орг. меры — базовый набор
    if not (getattr(isys, 'org_measures', '') or '').strip():
        res['org_measures'] = '\n'.join(BASE_ORG_MEASURES)

    # 7. Тех. меры — базовые + по СЗИ объекта.
    #    Только если объекту присвоена категория значимости (1/2/3):
    #    для незначимого объекта технические меры не требуются.
    if not (getattr(isys, 'tech_measures', '') or '').strip():
        cat = (getattr(isys, 'category', '') or '').strip()
        has_category = cat in ('1', '2', '3')
        if has_category:
            tech = list(BASE_TECH_MEASURES)
            defense = ' '.join(getattr(isys, 'defense_tools_list', []) or [])
            dlow = defense.lower()
            for kw, measure in DEFENSE_TOOL_MEASURES:
                if kw in dlow and measure not in tech:
                    tech.append(measure)
            res['tech_measures'] = '\n'.join(tech)
        # else: категория не присвоена — технические меры не заполняем

    return res


# ---------------------------------------------------------------
# Подбор типового отраслевого объекта КИИ по характеристикам объекта
# ---------------------------------------------------------------

# Стоп-слова: не несут смысла при сопоставлении названия объекта и типового объекта
STOPWORDS = {
    'информационные', 'информационно', 'телекоммуникационные', 'автоматизированные',
    'системы', 'система', 'сети', 'сеть', 'обеспечивающие', 'обеспечивающих',
    'предназначенные', 'предназначенных', 'управление', 'контроль', 'мониторинг',
    'объекта', 'объектов', 'объект', 'объекты', 'кии', 'рф', 'российской',
    'федерации', 'исполнительной', 'власти', 'органов', 'организаций', 'организации',
    'подведомственных', 'функций', 'функции', 'осуществление', 'полномочий',
    'обеспечение', 'процессов', 'процесса', 'процессы', 'данных', 'информации',
    'сбор', 'хранение', 'обработка', 'представление', 'использование', 'передача',
    'автоматизации', 'средств', 'программных', 'программно', 'аппаратных',
    'технических', 'деятельность', 'деятельности', 'осуществляющих',
    'размещенные', 'размещенных', 'расположенных', 'используемых', 'используемые',
    'проведении', 'проведения', 'выполнении', 'выполнения', 'реализация', 'реализации',
    'электронных', 'электронного', 'информационных', 'информационного',
    # Общие словоформы, встречающиеся почти у всех типовых объектов:
    'контроль', 'контроля', 'контролю', 'контролем', 'контроле',
    'управление', 'управления', 'управлению', 'управлением', 'управлении',
    'безопасность', 'безопасности', 'безопасностью', 'безопасном',
    'обеспечение', 'обеспечения', 'обеспечению', 'обеспечением',
    'предназначен', 'предназначена', 'предназначено', 'предназначены',
    'предоставление', 'предоставления', 'предоставлению',
    'взаимодействие', 'взаимодействия', 'взаимодействию',
    'функционирование', 'функционирования', 'функционированию',
    'деятельность', 'деятельности', 'деятельностью',
    'автоматизирован', 'автоматизированной', 'автоматизированных',
    'телекоммуникацион', 'телекоммуникационной', 'телекоммуникационных',
    'информационн', 'информационной', 'информационных', 'информационное',
    'осуществление', 'осуществления', 'осуществлению',
    'для', 'систем', 'системы', 'система', 'ресурсов', 'ресурсы',
    'целью', 'цели', 'помощи', 'помощь', 'также', 'или', 'иных', 'иное',
    'при', 'на', 'по', 'в', 'с', 'и', 'не', 'то', 'числе',
}


def _tokenize(text):
    """Значимые слова текста (без стоп-слов)."""
    import re as _re
    words = _re.findall(r'[а-яёa-z0-9]+', (text or '').lower())
    return [w for w in words if len(w) > 2 and w not in STOPWORDS]


def _load_all_typical(dict_dir=None):
    """Все типовые отраслевые объекты из typical_industry_objects.json (по сферам)."""
    import os as _os
    if dict_dir is None:
        dict_dir = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), 'dictionaries')
    path = _os.path.join(dict_dir, 'typical_industry_objects.json')
    try:
        with open(path, encoding='utf-8') as f:
            data = json.load(f)
    except Exception:
        return []
    out = []
    for sphere, items in data.items():
        if sphere == 'Иная сфера деятельности':
            continue
        for it in items:
            out.append({'sphere': sphere, 'num': it.get('num', ''),
                        'name': it.get('name', '')})
    return out


_IDF_CACHE = None


def _idf_weights(cands):
    """IDF-веса слов: чем реже слово в перечне типовых объектов, тем больше вес.

    Общие слова («информационные», «управление», «контроль») встречаются почти
    у всех 392 объектов и почти ничего не значат; редкие («доступом», «СКУД»,
    «сигнализации») — сильно различают объекты.
    """
    global _IDF_CACHE
    if _IDF_CACHE is not None:
        return _IDF_CACHE
    import math
    df = {}
    for c in cands:
        for w in set(_tokenize(c['name'])):
            df[w] = df.get(w, 0) + 1
    n = max(len(cands), 1)
    weights = {w: math.log((n + 1) / (df.get(w, 0) + 1)) + 1.0 for w in df}
    _IDF_CACHE = weights
    return weights


def suggest_typical_object_program(isys, limit=10, sphere=None):
    """Подбор типового отраслевого объекта КИИ по характеристикам объекта.

    Логика: значимые слова из названия/назначения/описания объекта КИИ
    сопоставляются со значимыми словами названий типовых объектов
    (перечень Распоряжения ПРФ № 360-р, 392 объекта). Скоринг по IDF:
    редкие слова (например, «доступом», «сигнализации») весят больше общих.
    Пример: объект «СКУД» / «контроль и управление доступом» →
    «ИС, АСУ, ИТКС, обеспечивающие контроль и управление доступом на территорию».

    Возвращает список dict: {sphere, num, name, score} — по убыванию релевантности.
    """
    text = ' '.join([
        getattr(isys, 'name', '') or '',
        getattr(isys, 'purpose', '') or '',
        getattr(isys, 'description', '') or '',
    ])
    obj_words = set(_tokenize(text))
    if not obj_words:
        return []

    # Фильтр по сфере (важно: не подбирать ОПК-объекту сферу из здравоохранения):
    # 1) сфера, переданная явно (выбрана в комбобоксе);
    # 2) сфера, уже заполненная у объекта (isys.sphere);
    # 3) автодетект по названию/назначению/описанию;
    # 4) иначе — все сферы (подбор без фильтра).
    used_sphere = sphere
    if not used_sphere:
        used_sphere = (getattr(isys, 'sphere', '') or '').strip() or None
    if not used_sphere:
        used_sphere = detect_sphere(isys)

    cands = _load_all_typical()
    if used_sphere:
        cands = [c for c in cands if c['sphere'] == used_sphere]
        if not cands:
            # Сфера определена, но объектов нет — не расширяем на другие сферы,
            # возвращаем пусто (оператор выберет сферу вручную)
            return []

    weights = _idf_weights(cands)

    scored = []
    for c in cands:
        tw = set(_tokenize(c['name']))
        common = tw & obj_words
        if common:
            score = sum(weights.get(w, 1.0) for w in common)
            scored.append({'sphere': c['sphere'], 'num': c['num'],
                           'name': c['name'], 'score': round(score, 2),
                           'matched': sorted(common)})

    scored.sort(key=lambda x: (-x['score'], x['name']))
    return scored[:limit]


# ---------------------------------------------------------------
# Автоопределение сферы деятельности объекта по ключевым словам
# ---------------------------------------------------------------
SPHERE_KEYWORDS = [
    ('Здравоохранение', ['медицин', 'здрав', 'поликлиник', 'больниц', 'лечеб', 'клиник', 'фармацевт', 'аптек', 'пациент', 'врач', 'диагностик', 'лабораторн', 'медпомощ']),
    ('Наука', ['научн', 'исследован', 'разработк', 'лаборатор', 'академи', 'институт', 'ниокр', 'наука']),
    ('Транспорт', ['транспорт', 'перевоз', 'автомобил', 'железнодорожн', 'авиа', 'морск', 'речн', 'метрополитен', 'вокзал', 'логистик', 'движени']),
    ('Связь', ['связ', 'телекоммуникац', 'оператор связи', 'интернет-провайдер', 'передач данных', 'сети связи', 'телефон']),
    ('Энергетика', ['энерг', 'электро', 'теплоснабж', 'электроснабж', 'генерац', 'подстанц', 'котельн', 'энергосистем']),
    ('Государственная регистрация прав', ['регистрац', 'кадастр', 'недвижим', 'реестр']),
    ('Банковская сфера и финансовый рынок', ['банк', 'финанс', 'кредит', 'платеж', 'расчет', 'страхов', 'клирин', 'депозитар', 'пенсионн', 'фондов']),
    ('Топливно-энергетический комплекс', ['нефт', 'газ', 'топлив', 'трубопровод', 'месторожден', 'переработк', 'уголь', 'нефте']),
    ('Атомная энергетика', ['атомн', 'ядерн', 'радиацион', 'аэс', 'реактор', 'атом']),
    ('Оборонная промышленность', ['оборон', 'военн', 'вооружен', 'оружи', 'оборонно', 'опк', 'госзаказ', 'госконтракт', 'боеприпас', 'ввт', 'минобороны']),
    ('Ракетно-космическая промышленность', ['ракет', 'космическ', 'спутник', 'космодром', 'орбит', 'космос', 'пуск']),
    ('Горнодобывающая промышленность', ['горн', 'добыч', 'шахт', 'рудник', 'карьер', 'месторожден']),
    ('Металлургическая промышленность', ['металлург', 'прокат', 'стал', 'чугун', 'цветн', 'сплав']),
    ('Химическая промышленность', ['химическ', 'хим', 'нефтехим', 'взрывчат', 'кислот', 'реактив', 'удобрен']),
]


def detect_sphere(isys):
    """Определяет сферу деятельности объекта КИИ по названию/назначению/описанию.

    Возвращает название сферы или None, если определить не удалось.
    """
    text = ' '.join([
        getattr(isys, 'name', '') or '',
        getattr(isys, 'purpose', '') or '',
        getattr(isys, 'description', '') or '',
    ]).lower()
    best = None
    best_score = 0
    for sphere, keywords in SPHERE_KEYWORDS:
        score = sum(1 for kw in keywords if kw in text)
        if score > best_score:
            best = sphere
            best_score = score
    return best if best_score > 0 else None
