from __future__ import annotations

import asyncio
import os
import shutil
from datetime import datetime, timezone
from pathlib import Path

from .models import AgentSession, ControlError, Project
from .projects import ProjectRegistry
from .rpc import JsonProcess
from .settings import idle_only, validate_settings


class PiAdapter:
    agent_type = "pi"

    def __init__(self, projects: ProjectRegistry, live_registry, rpc=None, approvals=None):
        self.projects, self.live_registry = projects, live_registry
        self.approvals = approvals
        self.lock = asyncio.Lock()
        repo = Path(__file__).resolve().parents[3]
        node = os.environ.get("GATEWAY_NODE_BIN") or shutil.which("node") or "node"
        pi_binary = shutil.which("pi")
        sdk = os.environ.get("PI_SDK_MODULE", "")
        if not sdk and pi_binary:
            sdk = str(Path(pi_binary).resolve().parents[2] / "dist/index.js")
        env = {**os.environ, "PI_SDK_MODULE": sdk, "DEVICE_LAB_SDK_HOST": "1"}
        env.setdefault("DEVICE_LAB_CREDENTIALS_FILE", os.environ.get("DEVICE_CREDENTIALS_FILE", str(Path.home() / ".config/esp32-device-gateway/device.json")))
        env.setdefault("DEVICE_LAB_GATEWAY_URL", f"http://127.0.0.1:{os.environ.get('GATEWAY_PORT', '8000')}")
        self.rpc = rpc or JsonProcess([node, "--experimental-strip-types", str(repo / "integrations/pi/sdk-host.mjs")], env=env)

    def live_hint(self, session_id: str):
        item = self.live_registry.get(session_id)
        if item and item.state != "OFFLINE" and (datetime.now(timezone.utc) - item.last_activity).total_seconds() < 45:
            return item
        return None

    async def list_sessions(self, project: Project) -> list[AgentSession]:
        if not Path(project.path).is_dir():
            raise ControlError("registered project directory no longer exists", 409)
        catalog = await self.rpc.call("list", {"cwd": project.path})
        result = []
        for item in catalog:
            session = AgentSession(agent="pi", project_id=project.id, **item)
            overlay = self.live_registry.get(session.session_id)
            if overlay and (session.live or self.live_hint(session.session_id)):
                session.live = True
                session.state = overlay.state if overlay.state != "OFFLINE" else item["state"]
                session.updated_at = max(session.updated_at, overlay.last_activity.isoformat())
                session.latest_activity = overlay.latest_activity
                session.recent_text = overlay.public()["recent_text"]
                session.final_text = overlay.public()["final_text"]
                session.activity_type = overlay.activity_type
                session.waiting_reason = overlay.waiting_reason
            result.append(session)
        # Live sessions can precede their first persisted assistant response.
        known = {s.session_id for s in result}
        for overlay in list(self.live_registry.sessions.values()):
            if overlay.session_id not in known and overlay.cwd == project.path and self.live_hint(overlay.session_id):
                result.append(AgentSession(agent="pi", project_id=project.id, session_id=overlay.session_id,
                    name=overlay.name, cwd=overlay.cwd, state=overlay.state, live=True,
                    updated_at=overlay.last_activity.isoformat(), latest_activity=overlay.latest_activity,
                    final_text=overlay.public()["final_text"]))
        if self.approvals:
            result = [self.approvals.overlay(s) for s in result]
        return sorted(result, key=lambda s: s.updated_at, reverse=True)

    async def get_session(self, session_id: str) -> AgentSession:
        for project in self.projects.list():
            for session in await self.list_sessions(project):
                if session.session_id == session_id:
                    return session
        raise ControlError("session not found in registered projects", 404)

    async def resume_session(self, session_id: str) -> bool:
        async with self.lock:
            session = await self.get_session(session_id)
            result = await self.rpc.call("resume", {"session_id": session_id, "cwd": session.cwd,
                "allow_resume": self.live_hint(session_id) is None})
            return result["resumed"]

    async def send_input(self, session_id: str, text: str) -> dict:
        async with self.lock:
            session = await self.get_session(session_id)
            return await self.rpc.call("input", {"session_id": session_id, "cwd": session.cwd,
                "text": text, "allow_resume": self.live_hint(session_id) is None})

    async def get_settings(self, session_id):
        session = await self.get_session(session_id)
        return await self.rpc.call("settings", {"session_id": session_id, "cwd": session.cwd,
            "allow_resume": self.live_hint(session_id) is None})

    async def list_models(self, session_id):
        return (await self.get_settings(session_id))["available_models"]

    async def update_settings(self, session_id, value, *, still_valid=lambda: True):
        async with self.lock:
            session = await self.get_session(session_id)
            idle_only(session)
            current = await self.get_settings(session_id)
            model, effort = validate_settings(value, current)
            if not still_valid(): raise ControlError("settings request cancelled", 409)
            idle_only(await self.get_session(session_id))
            return await self.rpc.call("update_settings", {"session_id": session_id, "cwd": session.cwd,
                "model": model, "allow_resume": self.live_hint(session_id) is None})

    async def close(self):
        await self.rpc.close()
