#!/usr/bin/env python3
"""
MeDBia Disc Scanner — macOS client
===================================
Polls the optical drive, inspects disc contents, and submits records
to the remote videoDB API:

  • Video discs  → one record per disc, listing video filenames
  • Photo discs  → one record per gallery folder, listing photo filenames
  • Mixed discs  → both: a video record + one record per photo folder

Setup:
    pip3 install requests
    export VIDEODB_URL=http://your-server:6761
    export VIDEODB_TOKEN=change_this_secret_token
    python3 scan_disc.py
"""

import os
import re
import sys
import time
import subprocess
from collections import defaultdict
from pathlib import Path

try:
    import requests
except ImportError:
    sys.exit("Install requests first:  pip3 install requests")

# ── Config ─────────────────────────────────────────────────────────────────────
API_URL   = os.environ.get("VIDEODB_URL",   "http://your-server:6761").rstrip("/") + "/api_ingest.php"
API_TOKEN = os.environ.get("VIDEODB_TOKEN", "change_this_secret_token")
POLL_SEC  = int(os.environ.get("POLL_INTERVAL", "5"))

# videoDB mediatype IDs (from install.sql)
MT_DVD    = 1
MT_BLURAY = 16
MT_CD     = 18

VIDEO_EXT = {
    ".mp4", ".mkv", ".avi", ".mov", ".m4v", ".ts", ".m2ts", ".mts",
    ".wmv", ".flv", ".webm", ".vob", ".mpg", ".mpeg", ".iso", ".divx",
}

PHOTO_EXT = {
    ".jpg", ".jpeg", ".png", ".gif", ".tiff", ".tif", ".bmp",
    ".heic", ".heif", ".webp",
    ".raw", ".cr2", ".cr3", ".nef", ".arw", ".dng", ".orf", ".rw2", ".raf",
}

IGNORE_VOLUMES = {"Macintosh HD", "Preboot", "Recovery", "VM", "Data", "Update"}


# ── Shell helper ───────────────────────────────────────────────────────────────

def run(cmd: list) -> str:
    try:
        r = subprocess.run(cmd, capture_output=True, text=True, timeout=20)
        return r.stdout
    except Exception:
        return ""


# ── Disc presence ──────────────────────────────────────────────────────────────

def disc_status() -> dict | None:
    out = run(["drutil", "status"])
    if not out or "No Media" in out:
        return None

    info: dict = {}
    m = re.search(r"Type:\s+(.+?)(?:\s{3,}|$)", out, re.MULTILINE)
    if m:
        info["drutil_type"] = m.group(1).strip()
    m = re.search(r"Name:\s+(/dev/\S+)", out)
    if m:
        info["device"] = m.group(1)
    m = re.search(r"Space Used:\s+([\d.]+)\s*GB", out)
    if m:
        info["used_gb"] = float(m.group(1))
    return info


# ── Mount ──────────────────────────────────────────────────────────────────────

def find_mount(device: str) -> str | None:
    for line in run(["mount"]).splitlines():
        if device in line:
            m = re.search(r" on (/Volumes/[^\s(]+)", line)
            if m:
                return m.group(1)
    try:
        volumes = set(os.listdir("/Volumes/")) - IGNORE_VOLUMES
        if volumes:
            return f"/Volumes/{sorted(volumes)[0]}"
    except Exception:
        pass
    return None


# ── Media type inference ───────────────────────────────────────────────────────

def mediatype_from_drutil(drutil_type: str) -> tuple[int, str] | None:
    t = drutil_type.upper()
    if "BD" in t:
        return MT_BLURAY, "Blu-ray"
    if "DVD" in t:
        return MT_DVD, "DVD"
    if "CD" in t:
        return MT_CD, "CD"
    return None

def mediatype_from_size(used_gb: float) -> tuple[int, str]:
    if used_gb > 8.0:
        return MT_BLURAY, "Blu-ray"
    if used_gb > 0.68:
        return MT_DVD, "DVD"
    return MT_CD, "CD"


# ── Disc inventory ─────────────────────────────────────────────────────────────

def inventory_disc(mount: str) -> dict:
    """
    Walk the disc and return:
      video_files: list of relative paths to video files
      photo_folders: dict of { relative_folder_path -> [photo filenames] }
    """
    video_files: list[str] = []
    # folder path (relative to mount) -> list of photo filenames in that folder
    photo_folders: dict[str, list[str]] = defaultdict(list)

    try:
        for root, _dirs, files in os.walk(mount):
            for fname in files:
                ext = Path(fname).suffix.lower()
                rel_path = os.path.relpath(os.path.join(root, fname), mount)
                rel_folder = os.path.relpath(root, mount)
                if rel_folder == ".":
                    rel_folder = "(root)"

                if ext in VIDEO_EXT:
                    video_files.append(rel_path)
                elif ext in PHOTO_EXT:
                    photo_folders[rel_folder].append(fname)
    except PermissionError:
        pass

    return {
        "video_files":   sorted(video_files),
        "photo_folders": dict(photo_folders),
    }


def disc_size_bytes(mount: str) -> int:
    out = run(["df", "-k", mount])
    for line in out.splitlines()[1:]:
        parts = line.split()
        if len(parts) >= 3:
            try:
                return int(parts[2]) * 1024
            except ValueError:
                pass
    return 0


def volume_label(mount: str | None, device: str) -> str:
    if mount:
        label = os.path.basename(mount)
        if label:
            return label
    if device:
        for line in run(["diskutil", "info", device]).splitlines():
            if "Volume Name" in line:
                return line.split(":", 1)[-1].strip()
    return "Unknown Disc"


# ── Eject ──────────────────────────────────────────────────────────────────────

def eject(mount: str | None):
    if mount:
        r = subprocess.run(["diskutil", "eject", mount], capture_output=True)
        if r.returncode == 0:
            return
    run(["drutil", "eject"])


# ── API submission ─────────────────────────────────────────────────────────────

def submit(payload: dict) -> bool:
    try:
        resp = requests.post(
            API_URL,
            json=payload,
            headers={"X-API-Token": API_TOKEN},
            timeout=15,
        )
        if resp.status_code == 200:
            data = resp.json()
            print(f"    [OK]  Entry #{data.get('id', '?')}: {payload['title']}")
            return True
        else:
            print(f"    [ERR] API {resp.status_code}: {resp.text[:300]}")
            return False
    except requests.ConnectionError:
        print(f"    [ERR] Cannot reach {API_URL}")
        return False
    except Exception as e:
        print(f"    [ERR] {e}")
        return False


# ── Record builders ────────────────────────────────────────────────────────────

def build_video_record(disc_label: str, disklabel: str, video_files: list[str],
                       mediatype_id: int, drutil_type: str, size_b: int) -> dict:
    names = [Path(f).name for f in video_files]
    comment = f"{len(names)} video file{'s' if len(names) != 1 else ''}"
    plot = "\n".join(names)   # full list in the TEXT field — no length limit
    # Short preview for comment (VARCHAR 255)
    preview = ", ".join(names[:8])
    if len(names) > 8:
        preview += f" … +{len(names) - 8} more"
    comment = f"{len(names)} video files: {preview}"

    return {
        "title":     disc_label,
        "subtitle":  "",
        "mediatype": mediatype_id,
        "comment":   comment[:255],
        "plot":      plot,
        "filesize":  size_b,
        "disklabel": disklabel,
        "custom1":   drutil_type,
        "custom2":   str(len(names)),
        "custom3":   "video",
    }


def build_photo_records(disc_label: str, disklabel: str, photo_folders: dict[str, list[str]],
                        mediatype_id: int, drutil_type: str, size_b: int) -> list[dict]:
    """One record per gallery folder."""
    records = []
    for folder, photos in photo_folders.items():
        photos_sorted = sorted(photos)
        count = len(photos_sorted)

        # Title: disc label + folder name (skip "(root)" clutter if only one folder)
        if folder == "(root)" and len(photo_folders) == 1:
            title = disc_label
        else:
            folder_display = folder.replace("/", " / ")
            title = f"{disc_label} — {folder_display}"

        # Comment: short summary
        preview = ", ".join(photos_sorted[:6])
        if count > 6:
            preview += f" … +{count - 6} more"
        comment = f"{count} photo{'s' if count != 1 else ''}: {preview}"

        # Plot: full filename list — searchable via videoDB full-text search
        plot = "\n".join(photos_sorted)

        records.append({
            "title":     title[:255],
            "subtitle":  folder if folder != "(root)" else "",
            "mediatype": mediatype_id,
            "comment":   comment[:255],
            "plot":      plot,
            "filesize":  0,            # folder-level size not easily available
            "disklabel": disklabel,
            "custom1":   drutil_type,
            "custom2":   str(count),
            "custom3":   "photo",
        })
    return records


# ── Main scan routine ──────────────────────────────────────────────────────────

def scan_and_submit():
    status = disc_status()
    if not status:
        return False

    drutil_type = status.get("drutil_type", "Unknown")
    device      = status.get("device", "")
    used_gb     = status.get("used_gb", 0.0)

    print(f"  drutil type : {drutil_type}")
    print(f"  device      : {device}")
    print(f"  used        : {used_gb:.2f} GB")

    time.sleep(4)   # let macOS finish mounting

    mount = find_mount(device)
    print(f"  mount       : {mount or '(not mounted)'}")

    mt = mediatype_from_drutil(drutil_type) or mediatype_from_size(used_gb)
    mediatype_id, mediatype_name = mt
    disc_label = volume_label(mount, device)
    disklabel  = disc_label[:32]

    if not mount:
        print("  [WARN] Disc not mounted — submitting with disc label only")
        submit({
            "title":     disc_label,
            "subtitle":  "",
            "mediatype": mediatype_id,
            "comment":   f"Disc not mounted ({drutil_type})",
            "plot":      "",
            "filesize":  int(used_gb * 1024**3),
            "disklabel": disklabel,
            "custom1":   drutil_type,
            "custom2":   "0",
            "custom3":   "unknown",
        })
        eject(None)
        return True

    size_b = disc_size_bytes(mount)
    print(f"  Scanning contents...")
    inv = inventory_disc(mount)

    video_files   = inv["video_files"]
    photo_folders = inv["photo_folders"]

    n_videos = len(video_files)
    n_photos = sum(len(v) for v in photo_folders.values())
    n_galleries = len(photo_folders)

    print(f"  Found: {n_videos} video files, {n_photos} photos in {n_galleries} folder(s)")

    records = []

    if video_files:
        records.append(build_video_record(
            disc_label, disklabel, video_files, mediatype_id, drutil_type, size_b
        ))

    if photo_folders:
        records += build_photo_records(
            disc_label, disklabel, photo_folders, mediatype_id, drutil_type, size_b
        )

    if not records:
        # Disc has neither video nor photo files — index with raw file count
        total = sum(
            len(files)
            for _, _, files in os.walk(mount)
        )
        records.append({
            "title":     disc_label,
            "subtitle":  "",
            "mediatype": mediatype_id,
            "comment":   f"{total} files (no video or photo files detected)",
            "plot":      "",
            "filesize":  size_b,
            "disklabel": disklabel,
            "custom1":   drutil_type,
            "custom2":   str(total),
            "custom3":   "data",
        })

    print(f"\n  Submitting {len(records)} record(s)...")
    ok_count = 0
    for rec in records:
        if submit(rec):
            ok_count += 1

    print(f"  {ok_count}/{len(records)} records submitted.")
    print("  Ejecting disc...")
    eject(mount)
    return ok_count > 0


# ── Entry point ────────────────────────────────────────────────────────────────

def main():
    print("=" * 55)
    print("  MeDBia Disc Scanner")
    print(f"  API  : {API_URL}")
    print(f"  Poll : every {POLL_SEC}s")
    print("=" * 55)
    print("Insert a disc to index it. Ctrl-C to stop.\n")

    was_present = False

    while True:
        status = disc_status()

        if status and not was_present:
            was_present = True
            print("Disc detected!")
            scan_and_submit()
            was_present = False
            print("\nReady — insert next disc.\n")

        elif not status and was_present:
            was_present = False

        time.sleep(POLL_SEC)


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\nStopped.")
