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


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

import tkinter as tk
from tkinter import ttk, scrolledtext
from tkcalendar import DateEntry
from modules.forecast_modeling import make_forecast, generate_trajectory
from modules.forecast_visualization import update_graph # Эта функция теперь отвечает за отрисовку
import numpy as np


class AccordionFrame(ttk.Frame):
    """Виджет 'Аккордеон' для группировки настроек."""
    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):
        if title in self.panels:
            return
        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)

        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:
            pass 

        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, legend_checkbox_widget=None, project_manager=None):
        self.root_window = parent
        self.project_manager = project_manager
        self.window = tk.Toplevel(parent)
        self.window.title("Прогнозирование")
        self.window.geometry("900x700") 
        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) # Настройки уже

        graph_frame = ttk.LabelFrame(top_block, text="График прогноза", padding="10")
        graph_frame.grid(row=0, column=0, sticky="nsew")

        from matplotlib.figure import Figure
        from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

        self.figure = Figure(figsize=(5, 4), dpi=100)
        self.ax = self.figure.add_subplot(111)
        self.canvas = FigureCanvasTkAgg(self.figure, master=graph_frame)
        self.canvas.draw()
        self.canvas.get_tk_widget().pack(fill="both", expand=True)

        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)

        project_frame = ttk.Frame(project_panel)
        project_frame.pack(fill="x", pady=(0, 10))
        ttk.Label(project_frame, text="Выбрать проект:").pack(side="left", anchor="w", padx=(0, 5))
        self.project_combobox = ttk.Combobox(project_frame, state="readonly", width=25)
        self.project_combobox.pack(side="left", anchor="w")
        self.project_combobox.bind("<<ComboboxSelected>>", lambda event: self.update_start_date())

        self.refresh_button = ttk.Button(project_panel, text="Обновить данные", command=self.load_projects)
        self.refresh_button.pack(pady=(5, 15)) 

        date_frame = ttk.Frame(project_panel)
        date_frame.pack(fill="x", pady=(0, 10))
        ttk.Label(date_frame, text="Дата начала:").pack(side="left", anchor="w", padx=(0, 5))
        self.start_date_picker = DateEntry(
            date_frame,
            width=18,
            background='darkblue',
            foreground='white',
            borderwidth=2,
            date_pattern='y-mm-dd'
        )
        self.start_date_picker.pack(side="left", anchor="w")

        s_max_frame = ttk.Frame(project_panel)
        s_max_frame.pack(fill="x", pady=(0, 10))
        ttk.Label(s_max_frame, text="Верхняя граница (Smax):").pack(side="left", anchor="w", padx=(0, 5))
        self.s_max_entry = ttk.Entry(s_max_frame, width=10)
        self.s_max_entry.insert(0, "95")
        self.s_max_entry.pack(side="left", anchor="w")

        s_min_frame = ttk.Frame(project_panel)
        s_min_frame.pack(fill="x")
        ttk.Label(s_min_frame, text="Нижняя граница (Smin):").pack(side="left", anchor="w", padx=(0, 5))
        self.s_min_entry = ttk.Entry(s_min_frame, width=10)
        self.s_min_entry.insert(0, "5")
        self.s_min_entry.pack(side="left", anchor="w")

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

        # --- Панель 2: Краткосрочный прогноз ---
        model_panel = ttk.Frame(self.accordion.content_frame)
        self.manual_input_var = tk.BooleanVar(value=True)
        ttk.Checkbutton(model_panel, text="Ручной ввод", variable=self.manual_input_var,
                       command=self.toggle_manual_mode).pack(pady=(5, 10))

        intensity_frame = ttk.Frame(model_panel)
        intensity_frame.pack(fill="x")
        ttk.Label(intensity_frame, text="Интенсивность (в день):").pack(side="left")
        self.lambda_entry = ttk.Entry(intensity_frame, width=10)
        self.lambda_entry.insert(0, "5")
        self.lambda_entry.pack(side="left", padx=5)

        avg_frame = ttk.Frame(model_panel)
        avg_frame.pack(fill="x")
        ttk.Label(avg_frame, text="Среднее за:").pack(side="left")
        self.avg_combobox = ttk.Combobox(avg_frame, state='readonly', values=[3, 5, 7, 10, 30], width=5)
        self.avg_combobox.current(2)
        self.avg_combobox.pack(side="left", padx=5)

        period_frame = ttk.Frame(model_panel)
        period_frame.pack(fill="x", pady=(5, 15))
        ttk.Label(period_frame, text="Прогноз на:").pack(side="left")
        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")

        self.accordion.add_panel("Краткосрочный прогноз", model_panel)
        self.toggle_manual_mode() # Вызываем вручную для начальной блокировки полей

        # --- Панель 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="Кол-во итераций:", 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: Настройка графика ---
        panel4 = ttk.Frame(self.accordion.content_frame)
        step_days_frame = ttk.Frame(panel4)
        step_days_frame.pack(fill="x", padx=10)
        ttk.Label(step_days_frame, text="Шаг (дни):").pack(side="left")
        self.step_entry = ttk.Entry(step_days_frame, width=8)
        self.step_entry.insert(0, "1")
        self.step_entry.pack(side="left", padx=(5, 25))

        step_events_frame = ttk.Frame(panel4)
        step_events_frame.pack(fill="x", padx=10)
        ttk.Label(step_events_frame, text="Шаг (события):").pack(side="left")
        self.step_events_entry = ttk.Entry(step_events_frame, width=8)
        self.step_events_entry.insert(0, "1")
        self.step_events_entry.pack(side="left")

        self.show_legend_var = tk.BooleanVar(value=True)
        legend_checkbox = ttk.Checkbutton(
            panel4,
            text='Показать легенду',
            variable=self.show_legend_var,
            command=self.redraw_graph
        )
        legend_checkbox.pack(pady=(25, 15))
        panel4.update()
        self.accordion.add_panel("Настройка графика", panel4)

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

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

        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 = scrolledtext.ScrolledText(result_frame, height=4, 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")

        self.window.after(100, self.load_projects)

    # ---- МЕТОДЫ ДЛЯ ИНТЕРФЕЙСА ----
    def toggle_manual_mode(self):
        """Включает/выключает режим ручного ввода параметров."""
        if self.manual_input_var.get():
            self.lambda_entry.config(state='normal')
            self.avg_combobox.config(state='disabled')
        else:
            self.lambda_entry.config(state='disabled')
            self.avg_combobox.config(state='readonly')

    def redraw_graph(self):
        """
        Перерисовывает график БЕЗ повторного запуска симуляции.
        Обновляет только видимость легенды.
        """
        if not hasattr(self, '_last_result_data'):
            return

        current_state = self.show_legend_var.get()
        self._last_result_data["show_legend"] = current_state
        update_graph(self.ax, self.canvas, self._last_result_data)

    # ---- ОСНОВНОЙ МЕТОД РАСЧЕТА ПРОГНОЗА ----
    def run_forecast(self):
        """Собирает параметры и запускает расчет."""
        self._clear_result_text()
        try:
            iterations = int(self.iterations_entry.get())
            lambda_val = float(self.lambda_entry.get())
            
            selected_period_str = self.period_combobox.get()
            days_forward_dict = {
                "Месяц (30 дней)": 30,
                "Квартал (90 дней)": 90,
                "Полгода (180 дней)": 180
            }
            days_forward = days_forward_dict.get(selected_period_str, 30)

            s_min = float(self.s_min_entry.get()) / 100
            s_max = float(self.s_max_entry.get()) / 100

            results = make_forecast(lambda_val=lambda_val, days_forward=days_forward,
                                    iterations=iterations, s_min=s_min, s_max=s_max)

            final_lower = results["lower_bound"]
            final_upper = results["upper_bound"]
            final_median = results["median"]

            output_text = (
                f"ПРОГНОЗ ДОСТИЖЕНИЯ ЦЕЛИ:\n\n"
                f" Цель (Нижняя граница): {final_lower:.2f}\n\n"
                f" Прогнозируемый диапазон значений:\n"
                f" Минимум: {final_lower:.2f}\n"
                f" Максимум: {final_upper:.2f}\n\n"
            )
            self._insert_into_result(output_text)

            step_days = int(self.step_entry.get())
            trajectory_daily, _ = generate_trajectory(lambda_val, days_forward, step_days=step_days)

            self._last_result_data = {
                "lower_bound": final_lower,
                "upper_bound": final_upper,
                "median": final_median,
                "base_level": trajectory_daily[0],
                "delta_history": trajectory_daily,
                "show_legend": self.show_legend_var.get()
            }

            update_graph(self.ax, self.canvas, self._last_result_data)

        except ValueError as ve:
            error_msg = f"Ошибка ввода: {ve}"
            print(error_msg)
            self._insert_into_result(f"ОШИБКА: {str(ve)}")
        except Exception as e:
            error_msg = f"Произошла непредвиденная ошибка: {e}"
            print(error_msg)
            self._insert_into_result(f"НЕПРЕДВИДЕННАЯ ОШИБКА: {str(e)}")

    # ---- МЕТОДЫ РАБОТЫ С ТЕКСТОМ В ПОЛЕ РЕЗУЛЬТАТОВ ----
    def _insert_into_result(self, text):
        """Добавляет строку текста в поле вывода результатов."""
        self.result_text.config(state='normal')
        self.result_text.insert(tk.END, text + "\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):
        """
        Загружает список проектов в Combobox.
        НЕ сбрасывает текущий выбор пользователя.
        """
        try:
            if not self.project_manager:
                print("[DEBUG] Менеджер проектов недоступен.")
                return False

            projects = self.project_manager.list_projects()
            if not projects:
                print("Нет доступных проектов.")
                self.project_combobox['values'] = []
                return False

            # Сохраняем имя текущего выбранного проекта
            current_selection = self.project_combobox.get()

            project_names = [p.name for p in projects]
            self.project_combobox['values'] = project_names

            # Восстанавливаем выбор пользователя, если он еще актуален
            if current_selection and current_selection in project_names:
                self.project_combobox.set(current_selection)
            elif project_names: # Если старый выбор недоступен, выбираем первый
                self.project_combobox.current(0)

            # Обновляем дату начала для текущего выбранного проекта
            self.update_start_date()
            return True
        except AttributeError as e:
            print(f"[DEBUG] Ошибка при загрузке проектов: {e}")
            return False

    def update_start_date(self):
        """Устанавливает дату создания выбранного проекта в календарь."""
        selected_project_name = self.project_combobox.get()
        if not selected_project_name or not self.project_manager:
            return

        projects = self.project_manager.list_projects()
        selected_project = next((p for p in projects if p.name == selected_project_name), None)
        if not selected_project:
            return

        try:
            from modules.project_database import ProjectDatabase, TABLE_PROJECT_DATA
            db = ProjectDatabase(project_code=selected_project.id)
            conn = db.connect()
            cursor = conn.cursor()
            cursor.execute(
                f"SELECT created_at FROM {TABLE_PROJECT_DATA} WHERE project_code = ? LIMIT 1",
                (selected_project.id,)
            )
            row = cursor.fetchone()
            if row and row[0]:
                self.start_date_picker.set_date(row[0])
        except Exception as e:
            print(f"[DEBUG] Ошибка при получении даты: {e}")
        finally:
            if 'db' in locals():
                db.close()