from __future__ import annotations

import json
import socket
from dataclasses import dataclass
from typing import Any, Dict, Optional, Tuple


@dataclass(frozen=True)
class DaemonResponse:
    ok: bool
    request_id: Optional[str] = None
    error: Optional[str] = None
    raw: Optional[Dict[str, Any]] = None


class DaemonClientError(Exception):
    """ """


def _recv_line(conn: socket.socket, limit_bytes: int = 1024 * 1024) -> bytes:
    buf = bytearray()
    while True:
        chunk = conn.recv(4096)
        if not chunk:
            break
        buf.extend(chunk)
        if b"\n" in chunk:
            break
        if len(buf) > limit_bytes:
            raise DaemonClientError("Response too large")

    nl = buf.find(b"\n")
    if nl != -1:
        return bytes(buf[: nl + 1])
    return bytes(buf)


def call_daemon(
    socket_path: str,
    message: Dict[str, Any],
    *,
    timeout_s: float = 5.0,
) -> DaemonResponse:

    if not isinstance(message, dict):
        raise ValueError("message must be a dict")

    payload = (json.dumps(message, sort_keys=False) + "\n").encode("utf-8")

    try:
        with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as conn:
            conn.settimeout(timeout_s)
            conn.connect(socket_path)

            conn.sendall(payload)

            try:
                conn.shutdown(socket.SHUT_WR)
            except OSError:
                pass

            raw = _recv_line(conn)

    except FileNotFoundError as e:
        raise DaemonClientError(
            f"Daemon socket not found at {socket_path!r}. Is schedintd running?"
        ) from e
    except ConnectionRefusedError as e:
        raise DaemonClientError(
            f"Connection refused to {socket_path!r}. Is schedintd running?"
        ) from e
    except socket.timeout as e:
        raise DaemonClientError("Timed out talking to schedintd") from e
    except OSError as e:
        raise DaemonClientError(f"OS Error talking to daemon: {e}") from e

    try:
        resp = json.loads(raw.decode("utf-8").splitlines()[0])
    except Exception as e:
        raise DaemonClientError(
            f"Invalid response from daemon: {raw!r}"
        ) from e

    if not isinstance(resp, dict) or "ok" not in resp:
        raise DaemonClientError(f"Malformed response from daemon: {resp!r}")

    ok = bool(resp.get("ok"))
    return DaemonResponse(
        ok=ok,
        request_id=resp.get("request_id"),
        error=resp.get("error"),
        raw=resp,
    )


def get_cadence_multiplier(
    socket_path: str,
    *,
    timeout_s: float = 5.0,
) -> DaemonResponse:

    return call_daemon(
        socket_path,
        {"action": "get_cadence_multiplier"},
        timeout_s=timeout_s,
    )


def set_cadence_multiplier(
    socket_path: str,
    *,
    value: float,
    timeout_s: float = 5.0,
) -> DaemonResponse:

    return call_daemon(
        socket_path,
        {"action": "set_cadence_multiplier", "cadence_multiplier": value},
        timeout_s=timeout_s,
    )


def list_requests(
    socket_path: str,
    *,
    request_id: Optional[str] = None,
    source: Optional[str] = None,
    submitted_by: Optional[str] = None,
    active: Optional[bool] = None,
    expired: Optional[bool] = None,
    between: Optional[Tuple[str, str]] = None,
    timeout_s: float = 5.0,
) -> DaemonResponse:

    msg: Dict[str, Any] = {"action": "list"}

    if request_id is not None:
        msg["request_id"] = request_id
    if source is not None:
        msg["source"] = source
    if submitted_by is not None:
        msg["submitted_by"] = submitted_by
    if active is not None:
        msg["active"] = active
    if expired is not None:
        msg["expired"] = expired
    if between is not None:
        start, end = between
        msg["between"] = [start, end]

    return call_daemon(socket_path, msg, timeout_s=timeout_s)


def cancel_request(
    socket_path: str,
    *,
    request_id_prefix: str,
    timeout_s: float = 5.0,
) -> DaemonResponse:
    msg: Dict[str, Any] = {
        "action": "cancel",
        "id": request_id_prefix,
    }
    return call_daemon(socket_path, msg, timeout_s=timeout_s)


def submit_request(
    socket_path: str,
    *,
    source: str,
    overrides: Dict[str, Dict[str, Any]],
    reason: Optional[str] = None,
    timeout_s: float = 5.0,
) -> DaemonResponse:
    """ """
    msg: Dict[str, Any] = {
        "action": "submit",
        "source": source,
        "overrides": overrides,
    }
    if reason is not None:
        msg["reason"] = reason

    return call_daemon(socket_path, msg, timeout_s=timeout_s)
