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


# -*- coding: utf-8 -*-
# gui/forecast_window.py

import tkinter as tk
import sqlite3
from tkinter import ttk
from modules.forecasting import monte_carlo_forecast
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np

# --- НОВЫЙ БЛОК: Класс для виджета Аккордеон (финальный) ---
class AccordionFrame(ttk.Frame):
    """
    Виджет "Аккордеон" для tkinter с автоматическим выравниванием заголовков.
    Использует временную метку для измерения ширины текста.
    """
    def __init__(self, parent, root_window, **kwargs):
        super().__init__(parent, **kwargs)
        self.parent = parent
        self.root_window = root_window
        self.panels = {}
        self.current_open = None
        self.max_button_width = 0

        self.header_frame = ttk.Frame(self)
        self.header_frame.pack(fill="x", padx=10, pady=(10, 0))

        self.content_frame = ttk.Frame(self)
        self.content_frame.pack(fill="both", expand=True, padx=10, pady=5)

    def add_panel(self, title, content_frame):
        """
        Добавляет новую панель в аккордеон.
        Автоматически выравнивает ширину всех кнопок-заголовков.
        """
        self.panels[title] = {'frame': content_frame, 'state': 'closed'}

        def on_click():
            if self.current_open == title:
                self._hide_panel(title)
                self.current_open = None
            else:
                if self.current_open:
                    self._hide_panel(self.current_open)
                self._show_panel(title)
                self.current_open = title

        btn = ttk.Button(self.header_frame, text=f"+ {title}", command=on_click)
        
        # --- ИСПРАВЛЕННАЯ ЛОГИКА ИЗМЕРЕНИЯ ---
        # Создаем временную метку (Label) для измерения ширины текста.
        # Это самый надежный способ в Tkinter.
        try:
            temp_label = tk.Label(self.root_window)
            text_width = temp_label.winfo_font().measure(f"+ {title}")
            temp_label.destroy() # Убираем временный виджет

            if text_width > self.max_button_width:
                self.max_button_width = text_width

            for panel_info in self.panels.values():
                if 'button' in panel_info:
                    panel_info['button'].config(width=self.max_button_width)
                    
        except Exception as e:
            print(f"[DEBUG] Ошибка при выравнивании заголовков: {e}. Заголовки могут быть неровными.")
        
        btn.pack(anchor='w', pady=2, fill='x')
        self.panels[title]['button'] = btn

    def _show_panel(self, title):
        """Показывает панель"""
        panel_info = self.panels[title]
        panel_info['frame'].pack(fill="both", expand=True, padx=10, pady=10)
        panel_info['button'].config(text=f"- {title}")

    def _hide_panel(self, title):
        """Скрывает панель"""
        panel_info = self.panels[title]
        panel_info['frame'].pack_forget()
        panel_info['button'].config(text=f"+ {title}")

class ForecastWindow:
    """
    Окно для отображения прогноза.
    Принимает на вход родительское окно (root), чтобы иметь доступ к его данным.
    """

    def __init__(self, parent):
        # Сохраняем ссылку на главное окно
        self.root_window = parent

        # Создаем новое окно (Toplevel)
        self.window = tk.Toplevel(parent)
        self.window.title("Прогнозирование")
        self.window.geometry("1050x650")
        self.window.resizable(True, True)

        # --- ГЛАВНЫЙ КОНТЕЙНЕР ---
        main_container = ttk.Frame(self.window)
        main_container.pack(fill="both", expand=True)

        # Делим главное окно на две части: Верх (График+Настройки) и Низ (Результаты)
        main_container.grid_rowconfigure(0, weight=4)  # Верхняя часть выше
        main_container.grid_rowconfigure(1, weight=1)  # Нижняя часть ниже

        # --- ВЕРХНИЙ БЛОК (График и Настройки) ---
        top_block = ttk.Frame(main_container)
        top_block.grid(row=0, column=0, sticky="nsew")

        # Внутри верхнего блока делим на два фрейма: График (слева) и Настройки (справа)
        top_block.grid_columnconfigure(0, weight=3)  # График шире
        top_block.grid_columnconfigure(1, weight=2)  # Настройки уже

        # 1. Левая панель: График
        graph_frame = ttk.LabelFrame(top_block, text="График прогноза", padding="10")
        graph_frame.grid(row=0, column=0, sticky="nsew")

        self.figure = Figure(figsize=(5, 4), dpi=100)
        self.ax = self.figure.add_subplot(111)
        self.ax.grid(True, linestyle='--', alpha=0.7)

        self.canvas = FigureCanvasTkAgg(self.figure, master=graph_frame)
        self.canvas.draw()
        self.canvas.get_tk_widget().pack(fill="both", expand=True)

        # --- ПРАВАЯ ПАНЕЛЬ: ЗАМЕНА НА АККОРДЕОН ---
        # ИСПРАВЛЕНО: Передаем self.window как второй аргумент
        self.accordion = AccordionFrame(top_block, self.window)
        self.accordion.grid(row=0, column=1, sticky="ns")

        # --- СОЗДАЕМ ФРЕЙМЫ ДЛЯ КАЖДОЙ ПАНЕЛИ АККОРДЕОНА ---

        # Панель 1: Выбор проекта
        project_panel = ttk.Frame(self.accordion.content_frame)
        ttk.Label(project_panel, text="Выбрать проект:").pack(side="left", anchor="w", padx=(0, 5))
        self.project_combobox = ttk.Combobox(project_panel, state="readonly", width=25)
        self.project_combobox.pack(side="left", anchor="w")

        self.refresh_button = ttk.Button(project_panel, text="Обновить данные", command=self.load_projects)
        self.refresh_button.pack(side="left", anchor="w", padx=(10, 0))

        self.accordion.add_panel("Основные настройки", project_panel)

        # Панель 2: Параметры модели
        model_panel = ttk.Frame(self.accordion.content_frame)

        input_frame = ttk.Frame(model_panel)
        input_frame.pack(fill="x", padx=10, pady=(5, 0))

        ttk.Label(input_frame, text="Интенсивность событий (в день):").pack(side="left", anchor="w", padx=(0, 10))
        self.lambda_entry = ttk.Entry(input_frame, width=10)
        self.lambda_entry.insert(0, "5")
        self.lambda_entry.pack(side="left", anchor="w")

        ttk.Label(input_frame, text="Шаг (дни):").pack(side="left", anchor="w", padx=(20, 10))
        self.step_entry = ttk.Entry(input_frame, width=10)
        self.step_entry.insert(0, "1")
        self.step_entry.pack(side="left", anchor="w")

        # Поле "Прогноз на:"
        period_frame = ttk.Frame(model_panel)
        period_frame.pack(fill="x", padx=10, pady=(5, 15))

        ttk.Label(period_frame, text="Прогноз на:").pack(side="left", anchor="w", padx=(0, 10))
        self.period_combobox = ttk.Combobox(period_frame, state="readonly",
                                      values=["Месяц (30 дней)", "Квартал (90 дней)", "Полгода (180 дней)"],
                                      width=25)
        self.period_combobox.current(0)
        self.period_combobox.pack(side="left", anchor="w")

        self.accordion.add_panel("Параметры модели", model_panel)

        # Панель 3: Настройки симуляции
        sim_panel = ttk.Frame(self.accordion.content_frame)

        sim_settings_frame = ttk.Frame(sim_panel)
        sim_settings_frame.pack(fill="x", padx=10, pady=(5, 25))

        ttk.Label(sim_settings_frame, text="Кол-во итераций:\n(в симуляции)", justify="left").pack(
         side="left", anchor="nw", padx=(0, 10))
         
        self.iterations_entry = ttk.Entry(sim_settings_frame, width=25)
        self.iterations_entry.insert(0, "10000")
        self.iterations_entry.pack(side="left", anchor="w")

        # ДОБАВЛЯЕМ ПАНЕЛЬ СРАЗУ ПОСЛЕ ЕЁ СОЗДАНИЯ
        self.accordion.add_panel("Настройки симуляции", sim_panel)

        # --- ДОБАВЛЯЕМ НОВЫЕ ПАНЕЛИ С ЗАГЛУШКАМИ ---

        # Панель 4: Заголовок 4
        panel4 = ttk.Frame(self.accordion.content_frame)
        tk.Label(panel4, text="Это заглушка для четвертой панели. Здесь будет функционал.").pack(pady=20)
        self.accordion.add_panel("Заголовок 4", panel4)

        # Панель 5: Заголовок 5
        panel5 = ttk.Frame(self.accordion.content_frame)
        tk.Label(panel5, text="Это заглушка для пятой панели. Здесь будет функционал.").pack(pady=20)
        self.accordion.add_panel("Заголовок 5", panel5)

        # Панель 6: Заголовок 6
        panel6 = ttk.Frame(self.accordion.content_frame)
        tk.Label(panel6, text="Это заглушка для шестой панели. Здесь будет функционал.").pack(pady=20)
        self.accordion.add_panel("Заголовок 6", panel6)


         
        # --- КНОПКА "ПОСТРОИТЬ ПРОГНОЗ" ---
        # Размещаем её под аккордеоном в той же колонке (column=1), но в новой строке (row=1)
        button_frame = ttk.Frame(top_block)
        button_frame.grid(row=1, column=1) 

        self.build_button = ttk.Button(button_frame, text="Построить прогноз", command=self.run_forecast)
        self.build_button.pack(pady=30)


        # --- НИЖНИЙ БЛОК (Результаты расчета) ---
        result_frame = ttk.LabelFrame(main_container, text="Результаты расчета", padding="5")
        result_frame.grid(row=1, column=0, sticky="ew")

        self.result_text = tk.Text(result_frame, height=2, wrap='word', state='disabled')
        scrollbar = ttk.Scrollbar(result_frame, command=self.result_text.yview)
        scrollbar.pack(side="right", fill="y")
        self.result_text.configure(yscrollcommand=scrollbar.set)
        self.result_text.pack(padx=10, pady=5, fill="x")

    def _insert_into_result(self, text):
        """Вставляет текст в поле результатов."""
        self.result_text.config(state='normal')
        self.result_text.insert(tk.END, text + "\n") # Добавляем \n для красоты вывода
        self.result_text.config(state='disabled')
        self.result_text.see(tk.END) # Автоматически прокручивает вниз

    def _clear_result_text(self):
        """Очищает поле результатов перед новым расчетом."""
        self.result_text.config(state='normal')
        self.result_text.delete(1.0, tk.END)
        self.result_text.config(state='disabled')
  
    def load_projects(self):
        """
        Загружает список проектов из главного окна в выпадающий список.
        """
        try:
            projects = self.root_window.project_manager.list_projects()
            project_names = [p.name for p in projects]
            project_ids = [p.id for p in projects]
            self.project_combobox['values'] = project_names
            if project_names:
                self.project_combobox.current(0)
                return True
            else:
                print("Нет доступных проектов.")
                return False
                
        except AttributeError:
            print("Ошибка: Не найден project_manager в главном окне.")
            return False

    def run_forecast(self):
        """
        Запускает процесс прогнозирования и отрисовывает результаты.
        """
        # Очищаем поле результатов перед новым расчетом
        self._clear_result_text()

        # --- ВАЛИДАЦИЯ ВХОДНЫХ ДАННЫХ И ОСНОВНАЯ ЛОГИКА В БЛОКЕ TRY ---
        try:
            iterations = int(self.iterations_entry.get())
            lambda_val = float(self.lambda_entry.get())
            
            selected_period = self.period_combobox.get()
            days_forward_dict = {"Месяц": 30, "Квартал": 90, "Полгода": 180}
            period_key = next((k for k in days_forward_dict if k in selected_period), None)
            days_forward = days_forward_dict.get(period_key, 30)
                
            step_days = int(self.step_entry.get())

            # Проверка на положительные значения
            if iterations <= 0 or lambda_val <= 0 or days_forward <= 0 or step_days <= 0:
                raise ValueError("Все параметры (итерации, интенсивность, период, шаг) должны быть больше нуля.")
                
            if not self.load_projects():
                raise ValueError("Не удалось загрузить список проектов. Выберите проект из списка.")

            from datetime import datetime, timedelta
            import numpy as np

            # --- ГЕНЕРАЦИЯ ТРАЕКТОРИИ И СИМУЛЯЦИЯ ---
            value_pool_positive = [1, 2, 3, 4, 5]
            trajectory_daily = []  # Для точного поиска даты по дням
            trajectory_for_plot = []  # Для отрисовки на графике с учетом шага

            current_value = 0
            start_date = datetime.now().date()
            
            for day_offset in range(days_forward + 1):
                # Генерируем события за день и суммируем их влияние
                num_events = np.random.poisson(lam=lambda_val)
                daily_sum = sum(np.random.choice(value_pool_positive) for _ in range(num_events))
                
                current_value += daily_sum
                trajectory_daily.append(current_value) # Сохраняем каждый день
                
                # Добавляем точку на график согласно шагу отображения
                if day_offset % step_days == 0 or day_offset == days_forward:
                    trajectory_for_plot.append(current_value)
                    
                # Вывод хода симуляции в КОНСОЛЬ (для отладки)
                current_date = start_date + timedelta(days=day_offset)
                print(f"{current_date}: Событий: {num_events:2d}. Итог дня: {daily_sum:6.2f}. Накоплено: {current_value:6.2f}")

            if not trajectory_for_plot:
                raise RuntimeError("Не удалось сгенерировать данные для прогноза.")

            # --- РАСЧЕТ ГРАНИЦ ПРОГНОЗА ---
            sim_result = monte_carlo_forecast(
                pool_values=trajectory_daily,
                num_simulations=iterations,
                p1=0.05,
                p2=0.95
            )
            
            final_lower = sim_result["lower_bound"]
            final_upper = sim_result["upper_bound"]
            final_median = sim_result["median"]
            
             # --- ИЩЕМ ДАТУ ПЕРЕСЕЧЕНИЯ С НИЖНЕЙ ГРАНИЦЕЙ ---
            date_of_intersection = None
            for idx, val in enumerate(trajectory_daily):
                if val >= final_lower:
                    date_of_intersection = start_date + timedelta(days=idx)
                    break

             # Формируем текст для вывода в виджет в нужном формате
            output_text = (
                 f"ПРОГНОЗ ДОСТИЖЕНИЯ ЦЕЛИ:\n"
                 f"   Цель (Нижняя граница): {final_lower:.2f}\n\n"
                 f"   Прогнозируемый диапазон значений:\n"
                 f"      Минимум: {final_lower:.2f}\n"
                 f"      Максимум: {final_upper:.2f}\n\n"
            )
 
            if date_of_intersection:
                output_text += f"Достижение минимума прогнозируется: {date_of_intersection.strftime('%d.%m.%Y')}"
            else:
                output_text += "Достижение минимума в заданный период не прогнозируется."
 
             # --- ВЫВОДИМ РЕЗУЛЬТАТ В ИНТЕРФЕЙС ---
            self._insert_into_result(output_text)
 
             # Подготовка данных для графика
            result_data = {
                 "lower_bound": final_lower,
                 "upper_bound": final_upper,
                 "median": final_median,
                 "base_level": trajectory_for_plot[0],
                 "delta_history": trajectory_for_plot,
                 "intersections": {
                     "date": date_of_intersection,
                     "value": final_lower
                }
            }
            self.update_graph(result_data)
             
        # --- БЛОК ОБРАБОТКИ ОШИБОК (EXCEPT) ---
        except ValueError as e:
            # Обработка ошибок, связанных с вводом данных (например, буквы вместо цифр)
            error_msg = f"Ошибка ввода: {e}. Проверьте все поля."
            print(error_msg)
            self._insert_into_result("ОШИБКА: " + str(e))
        except Exception as e:
            # Обработка всех остальных непредвиденных ошибок
            error_msg = f"Произошла непредвиденная ошибка: {e}"
            print(error_msg)
            self._insert_into_result("НЕПРЕДВИДЕННАЯ ОШИБКА: " + str(e))

 
    def get_project_history(self):
        """
        Получает историю проекта из БД.
        Этот метод может требовать доработки под вашу структуру базы данных.
        """
        print("--- Режим работы с реальной базой данных ---")
        
        selected_index = self.project_combobox.current()
        if selected_index == -1:
            print("Ошибка: Не выбран проект.")
            return None

        projects = self.root_window.project_manager.list_projects()
        try:
            selected_project_id = projects[selected_index].id
        except IndexError:
            print("Ошибка: Проект не найден в списке.")
            return None

        try:
            # --- ИСПРАВЛЕННЫЙ БЛОК ---
            # Весь код внутри этого 'try' должен иметь одинаковый отступ
            conn = sqlite3.connect(f"project_dbs/{selected_project_id}.db")
            cursor = conn.cursor()
            cursor.execute("""
                SELECT event_sign FROM project_data WHERE project_code = ?
            """, (selected_project_id,))
            rows = cursor.fetchall()
            conn.close()
            
            delta_history = [row[0] for row in rows if row[0] is not None]
            print(f"Динамика из БД: {delta_history}")
            return {"base_level": 0, "delta_history": delta_history}
            # --- КОНЕЦ ИСПРАВЛЕННОГО БЛОКА ---
            
        except Exception as e:
            # Блок 'except' находится на том же уровне, что и 'try'
            print(f"Ошибка при работе с базой данных: {e}")
            return None

    def update_graph(self, result_data):
        """
        Обновляет график на основе переданных данных.
        """
        lower_bound = result_data['lower_bound']
        median = result_data['median']
        upper_bound = result_data['upper_bound']
        base_level = result_data['base_level']
        trajectory = result_data['delta_history']
        
        # Очищаем график
        self.ax.clear()
        
        # Рисуем горизонтальные линии границ и медианы
        if lower_bound is not None:
            self.ax.axhline(y=lower_bound, color='red', linestyle='--', label=f'Нижняя граница')
        if median is not None:
            self.ax.axhline(y=median , color='green', linestyle='-', label=f'Медиана')
        if upper_bound is not None:
            self.ax.axhline(y=upper_bound , color='blue', linestyle='--', label=f'Верхняя граница')
        
        # Рисуем траекторию (линию графика), если есть данные
        if trajectory and len(trajectory) > 1:
            from datetime import datetime, timedelta
            
            start_date = datetime.now().date()
            step_days = int(self.step_entry.get())
            
            dates_for_plot = []
            for i in range(len(trajectory)):
                day_offset = i * step_days
                plot_date = start_date + timedelta(days=day_offset)
                dates_for_plot.append(plot_date)
            
            # Сама линия траектории
            self.ax.plot(dates_for_plot, trajectory,
                         color='orange', marker='o',
                         linewidth=2, markersize=6,
                         label='Смоделированная динамика')
            
            # Настраиваем ось X с датами
            self.ax.set_xticks(dates_for_plot)
            self.ax.set_xticklabels([d.strftime('%d.%m') for d in dates_for_plot])
        
        # Настраиваем внешний вид графика
        self.ax.set_ylabel('Накопленное значение')
        self.ax.set_xlabel('Дата')
        self.ax.grid(True, linestyle='--', alpha=0.7)
        self.ax.legend(loc='upper left')
        
        # Перерисовываем canvas ОЧЕНЬ ВАЖНО!
        self.canvas.draw()