"""Shared helpers for executing news subscriptions.

These centralize logic that was previously copy-pasted across the three places
that run a subscription:

* ``run_subscription_now`` (manual "run now" button) and
  ``check_overdue_subscriptions`` (the overdue sweep) -- both start research
  in-process via ``flask_api._call_start_research_internal`` and share the
  request payload built here. (They previously issued a loopback HTTP POST to
  ``/research/api/start``, which always failed CSRF validation.)
* ``BackgroundJobScheduler`` -- triggers research programmatically, but shares
  the refresh-schedule arithmetic.

Keeping the payload shape and the schedule arithmetic in one place stops the
three call sites from drifting (which they previously had: differing metadata,
a forgotten search-engine field, and a refresh-time update that one path
skipped entirely).
"""

from datetime import datetime, timedelta, timezone
from typing import Any, Dict, Optional


def build_subscription_request_data(
    *,
    query_template: str,
    current_date: str,
    triggered_by: str,
    subscription_id: Any,
    model_provider: Optional[str] = None,
    model: Optional[str] = None,
    search_strategy: Optional[str] = None,
    search_engine: Optional[str] = None,
    custom_endpoint: Optional[str] = None,
    title: Optional[str] = None,
    mode: str = "quick",
) -> Dict[str, Any]:
    """Build the ``/research/api/start`` payload for a subscription run.

    Replaces the ``YYYY-MM-DD`` placeholder in ``query_template`` with
    ``current_date`` (the user's timezone-local date) and assembles the news
    metadata block. Shared by the manual run-now route and the overdue sweep so
    their payloads cannot diverge.

    ``model_provider``/``model`` are passed through as-is, including falsy
    values: an unset provider/model is intentional and makes the backend fall
    back to the user's ``llm.provider`` / ``llm.model`` settings (see
    ``research_routes._extract_research_params``). Hardcoding a provider here
    would override the user's configured default for subscriptions created
    without an explicit model.
    """
    query = query_template.replace("YYYY-MM-DD", current_date)

    request_data: Dict[str, Any] = {
        "query": query,
        "mode": mode,
        "model_provider": model_provider,
        "model": model,
        "strategy": search_strategy or "news_aggregation",
        "metadata": {
            "is_news_search": True,
            "search_type": "news_analysis",
            "display_in": "news_feed",
            "subscription_id": str(subscription_id),
            "triggered_by": triggered_by,
            # Original query keeps the placeholder; processed_query/news_date
            # record what was actually run.
            "original_query": query_template,
            "processed_query": query,
            "news_date": current_date,
            "title": title or None,
        },
    }

    # Optional fields are only included when set, matching the research API's
    # "absent means use default" contract.
    if search_engine:
        request_data["search_engine"] = search_engine
    if custom_endpoint:
        request_data["custom_endpoint"] = custom_endpoint

    return request_data


def advance_refresh_schedule(
    subscription, now: Optional[datetime] = None
) -> None:
    """Advance a subscription's refresh timestamps after a successful run.

    Sets ``last_refresh`` to ``now`` and ``next_refresh`` one
    ``refresh_interval_minutes`` later. Shared by the manual run, the overdue
    sweep, and the scheduler so the arithmetic -- and remembering to advance
    the schedule at all -- lives in one place.
    """
    if now is None:
        now = datetime.now(timezone.utc)
    subscription.last_refresh = now
    subscription.next_refresh = now + timedelta(
        minutes=subscription.refresh_interval_minutes
    )


def advance_refresh_schedule_by_id(db_session, subscription_id) -> bool:
    """Load a subscription by id and advance its refresh schedule.

    Convenience wrapper for callers that only have an id and an open session
    (e.g. the research service's post-completion update, which runs once for
    the quick path and once for the detailed path). Returns True if the
    subscription was found and advanced. Does not commit -- the caller owns
    the transaction.
    """
    from ..database.models.news import NewsSubscription

    subscription = (
        db_session.query(NewsSubscription)
        .filter(NewsSubscription.id == str(subscription_id))
        .first()
    )
    if not subscription:
        return False
    advance_refresh_schedule(subscription, datetime.now(timezone.utc))
    return True


def mark_subscription_due_by_id(db_session, subscription_id) -> bool:
    """Reset a subscription to "due" after a FAILED run.

    ``run_subscription_now`` (and the overdue sweep) advance ``next_refresh``
    at spawn time so the scheduler will not double-run a subscription whose
    research is still in flight. If that research then FAILS, the success-only
    completion advance never fires, leaving ``next_refresh`` pushed a full
    interval into the future and the subscription silently skipped until then.
    The research failure handler calls this to reset ``next_refresh`` to now so
    the scheduler picks the subscription up again on its next cycle -- matching
    the pre-consolidation behavior where a failed manual run left the
    subscription due. ``last_refresh`` is intentionally left untouched (it
    records the last *successful* refresh). Returns True if the subscription
    was found. Does not commit -- the caller owns the transaction.
    """
    from ..database.models.news import NewsSubscription

    subscription = (
        db_session.query(NewsSubscription)
        .filter(NewsSubscription.id == str(subscription_id))
        .first()
    )
    if not subscription:
        return False
    subscription.next_refresh = datetime.now(timezone.utc)
    return True
