"""
wivexx_workmode.py — photographer's work mode  (v1.11)
═══════════════════════════════════════════════════════════════════════
Two helpers for the delivery workflow:

1. ZIP auto-extract — while work mode is ON, every .zip that finishes
   downloading into the Downloads folder is unpacked into a folder named
   after the archive (next to it).  A download is considered finished when
   its size stays stable across two polls and no .crdownload/.part twin
   exists.  Toggle by voice: "enable work mode" / "disable work mode".

2. import_latest_to_lightroom() — opens the NEWEST folder in Downloads with
   Adobe Lightroom Classic (LrC shows its import dialog pointed at that
   source).  Bound to the rebindable `lightroom_import` shortcut.

Public API:
    start(cfg, announce, logger) -> bool      # starts the watcher thread
    set_enabled(on) / is_enabled()
    import_latest_to_lightroom(cfg, logger) -> str   # spoken result
"""

import os
import subprocess
import threading
import time
import zipfile
from typing import Callable, Optional

_enabled = False
_started = False
_logger = None
_announce: Optional[Callable[[str], None]] = None
_downloads = ""
_scan_now = threading.Event()        # set to make the watcher scan immediately

# partial-download twins various browsers / managers leave next to the real file
_TEMP_EXTS = (".crdownload", ".part", ".partial", ".download", ".opdownload",
              ".tmp", ".!ut", ".aria2")

_LIGHTROOM_PATHS = [
    r"%ProgramFiles%\Adobe\Adobe Lightroom Classic\Lightroom.exe",
    r"%ProgramFiles%\Adobe\Lightroom Classic\Lightroom.exe",
]


def is_enabled() -> bool:
    return _enabled


def set_enabled(on: bool) -> None:
    global _enabled
    _enabled = bool(on)
    if _enabled:
        _scan_now.set()          # extract anything already waiting in Downloads, now
    if _logger:
        _logger.info("work mode: %s", "ON" if _enabled else "OFF")


def _downloads_dir(cfg) -> str:
    return os.path.expandvars(os.path.expanduser(
        str(getattr(cfg, "WORK_DOWNLOADS_DIR", r"~\Downloads"))))


# ── ZIP auto-extract watcher ───────────────────────────────────────────────────
def _already_extracted(zip_path: str) -> bool:
    dest = os.path.join(os.path.dirname(zip_path),
                        os.path.splitext(os.path.basename(zip_path))[0])
    return os.path.isdir(dest) and bool(os.listdir(dest))


def _is_downloading(zip_path: str) -> bool:
    """True while a partial-download twin still sits next to the archive."""
    return any(os.path.exists(zip_path + ext) for ext in _TEMP_EXTS)


def _extract_zip(zip_path: str) -> Optional[str]:
    """Unpack *zip_path* into a sibling folder named after it. Returns the dest
    folder on success, or None if it was already extracted. RAISES on a real
    failure (e.g. a still-incomplete archive) so the watcher can retry it."""
    stem = os.path.splitext(os.path.basename(zip_path))[0]
    dest = os.path.join(os.path.dirname(zip_path), stem)
    if os.path.isdir(dest) and os.listdir(dest):
        return None                       # already extracted earlier
    n = 1
    while os.path.exists(dest) and os.listdir(dest):
        dest = os.path.join(os.path.dirname(zip_path), f"{stem}_{n}")
        n += 1
    with zipfile.ZipFile(zip_path) as zf:
        zf.extractall(dest)
    return dest


def _watcher() -> None:
    seen_sizes: dict = {}        # path → last size (stability check)
    done: set = set()           # successfully handled (extracted or already-unpacked)
    fails: dict = {}            # path → consecutive failed extracts
    while True:
        # wake on a 4 s poll OR immediately when work mode is switched on
        _scan_now.wait(timeout=4.0)
        _scan_now.clear()
        if not _enabled:
            seen_sizes.clear()
            continue
        try:
            for fname in os.listdir(_downloads):
                if not fname.lower().endswith(".zip"):
                    continue
                full = os.path.join(_downloads, fname)
                if full in done:
                    continue
                if _already_extracted(full):
                    done.add(full)        # its folder exists — nothing to do
                    continue
                try:
                    st = os.stat(full)
                except OSError:
                    continue
                if _is_downloading(full):  # a partial-download twin is present
                    seen_sizes[full] = st.st_size
                    continue
                last = seen_sizes.get(full)
                seen_sizes[full] = st.st_size
                # "complete" = settled on disk for >2 s, OR a stable size across polls
                settled = (time.time() - st.st_mtime) > 2.0
                if not settled and last != st.st_size:
                    continue              # still being written — wait
                try:
                    dest = _extract_zip(full)
                except (zipfile.BadZipFile, OSError) as e:
                    fails[full] = fails.get(full, 0) + 1
                    if fails[full] >= 4:  # give up after a few tries (truly corrupt)
                        done.add(full)
                        if _logger:
                            _logger.warning("work mode: gave up on %s (%s)", fname, e)
                    continue
                done.add(full)
                if dest:
                    if _logger:
                        _logger.info("work mode: extracted %s → %s", fname, dest)
                    if _announce:
                        _announce(f"Work mode, sir — I've extracted "
                                  f"{os.path.basename(fname)} into its folder.")
        except Exception as e:
            if _logger:
                _logger.debug("work mode watcher: %s", e)


def start(cfg, announce: Optional[Callable[[str], None]] = None,
          logger=None) -> bool:
    global _started, _logger, _announce, _downloads, _enabled
    _logger = logger
    _announce = announce
    _downloads = _downloads_dir(cfg)
    _enabled = bool(getattr(cfg, "WORK_MODE_DEFAULT", False))
    if _started:
        return True
    if not os.path.isdir(_downloads):
        if logger:
            logger.warning("work mode: downloads dir missing: %s", _downloads)
        return False
    _started = True
    threading.Thread(target=_watcher, daemon=True,
                     name="wivexx-workmode").start()
    if logger:
        logger.info("work mode: watcher ready on %s (currently %s)",
                    _downloads, "ON" if _enabled else "OFF")
    return True


# ── Lightroom import shortcut ──────────────────────────────────────────────────
def _find_lightroom() -> Optional[str]:
    for tpl in _LIGHTROOM_PATHS:
        p = os.path.expandvars(tpl)
        if os.path.exists(p):
            return p
    return None


def latest_download_folder(cfg) -> Optional[str]:
    root = _downloads_dir(cfg)
    try:
        dirs = [os.path.join(root, d) for d in os.listdir(root)
                if os.path.isdir(os.path.join(root, d))
                and not d.startswith(("$", "."))]
        if not dirs:
            return None
        return max(dirs, key=lambda p: os.path.getmtime(p))
    except OSError:
        return None


def import_latest_to_lightroom(cfg, logger=None) -> str:
    folder = latest_download_folder(cfg)
    if folder is None:
        return "There is no folder in Downloads to import, sir."
    exe = _find_lightroom()
    try:
        if exe:
            subprocess.Popen([exe, folder],
                             creationflags=subprocess.DETACHED_PROCESS
                             | subprocess.CREATE_NEW_PROCESS_GROUP
                             | subprocess.CREATE_NO_WINDOW,
                             close_fds=True, stdout=subprocess.DEVNULL,
                             stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
        else:
            # fall back to the shell association for lightroom files/folder
            subprocess.Popen(["cmd", "/c", "start", "lightroom", folder],
                             creationflags=subprocess.CREATE_NO_WINDOW,
                             close_fds=True, stdout=subprocess.DEVNULL,
                             stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
        if logger:
            logger.info("work mode: sent %s to Lightroom Classic", folder)
        return (f"Importing {os.path.basename(folder)} into Lightroom "
                f"Classic, sir.")
    except Exception as e:
        if logger:
            logger.error("work mode: lightroom import failed: %s", e)
        return "I couldn't launch Lightroom Classic, sir."
