# -*- coding: utf-8 -*-
"""DokoGen — Справочник переменных шаблонов и резолвер значений"""

from datetime import datetime
from typing import Dict, Any, List, Optional
import re

from . import declension as dc
from .models import Company, CommissionMember


def _norm_name(s):
    """Нормализация ФИО для сравнения: регистр, пробелы, точки, тире игнорируются."""
    s = (s or '').lower().replace('ё', 'е')
    return re.sub(r'[\s.,;:()«»"\'\-]+', '', s)


# ============================================================
# ОПИСАНИЕ ПЕРЕМЕННЫХ
# ============================================================
# Формат: (имя_переменной, группа, описание, ключ_резолвера)
# Группы: A — общие, B — 187-ФЗ

VARIABLE_DEFS = [
    # === Полное название организации (6 падежей) ===
    ('{{company_full_name}}', 'A', 'Полное наименование организации (им.п.)', 'company:full_name'),
    ('{{company_full_name_genitive}}', 'A', 'Полное наименование (род.п.)', 'decl:full_name:gent'),
    ('{{company_full_name_dative}}', 'A', 'Полное наименование (дат.п.)', 'decl:full_name:datv'),
    ('{{company_full_name_accs}}', 'A', 'Полное наименование (вин.п.)', 'decl:full_name:accs'),
    ('{{company_full_name_ablt}}', 'A', 'Полное наименование (твор.п.)', 'decl:full_name:ablt'),
    ('{{company_full_name_loct}}', 'A', 'Полное наименование (пр.п.)', 'decl:full_name:loct'),

    # === Краткое название организации (6 падежей) ===
    ('{{company_short_name}}', 'A', 'Краткое наименование (им.п.)', 'company:short_name'),
    ('{{company_short_name_genitive}}', 'A', 'Краткое наименование (род.п.)', 'decl:short_name:gent'),
    ('{{company_short_name_dative}}', 'A', 'Краткое наименование (дат.п.)', 'decl:short_name:datv'),
    ('{{company_short_name_accs}}', 'A', 'Краткое наименование (вин.п.)', 'decl:short_name:accs'),
    ('{{company_short_name_ablt}}', 'A', 'Краткое наименование (твор.п.)', 'decl:short_name:ablt'),
    ('{{company_short_name_loct}}', 'A', 'Краткое наименование (пр.п.)', 'decl:short_name:loct'),

    # === Должностные лица ===
    ('{{chief_position}}', 'A', 'Должность руководителя', 'company:chief_position'),
    ('{{chief_fio_initials}}', 'A', 'И.О. Фамилия руководителя', 'initials:chief'),
    ('{{chief_fio_short}}', 'A', 'Фамилия И.О. руководителя', 'short_fio:chief'),
    ('{{responsible_position}}', 'A', 'Должность ответственного', 'company:responsible_position'),
    ('{{responsible_fio_initials}}', 'A', 'И.О. Фамилия ответственного', 'initials:responsible'),
    ('{{responsible_fio_short}}', 'A', 'Фамилия И.О. ответственного', 'short_fio:responsible'),
    ('{{admin_position}}', 'A', 'Должность администратора', 'company:admin_position'),
    ('{{admin_fio_initials}}', 'A', 'И.О. Фамилия администратора', 'initials:admin'),
    ('{{admin_fio_short}}', 'A', 'Фамилия И.О. администратора', 'short_fio:admin'),

    # === Сведения об организации ===
    ('{{company_address}}', 'A', 'Адрес организации', 'company:address'),
    ('{{city_name}}', 'A', 'Город', 'company:city_name'),
    ('{{company_inn}}', 'A', 'ИНН', 'company:inn'),
    ('{{company_ogrn}}', 'A', 'ОГРН', 'company:ogrn'),
    ('{{company_ogrn_date}}', 'A', 'Дата ОГРН', 'company:ogrn_date'),
    ('{{company_kpp}}', 'A', 'КПП', 'company:kpp'),
    ('{{security_department}}', 'A', 'Наименование структурного подразделения по безопасности', 'company:security_department'),
    ('{{company_okved}}', 'A', 'Основной ОКВЭД', 'company:okved'),
    ('{{company_okved_extra}}', 'A', 'Дополнительные ОКВЭД', 'company:okved_extra'),
    ('{{company_profit}}', 'A', 'Сумма прибыли за предыдущий год (руб.)', 'company:profit'),
    ('{{company_profit_tax}}', 'A', 'Сумма налога с прибыли (руб.)', 'company:profit_tax'),
    ('{{company_income_5y}}', 'A', 'Доход за 5 лет суммарно (руб.) — показатель 8', 'company:income_5y'),
    ('{{company_income_year_1}}', 'A', 'Доход за 5 лет назад (руб.) — показатель 8 (обратная совместимость)', 'company:income_year_1'),
    ('{{company_income_year_2}}', 'A', 'Доход за 4 года назад (руб.) — показатель 8', 'company:income_year_2'),
    ('{{company_income_year_3}}', 'A', 'Доход за 3 года назад (руб.) — показатель 8', 'company:income_year_3'),
    ('{{company_income_year_4}}', 'A', 'Доход за 2 года назад (руб.) — показатель 8', 'company:income_year_4'),
    ('{{company_income_year_5}}', 'A', 'Доход за прошлый год (руб.) — показатель 8', 'company:income_year_5'),
    ('{{company_income_loss}}', 'A', 'Суммарный экономический ущерб от нарушения критических процессов (руб.) — показатель 8', 'company:income_loss'),

    # === Комиссия ===
    ('{{commission_list}}', 'A', 'Список комиссии', 'commission_list'),
    ('{{fstek_district}}', 'A', 'Наименование управления ФСТЭК России (по адресу организации)', 'fstek_district'),
    ('{{fstek_recipient_address}}', 'A', 'Адрес управления ФСТЭК России для направления сведений', 'fstek_recipient_address'),

    # === Специальные переменные ===
    ('{{date_year}}', 'A', 'Текущий год', 'date:year'),
    ('{{date}}', 'A', 'Текущая дата', 'date:full'),
    ('{{document_date}}', 'A', 'Дата документа', 'doc_date'),
    ('{{document_number}}', 'A', 'Номер документа', 'doc_number'),

    # === 187-ФЗ ===
    ('{{is_security_requirements}}', 'B', 'Требования к защите объекта КИИ', 'security_reqs'),
    ('{{is_list}}', 'B', 'Список всех объектов КИИ (нумерованный)', 'is_list'),
    ('{{is_name}}', 'B', 'Наименование первого объекта КИИ', 'is:name'),
    ('{{name}}', 'B', 'Название объекта КИИ (алиас)', 'is:name'),
    ('{{description}}', 'B', 'Описание объекта КИИ (алиас)', 'is:description'),
    ('{{category}}', 'B', 'Категория значимости объекта КИИ (алиас)', 'is:category'),
    ('{{number}}', 'B', 'Номер (алиас)', 'doc_number'),
    ('{{users}}', 'B', 'Пользователи ОКИИ (алиас)', 'is:users'),
    ('{{full_name_genitive}}', 'A', 'Полное наименование (род.п., алиас)', 'decl:full_name:gent'),
    ('{{responsible_fio_accs}}', 'A', 'ФИО ответственного (вин.п.)', 'fio_acl:responsible'),
    ('{{commission1}}', 'A', 'Должность члена комиссии 1', 'commission_member:0:position'),
    ('{{commission1_fio}}', 'A', 'ФИО члена комиссии 1', 'commission_member:0:fio'),
    ('{{commission2}}', 'A', 'Должность члена комиссии 2', 'commission_member:1:position'),
    ('{{commission2_fio}}', 'A', 'ФИО члена комиссии 2', 'commission_member:1:fio'),
    ('{{commission3}}', 'A', 'Должность члена комиссии 3', 'commission_member:2:position'),
    ('{{commission3_fio}}', 'A', 'ФИО члена комиссии 3', 'commission_member:2:fio'),
]

# Маппинг старых переменных <...> → {{...}}
OLD_VAR_MAP = {
    '<CompanyName1>': '{{company_full_name}}',
    '<CompanyName2>': '{{company_short_name}}',
    '<CompanyName1Gent>': '{{company_full_name_genitive}}',
    '<CompanyName2Gent>': '{{company_short_name_genitive}}',
    '<CompanyName1Datv>': '{{company_full_name_dative}}',
    '<CompanyName2Datv>': '{{company_short_name_dative}}',
    '<CompanyName1Accs>': '{{company_full_name_accs}}',
    '<CompanyName2Accs>': '{{company_short_name_accs}}',
    '<CompanyName1Ablt>': '{{company_full_name_ablt}}',
    '<CompanyName2Ablt>': '{{company_short_name_ablt}}',
    '<CompanyName1Loct>': '{{company_full_name_loct}}',
    '<CompanyName2Loct>': '{{company_short_name_loct}}',
    '<Address>': '{{company_address}}',
    '<INN>': '{{company_inn}}',
    '<OGRN>': '{{company_ogrn}}',
    '<KPP>': '{{company_kpp}}',
    '<ChiefFio>': '{{chief_fio_initials}}',
    '<ChiefFioShort>': '{{chief_fio_short}}',
    '<ChiefPosition>': '{{chief_position}}',
    '<RspFio>': '{{responsible_fio_initials}}',
    '<RspFioShort>': '{{responsible_fio_short}}',
    '<RspPosition>': '{{responsible_position}}',
    '<CommissionList>': '{{commission_list}}',
    '<DocumentNumber>': '{{document_number}}',
    '<DocumentDate>': '{{document_date}}',
    '<Date>': '{{date}}',
    '<SurveyDate>': '{{document_date}}',
}

VAR_PATTERN = re.compile(r'\{\{[^}]+\}\}')
OLD_VAR_PATTERN = re.compile(r'<[A-Za-z0-9_]+>')


# ============================================================
# ПОСТРОЕНИЕ СЛОВАРЯ ЗАМЕН
# ============================================================

def build_replacements(
    company: Company,
    is_list: List = None,
    doc_number: str = "",
    doc_date: str = "",
    direction: str = "187fz",
) -> Dict[str, str]:
    """Строит полный словарь замен {{переменная}} → значение."""
    if is_list is None:
        is_list = []

    # ---------- Комиссия ----------
    commission_data = _build_commission_data(company)

    # ---------- Контекст ----------
    ctx = {
        'company': company,
        'is_list': is_list,
        'commission_data': commission_data,
        'doc_number': doc_number or '',
        'doc_date': doc_date or '',
        'direction': direction,
    }

    replacements = {}
    for var_name, group, desc, source in VARIABLE_DEFS:
        if direction == '187fz' and group not in ('A', 'B'):
            continue
        value = _resolve(source, ctx)
        replacements[var_name] = str(value) if value else '—'

    # Индексированные переменные ИС (is_name_1, is_pd_list_2 и т.д.)
    for i, isys in enumerate(is_list, 1):
        _add_is_indexed_vars(replacements, isys, i)

    # Циклы
    replacements['_loops'] = _build_loop_data(company, is_list)
    return replacements


def _commission_role_display(members) -> List[str]:
    """Группирует роли комиссии для вывода:
    - Председатель/Секретарь/Заместитель — отдельной строкой со своей ролью;
    - обычные члены — первый получает «Члены комиссии», остальные — пустую роль
      (чтобы не повторять «Член комиссии» в каждой строке)."""
    result = []
    member_used = False
    for m in members:
        role = (m.role or 'член комиссии').strip()
        role_lower = role.lower()
        if ('председател' in role_lower or 'секретар' in role_lower
                or 'заместител' in role_lower or 'зам. председателя' in role_lower):
            result.append(role)
            continue
        # обычный член комиссии
        if not member_used:
            result.append('Члены комиссии')
            member_used = True
        else:
            result.append('')
    return result


def _build_commission_data(company: Company) -> Dict[str, str]:
    data = {}
    members = company.commission or []

    display_roles = _commission_role_display(members)
    lines = []
    for i, (m, role_disp) in enumerate(zip(members, display_roles), 1):
        if role_disp:
            line = (f"{i}. {role_disp}: {m.position} — {m.fio}" if m.position
                    else f"{i}. {role_disp}: {m.fio}")
        else:
            line = (f"{i}. {m.position} — {m.fio}" if m.position
                    else f"{i}. {m.fio}")
        lines.append(line)
    data['commission_list'] = '\n'.join(lines) if lines else '—'

    return data


def _add_is_indexed_vars(replacements: Dict[str, str], isys, idx: int):
    fields = {
        'is_name': isys.name or '—',
        'is_address': getattr(isys, 'address', '') or '—',
        'is_description': isys.description or '—',
        'is_has_internet': 'да' if isys.is_internet else 'нет',
        'is_category': isys.category or '—',
        'is_users': ', '.join(isys.users_list or []) or '—',
        'is_purpose': isys.purpose or '—',
        'is_structure': getattr(isys, 'structure', '') or '—',
        'is_software': getattr(isys, 'software', '') or '—',
        'is_sphere': getattr(isys, 'sphere', '') or '—',
        'is_object_type': getattr(isys, 'object_type', '') or '—',
        'is_architecture': getattr(isys, 'architecture', '') or '—',
        'is_typical_object': getattr(isys, 'typical_object', '') or '—',
        'is_net_category': getattr(isys, 'net_category', '') or '—',
        'is_net_operator': getattr(isys, 'net_operator', '') or '—',
        'is_net_purpose': getattr(isys, 'net_purpose', '') or '—',
        'is_net_method': getattr(isys, 'net_method', '') or '—',
        'is_hw_sw': getattr(isys, 'hw_sw', '') or '—',
        'is_os_sw': getattr(isys, 'os_sw', '') or '—',
        'is_app_sw': getattr(isys, 'app_sw', '') or '—',
    }
    for f, v in fields.items():
        replacements[f'{{{{{f}_{idx}}}}}'] = str(v)


def _build_loop_data(company: Company, is_list: List) -> Dict[str, List[Dict]]:
    loops = {}

    # Комиссия
    members = company.commission or []
    if members:
        display_roles = _commission_role_display(members)
        items = []
        for m, role_disp in zip(members, display_roles):
            pos = m.position or '—'
            fio = m.fio or '—'
            if role_disp:
                full = f"{role_disp} {pos} — {fio}" if pos != '—' else f"{role_disp} — {fio}"
            else:
                full = f"{pos} — {fio}" if pos != '—' else fio
            # Строка подписи: «___________ / » (только подчёркивание и слэш).
            # Роль НЕ включаем — в шаблонах она выводится отдельной колонкой {{item.role}},
            # иначе получается дубль «Председатель комиссии | Председатель комиссии: ___».
            if 'Председатель' in role_disp:
                sig = f"{'_' * 11} / "
                sig_full = f"{role_disp}: {'_' * 11} / "
            elif role_disp:
                sig = f"{'_' * 17} / "
                sig_full = f"{role_disp}: {'_' * 17} / "
            else:
                sig = f"{'_' * 17} / "
                sig_full = f"{'_' * 17} / "
            initials = dc.get_initials(fio) if fio != '—' else ''
            items.append({
                'role': role_disp,
                'position': pos,
                'fio': fio,
                'fio_initials': dc.get_initials(fio) if fio != '—' else '',
                'fio_short': dc.get_short_fio(fio) if fio != '—' else '',
                'full': full,
                # Рамка подписи без роли и ФИО: «___________ / » (ФИО добавится)
                'signature': sig,
                # Полная подпись с ролью и ФИО: «Роль: ___________ / И.О. Фамилия»
                'signature_full': (sig_full + initials).strip(),
            })
        loops['commission'] = items

    # ИС
    if is_list:
        items = []
        for isys in is_list:
            # Пользователи ИС для вложенного цикла <!-- loop:users_loop -->
            # ВСЕ пользователи, включая администратора (без дублей по ФИО)
            seen = set()
            users_loop = []
            for u in (isys.users_list or []):
                key = _norm_name(u)
                if key and key in seen:
                    continue
                seen.add(key)
                users_loop.append({'fio': u, 'position': ''})
            items.append({
                'name': isys.name or '—',
                'description': isys.description or '—',
                'address': getattr(isys, 'address', '') or '—',
                'is_has_lan': 'Да' if isys.is_local_network else 'Нет',
                'has_internet': 'да' if isys.is_internet else 'нет',
                'category': isys.category or '—',
                'users': ', '.join(isys.users_list or []) or '—',
                'users_loop': users_loop,
                'defense_tools_list': ', '.join(isys.defense_tools_list or []) or '—',
                'is_purpose': isys.purpose or '—',
                'structure': getattr(isys, 'structure', '') or '—',
                'software': getattr(isys, 'software', '') or '—',
                # 236 приказ
                'purpose': isys.purpose or '—',
                'sphere': getattr(isys, 'sphere', '') or '—',
                'object_type': getattr(isys, 'object_type', '') or '—',
                'architecture': getattr(isys, 'architecture', '') or '—',
                'typical_object': getattr(isys, 'typical_object', '') or '—',
                'subject_name': getattr(isys, 'subject_name', '') or '—',
                'subject_address': getattr(isys, 'subject_address', '') or '—',
                'subject_element': getattr(isys, 'subject_element', '') or '—',
                'subject_inn': getattr(isys, 'subject_inn', '') or '—',
                'subject_kpp': getattr(isys, 'subject_kpp', '') or '—',
                'net_category': getattr(isys, 'net_category', '') or '—',
                'net_operator': getattr(isys, 'net_operator', '') or '—',
                'net_purpose': getattr(isys, 'net_purpose', '') or '—',
                'net_method': getattr(isys, 'net_method', '') or '—',
                'hw_sw': getattr(isys, 'hw_sw', '') or '—',
                'os_sw': getattr(isys, 'os_sw', '') or '—',
                'app_sw': getattr(isys, 'app_sw', '') or '—',
                'balance_value': getattr(isys, 'balance_value', '') or '—',
                'attacker_category': getattr(isys, 'attacker_category', '') or '—',
                'attacker_level': getattr(isys, 'attacker_level', '') or '—',
                'attacker_types': getattr(isys, 'attacker_types', '') or '—',
                'threats': getattr(isys, 'threats', '') or '—',
                # Алиасы под шаблон «Сведения о результатах категорирования» (ФСТЭК)
                'objective': getattr(isys, 'purpose', '') or '—',
                'standard_name': getattr(isys, 'typical_object', '') or '—',
                'network_category': getattr(isys, 'net_category', '') or '—',
                'provider': getattr(isys, 'net_operator', '') or '—',
                'interaction_purpose': getattr(isys, 'net_purpose', '') or '—',
                'interaction_method': getattr(isys, 'net_method', '') or '—',
                'hardware': getattr(isys, 'hw_sw', '') or '—',
                'system_software': getattr(isys, 'os_sw', '') or '—',
                'defense_tools': ', '.join(isys.defense_tools_list or []) or '—',
                'component': getattr(isys, 'subject_element', '') or '—',
                'exploiting_org': getattr(isys, 'subject_name', '') or '—',
                'exploiting_inn': getattr(isys, 'subject_inn', '') or '—',
                'domain': getattr(isys, 'domain_name', '') or '—',
                'threat_actor': getattr(isys, 'attacker_category', '') or '—',
                'threat_justification': getattr(isys, 'threats', '') or '—',
                'incidents': getattr(isys, 'incidents', '') or '—',
                'criteria_values': getattr(isys, 'criteria_values', '') or '—',
                'criteria_justification': getattr(isys, 'criteria_justification', '') or '—',
                # Орг. меры: если не заполнены вручную — автогенерация по опроснику
                # (с учётом ответов раздела «Физическая безопасность»).
                'org_measures': (getattr(isys, 'org_measures', '') or '')
                                or _build_org_measures_text(company, isys),
                'tech_measures': getattr(isys, 'tech_measures', '') or '—',
            })
        loops['information_systems'] = items

    # Сотрудники с доступом
    employees = getattr(company, 'employees_access', None) or []
    if employees:
        items = []
        for e in employees:
            items.append({
                'position': e.get('position', ''),
                'fio': e.get('fio', ''),
            })
        loops['employees_access'] = items

    # --- Шаблон «Сведения о результатах категорирования» (ФСТЭК) ---
    # Цикл объектов КИИ — тот же список, что information_systems.
    if is_list:
        loops['kii_objects'] = loops.get('information_systems', [])

    # Цикл показателей критериев значимости (ПП-127): автоматически
    # формируем строки таблицы с обоснованием «применим/не применим» по сфере
    # и расчётом для показателей 8 и 9 (как в примере заказчика).
    loops['categorization_indicators'] = _build_categorization_indicators(company, is_list)

    return loops


# ============================================================
# ПОКАЗАТЕЛИ КРИТЕРИЕВ ЗНАЧИМОСТИ (ПП-127)
# ============================================================

def _num(s):
    """«121 118 270 руб», «1 000», «2 212.5» → число (float) или None."""
    s = str(s or '').strip()
    if not s:
        return None
    s = s.replace('\u00a0', ' ').replace(' ', '')\
        .replace('руб', '').replace('₽', '').replace('р.', '').replace(',', '.')
    try:
        return float(s)
    except Exception:
        return None


def _load_json(name):
    import json as _json
    import os as _os
    p = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), 'dictionaries', name)
    try:
        with open(p, encoding='utf-8') as f:
            return _json.load(f)
    except Exception:
        return {}


def _calc_criterion_8_9(company, isys, num):
    """Расчёт показателя 8 (ущерб субъекту КИИ) или 9 (ущерб бюджету РФ).

    Возвращает (текст_обоснования, категория или None).
    Логика — как в авторасчёте программы (окно ПП-127), текст — по образцу
    заполнения заказчика для ОПК.
    """
    # ΔДоходов: ущерб из вкладки «Организация» или балансовая стоимость объекта
    d = _num(getattr(company, 'income_loss', '') or '')
    if d is None:
        d = _num(getattr(isys, 'balance_value', '') or '')
    # Доходы за 5 лет: основное поле income_5y (суммарно), иначе — обратная совместимость по годам
    inc5 = _num(getattr(company, 'income_5y', '') or '')
    if inc5:
        r_avg = inc5 / 5.0
    else:
        incs = [_num(getattr(company, f'income_year_{i}', '') or '') for i in range(1, 6)]
        incs = [x for x in incs if x is not None]
        r_avg = (sum(incs) / len(incs)) if incs else None

    if num == '8':
        # Фолбэк: ущерб не заполнен → оценка «доход за год × 5» (наихудший сценарий)
        d_estimated = False
        if d is None and r_avg:
            d = r_avg * 5
            d_estimated = True
        if d is None or not r_avg:
            return ("Показатель применим. Для расчёта заполните данные организации: "
                    "«Снижение уровня дохода» и доходы за 5 лет, или балансовую стоимость объекта КИИ.", None)
        val_pct = d / r_avg * 100
        if val_pct > 20:
            cat = 'I'
        elif val_pct > 10:
            cat = 'II'
        elif val_pct >= 1:
            cat = 'III'
        else:
            cat = None
        text = (f"Реализация компьютерных атак на объект не приведет к снижению доходов субъекта КИИ. "
                f"Объем среднегодового дохода субъекта КИИ (с учетом НДС), усредненный за прошедший "
                f"5-летний период, составляет {r_avg:,.2f} руб. ")
        if d_estimated:
            text += (f"Снижение уровня дохода не заполнено, поэтому оценка ущерба принята по наихудшему "
                     f"сценарию: доход за год × 5 лет = {r_avg:,.2f} × 5 = {d:,.2f} руб. ")
        else:
            text += (f"В случае выхода из строя объекта КИИ потребуется замена, установка и настройка, "
                     f"стоимость которых составит около {d:,.2f} руб. ")
        text += (f"Абсолютный ущерб субъекта КИИ: U={d:,.2f} руб. "
                 f"Показатель ущерба субъекту КИИ: U_8 = U / R_год = {d:,.2f} / {r_avg:,.2f} "
                 f"= {val_pct:.6f} %.")
        if cat:
            text += (f" Так как {val_pct:.6f} % более 1% (пороговое значение для 3-й категории "
                     f"значимости согласно Постановлению № 127), по данному показателю объекту "
                     f"присваивается {cat}-я категория значимости.")
        else:
            text += (f" Так как {val_pct:.6f} % менее 1% (пороговое значение для 3-й категории "
                     f"значимости согласно Постановлению № 127), по данному показателю объекту "
                     f"категория значимости не присваивается.")
        return text, cat

    # num == '9'
    if d is None:
        return ("Показатель применим. Для расчёта заполните данные организации: "
                "«Снижение уровня дохода» или балансовую стоимость объекта КИИ.", None)
    profit = _num(getattr(company, 'profit', '') or '')
    tax = _num(getattr(company, 'profit_tax', '') or '')
    if profit and tax is not None and profit > 0:
        knp = tax / profit
        dnp = d * knp
        if d > profit:
            dnp = tax
    else:
        knp = 0.0
        dnp = 0.0
    DEFAULT_BUDGET_AVG = 21228053357700.0  # Бусредн — прогноз годового дохода федбюджета, усреднённый за 3 года
    b = DEFAULT_BUDGET_AVG
    val_pct = dnp / b * 100
    if val_pct > 0.001:
        cat = 'I'
    elif val_pct > 0.0006:
        cat = 'II'
    elif val_pct > 0.0003:
        cat = 'III'
    else:
        cat = None
    text = (f"Реализация компьютерных атак на объект не приведет к снижению доходов федерального бюджета. "
            f"Объем прогнозируемого годового дохода федерального бюджета Российской Федерации, "
            f"усредненный за планируемый трехлетний период, составляет {b:,.2f} руб. "
            f"Коэффициент выплат (отчислений) от налога на прибыль субъекта КИИ = {knp:.4f}. "
            f"В случае выхода из строя объекта КИИ потребуется замена, установка и настройка, "
            f"стоимость которых составит около {d:,.2f} руб. "
            f"Снижение выплат (отчислений) от налога на прибыль субъекта КИИ в бюджеты Российской Федерации "
            f"Уабс = {d:,.2f} × {knp:.4f} = {dnp:,.2f} руб. (приближённо). "
            f"Показатель ущерба бюджетам = {val_pct:.6f} %.")
    if cat:
        text += (f" Так как {val_pct:.6f} % более 0,0003% (пороговое значение для 3-й категории "
                 f"значимости согласно Постановлению № 127), по данному показателю объекту "
                 f"присваивается {cat}-я категория значимости.")
    else:
        text += (f" Так как {val_pct:.6f} % менее 0,0003% (пороговое значение для 3-й категории "
                 f"значимости согласно Постановлению № 127), по данному показателю объекту "
                 f"категория значимости не присваивается.")
    return text, cat


# Пороги показателя 10 и подпоказателей 10.1–10.7 (ПП-127, «Значение показателя»)
CRITERION_10_THRESHOLDS = {
    # num: (категория → (нижняя граница, верхняя граница)), границы «включительно»
    # Показатель 10 — среднедневное количество операций (млн ед.):
    #   III — менее или равно 70, II — более 70 но менее или равно 120, I — более 120.
    '10':   {'I': (120, None), 'II': (70, 120), 'III': (None, 70)},
    '10.1': {'I': (10, None), 'II': (1, 10), 'III': (None, 1)},        # трлн руб.
    '10.2': {'I': (25, None), 'II': (10, 25), 'III': (None, 10)},      # тыс. шт.
    '10.3': {'I': (2000, None), 'II': (1000, 2000), 'III': (50, 1000)},  # млрд руб.
    '10.4': {'I': (5000, None), 'II': (1500, 5000), 'III': (100, 1500)}, # млрд руб.
    '10.5': {'I': (150, None), 'II': (100, 150), 'III': (25, 100)},      # договоров
    '10.6': {'I': (5000, None), 'II': (1000, 5000), 'III': (100, 1000)}, # млрд руб.
    '10.7': {'I': (500, None), 'II': (200, 500), 'III': (30, 200)},      # млн ед.
}


def _criterion_10_cat(num, value):
    """Категория значимости по показателю 10 / 10.x (ПП-127).

    value — численное значение показателя. Возвращает 'I'/'II'/'III' или None.
    """
    thr = CRITERION_10_THRESHOLDS.get(str(num))
    if not thr or value is None:
        return None
    for cat in ('I', 'II', 'III'):
        lo, hi = thr[cat]
        if lo is not None and value < lo:
            continue
        if hi is not None and value > hi:
            continue
        return cat
    return None


def _calc_criterion_10(company, isys, num):
    """Расчёт показателя 10 (прекращение финансовых операций) и подпоказателей 10.1–10.7.

    Показатель 10: среднедневное количество операций = операции за год (млн ед.) / дни.
    Подпоказатели 10.x: значение показателя (в своих единицах) сравнивается с порогами.
    Возвращает (текст_обоснования, категория или None).
    """
    num = str(num)
    if num not in CRITERION_10_THRESHOLDS:
        return None, None
    if num == '10':
        ops = _num(getattr(company, 'ops_count', '') or '')
        days = _num(getattr(company, 'calendar_days', '') or '')
        if days is None or days <= 0:
            days = 365
        if ops is None:
            return ("Показатель применим. Для расчёта заполните данные организации: "
                    "«Количество операций за прошедший год (млн ед.)».", None)
        val = ops / days
        cat = _criterion_10_cat('10', val)
        text = (f"Показатель прекращения финансовых операций оценивается среднедневным количеством "
                f"операций, проведенных клиентами по банковским счетам и (или) без открытия банковского "
                f"счета. Количество операций за прошедший год: {ops:,.2f} млн ед. Количество календарных "
                f"дней в году: {days:.0f}. Среднедневное количество операций: {ops:,.2f} / {days:.0f} "
                f"= {val:.6f} млн ед./день.")
    else:
        value = _num(getattr(company, 'criterion10_value', '') or '')
        if value is None:
            return (f"Показатель применим. Для расчёта заполните значение показателя "
                    f"«{num}» в данных организации.", None)
        val = value
        cat = _criterion_10_cat(num, val)
        text = (f"Показатель {num} оценивается значением показателя: {val:,.6f} "
                f"(в единицах, установленных ПП-127).")
    if cat:
        text += (f" Согласно Постановлению № 127 полученное значение соответствует "
                 f"{cat}-й категории значимости, по данному показателю объекту присваивается "
                 f"{cat}-я категория значимости.")
    else:
        text += (" Значение не достигает порога III категории — по данному показателю "
                 "категория значимости не присваивается.")
    return text, cat


def _build_org_measures_text(company, isys=None):
    """Формирует текст организационных мер из словаря org_measures.json
    с учётом ответов раздела «Физическая безопасность» опросника.

    Если в опроснике указано «нет» (охрана, сигнализация, тревожная кнопка,
    видеонаблюдение, металлические двери и т.п.) — соответствующие пункты
    из текста убираются. Пункты, не привязанные к средствам, остаются всегда.
    """
    import os as _os
    items = _load_json('org_measures.json')
    if not isinstance(items, list):
        items = []
    ps = getattr(company, 'physical_security', None) or {}

    def _no(*keys):
        """True, если хотя бы по одному ключу явно указано «нет»/«не имеется»."""
        for k in keys:
            v = str(ps.get(k) or '').strip().lower()
            if v in ('нет', 'не имеется', 'нет,', '0', 'false'):
                return True
        return False

    rules = [
        # (подстрока пункта, ключи physical_security, при которых пункт УБИРАЕМ)
        ('Физическая охрана периметра', ('physicalGuard', 'physicalTurnstile', 'physicalVisitorLog')),
        ('Имеется система видеонаблюдения', ('physicalVideoSurveillance',)),
        ('Здания не имеют окон', ('physicalMetalDoors',)),
        ('охранно-пожарная сигнализация и тревожная кнопка', ('physicalFireAlarm', 'physicalPanicButton')),
        ('охранно-пожарная сигнализация', ('physicalFireAlarm',)),
        ('тревожная кнопка', ('physicalPanicButton',)),
        ('турникетом на проходной', ('physicalTurnstile',)),
        ('СКУД', ('physicalTurnstile',)),
        ('учет посетителей', ('physicalVisitorLog',)),
    ]

    out = []
    for line in items:
        line = str(line).strip()
        if not line:
            continue
        drop = False
        for sub, keys in rules:
            if sub in line and _no(*keys):
                drop = True
                break
        if not drop:
            out.append(line)
    return '\n'.join(out) if out else '—'


def _build_categorization_indicators(company, is_list):
    """Строки таблицы показателей критериев значимости для шаблона
    «Сведения о результатах категорирования» (цикл categorization_indicators).

    Для каждого показателя ПП-127 (5 групп, показатели 1–14 с подпунктами):
      section, name, number, value, justification, category.
    - Если показатель не применим для сферы объекта — value = «Показатель не применим.»,
      justification — текст из словаря обоснований по сфере (или заготовка).
    - Если применим (8/9) — автоматический расчёт из данных организации.
    """
    import re as _re
    criteria = _load_json('criteria_127.json')
    applicability = _load_json('criteria_applicability.json')
    justifications = _load_json('criteria_justifications.json')
    if not isinstance(criteria, list):
        criteria = []

    sphere = getattr(is_list[0], 'sphere', '') if is_list else ''
    sphere = (sphere or '').strip()
    appl = applicability.get(sphere) if isinstance(applicability, dict) else None
    just = (justifications.get(sphere) or {}) if isinstance(justifications, dict) else {}

    isys = is_list[0] if is_list else None

    rows = []
    first_cat_num = None  # номер показателя, по которому объект отнесён к I категории (п. 6 ПП-127)
    for grp in criteria:
        section = grp.get('criteria', '')
        for ind in grp.get('indicators', []):
            num = str(ind.get('num', ''))
            name = ind.get('name', '')
            m = _re.match(r'(\d+)', num)
            grp_num = m.group(1) if m else num
            applicable = appl is None or grp_num in appl

            value = '—'
            justification = '—'
            category = '—'
            if first_cat_num is not None:
                # Остановка расчёта: объект уже отнесён к I категории по одному из показателей
                # (п. 6 Правил, утв. ПП РФ № 127: «расчет по остальным показателям не проводится»).
                if applicable:
                    value = '—'
                    justification = (f"Расчёт по данному показателю не проводится: объект отнесён к "
                                     f"I категории значимости по показателю {first_cat_num} "
                                     f"(п. 6 Правил категорирования, утв. ПП РФ № 127).")
                    category = '—'
                else:
                    value = 'Показатель не применим.'
                    justification = (just.get(num) or just.get(grp_num)
                                     or f"Показатель не применим к объекту КИИ организации сферы «{sphere or '—'}».")
            elif not applicable:
                value = 'Показатель не применим.'
                justification = (just.get(num) or just.get(grp_num)
                                 or f"Показатель не применим к объекту КИИ организации сферы «{sphere or '—'}».")
            elif grp_num in ('8', '9'):
                text, cat = _calc_criterion_8_9(company, isys, grp_num)
                if text:
                    value = text
                if cat:
                    category = f"{cat}-я категория"
                else:
                    category = '—'
            elif grp_num == '10':
                text, cat = _calc_criterion_10(company, isys, num)
                if text:
                    value = text
                if cat:
                    category = f"{cat}-я категория"
                else:
                    category = '—'
            else:
                # Применимый показатель без авторасчёта — оператор заполняет значение
                value = '—'
                justification = (just.get(num) or just.get(grp_num)
                                 or 'Показатель применим к объекту КИИ; значение определяется по данным объекта.')

            # Если по показателю присвоена I категория — фиксируем для остановки расчёта
            if first_cat_num is None and category.startswith('I'):
                first_cat_num = num

            rows.append({
                'section': section,
                'name': name,
                'number': num,
                'value': value,
                'justification': justification,
                'category': category,
            })
    return rows


# ============================================================
# РЕЗОЛВЕР ЗНАЧЕНИЙ
# ============================================================

def _find_fstek_office(address_text: str):
    """Определяет управление ФСТЭК России по адресу организации.

    Ищет город/регион из адреса в словаре fstek_offices.json.
    Возвращает dict {'name': ..., 'address': ...} или None.
    """
    import json as _json
    import os as _os
    if not address_text:
        return None
    dict_path = _os.path.join(_os.path.dirname(__file__), 'dictionaries', 'fstek_offices.json')
    try:
        with open(dict_path, encoding='utf-8') as _f:
            offices = _json.load(_f)
    except Exception:
        return None
    hay = address_text.lower()
    # Нормализация сокращений: обл.→область, респ.→республика, г.о.→городской округ
    hay = hay.replace('обл.', 'область').replace('обл,', 'область')
    hay = hay.replace('респ.', 'республика').replace('респ,', 'республика')
    hay = hay.replace('г.о.', 'городской округ').replace('г.о,', 'городской округ')
    hay = hay.replace('ао', 'автономный округ')
    best = None
    best_len = 0
    for off in offices:
        # Ищем по городам
        for city in off.get('cities', []):
            if city in hay and len(city) > best_len:
                best = off
                best_len = len(city)
        # Ищем по регионам (область/край/республика)
        for region in off.get('regions', []):
            if region in hay and len(region) > best_len:
                best = off
                best_len = len(region)
    return best


def _resolve(source: str, ctx: Dict) -> str:
    """Преобразует ключ-источник в значение, подставляя из контекста."""
    company = ctx.get('company')
    is_list = ctx.get('is_list', [])
    commission_data = ctx.get('commission_data', {})
    doc_number = ctx.get('doc_number', '')
    doc_date = ctx.get('doc_date', '')

    # Прямые поля Company
    if source.startswith('company:'):
        field = source[8:]
        return str(getattr(company, field, '') or '')

    # Склонение названия
    if source.startswith('decl:'):
        _, field, case = source.split(':')
        name = getattr(company, field, '') or ''
        case_gr = dc._normalize_case(case)
        # Сначала берём ГОТОВОЕ склонение из модели (пользователь мог
        # подставить через Морфер/ИИ или ввести вручную), пересчитываем
        # только если поле пустое
        case_idx = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6}.get(case_gr)
        if case_idx:
            if field == 'full_name':
                ready = getattr(company, f'company_name_{case_idx}', '') or ''
                if ready:
                    return ready
            elif field == 'short_name':
                ready = getattr(company, f'short_name_{case_idx}', '') or ''
                if ready:
                    return ready
        return dc.company_name_decline(name, case_gr) if name else '—'

    # Форматы ФИО
    if source.startswith('short_fio:'):
        who = source.split(':', 1)[1]
        fio = _get_fio(company, who)
        return dc.get_short_fio(fio) if fio else '—'
    if source.startswith('initials:'):
        who = source.split(':', 1)[1]
        fio = _get_fio(company, who)
        return dc.get_initials(fio) if fio else '—'

    # Комиссия
    if source in commission_data:
        return str(commission_data[source])

    # Управление ФСТЭК (адресат отправки сведений о категорировании)
    if source in ('fstek_recipient_address', 'fstek_district', 'fstek_office_name'):
        addr = getattr(company, 'address', '') or ''
        off = _find_fstek_office(addr)
        if off:
            if source == 'fstek_recipient_address':
                return off.get('address', '') or ''
            return off.get('name', '') or ''
        return ''

    # Даты
    if source.startswith('date:'):
        fmt = source.split(':', 1)[1]
        now = datetime.now()
        if fmt == 'year':
            return str(now.year)
        return now.strftime('%d.%m.%Y')
    if source == 'doc_date':
        return doc_date or datetime.now().strftime('%d.%m.%Y')
    if source == 'doc_number':
        num = str(doc_number).strip()
        return re.sub(r'^[Nn]?[oо]?[№#]\s*', '', num) or '—'

    # Требования защищённости
    if source == 'security_reqs':
        return _get_security_text(ctx)

    # IS-алиасы (первая ИС)
    if source.startswith('is:'):
        field = source[3:]
        if is_list:
            first = is_list[0]
            field_map = {
                'name': 'name',
                'description': 'description',
                'category': 'category',
                'users': lambda: ', '.join(first.users_list or []),
            }
            if field in field_map:
                val = field_map[field]
                if callable(val):
                    return val() or '—'
                return str(getattr(first, val, '') or '—') if isinstance(val, str) else '—'
        return '—'
    
    # Список ИС
    if source == 'is_list':
        if is_list:
            return '\n'.join(f"{i}. {isys.name}" for i, isys in enumerate(is_list, 1))
        return '—'
    if source == 'is_list_comma':
        if is_list:
            return ', '.join(isys.name for isys in is_list)
        return '—'
    
    # Члены комиссии
    if source.startswith('commission_member:'):
        parts = source.split(':')
        idx, field = int(parts[1]), parts[2]
        members = getattr(company, 'commission', None) or []
        if idx < len(members):
            return str(getattr(members[idx], field, '') or '—')
        return '—'
    
    # Склонение ФИО по падежам
    if source.startswith('fio_acl:'):
        who = source.split(':', 1)[1]
        fio = _get_fio(company, who)
        return dc.decline(fio, 'accs') if fio else '—'

    return '—'


def _get_fio(company: Company, who: str) -> str:
    mapping = {
        'chief': 'chief_fio',
        'responsible': 'responsible_fio',
        'admin': 'admin_fio',
    }
    field = mapping.get(who, who + '_fio')
    return getattr(company, field, '') or ''


def _get_security_text(ctx: Dict) -> str:
    """Возвращает текст требований к защите объекта КИИ по категории значимости."""
    is_list = ctx.get('is_list', [])
    category = ''
    if is_list:
        category = str(getattr(is_list[0], 'category', '') or '').strip()

    TEXTS = {
        '1': """Для значимого объекта КИИ 1-й категории значимости необходимо:
— создание системы безопасности значимого объекта КИИ;
— обеспечение защиты информации от компьютерных атак;
— категорирование объектов КИИ и контроль его результатов.""",
        '2': """Для значимого объекта КИИ 2-й категории значимости необходимо:
— создание системы безопасности значимого объекта КИИ;
— обеспечение защиты информации от компьютерных атак.""",
        '3': """Для значимого объекта КИИ 3-й категории значимости необходимо:
— обеспечение защиты информации от компьютерных атак.""",
    }
    if category in TEXTS:
        return TEXTS[category]
    return """Для объекта КИИ, не являющегося значимым, необходимо:
— принятие мер по защите информации, предусмотренных законодательством РФ о безопасности КИИ."""

