# retoor <retoor@molodetz.nl>

import json
import logging
from typing import Any

from devplacepy.database import get_table, get_users_by_uids, resolve_by_slug
from devplacepy.services.containers import api, store
from devplacepy.services.containers.api import ContainerError
from devplacepy.services.containers.runtime import get_backend
from devplacepy.services.devii.errors import ToolInputError
from devplacepy.services.devii.tasks.schedule import Schedule, from_iso

logger = logging.getLogger("devii.container")


class ContainerController:
    def __init__(self, client: Any = None, owner_id: str = "") -> None:
        self._client = client
        self._owner_id = owner_id

    def _ingress_url(self, instance: dict):
        slug = instance.get("ingress_slug")
        if not slug:
            return None
        from devplacepy.seo import public_base_url

        base = public_base_url()
        return f"{base}/p/{slug}" if base else f"/p/{slug}"

    def _actor_user(self) -> dict:
        username = getattr(self._client, "username", None)
        if username and username != "api-key":
            user = get_table("users").find_one(username=username)
            if user:
                return user
        if self._owner_id:
            user = get_users_by_uids([self._owner_id]).get(self._owner_id)
            if user:
                return user
        return {
            "uid": "admin",
            "username": username or "admin",
            "role": "Admin",
        }

    def _project(self, arguments: dict) -> dict:
        from devplacepy.content import can_view_project_containers

        slug = str(arguments.get("project_slug", "")).strip()
        project = resolve_by_slug(get_table("projects"), slug) if slug else None
        if not project or not can_view_project_containers(project, self._actor_user()):
            raise ToolInputError(f"project not found: {slug}")
        return project

    def _require_manage(self, project: dict, inst: dict) -> None:
        from devplacepy.content import can_manage_instance

        if not can_manage_instance(inst, project, self._actor_user()):
            raise ToolInputError(
                "only the container owner or the primary administrator can manage this instance"
            )

    def _instance(self, project: dict, ref: str) -> dict:
        inst = store.get_instance(ref)
        if inst is None or inst["project_uid"] != project["uid"]:
            for candidate in store.list_instances(project["uid"]):
                if candidate["name"] == ref:
                    return candidate
            raise ToolInputError(f"instance not found: {ref}")
        return inst

    async def dispatch(self, name: str, arguments: dict[str, Any]) -> str:
        handler = (
            getattr(self, "_" + name[len("container_") :], None)
            if name.startswith("container_")
            else None
        )
        if handler is None:
            raise ToolInputError(f"unknown container tool: {name}")
        try:
            return await handler(arguments)
        except ContainerError as exc:
            return json.dumps({"error": "container_error", "message": str(exc)})

    async def _list_instances(self, arguments) -> str:
        project = self._project(arguments)
        instances = store.list_instances(project["uid"])
        for inst in instances:
            inst["ingress_url"] = self._ingress_url(inst)
        return json.dumps({"instances": instances}, default=str)

    async def _create_instance(self, arguments) -> str:
        project = self._project(arguments)
        autostart = str(arguments.get("autostart", "true")).lower() not in (
            "false",
            "0",
            "no",
            "off",
        )
        start_on_boot = str(arguments.get("start_on_boot", "false")).lower() in (
            "true",
            "1",
            "yes",
            "on",
        )
        inst = await api.create_instance(
            project,
            name=str(arguments.get("name", "")),
            boot_command=str(arguments.get("boot_command", "")),
            boot_language=str(arguments.get("boot_language", "none")),
            boot_script=str(arguments.get("boot_script", "")),
            run_as_uid=str(arguments.get("run_as_uid", "")),
            start_on_boot=start_on_boot,
            env=arguments.get("env", ""),
            cpu_limit=str(arguments.get("cpu_limit", "")),
            mem_limit=str(arguments.get("mem_limit", "")),
            ports=arguments.get("ports", ""),
            restart_policy=str(arguments.get("restart_policy", "never")),
            autostart=autostart,
            ingress_slug=str(arguments.get("ingress_slug", "")),
            ingress_port=arguments.get("ingress_port"),
            actor=("user", self._actor_user()["uid"]),
        )
        return json.dumps(
            {"instance": inst, "ingress_url": self._ingress_url(inst)}, default=str
        )

    async def _instance_action(self, arguments) -> str:
        project = self._project(arguments)
        inst = self._instance(project, str(arguments.get("instance", "")))
        self._require_manage(project, inst)
        action = str(arguments.get("action", "")).lower()
        actor = ("user", self._actor_user()["uid"])
        if action == "delete":
            api.mark_for_removal(inst, actor=actor)
        elif action == "sync":
            counts = await api.sync_workspace(inst, self._actor_user())
            return json.dumps({"status": "synced", **counts})
        elif action == "restart":
            api.request_restart(inst, actor=actor)
        elif action in ("start", "resume"):
            api.set_desired_state(inst, store.DESIRED_RUNNING, actor=actor)
        elif action == "stop":
            api.set_desired_state(inst, store.DESIRED_STOPPED, actor=actor)
        elif action == "pause":
            api.set_desired_state(inst, store.DESIRED_PAUSED, actor=actor)
        else:
            raise ToolInputError(f"unknown action: {action}")
        return json.dumps({"status": "ok", "action": action, "instance": inst["uid"]})

    async def _configure_instance(self, arguments) -> str:
        project = self._project(arguments)
        inst = self._instance(project, str(arguments.get("instance", "")))
        self._require_manage(project, inst)
        actor = ("user", self._actor_user()["uid"])
        kwargs: dict = {}
        for key in (
            "run_as_uid",
            "boot_language",
            "boot_script",
            "boot_command",
            "restart_policy",
            "cpu_limit",
            "mem_limit",
        ):
            if key in arguments and arguments.get(key) is not None:
                kwargs[key] = str(arguments.get(key))
        if "start_on_boot" in arguments and arguments.get("start_on_boot") is not None:
            kwargs["start_on_boot"] = str(arguments.get("start_on_boot")).lower() in (
                "true",
                "1",
                "yes",
                "on",
            )
        updated = api.update_instance_config(inst, actor=actor, **kwargs)
        return json.dumps({"status": "configured", "instance": updated}, default=str)

    async def _logs(self, arguments) -> str:
        project = self._project(arguments)
        inst = self._instance(project, str(arguments.get("instance", "")))
        if not inst.get("container_id"):
            return json.dumps({"logs": "", "status": inst["status"]})
        lines: list = []

        async def collect(line: str) -> None:
            lines.append(line)

        tail = int(arguments.get("tail", 200) or 200)
        await get_backend().logs(
            inst["container_id"],
            follow=False,
            tail=max(1, min(tail, 2000)),
            on_log=collect,
        )
        return json.dumps({"logs": "\n".join(lines)})

    async def _exec(self, arguments) -> str:
        project = self._project(arguments)
        inst = self._instance(project, str(arguments.get("instance", "")))
        self._require_manage(project, inst)
        if not inst.get("container_id"):
            raise ToolInputError("instance is not running")
        command = str(arguments.get("command", "")).strip()
        if not command:
            raise ToolInputError("command is required")
        result = await get_backend().exec(
            inst["container_id"], ["/bin/sh", "-c", command]
        )
        actor = self._actor_user()
        store.record_event(
            inst,
            "exec",
            "user",
            actor["uid"],
            {"command": command, "exit_code": result.exit_code},
        )
        return json.dumps({"exit_code": result.exit_code, "output": result.output})

    async def _stats(self, arguments) -> str:
        project = self._project(arguments)
        inst = self._instance(project, str(arguments.get("instance", "")))
        return json.dumps(
            {
                "instance": inst["name"],
                "status": inst["status"],
                "ingress_url": self._ingress_url(inst),
                "runtime": await api.instance_runtime(inst),
                "stats": api.instance_stats(inst["uid"]),
            },
            default=str,
        )

    async def _schedule(self, arguments) -> str:
        project = self._project(arguments)
        inst = self._instance(project, str(arguments.get("instance", "")))
        self._require_manage(project, inst)
        run_at = arguments.get("run_at")
        try:
            schedule = Schedule(
                kind=str(arguments.get("kind", "")),
                cron=arguments.get("cron") or None,
                run_at=from_iso(run_at) if run_at else None,
                every_seconds=arguments.get("every_seconds"),
            )
        except ValueError as exc:
            raise ToolInputError(str(exc))
        sched = api.add_schedule(inst, str(arguments.get("action", "")), schedule)
        return json.dumps({"schedule": sched}, default=str)
