"""Auto-backup engine — periodic SQLite snapshot backups."""

import asyncio
import os
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import List, Optional

from wled_controller.storage.database import Database
from wled_controller.utils import get_logger

logger = get_logger(__name__)

DEFAULT_SETTINGS = {
    "enabled": False,
    "interval_hours": 24,
    "max_backups": 10,
}

# Skip the immediate-on-start backup if a recent backup exists within this window.
_STARTUP_BACKUP_COOLDOWN = timedelta(minutes=5)

_BACKUP_EXT = ".db"


class AutoBackupEngine:
    """Creates periodic SQLite snapshot backups of the database."""

    def __init__(
        self,
        backup_dir: Path,
        db: Database,
    ):
        self._backup_dir = Path(backup_dir)
        self._db = db
        self._task: Optional[asyncio.Task] = None
        self._last_backup_time: Optional[datetime] = None

        self._settings = self._load_settings()
        self._backup_dir.mkdir(parents=True, exist_ok=True)

    # ─── Settings persistence ──────────────────────────────────

    def _load_settings(self) -> dict:
        data = self._db.get_setting("auto_backup")
        if data:
            return {**DEFAULT_SETTINGS, **data}
        return dict(DEFAULT_SETTINGS)

    def _save_settings(self) -> None:
        self._db.set_setting("auto_backup", {
            "enabled": self._settings["enabled"],
            "interval_hours": self._settings["interval_hours"],
            "max_backups": self._settings["max_backups"],
        })

    # ─── Lifecycle ─────────────────────────────────────────────

    async def start(self) -> None:
        if self._settings["enabled"]:
            self._start_loop()
            logger.info(
                f"Auto-backup engine started (every {self._settings['interval_hours']}h, "
                f"max {self._settings['max_backups']})"
            )
        else:
            logger.info("Auto-backup engine initialized (disabled)")

    async def stop(self) -> None:
        self._cancel_loop()
        logger.info("Auto-backup engine stopped")

    def _start_loop(self) -> None:
        self._cancel_loop()
        self._task = asyncio.create_task(self._backup_loop())

    def _cancel_loop(self) -> None:
        if self._task is not None:
            self._task.cancel()
            self._task = None

    def _most_recent_backup_age(self) -> timedelta | None:
        """Return the age of the newest backup file, or None if no backups exist."""
        files = list(self._backup_dir.glob(f"*{_BACKUP_EXT}"))
        if not files:
            return None
        newest = max(files, key=lambda p: p.stat().st_mtime)
        mtime = datetime.fromtimestamp(newest.stat().st_mtime, tz=timezone.utc)
        return datetime.now(timezone.utc) - mtime

    async def _backup_loop(self) -> None:
        try:
            age = self._most_recent_backup_age()
            if age is None or age > _STARTUP_BACKUP_COOLDOWN:
                await self._perform_backup()
                self._prune_old_backups()
            else:
                logger.info(
                    f"Skipping startup backup — most recent is only {age.total_seconds():.0f}s old"
                )

            interval_secs = self._settings["interval_hours"] * 3600
            while True:
                await asyncio.sleep(interval_secs)
                try:
                    await self._perform_backup()
                    self._prune_old_backups()
                except Exception as e:
                    logger.error(f"Auto-backup failed: {e}", exc_info=True)
        except asyncio.CancelledError:
            logger.debug("Auto-backup loop cancelled")
            pass

    # ─── Backup operations ─────────────────────────────────────

    async def _perform_backup(self) -> None:
        await asyncio.to_thread(self._perform_backup_sync)

    def _perform_backup_sync(self) -> None:
        now = datetime.now(timezone.utc)
        timestamp = now.strftime("%Y-%m-%dT%H%M%S")
        filename = f"ledgrab-backup-{timestamp}{_BACKUP_EXT}"
        file_path = self._backup_dir / filename

        self._db.backup_to(file_path)

        self._last_backup_time = now
        logger.info(f"Backup created: {filename}")

    def _prune_old_backups(self) -> None:
        max_backups = self._settings["max_backups"]
        files = sorted(self._backup_dir.glob(f"*{_BACKUP_EXT}"), key=lambda p: p.stat().st_mtime)
        excess = len(files) - max_backups
        if excess > 0:
            for f in files[:excess]:
                try:
                    f.unlink()
                    logger.info(f"Pruned old backup: {f.name}")
                except Exception as e:
                    logger.warning(f"Failed to prune {f.name}: {e}")

    # ─── Public API ────────────────────────────────────────────

    def get_settings(self) -> dict:
        next_backup = None
        if self._settings["enabled"] and self._last_backup_time:
            next_backup = (
                self._last_backup_time + timedelta(hours=self._settings["interval_hours"])
            ).isoformat()

        return {
            "enabled": self._settings["enabled"],
            "interval_hours": self._settings["interval_hours"],
            "max_backups": self._settings["max_backups"],
            "last_backup_time": self._last_backup_time.isoformat() if self._last_backup_time else None,
            "next_backup_time": next_backup,
        }

    async def update_settings(self, enabled: bool, interval_hours: float, max_backups: int) -> dict:
        self._settings["enabled"] = enabled
        self._settings["interval_hours"] = interval_hours
        self._settings["max_backups"] = max_backups
        self._save_settings()

        if enabled:
            self._start_loop()
            logger.info(
                f"Auto-backup enabled (every {interval_hours}h, max {max_backups})"
            )
        else:
            self._cancel_loop()
            logger.info("Auto-backup disabled")

        self._prune_old_backups()
        return self.get_settings()

    def list_backups(self) -> List[dict]:
        backups = []
        for f in sorted(self._backup_dir.glob(f"*{_BACKUP_EXT}"), key=lambda p: p.stat().st_mtime, reverse=True):
            stat = f.stat()
            backups.append({
                "filename": f.name,
                "size_bytes": stat.st_size,
                "created_at": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
            })
        return backups

    def _safe_backup_path(self, filename: str) -> Path:
        """Resolve a backup filename to an absolute path, guarding against path traversal."""
        if not filename or os.sep in filename or "/" in filename or ".." in filename:
            raise ValueError("Invalid filename")
        target = (self._backup_dir / filename).resolve()
        if not target.is_relative_to(self._backup_dir.resolve()):
            raise ValueError("Invalid filename")
        return target

    async def trigger_backup(self) -> dict:
        """Manually trigger a backup and prune old ones. Returns the created backup info."""
        await self._perform_backup()
        self._prune_old_backups()
        backups = self.list_backups()
        return backups[0] if backups else {}

    def delete_backup(self, filename: str) -> None:
        target = self._safe_backup_path(filename)
        if not target.exists():
            raise FileNotFoundError(f"Backup not found: {filename}")
        target.unlink()
        logger.info(f"Deleted backup: {filename}")

    def get_backup_path(self, filename: str) -> Path:
        target = self._safe_backup_path(filename)
        if not target.exists():
            raise FileNotFoundError(f"Backup not found: {filename}")
        return target
