"""Deleting files on a system that will not let you.

POSIX unlinks a file whether or not anything has it open; the name goes
away and the data follows when the last handle closes. Windows refuses
outright — WinError 32, "the process cannot access the file because it is
being used by another process" — and it refuses a RENAME too, unless the
holder opened the file with FILE_SHARE_DELETE, which Python's open() does
not use. There is no forcing it.

Every delete path in this app was written on Linux and therefore assumes
the POSIX behaviour. The holder is nearly always transient and usually not
us: a video still streaming to the browser, numpy's lazy npz handle, an
antivirus scanner that opened the file the moment we wrote it.

So: try a few times, and if it still will not go, remember it and retry on
the minute loop. The caller carries on as though the file were deleted,
because it will be. A capture that cannot be discarded this second is a
nuisance; a 500 in the middle of tidying up is worse.
"""
import shutil
import time
from pathlib import Path

RETRIES = 4
BACKOFF_S = 0.15
QUEUE_NAME = "pending-deletes.txt"


def _queue_file() -> Path:
    from clipshelf.config import DATA
    return Path(DATA) / QUEUE_NAME


def defer(path) -> None:
    """Remember a file we could not delete yet."""
    try:
        q = _queue_file()
        q.parent.mkdir(parents=True, exist_ok=True)
        have = set()
        if q.exists():
            have = {ln.strip() for ln in q.read_text(encoding="utf-8").splitlines()}
        have.discard("")
        have.add(str(Path(path).resolve()))
        q.write_text("\n".join(sorted(have)) + "\n", encoding="utf-8")
    except Exception:
        pass


def remove(path, missing_ok: bool = True) -> bool:
    """Delete a file. True if it is gone now, False if it was deferred.

    False is not an error: treat the file as gone and carry on."""
    p = Path(path)
    for attempt in range(RETRIES):
        try:
            p.unlink()
            return True
        except FileNotFoundError:
            return bool(missing_ok)
        except PermissionError:
            if attempt == RETRIES - 1:
                break
            time.sleep(BACKOFF_S * (attempt + 1))
        except OSError:
            break
    if not p.exists():
        return True
    defer(p)
    return False


def sweep_pending() -> int:
    """Retry everything deferred. Cheap; runs on the same minute loop that
    processes clip marks."""
    q = _queue_file()
    try:
        paths = [ln.strip() for ln in q.read_text(encoding="utf-8").splitlines() if ln.strip()]
    except Exception:
        return 0
    left, gone = [], 0
    for entry in paths:
        p = Path(entry)
        if not p.exists():
            gone += 1
            continue
        try:
            p.unlink()
            gone += 1
        except OSError:
            left.append(entry)
    try:
        if left:
            q.write_text("\n".join(left) + "\n", encoding="utf-8")
        else:
            q.unlink(missing_ok=True)
    except Exception:
        pass
    if gone:
        print(f"[files] cleaned up {gone} deferred delete(s)", flush=True)
    return gone


def remove_tree(path) -> bool:
    """Best-effort recursive delete with the same tolerance."""
    p = Path(path)
    if not p.exists():
        return True
    for child in sorted(p.rglob("*"), key=lambda c: -len(c.parts)):
        if child.is_file() or child.is_symlink():
            remove(child)
    shutil.rmtree(p, ignore_errors=True)
    return not p.exists()
