#!/usr/bin/env python3
"""
Pi Agent Server — Lightweight HTTP API for inter-agent communication.
Hermes (or other agents) can discover, task, and query Pi via this endpoint.

Bind: 0.0.0.0:4747
Security: Accepts only localhost, local LAN (192.168.0.0/24), and Tailscale (100.64.0.0/10)
"""

import http.server
import json
import os
import socketserver
import subprocess
import sys
import threading
import time
import uuid
from datetime import datetime, timezone
from urllib.parse import urlparse

PORT = 4747
ALLOWED_NETWORKS = [
    ("127.0.0.0", 8),      # localhost
    ("192.168.0.0", 24),   # local LAN
    ("100.64.0.0", 10),    # Tailscale CGNAT
]

TASKS = {}
TASK_LOCK = threading.Lock()


def ip_in_allowed_network(client_ip: str) -> bool:
    """Check if client IP is in an allowed network."""
    try:
        parts = [int(x) for x in client_ip.split(".")]
        ip_int = (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]
        for network, prefix in ALLOWED_NETWORKS:
            net_parts = [int(x) for x in network.split(".")]
            net_int = (net_parts[0] << 24) | (net_parts[1] << 16) | (net_parts[2] << 8) | net_parts[3]
            mask = (0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF
            if (ip_int & mask) == (net_int & mask):
                return True
        return False
    except Exception:
        return False


class AgentHandler(http.server.BaseHTTPRequestHandler):
    def log_message(self, format, *args):
        ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
        client = self.client_address[0]
        print(f"[{ts}] {client} {args[0]}", flush=True)

    def do_OPTIONS(self):
        self.send_response(200)
        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")
        self.end_headers()

    def _reject(self, code=403, message="Forbidden"):
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps({"error": message}).encode())

    def _json_ok(self, data):
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps(data, indent=2).encode())

    def _check_auth(self):
        client_ip = self.client_address[0]
        if not ip_in_allowed_network(client_ip):
            self._reject(403, f"IP {client_ip} not in allowed networks")
            return False
        return True

    def do_GET(self):
        if not self._check_auth():
            return

        parsed = urlparse(self.path)
        path = parsed.path

        # ── Health / Status ──
        if path == "/status" or path == "/":
            self._json_ok({
                "agent": "pi",
                "version": "0.74.2",
                "status": "ready",
                "timestamp": datetime.now(timezone.utc).isoformat(),
                "workspace": "/Users/wmj2024/Desktop/Projects/FalahMobile",
                "tailscale_ip": "100.72.2.115",
                "local_ip": "192.168.0.10",
                "tasks_active": len([t for t in TASKS.values() if t["status"] == "running"]),
                "tasks_completed": len([t for t in TASKS.values() if t["status"] == "done"]),
            })
            return

        # ── Agent Manifest ──
        if path == "/agent/manifest":
            self._json_ok({
                "id": "pi",
                "name": "Pi Coding Agent",
                "version": "0.74.2",
                "hostname": "mac-mini-1",
                "endpoints": {
                    "status": "GET /status",
                    "manifest": "GET /agent/manifest",
                    "task_submit": "POST /agent/task",
                    "task_query": "GET /agent/task/<id>",
                    "signal_read": "GET /agent/signal/<name>",
                    "signal_write": "POST /agent/signal/<name>",
                },
                "capabilities": [
                    "file_read", "file_write", "file_edit",
                    "bash_exec", "git_ops",
                    "code_generation", "schema_design",
                    "content_authoring", "tts_pipeline",
                ],
                "languages": ["typescript", "javascript", "python", "bash", "prisma", "sql"],
                "platforms": ["macos", "linux", "docker"],
                "collaboration": {
                    "herdr_workspace": "w1",
                    "github_repo": "maifors/falah-mobile",
                    "gitea_repo": "wmj/falahmobile-content",
                },
                "contact": {
                    "tailscale_ip": "100.72.2.115",
                    "local_ip": "192.168.0.10",
                    "ssh_port": 22,
                    "agent_port": PORT,
                },
            })
            return

        # ── Task Query ──
        if path.startswith("/agent/task/"):
            task_id = path.split("/")[-1]
            task = TASKS.get(task_id)
            if not task:
                self._reject(404, "Task not found")
                return
            self._json_ok(task)
            return

        # ── Signal Read ──
        if path.startswith("/agent/signal/"):
            name = path.split("/")[-1]
            signal_path = f"/Users/wmj2024/Desktop/Projects/FalahMobile/.pi/{name}.json"
            if os.path.exists(signal_path):
                with open(signal_path, "r") as f:
                    self._json_ok(json.load(f))
            else:
                self._reject(404, f"Signal {name} not found")
            return

        self._reject(404, "Unknown endpoint")

    def do_POST(self):
        if not self._check_auth():
            return

        parsed = urlparse(self.path)
        path = parsed.path
        content_length = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(content_length).decode("utf-8") if content_length > 0 else "{}"

        try:
            payload = json.loads(body) if body else {}
        except json.JSONDecodeError:
            self._reject(400, "Invalid JSON body")
            return

        # ── Task Submit ──
        if path == "/agent/task":
            task_id = str(uuid.uuid4())[:8]
            task = {
                "id": task_id,
                "status": "queued",
                "created_at": datetime.now(timezone.utc).isoformat(),
                "completed_at": None,
                "request": payload,
                "result": None,
                "error": None,
            }
            with TASK_LOCK:
                TASKS[task_id] = task

            # Spawn worker thread
            threading.Thread(target=self._run_task, args=(task_id,), daemon=True).start()

            self._json_ok({"task_id": task_id, "status": "queued", "query_url": f"/agent/task/{task_id}"})
            return

        # ── Signal Write ──
        if path.startswith("/agent/signal/"):
            name = path.split("/")[-1]
            signal_path = f"/Users/wmj2024/Desktop/Projects/FalahMobile/.pi/{name}.json"
            with open(signal_path, "w") as f:
                json.dump(payload, f, indent=2)
            self._json_ok({"signal": name, "saved": True, "path": signal_path})
            return

        self._reject(404, "Unknown endpoint")

    def _run_task(self, task_id):
        """Execute a task asynchronously."""
        with TASK_LOCK:
            task = TASKS[task_id]
            task["status"] = "running"

        req = task["request"]
        action = req.get("action")

        try:
            if action == "read_file":
                filepath = req.get("path")
                if not os.path.exists(filepath):
                    raise FileNotFoundError(f"File not found: {filepath}")
                with open(filepath, "r") as f:
                    result = {"content": f.read()}

            elif action == "write_file":
                filepath = req.get("path")
                content = req.get("content", "")
                os.makedirs(os.path.dirname(filepath), exist_ok=True)
                with open(filepath, "w") as f:
                    f.write(content)
                result = {"path": filepath, "bytes_written": len(content)}

            elif action == "bash":
                cmd = req.get("command", "")
                timeout = req.get("timeout", 60)
                cwd = req.get("cwd", "/Users/wmj2024/Desktop/Projects/FalahMobile")
                proc = subprocess.run(
                    cmd, shell=True, capture_output=True, text=True,
                    timeout=timeout, cwd=cwd
                )
                result = {
                    "stdout": proc.stdout,
                    "stderr": proc.stderr,
                    "returncode": proc.returncode,
                }

            elif action == "git_push":
                branch = req.get("branch", "feat/learn-module")
                remote = req.get("remote", "github")
                message = req.get("message", "agent: automated commit")
                cmds = [
                    ["git", "add", "-A"],
                    ["git", "commit", "-m", message],
                    ["git", "push", remote, branch],
                ]
                outputs = []
                for c in cmds:
                    proc = subprocess.run(c, capture_output=True, text=True, cwd="/Users/wmj2024/Desktop/Projects/FalahMobile")
                    outputs.append({"cmd": c, "rc": proc.returncode, "out": proc.stdout[-500:], "err": proc.stderr[-500:]})
                result = {"operations": outputs}

            elif action == "schema_verify":
                schema_path = req.get("path", "prisma/schema.prisma")
                with open(schema_path, "r") as f:
                    schema = f.read()
                models = [line.strip() for line in schema.split("\n") if line.strip().startswith("model ")]
                result = {"models_found": models, "schema_path": schema_path}

            else:
                result = {"error": f"Unknown action: {action}"}

            with TASK_LOCK:
                task["status"] = "done"
                task["completed_at"] = datetime.now(timezone.utc).isoformat()
                task["result"] = result

        except Exception as e:
            with TASK_LOCK:
                task["status"] = "error"
                task["completed_at"] = datetime.now(timezone.utc).isoformat()
                task["error"] = str(e)


def run_server():
    with socketserver.TCPServer(("0.0.0.0", PORT), AgentHandler) as httpd:
        print(f"[AGENT] Pi server listening on 0.0.0.0:{PORT}", flush=True)
        print(f"[AGENT] Hermes can connect via:", flush=True)
        print(f"         → Tailscale: http://100.72.2.115:{PORT}/agent/manifest", flush=True)
        print(f"         → Local LAN: http://192.168.0.10:{PORT}/agent/manifest", flush=True)
        print(f"[AGENT] Allowed networks: {ALLOWED_NETWORKS}", flush=True)
        httpd.serve_forever()


if __name__ == "__main__":
    run_server()
