from __future__ import annotations

import argparse
import datetime as dt
import json
import os
import socket
from dataclasses import dataclass
from typing import Any, Dict, Optional

from schedint.core.storage import (
    AmbiguousRequestIdError,
    OverrideWindow,
    RequestNotFoundError,
    _now_utc,
    _parse_utc,
    append_request_atomic,
    build_request,
    delete_request_by_prefix_atomic,
    filter_requests,
    load_state,
    set_cadence_multiplier_atomic,
)
from schedint.core.validate import OverlapError, assert_no_overlap_for_source
from schedint.logging_config import configure_logging, get_logger

logger = get_logger("daemon.server")


@dataclass(frozen=True)
class ServerConfig:
    state_path: str
    socket_path: str


class RequestError(Exception):
    """
    Raised for bad client requests (400 style)
    """


def _ensure_socket_dir(socket_path: str) -> None:
    sock_dir = os.path.dirname(socket_path) or "."
    os.makedirs(sock_dir, exist_ok=True)


def _remove_stale_socket(socket_path: str) -> None:
    """
    If a previous daemon crashed, the filesystem entry may
    stick around. It's safe to unlink before bind is nothing
    is listening
    """
    try:
        os.unlink(socket_path)
    except FileNotFoundError:
        return


def _fmt_utc_z(t: Optional[dt.datetime]) -> Optional[str]:
    if t is None:
        return None
    if t.tzinfo is None:
        t = t.replace(tzinfo=dt.timezone.utc)
    return t.astimezone(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def _json_safe_value(v: Any) -> Any:
    try:
        json.dumps(v)
        return v
    except TypeError:
        return str(v)


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 RequestError("Request too large")

    # Trim to first newline if present
    nl = buf.find(b"\n")
    if nl != -1:
        return bytes(buf[:nl])
    return bytes(buf)


def _json_response(ok: bool, **kwargs: Any) -> bytes:
    payload = {"ok": ok, **kwargs}
    return (json.dumps(payload, sort_keys=False) + "\n").encode("utf-8")


def _parse_override_windows(overrides_raw: Any) -> Dict[str, OverrideWindow]:
    if not isinstance(overrides_raw, dict) or not overrides_raw:
        raise RequestError("'overrides' must be non-empty mapping")

    overrides: Dict[str, OverrideWindow] = {}

    for key, block in overrides_raw.items():
        if not isinstance(key, str) or not key:
            raise RequestError("override keys must be non-empty string")

        if not isinstance(block, dict):
            raise RequestError(f"overrides.{key} must be a mapping")

        value = block.get("value", None)
        start = _parse_utc(block.get("start_time"))
        end = _parse_utc(block.get("end_time"))

        overrides[key] = OverrideWindow(value=value, start=start, end=end)

    stars = overrides.get("stars")
    cadence = overrides.get("cadence")
    if stars is not None and cadence is not None:
        if stars.value is not None and cadence.value is not None:
            raise RequestError(
                "Cannot set both 'stars' and 'cadence' - pick one or the other"
            )

    return overrides


def _request_to_dict(req) -> Dict[str, Any]:
    return {
        "request_id": req.request_id,
        "source": req.source,
        "submitted_at": _fmt_utc_z(req.submitted_at),
        "submitted_by": req.submitted_by,
        "submitted_from_host": req.submitted_from_host,
        "reason": req.reason,
        "overrides": {
            name: {
                "value": _json_safe_value(ow.value),
                "start_time": _fmt_utc_z(ow.start),
                "end_time": _fmt_utc_z(ow.end),
            }
            for name, ow in req.overrides.items()
        },
    }


def _handle_get_cadence_multiplier(
    msg: Dict[str, Any], cfg: ServerConfig
) -> Dict[str, Any]:
    state = load_state(cfg.state_path)

    cadence_multiplier = getattr(state, "cadence_multiplier", 1.0)
    return {"cadence_multiplier": float(cadence_multiplier)}


def _handle_set_cadence_multiplier(
    msg: Dict[str, Any], cfg: ServerConfig
) -> Dict[str, Any]:
    cadence_multiplier = msg.get("cadence_multiplier")
    if not isinstance(cadence_multiplier, (int, float)):
        raise RequestError("cadence_multiplier must be a number")
    if cadence_multiplier <= 0:
        raise RequestError("cadence_multiplier must be >= 0")

    updated = set_cadence_multiplier_atomic(
        cfg.state_path, float(cadence_multiplier)
    )
    return {"cadence_multiplier": float(updated.cadence_multiplier)}


def _handle_list(msg: Dict[str, Any], cfg: ServerConfig) -> Dict[str, Any]:
    state = load_state(cfg.state_path)

    request_id = msg.get("request_id")
    source = msg.get("source")
    submitted_by = msg.get("submitted_by")
    active = msg.get("active")
    expired = msg.get("expired")
    between = msg.get("between")

    if request_id is not None and not isinstance(request_id, str):
        raise RequestError("'request_id' must be a string or null")
    if source is not None and not isinstance(source, str):
        raise RequestError("'source' must be a string or null")
    if submitted_by is not None and not isinstance(submitted_by, str):
        raise RequestError("'submitted_by' must be a string or null")
    if active is not None and not isinstance(active, bool):
        raise RequestError("'active' must be a boolean or null")
    if expired is not None and not isinstance(expired, bool):
        raise RequestError("'expired' must be a boolean or null")

    between_parsed = None
    if between is not None:
        if not isinstance(between, list) or len(between) != 2:
            raise RequestError("'between' must be [START, END] or null")

        start = _parse_utc(between[0])
        end = _parse_utc(between[1])
        if start is None or end is None:
            raise RequestError("'between' requires two non-null timestamps")
        between_parsed = (start, end)

    reqs = filter_requests(
        state,
        request_id=request_id,
        source=source,
        submitted_by=submitted_by,
        active=active,
        expired=expired,
        between=between_parsed,
        when=_now_utc(),
    )

    return {"requests": [_request_to_dict(r) for r in reqs]}


def _handle_delete(msg: Dict[str, Any], cfg: ServerConfig) -> Dict[str, Any]:
    prefix = msg.get("id") or msg.get("request_id") or msg.get("prefix")

    if not isinstance(prefix, str) or not prefix:
        raise RequestError(
            "'id' must be a non-empty string (request id prefix)"
        )

    if len(prefix) < 6:
        raise RequestError(
            "Please provide at least 5 characters of the request id"
        )

    try:
        deleted = delete_request_by_prefix_atomic(cfg.state_path, prefix)

    except RequestNotFoundError as e:
        raise RequestError(str(e))

    except AmbiguousRequestIdError as e:
        raise RequestError(str(e))

    return {
        "deleted_request_id": deleted.request_id,
        "source": deleted.source,
    }


def _handle_submit(msg: Dict[str, Any], cfg: ServerConfig) -> Dict[str, Any]:
    source = msg.get("source")
    reason = msg.get("reason")
    overrides_raw = msg.get("overrides")

    if not isinstance(source, str) or not source:
        raise RequestError("source must be a non-empty string")

    if reason is not None and not isinstance(reason, str):
        raise RequestError("reason must be  a string or null")

    overrides = _parse_override_windows(overrides_raw)

    req = build_request(source=source, overrides=overrides, reason=reason)

    append_request_atomic(
        cfg.state_path,
        req,
        overlap_check=assert_no_overlap_for_source,
    )

    return {"request_id": req.request_id}


def _handle_message(msg: Dict[str, Any], cfg: ServerConfig) -> Dict[str, Any]:
    action = msg.get("action")
    if action == "submit":
        return _handle_submit(msg, cfg)
    if action in ("cancel", "delete"):
        return _handle_delete(msg, cfg)
    if action == "list":
        return _handle_list(msg, cfg)
    if action == "set_cadence_multiplier":
        return _handle_set_cadence_multiplier(msg, cfg)
    if action == "get_cadence_multiplier":
        return _handle_get_cadence_multiplier(msg, cfg)
    raise RequestError(f"unknown action: {action!r}")


def serve_forever(cfg: ServerConfig) -> None:
    """
    Blocking server loop
    """
    _ensure_socket_dir(cfg.socket_path)
    _remove_stale_socket(cfg.socket_path)

    with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as srv:
        srv.bind(cfg.socket_path)

        os.chmod(cfg.socket_path, 0o660)

        srv.listen(50)
        logger.info("schedint daemon listening on %s", cfg.socket_path)

        while True:
            conn, _ = srv.accept()
            with conn:
                try:
                    raw = _recv_line(conn)
                    if not raw.strip():
                        raise RequestError("Empty request")

                    msg = json.loads(raw.decode("utf-8"))
                    if not isinstance(msg, dict):
                        raise RequestError("Top level json must be an object")

                    result = _handle_message(msg, cfg)
                    conn.sendall(_json_response(True, **result))

                except OverlapError as e:
                    conn.sendall(_json_response(False, error=str(e)))

                except RequestError as e:
                    conn.sendall(_json_response(False, error=str(e)))

                except Exception as e:
                    logger.exception("Unhandle error processing request %s", e)
                    conn.sendall(
                        _json_response(False, error="Internal server error")
                    )


def main() -> int:
    parser = argparse.ArgumentParser(
        prog="schedintd",
        description="schedint override daemon",
    )

    parser.add_argument(
        "--state-path",
        default="/var/lib/schedint/overrides.yaml",
        help="Path to overrides state file",
    )

    parser.add_argument(
        "--socket-path",
        default="/run/schedint/schedint.sock",
        help="Unix domain socket path",
    )

    parser.add_argument(
        "--log-level",
        default="INFO",
        help="Logging level",
    )

    args = parser.parse_args()
    configure_logging(level=args.log_level)

    cfg = ServerConfig(
        state_path=args.state_path,
        socket_path=args.socket_path,
    )

    serve_forever(cfg)
    return 0
