# retoor <retoor@molodetz.nl>
from __future__ import annotations

import hashlib
import logging
import re
import shutil
from pathlib import Path

logger = logging.getLogger(__name__)

SLUG_MAX_LENGTH: int = 80


def slugify(value: str) -> str:
    value = re.sub(r"^[a-z]+://", "", value.strip().lower())
    value = re.sub(r"[^a-z0-9._-]+", "-", value).strip("-._")
    if not value:
        value = "source"
    return value[:SLUG_MAX_LENGTH]


def workspace_for(workspaces_dir: Path, source_url: str, analysis_uid: str) -> Path:
    digest = hashlib.sha256(source_url.encode("utf-8")).hexdigest()[:10]
    slug = slugify(source_url)
    workspace = (workspaces_dir / f"{slug}-{digest}-{analysis_uid}").resolve()
    if workspaces_dir.resolve() != workspace.parent:
        raise ValueError(f"Workspace path escapes workspaces directory: {workspace}")
    logger.info("Workspace resolved: %s", workspace)
    return workspace


def reset_workspace(workspace: Path) -> Path:
    if workspace.exists():
        logger.info("Removing existing workspace %s", workspace)
        shutil.rmtree(workspace)
    workspace.mkdir(parents=True, exist_ok=True)
    return workspace


def safe_relative_path(workspace: Path, candidate: str) -> Path:
    cleaned = re.sub(r"[^A-Za-z0-9._/-]+", "-", candidate).strip("/")
    cleaned = re.sub(r"\.\.+", ".", cleaned)
    target = (workspace / cleaned).resolve()
    if workspace.resolve() not in target.parents and target != workspace.resolve():
        raise ValueError(f"Refusing path traversal: {candidate}")
    return target


def remove_workspace(workspace: Path) -> bool:
    if not workspace.exists():
        return False
    try:
        shutil.rmtree(workspace)
    except OSError as error:
        logger.error("Workspace removal failed for %s: %s", workspace, error)
        return False
    logger.info("Workspace removed: %s", workspace)
    return True


def directory_size_bytes(path: Path) -> int:
    total = 0
    for entry in path.rglob("*"):
        if entry.is_file() and not entry.is_symlink():
            try:
                total += entry.stat().st_size
            except OSError as error:
                logger.debug("Size stat failed for %s: %s", entry, error)
    return total


def content_hash(workspace: Path) -> str:
    digest = hashlib.sha256()
    entries: list[tuple[str, Path]] = []
    for entry in workspace.rglob("*"):
        if entry.is_file() and not entry.is_symlink() and ".git" not in entry.parts:
            entries.append((str(entry.relative_to(workspace)), entry))
    for relative, entry in sorted(entries):
        digest.update(relative.encode("utf-8"))
        try:
            digest.update(hashlib.sha256(entry.read_bytes()).digest())
        except OSError as error:
            logger.debug("Hash read failed for %s: %s", entry, error)
    return digest.hexdigest()
