import os
import io
import sqlite3
import requests

from PyQt5 import QtWidgets, QtGui, QtCore
from cryptography.fernet import Fernet
from PIL import Image


# ============== Работа с Базой Данных и Шифрованием ==============

class Database:
    def __init__(self, db_path="gallery.db", key_path="key.key"):
        self.db_path = db_path
        self.key_path = key_path
        self.key = self.load_key()
        self.fernet = Fernet(self.key)
        self.conn = sqlite3.connect(self.db_path)
        self.create_tables()

    def load_key(self):
        """
        Загружает ключ шифрования из файла или создаёт новый, если его нет.
        """
        if os.path.exists(self.key_path):
            with open(self.key_path, "rb") as f:
                return f.read()
        else:
            key = Fernet.generate_key()
            with open(self.key_path, "wb") as f:
                f.write(key)
            return key

    def create_tables(self):
        """
        Создаёт таблицу для хранения изображений, если её ещё нет.
        """
        cursor = self.conn.cursor()
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS images (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                category TEXT,
                filename TEXT,
                image BLOB,
                thumbnail BLOB
            )
        ''')
        self.conn.commit()

    def add_image(self, category, filename, image_bytes, thumbnail_bytes):
        """
        Шифрует и сохраняет изображение и его эскиз в базу.
        """
        encrypted_image = self.fernet.encrypt(image_bytes)
        encrypted_thumbnail = self.fernet.encrypt(thumbnail_bytes)
        cursor = self.conn.cursor()
        cursor.execute(
            "INSERT INTO images (category, filename, image, thumbnail) VALUES (?, ?, ?, ?)",
            (category, filename, encrypted_image, encrypted_thumbnail)
        )
        self.conn.commit()

    def get_images(self, category=None):
        """
        Возвращает список изображений (с расшифровкой), возможно, отфильтрованный по категории.
        """
        cursor = self.conn.cursor()
        if category and category != "Все":
            cursor.execute("SELECT id, category, filename, image, thumbnail FROM images WHERE category = ?", (category,))
        else:
            cursor.execute("SELECT id, category, filename, image, thumbnail FROM images")
        results = cursor.fetchall()
        images = []
        for row in results:
            id, cat, filename, encrypted_image, encrypted_thumbnail = row
            try:
                image_bytes = self.fernet.decrypt(encrypted_image)
                thumbnail_bytes = self.fernet.decrypt(encrypted_thumbnail)
            except Exception as e:
                print(f"Ошибка расшифровки изображения id {id}: {e}")
                continue
            images.append({
                "id": id,
                "category": cat,
                "filename": filename,
                "image": image_bytes,
                "thumbnail": thumbnail_bytes
            })
        return images


# ============== Графический Интерфейс ==============

class GalleryWindow(QtWidgets.QMainWindow):
    """
    Главное окно приложения – список категорий и сетка эскизов изображений.
    """
    def __init__(self, db):
        super().__init__()
        self.db = db
        self.init_ui()

    def init_ui(self):
        self.setWindowTitle("Галерея изображений")
        self.resize(800, 600)

        # Центральный виджет с горизонтальным делением: список категорий и область изображений
        central_widget = QtWidgets.QWidget()
        self.setCentralWidget(central_widget)
        main_layout = QtWidgets.QHBoxLayout(central_widget)

        # Список категорий (слева)
        self.category_list = QtWidgets.QListWidget()
        self.category_list.addItem("Все")
        self.category_list.itemClicked.connect(self.category_selected)
        self.load_categories()
        main_layout.addWidget(self.category_list, 1)

        # Область с изображениями (справа)
        self.image_scroll = QtWidgets.QScrollArea()
        self.image_widget = QtWidgets.QWidget()
        self.image_layout = QtWidgets.QGridLayout(self.image_widget)
        self.image_scroll.setWidgetResizable(True)
        self.image_scroll.setWidget(self.image_widget)
        main_layout.addWidget(self.image_scroll, 4)

        # Панель инструментов с кнопкой добавления нового изображения
        toolbar = self.addToolBar("Main Toolbar")
        add_action = QtWidgets.QAction("Добавить изображение", self)
        add_action.triggered.connect(self.open_add_dialog)
        toolbar.addAction(add_action)

        self.load_images()

    def load_categories(self):
        """
        Загружает список категорий из базы (уникальные значения поля category).
        """
        cursor = self.db.conn.cursor()
        cursor.execute("SELECT DISTINCT category FROM images")
        rows = cursor.fetchall()
        categories = set()
        for row in rows:
            if row[0]:
                categories.add(row[0])
        for cat in categories:
            if cat != "Все":
                self.category_list.addItem(cat)

    def category_selected(self, item):
        """
        При выборе категории – перезагружает изображения.
        """
        category = item.text()
        self.load_images(category)

    def load_images(self, category=None):
        """
        Загружает изображения (и их эскизы) из базы и размещает их в виде кнопок в сетке.
        """
        # Удаляем старые виджеты из сетки
        for i in reversed(range(self.image_layout.count())):
            widget_to_remove = self.image_layout.itemAt(i).widget()
            if widget_to_remove is not None:
                self.image_layout.removeWidget(widget_to_remove)
                widget_to_remove.setParent(None)

        images = self.db.get_images(category)
        row = 0
        col = 0
        for img in images:
            thumbnail = self.byte_to_qpixmap(img["thumbnail"])
            btn = QtWidgets.QPushButton()
            btn.setIcon(QtGui.QIcon(thumbnail))
            btn.setIconSize(QtCore.QSize(thumbnail.width(), thumbnail.height()))
            btn.setFixedSize(thumbnail.width() + 10, thumbnail.height() + 10)
            # При нажатии открываем изображение в полном размере
            btn.clicked.connect(lambda checked, img=img: self.open_full_image(img))
            self.image_layout.addWidget(btn, row, col)
            col += 1
            if col >= 4:
                col = 0
                row += 1

    def byte_to_qpixmap(self, byte_data):
        """
        Преобразует бинарные данные в QPixmap.
        """
        image = QtGui.QImage.fromData(byte_data)
        pixmap = QtGui.QPixmap.fromImage(image)
        return pixmap

    def open_full_image(self, img):
        """
        Открывает окно с изображением в полном размере.
        """
        dialog = FullImageView(img)
        dialog.exec_()

    def open_add_dialog(self):
        """
        Открывает диалог добавления нового изображения.
        """
        dialog = AddImageDialog(self.db, self)
        if dialog.exec_() == QtWidgets.QDialog.Accepted:
            # После добавления – обновляем список категорий и изображений
            self.category_list.clear()
            self.category_list.addItem("Все")
            self.load_categories()
            self.load_images()


class AddImageDialog(QtWidgets.QDialog):
    """
    Диалог добавления нового изображения.
    Пользователь может указать URL или выбрать файл, а также задать категорию.
    """
    def __init__(self, db, parent=None):
        super().__init__(parent)
        self.db = db
        self.setWindowTitle("Добавить изображение")
        self.selected_file = None
        self.init_ui()

    def init_ui(self):
        layout = QtWidgets.QVBoxLayout(self)
        form_layout = QtWidgets.QFormLayout()

        self.category_edit = QtWidgets.QLineEdit()
        self.url_edit = QtWidgets.QLineEdit()
        self.file_button = QtWidgets.QPushButton("Выбрать файл")
        self.file_button.clicked.connect(self.select_file)

        form_layout.addRow("Категория:", self.category_edit)
        form_layout.addRow("URL (если есть):", self.url_edit)
        form_layout.addRow("Файл:", self.file_button)
        layout.addLayout(form_layout)

        button_box = QtWidgets.QDialogButtonBox(
            QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
        )
        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)
        layout.addWidget(button_box)

    def select_file(self):
        file_name, _ = QtWidgets.QFileDialog.getOpenFileName(
            self, "Выберите изображение", "", "Images (*.png *.jpg *.jpeg *.bmp)"
        )
        if file_name:
            self.selected_file = file_name
            self.file_button.setText(os.path.basename(file_name))

    def accept(self):
        category = self.category_edit.text().strip() or "Без категории"
        image_bytes = None
        filename = ""
        # Приоритет: если указан URL, пробуем загрузить изображение по нему
        if self.url_edit.text().strip():
            url = self.url_edit.text().strip()
            try:
                response = requests.get(url)
                if response.status_code == 200:
                    image_bytes = response.content
                    filename = url.split("/")[-1]
                else:
                    QtWidgets.QMessageBox.warning(self, "Ошибка", "Не удалось загрузить изображение по URL")
                    return
            except Exception as e:
                QtWidgets.QMessageBox.warning(self, "Ошибка", f"Ошибка загрузки по URL: {e}")
                return
        # Если URL не указан – проверяем, выбран ли файл
        elif self.selected_file:
            try:
                with open(self.selected_file, "rb") as f:
                    image_bytes = f.read()
                    filename = os.path.basename(self.selected_file)
            except Exception as e:
                QtWidgets.QMessageBox.warning(self, "Ошибка", f"Ошибка чтения файла: {e}")
                return
        else:
            QtWidgets.QMessageBox.warning(self, "Ошибка", "Укажите URL или выберите файл")
            return

        if not image_bytes:
            QtWidgets.QMessageBox.warning(self, "Ошибка", "Не удалось получить изображение")
            return

        # Создаём эскиз с помощью Pillow
        try:
            thumb_io = io.BytesIO()
            image = Image.open(io.BytesIO(image_bytes))
            image.thumbnail((200, 200))
            image.save(thumb_io, format="PNG")
            thumbnail_bytes = thumb_io.getvalue()
        except Exception as e:
            QtWidgets.QMessageBox.warning(self, "Ошибка", f"Ошибка создания эскиза: {e}")
            return

        self.db.add_image(category, filename, image_bytes, thumbnail_bytes)
        super().accept()


class FullImageView(QtWidgets.QDialog):
    """
    Окно для просмотра изображения в полном размере с возможностью масштабирования и перемещения.
    """
    def __init__(self, img, parent=None):
        super().__init__(parent)
        self.view = None
        self.setWindowTitle(img["filename"])
        flags = self.windowFlags() | QtCore.Qt.WindowMinimizeButtonHint | QtCore.Qt.WindowMaximizeButtonHint
        self.setWindowFlags(flags)
        self.img = img
        self.init_ui()

    def init_ui(self):
        layout = QtWidgets.QVBoxLayout(self)
        self.view = ImageViewer(self.img["image"])
        layout.addWidget(self.view)


class ImageViewer(QtWidgets.QGraphicsView):
    """
    QGraphicsView с поддержкой масштабирования (колёсиком мыши) и перетаскиванием.
    """
    def __init__(self, image_bytes, parent=None):
        super().__init__(parent)
        self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
        self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
        self.scene = QtWidgets.QGraphicsScene(self)
        self.setScene(self.scene)
        image = QtGui.QImage.fromData(image_bytes)
        self.pixmap = QtGui.QPixmap.fromImage(image)
        self.pixmap_item = self.scene.addPixmap(self.pixmap)
        self.setSceneRect(QtCore.QRectF(self.pixmap.rect()))
        self.scale_factor = 1.0

    def wheelEvent(self, event):
        zoom_in_factor = 1.25
        zoom_out_factor = 1 / zoom_in_factor
        if event.angleDelta().y() > 0:
            factor = zoom_in_factor
        else:
            factor = zoom_out_factor
        self.scale(factor, factor)
        self.scale_factor *= factor


# ============== Запуск приложения ==============

def main():
    import sys
    app = QtWidgets.QApplication(sys.argv)
    db = Database()
    window = GalleryWindow(db)
    window.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()

Как сделать чтобы в окне просмотра изображений, можно было стрелками на клавиатуре листать изображения в категории?