#!/usr/bin/env python3
"""
scanner_bridge.py
-----------------
Lightweight HTTP bridge between the browser and a USB scanner.
Runs locally on the user's machine. Exposes:
  GET  /health      → health check
  GET  /scanners    → list available scanners
  POST /scan        → trigger a scan, return base64 JPEG

OS support:
  macOS   → osascript + Image Capture (built-in, zero install needed)
  Linux   → scanimage via SANE  (sudo apt install sane-utils)
  Windows → PowerShell WIA COM  (built-in, zero install needed)

Python requirements (all platforms):
  pip install flask flask-cors Pillow

macOS permission note:
  The first time you run this, macOS may ask to allow Terminal (or your
  Python interpreter) to control "Image Capture" via Automation.
  Grant it in: System Settings → Privacy & Security → Automation.

Run:
  python scanner_bridge.py      # http://localhost:5555
"""

import base64
import io
import os
import platform
import subprocess
import tempfile

from flask import Flask, jsonify, request
from flask_cors import CORS
from PIL import Image

app = Flask(__name__)

# Restrict to your Laravel origins. Tighten in production.
CORS(app, origins=[
    "http://localhost:8000",
    "http://127.0.0.1:8000",
    "https://your-laravel-app.com",
])

OS   = platform.system()   # 'Darwin' | 'Linux' | 'Windows'
PORT = 5555


# ─────────────────────────────────────────────────────────────
#  SCANNER DISCOVERY
# ─────────────────────────────────────────────────────────────

def get_scanners_mac():
    """
    List scanners via osascript → Image Capture (ICA framework).
    macOS uses ICA, NOT SANE — no brew install required.
    """
    script = '''
    tell application "Image Capture"
        set out to {}
        repeat with d in devices
            try
                set end of out to (name of d as string) & "|" & (id of d as string)
            end try
        end repeat
        return out
    end tell
    '''
    try:
        result = subprocess.run(
            ["osascript", "-e", script],
            capture_output=True, text=True, timeout=15
        )
        devices = []
        raw = result.stdout.strip()
        # osascript returns comma-separated list items on one line
        for token in raw.split(","):
            token = token.strip()
            if "|" in token:
                label, did = token.split("|", 1)
                devices.append({"id": did.strip(), "label": label.strip()})
            elif token:
                devices.append({"id": token, "label": token})
        return devices
    except Exception:
        return []


def get_scanners_linux():
    """List scanners via SANE / scanimage (Linux)."""
    try:
        result = subprocess.run(
            ["scanimage", "--list-devices"],
            capture_output=True, text=True, timeout=15
        )
        devices = []
        for line in result.stdout.strip().splitlines():
            if "device" in line.lower() and "`" in line:
                # device `epson2:libusb:001:004' is a Epson Perfection V39
                dev_id = line.split("`")[1].split("'")[0]
                label  = line.split("'", 2)[-1].strip().lstrip("is a ").strip()
                devices.append({"id": dev_id, "label": label or dev_id})
        return devices
    except FileNotFoundError:
        return []


def get_scanners_windows():
    """List scanners via PowerShell WIA COM (Windows — no install needed)."""
    ps = """
    $wia = New-Object -ComObject WIA.DeviceManager
    foreach ($info in $wia.DeviceInfos) {
        Write-Output ($info.DeviceID + '|' + $info.Properties['Name'].Value)
    }
    """
    try:
        result = subprocess.run(
            ["powershell", "-NoProfile", "-Command", ps],
            capture_output=True, text=True, timeout=15
        )
        devices = []
        for line in result.stdout.strip().splitlines():
            if "|" in line:
                did, name = line.split("|", 1)
                devices.append({"id": did.strip(), "label": name.strip()})
        return devices
    except Exception:
        return []


def list_scanners():
    if OS == "Darwin":
        return get_scanners_mac()
    if OS == "Windows":
        return get_scanners_windows()
    return get_scanners_linux()


# ─────────────────────────────────────────────────────────────
#  SCANNING
# ─────────────────────────────────────────────────────────────

def scan_mac(device_id, resolution=200):
    """
    Scan on macOS using osascript → Image Capture.
    Saves whatever format ICA produces (TIFF/PDF) then converts to JPEG
    via Pillow — no extra tools needed.
    """
    out_dir = tempfile.mkdtemp()

    # ICA colorMode values: 0=BW, 1=Grayscale, 2=Color (RGB)
    script = f'''
    set outFolder to POSIX file "{out_dir}" as alias
    tell application "Image Capture"
        set targetDevice to missing value
        repeat with d in devices
            try
                if (name of d as string) is "{device_id}" or (id of d as string) is "{device_id}" then
                    set targetDevice to d
                    exit repeat
                end if
            end try
        end repeat
        if targetDevice is missing value then
            if (count of devices) > 0 then
                set targetDevice to item 1 of devices
            else
                error "No scanner found"
            end if
        end if
        try
            set documentations of targetDevice to {{resolution:{resolution}, colorMode:2}}
        end try
        scan targetDevice to outFolder
    end tell
    '''

    result = subprocess.run(
        ["osascript", "-e", script],
        capture_output=True, text=True, timeout=120
    )

    if result.returncode != 0:
        err = result.stderr.strip() or "osascript scan failed"
        raise RuntimeError(err)

    # Find the file Image Capture wrote (TIFF, PNG, PDF, JPG, ...)
    files = sorted(
        [f for f in os.listdir(out_dir) if not f.startswith(".")],
        key=lambda f: os.path.getmtime(os.path.join(out_dir, f)),
        reverse=True,
    )
    if not files:
        raise RuntimeError("Scan completed but no output file was found.")

    raw_path    = os.path.join(out_dir, files[0])
    pil_img     = Image.open(raw_path).convert("RGB")
    buf         = io.BytesIO()
    pil_img.save(buf, format="JPEG", quality=92, optimize=True)
    image_bytes = buf.getvalue()

    os.unlink(raw_path)
    os.rmdir(out_dir)

    return image_bytes, "image/jpeg"


def scan_linux(device_id, resolution=200, mode="Color"):
    """Scan on Linux using scanimage (SANE)."""
    with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
        tmp_path = tmp.name

    cmd = [
        "scanimage",
        f"--device-name={device_id}",
        f"--resolution={resolution}",
        f"--mode={mode}",
        "--format=png",
        f"--output-file={tmp_path}",
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=90)
    if result.returncode != 0:
        os.unlink(tmp_path)
        raise RuntimeError(result.stderr or "scanimage failed")

    with open(tmp_path, "rb") as f:
        data = f.read()
    os.unlink(tmp_path)
    return data, "image/png"


def scan_windows(device_id, resolution=200):
    """Scan on Windows using PowerShell WIA COM."""
    ps = f"""
    $wia    = New-Object -ComObject WIA.DeviceManager
    $device = $null
    foreach ($info in $wia.DeviceInfos) {{
        if ($info.DeviceID -eq '{device_id}') {{
            $device = $info.Connect(); break
        }}
    }}
    if (-not $device) {{ throw "Scanner not found: {device_id}" }}
    $item = $device.Items[1]
    $item.Properties["Horizontal Resolution"].Value = {resolution}
    $item.Properties["Vertical Resolution"].Value   = {resolution}
    $item.Properties["Current Intent"].Value        = 4
    $img  = $item.Transfer()
    $path = [IO.Path]::GetTempFileName() -replace '\\.tmp$','.jpg'
    $img.SaveFile($path)
    Write-Output $path
    """
    result = subprocess.run(
        ["powershell", "-NoProfile", "-Command", ps],
        capture_output=True, text=True, timeout=90
    )
    if result.returncode != 0:
        raise RuntimeError(result.stderr or "WIA scan failed")

    tmp_path = result.stdout.strip()
    with open(tmp_path, "rb") as f:
        data = f.read()
    os.unlink(tmp_path)
    return data, "image/jpeg"


def do_scan(device_id, resolution=200, mode="Color"):
    if OS == "Darwin":
        return scan_mac(device_id, resolution)
    if OS == "Windows":
        return scan_windows(device_id, resolution)
    return scan_linux(device_id, resolution, mode)


# ─────────────────────────────────────────────────────────────
#  ROUTES
# ─────────────────────────────────────────────────────────────

@app.route("/health")
def health():
    return jsonify({"status": "ok", "os": OS})


@app.route("/scanners")
def scanners():
    devices = list_scanners()
    return jsonify({"scanners": devices})


@app.route("/scan", methods=["POST"])
def scan():
    body       = request.get_json(silent=True) or {}
    device_id  = body.get("device_id", "")
    resolution = int(body.get("resolution", 200))
    mode       = body.get("mode", "Color")   # Color | Gray | Lineart (Linux only)

    if not device_id:
        devices = list_scanners()
        if not devices:
            return jsonify({"error": "No scanner found"}), 404
        device_id = devices[0]["id"]

    try:
        image_bytes, mime = do_scan(device_id, resolution, mode)
    except RuntimeError as e:
        return jsonify({"error": str(e)}), 500

    # Normalise everything to JPEG via Pillow
    try:
        img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
        buf = io.BytesIO()
        img.save(buf, format="JPEG", quality=92, optimize=True)
        image_bytes = buf.getvalue()
        mime        = "image/jpeg"
    except Exception:
        pass

    b64 = base64.b64encode(image_bytes).decode("utf-8")
    return jsonify({
        "image":    b64,
        "mimeType": mime,
        "dataUrl":  f"data:{mime};base64,{b64}",
    })


# ─────────────────────────────────────────────────────────────
#  ENTRY POINT
# ─────────────────────────────────────────────────────────────

if __name__ == "__main__":
    print("╔══════════════════════════════════════════╗")
    print(f"║  Scanner Bridge  •  port {PORT}             ║")
    print(f"║  OS detected: {OS:<28}║")
    print("╚══════════════════════════════════════════╝")
    if OS == "Darwin":
        print("▶ macOS: using Image Capture via osascript — no brew/SANE needed.")
        print("  If prompted, allow Automation access in System Settings.")
    elif OS == "Linux":
        print("▶ Linux: using SANE.  Install with:  sudo apt install sane-utils")
    else:
        print("▶ Windows: using WIA COM via PowerShell — no install needed.")
    print(f"\n  Listening on http://127.0.0.1:{PORT}   (Ctrl+C to stop)\n")
    app.run(host="127.0.0.1", port=PORT, debug=False)
