from __future__ import annotations

import shutil
import sqlite3
import zipfile
from datetime import datetime
from pathlib import Path
from tempfile import TemporaryDirectory


class BackupService:
    BACKUP_DIR_KEY = "backup_dir"
    LAST_AUTO_BACKUP_KEY = "last_auto_backup_path"

    def __init__(self, conn: sqlite3.Connection, paths: dict[str, Path]):
        self.conn = conn
        self.paths = paths

    @property
    def db_path(self) -> Path:
        return self.paths["db"] / "app.sqlite"

    def _get_app_setting(self, key: str, default: str = "") -> str:
        try:
            row = self.conn.execute("SELECT value FROM app_settings WHERE key=?;", (key,)).fetchone()
            if row:
                return str(row[0] if not isinstance(row, sqlite3.Row) else row["value"])
        except Exception:
            pass
        return default

    def _set_app_setting(self, key: str, value: str) -> None:
        self.conn.execute(
            "INSERT INTO app_settings(key, value, updated_at) VALUES(?,?, datetime('now')) "
            "ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=datetime('now'));",
            (key, str(value)),
        )
        self.conn.commit()

    def get_backup_dir(self) -> Path:
        default_dir = self.paths["backups"]
        saved = self._get_app_setting(self.BACKUP_DIR_KEY, str(default_dir))
        try:
            path = Path(saved) if saved else default_dir
        except Exception:
            path = default_dir
        path.mkdir(parents=True, exist_ok=True)
        return path

    def set_backup_dir(self, directory: Path | str) -> Path:
        path = Path(directory).expanduser().resolve()
        path.mkdir(parents=True, exist_ok=True)
        self._set_app_setting(self.BACKUP_DIR_KEY, str(path))
        return path


    def _timestamp(self) -> str:
        return datetime.now().strftime("%Y%m%d_%H%M%S")

    def export_database(self, destination: Path) -> Path:
        destination.parent.mkdir(parents=True, exist_ok=True)
        target_conn = sqlite3.connect(str(destination))
        try:
            self.conn.backup(target_conn)
        finally:
            target_conn.close()
        return destination

    def create_backup(self, destination: Path | None = None, *, include_documents: bool = True, include_media: bool = True) -> Path:
        backups_dir = self.get_backup_dir()
        backups_dir.mkdir(parents=True, exist_ok=True)
        if destination is None:
            destination = backups_dir / f"mi100_backup_{self._timestamp()}.zip"
        destination.parent.mkdir(parents=True, exist_ok=True)

        with TemporaryDirectory() as tmpdir:
            tmp = Path(tmpdir)
            export_db = tmp / "db" / "app.sqlite"
            export_db.parent.mkdir(parents=True, exist_ok=True)
            self.export_database(export_db)

            with zipfile.ZipFile(destination, "w", compression=zipfile.ZIP_DEFLATED) as zf:
                zf.write(export_db, arcname="db/app.sqlite")

                if include_documents:
                    docs_dir = self.paths.get("docs")
                    if docs_dir and docs_dir.exists():
                        for path in docs_dir.rglob("*"):
                            if path.is_file():
                                zf.write(path, arcname=str(Path("docs") / path.relative_to(docs_dir)))

                if include_media:
                    media_dir = self.paths.get("media")
                    if media_dir and media_dir.exists():
                        for path in media_dir.rglob("*"):
                            if path.is_file():
                                zf.write(path, arcname=str(Path("media") / path.relative_to(media_dir)))

        return destination

    def list_backups(self) -> list[Path]:
        backups_dir = self.get_backup_dir()
        return sorted(backups_dir.glob("*.zip"), reverse=True)

    def create_auto_backup(self, *, include_documents: bool = True, include_media: bool = True) -> Path:
        backups_dir = self.get_backup_dir()
        destination = backups_dir / f"mi100_autobackup_{self._timestamp()}.zip"
        path = self.create_backup(destination, include_documents=include_documents, include_media=include_media)

        # Після успішного створення автобекапу прибираємо всі попередні
        # автоматичні резервні копії в цій самій папці, залишаючи тільки найновішу.
        try:
            current_resolved = path.resolve()
        except Exception:
            current_resolved = path

        for old_path in backups_dir.glob("mi100_autobackup_*.zip"):
            try:
                old_resolved = old_path.resolve()
            except Exception:
                old_resolved = old_path
            if old_resolved == current_resolved:
                continue
            try:
                old_path.unlink()
            except Exception:
                pass

        previous = self._get_app_setting(self.LAST_AUTO_BACKUP_KEY, "").strip()
        if previous:
            prev_path = Path(previous)
            try:
                prev_resolved = prev_path.resolve()
            except Exception:
                prev_resolved = prev_path
            if prev_path.exists() and prev_resolved != current_resolved:
                try:
                    prev_path.unlink()
                except Exception:
                    pass
        self._set_app_setting(self.LAST_AUTO_BACKUP_KEY, str(path))
        return path

    def restore_from_backup(self, archive_path: Path) -> None:
        archive_path = Path(archive_path)
        if not archive_path.exists():
            raise FileNotFoundError(str(archive_path))

        # Страхувальна копія поточного стану перед відновленням.
        self.create_backup()

        with TemporaryDirectory() as tmpdir:
            tmp = Path(tmpdir)
            with zipfile.ZipFile(archive_path, "r") as zf:
                zf.extractall(tmp)

            db_src = tmp / "db" / "app.sqlite"
            docs_src = tmp / "docs"
            media_src = tmp / "media"

            if not db_src.exists():
                raise ValueError("У резервній копії відсутня база даних db/app.sqlite")

            self.conn.commit()
            try:
                self.conn.close()
            except Exception:
                pass

            target_db = self.db_path
            target_db.parent.mkdir(parents=True, exist_ok=True)
            if target_db.exists():
                try:
                    target_db.unlink()
                except Exception:
                    pass
            shutil.copy2(db_src, target_db)

            docs_target = self.paths.get("docs")
            if docs_target is not None:
                if docs_target.exists():
                    shutil.rmtree(docs_target, ignore_errors=True)
                if docs_src.exists():
                    shutil.copytree(docs_src, docs_target)
                else:
                    docs_target.mkdir(parents=True, exist_ok=True)

            media_target = self.paths.get("media")
            if media_target is not None:
                if media_target.exists():
                    shutil.rmtree(media_target, ignore_errors=True)
                if media_src.exists():
                    shutil.copytree(media_src, media_target)
                else:
                    media_target.mkdir(parents=True, exist_ok=True)
