"""One-button forensic artifact wipe.

Surfaces purged:

  • %TEMP%                                       — per-user temp
  • %SystemRoot%\\Prefetch\\*.pf (Admin only)    — file-launch history
  • %APPDATA%\\Microsoft\\Windows\\Recent\\*     — recent files
  • %APPDATA%\\Microsoft\\Windows\\Recent\\AutomaticDestinations\\*  — Jump Lists
  • %APPDATA%\\Microsoft\\Windows\\Recent\\CustomDestinations\\*     — Jump Lists
  • HKCU\\…\\Explorer\\RunMRU                    — Win+R history
  • HKCU\\…\\Explorer\\TypedPaths                — Explorer typed paths
  • HKCU\\…\\Explorer\\WordWheelQuery            — Start / Explorer search history
  • HKCU\\…\\Explorer\\RecentDocs                — recent docs MRU
  • Clipboard                                    — current contents

Counts are reported but specific filenames are never logged (this is the
opposite of what we want to leak). All deletes use ``ignore_errors=True``
because files in use by other apps are expected.
"""
from __future__ import annotations

import ctypes
import logging
import os
import shutil
import subprocess
import winreg
from dataclasses import dataclass, field
from pathlib import Path

from .firewall import is_admin

log = logging.getLogger(__name__)


_REG_MRU_KEYS = (
    r"Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU",
    r"Software\Microsoft\Windows\CurrentVersion\Explorer\TypedPaths",
    r"Software\Microsoft\Windows\CurrentVersion\Explorer\WordWheelQuery",
    r"Software\Microsoft\Windows\CurrentVersion\Explorer\RecentDocs",
)


@dataclass
class WipeReport:
    files_deleted: int = 0
    bytes_freed: int = 0
    folders_skipped: int = 0
    registry_keys_cleared: int = 0
    clipboard_cleared: bool = False
    errors: list[str] = field(default_factory=list)

    def summary(self) -> str:
        mb = self.bytes_freed / (1024 * 1024)
        return (
            f"{self.files_deleted} files / {mb:.1f} MB freed, "
            f"{self.registry_keys_cleared} MRU entries cleared, "
            f"clipboard={'yes' if self.clipboard_cleared else 'no'}, "
            f"errors={len(self.errors)}"
        )


def _walk_size(path: Path) -> int:
    total = 0
    for p in path.rglob("*"):
        try:
            if p.is_file():
                total += p.stat().st_size
        except OSError:
            pass
    return total


def _purge_dir(path: Path, rep: WipeReport, keep_root: bool = True) -> None:
    """Delete contents of `path`. Preserves the directory itself when keep_root."""
    if not path.exists():
        return
    try:
        size_before = _walk_size(path)
    except Exception:
        size_before = 0

    count = 0
    for child in path.iterdir():
        try:
            if child.is_dir():
                shutil.rmtree(child, ignore_errors=True)
            else:
                child.unlink(missing_ok=True)
            count += 1
        except OSError:
            rep.folders_skipped += 1

    if not keep_root:
        try:
            path.rmdir()
        except OSError:
            pass

    rep.files_deleted += count
    try:
        size_after = _walk_size(path)
    except Exception:
        size_after = 0
    rep.bytes_freed += max(0, size_before - size_after)


def _clear_mru_key(path: str, rep: WipeReport) -> None:
    """Remove every value under a Run/Typed/Search MRU key."""
    try:
        with winreg.OpenKey(
            winreg.HKEY_CURRENT_USER, path, 0, winreg.KEY_ALL_ACCESS
        ) as key:
            count = 0
            try:
                while True:
                    name, _, _ = winreg.EnumValue(key, 0)
                    try:
                        winreg.DeleteValue(key, name)
                        count += 1
                    except OSError:
                        break
            except OSError:
                pass
            # Recurse into subkeys (e.g. RecentDocs/.png)
            sub_count = 0
            try:
                while True:
                    sub_count += 1
                    sub_name = winreg.EnumKey(key, 0)
                    try:
                        winreg.DeleteKey(key, sub_name)
                    except OSError:
                        break
                    if sub_count > 200:  # safety cap
                        break
            except OSError:
                pass
            if count or sub_count:
                rep.registry_keys_cleared += 1
    except OSError:
        pass


def _clear_clipboard(rep: WipeReport) -> None:
    try:
        user32 = ctypes.windll.user32  # type: ignore[attr-defined]
        if user32.OpenClipboard(None):
            try:
                user32.EmptyClipboard()
                rep.clipboard_cleared = True
            finally:
                user32.CloseClipboard()
    except Exception as e:
        rep.errors.append(f"clipboard: {e}")


def wipe_artifacts(include_prefetch: bool = True) -> WipeReport:
    rep = WipeReport()
    appdata = Path(os.environ.get("APPDATA", "")) if os.environ.get("APPDATA") else None
    temp = Path(os.environ.get("TEMP", "")) if os.environ.get("TEMP") else None
    sysroot = Path(os.environ.get("SystemRoot", r"C:\Windows"))

    # %TEMP%
    if temp and temp.exists():
        _purge_dir(temp, rep, keep_root=True)

    # Prefetch (Admin)
    if include_prefetch and is_admin():
        pf = sysroot / "Prefetch"
        if pf.exists():
            count = 0
            for f in pf.glob("*.pf"):
                try:
                    sz = f.stat().st_size
                    f.unlink(missing_ok=True)
                    count += 1
                    rep.bytes_freed += sz
                except OSError as e:
                    rep.errors.append(f"prefetch: {e}")
            rep.files_deleted += count

    # Recent / Jump Lists
    if appdata:
        recent = appdata / "Microsoft" / "Windows" / "Recent"
        if recent.exists():
            _purge_dir(recent / "AutomaticDestinations", rep, keep_root=True)
            _purge_dir(recent / "CustomDestinations", rep, keep_root=True)
            _purge_dir(recent, rep, keep_root=True)

    # MRU registry keys
    for path in _REG_MRU_KEYS:
        _clear_mru_key(path, rep)

    # Clipboard
    _clear_clipboard(rep)

    # Trigger Explorer's "Clear recent items" via Shell API (covers Win10/11
    # Quick Access pinned ↔ recent lists not covered by raw file delete).
    try:
        shell32 = ctypes.windll.shell32  # type: ignore[attr-defined]
        # SHCNE_ASSOCCHANGED tells Explorer to refresh its caches.
        shell32.SHChangeNotify(0x08000000, 0x0000, None, None)
    except Exception:
        pass

    # Best-effort Defender quick-scan history flush — non-fatal.
    try:
        subprocess.run(
            ["powershell", "-NoProfile", "-NonInteractive", "-Command",
             "Clear-RecycleBin -Force -ErrorAction SilentlyContinue"],
            capture_output=True, text=True, timeout=20,
            creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
        )
    except Exception:
        pass

    return rep
