#!/usr/bin/env python3
"""
=============================================================================
ARSchool Scanner Bridge Agent
Multi-Platform Physical Scanner Service for ARSchool Web Application
Supported Platforms: Windows (WIA/TWAIN) & Linux (SANE)
Port: 18080 (http://127.0.0.1:18080)
=============================================================================
"""

import sys
import os
import platform
import subprocess
import json
import base64
import tempfile
import time
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs

PORT = 18080
VERSION = "1.0.0"
SYSTEM_OS = platform.system().lower() # 'windows', 'linux', 'darwin'

print("=" * 60)
print(f"🚀 ARSchool Scanner Bridge Agent v{VERSION}")
print(f"🖥️  Sistem Operasi Terdeteksi: {platform.system()} ({platform.release()})")
print(f"🌐 Menjalankan Local Service di: http://127.0.0.1:{PORT}")
print("=" * 60)

def detect_devices_linux():
    """Mendeteksi scanner via SANE (scanimage -L) di Linux"""
    devices = []
    try:
        res = subprocess.run(['scanimage', '-L'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=10)
        output = res.stdout + "\n" + res.stderr
        for line in output.splitlines():
            line = line.strip()
            if line.startswith("device `"):
                parts = line.split("'", 1)
                dev_id = parts[0].replace("device `", "").strip()
                dev_desc = parts[1].replace("is a ", "").strip() if len(parts) > 1 else dev_id
                devices.append({
                    "id": dev_id,
                    "name": dev_desc,
                    "type": "SANE"
                })
    except FileNotFoundError:
        print("⚠️ Perintah 'scanimage' belum terpasang. Pasang dengan: sudo apt-get install sane sane-utils")
    except Exception as e:
        print(f"⚠️ Gagal mendeteksi scanner SANE: {e}")

    if not devices:
        devices.append({
            "id": "default",
            "name": "Mesin Scanner Default (SANE Auto-Detect)",
            "type": "SANE"
        })
    return devices

def detect_devices_windows():
    """Mendeteksi scanner via Windows Image Acquisition (WIA) COM Object di Windows"""
    devices = []
    ps_script = """
    try {
        $dm = New-Object -ComObject WIA.DeviceManager
        $list = @()
        foreach ($info in $dm.DeviceInfos) {
            if ($info.Type -eq 1) { # 1 = ScannerDeviceType
                $name = $info.Properties.Item("Name").Value
                $id = $info.DeviceID
                $list += @{ id = $id; name = $name; type = "WIA" }
            }
        }
        $list | ConvertTo-Json -Compress
    } catch {
        Write-Output "[]"
    }
    """
    try:
        res = subprocess.run(["powershell", "-NoProfile", "-NonInteractive", "-Command", ps_script],
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=12)
        out = res.stdout.strip()
        if out and out != "[]":
            data = json.loads(out)
            if isinstance(data, dict):
                devices.append(data)
            elif isinstance(data, list):
                devices.extend(data)
    except Exception as e:
        print(f"⚠️ Gagal mendeteksi scanner Windows WIA: {e}")

    if not devices:
        devices.append({
            "id": "default",
            "name": "Mesin Scanner Utama PC (WIA / TWAIN)",
            "type": "WIA"
        })
    return devices

def get_connected_devices():
    if "windows" in SYSTEM_OS:
        return detect_devices_windows()
    elif "linux" in SYSTEM_OS:
        return detect_devices_linux()
    else:
        return [{"id": "default", "name": f"Mesin Scanner ({platform.system()})", "type": "Generic"}]

def scan_document_linux(device_id, dpi=200, mode="Color"):
    """Melakukan scan di Linux via scanimage"""
    temp_dir = tempfile.gettempdir()
    out_file = os.path.join(temp_dir, f"arschool_scan_{int(time.time())}.jpg")
    
    sane_mode = "Color"
    if mode.lower() in ["gray", "grayscale", "abu-abu"]:
        sane_mode = "Gray"
    elif mode.lower() in ["lineart", "bw", "hitam-putih"]:
        sane_mode = "Lineart"

    cmd = ["scanimage", f"--resolution={dpi}", f"--mode={sane_mode}", f"--format=jpeg", f"--output-file={out_file}"]
    if device_id and device_id != "default":
        cmd.extend(["-d", device_id])

    try:
        print(f"⏳ Menjalankan scan Linux: {' '.join(cmd)}")
        res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=60)
        if os.path.exists(out_file) and os.path.getsize(out_file) > 100:
            with open(out_file, "rb") as f:
                img_data = f.read()
            try: os.remove(out_file)
            except: pass
            return base64.b64encode(img_data).decode("utf-8")
        else:
            pnm_file = os.path.join(temp_dir, f"arschool_scan_{int(time.time())}.pnm")
            cmd_pnm = ["scanimage", f"--resolution={dpi}", f"--mode={sane_mode}", f"--output-file={pnm_file}"]
            if device_id and device_id != "default":
                cmd_pnm.extend(["-d", device_id])
            subprocess.run(cmd_pnm, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=60)
            if os.path.exists(pnm_file):
                try:
                    from PIL import Image
                    with Image.open(pnm_file) as im:
                        im.save(out_file, "JPEG", quality=90)
                    with open(out_file, "rb") as f:
                        img_data = f.read()
                    try: os.remove(pnm_file); os.remove(out_file)
                    except: pass
                    return base64.b64encode(img_data).decode("utf-8")
                except:
                    pass
            raise Exception(res.stderr or "Gagal membaca hasil scan dari perangkat SANE.")
    except Exception as e:
        raise Exception(f"Gagal melakukan scan SANE: {str(e)}")

def scan_document_windows(device_id, dpi=200, mode="Color"):
    """Melakukan scan di Windows via Windows Image Acquisition (WIA) PowerShell Script"""
    temp_dir = tempfile.gettempdir()
    out_file = os.path.join(temp_dir, f"arschool_scan_{int(time.time())}.jpg").replace("\\", "/")
    
    intent_val = 1
    if mode.lower() in ["gray", "grayscale", "abu-abu"]:
        intent_val = 2
    elif mode.lower() in ["lineart", "bw", "hitam-putih"]:
        intent_val = 4

    ps_script = f"""
    try {{
        $dialog = New-Object -ComObject WIA.CommonDialog
        $image = $dialog.ShowAcquireImage(1, {intent_val}, 131072, "{'{'}B96B3CAE-0728-11D3-9D7B-0000F81EF32E{'}'}", $false, $false, $false)
        if ($image) {{
            $image.SaveFile("{out_file}")
            Write-Output "SUCCESS"
        }} else {{
            Write-Output "CANCELLED"
        }}
    }} catch {{
        Write-Error $_.Exception.Message
    }}
    """
    try:
        print(f"⏳ Menjalankan scan Windows WIA...")
        res = subprocess.run(["powershell", "-NoProfile", "-NonInteractive", "-Command", ps_script],
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=90)
        
        if os.path.exists(out_file) and os.path.getsize(out_file) > 100:
            with open(out_file, "rb") as f:
                img_data = f.read()
            try: os.remove(out_file)
            except: pass
            return base64.b64encode(img_data).decode("utf-8")
        else:
            err_msg = res.stderr.strip() or res.stdout.strip()
            if "CANCELLED" in err_msg:
                raise Exception("Pemindaian dibatalkan oleh pengguna.")
            raise Exception(err_msg or "Tidak ada data gambar yang dihasilkan dari pemindai Windows.")
    except Exception as e:
        raise Exception(f"Gagal melakukan scan WIA: {str(e)}")

class ScannerBridgeHandler(BaseHTTPRequestHandler):
    def _set_cors_headers(self, status=200, content_type="application/json"):
        self.send_response(status)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With, Access-Control-Request-Private-Network")
        self.send_header("Access-Control-Allow-Private-Network", "true")
        self.send_header("Content-Type", content_type)
        self.end_headers()

    def do_OPTIONS(self):
        self._set_cors_headers(204)

    def do_GET(self):
        parsed = urlparse(self.path)
        path = parsed.path

        if path == "/status" or path == "/":
            response_data = {
                "status": "online",
                "service": "ARSchool Scanner Bridge Agent",
                "version": VERSION,
                "os": platform.system(),
                "os_type": "windows" if "windows" in SYSTEM_OS else "linux",
                "ready": True
            }
            self._set_cors_headers(200)
            self.wfile.write(json.dumps(response_data).encode("utf-8"))

        elif path == "/devices":
            devices = get_connected_devices()
            response_data = {
                "status": "success",
                "os": platform.system(),
                "devices": devices
            }
            self._set_cors_headers(200)
            self.wfile.write(json.dumps(response_data).encode("utf-8"))

        else:
            self._set_cors_headers(404)
            self.wfile.write(json.dumps({"error": "Endpoint tidak ditemukan"}).encode("utf-8"))

    def do_POST(self):
        parsed = urlparse(self.path)
        path = parsed.path

        if path == "/scan":
            content_length = int(self.headers.get("Content-Length", 0))
            body = self.rfile.read(content_length).decode("utf-8") if content_length > 0 else "{}"
            try:
                params = json.loads(body) if body else {}
            except:
                params = {}

            device_id = params.get("device_id", "default")
            dpi = int(params.get("resolution", 200))
            mode = params.get("mode", "Color")

            print(f"📥 Menerima permintaan scan: Device='{device_id}', DPI={dpi}, Mode='{mode}'")

            try:
                if "windows" in SYSTEM_OS:
                    b64_data = scan_document_windows(device_id, dpi, mode)
                else:
                    b64_data = scan_document_linux(device_id, dpi, mode)

                response_data = {
                    "status": "success",
                    "message": "Dokumen berhasil dipindai dari mesin scanner!",
                    "image_base64": f"data:image/jpeg;base64,{b64_data}",
                    "dpi": dpi,
                    "mode": mode,
                    "timestamp": int(time.time())
                }
                self._set_cors_headers(200)
                self.wfile.write(json.dumps(response_data).encode("utf-8"))
            except Exception as e:
                print(f"❌ Error Scan: {e}")
                self._set_cors_headers(500)
                self.wfile.write(json.dumps({
                    "status": "error",
                    "message": str(e)
                }).encode("utf-8"))
        else:
            self._set_cors_headers(404)
            self.wfile.write(json.dumps({"error": "Endpoint tidak ditemukan"}).encode("utf-8"))

    def log_message(self, format, *args):
        print(f"[{time.strftime('%H:%M:%S')}] {args[0]} - {args[1]}")

def run_server():
    server_address = ("0.0.0.0", PORT)
    httpd = HTTPServer(server_address, ScannerBridgeHandler)
    print(f"✅ Layanan ARSchool Scanner Bridge siap digunakan!")
    print(f"💡 Buka ARSchool di browser Anda dan klik tombol 'Pindai dari Mesin Scanner'.")
    print(f"🛑 Tekan Ctrl+C untuk menghentikan layanan.\n")
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("\n👋 Menghentikan ARSchool Scanner Bridge Agent...")
        httpd.server_close()

if __name__ == "__main__":
    run_server()
