"""Resolution of the unified ingest shared root and batch claiming.

Discovers and initializes a single shared folder used across devices
(Debian Mentem, virtual machine with CyberBrain26, Android phone, tablet
with local model container). The orchestrator claims files from inbox/
into processing/ batches and records everything in an append-only manifest.
"""

from __future__ import annotations

import hashlib
import json
import logging
import os
import re
import time
from pathlib import Path
from uuid import uuid4

logger = logging.getLogger(__name__)

ENV_SHARED_ROOT: str = "UNIFIED_INGEST_ROOT"
DEFAULT_SHARED_ROOT: str = "~/UnifiedIngest"

SYNCTHING_CONFIG: str = "~/.config/syncthing/config.xml"
RCLONE_CONFIG: str = "~/.config/rclone/rclone.conf"

INGEST_SUBDIRS: tuple[str, ...] = (
    "inbox",
    "processing",
    "sorted",
    "duplicates",
    "index",
    "tasks",
    "manifest",
)

MANIFEST_FILENAME: str = "ingest-manifest.jsonl"
BATCH_LIMIT_MAX: int = 10

SENSITIVE_PATTERNS: tuple[str, ...] = (
    ".ssh/", ".gnupg/", ".password-store/",
    "auth.json", "credentials.json",
    "secret", "token", "cred", "password",
)

SENSITIVE_EXTENSIONS: tuple[str, ...] = (".key", ".pem", ".kdbx")

__all__ = [
    "BATCH_LIMIT_MAX",
    "DEFAULT_SHARED_ROOT",
    "ENV_SHARED_ROOT",
    "INGEST_SUBDIRS",
    "MANIFEST_FILENAME",
    "RCLONE_CONFIG",
    "SENSITIVE_EXTENSIONS",
    "SENSITIVE_PATTERNS",
    "SYNCTHING_CONFIG",
    "claim_batch",
    "detect_syncthing_folders",
    "ensure_shared_root",
    "is_rclone_configured",
    "is_sensitive_path",
    "list_mountpoints",
    "manifest_path",
    "manifest_write",
    "resolve_shared_root",
]


def _expand(path: str | Path) -> Path:
    return Path(os.path.expandvars(os.path.expanduser(str(path))))


def resolve_shared_root(explicit: str | Path | None = None) -> Path:
    """Resolve the shared ingest root.

    Precedence: explicit argument > env UNIFIED_INGEST_ROOT >
    existing Syncthing folder > ~/Sync/ > ~/Documents/ > ~/Downloads/ >
    fallback ~/UnifiedIngest/. Does NOT create the directory.
    """
    if explicit is not None:
        return _expand(explicit).resolve()
    env_value = os.environ.get(ENV_SHARED_ROOT)
    if env_value and env_value.strip():
        return _expand(env_value.strip()).resolve()
    for folder in detect_syncthing_folders():
        return folder
    for candidate in ("~/Sync/", "~/Documents/", "~/Downloads/"):
        p = _expand(candidate).resolve()
        if p.exists():
            return p
    return _expand(DEFAULT_SHARED_ROOT).resolve()


def detect_syncthing_folders() -> list[Path]:
    """Return paths of configured Syncthing folders (config existence only)."""
    cfg = _expand(SYNCTHING_CONFIG)
    if not cfg.exists():
        return []
    try:
        text = cfg.read_text(encoding="utf-8", errors="replace")
    except OSError:
        return []
    folders: list[Path] = []
    for match in re.finditer(r'<path>([^<]+)</path>', text):
        p = Path(match.group(1)).expanduser().resolve()
        if p.exists():
            folders.append(p)
    return folders


def is_rclone_configured() -> bool:
    """Check whether rclone config exists (never runs rclone config)."""
    return _expand(RCLONE_CONFIG).exists()


def list_mountpoints() -> list[Path]:
    """List candidate mount points (/proc/mounts, /mnt, /media)."""
    mounts: list[Path] = []
    try:
        with open("/proc/mounts", encoding="utf-8", errors="replace") as f:
            for line in f:
                parts = line.split()
                if len(parts) >= 2:
                    p = Path(parts[1])
                    if p.is_absolute():
                        mounts.append(p)
    except OSError:
        pass
    for base in ("/mnt", "/media"):
        bp = Path(base)
        if bp.exists():
            for child in bp.iterdir():
                if child.is_dir():
                    mounts.append(child)
    return mounts


def is_sensitive_path(path: str | Path) -> bool:
    """Return True if path contains sensitive credentials/keys/secrets."""
    text = str(path).lower()
    for pat in SENSITIVE_PATTERNS:
        if pat in text:
            return True
    for ext in SENSITIVE_EXTENSIONS:
        if text.endswith(ext):
            return True
    return False


def manifest_path(shared_root: str | Path | None = None) -> Path:
    """Return the path to ingest-manifest.jsonl."""
    root = Path(shared_root).expanduser().resolve() if shared_root else resolve_shared_root()
    return root / "manifest" / MANIFEST_FILENAME


def manifest_write(
    entry: dict,
    shared_root: str | Path | None = None,
) -> None:
    """Append one JSON line to the manifest (mode 0o600). All paths relative."""
    mpath = manifest_path(shared_root)
    mpath.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
    line = json.dumps(entry, separators=(",", ":"), ensure_ascii=False, allow_nan=False)
    with open(mpath, "a", encoding="utf-8") as f:
        f.write(line + "\n")
    try:
        mpath.chmod(0o600)
    except OSError:
        pass


def ensure_shared_root(
    shared_root: str | Path | None = None,
) -> Path:
    """Create the shared root and all ingest subdirectories (mode 0o700)."""
    root = Path(shared_root).expanduser().resolve() if shared_root else resolve_shared_root()
    existed = root.exists()
    root.mkdir(parents=True, exist_ok=True, mode=0o700)
    if not existed:
        try:
            root.chmod(0o700)
        except OSError:
            pass
    for sub in INGEST_SUBDIRS:
        (root / sub).mkdir(parents=True, exist_ok=True, mode=0o700)
    # Create empty manifest if missing
    mpath = manifest_path(root)
    if not mpath.exists():
        mpath.touch(mode=0o600)
    return root


def claim_batch(
    shared_root: str | Path | None = None,
    limit: int = BATCH_LIMIT_MAX,
) -> dict:
    """Claim up to `limit` files from inbox/ into processing/<batch_id>/.

    Returns a dict with batch_id, claimed (list of relative paths),
    skipped (list of sensitive paths), and manifest entries written.
    """
    root = Path(shared_root).expanduser().resolve() if shared_root else resolve_shared_root()
    inbox = root / "inbox"
    batch_id = f"b-{uuid4().hex[:8]}"
    processing = root / "processing" / batch_id
    processing.mkdir(parents=True, exist_ok=True, mode=0o700)

    claimed: list[str] = []
    skipped: list[str] = []
    count = 0

    if inbox.exists():
        for entry in sorted(inbox.iterdir()):
            if count >= limit:
                break
            if not entry.is_file():
                continue
            if is_sensitive_path(entry):
                skipped.append(str(entry.relative_to(root)))
                manifest_write(
                    {
                        "id": uuid4().hex,
                        "batch_id": batch_id,
                        "ts": time.time(),
                        "src_path_rel": str(entry.relative_to(root)),
                        "dst_path_rel": None,
                        "sha256": None,
                        "size": entry.stat().st_size,
                        "mime_or_ext": entry.suffix.lower(),
                        "type": "skipped",
                        "status": "skipped",
                        "worker": "orchestrator",
                        "reason": "[REDACTED]",
                        "dup_of_sha256": None,
                        "task_hits": 0,
                    },
                    shared_root=root,
                )
                continue
            sha256 = hashlib.sha256(entry.read_bytes()).hexdigest()
            dst = processing / entry.name
            try:
                entry.replace(dst)  # atomic rename within same FS
                claimed.append(str(dst.relative_to(root)))
                manifest_write(
                    {
                        "id": uuid4().hex,
                        "batch_id": batch_id,
                        "ts": time.time(),
                        "src_path_rel": str(entry.relative_to(root)),
                        "dst_path_rel": str(dst.relative_to(root)),
                        "sha256": sha256,
                        "size": dst.stat().st_size,
                        "mime_or_ext": dst.suffix.lower(),
                        "type": "other",
                        "status": "claimed",
                        "worker": "orchestrator",
                        "reason": None,
                        "dup_of_sha256": None,
                        "task_hits": 0,
                    },
                    shared_root=root,
                )
                count += 1
            except OSError as exc:
                logger.warning("Could not claim %s: %s", entry, exc)

    return {
        "batch_id": batch_id,
        "claimed": claimed,
        "skipped": skipped,
        "count": count,
    }
