"""
wivexx_disk.py — drive usage monitor + large-stale-file analytics  (v1.8.1)
═══════════════════════════════════════════════════════════════════════════
Two features:

1. start_usage_monitor(...) — watches each fixed drive (C:, D:, …) and fires a
   callback when one crosses USAGE warn threshold (default 85 %).  The callback
   is delivered through WIVEXX's non-interrupting announcement queue so it can
   never cut off something he is already saying.

2. analyze_drives(...) — scans the drives for the user's BIGGEST files that
   have not been opened in 3+ months, skipping system/program/app locations,
   and writes a readable report (one path per line, biggest first) to
   <saved_dir>\\Analytics\\.  Returns a short spoken summary.

Safety: analysis is READ-ONLY.  It never deletes anything — it only lists
candidate paths for YOU to review and delete manually.
"""

import datetime as _dt
import os
import threading
import time
from typing import Callable, Optional

# Locations that are never user content (case-insensitive path fragments)
_SKIP_FRAGMENTS = [
    "\\windows", "\\program files", "\\program files (x86)", "\\programdata",
    "\\$recycle.bin", "\\system volume information", "\\appdata\\local\\temp",
    "\\appdata\\local\\packages", "\\appdata\\locallow", "\\$windows.~",
    "\\recovery", "\\config.msi", "\\onedrivetemp",
    "\\node_modules", "\\.git", "\\__pycache__", "\\venv", "\\.cache",
    "\\steamapps", "\\epic games", "\\riot games", "\\nvidia",
    "\\program files\\windowsapps", "\\microsoft\\windows",
]
# Extensions that are almost always part of an app/system, never user media/docs
_SKIP_EXTS = {
    ".dll", ".sys", ".exe", ".msi", ".cab", ".lib", ".obj", ".pdb", ".tmp",
    ".log", ".etl", ".dmp", ".pak", ".bin", ".dat", ".pyc", ".manifest",
    ".cat", ".inf", ".mui", ".nls", ".ax", ".ocx", ".drv", ".vxd",
}

_usage_thread: Optional[threading.Thread] = None
_usage_stop = threading.Event()

# Shared analysis state (read by the GUI: working animation + progress card)
ANALYSIS = {"active": False, "scanned": 0, "drive": "", "found": 0,
            "current": "", "phase": "idle", "started": 0.0, "summary": ""}
# Structured result of the LAST completed scan — drives the in-app "drive
# cleanup" tab (file list + per-file open-folder / delete, recoverable).
LAST_ANALYSIS = {"files": [], "total_gb": 0.0, "generated": "",
                 "drives": "", "report": ""}
_analysis_lock = threading.Lock()


# ── 1. Usage monitor ───────────────────────────────────────────────────────────
def _fixed_drives() -> list[str]:
    import ctypes
    drives = []
    try:
        mask = ctypes.windll.kernel32.GetLogicalDrives()
        for i in range(26):
            if mask & (1 << i):
                root = f"{chr(65 + i)}:\\"
                if ctypes.windll.kernel32.GetDriveTypeW(root) == 3:   # DRIVE_FIXED
                    drives.append(root)
    except Exception:
        pass
    return drives


def start_usage_monitor(cfg, announce: Callable[[str], None], logger=None) -> bool:
    """Spawn the background usage watcher. `announce` must be non-interrupting."""
    global _usage_thread
    if not getattr(cfg, "DISK_MONITOR_ENABLED", True):
        return False
    try:
        import psutil  # noqa: F401
    except ImportError:
        if logger:
            logger.warning("disk monitor: psutil missing — disabled.")
        return False
    if _usage_thread is not None and _usage_thread.is_alive():
        return True

    roots = [r.rstrip("\\") + "\\" for r in
             getattr(cfg, "DISK_MONITOR_DRIVES", None) or _fixed_drives()]
    warn = float(getattr(cfg, "DISK_WARN_PERCENT", 85))
    interval = float(getattr(cfg, "DISK_CHECK_INTERVAL", 300))
    # v1.12: ONE combined announcement per WIVEXX session — drives that live
    # above the threshold no longer get re-announced on every restart/check.
    once_per_session = bool(getattr(cfg, "DISK_WARN_ONCE_PER_SESSION", True))
    # re-warn only after dropping back under (warn - hysteresis)
    hysteresis = 3.0
    warned: dict[str, bool] = {}
    announced_this_session = False

    def _loop() -> None:
        nonlocal announced_this_session
        import psutil
        # prime
        time.sleep(10)
        while not _usage_stop.wait(interval):
            over: list[str] = []          # human-readable parts per full drive
            for root in roots:
                try:
                    usage = psutil.disk_usage(root)
                    pct = usage.percent
                except Exception:
                    continue
                name = root.rstrip("\\")
                if pct >= warn:
                    if not warned.get(root):
                        warned[root] = True
                        over.append(f"drive {name} is {pct:.0f} percent full "
                                    f"({usage.free / (1024 ** 3):.0f} gigabytes free)")
                        if logger:
                            logger.info("disk monitor: %s at %.0f%%", name, pct)
                elif pct < warn - hysteresis and warned.get(root):
                    warned[root] = False
            if over and not (once_per_session and announced_this_session):
                try:                          # Settings → disk-space warnings toggle
                    import wivexx_settings as _ws
                    _warn_ok = bool(_ws.get("disk_warnings", True))
                except Exception:
                    _warn_ok = True
                if _warn_ok:
                    announced_this_session = True
                    msg = (f"A heads-up, sir — {' and '.join(over)}. You may want "
                           f"to free up some space — say 'analyze my drives' and "
                           f"I'll find the biggest unused files.")
                    if once_per_session:
                        msg += " I won't mention it again this session."
                    announce(msg)

    _usage_stop.clear()
    _usage_thread = threading.Thread(target=_loop, daemon=True,
                                     name="wivexx-disk-monitor")
    _usage_thread.start()
    if logger:
        logger.info("disk monitor: watching %s (warn at %.0f%%)",
                    ", ".join(roots), warn)
    return True


# ── 2. Large stale-file analytics ──────────────────────────────────────────────
def _should_skip(path: str) -> bool:
    low = path.lower()
    return any(frag in low for frag in _SKIP_FRAGMENTS)


def analyze_drives(cfg, logger=None,
                   drives: Optional[list] = None) -> str:
    """Scan for the biggest user files untouched for 3+ months. Returns a
    spoken summary and writes the full list under <saved_dir>\\Analytics."""
    with _analysis_lock:
        if ANALYSIS["active"]:
            return "I'm already analysing the drives, sir. One moment."
        ANALYSIS.update({"active": True, "scanned": 0, "drive": "", "found": 0,
                         "current": "", "phase": "scanning",
                         "started": time.time(), "summary": ""})

    try:
        roots = [r.rstrip("\\") + "\\" for r in
                 (drives or getattr(cfg, "DISK_MONITOR_DRIVES", None)
                  or _fixed_drives())]
        stale_days = int(getattr(cfg, "DISK_STALE_DAYS", 90))
        min_mb = float(getattr(cfg, "DISK_MIN_FILE_MB", 100))
        top_n = int(getattr(cfg, "DISK_TOP_N", 40))
        cutoff = time.time() - stale_days * 86400
        min_bytes = int(min_mb * 1024 * 1024)

        found: list[tuple] = []   # (size, atime, path)
        scanned = 0
        last_cur = 0.0
        for root in roots:
            with _analysis_lock:
                ANALYSIS["drive"] = root.rstrip("\\")
            for dirpath, dirs, files in os.walk(root):
                now = time.time()
                if now - last_cur >= 0.25:            # live path for the progress card
                    last_cur = now
                    with _analysis_lock:
                        ANALYSIS["current"] = dirpath
                        ANALYSIS["scanned"] = scanned
                if _should_skip(dirpath):
                    dirs[:] = []
                    continue
                dirs[:] = [d for d in dirs
                           if not d.startswith(("$", "."))
                           and not _should_skip(os.path.join(dirpath, d))]
                for fname in files:
                    ext = os.path.splitext(fname)[1].lower()
                    if ext in _SKIP_EXTS:
                        continue
                    full = os.path.join(dirpath, fname)
                    try:
                        st = os.stat(full)
                    except (OSError, ValueError):
                        continue
                    scanned += 1
                    if scanned % 2000 == 0:
                        with _analysis_lock:
                            ANALYSIS["scanned"] = scanned
                    if st.st_size < min_bytes:
                        continue
                    # "last opened" ≈ access time; fall back to mtime if atime
                    # tracking is disabled. Require BOTH old to be safe.
                    last_used = max(st.st_atime, 0)
                    if last_used and last_used > cutoff:
                        continue
                    if st.st_mtime > cutoff:
                        continue   # recently modified — keep
                    found.append((st.st_size, st.st_atime or st.st_mtime, full))
                    with _analysis_lock:
                        ANALYSIS["found"] = len(found)

        found.sort(key=lambda t: t[0], reverse=True)
        found = found[:top_n]

        saved_dir = getattr(cfg, "WEB_SAVED_DIR", "") or cfg.default_saved_dir()
        out_dir = os.path.join(saved_dir, "Analytics")
        os.makedirs(out_dir, exist_ok=True)
        stamp = _dt.datetime.now().strftime("%Y-%m-%d_%H-%M")
        report = os.path.join(out_dir, f"large_stale_files_{stamp}.txt")

        total_gb = sum(s for s, _, _ in found) / (1024 ** 3)
        with open(report, "w", encoding="utf-8") as f:
            f.write("Wivexx — Large files not opened in "
                    f"{stale_days}+ days\n")
            f.write(f"Generated: {_dt.datetime.now():%Y-%m-%d %H:%M}\n")
            f.write(f"Drives scanned: {', '.join(r.rstrip(chr(92)) for r in roots)}\n")
            f.write(f"Files listed: {len(found)}   "
                    f"Reclaimable: {total_gb:.1f} GB\n")
            f.write("Review each path and delete manually if you no longer "
                    "need it. WIVEXX did NOT delete anything.\n")
            f.write("=" * 70 + "\n\n")
            for size, atime, path in found:
                gb = size / (1024 ** 3)
                size_str = (f"{gb:.2f} GB" if gb >= 1
                            else f"{size / (1024**2):.0f} MB")
                last = (_dt.datetime.fromtimestamp(atime).strftime("%Y-%m-%d")
                        if atime else "unknown")
                f.write(f"[{size_str:>9}]  last opened {last}\n    {path}\n\n")

        if logger:
            logger.info("disk analysis: %d files, %.1f GB → %s",
                        len(found), total_gb, report)

        # Structured list for the in-app cleanup tab (newest scan wins).
        files_out = []
        for size, atime, path in found:
            gb = size / (1024 ** 3)
            size_str = (f"{gb:.2f} GB" if gb >= 1
                        else f"{size / (1024**2):.0f} MB")
            last = (_dt.datetime.fromtimestamp(atime).strftime("%Y-%m-%d")
                    if atime else "unknown")
            files_out.append({"path": path, "name": os.path.basename(path),
                              "folder": os.path.dirname(path), "size": size,
                              "size_str": size_str, "last": last})

        with _analysis_lock:
            ANALYSIS["phase"] = "done"
            ANALYSIS["found"] = len(found)
            ANALYSIS["summary"] = (
                f"{len(found)} large files · {total_gb:.0f} GB reclaimable"
                if found else "no large stale files found")
            LAST_ANALYSIS.update({
                "files": files_out, "total_gb": round(total_gb, 2),
                "generated": _dt.datetime.now().strftime("%Y-%m-%d %H:%M"),
                "drives": ", ".join(r.rstrip("\\") for r in roots),
                "report": report})

        if not found:
            return (f"I scanned your drives, sir, and found no large user files "
                    f"left untouched for {stale_days // 30} months. Nicely kept.")
        return (f"Analysis complete, sir. I found {len(found)} large files you "
                f"haven't opened in over {stale_days // 30} months, totalling "
                f"{total_gb:.1f} gigabytes. I've opened the full list in the drive "
                f"cleanup tab — check the notification — where you can open each "
                f"file's folder or send it to the Recycle Bin.")
    except Exception as e:
        if logger:
            logger.error("disk analysis failed: %s", e)
        return "The drive analysis ran into a problem, sir. Check the log."
    finally:
        with _analysis_lock:
            ANALYSIS.update({"active": False})


def analysis_status() -> dict:
    with _analysis_lock:
        return dict(ANALYSIS)


def analysis_transfer_status() -> dict:
    """Drive-scan progress in the transfer-card shape, so the same delivery-style
    progress bar + notification surface can render it (kind='scan')."""
    with _analysis_lock:
        a = dict(ANALYSIS)
    active = bool(a.get("active"))
    phase = "scanning" if active else ("done" if a.get("phase") == "done" else "idle")
    started = a.get("started") or 0.0
    label = "analysing drives" + (f" · {a['drive']}" if a.get("drive") else "")
    return {"kind": "scan", "active": active, "phase": phase, "label": label,
            "current": a.get("current", ""), "scanned": int(a.get("scanned", 0)),
            "found": int(a.get("found", 0)), "started": started,
            "elapsed": int(time.time() - started) if started else 0,
            "percent": 100 if phase == "done" else 0,
            "summary": a.get("summary", "")}


def last_results() -> dict:
    """The structured file list from the most recent completed scan, for the
    in-app 'drive cleanup' tab."""
    with _analysis_lock:
        r = dict(LAST_ANALYSIS)
        r["files"] = [dict(f) for f in LAST_ANALYSIS["files"]]
    return r


def move_to_recycle(paths) -> dict:
    """Send the given files to the Windows Recycle Bin — RECOVERABLE, never a
    permanent delete. Returns {deleted, failed, errors}. Also drops the removed
    paths from LAST_ANALYSIS so the cleanup tab stays in sync.
    Implemented with SHFileOperationW (no extra dependency)."""
    res = {"deleted": 0, "failed": 0, "errors": []}
    if isinstance(paths, str):
        paths = [paths]
    valid = []
    for p in (paths or []):
        try:
            if p and os.path.isfile(p):
                valid.append(os.path.normpath(os.path.abspath(p)))
        except Exception:
            pass
    if not valid:
        return res

    import ctypes
    from ctypes import wintypes

    FO_DELETE = 3
    FOF_ALLOWUNDO = 0x0040       # → Recycle Bin, not permanent
    FOF_NOCONFIRMATION = 0x0010
    FOF_SILENT = 0x0004
    FOF_NOERRORUI = 0x0400

    class SHFILEOPSTRUCTW(ctypes.Structure):
        _fields_ = [("hwnd", wintypes.HWND),
                    ("wFunc", wintypes.UINT),
                    ("pFrom", wintypes.LPCWSTR),
                    ("pTo", wintypes.LPCWSTR),
                    ("fFlags", ctypes.c_uint16),
                    ("fAnyOperationsAborted", wintypes.BOOL),
                    ("hNameMappings", ctypes.c_void_p),
                    ("lpszProgressTitle", wintypes.LPCWSTR)]

    shell32 = ctypes.windll.shell32
    shell32.SHFileOperationW.argtypes = [ctypes.POINTER(SHFILEOPSTRUCTW)]
    shell32.SHFileOperationW.restype = ctypes.c_int

    op = SHFILEOPSTRUCTW()
    op.hwnd = None
    op.wFunc = FO_DELETE
    # pFrom must be a double-null-terminated list of paths.
    op.pFrom = "\0".join(valid) + "\0\0"
    op.pTo = None
    op.fFlags = (FOF_ALLOWUNDO | FOF_NOCONFIRMATION
                 | FOF_SILENT | FOF_NOERRORUI)
    try:
        rc = shell32.SHFileOperationW(ctypes.byref(op))
    except Exception as e:
        res["failed"] = len(valid)
        res["errors"].append(str(e))
        return res
    if rc != 0:
        res["errors"].append(f"SHFileOperation returned {rc}")

    # Count by checking what actually went away, and prune LAST_ANALYSIS.
    gone = set()
    for p in valid:
        if not os.path.exists(p):
            gone.add(p)
            res["deleted"] += 1
        else:
            res["failed"] += 1
    if gone:
        with _analysis_lock:
            LAST_ANALYSIS["files"] = [
                f for f in LAST_ANALYSIS["files"]
                if os.path.normpath(os.path.abspath(f.get("path", ""))) not in gone]
            LAST_ANALYSIS["total_gb"] = round(
                sum(f.get("size", 0) for f in LAST_ANALYSIS["files"]) / (1024 ** 3), 2)
    return res
