import json
from dataclasses import dataclass
import os
import signal
import socket
import subprocess
from pathlib import Path

from features.plugins.declared import command_text

CAP = 64 * 1024
SECONDS = 10.0
LONGEST = 60.0
SHOWN = 400


def call(command, cwd: Path, env: dict, payload: dict, seconds: float = SECONDS) -> tuple[bool, dict | str]:
    shell = isinstance(command, str)
    seconds = max(0.1, min(float(seconds), LONGEST))
    try:
        child = subprocess.Popen(["/bin/sh", "-c", command] if shell else list(command), cwd=cwd, env=env,
                                 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, start_new_session=True)
    except (OSError, ValueError) as error:
        return False, str(error)
    try:
        out, err = child.communicate(json.dumps(payload), timeout=seconds)
    except subprocess.TimeoutExpired:
        stop(child)
        return False, f"{command_text(command)} was still running after {seconds:g}s"
    if child.returncode:
        return False, (err.strip() or out.strip() or f"{command_text(command)} exited {child.returncode}")[-SHOWN:]
    return read(out[:CAP])


def asked(where: Path, payload: dict, seconds: float = SECONDS) -> tuple[bool, dict | str] | None:
    try:
        with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as line:
            line.settimeout(max(0.1, min(float(seconds), LONGEST)))
            try:
                line.connect(str(where))
            except OSError:
                return None
            line.sendall(json.dumps(payload).encode() + b"\n")
            out = b""
            while len(out) < CAP and (chunk := line.recv(CAP)):
                out += chunk
    except OSError as error:
        return False, f"{where.name} did not answer: {error}"
    return read(out[:CAP].decode(errors="replace"))


@dataclass(frozen=True)
class PluginReply:
    refuse: str = ""
    cancel: str = ""

    @classmethod
    def from_json(cls, raw) -> "PluginReply":
        raw = raw if isinstance(raw, dict) else {}
        refuse, cancel = raw.get("refuse"), raw.get("cancel")
        return cls(str(refuse).strip() if refuse else "", str(cancel).strip() if cancel else "")


def read(out: str) -> tuple[bool, dict | str]:
    if not out.strip():
        return True, {}
    try:
        reply = json.loads(out)
    except ValueError:
        return False, f"the reply was not JSON: {out.strip()[:SHOWN]}"
    if reply == []:
        return True, {}
    if not isinstance(reply, dict):
        return False, "the reply was not an object"
    return True, reply


def stop(child) -> None:
    for sign in (signal.SIGTERM, signal.SIGKILL):
        try:
            os.killpg(child.pid, sign)
        except (OSError, ProcessLookupError):
            break
        try:
            child.communicate(timeout=2)
            return
        except subprocess.TimeoutExpired:
            continue
