Загрузка данных


# -*- coding: utf-8 -*-
# -*- database_module.py -*-

import logging
import numpy as np
from functools import wraps

from sqlalchemy import (
    Column,
    Float,
    ForeignKey,
    Integer,
    String,
    create_engine,
    func,
    inspect,
)
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import declarative_base, relationship, sessionmaker
from sqlalchemy.sql.expression import text

# Настройки логирования
logging.basicConfig(
    level=logging.WARNING, format="%(asctime)s [%(levelname)s]: %(message)s"
)
logger = logging.getLogger(__name__)

# Заглушаем чрезмерные логи от сторонних библиотек
logging.getLogger("sqlalchemy").propagate = False
# дополнительно отключаем NumExpr
logging.getLogger("numexpr").propagate = False

# Настройка движка базы данных
DATABASE_PATH = "database.db"
engine = create_engine(f"sqlite:///{DATABASE_PATH}")
Base = declarative_base()
Session = sessionmaker(bind=engine)


# Определение моделей
class Group(Base):
    __tablename__ = "groups"
    id = Column(Integer, primary_key=True)
    name = Column(String)
    code = Column(String, unique=True)  # Уникальный код группы
    kov = Column(Float, default=None)  # Коэффициент KOV группы


class Indicator(Base):
    __tablename__ = "indicators"
    id = Column(Integer, primary_key=True)
    name = Column(String)
    code = Column(String, unique=True)  # Код индикатора
    group_code = Column(String, ForeignKey("groups.code"))
    group = relationship("Group", backref="indicators", lazy="select")
    kov = Column(Float, default=None) # Добавляем колонку для веса.

class MatrixEntry(Base):
    __tablename__ = "matrices"
    id = Column(Integer, primary_key=True)

    # Связь с группой по ID (основная и правильная)
    group_id = Column(Integer, ForeignKey("groups.id"), nullable=False)
    # Оставляем только эту строку
    group = relationship("Group", backref="matrices")

    row = Column(Integer, nullable=False)
    col = Column(Integer, nullable=False)
    value = Column(Float, nullable=True)

    def __repr__(self):
        return f"<MatrixEntry({self.row}, {self.col}, {self.value})>"


# Инициализация базы данных
def initialize_database():
    logger.info("Инициализация базы данных...")
    # Эта команда создает таблицы, если их нет. Существующие данные НЕ УДАЛЯЕТ.
    Base.metadata.create_all(engine)
    logger.info("База данных создана успешно.")


# Функция миграции (добавляет недостающие колонки)
def migrate_database():
    logger.info("Начало миграции базы данных...")
    inspector = inspect(engine)
    tables = inspector.get_table_names()

    # Если таблица 'groups' отсутствует, создаём её
    if "groups" not in tables:
        logger.warning("'groups' table does not exist. Creating it now.")
        Base.metadata.tables["groups"].create(engine)

    # Проверка наличия колонки 'kov'
    columns = inspector.get_columns("groups")
    column_names = [column["name"] for column in columns]
    if "kov" not in column_names:
        logger.warning("Column 'kov' doesn't exist. Adding it now.")
        with engine.connect() as conn:
            conn.execute(text("ALTER TABLE groups ADD COLUMN kov FLOAT DEFAULT NULL;"))
        logger.info("Migration completed successfully.")
    else:
        logger.info("Column 'kov' already exists. Skipping migration.")


# CRUD операции для групп


def add_group(name, code, kov=None):
    """Добавление новой группы"""
    session = Session()
    new_group = Group(name=name, code=code, kov=kov)
    session.add(new_group)
    session.commit()
    return new_group


def get_group(code):
    """Получение группы по коду"""
    session = Session()
    return session.query(Group).filter_by(code=code).first()


def update_group(code, new_name=None, new_kov=None):
    """Обновление группы"""
    session = Session()
    group = session.query(Group).filter_by(code=code).first()
    if group:
        if new_name:
            group.name = new_name
        if new_kov is not None:
            group.kov = new_kov
        session.commit()
        return group
    return None


def delete_group(code):
    """Удаление группы"""
    session = Session()
    group = session.query(Group).filter_by(code=code).first()
    if group:
        session.delete(group)
        session.commit()
        return True
    return False


# CRUD операции для индикаторов


def insert_indicator(name, code, group_code):
    """Вставка нового индикатора"""
    session = Session()
    new_indicator = Indicator(name=name, code=code, group_code=group_code)
    session.add(new_indicator)
    session.commit()
    return new_indicator


def get_indicator(code):
    """Получение индикатора по коду"""
    session = Session()
    return session.query(Indicator).filter_by(code=code).first()


def update_indicator(code, new_name=None, new_group_code=None):
    """Обновление индикатора"""
    session = Session()
    indicator = session.query(Indicator).filter_by(code=code).first()
    if indicator:
        if new_name:
            indicator.name = new_name
        if new_group_code:
            indicator.group_code = new_group_code
        session.commit()
        return indicator
    return None


def delete_indicator_by_code(code):
    """Удаление индикатора по его уникальному коду"""
    session = Session()
    try:
        indicator = session.query(Indicator).filter_by(code=code).first()
        if indicator:
            session.delete(indicator)
            session.commit()
            return True
        return False
    except Exception as e:
        # Если что-то пошло не так, откатываем изменения
        session.rollback()
        print(f"Ошибка при удалении: {e}")  # Или logger.error()
        return False
    finally:
        # Эта строка выполнится ВСЕГДА, даже если была ошибка
        session.close()


# Функция для получения всех индикаторов по группе (по уникальному коду)
def get_all_indicators_by_group(group_code, session=None):
    """Возвращает все индикаторы для группы по её коду."""
    close_session = False

    if session is None:
        session = Session()
        close_session = True

    try:
        indicators = session.query(Indicator).filter_by(group_code=group_code).all()
        return indicators
    finally:
        if close_session:
            session.close()


# Функция для получения всех индикаторов по группе (по внутреннему ID)
def list_indicators(group_code):
    session = Session()
    return session.query(Indicator).filter_by(group_code=group_code).all()


# Функция для получения списка всех групп
def list_groups(session=None):
    """
    Возвращает список всех групп. Принимает внешнюю сессию или создает свою.
    """
    # Флаг, чтобы знать, закрывать ли сессию в конце
    close_session = False

    # Если сессию не передали извне, создаем свою
    if session is None:
        session = Session()
        close_session = True

    try:
        groups = session.query(Group).all()
        return groups
    finally:
        # Эта строка выполнится всегда, даже если была ошибка
        if close_session:
            session.close()


# Получение матричных записей
def get_matrix_entries(session, group_id):
    return session.query(MatrixEntry).filter_by(group_id=group_id).all()


def update_matrix_value(session, group_id, row_index, col_index, value):
    """
    Обновляем значение ячейки матрицы.
    :param session: Текущая сессия SQLAlchemy
    :param group_id: Идентификатор группы
    :param row_index: Индекс строки
    :param col_index: Индекс столбца
    :param value: Новое значение ячейки
    """
    entry = (
        session.query(MatrixEntry)
        .filter_by(group_id=group_id, row=row_index, col=col_index)
        .first()
    )

    if entry:
        entry.value = value
    else:
        new_entry = MatrixEntry(
            group_id=group_id, row=row_index, col=col_index, value=value
        )
        session.add(new_entry)

    session.commit()


def calculate_indicator_weights(matrix_entries):
    """
    Рассчитывает вектор коэффициентов относительной важности (КОВ/весов)
    для каждого индикатора на основе матрицы парных сравнений.
    :param matrix_entries: Список объектов MatrixEntry для одной группы.
    :return: Список весов (float), где индекс соответствует индексу индикатора.
    """
    if not matrix_entries:
        return None

    # Определяем размерность матрицы (n x n)
    max_row = max(entry.row for entry in matrix_entries)
    max_col = max(entry.col for entry in matrix_entries)
    n = max(max_row, max_col) + 1

    # Создаем матрицу, заполненную нулями
    matrix = np.zeros((n, n))
    
    # Заполняем матрицу значениями из базы
    for entry in matrix_entries:
        i, j, value = entry.row, entry.col, entry.value
        if value is not None and value > 0:
            matrix[i][j] = value
            # Для матрицы сравнений добавляем обратное значение
            if i != j:
                matrix[j][i] = 1 / value

    # Нормируем столбцы
    col_sums = matrix.sum(axis=0)
    # Избегаем деления на ноль
    col_sums[col_sums == 0] = 1
    normalized_matrix = matrix / col_sums

    # Вычисляем среднее по строкам - это и есть веса индикаторов
    weights_vector = normalized_matrix.mean(axis=1)
    
    return weights_vector.tolist()

# --- НАЧАЛО БЛОКА ДЛЯ ДОБАВЛЕНИЯ ТЕСТОВЫХ ДАННЫХ ---


def populate_test_data():
    """
    Заполняет базу данных тестовыми данными, если она пуста.
    """
    session = Session()
    try:
        # Проверяем, есть ли уже группы в базе
        # func.count(Group.id) посчитает количество записей
        group_count = session.query(func.count(Group.id)).scalar()

        # Если групп нет (count == 0), добавляем данные
        if group_count == 0:
            print("[INFO] База данных пуста. Добавляем тестовые данные...")

            # --- ДОБАВЛЯЕМ ГРУППЫ ---
            # Создаем группу "Фанаты"
            group_fans = add_group(name="Фанаты", code="ФАН")

            # Создаем группу "Болельщики"
            group_supporters = add_group(name="Болельщики", code="БОЛ")

            # --- ДОБАВЛЯЕМ ПОКАЗАТЕЛИ (ИНДИКАТОРЫ) ---
            # Добавляем показатели для группы "Фанаты" (код "ФАН")
            insert_indicator(name="Прибыль", code="IND-PROFIT-1", group_code="ФАН")
            insert_indicator(name="Риск", code="IND-RISK-1", group_code="ФАН")

            # Добавляем показатели для группы "Болельщики" (код "БОЛ")
            insert_indicator(name="Лояльность", code="IND-LOYALTY-1", group_code="БОЛ")
            insert_indicator(name="Охват", code="IND-REACH-1", group_code="БОЛ")

            print("[SUCCESS] Тестовые данные добавлены в базу.")
        else:
            print(
                "[INFO] В базе данных уже есть записи. Пропускаем добавление тестовых данных."
            )

    except Exception as e:
        # Если что-то пошло не так, выводим ошибку
        print(f"[ERROR] Не удалось добавить тестовые данные: {e}")
    finally:
        # Гарантируем закрытие сессии
        session.close()


# --- ВЫЗЫВАЕМ ФУНКЦИЮ ПОСЛЕ СОЗДАНИЯ БАЗЫ И МИГРАЦИИ ---
populate_test_data()

# --- КОНЕЦ БЛОКА ---