"""Workspace detection and sync for Specter.

Detects the project root (git repo, pyproject.toml, etc.) and syncs it
to the remote machine via rsync before execution.
"""

from __future__ import annotations

import hashlib
import logging
import subprocess
from pathlib import Path

log = logging.getLogger(__name__)

# Cache file for the last-synced workspace hash.
_HASH_CACHE = ".specter-sync-hash"

# Markers that indicate a project root (checked in order).
_PROJECT_MARKERS = [
    ".git",
    "pyproject.toml",
    "setup.py",
    "setup.cfg",
    "Makefile",
    "requirements.txt",
    ".specter.toml",
]

# Default excludes for rsync — large dirs that shouldn't be synced.
_DEFAULT_EXCLUDES = [
    ".git",
    "__pycache__",
    "*.pyc",
    ".venv",
    "venv",
    "env",
    "node_modules",
    ".tox",
    ".mypy_cache",
    ".pytest_cache",
    ".ruff_cache",
    "*.egg-info",
    "dist",
    "build",
    ".specter",
    ".specter-sync-hash",
]


def find_project_root(start: Path | None = None) -> Path:
    """Walk up from *start* (default: cwd) to find the project root.

    Returns the directory containing the nearest project marker, or
    *start* itself if no marker is found.
    """
    current = (start or Path.cwd()).resolve()

    # Don't walk above home directory.
    home = Path.home()

    while True:
        for marker in _PROJECT_MARKERS:
            if (current / marker).exists():
                log.debug("project root: %s (found %s)", current, marker)
                return current
        parent = current.parent
        if parent == current or current == home:
            break
        current = parent

    # No marker found — use the starting directory.
    fallback = (start or Path.cwd()).resolve()
    log.debug("project root: %s (no marker found, using cwd)", fallback)
    return fallback


def load_specterignore(project_root: Path) -> list[str]:
    """Load additional exclude patterns from .specterignore if it exists.

    File format: one pattern per line, empty lines and #comments ignored.
    Patterns are rsync-compatible.
    """
    ignore_file = project_root / ".specterignore"
    if not ignore_file.is_file():
        return []

    patterns: list[str] = []
    for line in ignore_file.read_text().splitlines():
        line = line.strip()
        if line and not line.startswith("#"):
            patterns.append(line)
    return patterns


def workspace_hash(project_root: Path) -> str | None:
    """Compute a hash of the workspace state using git.

    Combines ``git rev-parse HEAD`` (committed state) with a hash of
    ``git diff`` output (uncommitted changes) and ``git ls-files --others``
    (untracked files).  Returns a hex digest, or ``None`` if git is not
    available or the directory is not a git repo.
    """
    try:
        # Current HEAD commit.
        head = subprocess.run(
            ["git", "rev-parse", "HEAD"],
            capture_output=True,
            text=True,
            cwd=project_root,
            timeout=5,
        )
        if head.returncode != 0:
            return None  # Not a git repo.

        # Uncommitted changes (staged + unstaged).
        diff = subprocess.run(
            ["git", "diff", "HEAD"],
            capture_output=True,
            text=True,
            cwd=project_root,
            timeout=10,
        )

        # Untracked files (names only — content changes won't be caught,
        # but new/deleted files will change the hash).
        untracked = subprocess.run(
            ["git", "ls-files", "--others", "--exclude-standard"],
            capture_output=True,
            text=True,
            cwd=project_root,
            timeout=5,
        )

        combined = head.stdout.strip() + diff.stdout + untracked.stdout
        return hashlib.sha256(combined.encode()).hexdigest()[:16]

    except Exception:
        # Broad catch: workspace_hash is a non-critical optimization.
        # In test environments, subprocess.Popen mocking can break
        # subprocess.run internals.  Gracefully degrade to always-sync.
        return None


def _load_cached_hash(project_root: Path) -> str | None:
    """Load the last-synced workspace hash from the cache file."""
    cache = project_root / _HASH_CACHE
    if cache.is_file():
        try:
            return cache.read_text().strip()
        except OSError:
            return None
    return None


def _save_cached_hash(project_root: Path, hash_val: str) -> None:
    """Save the workspace hash to the cache file."""
    import contextlib

    cache = project_root / _HASH_CACHE
    with contextlib.suppress(OSError):
        cache.write_text(hash_val + "\n")


def rsync_workspace(
    project_root: Path,
    rsync_ssh: str,
    remote_dest: str,
    remote_workspace: str = "~/.specter/workspace",
    *,
    delete: bool = True,
    extra_excludes: list[str] | None = None,
    timeout: int = 120,
) -> subprocess.CompletedProcess[str]:
    """Sync the project directory to the remote machine.

    Parameters
    ----------
    project_root:
        Local project root directory.
    rsync_ssh:
        Pre-built SSH command string for rsync's ``-e`` flag.
        Use :func:`specter_cli.ssh_config.rsync_ssh_args` to build this.
    remote_dest:
        SSH destination, e.g. "user@host".
    remote_workspace:
        Remote directory to sync into.
    delete:
        Remove extraneous files on remote.
    extra_excludes:
        Additional rsync exclude patterns.
    timeout:
        Seconds before killing rsync.

    Returns
    -------
    subprocess.CompletedProcess with stdout/stderr.

    Raises
    ------
    subprocess.CalledProcessError if rsync fails.
    """
    local_path = str(project_root).rstrip("/") + "/"

    excludes = _DEFAULT_EXCLUDES.copy()
    excludes.extend(load_specterignore(project_root))
    if extra_excludes:
        excludes.extend(extra_excludes)

    args = [
        "rsync",
        "-az",  # archive + compress
        "--info=stats1",  # summary stats only (not per-file noise)
        "-e",
        rsync_ssh,
    ]
    if delete:
        args.append("--delete")
    for pattern in excludes:
        args.extend(["--exclude", pattern])
    args.extend([local_path, f"{remote_dest}:{remote_workspace}/"])

    log.debug("rsync: %s", " ".join(args))
    result = subprocess.run(
        args,
        capture_output=True,
        text=True,
        timeout=timeout,
    )
    if result.returncode != 0:
        log.error("rsync failed (exit %d): %s", result.returncode, result.stderr)
        raise subprocess.CalledProcessError(result.returncode, args, result.stdout, result.stderr)
    return result
