"""Versioned boilerplate snapshot store.

Snapshots are gzip-compressed tarballs at:
  <snapshot_dir>/<project_type>@<version>.tar.gz

e.g.
  boilerplates/snapshots/react_app@2026.04.02.1.tar.gz
  boilerplates/snapshots/nuxt_app@2026.04.02.1.tar.gz
  boilerplates/snapshots/html@2026.04.02.1.tar.gz
  boilerplates/snapshots/wordpress_companion@2026.04.02.1.tar.gz

Version format: YYYY.MM.DD.N  (lexicographically sortable, N is the daily build index).

When no snapshot exists for a type, ``apply_snapshot`` falls back to copying the live
``boilerplates/<type>/`` directory — this keeps the system working in dev without
requiring a pre-built snapshot.

Build new snapshots with:
  python scripts/build_boilerplate_snapshots.py --version 2026.04.02.1
  python scripts/build_boilerplate_snapshots.py --type wordpress_companion --version 2026.04.02.1
"""
from __future__ import annotations

import os
import shutil
import tarfile
from typing import Optional

import structlog

logger = structlog.get_logger()

_EXCLUDED_FROM_SNAPSHOT = frozenset({
    ".git", "__pycache__", ".venv", "venv",
    "node_modules", ".nuxt", "dist", ".output", ".next",
})


def _default_snapshot_dir() -> str:
    """<project_root>/boilerplates/snapshots/"""
    here = os.path.dirname(os.path.abspath(__file__))            # src/workers/
    project_root = os.path.dirname(os.path.dirname(here))        # project root
    return os.path.join(project_root, "boilerplates", "snapshots")


def get_snapshot_dir(settings=None) -> str:
    configured = ""
    if settings is not None:
        configured = (getattr(settings, "boilerplate_snapshot_dir", "") or "").strip()
    return os.path.abspath(configured) if configured else _default_snapshot_dir()


def find_latest_version(project_type: str, snapshot_dir: str) -> Optional[str]:
    """Return the latest version tag for *project_type*, or None if no snapshots exist."""
    if not os.path.isdir(snapshot_dir):
        return None
    prefix = f"{project_type}@"
    suffix = ".tar.gz"
    versions = [
        f[len(prefix):-len(suffix)]
        for f in os.listdir(snapshot_dir)
        if f.startswith(prefix) and f.endswith(suffix)
    ]
    return max(versions) if versions else None  # YYYY.MM.DD.N sorts correctly as a string


def snapshot_path(project_type: str, version: str, snapshot_dir: str) -> str:
    return os.path.join(snapshot_dir, f"{project_type}@{version}.tar.gz")


def apply_snapshot(repo_path: str, project_type: str, version: str, snapshot_dir: str) -> bool:
    """Extract snapshot tarball into *repo_path*, skipping .git entries.

    Returns True if a snapshot was applied, False if the archive was not found.
    Raises on corrupt archive or extraction failure.
    """
    path = snapshot_path(project_type, version, snapshot_dir)
    if not os.path.isfile(path):
        return False

    with tarfile.open(path, "r:gz") as tf:
        safe_members = [
            m for m in tf.getmembers()
            if not m.name.startswith("/")
            and ".." not in m.name.split(os.sep)
            and not m.name.startswith(".git/")
            and m.name != ".git"
        ]
        tf.extractall(repo_path, members=safe_members)

    logger.info("boilerplate_snapshot_applied", project_type=project_type, version=version, path=repo_path)
    return True


def apply_live_boilerplate(repo_path: str, project_type: str) -> None:
    """Fallback: copy live boilerplates/<type>/ into repo_path (no snapshot available).

    Skips VCS dirs, node_modules, and build artefacts.
    """
    here = os.path.dirname(os.path.abspath(__file__))
    project_root = os.path.dirname(os.path.dirname(here))
    src = os.path.join(project_root, "boilerplates", project_type)
    if not os.path.isdir(src):
        raise RuntimeError(f"No boilerplate directory found for type '{project_type}' at {src}")

    def _ignore(dirpath: str, names: list[str]) -> set[str]:
        return {n for n in names if n in _EXCLUDED_FROM_SNAPSHOT}

    # Copy each item individually so we can overwrite existing files
    for item in os.listdir(src):
        if item in _EXCLUDED_FROM_SNAPSHOT:
            continue
        src_item = os.path.join(src, item)
        dst_item = os.path.join(repo_path, item)
        if os.path.isdir(src_item):
            if os.path.exists(dst_item):
                shutil.rmtree(dst_item)
            shutil.copytree(src_item, dst_item, ignore=_ignore)
        else:
            shutil.copy2(src_item, dst_item)

    logger.info("boilerplate_live_applied", project_type=project_type, path=repo_path)
