"""K2Aley GAR-BOT Raspberry Pi 5 local AI agent."""

from __future__ import annotations

import argparse
import json
import logging
import signal
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path

from .brain import LocalBrain, RobotCommand
from .claws import ClawController
from .config import AgentConfig, load_config
from .llm import LocalLLM
from .motors import MotorController

LOG = logging.getLogger("gar-bot-agent")


def _lora_message_text(raw: bytes | str) -> str:
    text = raw.decode("utf-8", errors="ignore") if isinstance(raw, (bytes, bytearray)) else str(raw)
    text = text.strip()
    if not text:
        return ""

    try:
        payload = json.loads(text)
    except json.JSONDecodeError:
        return text

    if isinstance(payload, dict):
        if isinstance(payload.get("text"), str):
            return payload["text"]
        if isinstance(payload.get("command"), str):
            return payload["command"]
    return ""


def _run_lora_listener(agent: "GarBotAgent", lora_device: str, lora_baud: int, stop_event: threading.Event) -> None:
    try:
        import serial  # type: ignore
    except Exception:
        LOG.warning("pyserial not available; LoRa listener disabled")
        return

    LOG.info("Starting LoRa listener on %s at %d", lora_device, lora_baud)
    while not stop_event.is_set():
        try:
            with serial.Serial(lora_device, baudrate=lora_baud, timeout=0.25, write_timeout=0.25) as link:
                while not stop_event.is_set():
                    frame = link.readline()
                    if not frame:
                        continue
                    text = _lora_message_text(frame)
                    if not text:
                        continue
                    try:
                        agent.execute_text(text)
                    except Exception:
                        LOG.exception("Failed to execute LoRa command: %s", text)
        except Exception:
            LOG.warning("LoRa listener lost serial link; retrying in 1s", exc_info=True)
            time.sleep(1.0)


class GarBotAgent:
    def __init__(self, config: AgentConfig):
        self.config = config
        self.brain = LocalBrain()
        self.llm = LocalLLM(config.ollama_enabled, config.ollama_url, config.ollama_model)
        self.motors = MotorController(config)
        self.claws = ClawController(config)
        self.started_at = time.time()
        self.last_command = "boot"

    def execute_text(self, text: str) -> dict:
        classified = self.llm.classify(text)
        command = self.brain.parse(classified)
        result = self.execute(command, raw=text)
        result["classified"] = classified
        return result

    def execute(self, command: RobotCommand, raw: str = "") -> dict:
        self.last_command = command.action
        if command.action == "stop":
            self.motors.stop()
        elif command.action == "drive":
            self.motors.drive(command.value * self.config.max_speed, 0)
        elif command.action == "turn":
            self.motors.drive(0, command.value * self.config.turn_speed)
        elif command.action == "claw_open":
            self.claws.open(command.side)
        elif command.action == "claw_close":
            self.claws.close(command.side)
        elif command.action == "claw_home":
            self.claws.home(command.side)
        elif command.action == "status":
            pass
        else:
            self.motors.stop()
            command = RobotCommand("stop")
        return self.status(extra={"raw": raw, "action": command.action, "side": command.side})

    def status(self, extra: dict | None = None) -> dict:
        data = {
            "robot": self.config.robot_name,
            "hardware_enabled": self.config.hardware_enabled,
            "uptime_s": round(time.time() - self.started_at, 1),
            "last_command": self.last_command,
            "safe_wet_use_rule": "no exposed electronics",
            "network_plan": {
                "rp5_wifi": "SSH, setup, touchscreen/API service, and local maintenance",
                "rpzero2w_wifi": "transmitter SSH, Picovoice setup, and LAN command tests",
                "field_control": "LoRa long-range control/status path",
            },
        }
        if extra:
            data.update(extra)
        return data

    def shutdown(self) -> None:
        self.motors.close()
        self.claws.shutdown()


def make_handler(agent: GarBotAgent):
    class Handler(BaseHTTPRequestHandler):
        def _send(self, code: int, payload: dict) -> None:
            body = json.dumps(payload, indent=2).encode("utf-8")
            self.send_response(code)
            self.send_header("Content-Type", "application/json")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)

        def do_GET(self) -> None:  # noqa: N802
            if self.path in ("/", "/status"):
                self._send(200, agent.status())
            else:
                self._send(404, {"error": "not found"})

        def do_POST(self) -> None:  # noqa: N802
            length = int(self.headers.get("Content-Length", "0"))
            body = self.rfile.read(length).decode("utf-8") if length else "{}"
            try:
                payload = json.loads(body)
            except json.JSONDecodeError:
                self._send(400, {"error": "invalid json"})
                return
            if self.path == "/command":
                self._send(200, agent.execute_text(str(payload.get("text", ""))))
            elif self.path == "/stop":
                self._send(200, agent.execute(RobotCommand("stop")))
            else:
                self._send(404, {"error": "not found"})

        def log_message(self, fmt: str, *args) -> None:
            LOG.info("%s - %s", self.address_string(), fmt % args)

    return Handler


def main() -> int:
    parser = argparse.ArgumentParser(description="K2Aley GAR-BOT local AI agent")
    parser.add_argument("--config", default="config/gar_bot_agent.json")
    parser.add_argument("--once", help="Run one text command and exit")
    args = parser.parse_args()

    logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
    config = load_config(Path(args.config))
    agent = GarBotAgent(config)

    if args.once:
        print(json.dumps(agent.execute_text(args.once), indent=2))
        agent.shutdown()
        return 0

    stop = False
    stop_event = threading.Event()

    def _handle_signal(_signum, _frame):
        nonlocal stop
        stop = True
        stop_event.set()
        agent.motors.stop()

    signal.signal(signal.SIGTERM, _handle_signal)
    signal.signal(signal.SIGINT, _handle_signal)

    server = ThreadingHTTPServer((config.http_host, config.http_port), make_handler(agent))
    LOG.info("Starting %s HTTP agent on %s:%s", config.robot_name, config.http_host, config.http_port)

    lora_thread = None
    if config.lora_device:
        lora_thread = threading.Thread(
            target=_run_lora_listener,
            args=(agent, config.lora_device, int(config.lora_baud), stop_event),
            daemon=True,
        )
        lora_thread.start()

    try:
        while not stop:
            server.handle_request()
    finally:
        stop_event.set()
        if lora_thread is not None:
            lora_thread.join(timeout=1.0)
        agent.shutdown()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
