import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext, simpledialog, filedialog
import feedparser
import json
import os
import webbrowser
import threading
import time
import logging
from concurrent.futures import ThreadPoolExecutor
from plyer import notification
import ttkbootstrap as ttkb
from ttkbootstrap.constants import *

# Конфигурация логирования
logging.basicConfig(
    filename='rss_reader.log',
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

FEEDS_FILE = 'feeds.json'
MAX_THREADS = 5  # Максимальное количество потоков для обработки обновлений

class RSSReader(ttkb.Window):
    def __init__(self):
        super().__init__(themename="light")  # Тема по умолчанию
        self.title("Профессиональный RSS Парсер")
        self.geometry("1200x800")
        self.resizable(True, True)

        # Инициализация данных
        self.feeds = []
        self.executor = ThreadPoolExecutor(max_workers=MAX_THREADS)
        self.stop_event = threading.Event()

        # Настройки приложения
        self.auto_update_interval = 300  # По умолчанию 5 минут

        # Создание компонентов интерфейса
        self.create_menu()
        self.create_widgets()
        self.load_feeds()
        self.start_auto_update()

    def create_menu(self):
        """Создание верхнего меню приложения."""
        menubar = tk.Menu(self)
        self.config(menu=menubar)

        # Меню "Файл"
        file_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="Файл", menu=file_menu)
        file_menu.add_command(label="Экспорт лент", command=self.export_feeds)
        file_menu.add_command(label="Импорт лент", command=self.import_feeds)
        file_menu.add_separator()
        file_menu.add_command(label="Выход", command=self.on_close)

        # Меню "Настройки"
        settings_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="Настройки", menu=settings_menu)
        settings_menu.add_command(label="Интервал обновления", command=self.set_update_interval)
        settings_menu.add_command(label="Выбрать тему", command=self.choose_theme)

    def create_widgets(self):
        """Создание основных компонентов интерфейса."""
        # Основной фрейм
        main_frame = ttk.Frame(self)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # Левый фрейм для управления
        control_frame = ttk.Frame(main_frame, width=350)
        control_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0,10))

        # Вкладки для управления
        tab_control = ttk.Notebook(control_frame)
        tab_control.pack(fill=tk.BOTH, expand=True)

        # Вкладка "Ленты"
        feed_tab = ttk.Frame(tab_control)
        tab_control.add(feed_tab, text='Ленты')

        # Поле ввода URL
        url_label = ttk.Label(feed_tab, text="RSS URL:")
        url_label.pack(pady=(10,5), anchor='w')

        self.url_entry = ttk.Entry(feed_tab, width=40)
        self.url_entry.pack(pady=(0,10), anchor='w')

        # Выпадающий список категорий
        category_label = ttk.Label(feed_tab, text="Категория:")
        category_label.pack(pady=(0,5), anchor='w')

        self.category_var = tk.StringVar()
        self.category_combobox = ttk.Combobox(feed_tab, textvariable=self.category_var, state='readonly')
        self.category_combobox['values'] = self.get_categories()
        self.category_combobox.pack(pady=(0,10), anchor='w')
        self.category_combobox.set("Без категории")

        # Кнопка добавления URL
        add_btn = ttk.Button(feed_tab, text="Добавить", command=self.add_feed)
        add_btn.pack(pady=(0,10), fill=tk.X)

        # Список добавленных лент с категориями
        feed_label = ttk.Label(feed_tab, text="Добавленные ленты:")
        feed_label.pack(anchor='w')

        self.feed_listbox = tk.Listbox(feed_tab, height=10)
        self.feed_listbox.pack(fill=tk.BOTH, expand=True, pady=(5,10))
        self.feed_listbox.bind('<<ListboxSelect>>', self.display_feed)

        # Кнопки управления
        btn_frame = ttk.Frame(feed_tab)
        btn_frame.pack(fill=tk.X, pady=(0,10))

        refresh_btn = ttk.Button(btn_frame, text="Обновить", command=self.refresh_selected_feed)
        refresh_btn.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0,5))

        delete_btn = ttk.Button(btn_frame, text="Удалить", command=self.delete_feed)
        delete_btn.pack(side=tk.RIGHT, fill=tk.X, expand=True, padx=(5,0))

        # Вкладка "Категории"
        category_tab = ttk.Frame(tab_control)
        tab_control.add(category_tab, text='Категории')

        # Список категорий
        category_list_label = ttk.Label(category_tab, text="Категории:")
        category_list_label.pack(pady=(10,5), anchor='w')

        self.category_listbox = tk.Listbox(category_tab, height=10)
        self.category_listbox.pack(fill=tk.BOTH, expand=True, pady=(5,10))
        self.category_listbox.bind('<<ListboxSelect>>', self.filter_by_category)

        # Кнопки управления категориями
        category_btn_frame = ttk.Frame(category_tab)
        category_btn_frame.pack(fill=tk.X, pady=(0,10))

        add_category_btn = ttk.Button(category_btn_frame, text="Добавить категорию", command=self.add_category)
        add_category_btn.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0,5))

        delete_category_btn = ttk.Button(category_btn_frame, text="Удалить категорию", command=self.delete_category)
        delete_category_btn.pack(side=tk.RIGHT, fill=tk.X, expand=True, padx=(5,0))

        # Поиск статей
        search_label = ttk.Label(control_frame, text="Поиск статей:")
        search_label.pack(anchor='w', pady=(10,5))

        self.search_var = tk.StringVar()
        self.search_var.trace('w', self.update_search)
        search_entry = ttk.Entry(control_frame, textvariable=self.search_var, width=35)
        search_entry.pack(pady=(0,10), anchor='w')

        # Правый фрейм для отображения результатов
        result_frame = ttk.Frame(main_frame)
        result_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)

        # Заголовок выбранной ленты
        self.feed_title = ttk.Label(result_frame, text="Выберите RSS ленту слева", font=("Helvetica", 16))
        self.feed_title.pack(pady=(0,10))

        # Область для отображения контента в виде Treeview
        columns = ("Заголовок", "Дата", "Статус")
        self.content_tree = ttk.Treeview(result_frame, columns=columns, show='headings')
        for col in columns:
            self.content_tree.heading(col, text=col)
            self.content_tree.column(col, width=200, anchor='w')
        self.content_tree.pack(fill=tk.BOTH, expand=True)
        self.content_tree.bind("<Double-1>", self.open_article)

        # Переменная для отслеживания выбранной категории
        self.current_category = None

    def get_categories(self):
        """Получение списка уникальных категорий из лент."""
        categories = set()
        for feed in self.feeds:
            categories.add(feed.get('category', 'Без категории'))
        return sorted(categories)

    def add_category(self):
        """Добавление новой категории."""
        category = simpledialog.askstring("Добавить категорию", "Введите название категории:")
        if category:
            category = category.strip()
            if category in self.category_combobox['values']:
                messagebox.showwarning("Предупреждение", "Такая категория уже существует.")
            else:
                self.category_combobox['values'] = self.get_categories() + [category]
                self.category_listbox.insert(tk.END, category)
                messagebox.showinfo("Информация", f"Категория '{category}' добавлена.")
                self.save_feeds()

    def delete_category(self):
        """Удаление выбранной категории."""
        selection = self.category_listbox.curselection()
        if not selection:
            messagebox.showwarning("Предупреждение", "Пожалуйста, выберите категорию для удаления.")
            return
        index = selection[0]
        category = self.category_listbox.get(index)
        if category == "Без категории":
            messagebox.showwarning("Предупреждение", "Нельзя удалить стандартную категорию.")
            return
        confirm = messagebox.askyesno("Подтверждение", f"Вы уверены, что хотите удалить категорию '{category}'?")
        if confirm:
            # Удаляем категорию из всех лент
            for feed in self.feeds:
                if feed.get('category') == category:
                    feed['category'] = "Без категории"
            self.save_feeds()
            self.load_feeds()
            messagebox.showinfo("Информация", f"Категория '{category}' удалена.")

    def add_feed(self):
        """Добавление новой RSS-ленты."""
        url = self.url_entry.get().strip()
        category = self.category_var.get()
        if not url:
            messagebox.showwarning("Предупреждение", "Пожалуйста, введите URL.")
            return
        # Проверка корректности URL
        try:
            feed = feedparser.parse(url)
            if feed.bozo:
                raise ValueError("Невозможно парсить данный RSS URL.")
            title = feed.feed.get('title', 'Без названия')
        except Exception as e:
            logging.error(f"Ошибка при добавлении ленты: {e}")
            messagebox.showerror("Ошибка", "Невозможно парсить данный RSS URL.")
            return

        if any(f['url'] == url for f in self.feeds):
            messagebox.showwarning("Предупреждение", "Эта лента уже добавлена.")
            return

        latest_entry = feed.entries[0].get('id', feed.entries[0].get('link')) if feed.entries else None
        self.feeds.append({
            'url': url,
            'category': category,
            'last_entry': latest_entry
        })
        self.feed_listbox.insert(tk.END, title)
        self.save_feeds()
        self.url_entry.delete(0, tk.END)
        self.category_combobox['values'] = self.get_categories()
        self.category_combobox.set("Без категории")
        logging.info(f"Добавлена новая лента: {title} ({url})")

    def display_feed(self, event):
        """Отображение статей выбранной ленты."""
        selection = self.feed_listbox.curselection()
        if not selection:
            return
        index = selection[0]
        feed_info = self.feeds[index]
        url = feed_info['url']
        self.feed_title.config(text="Загрузка...")
        self.executor.submit(self.fetch_feed, index, url)

    def fetch_feed(self, index, url):
        """Асинхронное получение данных ленты."""
        try:
            feed = feedparser.parse(url)
            title = feed.feed.get('title', 'Без названия')
            entries = feed.entries
            self.feeds[index]['last_entry'] = entries[0].get('id', entries[0].get('link')) if entries else None
            # Обновление интерфейса должно выполняться в основном потоке
            self.after(0, self.update_feed_display, index, title, entries)
            logging.info(f"Лента '{title}' загружена.")
        except Exception as e:
            logging.error(f"Ошибка при загрузке ленты {url}: {e}")
            self.after(0, lambda: messagebox.showerror("Ошибка", f"Не удалось загрузить ленту: {e}"))

    def update_feed_display(self, index, title, entries):
        """Обновление интерфейса после получения данных ленты."""
        self.feed_listbox.delete(index)
        self.feed_listbox.insert(index, title)
        self.feed_title.config(text=title)
        # Очистка Treeview
        for item in self.content_tree.get_children():
            self.content_tree.delete(item)
        # Добавление статей в Treeview
        for entry in entries:
            entry_title = entry.get('title', 'Без заголовка')
            published = entry.get('published', 'Не указана')
            link = entry.get('link', 'Нет ссылки')
            # Проверка на прочитанность
            read = entry.get('read', False)
            status = "Прочитана" if read else "Не прочитана"
            self.content_tree.insert('', tk.END, values=(entry_title, published, status), tags=('read',) if read else ())
        # Применение стиля для прочитанных статей
        self.content_tree.tag_configure('read', foreground='gray')

    def refresh_selected_feed(self):
        """Обновление выбранной ленты."""
        selection = self.feed_listbox.curselection()
        if not selection:
            messagebox.showwarning("Предупреждение", "Пожалуйста, выберите ленту для обновления.")
            return
        index = selection[0]
        feed_info = self.feeds[index]
        url = feed_info['url']
        self.feed_title.config(text="Обновление...")
        self.executor.submit(self.fetch_feed, index, url)

    def delete_feed(self):
        """Удаление выбранной ленты."""
        selection = self.feed_listbox.curselection()
        if not selection:
            messagebox.showwarning("Предупреждение", "Пожалуйста, выберите ленту для удаления.")
            return
        index = selection[0]
        title = self.feed_listbox.get(index)
        confirm = messagebox.askyesno("Подтверждение", f"Вы уверены, что хотите удалить ленту '{title}'?")
        if confirm:
            self.feed_listbox.delete(index)
            del self.feeds[index]
            self.save_feeds()
            # Очистка отображаемых статей, если удаляемая лента была выбрана
            self.content_tree.delete(*self.content_tree.get_children())
            self.feed_title.config(text="Выберите RSS ленту слева")
            self.category_combobox['values'] = self.get_categories()
            logging.info(f"Лента '{title}' удалена.")

    def open_article(self, event):
        """Открытие выбранной статьи в веб-браузере."""
        selected_item = self.content_tree.selection()
        if not selected_item:
            return
        item = self.content_tree.item(selected_item)
        entry_title = item['values'][0]
        published = item['values'][1]
        status = item['values'][2]

        # Поиск ленты и статьи по заголовку
        selection = self.feed_listbox.curselection()
        if not selection:
            return
        index = selection[0]
        feed_info = self.feeds[index]
        url = feed_info['url']
        feed = feedparser.parse(url)
        entries = feed.entries
        link = "Нет ссылки"
        for entry in entries:
            if entry.get('title', '') == entry_title:
                link = entry.get('link', 'Нет ссылки')
                # Пометить статью как прочитанную
                entry['read'] = True
                break
        if link and link != 'Нет ссылки':
            webbrowser.open(link)
            # Обновить статус в Treeview
            self.content_tree.set(selected_item, column="Статус", value="Прочитана")
            self.content_tree.item(selected_item, tags=('read',))
            self.content_tree.tag_configure('read', foreground='gray')
            # Сохранить изменения
            self.save_feeds()

    def save_feeds(self):
        """Сохранение списка лент в файл JSON."""
        try:
            with open(FEEDS_FILE, 'w', encoding='utf-8') as f:
                json.dump(self.feeds, f, ensure_ascii=False, indent=4)
            logging.info("Ленты успешно сохранены.")
        except Exception as e:
            logging.error(f"Ошибка при сохранении лент: {e}")
            messagebox.showerror("Ошибка", f"Не удалось сохранить ленты: {e}")

    def load_feeds(self):
        """Загрузка списка лент из файла JSON."""
        if os.path.exists(FEEDS_FILE):
            try:
                with open(FEEDS_FILE, 'r', encoding='utf-8') as f:
                    self.feeds = json.load(f)
                # Обновление категорий
                self.category_combobox['values'] = self.get_categories()
                self.category_listbox.delete(0, tk.END)
                self.category_listbox.insert(tk.END, "Без категории")
                for category in self.get_categories():
                    self.category_listbox.insert(tk.END, category)
                # Загрузка лент в listbox
                for feed in self.feeds:
                    self.feed_listbox.insert(tk.END, "Загрузка...")
                    self.executor.submit(self.fetch_feed, self.feeds.index(feed), feed['url'])
                logging.info("Ленты успешно загружены.")
            except Exception as e:
                logging.error(f"Ошибка при загрузке лент: {e}")
                messagebox.showerror("Ошибка", f"Не удалось загрузить сохранённые ленты: {e}")

    def filter_by_category(self, event):
        """Фильтрация лент по выбранной категории."""
        selection = self.category_listbox.curselection()
        if not selection:
            return
        index = selection[0]
        category = self.category_listbox.get(index)
        self.feed_listbox.delete(0, tk.END)
        for feed in self.feeds:
            if feed.get('category', 'Без категории') == category:
                parsed_feed = feedparser.parse(feed['url'])
                title = parsed_feed.feed.get('title', 'Без названия')
                self.feed_listbox.insert(tk.END, title)
        self.feed_title.config(text=f"Категория: {category}")
        self.content_tree.delete(*self.content_tree.get_children())
        logging.info(f"Фильтрация по категории: {category}")

    def update_search(self, *args):
        """Поиск статей по ключевым словам."""
        query = self.search_var.get().lower()
        selected_feed = self.feed_listbox.curselection()
        if not selected_feed:
            return
        index = selected_feed[0]
        feed_info = self.feeds[index]
        url = feed_info['url']
        feed = feedparser.parse(url)
        entries = feed.entries
        # Фильтрация статей
        if query:
            filtered = [
                entry for entry in entries
                if query in entry.get('title', '').lower() or query in entry.get('summary', '').lower()
            ]
        else:
            filtered = entries
        # Обновление Treeview
        self.content_tree.delete(*self.content_tree.get_children())
        for entry in filtered:
            entry_title = entry.get('title', 'Без заголовка')
            published = entry.get('published', 'Не указана')
            link = entry.get('link', 'Нет ссылки')
            read = entry.get('read', False)
            status = "Прочитана" if read else "Не прочитана"
            self.content_tree.insert('', tk.END, values=(entry_title, published, status), tags=('read',) if read else ())
        # Применение стиля для прочитанных статей
        self.content_tree.tag_configure('read', foreground='gray')
        logging.info(f"Выполнен поиск по запросу: {query}")

    def export_feeds(self):
        """Экспорт списка лент в файл JSON."""
        filepath = filedialog.asksaveasfilename(
            defaultextension=".json",
            filetypes=[("JSON files", "*.json")],
            title="Экспортировать ленты"
        )
        if filepath:
            try:
                with open(filepath, 'w', encoding='utf-8') as f:
                    json.dump(self.feeds, f, ensure_ascii=False, indent=4)
                messagebox.showinfo("Успех", f"Ленты успешно экспортированы в {filepath}")
                logging.info(f"Ленты экспортированы в {filepath}")
            except Exception as e:
                logging.error(f"Ошибка при экспорте лент: {e}")
                messagebox.showerror("Ошибка", f"Не удалось экспортировать ленты: {e}")

    def import_feeds(self):
        """Импорт списка лент из файла JSON."""
        filepath = filedialog.askopenfilename(
            defaultextension=".json",
            filetypes=[("JSON files", "*.json")],
            title="Импортировать ленты"
        )
        if filepath:
            try:
                with open(filepath, 'r', encoding='utf-8') as f:
                    imported_feeds = json.load(f)
                # Проверка на дубликаты
                for feed in imported_feeds:
                    if not any(f['url'] == feed['url'] for f in self.feeds):
                        self.feeds.append(feed)
                        self.feed_listbox.insert(tk.END, "Загрузка...")
                        self.executor.submit(self.fetch_feed, self.feeds.index(feed), feed['url'])
                self.save_feeds()
                messagebox.showinfo("Успех", f"Ленты успешно импортированы из {filepath}")
                logging.info(f"Ленты импортированы из {filepath}")
            except Exception as e:
                logging.error(f"Ошибка при импорте лент: {e}")
                messagebox.showerror("Ошибка", f"Не удалось импортировать ленты: {e}")

    def choose_theme(self):
        """Выбор темы интерфейса."""
        themes = ttkb.Style.themes()
        theme = simpledialog.askstring("Выбрать тему", f"Доступные темы:\n{', '.join(themes)}\nВведите название темы:")
        if theme in themes:
            self.style.theme_use(theme)
            logging.info(f"Выбрана тема: {theme}")
        else:
            messagebox.showwarning("Предупреждение", "Недопустимое название темы.")

    def set_update_interval(self):
        """Установка интервала автоматического обновления в минутах."""
        interval = simpledialog.askinteger(
            "Настройки",
            "Введите интервал обновления в минутах:",
            minvalue=1,
            initialvalue=self.auto_update_interval // 60
        )
        if interval:
            self.auto_update_interval = interval * 60  # перевод в секунды
            messagebox.showinfo("Информация", f"Интервал обновления установлен на {interval} минут(ы).")
            logging.info(f"Интервал обновления установлен на {interval} минут(ы).")

    def fetch_feed(self, index, url):
        """Асинхронное получение данных ленты."""
        try:
            feed = feedparser.parse(url)
            title = feed.feed.get('title', 'Без названия')
            entries = feed.entries
            latest_entry = entries[0].get('id', entries[0].get('link')) if entries else None

            # Проверка наличия новых статей
            if self.feeds[index].get('last_entry') != latest_entry and latest_entry is not None:
                self.feeds[index]['last_entry'] = latest_entry
                self.save_feeds()
                if self.feeds[index].get('last_entry'):
                    notification.notify(
                        title="Новая статья",
                        message=f"В ленте '{title}' появилась новая статья: {entries[0].get('title', 'Без названия')}",
                        timeout=5
                    )
            # Обновление интерфейса должно выполняться в основном потоке
            self.after(0, self.update_feed_display, index, title, entries)
            logging.info(f"Лента '{title}' успешно обновлена.")
        except Exception as e:
            logging.error(f"Ошибка при обновлении ленты {url}: {e}")
            self.after(0, lambda: messagebox.showerror("Ошибка", f"Не удалось обновить ленту: {e}"))

    def start_auto_update(self):
        """Запуск процесса автоматического обновления лент."""
        self.auto_update_thread = threading.Thread(target=self.auto_update_feeds, daemon=True)
        self.auto_update_thread.start()
        logging.info("Запущен автоматический процесс обновления лент.")

    def auto_update_feeds(self):
        """Фоновый поток для автоматического обновления лент."""
        while not self.stop_event.is_set():
            time.sleep(self.auto_update_interval)
            logging.info("Автоматическое обновление лент.")
            for index, feed_info in enumerate(self.feeds):
                url = feed_info['url']
                self.executor.submit(self.fetch_feed, index, url)
            logging.info("Автоматическое обновление завершено.")

    def on_close(self):
        """Обработка закрытия приложения."""
        self.stop_event.set()
        self.executor.shutdown(wait=False)
        logging.info("Приложение закрывается.")
        self.destroy()

    def export_feeds(self):
        """Экспорт списка лент в файл JSON."""
        filepath = filedialog.asksaveasfilename(
            defaultextension=".json",
            filetypes=[("JSON files", "*.json")],
            title="Экспортировать ленты"
        )
        if filepath:
            try:
                with open(filepath, 'w', encoding='utf-8') as f:
                    json.dump(self.feeds, f, ensure_ascii=False, indent=4)
                messagebox.showinfo("Успех", f"Ленты успешно экспортированы в {filepath}")
                logging.info(f"Ленты экспортированы в {filepath}")
            except Exception as e:
                logging.error(f"Ошибка при экспорте лент: {e}")
                messagebox.showerror("Ошибка", f"Не удалось экспортировать ленты: {e}")

    def import_feeds(self):
        """Импорт списка лент из файла JSON."""
        filepath = filedialog.askopenfilename(
            defaultextension=".json",
            filetypes=[("JSON files", "*.json")],
            title="Импортировать ленты"
        )
        if filepath:
            try:
                with open(filepath, 'r', encoding='utf-8') as f:
                    imported_feeds = json.load(f)
                # Проверка на дубликаты
                new_feeds = 0
                for feed in imported_feeds:
                    if not any(f['url'] == feed['url'] for f in self.feeds):
                        self.feeds.append(feed)
                        self.feed_listbox.insert(tk.END, "Загрузка...")
                        self.executor.submit(self.fetch_feed, self.feeds.index(feed), feed['url'])
                        new_feeds += 1
                self.save_feeds()
                messagebox.showinfo("Успех", f"Ленты успешно импортированы из {filepath}. Добавлено {new_feeds} новых лент.")
                logging.info(f"Ленты импортированы из {filepath}. Добавлено {new_feeds} новых лент.")
            except Exception as e:
                logging.error(f"Ошибка при импорте лент: {e}")
                messagebox.showerror("Ошибка", f"Не удалось импортировать ленты: {e}")

    def choose_theme(self):
        """Выбор темы интерфейса."""
        themes = ttkb.Style.themes()
        theme = simpledialog.askstring("Выбрать тему", f"Доступные темы:\n{', '.join(themes)}\nВведите название темы:")
        if theme and theme in themes:
            self.style.theme_use(theme)
            logging.info(f"Выбрана тема: {theme}")
        else:
            messagebox.showwarning("Предупреждение", "Недопустимое название темы.")

    def on_close(self):
        """Обработка закрытия приложения."""
        self.stop_event.set()
        self.executor.shutdown(wait=False)
        logging.info("Приложение закрывается.")
        self.destroy()

    def run(self):
        """Запуск основного цикла приложения."""
        self.protocol("WM_DELETE_WINDOW", self.on_close)
        self.mainloop()

if __name__ == "__main__":
    app = RSSReader()
    app.run()