"""Resource and example dataset management for AutoPlasm."""

from __future__ import annotations

import hashlib
import json
import os
import shutil
import subprocess
import time
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import asdict, dataclass
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Dict, Iterable, List, Mapping, Sequence

from abi.plugins.metagenomic_plasmid._engine.config import (
    PROJECT_ROOT,
    resolved_mamba_root,
    write_yaml,
)
from abi.plugins.metagenomic_plasmid._engine.filesystem import ensure_directory
from abi.plugins.metagenomic_plasmid._engine.schemas import AutoPlasmError
from abi.plugins.metagenomic_plasmid._engine.skills.registry import ToolRegistry
from abi.plugins.metagenomic_plasmid._engine.timeouts import (
    DEFAULT_RESOURCE_TIMEOUT_SECONDS,
    mapping_block,
    timeout_from_env_or_value,
)
from abi.resources import _is_placeholder_resource_value


class ResourceError(AutoPlasmError):
    """Raised when an AutoPlasm database or example resource cannot be prepared."""


@dataclass
class ResourceSpec:
    resource_id: str
    tool_id: str
    field: str
    env_name: str
    executable: str
    default_subdir: str
    source_url: str
    command_template: List[str]
    version: str = "latest"
    auto_setup: bool = True
    resource_type: str = "database"
    # "database"      = download data files (existing behaviour)
    # "tool_git"      = git clone tool source  →  git clone <source_url> <target>
    # "tool_pip"      = pip install tool       →  pip install <source_url> --target <target>
    # "tool_download" = download tool binary   →  wget <source_url> -O <target>/<executable>
    install_post: str | None = None
    # Post-install command run inside target_dir after tool_git clone
    # (e.g. "pip install -e .")


@dataclass
class ResourceStatus:
    resource_id: str
    tool_id: str
    field: str
    path: str
    status: str
    version: str
    source_url: str
    checksum: str = ""
    checksum_method: str = ""
    date: str = ""
    command: List[str] | None = None
    ready_check: str = ""
    directory_file_count: int = 0
    directory_size_bytes: int = 0
    message: str = ""
    last_checked_at: str = ""
    mock: bool = False

    def to_dict(self) -> Dict[str, Any]:
        data = asdict(self)
        data["id"] = self.resource_id
        data["source"] = self.source_url
        data["license"] = ""
        data["checksum_sha256"] = self.checksum
        data["validated_at"] = self.last_checked_at
        return data


EXAMPLE_ACCESSIONS = {
    "plasmid_refseq_smoke": ["NC_002127.1", "NC_011977.1", "NC_002483.1"],
}
PLASMIDFINDER_DB_URL = "https://bitbucket.org/genomicepidemiology/plasmidfinder_db.git"
RESOURCE_READY_SENTINEL = ".autoplasm_resource_ready"
MAX_DIRECTORY_SUMMARY_ENTRIES = 100_000
# Default Kraken2 standard database snapshot. Update periodically by checking
# https://benlangmead.github.io/aws-indexes/k2 — a stale date still downloads
# successfully but ships older taxonomy. Users can override via
# config.resources.kraken2.version.
KRAKEN2_DEFAULT_VERSION = "standard_20260226"
ResourceProgressCallback = Callable[[str, str, str], None]


def default_resource_specs(config: Mapping[str, Any]) -> List[ResourceSpec]:
    root = resource_root(config)
    return [
        # ---- Level 1: fully automated (auto_setup=True) ----
        ResourceSpec(
            resource_id="genomad",
            tool_id="genomad",
            field="database",
            env_name="autoplasm-plasmid-detect",
            executable="genomad",
            default_subdir="genomad",
            source_url="https://portal.nersc.gov/genomad/",
            command_template=["genomad", "download-database", str(root / "genomad")],
        ),
        ResourceSpec(
            resource_id="bakta",
            tool_id="bakta",
            field="database",
            env_name="autoplasm-annotation",
            executable="bakta_db",
            default_subdir="bakta",
            source_url="https://bakta.readthedocs.io/en/latest/cli/database.html",
            command_template=[
                "bakta_db",
                "download",
                "--output",
                str(root / "bakta"),
                "--type",
                str(_resource_block(config, "bakta").get("type", "light")),
            ],
            version=str(_resource_block(config, "bakta").get("version", "light")),
        ),
        ResourceSpec(
            resource_id="mob_suite",
            tool_id="mob_suite",
            field="database",
            env_name="autoplasm-annotation",
            executable="mob_init",
            default_subdir="mob_suite",
            source_url="https://github.com/phac-nml/mob-suite",
            command_template=[
                "mob_init",
                "--database_directory",
                str(root / "mob_suite"),
            ],
        ),
        ResourceSpec(
            resource_id="plasmidfinder",
            tool_id="plasmidfinder",
            field="database",
            env_name="autoplasm-annotation",
            executable="git",
            default_subdir="plasmidfinder_db",
            source_url=PLASMIDFINDER_DB_URL,
            command_template=[
                "git",
                "clone",
                PLASMIDFINDER_DB_URL,
                str(root / "plasmidfinder_db"),
            ],
        ),
        ResourceSpec(
            resource_id="metaphlan",
            tool_id="metaphlan",
            field="database",
            env_name="stats",
            executable="metaphlan",
            default_subdir="metaphlan",
            source_url="https://github.com/biobakery/MetaPhlAn",
            command_template=[
                "metaphlan",
                "--install",
                "--db_dir",
                str(root / "metaphlan"),
            ],
        ),
        ResourceSpec(
            resource_id="amrfinderplus",
            tool_id="amrfinderplus",
            field="database",
            env_name="autoplasm-annotation",
            executable="amrfinder_update",
            default_subdir="amrfinderplus",
            source_url="https://github.com/ncbi/amr",
            command_template=[
                "amrfinder_update",
                "-d",
                str(root / "amrfinderplus"),
            ],
            install_post="__amrfinderplus_post__",
        ),
        ResourceSpec(
            resource_id="card",
            tool_id="rgi",
            field="database",
            env_name="autoplasm-annotation",
            executable="rgi",
            default_subdir="card",
            source_url="https://card.mcmaster.ca/download",
            command_template=[],
            auto_setup=False,
        ),
        ResourceSpec(
            resource_id="kraken2",
            tool_id="kraken2",
            field="database",
            env_name="stats",
            executable="kraken2-build",
            default_subdir="kraken2",
            source_url="https://benlangmead.github.io/aws-indexes/k2",
            # command_template is a dry-run placeholder; the real download is
            # driven by _run_kraken2_download (aria2c + tar, atomic). The
            # version is resolved from config.resources.kraken2.version or the
            # ``version`` field below.
            command_template=[
                "aria2c",
                "-x",
                "8",
                "-s",
                "8",
                "--continue=true",
                "https://genome-idx.s3.amazonaws.com/kraken/k2_standard_20260226.tar.gz",
            ],
            version=KRAKEN2_DEFAULT_VERSION,
        ),
        ResourceSpec(
            resource_id="gtdbtk",
            tool_id="gtdbtk",
            field="database",
            env_name="stats",
            executable="gtdbtk",
            default_subdir="gtdbtk",
            source_url="https://data.gtdb.ecogenomic.org/",
            command_template=[
                "gtdbtk",
                "db",
                "download",
            ],
            version="r220",
        ),
        ResourceSpec(
            resource_id="checkm2",
            tool_id="checkm2",
            field="database",
            env_name="stats",
            executable="checkm2",
            default_subdir="checkm2",
            source_url="https://github.com/chklovski/CheckM2",
            command_template=[
                "checkm2",
                "download",
                "--path",
                str(root / "checkm2"),
            ],
        ),
        ResourceSpec(
            resource_id="eggnog_mapper",
            tool_id="eggnog_mapper",
            field="database",
            env_name="autoplasm-annotation",
            executable="download_eggnog_data.py",
            default_subdir="eggnog_mapper",
            source_url="https://eggnog-mapper.embl.de/",
            command_template=[
                "download_eggnog_data.py",
                "-y",
                "--data_dir",
                str(root / "eggnog_mapper"),
            ],
        ),
        # ---- Level 2: guided (auto_setup=False) ----
        ResourceSpec(
            resource_id="abricate",
            tool_id="abricate",
            field="database",
            env_name="autoplasm-annotation",
            executable="abricate-get_db",
            default_subdir="abricate",
            source_url="https://github.com/tseemann/abricate#databases",
            command_template=[],
            auto_setup=False,
        ),
        ResourceSpec(
            resource_id="plasme",
            tool_id="plasme",
            field="database",
            env_name="autoplasm-plasmid-detect",
            executable="",
            default_subdir="plasme",
            source_url="https://github.com/ccb-hms/PLASMe",
            command_template=[],
            auto_setup=False,
        ),
        ResourceSpec(
            resource_id="plasx_annotations",
            tool_id="plasx",
            field="annotations",
            env_name="autoplasm-plasmid-detect",
            executable="",
            default_subdir="plasx",
            source_url="https://github.com/michaelgoldman/PlasX",
            command_template=[],
            auto_setup=False,
        ),
        ResourceSpec(
            resource_id="plasx_model",
            tool_id="plasx",
            field="model",
            env_name="autoplasm-plasmid-detect",
            executable="",
            default_subdir="plasx_model",
            source_url="https://github.com/michaelgoldman/PlasX",
            command_template=[],
            auto_setup=False,
        ),
        ResourceSpec(
            resource_id="copla_refgraph",
            tool_id="copla",
            field="refgraph",
            env_name="autoplasm-annotation",
            executable="",
            default_subdir="copla",
            source_url="https://github.com/BeatrizBonete/COPLA",
            command_template=[],
            auto_setup=False,
        ),
        ResourceSpec(
            resource_id="copla_reflist",
            tool_id="copla",
            field="reflist",
            env_name="autoplasm-annotation",
            executable="",
            default_subdir="copla_reflist",
            source_url="https://github.com/BeatrizBonete/COPLA",
            command_template=[],
            auto_setup=False,
        ),
        ResourceSpec(
            resource_id="blast",
            tool_id="blast",
            field="database",
            env_name="autoplasm-annotation",
            executable="",
            default_subdir="blast",
            source_url="https://ftp.ncbi.nlm.nih.gov/blast/db/",
            command_template=[],
            auto_setup=False,
        ),
        ResourceSpec(
            resource_id="plasmidhostfinder",
            tool_id="plasmidhostfinder",
            field="database",
            env_name="autoplasm-annotation",
            executable="",
            default_subdir="plasmidhostfinder",
            source_url="https://bitbucket.org/genomicepidemiology/plasmidfinder_db.git",
            command_template=[],
            auto_setup=False,
        ),
        # ---- Level 1: auto-install tools (resource_type=tool_*) ----
        ResourceSpec(
            resource_id="plasme_tool",
            tool_id="plasme",
            field="install_path",
            env_name="autoplasm-plasmid-detect",
            executable="PLASMe.py",
            default_subdir="PLASMe",
            source_url="https://github.com/ccb-hms/PLASMe.git",
            command_template=["git", "clone", "https://github.com/ccb-hms/PLASMe.git"],
            resource_type="tool_git",
            install_post="pip install -e .",
        ),
        ResourceSpec(
            resource_id="plasx_tool",
            tool_id="plasx",
            field="install_path",
            env_name="autoplasm-plasmid-detect",
            executable="plasx",
            default_subdir="PlasX",
            source_url="https://github.com/michaelgoldman/PlasX.git",
            command_template=["git", "clone", "https://github.com/michaelgoldman/PlasX.git"],
            resource_type="tool_git",
            install_post="pip install -e .",
        ),
        ResourceSpec(
            resource_id="platon_tool",
            tool_id="platon",
            field="install_path",
            env_name="autoplasm-plasmid-detect",
            executable="platon",
            default_subdir="platon",
            source_url="https://github.com/oschwengers/platon.git",
            command_template=["git", "clone", "https://github.com/oschwengers/platon.git"],
            resource_type="tool_git",
            install_post="pip install -e .",
        ),
        ResourceSpec(
            resource_id="macsyfinder_tool",
            tool_id="macsyfinder",
            field="install_path",
            env_name="autoplasm-annotation",
            executable="macsyfinder",
            default_subdir="macsyfinder",
            source_url="macsyfinder",
            command_template=["pip", "install", "macsyfinder"],
            resource_type="tool_pip",
        ),
        # ---- Level 2: guided tool install (auto_setup=False) ----
        ResourceSpec(
            resource_id="plasmaag_tool",
            tool_id="plasmaag",
            field="install_path",
            env_name="autoplasm-plasmid-binning",
            executable="plasmidag",
            default_subdir="PlasmidHostFinder",
            source_url="https://github.com/wanyuac/PlasmidHostFinder.git",
            command_template=[],
            resource_type="tool_git",
            auto_setup=False,
        ),
        ResourceSpec(
            resource_id="gplas2_tool",
            tool_id="gplas2",
            field="install_path",
            env_name="autoplasm-plasmid-binning",
            executable="gplas2",
            default_subdir="gplas2",
            source_url="https://github.com/simonrolph/gplas2.git",
            command_template=[],
            resource_type="tool_git",
            auto_setup=False,
        ),
        ResourceSpec(
            resource_id="scapp_tool",
            tool_id="scapp",
            field="install_path",
            env_name="autoplasm-plasmid-binning",
            executable="scapp",
            default_subdir="modified-scapp",
            source_url="https://github.com/avsastry/modified-scapp.git",
            command_template=[],
            resource_type="tool_git",
            auto_setup=False,
        ),
        ResourceSpec(
            resource_id="recycler_tool",
            tool_id="recycler",
            field="install_path",
            env_name="autoplasm-plasmid-binning",
            executable="recycle.py",
            default_subdir="Recycler",
            source_url="https://github.com/Shamir-Lab/Recycler.git",
            command_template=[],
            resource_type="tool_git",
            auto_setup=False,
        ),
        ResourceSpec(
            resource_id="copla_tool",
            tool_id="copla",
            field="install_path",
            env_name="autoplasm-annotation",
            executable="copla",
            default_subdir="COPLA",
            source_url="https://zenodo.org/records/10059131/files/COPLA_DB.tar.gz",
            command_template=[],
            resource_type="tool_download",
            auto_setup=False,
        ),
        ResourceSpec(
            resource_id="plasmidhostfinder_tool",
            tool_id="plasmidhostfinder",
            field="install_path",
            env_name="autoplasm-annotation",
            executable="plasmidhostfinder.py",
            default_subdir="plasmidhostfinder",
            source_url="https://bitbucket.org/genomicepidemiology/plasmidhostfinder.git",
            command_template=[],
            resource_type="tool_git",
            auto_setup=False,
        ),
        ResourceSpec(
            resource_id="conjscan_tool",
            tool_id="conjscan",
            field="install_path",
            env_name="autoplasm-annotation",
            executable="conjscan",
            default_subdir="conjscan",
            # Direct archive URL (the previous releases-page URL returned HTML).
            source_url="https://github.com/ruizhang84/conjscan/archive/refs/heads/master.tar.gz",
            command_template=[],
            resource_type="tool_download",
            auto_setup=False,
        ),
    ]


def resource_root(config: Mapping[str, Any]) -> Path:
    """Return the root directory holding ABI-managed databases.

    Resolution order (highest precedence first):
    1. ``ABI_RESOURCE_ROOT`` / ``AUTOPLASM_RESOURCE_ROOT`` env var (only when
       the path exists and is a directory — a misconfigured export falls
       through so it cannot silently break discovery).
    2. ``config.resources.root`` (when not a placeholder).
    3. ``PROJECT_ROOT / "resources" / "autoplasm"`` (default).
    """
    for var in ("ABI_RESOURCE_ROOT", "AUTOPLASM_RESOURCE_ROOT"):
        env_override = os.environ.get(var)
        if env_override:
            candidate = Path(env_override)
            if candidate.is_dir():
                return candidate
            # Fall through on missing/non-directory override.
    resources = config.get("resources", {})
    root = resources.get("root") if isinstance(resources, Mapping) else None
    if root and not _is_placeholder_resource_value(root):
        return Path(str(root))
    return Path(str(root or PROJECT_ROOT / "resources" / "autoplasm"))


def check_resources(
    config: Mapping[str, Any],
    *,
    resource_ids: Sequence[str] | None = None,
) -> List[Dict[str, Any]]:
    ids = set(resource_ids or [])
    rows = []
    for spec in default_resource_specs(config):
        if ids and spec.resource_id not in ids:
            continue
        rows.append(_status_for_spec(config, spec).to_dict())
    return rows


def setup_resources(
    config: Mapping[str, Any],
    *,
    resource_ids: Sequence[str] | None = None,
    dry_run: bool = False,
    mock: bool = False,
    progress_callback: ResourceProgressCallback | None = None,
) -> List[Dict[str, Any]]:
    root = resource_root(config)
    if not dry_run:
        ensure_directory(root, label="Resource root directory")
    ids = set(resource_ids or [])
    statuses: List[ResourceStatus] = []
    for spec in default_resource_specs(config):
        if ids and spec.resource_id not in ids:
            continue
        if not ids and not spec.auto_setup:
            continue
        _notify_progress(progress_callback, "start", spec.resource_id, "preparing")
        path = _configured_resource_path(config, spec)
        command = _resolved_resource_command(config, spec, path)
        if dry_run:
            status = _status_for_spec(
                config,
                spec,
                status_override="planned",
                command=command,
                message="Download was planned but not executed.",
                mock=mock,
            )
            statuses.append(status)
            _notify_progress(progress_callback, "finish", spec.resource_id, status.status)
            continue
        if mock:
            ensure_directory(path, label=f"Resource directory for {spec.resource_id}")
            (path / ".autoplasm_mock_resource").write_text(
                f"{spec.resource_id}\n", encoding="utf-8"
            )
            _write_ready_sentinel(path, spec)
            status = _status_for_spec(
                config,
                spec,
                status_override="ok",
                command=command,
                message="Mock resource prepared.",
                mock=mock,
            )
            statuses.append(status)
            _notify_progress(progress_callback, "finish", spec.resource_id, status.status)
            continue
        if _resource_path_ready(path, spec):
            status = _status_for_spec(
                config,
                spec,
                status_override="ok",
                command=command,
                message="Existing database found; download skipped.",
                mock=mock,
            )
            statuses.append(status)
            _notify_progress(progress_callback, "finish", spec.resource_id, status.status)
            continue
        if _resource_path_blocks_download(path):
            status = _status_for_spec(
                config,
                spec,
                status_override="incomplete",
                command=command,
                message=(
                    "Existing resource path is not a complete database; download skipped "
                    "to avoid overwriting. Remove it or configure another path before rerun."
                ),
                mock=mock,
            )
            statuses.append(status)
            _notify_progress(progress_callback, "finish", spec.resource_id, status.status)
            continue
        try:
            _notify_progress(progress_callback, "download", spec.resource_id, "downloading")
            _prepare_resource_download_target(path)
            _run_resource_command(config, spec, command)
            if spec.resource_id == "plasmidfinder":
                _run_plasmidfinder_install(config, path)
            _write_ready_sentinel(path, spec)
            status = _status_for_spec(
                config,
                spec,
                status_override="ok" if _resource_path_ready(path, spec) else "partial",
                command=command,
                message="Resource command completed.",
                mock=mock,
            )
            statuses.append(status)
            _notify_progress(progress_callback, "finish", spec.resource_id, status.status)
        except MemoryError:
            raise
        except ResourceError as exc:
            # A ResourceError indicates a real command/environment failure for
            # this resource; record it and continue so one bad DB doesn't abort
            # the whole batch, but surface the structured error distinctly.
            status = _status_for_spec(
                config,
                spec,
                status_override="failed",
                command=command,
                message=f"ResourceError: {exc}",
                mock=mock,
            )
            statuses.append(status)
            _notify_progress(progress_callback, "finish", spec.resource_id, status.status)
        except Exception as exc:  # pragma: no cover - external command boundary
            status = _status_for_spec(
                config,
                spec,
                status_override="failed",
                command=command,
                message=str(exc),
                mock=mock,
            )
            statuses.append(status)
            _notify_progress(progress_callback, "finish", spec.resource_id, status.status)
    if not dry_run:
        manifest_path = root / "resources.json"
        write_resource_manifest(statuses, manifest_path)
    return [status.to_dict() for status in statuses]


def write_resource_manifest(statuses: Iterable[ResourceStatus], path: str | Path) -> Path:
    manifest_path = Path(path)
    ensure_directory(manifest_path.parent, label="Resource manifest directory")
    data = {
        "analysis_type": "metagenomic_plasmid",
        "generated_at": datetime.now().isoformat(timespec="seconds"),
        "resources": [status.to_dict() for status in statuses],
    }
    manifest_path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
    return manifest_path


def write_resources_provenance(config: Mapping[str, Any], outdir: str | Path) -> Path:
    provenance_dir = Path(outdir) / "provenance"
    path = provenance_dir / "resources.json"
    allowed = set(ResourceStatus.__dataclass_fields__)
    statuses = [
        ResourceStatus(**{key: value for key, value in row.items() if key in allowed})
        for row in check_resources(config)
    ]
    write_resource_manifest(statuses, provenance_dir / "resource_manifest.json")
    return write_resource_manifest(statuses, path)


def write_environment_snapshot(
    config: Mapping[str, Any],
    registry: ToolRegistry,
    path: str | Path,
) -> Path:
    envs = []
    mamba_root = resolved_mamba_root()
    for tool in registry.list_tools():
        env_name = str(tool.get("env_name", ""))
        executable = str(tool.get("executable", ""))
        executable_path = mamba_root / "envs" / env_name / "bin" / executable
        envs.append(
            {
                "tool_id": tool.get("id"),
                "env_name": env_name,
                "executable": executable,
                "executable_path": str(executable_path),
                "executable_exists": executable_path.exists(),
            }
        )
    data = {
        "generated_at": datetime.now().isoformat(timespec="seconds"),
        "mamba_root": str(mamba_root),
        "profile": config.get("profile", ""),
        "environments": envs,
    }
    snapshot_path = Path(path)
    write_yaml(data, snapshot_path)
    return snapshot_path


def fetch_example_dataset(
    dataset: str,
    outdir: str | Path,
    *,
    mock: bool = False,
) -> Dict[str, Any]:
    if dataset not in EXAMPLE_ACCESSIONS:
        raise ResourceError(f"Unsupported example dataset: {dataset}")
    output_dir = ensure_directory(outdir, label="Example dataset output directory")
    rows = []
    files = []
    failures = []
    for accession in EXAMPLE_ACCESSIONS[dataset]:
        url = _efetch_url(accession)
        fasta_path = output_dir / f"{accession}.fasta"
        if mock:
            text = f">{accession} mock plasmid sequence\nATGCATGCATGCATGCATGC\n"
            fasta_path.write_text(text, encoding="utf-8")
        else:
            if not _fetch_fasta_atomically(url, fasta_path, accession):
                failures.append(accession)
                continue
        checksum = sha256_path(fasta_path)
        rows.append(
            {
                "sample_id": accession.replace(".", "_"),
                "group": "public",
                "platform": "assembly",
                "read1": "",
                "read2": "",
                "long_reads": "",
                "assembly": str(fasta_path),
                "technology": "RefSeq",
                "host_reference": "",
                "notes": f"source={url};sha256={checksum}",
            }
        )
        files.append(
            {
                "accession": accession,
                "url": url,
                "path": str(fasta_path),
                "sha256": checksum,
            }
        )
    sample_sheet = output_dir / "sample_sheet.tsv"
    fields = [
        "sample_id",
        "group",
        "platform",
        "read1",
        "read2",
        "long_reads",
        "assembly",
        "technology",
        "host_reference",
        "notes",
    ]
    with sample_sheet.open("w", encoding="utf-8") as handle:
        handle.write("\t".join(fields) + "\n")
        for row in rows:
            handle.write("\t".join(str(row[field]) for field in fields) + "\n")
    manifest = output_dir / "dataset_manifest.json"
    manifest.write_text(
        json.dumps(
            {
                "dataset": dataset,
                "generated_at": datetime.now().isoformat(timespec="seconds"),
                "mock": mock,
                "files": files,
                "sample_sheet": str(sample_sheet),
                "failures": failures,
            },
            indent=2,
            ensure_ascii=False,
        ),
        encoding="utf-8",
    )
    if failures and not rows:
        raise ResourceError(
            f"Failed to download any accessions for dataset {dataset!r}: {failures}"
        )
    return {"dataset": dataset, "sample_sheet": sample_sheet, "manifest": manifest, "files": files}


def _fetch_fasta_atomically(url: str, fasta_path: Path, accession: str) -> bool:
    """Download a single FASTA accession atomically with retry/backoff.

    Writes to a ``.part`` file and atomically renames on success, so a partial
    download is never reported as a valid file. Retries up to 3 times with
    exponential backoff to tolerate NCBI eutils 429/timeout transients. Returns
    True on success, False if all retries are exhausted (caller continues with
    remaining accessions).
    """
    part_path = fasta_path.with_suffix(".fasta.part")
    part_path.unlink(missing_ok=True)
    max_attempts = 3
    for attempt in range(1, max_attempts + 1):
        try:
            with urllib.request.urlopen(url, timeout=60) as response:  # noqa: S310
                data = response.read()
            if not data:
                raise ResourceError("empty response")
            part_path.write_bytes(data)
            os.replace(part_path, fasta_path)
            return True
        except (urllib.error.URLError, TimeoutError, OSError, ResourceError) as exc:
            part_path.unlink(missing_ok=True)
            if attempt >= max_attempts:
                print(f"  {accession}: download failed after {attempt} attempts: {exc}")
                return False
            backoff = 2 ** (attempt - 1)
            print(f"  {accession}: attempt {attempt} failed ({exc}); retrying in {backoff}s")
            time.sleep(backoff)
    return False


def required_resource_issues(
    config: Mapping[str, Any],
    selected_tools: Iterable[str],
) -> List[str]:
    selected = set(selected_tools)
    issues = []
    for spec in default_resource_specs(config):
        if spec.tool_id not in selected:
            continue
        status = _status_for_spec(config, spec)
        if status.status != "ok":
            issues.append(
                f"{spec.tool_id}.{spec.field} is {status.status}: {status.path}. "
                "Run setup-resources or configure a valid local path."
            )
    return issues


def sha256_path(path: str | Path) -> str:
    digest = hashlib.sha256()
    with Path(path).open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def _status_for_spec(
    config: Mapping[str, Any],
    spec: ResourceSpec,
    *,
    status_override: str | None = None,
    command: List[str] | None = None,
    message: str = "",
    mock: bool = False,
) -> ResourceStatus:
    path = _configured_resource_path(config, spec)
    if status_override:
        status = status_override
    elif any(marker in str(path) for marker in ("NOT_CONFIGURED", "PLACEHOLDER", "TODO")):
        status = "not_configured"
    elif _resource_path_ready(path, spec):
        status = "ok"
    else:
        status = "incomplete" if path.exists() else "missing"
    resource_block = _resource_block(config, spec.resource_id)
    checksum = str(resource_block.get("checksum", ""))
    checksum_method = "configured" if checksum else ""
    if not checksum and path.is_file():
        checksum = sha256_path(path)
        checksum_method = "sha256:file-content"
    elif not checksum and path.is_dir():
        checksum = _directory_manifest_checksum(path)
        checksum_method = "sha256:directory-manifest-v1"
    file_count, size_bytes = _directory_summary(path)
    checked_at = datetime.now().isoformat(timespec="seconds")
    return ResourceStatus(
        resource_id=spec.resource_id,
        tool_id=spec.tool_id,
        field=spec.field,
        path=str(path),
        status=status,
        version=str(resource_block.get("version", spec.version)),
        source_url=str(resource_block.get("source_url", spec.source_url)),
        checksum=checksum,
        checksum_method=checksum_method,
        date=str(resource_block.get("date", checked_at[:10])),
        command=command or _resolved_resource_command(config, spec, path),
        ready_check=_resource_ready_check(path, spec),
        directory_file_count=file_count,
        directory_size_bytes=size_bytes,
        message=message,
        last_checked_at=checked_at,
        mock=mock,
    )


def _directory_manifest_checksum(path: Path) -> str:
    """Return a bounded, deterministic fingerprint for a database directory.

    Hashing multi-hundred-GB databases at every run is not operationally
    acceptable.  This manifest checksum covers relative paths and file sizes,
    plus the content of small version/manifest metadata files.  A provider
    archive checksum can still be supplied in config and should be preferred
    when available.
    """
    digest = hashlib.sha256()
    entries = 0
    metadata_names = {
        "version",
        "version.txt",
        "manifest.json",
        "metadata.json",
        RESOURCE_READY_SENTINEL,
    }
    truncated = False
    for root, dirnames, filenames in os.walk(path):
        dirnames.sort()
        filenames.sort()
        for filename in filenames:
            if entries >= MAX_DIRECTORY_SUMMARY_ENTRIES:
                truncated = True
                break
            child = Path(root) / filename
            relative = child.relative_to(path).as_posix()
            stat = child.stat()
            digest.update(relative.encode("utf-8", errors="surrogateescape"))
            digest.update(b"\0")
            digest.update(str(stat.st_size).encode("ascii"))
            digest.update(b"\0")
            if child.name.lower() in metadata_names and stat.st_size <= 1024 * 1024:
                digest.update(child.read_bytes())
            entries += 1
        if truncated:
            digest.update(b"TRUNCATED")
            break
    return digest.hexdigest()


def _configured_resource_path(config: Mapping[str, Any], spec: ResourceSpec) -> Path:
    block = _resource_block(config, spec.resource_id)
    value = block.get(spec.field)
    if not value:
        return resource_root(config) / spec.default_subdir
    path = Path(str(value))
    if path.is_absolute() or _is_placeholder_resource_value(value):
        return path
    return resource_root(config) / path


def apply_resource_overrides(
    config: Dict[str, Any], overrides: Sequence[str]
) -> List[tuple[str, str, str]]:
    """Apply ``--resource id=path`` / ``--resource id:field=path`` overrides.

    Returns the list of ``(resource_id, field, path)`` tuples that were applied,
    so callers can surface them in provenance.  Raises ``ResourceError`` when
    an unknown ``resource_id`` is supplied (the field name is taken from the
    matching ``ResourceSpec`` when not explicitly given).
    """
    specs = {s.resource_id: s for s in default_resource_specs(config)}
    applied: List[tuple[str, str, str]] = []
    resources = config.setdefault("resources", {})
    if not isinstance(resources, Mapping):
        resources = {}
        config["resources"] = resources
    for item in overrides:
        key, _, path = item.partition("=")
        key = key.strip()
        path = path.strip()
        if not key or not path:
            raise ResourceError(
                f"Invalid --resource override (expected id=path or id:field=path): {item!r}"
            )
        resource_id: str
        field: str
        if ":" in key:
            resource_id, _, field = key.partition(":")
            resource_id = resource_id.strip()
            field = field.strip()
            if resource_id not in specs:
                raise ResourceError(
                    f"Unknown resource id '{resource_id}'; available: " + ", ".join(sorted(specs))
                )
        else:
            resource_id = key
            spec = specs.get(resource_id)
            if spec is None:
                raise ResourceError(
                    f"Unknown resource id '{resource_id}'; available: " + ", ".join(sorted(specs))
                )
            field = spec.field
        block = resources.setdefault(resource_id, {})
        if not isinstance(block, dict):
            block = {}
            resources[resource_id] = block
        block[field] = path
        applied.append((resource_id, field, path))
    return applied


def _resource_block(config: Mapping[str, Any], resource_id: str) -> Mapping[str, Any]:
    resources = config.get("resources", {})
    if not isinstance(resources, Mapping):
        return {}
    block = resources.get(resource_id, {})
    return block if isinstance(block, Mapping) else {}


def _kraken2_version(config: Mapping[str, Any], spec: ResourceSpec) -> str:
    """Resolve the Kraken2 database version from config or spec default."""
    block = _resource_block(config, spec.resource_id)
    configured = str(block.get("version") or "").strip()
    if configured and not _is_placeholder_text(configured):
        return configured
    return str(spec.version or KRAKEN2_DEFAULT_VERSION)


def _is_placeholder_text(value: str) -> bool:
    upper = value.upper()
    return any(marker in upper for marker in ("NOT_CONFIGURED", "TODO", "PLACEHOLDER"))


def _resolved_resource_command(
    config: Mapping[str, Any], spec: ResourceSpec, target_path: Path
) -> List[str]:
    # ── tool install commands (resource_type-prefixed) ──
    if spec.resource_type == "tool_git":
        return [
            "git",
            "clone",
            "--depth",
            "1",
            "--single-branch",
            spec.source_url,
            str(target_path),
        ]
    if spec.resource_type == "tool_pip":
        return ["pip", "install", spec.source_url, "--target", str(target_path)]
    if spec.resource_type == "tool_download":
        # Return a dry-run placeholder; the real download (with parent-dir
        # creation, tarball extraction, and atomic write) is driven by
        # _run_tool_download to avoid path-doubling and partial-file reuse.
        return [
            "wget",
            "-O",
            str(target_path / spec.executable),
            spec.source_url,
        ]
    # ── database download commands (resource_id-specific fallback) ──
    if spec.resource_id == "genomad":
        return ["genomad", "download-database", str(target_path)]
    if spec.resource_id == "bakta":
        db_type = str(_resource_block(config, "bakta").get("type", "light"))
        return ["bakta_db", "download", "--output", str(target_path), "--type", db_type]
    if spec.resource_id == "mob_suite":
        return ["mob_init", "--database_directory", str(target_path)]
    if spec.resource_id == "plasmidfinder":
        source_url = str(
            _resource_block(config, "plasmidfinder").get(
                "source_url",
                PLASMIDFINDER_DB_URL,
            )
        )
        return [
            "git",
            "clone",
            "--depth",
            "1",
            "--single-branch",
            source_url,
            str(target_path),
        ]
    if spec.resource_id == "amrfinderplus":
        return ["amrfinder_update", "-d", str(target_path)]
    if spec.resource_id == "kraken2":
        version = _kraken2_version(config, spec)
        return [
            "aria2c",
            "-x",
            "8",
            "-s",
            "8",
            "--continue=true",
            "--max-tries=3",
            f"https://genome-idx.s3.amazonaws.com/kraken/k2_{version}.tar.gz",
        ]
    if spec.resource_id == "gtdbtk":
        return ["gtdbtk", "db", "download"]
    if spec.resource_id == "checkm2":
        return ["checkm2", "download", "--path", str(target_path)]
    return list(spec.command_template)


def _timeout_output(output: Any) -> str:
    if output is None:
        return ""
    if isinstance(output, bytes):
        return output.decode("utf-8", errors="replace")
    return str(output)


def _resource_timeout_seconds(config: Mapping[str, Any]) -> float | None:
    execution = mapping_block(config, "execution")
    resources = mapping_block(config, "resources")
    value = resources.get("timeout_seconds")
    if value is None:
        value = execution.get("resource_timeout_seconds")
    if value is None:
        value = execution.get("tool_timeout_seconds")
    return timeout_from_env_or_value(
        "AUTOPLASM_RESOURCE_TIMEOUT_SECONDS",
        value,
        default=DEFAULT_RESOURCE_TIMEOUT_SECONDS,
    )


def _raise_on_timeout(
    spec: ResourceSpec,
    timeout_seconds: float | None,
    exc: subprocess.TimeoutExpired,
) -> None:
    stderr = _timeout_output(exc.stderr)
    timeout_text = "configured timeout" if timeout_seconds is None else f"{timeout_seconds:g}s"
    message = f"{spec.resource_id} setup timed out after {timeout_text}"
    details = "\n".join(text for text in [message, stderr.strip()] if text)
    raise ResourceError(details) from exc


def _run_resource_command(
    config: Mapping[str, Any], spec: ResourceSpec, command: List[str]
) -> None:
    # Kraken2 uses a multi-step atomic download (mkdir → aria2c → tar → rm)
    # run as separate safe list-form subprocess calls instead of a shell
    # string, to avoid command injection and partial-download wedging.
    if spec.resource_id == "kraken2":
        _run_kraken2_download(config, spec)
        return
    # tool_download resources need parent-dir creation + optional tarball
    # extraction handled atomically (see _run_tool_download).
    if spec.resource_type == "tool_download":
        _run_tool_download(config, spec)
        return
    executable = command[0]
    resolved = _resolve_executable(config, spec.env_name, executable)
    run_command = [str(resolved), *command[1:]]
    timeout_seconds = _resource_timeout_seconds(config)
    try:
        completed = subprocess.run(
            run_command,
            check=False,
            text=True,
            capture_output=True,
            env=_resource_runtime_env(config, spec.env_name, spec),
            timeout=timeout_seconds,
        )
    except subprocess.TimeoutExpired as exc:
        _raise_on_timeout(spec, timeout_seconds, exc)
    if completed.returncode != 0:
        details = "\n".join(
            text for text in [completed.stderr.strip(), completed.stdout.strip()] if text
        )
        raise ResourceError(
            f"{spec.resource_id} setup failed with code {completed.returncode}: {details}"
        )
    # Post-install command (e.g. "pip install -e ." after git clone)
    if spec.install_post:
        target_path = _configured_resource_path(config, spec)
        if spec.install_post == "__amrfinderplus_post__":
            _run_amrfinderplus_post(config, spec, target_path, timeout_seconds)
            return
        post_cmd = spec.install_post.split()
        post_resolved = _resolve_executable(config, spec.env_name, post_cmd[0])
        post_run = [str(post_resolved), *post_cmd[1:]]
        post_completed = subprocess.run(
            post_run,
            check=False,
            text=True,
            capture_output=True,
            cwd=str(target_path),
            env=_resource_runtime_env(config, spec.env_name, spec),
            timeout=timeout_seconds,
        )
        if post_completed.returncode != 0:
            raise ResourceError(
                f"{spec.resource_id} post-install '{spec.install_post}' failed: "
                f"{post_completed.stderr.strip()}"
            )


def _run_amrfinderplus_post(
    config: Mapping[str, Any],
    spec: ResourceSpec,
    target_path: Path,
    timeout_seconds: float | None,
) -> None:
    """Build a BLAST protein index from AMRProt.fa after amrfinder_update.

    Discovers the AMRProt.fa location dynamically instead of assuming a
    hardcoded ``latest/`` layout, so this survives upstream layout changes
    or pinned versioned paths.
    """
    candidates = list(target_path.rglob("AMRProt.fa"))
    if not candidates:
        raise ResourceError(f"amrfinderplus post-install: AMRProt.fa not found under {target_path}")
    amr_prot = candidates[0]
    makeblastdb = _resolve_executable(config, spec.env_name, "makeblastdb")
    completed = subprocess.run(
        [
            str(makeblastdb),
            "-in",
            str(amr_prot),
            "-dbtype",
            "prot",
            "-out",
            str(amr_prot),
        ],
        check=False,
        text=True,
        capture_output=True,
        cwd=str(target_path),
        env=_resource_runtime_env(config, spec.env_name, spec),
        timeout=timeout_seconds,
    )
    if completed.returncode != 0:
        raise ResourceError(
            f"amrfinderplus post-install 'makeblastdb' failed: {completed.stderr.strip()}"
        )


def _run_kraken2_download(config: Mapping[str, Any], spec: ResourceSpec) -> None:
    """Download and extract the Kraken2 standard database atomically.

    Avoids the shell-injection and partial-download-wedge bugs by running
    aria2c / tar / rm as separate list-form subprocess calls, writing the
    tarball to a ``.part`` path, extracting into a staging directory, and
    swapping it into place only on full success.
    """
    target_path = _configured_resource_path(config, spec)
    version = _kraken2_version(config, spec)
    url = f"https://genome-idx.s3.amazonaws.com/kraken/k2_{version}.tar.gz"
    timeout_seconds = _resource_timeout_seconds(config)
    runtime_env = _resource_runtime_env(config, spec.env_name, spec)

    target_path.parent.mkdir(parents=True, exist_ok=True)
    tarball = target_path.parent / "kraken2.tar.gz.part"
    staging = target_path.parent / "kraken2.staging"

    # Clean up any leftovers from a previous interrupted attempt.
    tarball.unlink(missing_ok=True)
    if staging.exists():
        shutil.rmtree(staging, ignore_errors=True)

    try:
        aria2c = _resolve_executable(config, spec.env_name, "aria2c")
        completed = subprocess.run(
            [
                str(aria2c),
                "-x",
                "8",
                "-s",
                "8",
                "--continue=true",
                "--max-tries=3",
                "--retry-wait=5",
                "-d",
                str(target_path.parent),
                "-o",
                tarball.name,
                url,
            ],
            check=False,
            text=True,
            capture_output=True,
            env=runtime_env,
            timeout=timeout_seconds,
        )
        if completed.returncode != 0:
            details = "\n".join(
                text for text in [completed.stderr.strip(), completed.stdout.strip()] if text
            )
            raise ResourceError(
                f"kraken2 download failed with code {completed.returncode}: {details}"
            )
        if not tarball.exists() or tarball.stat().st_size == 0:
            raise ResourceError("kraken2 download produced an empty tarball")

        staging.mkdir(parents=True, exist_ok=True)
        tar = _resolve_executable(config, spec.env_name, "tar")
        completed = subprocess.run(
            [str(tar), "xzf", str(tarball), "-C", str(staging)],
            check=False,
            text=True,
            capture_output=True,
            env=runtime_env,
            timeout=timeout_seconds,
        )
        if completed.returncode != 0:
            details = "\n".join(
                text for text in [completed.stderr.strip(), completed.stdout.strip()] if text
            )
            raise ResourceError(
                f"kraken2 extraction failed with code {completed.returncode}: {details}"
            )
        # Atomic swap: remove any prior partial target, then move staging in.
        if target_path.exists():
            shutil.rmtree(target_path, ignore_errors=True)
        os.replace(staging, target_path)
    except subprocess.TimeoutExpired as exc:
        _raise_on_timeout(spec, timeout_seconds, exc)
    finally:
        tarball.unlink(missing_ok=True)
        if staging.exists():
            shutil.rmtree(staging, ignore_errors=True)


def _flatten_single_top_level_dir(target_path: Path) -> None:
    """Lift contents of a lone top-level directory up to ``target_path``.

    GitHub source archives (``.../archive/refs/heads/<branch>.tar.gz``) and
    many Zenodo tarballs wrap their content in a single top-level directory
    named ``<repo>-<branch>/``. After ``tar xf -C target_path`` the real
    executable ends up at ``target_path/<repo>-<branch>/executable`` while
    readiness checks look for ``target_path/executable`` and fail. This helper
    detects that single-wrapper case and moves the inner entries up one level.
    """
    children = [c for c in target_path.iterdir() if not c.name.startswith(".")]
    if len(children) != 1 or not children[0].is_dir():
        return
    nested = children[0]
    for entry in nested.iterdir():
        os.replace(entry, target_path / entry.name)
    nested.rmdir()


def _run_tool_download(config: Mapping[str, Any], spec: ResourceSpec) -> None:
    """Download a tool resource atomically with parent-dir creation.

    Fixes the previous ``wget -O target/default_subdir url`` issues:
    missing ``mkdir``, path doubling (``_configured_resource_path`` already
    appends ``default_subdir``), and no extraction for tarball URLs. The
    download goes to a ``.part`` file, then is atomically renamed; tarball
    sources are extracted into the target directory.
    """
    target_path = _configured_resource_path(config, spec)
    timeout_seconds = _resource_timeout_seconds(config)
    runtime_env = _resource_runtime_env(config, spec.env_name, spec)

    target_path.mkdir(parents=True, exist_ok=True)
    is_tarball = spec.source_url.lower().endswith((".tar.gz", ".tgz", ".tar"))
    part_file = target_path.with_suffix(target_path.suffix + ".part")

    # Clean up leftovers from a previous interrupted attempt.
    part_file.unlink(missing_ok=True)

    try:
        wget = _resolve_executable(config, spec.env_name, "wget")
        completed = subprocess.run(
            [str(wget), "-O", str(part_file), spec.source_url],
            check=False,
            text=True,
            capture_output=True,
            env=runtime_env,
            timeout=timeout_seconds,
        )
        if completed.returncode != 0:
            details = "\n".join(
                text for text in [completed.stderr.strip(), completed.stdout.strip()] if text
            )
            raise ResourceError(
                f"{spec.resource_id} download failed with code {completed.returncode}: {details}"
            )
        if not part_file.exists() or part_file.stat().st_size == 0:
            raise ResourceError(f"{spec.resource_id} download produced an empty file")

        if is_tarball:
            tar = _resolve_executable(config, spec.env_name, "tar")
            completed = subprocess.run(
                [str(tar), "xf", str(part_file), "-C", str(target_path)],
                check=False,
                text=True,
                capture_output=True,
                env=runtime_env,
                timeout=timeout_seconds,
            )
            if completed.returncode != 0:
                details = "\n".join(
                    text for text in [completed.stderr.strip(), completed.stdout.strip()] if text
                )
                raise ResourceError(
                    f"{spec.resource_id} extraction failed with code "
                    f"{completed.returncode}: {details}"
                )
            # Flatten a single top-level directory (GitHub archives put content
            # under <repo>-<branch>/, e.g. conjscan-master/conjscan). Without
            # this, ready_check looks for the executable at target_path/executable
            # but it actually lives at target_path/<repo>-<branch>/executable.
            _flatten_single_top_level_dir(target_path)
        else:
            # Plain file: place it as the executable name inside the target.
            dest = target_path / spec.executable
            os.replace(part_file, dest)
    except subprocess.TimeoutExpired as exc:
        _raise_on_timeout(spec, timeout_seconds, exc)
    finally:
        part_file.unlink(missing_ok=True)


def _run_plasmidfinder_install(config: Mapping[str, Any], path: Path) -> None:
    install = path / "INSTALL.py"
    if not install.exists():
        return
    python = _resolve_executable(config, "autoplasm-annotation", "python")
    kma_index = _resolve_executable(config, "autoplasm-annotation", "kma_index")
    timeout_seconds = _resource_timeout_seconds(config)
    try:
        completed = subprocess.run(
            [str(python), str(install.resolve()), str(kma_index)],
            check=False,
            cwd=str(path),
            text=True,
            capture_output=True,
            env=_resource_runtime_env(config, "autoplasm-annotation"),
            timeout=timeout_seconds,
        )
    except subprocess.TimeoutExpired as exc:
        _raise_on_timeout(
            ResourceSpec(
                resource_id="plasmidfinder",
                tool_id="plasmidfinder",
                field="database",
                env_name="autoplasm-annotation",
                executable="python",
                default_subdir="plasmidfinder_db",
                source_url=PLASMIDFINDER_DB_URL,
                command_template=[],
            ),
            timeout_seconds,
            exc,
        )
    if completed.returncode != 0:
        details = "\n".join(
            text for text in [completed.stderr.strip(), completed.stdout.strip()] if text
        )
        raise ResourceError(
            f"plasmidfinder database INSTALL.py failed with code {completed.returncode}: {details}"
        )


def _resolve_executable(config: Mapping[str, Any], env_name: str, executable: str) -> Path:
    if executable == "git":
        git = shutil.which("git")
        if not git:
            raise ResourceError("git is required to download the PlasmidFinder database")
        return Path(git)
    mamba_root = resolved_mamba_root()
    env_bin = mamba_root / "envs" / env_name / "bin"
    candidate = env_bin / executable
    if candidate.exists():
        return candidate
    resolved = shutil.which(executable, path=str(env_bin))
    if resolved:
        return Path(resolved)
    # Fall back to system PATH (needed for bash, aria2c, tar, mkdir, etc.)
    system_resolved = shutil.which(executable)
    if system_resolved:
        return Path(system_resolved)
    if config.get("mock_tools"):
        return Path(executable)
    raise ResourceError(f"Executable {executable!r} was not found in {env_bin} or system PATH")


def _resource_runtime_env(
    config: Mapping[str, Any],
    env_name: str,
    spec: ResourceSpec | None = None,
) -> Dict[str, str]:
    env = os.environ.copy()
    mamba_root = resolved_mamba_root()
    env_bin = mamba_root / "envs" / env_name / "bin"
    if env_bin.exists():
        env["PATH"] = f"{env_bin}{os.pathsep}{env.get('PATH', '')}"
        env["CONDA_PREFIX"] = str(mamba_root / "envs" / env_name)
        env["MAMBA_ROOT_PREFIX"] = str(mamba_root)
        env.pop("PYTHONPATH", None)
    # GTDB-Tk needs GTDBTK_DATA_PATH to locate the database during download
    if spec is not None and spec.resource_id == "gtdbtk":
        block = _resource_block(config, spec.resource_id)
        path_value = block.get(spec.field)
        if path_value:
            env["GTDBTK_DATA_PATH"] = str(Path(str(path_value)))
        else:
            env["GTDBTK_DATA_PATH"] = str(resource_root(config) / spec.default_subdir)
    # CheckM2 may need CHECKM2DB to place the database
    if spec is not None and spec.resource_id == "checkm2":
        block = _resource_block(config, spec.resource_id)
        path_value = block.get(spec.field)
        if path_value:
            env["CHECKM2DB"] = str(Path(str(path_value)))
    return env


def _git_worktree_valid(path: Path) -> bool:
    """Return True if ``path`` is a valid git worktree (not a partial clone).

    A failed/interrupted ``git clone`` often leaves a ``.git`` directory, so
    checking only ``.git`` presence yields false positives. This runs
    ``git -C path rev-parse --is-inside-work-tree`` which only succeeds for a
    usable worktree with a valid HEAD.
    """
    if not (path.exists() and (path / ".git").exists()):
        return False
    try:
        completed = subprocess.run(
            ["git", "-C", str(path), "rev-parse", "--is-inside-work-tree"],
            check=False,
            text=True,
            capture_output=True,
            timeout=30,
        )
    except (OSError, subprocess.TimeoutExpired):
        return False
    return completed.returncode == 0 and completed.stdout.strip() == "true"


def _resource_path_ready(path: Path, spec: ResourceSpec) -> bool:
    if not path.exists():
        return False
    if path.is_file():
        return True
    if (path / ".lock").exists():
        return False
    if (path / RESOURCE_READY_SENTINEL).exists():
        return True
    if spec.resource_id == "genomad":
        return (path / "genomad_db").exists()
    if spec.resource_id == "mob_suite":
        blast_suffixes = {".nhr", ".nin", ".nsq", ".phr", ".pin", ".psq"}
        return any(file.suffix in blast_suffixes for file in path.glob("*"))
    if spec.resource_id == "plasmidfinder":
        return (path / "config").exists() and any(path.glob("*.fsa"))
    if spec.resource_id == "metaphlan":
        return any(path.glob("mpa_*.pkl")) or any(path.glob("*.bt2l"))
    if spec.resource_id == "amrfinderplus":
        return (
            (path / "AMR_CDS").exists()
            or any(path.glob("AMR.LIB*"))
            or (path / "database").is_dir()
        )
    if spec.resource_id == "kraken2":
        return (path / "taxonomy").is_dir() and (path / "library").is_dir()
    if spec.resource_id == "gtdbtk":
        return (path / "markers").is_dir() and any((path / "markers").glob("*.hmm"))
    if spec.resource_id == "checkm2":
        return any(path.glob("*.pkl")) or (path / "checkm2").is_dir()
    if spec.resource_id == "plasme":
        return (path / "PLASMe_DB").is_dir()
    if spec.resource_id == "plasx_annotations":
        return (path / "data").is_dir() or (path / "COG").is_dir()
    if spec.resource_id == "plasx_model":
        return (path / "plasx_model.pkl").exists() or (path / "data" / "plasx_model.pkl").exists()
    if spec.resource_id == "copla_refgraph":
        return (path / "COPLA_DB").is_dir() or any(path.glob("*_reference_graph.gml"))
    if spec.resource_id == "copla_reflist":
        return (path / "COPLA_DB").is_dir() or any(path.glob("*_reference_list.txt"))
    if spec.resource_id == "blast":
        blast_suffixes = {".nhr", ".nin", ".nsq", ".ndb", ".not", ".ntf", ".nto"}
        return any(file.suffix in blast_suffixes for file in path.glob("*.n*"))
    if spec.resource_id == "plasmidhostfinder":
        return (path / "config").exists() and any(path.glob("*.fsa"))
    # ── tool type readiness ──
    if spec.resource_type == "tool_git":
        return _git_worktree_valid(path)
    if spec.resource_type == "tool_pip":
        return path.exists() and any(path.iterdir())
    if spec.resource_type == "tool_download":
        if spec.executable:
            return path.exists() and any(path.glob(spec.executable))
        return path.is_dir()
    return path.is_dir() and any(path.iterdir())


def _resource_ready_check(path: Path, spec: ResourceSpec) -> str:
    if not path.exists():
        return "path missing"
    if path.is_file():
        return "resource file exists"
    if (path / ".lock").exists():
        return "lock file present"
    if (path / RESOURCE_READY_SENTINEL).exists():
        return "ready sentinel found"
    if spec.resource_id == "genomad":
        if (path / "genomad_db").exists():
            return "genomad_db directory found"
        return "genomad_db directory missing"
    if spec.resource_id == "mob_suite":
        blast_suffixes = {".nhr", ".nin", ".nsq", ".phr", ".pin", ".psq"}
        if any(file.suffix in blast_suffixes for file in path.glob("*")):
            return "MOB-suite BLAST index found"
        return "MOB-suite BLAST index missing"
    if spec.resource_id == "plasmidfinder":
        if (path / "config").exists() and any(path.glob("*.fsa")):
            return "PlasmidFinder config and fsa files found"
        return "PlasmidFinder config or fsa files missing"
    if spec.resource_id == "metaphlan":
        if any(path.glob("mpa_*.pkl")) or any(path.glob("*.bt2l")):
            return "MetaPhlAn marker database files found"
        return "MetaPhlAn marker database files missing"
    if spec.resource_id == "amrfinderplus":
        if (path / "AMR_CDS").exists() or any(path.glob("AMR.LIB*")):
            return "AMRFinderPlus database found (AMR_CDS/AMR.LIB)"
        if (path / "database").is_dir():
            return "AMRFinderPlus database directory found"
        return "AMRFinderPlus database missing — run 'amrfinder_update -d <path>'"
    if spec.resource_id == "kraken2":
        if (path / "taxonomy").is_dir() and (path / "library").is_dir():
            return "Kraken2 standard database found"
        return "Kraken2 standard database missing (WARNING: ~50 GB download)"
    if spec.resource_id == "gtdbtk":
        if (path / "markers").is_dir():
            return "GTDB-Tk marker database found"
        return "GTDB-Tk database missing (WARNING: ~30 GB download; sets GTDBTK_DATA_PATH)"
    if spec.resource_id == "checkm2":
        if any(path.glob("*.pkl")):
            return "CheckM2 model files found"
        return "CheckM2 database missing — run 'checkm2 download --path <path>'"
    if spec.resource_id == "plasme":
        if (path / "PLASMe_DB").is_dir():
            return "PLASMe_DB directory found"
        return (
            "PLASMe database missing. Download PLASMe_DB.tar.gz from "
            "https://github.com/ccb-hms/PLASMe/releases and extract to this path."
        )
    if spec.resource_id == "plasx_annotations":
        if (path / "data").is_dir() or (path / "COG").is_dir():
            return "PlasX annotations/cog data found"
        return (
            "PlasX annotations missing. Clone https://github.com/michaelgoldman/PlasX "
            "and configure this path to the data/ directory."
        )
    if spec.resource_id == "plasx_model":
        if (path / "plasx_model.pkl").exists() or (path / "data" / "plasx_model.pkl").exists():
            return "PlasX model file found"
        return (
            "PlasX model missing. Clone https://github.com/michaelgoldman/PlasX "
            "and configure this path to data/plasx_model.pkl."
        )
    if spec.resource_id == "copla_refgraph":
        if (path / "COPLA_DB").is_dir() or any(path.glob("*_reference_graph.gml")):
            return "COPLA reference graph found"
        return (
            "COPLA reference graph missing. Download COPLA_DB from "
            "https://github.com/BeatrizBonete/COPLA (Zenodo record) and extract to this path."
        )
    if spec.resource_id == "copla_reflist":
        if (path / "COPLA_DB").is_dir() or any(path.glob("*_reference_list.txt")):
            return "COPLA reference list found"
        return (
            "COPLA reference list missing. Download COPLA_DB from "
            "https://github.com/BeatrizBonete/COPLA (Zenodo record) and extract to this path."
        )
    if spec.resource_id == "blast":
        blast_suffixes = {".nhr", ".nin", ".nsq", ".ndb", ".not", ".ntf", ".nto"}
        if any(file.suffix in blast_suffixes for file in path.glob("*.n*")):
            return "BLAST nucleotide database found"
        return (
            "BLAST database missing. Use 'update_blastdb.pl --decompress nt' or "
            "download pre-built from https://ftp.ncbi.nlm.nih.gov/blast/db/."
        )
    if spec.resource_id == "plasmidhostfinder":
        if (path / "config").exists() and any(path.glob("*.fsa")):
            return "PlasmidHostFinder database found"
        return (
            "PlasmidHostFinder database missing. May reuse the PlasmidFinder database "
            "from https://bitbucket.org/genomicepidemiology/plasmidfinder_db.git."
        )
    # ── tool type ready checks ──
    if spec.resource_type == "tool_git":
        if _git_worktree_valid(path):
            return f"tool '{spec.resource_id}' cloned successfully (git repo found)"
        return (
            f"tool '{spec.resource_id}' not installed or clone is incomplete. "
            f"Run 'git clone --depth 1 --single-branch {spec.source_url} {path}' to install."
        )
    if spec.resource_type == "tool_pip":
        if path.exists() and any(path.iterdir()):
            return f"tool '{spec.resource_id}' installed via pip"
        return (
            f"tool '{spec.resource_id}' not installed. "
            f"Run 'pip install {spec.source_url} --target {path}' to install."
        )
    if spec.resource_type == "tool_download":
        if path.exists() and (
            any(path.glob(spec.executable)) if spec.executable else path.is_dir()
        ):
            return f"tool '{spec.resource_id}' downloaded"
        return (
            f"tool '{spec.resource_id}' not downloaded. Download from {spec.source_url} to {path}."
        )
    if path.is_dir() and any(path.iterdir()):
        return "non-empty database directory found"
    return "empty directory"


def _directory_summary(path: Path) -> tuple[int, int]:
    if not path.exists():
        return 0, 0
    if path.is_file():
        try:
            return 1, path.stat().st_size
        except OSError:
            return 1, 0
    count = 0
    size = 0
    for index, child in enumerate(path.rglob("*")):
        if index >= MAX_DIRECTORY_SUMMARY_ENTRIES:
            break
        try:
            if child.is_file():
                count += 1
                size += child.stat().st_size
        except OSError:
            continue
    return count, size


def _resource_path_blocks_download(path: Path) -> bool:
    return path.exists() and not _path_is_empty_directory(path)


def _prepare_resource_download_target(path: Path) -> None:
    if _path_is_empty_directory(path):
        path.rmdir()


def _path_is_empty_directory(path: Path) -> bool:
    return path.is_dir() and not any(path.iterdir())


def _write_ready_sentinel(path: Path, spec: ResourceSpec) -> None:
    if path.is_dir():
        (path / RESOURCE_READY_SENTINEL).write_text(f"{spec.resource_id}\n", encoding="utf-8")


def _notify_progress(
    callback: ResourceProgressCallback | None,
    event: str,
    resource_id: str,
    message: str,
) -> None:
    if callback:
        callback(event, resource_id, message)


def _efetch_url(accession: str) -> str:
    return (
        "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi"
        f"?db=nuccore&id={urllib.parse.quote(accession)}&rettype=fasta&retmode=text"
    )
