"""Async client for managing recurrent runs (cron jobs) in LangGraph."""

from __future__ import annotations

import warnings
from collections.abc import Mapping, Sequence
from datetime import datetime, tzinfo
from typing import Any

from langgraph_sdk._async.http import HttpClient
from langgraph_sdk._shared.utilities import (
    NOT_PROVIDED,
    _quote_path_param,
    _resolve_timezone,
)
from langgraph_sdk.schema import (
    All,
    Config,
    Context,
    Cron,
    CronSelectField,
    CronSortBy,
    Durability,
    Input,
    Json,
    OnCompletionBehavior,
    QueryParamTypes,
    Run,
    SortOrder,
    StreamMode,
)


class CronClient:
    """Client for managing recurrent runs (cron jobs) in LangGraph.

    A run is a single invocation of an assistant with optional input, config, and context.
    This client allows scheduling recurring runs to occur automatically.

    ???+ example "Example Usage"

        ```python
        client = get_client(url="http://localhost:2024"))
        cron_job = await client.crons.create_for_thread(
            thread_id="thread_123",
            assistant_id="asst_456",
            schedule="0 9 * * *",
            input={"message": "Daily update"}
        )
        ```

    !!! note "Feature Availability"

        The crons client functionality is not supported on all licenses.
        Please check the relevant license documentation for the most up-to-date
        details on feature availability.
    """

    def __init__(self, http_client: HttpClient) -> None:
        self.http = http_client

    async def create_for_thread(
        self,
        thread_id: str,
        assistant_id: str,
        *,
        schedule: str,
        input: Input | None = None,
        metadata: Mapping[str, Any] | None = None,
        config: Config | None = None,
        context: Context | None = None,
        checkpoint_during: bool | None = None,  # deprecated
        interrupt_before: All | list[str] | None = None,
        interrupt_after: All | list[str] | None = None,
        webhook: str | None = None,
        multitask_strategy: str | None = None,
        end_time: datetime | None = None,
        enabled: bool | None = None,
        timezone: str | tzinfo | None = None,
        stream_mode: StreamMode | Sequence[StreamMode] | None = None,
        stream_subgraphs: bool | None = None,
        stream_resumable: bool | None = None,
        durability: Durability | None = None,
        headers: Mapping[str, str] | None = None,
        params: QueryParamTypes | None = None,
    ) -> Run:
        """Create a cron job for a thread.

        Args:
            thread_id: the thread ID to run the cron job on.
            assistant_id: The assistant ID or graph name to use for the cron job.
                If using graph name, will default to first assistant created from that graph.
            schedule: The cron schedule to execute this job on.
                Schedules are interpreted in UTC unless a timezone is specified.
            input: The input to the graph.
            metadata: Metadata to assign to the cron job runs.
            config: The configuration for the assistant.
            context: Static context to add to the assistant.
                !!! version-added "Added in version 0.6.0"
            checkpoint_during: (deprecated) Whether to checkpoint during the run (or only at the end/interruption).
            interrupt_before: Nodes to interrupt immediately before they get executed.

            interrupt_after: Nodes to Nodes to interrupt immediately after they get executed.

            webhook: Webhook to call after LangGraph API call is done.
            multitask_strategy: Multitask strategy to use.
                Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'.
            end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely.
            enabled: Whether the cron job is enabled or not.
            timezone: IANA timezone for the cron schedule. Accepts a string (e.g. 'America/New_York') or a ``datetime.tzinfo`` instance (e.g. ``ZoneInfo("America/New_York")``).
            stream_mode: The stream mode(s) to use.
            stream_subgraphs: Whether to stream output from subgraphs.
            stream_resumable: Whether to persist the stream chunks in order to resume the stream later.
            durability: Durability level for the run. Must be one of 'sync', 'async', or 'exit'.
                "async" means checkpoints are persisted async while next graph step executes, replaces checkpoint_during=True
                "sync" means checkpoints are persisted sync after graph step executes, replaces checkpoint_during=False
                "exit" means checkpoints are only persisted when the run exits, does not save intermediate steps
            headers: Optional custom headers to include with the request.
            params: Optional query parameters to include with the request.

        Returns:
            The cron run.

        ???+ example "Example Usage"

            ```python
            client = get_client(url="http://localhost:2024")
            cron_run = await client.crons.create_for_thread(
                thread_id="my-thread-id",
                assistant_id="agent",
                schedule="27 15 * * *",
                input={"messages": [{"role": "user", "content": "hello!"}]},
                metadata={"name":"my_run"},
                context={"model_name": "openai"},
                interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"],
                interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"],
                webhook="https://my.fake.webhook.com",
                multitask_strategy="interrupt",
                enabled=True,
            )
            ```
        """
        if checkpoint_during is not None:
            warnings.warn(
                "`checkpoint_during` is deprecated and will be removed in a future version. Use `durability` instead.",
                DeprecationWarning,
                stacklevel=2,
            )

        payload = {
            "schedule": schedule,
            "input": input,
            "config": config,
            "metadata": metadata,
            "context": context,
            "assistant_id": assistant_id,
            "checkpoint_during": checkpoint_during,
            "interrupt_before": interrupt_before,
            "interrupt_after": interrupt_after,
            "webhook": webhook,
            "end_time": end_time.isoformat() if end_time else None,
            "enabled": enabled,
            "timezone": _resolve_timezone(timezone),
            "stream_mode": stream_mode,
            "stream_subgraphs": stream_subgraphs,
            "stream_resumable": stream_resumable,
            "durability": durability,
        }
        if multitask_strategy:
            payload["multitask_strategy"] = multitask_strategy
        payload = {k: v for k, v in payload.items() if v is not None}
        return await self.http.post(
            f"/threads/{_quote_path_param(thread_id)}/runs/crons",
            json=payload,
            headers=headers,
            params=params,
        )

    async def create(
        self,
        assistant_id: str,
        *,
        schedule: str,
        input: Input | None = None,
        metadata: Mapping[str, Any] | None = None,
        config: Config | None = None,
        context: Context | None = None,
        checkpoint_during: bool | None = None,  # deprecated
        interrupt_before: All | list[str] | None = None,
        interrupt_after: All | list[str] | None = None,
        webhook: str | None = None,
        on_run_completed: OnCompletionBehavior | None = None,
        multitask_strategy: str | None = None,
        end_time: datetime | None = None,
        enabled: bool | None = None,
        timezone: str | tzinfo | None = None,
        stream_mode: StreamMode | Sequence[StreamMode] | None = None,
        stream_subgraphs: bool | None = None,
        stream_resumable: bool | None = None,
        durability: Durability | None = None,
        headers: Mapping[str, str] | None = None,
        params: QueryParamTypes | None = None,
    ) -> Run:
        """Create a cron run.

        Args:
            assistant_id: The assistant ID or graph name to use for the cron job.
                If using graph name, will default to first assistant created from that graph.
            schedule: The cron schedule to execute this job on.
                Schedules are interpreted in UTC unless a timezone is specified.
            input: The input to the graph.
            metadata: Metadata to assign to the cron job runs.
            config: The configuration for the assistant.
            context: Static context to add to the assistant.
                !!! version-added "Added in version 0.6.0"
            checkpoint_during: (deprecated) Whether to checkpoint during the run (or only at the end/interruption).
            interrupt_before: Nodes to interrupt immediately before they get executed.
            interrupt_after: Nodes to Nodes to interrupt immediately after they get executed.
            webhook: Webhook to call after LangGraph API call is done.
            on_run_completed: What to do with the thread after the run completes.
                Must be one of 'delete' (default) or 'keep'. 'delete' removes the thread
                after execution. 'keep' creates a new thread for each execution but does not
                clean them up. Clients are responsible for cleaning up kept threads.
            multitask_strategy: Multitask strategy to use.
                Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'.
            end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely.
            enabled: Whether the cron job is enabled or not.
            timezone: IANA timezone for the cron schedule. Accepts a string (e.g. 'America/New_York') or a ``datetime.tzinfo`` instance (e.g. ``ZoneInfo("America/New_York")``).
            stream_mode: The stream mode(s) to use.
            stream_subgraphs: Whether to stream output from subgraphs.
            stream_resumable: Whether to persist the stream chunks in order to resume the stream later.
            durability: Durability level for the run. Must be one of 'sync', 'async', or 'exit'.
                "async" means checkpoints are persisted async while next graph step executes, replaces checkpoint_during=True
                "sync" means checkpoints are persisted sync after graph step executes, replaces checkpoint_during=False
                "exit" means checkpoints are only persisted when the run exits, does not save intermediate steps
            headers: Optional custom headers to include with the request.
            params: Optional query parameters to include with the request.

        Returns:
            The cron run.

        ???+ example "Example Usage"

            ```python
            client = get_client(url="http://localhost:2024")
            cron_run = client.crons.create(
                assistant_id="agent",
                schedule="27 15 * * *",
                input={"messages": [{"role": "user", "content": "hello!"}]},
                metadata={"name":"my_run"},
                context={"model_name": "openai"},
                interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"],
                interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"],
                webhook="https://my.fake.webhook.com",
                multitask_strategy="interrupt",
                enabled=True,
            )
            ```

        """
        if checkpoint_during is not None:
            warnings.warn(
                "`checkpoint_during` is deprecated and will be removed in a future version. Use `durability` instead.",
                DeprecationWarning,
                stacklevel=2,
            )

        payload = {
            "schedule": schedule,
            "input": input,
            "config": config,
            "metadata": metadata,
            "context": context,
            "assistant_id": assistant_id,
            "checkpoint_during": checkpoint_during,
            "interrupt_before": interrupt_before,
            "interrupt_after": interrupt_after,
            "webhook": webhook,
            "on_run_completed": on_run_completed,
            "end_time": end_time.isoformat() if end_time else None,
            "enabled": enabled,
            "timezone": _resolve_timezone(timezone),
            "stream_mode": stream_mode,
            "stream_subgraphs": stream_subgraphs,
            "stream_resumable": stream_resumable,
            "durability": durability,
        }
        if multitask_strategy:
            payload["multitask_strategy"] = multitask_strategy
        payload = {k: v for k, v in payload.items() if v is not None}
        return await self.http.post(
            "/runs/crons", json=payload, headers=headers, params=params
        )

    async def delete(
        self,
        cron_id: str,
        *,
        headers: Mapping[str, str] | None = None,
        params: QueryParamTypes | None = None,
    ) -> None:
        """Delete a cron.

        Args:
            cron_id: The cron ID to delete.
            headers: Optional custom headers to include with the request.
            params: Optional query parameters to include with the request.

        Returns:
            `None`

        ???+ example "Example Usage"

            ```python
            client = get_client(url="http://localhost:2024")
            await client.crons.delete(
                cron_id="cron_to_delete"
            )
            ```

        """
        await self.http.delete(
            f"/runs/crons/{_quote_path_param(cron_id)}", headers=headers, params=params
        )

    async def update(
        self,
        cron_id: str,
        *,
        schedule: str | None = None,
        end_time: datetime | None = NOT_PROVIDED,
        input: Input | None = None,
        metadata: Mapping[str, Any] | None = None,
        config: Config | None = None,
        context: Context | None = None,
        webhook: str | None = None,
        interrupt_before: All | list[str] | None = None,
        interrupt_after: All | list[str] | None = None,
        on_run_completed: OnCompletionBehavior | None = None,
        enabled: bool | None = None,
        timezone: str | tzinfo | None = None,
        stream_mode: StreamMode | Sequence[StreamMode] | None = None,
        stream_subgraphs: bool | None = None,
        stream_resumable: bool | None = None,
        durability: Durability | None = None,
        headers: Mapping[str, str] | None = None,
        params: QueryParamTypes | None = None,
    ) -> Cron:
        """Update a cron job by ID.

        Args:
            cron_id: The cron ID to update.
            schedule: The cron schedule to execute this job on.
                Schedules are interpreted in UTC unless a timezone is specified.
            end_time: The end date to stop running the cron. Pass ``None`` to
                clear a previously set end time; omit to leave it unchanged.
            input: The input to the graph.
            metadata: Metadata to assign to the cron job runs.
            config: The configuration for the assistant.
            context: Static context added to the assistant.
            webhook: Webhook to call after LangGraph API call is done.
            interrupt_before: Nodes to interrupt immediately before they get executed.
            interrupt_after: Nodes to interrupt immediately after they get executed.
            on_run_completed: What to do with the thread after the run completes.
                Must be one of 'delete' or 'keep'. 'delete' removes the thread
                after execution. 'keep' creates a new thread for each execution but does not
                clean them up.
            enabled: Enable or disable the cron job.
            timezone: IANA timezone for the cron schedule. Accepts a string (e.g. 'America/New_York') or a ``datetime.tzinfo`` instance (e.g. ``ZoneInfo("America/New_York")``).
            stream_mode: The stream mode(s) to use.
            stream_subgraphs: Whether to stream output from subgraphs.
            stream_resumable: Whether to persist the stream chunks in order to resume the stream later.
            durability: Durability level for the run. Must be one of 'sync', 'async', or 'exit'.
            headers: Optional custom headers to include with the request.
            params: Optional query parameters to include with the request.

        Returns:
            The updated cron job.

        ???+ example "Example Usage"

            ```python
            client = get_client(url="http://localhost:2024")
            updated_cron = await client.crons.update(
                cron_id="1ef3cefa-4c09-6926-96d0-3dc97fd5e39b",
                schedule="0 10 * * *",
                enabled=False,
            )
            ```

        """
        payload = {
            "schedule": schedule,
            "input": input,
            "metadata": metadata,
            "config": config,
            "context": context,
            "webhook": webhook,
            "interrupt_before": interrupt_before,
            "interrupt_after": interrupt_after,
            "on_run_completed": on_run_completed,
            "enabled": enabled,
            "timezone": _resolve_timezone(timezone),
            "stream_mode": stream_mode,
            "stream_subgraphs": stream_subgraphs,
            "stream_resumable": stream_resumable,
            "durability": durability,
        }
        payload = {k: v for k, v in payload.items() if v is not None}
        # An explicit end_time=None clears the end time; NOT_PROVIDED leaves it
        # unchanged. Inject after the None-strip so the explicit null survives.
        if end_time is not NOT_PROVIDED:
            payload["end_time"] = end_time.isoformat() if end_time is not None else None
        return await self.http.patch(
            f"/runs/crons/{_quote_path_param(cron_id)}",
            json=payload,
            headers=headers,
            params=params,
        )

    async def search(
        self,
        *,
        assistant_id: str | None = None,
        thread_id: str | None = None,
        enabled: bool | None = None,
        metadata: Json = None,
        limit: int = 10,
        offset: int = 0,
        sort_by: CronSortBy | None = None,
        sort_order: SortOrder | None = None,
        select: list[CronSelectField] | None = None,
        headers: Mapping[str, str] | None = None,
        params: QueryParamTypes | None = None,
    ) -> list[Cron]:
        """Get a list of cron jobs.

        Args:
            assistant_id: The assistant ID or graph name to search for.
            thread_id: the thread ID to search for.
            enabled: The enabled status to search for.
            metadata: Metadata to filter by. Exact match filter for each KV pair.
                !!! version-added "Added in Agent Server version 0.9.0"
            limit: The maximum number of results to return.
            offset: The number of results to skip.
            headers: Optional custom headers to include with the request.
            params: Optional query parameters to include with the request.

        Returns:
            The list of cron jobs returned by the search,

        ???+ example "Example Usage"

            ```python
            client = get_client(url="http://localhost:2024")
            cron_jobs = await client.crons.search(
                assistant_id="my_assistant_id",
                thread_id="my_thread_id",
                enabled=True,
                limit=5,
                offset=5,
            )
            print(cron_jobs)
            ```
            ```shell

            ----------------------------------------------------------

            [
                {
                    'cron_id': '1ef3cefa-4c09-6926-96d0-3dc97fd5e39b',
                    'assistant_id': 'my_assistant_id',
                    'thread_id': 'my_thread_id',
                    'user_id': None,
                    'payload':
                        {
                            'input': {'start_time': ''},
                            'schedule': '4 * * * *',
                            'assistant_id': 'my_assistant_id'
                        },
                    'schedule': '4 * * * *',
                    'next_run_date': '2024-07-25T17:04:00+00:00',
                    'end_time': None,
                    'created_at': '2024-07-08T06:02:23.073257+00:00',
                    'updated_at': '2024-07-08T06:02:23.073257+00:00'
                }
            ]
            ```

        """
        payload: dict[str, Any] = {
            "assistant_id": assistant_id,
            "thread_id": thread_id,
            "enabled": enabled,
            "limit": limit,
            "offset": offset,
        }
        if metadata:
            payload["metadata"] = metadata
        if sort_by:
            payload["sort_by"] = sort_by
        if sort_order:
            payload["sort_order"] = sort_order
        if select:
            payload["select"] = select
        payload = {k: v for k, v in payload.items() if v is not None}
        return await self.http.post(
            "/runs/crons/search", json=payload, headers=headers, params=params
        )

    async def count(
        self,
        *,
        assistant_id: str | None = None,
        thread_id: str | None = None,
        metadata: Json = None,
        headers: Mapping[str, str] | None = None,
        params: QueryParamTypes | None = None,
    ) -> int:
        """Count cron jobs matching filters.

        Args:
            assistant_id: Assistant ID to filter by.
            thread_id: Thread ID to filter by.
            metadata: Metadata to filter by. Exact match filter for each KV pair.
                !!! version-added "Added in Agent Server version 0.9.0"
            headers: Optional custom headers to include with the request.
            params: Optional query parameters to include with the request.

        Returns:
            int: Number of crons matching the criteria.
        """
        payload: dict[str, Any] = {}
        if assistant_id:
            payload["assistant_id"] = assistant_id
        if thread_id:
            payload["thread_id"] = thread_id
        if metadata:
            payload["metadata"] = metadata
        return await self.http.post(
            "/runs/crons/count", json=payload, headers=headers, params=params
        )
