#!/usr/bin/env python3
"""
VigiNet Phase 2 sentinel agent.

Minimal Raspberry Pi / webcam client:
- register once to get a camera_id and token
- send a heartbeat every 5 minutes
- optionally run an image command and upload candidate captures

Examples:
  python3 viginet_agent.py register --label "Chris roof cam" --lat 48.85 --lon 2.35 --location "Paris"
  python3 viginet_agent.py test
  python3 viginet_agent.py doctor
  python3 viginet_agent.py run --image-command "libcamera-still -n --timeout 1 -o {path} --width 1280 --height 720"
"""

from __future__ import annotations

import argparse
import json
import os
import platform
import shutil
import subprocess
import sys
import tempfile
import time
import uuid
from pathlib import Path
from urllib import request


DEFAULT_API = "https://vigi-sky.fr"
CONFIG_DIR = Path.home() / ".viginet"
CONFIG_PATH = CONFIG_DIR / "config.json"


def load_config() -> dict:
    if CONFIG_PATH.exists():
        return json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
    return {}


def save_config(config: dict) -> None:
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    CONFIG_PATH.write_text(json.dumps(config, indent=2), encoding="utf-8")
    os.chmod(CONFIG_PATH, 0o600)


def post_json(api: str, path: str, payload: dict) -> dict:
    data = json.dumps(payload).encode("utf-8")
    req = request.Request(
        api.rstrip("/") + path,
        data=data,
        headers={"Content-Type": "application/json", "User-Agent": "VigiNet-Agent/1.0"},
        method="POST",
    )
    with request.urlopen(req, timeout=30) as response:
        return json.loads(response.read().decode("utf-8"))


def get_json(api: str, path: str) -> dict:
    req = request.Request(
        api.rstrip("/") + path,
        headers={"User-Agent": "VigiNet-Agent/2.0"},
        method="GET",
    )
    with request.urlopen(req, timeout=20) as response:
        return json.loads(response.read().decode("utf-8"))


def post_multipart(api: str, path: str, fields: dict, image_path: Path | None = None) -> dict:
    boundary = "----viginet_agent_" + uuid.uuid4().hex
    parts: list[bytes] = []
    for key, value in fields.items():
        if value is None:
            continue
        parts.append(
            (
                f"--{boundary}\r\n"
                f'Content-Disposition: form-data; name="{key}"\r\n\r\n'
                f"{value}\r\n"
            ).encode("utf-8")
        )
    if image_path and image_path.exists():
        content = image_path.read_bytes()
        parts.append(
            (
                f"--{boundary}\r\n"
                f'Content-Disposition: form-data; name="image"; filename="{image_path.name}"\r\n'
                "Content-Type: image/jpeg\r\n\r\n"
            ).encode("utf-8")
        )
        parts.append(content)
        parts.append(b"\r\n")
    parts.append(f"--{boundary}--\r\n".encode("utf-8"))
    req = request.Request(
        api.rstrip("/") + path,
        data=b"".join(parts),
        headers={"Content-Type": f"multipart/form-data; boundary={boundary}", "User-Agent": "VigiNet-Agent/1.0"},
        method="POST",
    )
    with request.urlopen(req, timeout=60) as response:
        return json.loads(response.read().decode("utf-8"))


def image_metrics(current: Path, previous: Path | None) -> tuple[float, float]:
    try:
        from PIL import Image, ImageChops, ImageStat
    except Exception:
        return 0.0, 0.0

    try:
        with Image.open(current) as img:
            gray = img.convert("L").resize((160, 90))
            light_score = max(0.0, min(ImageStat.Stat(gray).mean[0] / 255.0, 1.0))
            if previous and previous.exists():
                with Image.open(previous) as prev_img:
                    prev = prev_img.convert("L").resize((160, 90))
                    diff = ImageChops.difference(gray, prev)
                    motion_score = max(0.0, min(ImageStat.Stat(diff).mean[0] / 45.0, 1.0))
            else:
                motion_score = 0.0
    except Exception:
        return 0.0, 0.0
    return round(motion_score, 3), round(light_score, 3)


def run_image_command(command: str, output_path: Path) -> bool:
    cmd = command.format(path=str(output_path))
    result = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=90)
    if result.returncode != 0:
        print(result.stderr.decode("utf-8", errors="ignore").strip())
        return False
    return output_path.exists() and output_path.stat().st_size > 0


def register(args: argparse.Namespace) -> None:
    payload = {
        "label": args.label,
        "latitude": args.lat,
        "longitude": args.lon,
        "location_label": args.location,
        "camera_type": args.camera_type,
        "contact_email": args.email,
        "public_profile": not args.private,
    }
    result = post_json(args.api, "/api/viginet/cameras/register", payload)
    config = {
        "api": args.api.rstrip("/"),
        "camera_id": result["camera_id"],
        "camera_token": result["camera_token"],
        "label": args.label,
        "latitude": args.lat,
        "longitude": args.lon,
        "location_label": args.location,
    }
    save_config(config)
    print(json.dumps(result, indent=2))
    print(f"Saved config to {CONFIG_PATH}")


def heartbeat(args: argparse.Namespace, config: dict | None = None) -> dict:
    config = config or load_config()
    payload = {
        "camera_id": args.camera_id or config.get("camera_id"),
        "camera_token": args.camera_token or config.get("camera_token"),
        "latitude": args.lat if args.lat is not None else config.get("latitude"),
        "longitude": args.lon if args.lon is not None else config.get("longitude"),
        "status": "online",
        "sky_condition": args.sky_condition,
    }
    if not payload["camera_id"]:
        raise SystemExit("Missing camera_id. Run register first.")
    result = post_json(args.api or config.get("api", DEFAULT_API), "/api/viginet/heartbeat", payload)
    print(json.dumps(result, indent=2))
    return result


def test(args: argparse.Namespace) -> None:
    config = load_config()
    payload = {
        "camera_id": args.camera_id or config.get("camera_id"),
        "camera_token": args.camera_token or config.get("camera_token"),
        "sky_condition": args.sky_condition,
    }
    if not payload["camera_id"]:
        raise SystemExit("Missing camera_id. Run register first.")
    result = post_json(args.api or config.get("api", DEFAULT_API), "/api/viginet/test-connection", payload)
    print(json.dumps(result, indent=2))


def doctor(args: argparse.Namespace) -> None:
    config = load_config()
    api = args.api or config.get("api", DEFAULT_API)
    checks = []

    def add(name: str, ok: bool, detail: str = "") -> None:
        checks.append((name, ok, detail))
        prefix = "OK" if ok else "WARN"
        print(f"[{prefix}] {name}" + (f" - {detail}" if detail else ""))

    add("Python", sys.version_info >= (3, 10), platform.python_version())
    add("Config file", CONFIG_PATH.exists(), str(CONFIG_PATH))
    add("camera_id", bool(config.get("camera_id") or args.camera_id), config.get("camera_id", "missing"))
    add("camera_token", bool(config.get("camera_token") or args.camera_token), "configured" if config.get("camera_token") or args.camera_token else "missing")
    try:
        import PIL  # noqa: F401

        add("Pillow", True, "image metrics available")
    except Exception:
        add("Pillow", False, "install with: sudo apt install python3-pil")
    add("libcamera-still", bool(shutil.which("libcamera-still")), shutil.which("libcamera-still") or "not found")

    try:
        cfg = get_json(api, "/api/viginet/agent-config")
        add("VigiNet API", cfg.get("status") == "ok", api)
    except Exception as exc:
        add("VigiNet API", False, str(exc))

    if args.image_command:
        sample = Path(tempfile.gettempdir()) / f"viginet_doctor_{uuid.uuid4().hex}.jpg"
        ok = run_image_command(args.image_command, sample)
        add("Image command", ok, str(sample) if ok else "capture failed")
        if ok:
            motion, light = image_metrics(sample, None)
            print(json.dumps({"sample_path": str(sample), "motion_score": motion, "light_score": light}, indent=2))

    failed = [name for name, ok, _ in checks if not ok and name in {"Python", "VigiNet API"}]
    if failed:
        raise SystemExit(1)


def run(args: argparse.Namespace) -> None:
    config = load_config()
    api = args.api or config.get("api", DEFAULT_API)
    camera_id = args.camera_id or config.get("camera_id")
    camera_token = args.camera_token or config.get("camera_token")
    if not camera_id:
        raise SystemExit("Missing camera_id. Run register first.")

    previous = None
    interval = max(30, int(args.interval))
    threshold = max(0.0, min(float(args.motion_threshold), 1.0))
    print(f"VigiNet agent running for {camera_id}. Interval={interval}s threshold={threshold}")

    while True:
        hb_args = argparse.Namespace(
            api=api,
            camera_id=camera_id,
            camera_token=camera_token,
            lat=args.lat if args.lat is not None else config.get("latitude"),
            lon=args.lon if args.lon is not None else config.get("longitude"),
            sky_condition=args.sky_condition,
        )
        heartbeat(hb_args, config)

        image_path = None
        motion_score = 0.0
        light_score = 0.0
        if args.image_command:
            image_path = Path(tempfile.gettempdir()) / f"viginet_{uuid.uuid4().hex}.jpg"
            if run_image_command(args.image_command, image_path):
                motion_score, light_score = image_metrics(image_path, previous)
                previous = image_path
                if args.debug:
                    print(json.dumps({"image": str(image_path), "motion_score": motion_score, "light_score": light_score}, indent=2))

        if args.dry_run:
            print(json.dumps({
                "dry_run": True,
                "image": str(image_path) if image_path else None,
                "motion_score": motion_score,
                "light_score": light_score,
                "would_upload": bool(image_path and motion_score >= threshold),
            }, indent=2))
        elif image_path and motion_score >= threshold:
            result = post_multipart(
                api,
                "/api/viginet/capture",
                {
                    "camera_id": camera_id,
                    "camera_token": camera_token,
                    "latitude": args.lat if args.lat is not None else config.get("latitude"),
                    "longitude": args.lon if args.lon is not None else config.get("longitude"),
                    "motion_score": motion_score,
                    "light_score": light_score,
                    "object_size": args.object_size,
                    "duration_seconds": interval,
                    "sky_condition": args.sky_condition,
                    "frames_seen": 1,
                    "label": "agent candidate motion",
                },
                image_path,
            )
            print(json.dumps(result, indent=2))
        elif image_path:
            result = post_multipart(
                api,
                "/api/viginet/capture",
                {
                    "camera_id": camera_id,
                    "camera_token": camera_token,
                    "latitude": args.lat if args.lat is not None else config.get("latitude"),
                    "longitude": args.lon if args.lon is not None else config.get("longitude"),
                    "motion_score": motion_score,
                    "light_score": light_score,
                    "object_size": args.object_size,
                    "duration_seconds": interval,
                    "sky_condition": args.sky_condition,
                    "frames_seen": 1,
                    "label": "agent sky sample",
                },
                None,
            )
            print(json.dumps({"frame_sample": result.get("frame")}, indent=2))
        elif args.debug:
            print(json.dumps({"heartbeat_only": True, "reason": "no image command or capture failed"}, indent=2))

        if args.once:
            break
        time.sleep(interval)


def main() -> None:
    parser = argparse.ArgumentParser(description="VigiNet sentinel agent")
    parser.add_argument("--api", default=DEFAULT_API)
    sub = parser.add_subparsers(dest="command", required=True)

    p = sub.add_parser("register")
    p.add_argument("--label", required=True)
    p.add_argument("--lat", type=float)
    p.add_argument("--lon", type=float)
    p.add_argument("--location")
    p.add_argument("--camera-type", default="raspberry-pi")
    p.add_argument("--email")
    p.add_argument("--private", action="store_true")
    p.set_defaults(func=register)

    for name in ("heartbeat", "test", "doctor"):
        p = sub.add_parser(name)
        p.add_argument("--camera-id")
        p.add_argument("--camera-token")
        p.add_argument("--lat", type=float)
        p.add_argument("--lon", type=float)
        p.add_argument("--sky-condition", default="clear")
        p.add_argument("--image-command")
        p.set_defaults(func=heartbeat if name == "heartbeat" else (test if name == "test" else doctor))

    p = sub.add_parser("run")
    p.add_argument("--camera-id")
    p.add_argument("--camera-token")
    p.add_argument("--lat", type=float)
    p.add_argument("--lon", type=float)
    p.add_argument("--sky-condition", default="clear")
    p.add_argument("--interval", type=int, default=300)
    p.add_argument("--motion-threshold", type=float, default=0.18)
    p.add_argument("--object-size", type=float, default=0.08)
    p.add_argument("--image-command")
    p.add_argument("--debug", action="store_true")
    p.add_argument("--dry-run", action="store_true")
    p.add_argument("--once", action="store_true")
    p.set_defaults(func=run)

    args = parser.parse_args()
    try:
        args.func(args)
    except KeyboardInterrupt:
        print("Stopped")
    except Exception as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        raise


if __name__ == "__main__":
    main()
