from __future__ import annotations

from datetime import datetime, timedelta, timezone
from typing import Callable

from backend.app.api.errors import AppError
from backend.app.api.schemas import (
    RemoteClientHeartbeatRequest,
    RemoteClientItem,
    RemoteClientListResponse,
    RemoteClientRegisterRequest,
)
from backend.app.db.remote_client_repository import RemoteClientRepository


class RemoteClientService:
    def __init__(
        self,
        repository: RemoteClientRepository,
        registration_token: str,
        offline_timeout_seconds: int,
        now: Callable[[], datetime] | None = None,
    ):
        self._repository = repository
        self._registration_token = registration_token.strip()
        self._offline_timeout_seconds = max(1, int(offline_timeout_seconds))
        self._now = now or (lambda: datetime.now(tz=timezone.utc))

    def list_clients(self) -> RemoteClientListResponse:
        self._refresh_stale_statuses()
        items = [RemoteClientItem(**row) for row in self._repository.list_clients()]
        return RemoteClientListResponse(items=items)

    def get_client(self, client_id: str) -> RemoteClientItem:
        normalized_client_id = (client_id or "").strip()
        if not normalized_client_id:
            raise AppError(
                code="invalid_request",
                message="client_id is required",
                status_code=400,
                details={"client_id": client_id},
            )
        self._refresh_stale_statuses()
        item = self._repository.get_client(normalized_client_id)
        if item is None:
            raise AppError(
                code="path_not_found",
                message="Remote client was not found",
                status_code=404,
                details={"client_id": normalized_client_id},
            )
        return RemoteClientItem(**item)

    def register_client(self, authorization: str | None, request: RemoteClientRegisterRequest) -> RemoteClientItem:
        self._require_registration_auth(authorization)
        payload = self._normalize_register_request(request)
        now_iso = self._to_iso(self._now())
        item = self._repository.upsert_client(now_iso=now_iso, **payload)
        return RemoteClientItem(**item)

    def record_heartbeat(self, authorization: str | None, request: RemoteClientHeartbeatRequest) -> RemoteClientItem:
        self._require_registration_auth(authorization)
        client_id = (request.client_id or "").strip()
        agent_version = (request.agent_version or "").strip()
        if not client_id:
            raise AppError(
                code="invalid_request",
                message="client_id is required",
                status_code=400,
                details={"client_id": request.client_id},
            )
        if not agent_version:
            raise AppError(
                code="invalid_request",
                message="agent_version is required",
                status_code=400,
                details={"agent_version": request.agent_version},
            )
        item = self._repository.record_heartbeat(
            client_id=client_id,
            agent_version=agent_version,
            now_iso=self._to_iso(self._now()),
        )
        if item is None:
            raise AppError(
                code="path_not_found",
                message="Remote client was not found",
                status_code=404,
                details={"client_id": client_id},
            )
        return RemoteClientItem(**item)

    def _require_registration_auth(self, authorization: str | None) -> None:
        if not self._registration_token:
            raise AppError(
                code="remote_client_registration_disabled",
                message="Remote client registration is not configured",
                status_code=503,
            )
        expected = f"Bearer {self._registration_token}"
        if (authorization or "").strip() != expected:
            raise AppError(
                code="forbidden",
                message="Invalid remote client registration token",
                status_code=403,
            )

    def _normalize_register_request(self, request: RemoteClientRegisterRequest) -> dict:
        client_id = (request.client_id or "").strip()
        display_name = (request.display_name or "").strip()
        platform = (request.platform or "").strip()
        agent_version = (request.agent_version or "").strip()
        endpoint = (request.endpoint or "").strip()
        shares = [
            {"key": (item.key or "").strip(), "label": (item.label or "").strip()}
            for item in request.shares
        ]
        shares = [item for item in shares if item["key"] and item["label"]]

        if not client_id:
            raise AppError("invalid_request", "client_id is required", 400, {"client_id": request.client_id})
        if not display_name:
            raise AppError("invalid_request", "display_name is required", 400, {"display_name": request.display_name})
        if not platform:
            raise AppError("invalid_request", "platform is required", 400, {"platform": request.platform})
        if not agent_version:
            raise AppError("invalid_request", "agent_version is required", 400, {"agent_version": request.agent_version})
        if not endpoint:
            raise AppError("invalid_request", "endpoint is required", 400, {"endpoint": request.endpoint})
        if not shares:
            raise AppError("invalid_request", "at least one share is required", 400, {"shares": "[]"})

        return {
            "client_id": client_id,
            "display_name": display_name,
            "platform": platform,
            "agent_version": agent_version,
            "endpoint": endpoint,
            "shares": shares,
        }

    def _refresh_stale_statuses(self) -> None:
        now = self._now()
        self._repository.mark_stale_clients_offline(
            cutoff_iso=self._to_iso(now - timedelta(seconds=self._offline_timeout_seconds)),
            now_iso=self._to_iso(now),
        )

    @staticmethod
    def _to_iso(value: datetime) -> str:
        return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
