"""
Direct API functions for news system.
These functions can be called directly by scheduler or wrapped by Flask endpoints.
"""

from typing import Dict, Any, Optional
from datetime import datetime, timezone, UTC
from loguru import logger
import re
import json

from ..constants import ResearchStatus
from ..llm.providers.base import normalize_provider
from .exceptions import (
    InvalidLimitException,
    SubscriptionNotFoundException,
    SubscriptionCreationException,
    SubscriptionUpdateException,
    SubscriptionDeletionException,
    DatabaseAccessException,
    NewsFeedGenerationException,
    NotImplementedException,
    NewsAPIException,
)
# Removed welcome feed import - no placeholders
# get_db_setting not available in merged codebase


def _notify_scheduler_about_subscription_change(
    action: str, user_id: Optional[str] = None
):
    """
    Notify the scheduler about subscription changes.

    Args:
        action: The action performed (created, updated, deleted)
        user_id: Optional user_id to use as fallback for username
    """
    try:
        from flask import session as flask_session
        from ..scheduler.background import get_background_job_scheduler

        scheduler = get_background_job_scheduler()
        if scheduler.is_running:
            # Get username, with optional fallback to user_id
            username = flask_session.get("username")
            if not username and user_id:
                username = user_id

            # Get password from session password store
            from ..database.session_passwords import session_password_store

            session_id = flask_session.get("session_id")
            password = None
            if session_id and username:
                password = session_password_store.get_session_password(
                    username, session_id
                )

            if password and username:
                # Update scheduler to reschedule subscriptions
                scheduler.update_user_info(username, password)
                logger.info(
                    f"Scheduler notified about {action} subscription for {username}"
                )
            else:
                logger.warning(
                    f"Could not notify scheduler - no password available{' for ' + username if username else ''}"
                )
    except Exception:
        logger.exception(
            f"Could not notify scheduler about {action} subscription"
        )


def get_news_feed(
    user_id: str = "anonymous",
    limit: int = 20,
    use_cache: bool = True,
    focus: Optional[str] = None,
    search_strategy: Optional[str] = None,
    subscription_id: Optional[str] = None,
) -> Dict[str, Any]:
    """
    Get personalized news feed by pulling from news_items table first, then research history.

    Args:
        user_id: User identifier
        limit: Maximum number of cards to return
        use_cache: Whether to use cached news
        focus: Optional focus area for news
        search_strategy: Override default recommendation strategy

    Returns:
        Dictionary with news items and metadata
    """
    # Validate limit - allow any positive number
    if limit < 1:
        raise InvalidLimitException(limit)

    try:
        logger.info(
            f"get_news_feed called with user_id={user_id}, limit={limit}"
        )

        # News is always enabled for now - per-user settings will be handled later
        # if not get_db_setting("news.enabled", True):
        #     return {"error": "News system is disabled", "news_items": []}

        # Import database functions
        from ..database.session_context import get_user_db_session
        from ..database.models import ResearchHistory

        news_items = []
        remaining_limit = limit

        # Query research history from user's database for news items
        logger.info("Getting news items from research history")
        try:
            # Use the user_id provided to the function
            with get_user_db_session(user_id) as db_session:
                # Build query using ORM
                query = db_session.query(ResearchHistory).filter(
                    ResearchHistory.status == ResearchStatus.COMPLETED
                )

                # Filter by subscription if provided
                if subscription_id and subscription_id != "all":
                    # Use JSON containment for PostgreSQL or LIKE for SQLite
                    query = query.filter(
                        ResearchHistory.research_meta.like(
                            f'%"subscription_id":"{subscription_id}"%'
                        )
                    )

                # Order by creation date and limit
                results = (
                    query.order_by(ResearchHistory.created_at.desc())
                    .limit(remaining_limit * 2)
                    .all()
                )

                # Convert ORM objects to dictionaries for compatibility
                results = [
                    {
                        "id": r.id,
                        "uuid_id": r.id,  # In ResearchHistory, id is the UUID
                        "query": r.query,
                        "title": r.title
                        if hasattr(r, "title")
                        else None,  # Include title field if exists
                        "created_at": r.created_at if r.created_at else None,
                        "completed_at": r.completed_at
                        if r.completed_at
                        else None,
                        "duration_seconds": r.duration_seconds
                        if hasattr(r, "duration_seconds")
                        else None,
                        "report_path": r.report_path
                        if hasattr(r, "report_path")
                        else None,
                        "report_content": r.report_content
                        if hasattr(r, "report_content")
                        else None,  # Include database content
                        "research_meta": r.research_meta,
                        "status": r.status,
                    }
                    for r in results
                ]

            logger.info(f"Database returned {len(results)} research items")
            if results and len(results) > 0:
                logger.info(f"First row keys: {list(results[0].keys())}")
                # Log first few items' metadata
                for i, row in enumerate(results[:3]):
                    logger.info(
                        f"Item {i}: query='{row['query'][:50]}...', has meta={bool(row.get('research_meta'))}"
                    )

            # Process results to find news items
            processed_count = 0
            error_count = 0

            for row in results:
                try:
                    # Parse metadata
                    metadata = {}
                    if row.get("research_meta"):
                        try:
                            # Handle both dict and string formats
                            if isinstance(row["research_meta"], dict):
                                metadata = row["research_meta"]
                            else:
                                metadata = json.loads(row["research_meta"])
                        except (json.JSONDecodeError, TypeError):
                            logger.exception("Error parsing metadata")
                            metadata = {}

                    # Check if this has news metadata (generated_headline or generated_topics)
                    # or if it's a news-related query
                    has_news_metadata = (
                        metadata.get("generated_headline") is not None
                        or metadata.get("generated_topics") is not None
                    )

                    query_lower = row["query"].lower()
                    is_news_query = (
                        has_news_metadata
                        or metadata.get("is_news_search")
                        or metadata.get("search_type") == "news_analysis"
                        or "breaking news" in query_lower
                        or "news stories" in query_lower
                        or (
                            "today" in query_lower
                            and (
                                "news" in query_lower
                                or "breaking" in query_lower
                            )
                        )
                        or "latest news" in query_lower
                    )

                    # Log the decision for first few items
                    if processed_count < 3 or error_count < 3:
                        logger.info(
                            f"Item check: query='{row['query'][:30]}...', is_news_search={metadata.get('is_news_search')}, "
                            f"has_news_metadata={has_news_metadata}, is_news_query={is_news_query}"
                        )

                    # Only show items that have news metadata or are news queries
                    if is_news_query:
                        processed_count += 1
                        logger.info(
                            f"Processing research item #{processed_count}: {row['query'][:50]}..."
                        )

                        # Always use database content
                        findings = ""
                        summary = ""
                        report_content_db = row.get(
                            "report_content"
                        )  # Get database content

                        # Use database content
                        content = report_content_db
                        if content:
                            logger.debug(
                                f"Using database content for research {row['id']}"
                            )

                            # Process database content
                            lines = content.split("\n") if content else []
                            # Use full content as findings
                            findings = content
                            # Extract summary from first non-empty line
                            for line in lines:
                                if line.strip() and not line.startswith("#"):
                                    summary = line.strip()
                                    break
                        else:
                            logger.debug(
                                f"No database content for research {row['id']}"
                            )

                        # Use stored headline/topics if available, otherwise generate
                        original_query = row["query"]

                        # Check for headline - first try database title, then metadata
                        headline = row.get("title") or metadata.get(
                            "generated_headline"
                        )

                        # For subscription results, generate headline from query if needed
                        if not headline and metadata.get("is_news_search"):
                            # Use subscription name or query as headline
                            subscription_name = metadata.get(
                                "subscription_name"
                            )
                            if subscription_name:
                                headline = f"News Update: {subscription_name}"
                            else:
                                # Generate headline from query
                                headline = f"News: {row['query'][:60]}..."

                        # Skip items without meaningful headlines or that are incomplete
                        if (
                            not headline
                            or headline == "[No headline available]"
                        ):
                            logger.debug(
                                f"Skipping item without headline: {row['id']}"
                            )
                            continue

                        # Skip items that are still in progress or suspended
                        if row["status"] in (
                            ResearchStatus.IN_PROGRESS,
                            ResearchStatus.SUSPENDED,
                        ):
                            logger.debug(
                                f"Skipping incomplete item: {row['id']} (status: {row['status']})"
                            )
                            continue

                        # Skip items without content (neither file nor database)
                        if not content:
                            logger.debug(
                                f"Skipping item without content: {row['id']}"
                            )
                            continue

                        # Use ID properly, preferring uuid_id
                        research_id = row.get("uuid_id") or str(row["id"])

                        # Use stored category and topics - no defaults
                        category = metadata.get("category")
                        if not category:
                            category = "[Uncategorized]"

                        topics = metadata.get("generated_topics")
                        if not topics:
                            topics = ["[No topics]"]

                        # Extract top 3 links from the database content
                        links = []
                        if content:
                            try:
                                report_lines = content.split("\n")
                                link_count = 0
                                for i, line in enumerate(
                                    report_lines[:100]
                                ):  # Check first 100 lines for links
                                    if "URL:" in line:
                                        url = line.split("URL:", 1)[1].strip()
                                        if url.startswith("http"):
                                            # Get the title from the previous line if available
                                            title = ""
                                            if i > 0:
                                                title_line = report_lines[
                                                    i - 1
                                                ].strip()
                                                # Remove citation numbers like [12, 26, 19]
                                                title = re.sub(
                                                    r"^\[[^\]]+\]\s*",
                                                    "",
                                                    title_line,
                                                ).strip()

                                            if not title:
                                                # Use domain as fallback
                                                domain = url.split("//")[
                                                    -1
                                                ].split("/")[0]
                                                title = domain.replace(
                                                    "www.", ""
                                                )

                                            links.append(
                                                {
                                                    "url": url,
                                                    "title": title[:50] + "..."
                                                    if len(title) > 50
                                                    else title,
                                                }
                                            )
                                            link_count += 1
                                            logger.debug(
                                                f"Found link: {title} - {url}"
                                            )
                                            if link_count >= 3:
                                                break
                            except Exception:
                                logger.exception(
                                    "Error extracting links from database content"
                                )

                        # Create news item from research
                        news_item = {
                            "id": f"news-{research_id}",
                            "headline": headline,
                            "category": category,
                            "summary": summary
                            or f"Research analysis for: {headline[:100]}",
                            "findings": findings,
                            "impact_score": metadata.get(
                                "impact_score", 0
                            ),  # 0 indicates missing
                            "time_ago": _format_time_ago(row["created_at"]),
                            "upvotes": metadata.get("upvotes", 0),
                            "downvotes": metadata.get("downvotes", 0),
                            "source_url": f"/results/{research_id}",
                            "topics": topics,  # Use generated topics
                            "links": links,  # Add extracted links
                            "research_id": research_id,
                            "created_at": row["created_at"],
                            "duration_seconds": row.get("duration_seconds", 0),
                            "original_query": original_query,  # Keep original query for reference
                            "is_news": metadata.get(
                                "is_news_search", False
                            ),  # Flag for news searches
                            "news_date": metadata.get(
                                "news_date"
                            ),  # If specific date for news
                            "news_source": metadata.get(
                                "news_source"
                            ),  # If from specific source
                            "priority": metadata.get(
                                "priority", "normal"
                            ),  # Priority level
                        }

                        news_items.append(news_item)
                        logger.info(f"Added news item: {headline[:50]}...")

                        if len(news_items) >= limit:
                            break

                except Exception:
                    error_count += 1
                    logger.exception(
                        f"Error processing research item with query: {row.get('query', 'UNKNOWN')[:100]}"
                    )
                    continue

            logger.info(
                f"Processing summary: total_results={len(results)}, processed={processed_count}, "
                f"errors={error_count}, added={len(news_items)}"
            )

            # Log subscription-specific items if we were filtering
            if subscription_id and subscription_id != "all":
                sub_items = [
                    item for item in news_items if item.get("is_news", False)
                ]
                logger.info(
                    f"Subscription {subscription_id}: found {len(sub_items)} items"
                )

        except Exception as db_error:
            logger.exception(f"Database error in research history: {db_error}")
            raise DatabaseAccessException(
                "research_history_query", str(db_error)
            )

        # If no news items found, return empty list
        if not news_items:
            logger.info("No news items found, returning empty list")
            news_items = []

        logger.info(f"Returning {len(news_items)} news items to client")

        # Determine the source
        source = (
            "news_items"
            if any(item.get("is_news", False) for item in news_items)
            else "research_history"
        )

        return {
            "news_items": news_items[:limit],
            "generated_at": datetime.now(timezone.utc).isoformat(),
            "focus": focus,
            "search_strategy": search_strategy or "default",
            "total_items": len(news_items),
            "source": source,
        }

    except NewsAPIException:
        # Re-raise our custom exceptions
        raise
    except Exception as e:
        logger.exception("Error getting news feed")
        raise NewsFeedGenerationException(str(e), user_id=user_id)


def get_subscription_history(
    subscription_id: str, limit: int = 20
) -> Dict[str, Any]:
    """
    Get research history for a specific subscription.

    Args:
        subscription_id: The subscription UUID
        limit: Maximum number of history items to return

    Returns:
        Dict containing subscription info and its research history
    """
    try:
        from ..database.session_context import get_user_db_session
        from ..database.models import ResearchHistory
        from ..database.models.news import NewsSubscription

        # Get subscription details using ORM from user's encrypted database
        with get_user_db_session() as session:
            subscription = (
                session.query(NewsSubscription)
                .filter_by(id=subscription_id)
                .first()
            )

            if not subscription:
                raise SubscriptionNotFoundException(subscription_id)  # noqa: TRY301 — re-raised by except NewsAPIException

            # Convert to dict for response
            subscription_dict = {
                "id": subscription.id,
                "query_or_topic": subscription.query_or_topic,
                "subscription_type": subscription.subscription_type,
                "refresh_interval_minutes": subscription.refresh_interval_minutes,
                "refresh_count": subscription.refresh_count or 0,
                "created_at": subscription.created_at.isoformat()
                if subscription.created_at
                else None,
                "next_refresh": subscription.next_refresh.isoformat()
                if subscription.next_refresh
                else None,
            }

        # Now get research history from the research database
        # Get user_id from subscription
        sub_user_id = subscription_dict.get("user_id", "anonymous")

        with get_user_db_session(sub_user_id) as db_session:
            # Get all research runs that were triggered by this subscription
            # Look for subscription_id in the research_meta JSON
            # Note: JSON format has space after colon
            like_pattern = f'%"subscription_id": "{subscription_id}"%'
            logger.info(
                f"Searching for research history with pattern: {like_pattern}"
            )

            history_items = (
                db_session.query(ResearchHistory)
                .filter(ResearchHistory.research_meta.like(like_pattern))
                .order_by(ResearchHistory.created_at.desc())
                .limit(limit)
                .all()
            )

            # Convert to dict format for compatibility
            history_items = [
                {
                    "id": h.id,
                    "uuid_id": h.uuid_id,
                    "query": h.query,
                    "status": h.status,
                    "created_at": h.created_at.isoformat()
                    if h.created_at
                    else None,
                    "completed_at": h.completed_at.isoformat()
                    if h.completed_at
                    else None,
                    "duration_seconds": h.duration_seconds,
                    "research_meta": h.research_meta,
                    "report_path": h.report_path,
                }
                for h in history_items
            ]

        # Process history items
        processed_history = []
        for item in history_items:
            processed_item = {
                "research_id": item.get("uuid_id") or str(item.get("id")),
                "query": item["query"],
                "status": item["status"],
                "created_at": item["created_at"],
                "completed_at": item.get("completed_at"),
                "duration_seconds": item.get("duration_seconds", 0),
                "url": f"/progress/{item.get('uuid_id') or item.get('id')}",
            }

            # Parse metadata if available to get headline and topics
            if item.get("research_meta"):
                try:
                    meta = json.loads(item["research_meta"])
                    processed_item["triggered_by"] = meta.get(
                        "triggered_by", "subscription"
                    )
                    # Add headline and topics from metadata
                    processed_item["headline"] = meta.get(
                        "generated_headline", "[No headline]"
                    )
                    processed_item["topics"] = meta.get("generated_topics", [])
                except Exception:
                    processed_item["headline"] = "[No headline]"
                    processed_item["topics"] = []
            else:
                processed_item["headline"] = "[No headline]"
                processed_item["topics"] = []

            processed_history.append(processed_item)

        return {
            "subscription": subscription_dict,
            "history": processed_history,
            "total_runs": len(processed_history),
        }

    except NewsAPIException:
        # Re-raise our custom exceptions
        raise
    except Exception as e:
        logger.exception("Error getting subscription history")
        raise DatabaseAccessException("get_subscription_history", str(e))


def _format_time_ago(timestamp: str) -> str:
    """Format timestamp as 'X hours ago' string."""
    try:
        from dateutil import parser
        from loguru import logger

        dt = parser.parse(timestamp)

        # If dt is naive, assume it's in UTC
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)

        now = datetime.now(timezone.utc)
        diff = now - dt

        if diff.days > 0:
            return f"{diff.days} day{'s' if diff.days > 1 else ''} ago"
        if diff.seconds > 3600:
            hours = diff.seconds // 3600
            return f"{hours} hour{'s' if hours > 1 else ''} ago"
        if diff.seconds > 60:
            minutes = diff.seconds // 60
            return f"{minutes} minute{'s' if minutes > 1 else ''} ago"
        return "Just now"
    except Exception:
        logger.exception(f"Error parsing timestamp '{timestamp}'")
        return "Recently"


def get_subscription(subscription_id: str) -> Optional[Dict[str, Any]]:
    """
    Get a single subscription by ID.

    Args:
        subscription_id: Subscription identifier

    Returns:
        Dictionary with subscription data or None if not found
    """
    try:
        # Get subscription directly from user's encrypted database
        from ..database.session_context import get_user_db_session
        from ..database.models.news import NewsSubscription

        with get_user_db_session() as db_session:
            subscription = (
                db_session.query(NewsSubscription)
                .filter_by(id=subscription_id)
                .first()
            )

            if not subscription:
                raise SubscriptionNotFoundException(subscription_id)  # noqa: TRY301 — re-raised by except NewsAPIException

            # Convert to API format matching the template expectations
            return {
                "id": subscription.id,
                "name": subscription.name or "",
                "query_or_topic": subscription.query_or_topic,
                "subscription_type": subscription.subscription_type,
                "refresh_interval_minutes": subscription.refresh_interval_minutes,
                "is_active": subscription.status == "active",
                "status": subscription.status,
                "folder_id": subscription.folder_id,
                "model_provider": subscription.model_provider,
                "model": subscription.model,
                "search_strategy": subscription.search_strategy,
                "custom_endpoint": subscription.custom_endpoint,
                "search_engine": subscription.search_engine,
                "search_iterations": subscription.search_iterations or 3,
                "questions_per_iteration": subscription.questions_per_iteration
                or 5,
                "created_at": subscription.created_at.isoformat()
                if subscription.created_at
                else None,
                "updated_at": subscription.updated_at.isoformat()
                if subscription.updated_at
                else None,
            }

    except NewsAPIException:
        # Re-raise our custom exceptions
        raise
    except Exception as e:
        logger.exception(f"Error getting subscription {subscription_id}")
        raise DatabaseAccessException("get_subscription", str(e))


def get_subscriptions(user_id: str) -> Dict[str, Any]:
    """
    Get all subscriptions for a user.

    Args:
        user_id: User identifier

    Returns:
        Dictionary with subscriptions list
    """
    try:
        # Get subscriptions directly from user's encrypted database
        from ..database.session_context import get_user_db_session
        from ..database.models import ResearchHistory
        from ..database.models.news import NewsSubscription
        from sqlalchemy import func

        sub_list = []

        with get_user_db_session(user_id) as db_session:
            # Query all subscriptions for this user
            subscriptions = db_session.query(NewsSubscription).all()

            for sub in subscriptions:
                # Count actual research runs for this subscription
                like_pattern = f'%"subscription_id": "{sub.id}"%'
                total_runs = (
                    db_session.query(func.count(ResearchHistory.id))
                    .filter(ResearchHistory.research_meta.like(like_pattern))
                    .scalar()
                    or 0
                )

                # Convert ORM object to API format
                sub_dict = {
                    "id": sub.id,
                    "query": sub.query_or_topic,
                    "type": sub.subscription_type,
                    "refresh_minutes": sub.refresh_interval_minutes,
                    "created_at": sub.created_at.isoformat()
                    if sub.created_at
                    else None,
                    "next_refresh": sub.next_refresh.isoformat()
                    if sub.next_refresh
                    else None,
                    "last_refreshed": sub.last_refresh.isoformat()
                    if sub.last_refresh
                    else None,
                    "is_active": sub.status == "active",
                    "total_runs": total_runs,  # Use actual count from research_history
                    "name": sub.name or "",
                    "folder_id": sub.folder_id,
                }
                sub_list.append(sub_dict)

        return {"subscriptions": sub_list, "total": len(sub_list)}

    except Exception as e:
        logger.exception("Error getting subscriptions")
        raise DatabaseAccessException("get_subscriptions", str(e))


def update_subscription(
    subscription_id: str, data: Dict[str, Any]
) -> Dict[str, Any]:
    """
    Update an existing subscription.

    Args:
        subscription_id: Subscription identifier
        data: Dictionary with fields to update

    Returns:
        Dictionary with updated subscription data
    """
    try:
        from ..database.session_context import get_user_db_session
        from ..database.models.news import NewsSubscription
        from datetime import datetime, timedelta

        with get_user_db_session() as db_session:
            # Get existing subscription
            subscription = (
                db_session.query(NewsSubscription)
                .filter_by(id=subscription_id)
                .first()
            )
            if not subscription:
                raise SubscriptionNotFoundException(subscription_id)  # noqa: TRY301 — re-raised by except NewsAPIException

            # Update fields
            if "name" in data:
                subscription.name = data["name"]
            if "query_or_topic" in data:
                subscription.query_or_topic = data["query_or_topic"]
            if "subscription_type" in data:
                subscription.subscription_type = data["subscription_type"]
            if "refresh_interval_minutes" in data:
                old_interval = subscription.refresh_interval_minutes
                subscription.refresh_interval_minutes = data[
                    "refresh_interval_minutes"
                ]
                # Recalculate next_refresh if interval changed
                if old_interval != subscription.refresh_interval_minutes:
                    subscription.next_refresh = datetime.now(UTC) + timedelta(
                        minutes=subscription.refresh_interval_minutes
                    )
            if "is_active" in data:
                subscription.status = (
                    "active" if data["is_active"] else "paused"
                )
            if "status" in data:
                subscription.status = data["status"]
            if "folder_id" in data:
                subscription.folder_id = data["folder_id"]
            if "model_provider" in data:
                subscription.model_provider = normalize_provider(
                    data["model_provider"]
                )
            if "model" in data:
                subscription.model = data["model"]
            if "search_strategy" in data:
                subscription.search_strategy = data["search_strategy"]
            if "custom_endpoint" in data:
                subscription.custom_endpoint = data["custom_endpoint"]
            if "search_engine" in data:
                subscription.search_engine = data["search_engine"]
            if "search_iterations" in data:
                subscription.search_iterations = data["search_iterations"]
            if "questions_per_iteration" in data:
                subscription.questions_per_iteration = data[
                    "questions_per_iteration"
                ]

            # Update timestamp
            subscription.updated_at = datetime.now(UTC)

            # Commit changes
            db_session.commit()

            # Notify scheduler about updated subscription
            _notify_scheduler_about_subscription_change("updated")

            # Convert to API format
            return {
                "status": "success",
                "subscription": {
                    "id": subscription.id,
                    "name": subscription.name or "",
                    "query_or_topic": subscription.query_or_topic,
                    "subscription_type": subscription.subscription_type,
                    "refresh_interval_minutes": subscription.refresh_interval_minutes,
                    "is_active": subscription.status == "active",
                    "status": subscription.status,
                    "folder_id": subscription.folder_id,
                    "model_provider": subscription.model_provider,
                    "model": subscription.model,
                    "search_strategy": subscription.search_strategy,
                    "custom_endpoint": subscription.custom_endpoint,
                    "search_engine": subscription.search_engine,
                    "search_iterations": subscription.search_iterations or 3,
                    "questions_per_iteration": subscription.questions_per_iteration
                    or 5,
                },
            }

    except NewsAPIException:
        # Re-raise our custom exceptions
        raise
    except Exception as e:
        logger.exception("Error updating subscription")
        raise SubscriptionUpdateException(subscription_id, str(e))


def create_subscription(
    user_id: str,
    query: str,
    subscription_type: str = "search",
    refresh_minutes: Optional[int] = None,
    source_research_id: Optional[str] = None,
    model_provider: Optional[str] = None,
    model: Optional[str] = None,
    search_strategy: Optional[str] = None,
    custom_endpoint: Optional[str] = None,
    name: Optional[str] = None,
    folder_id: Optional[str] = None,
    is_active: bool = True,
    search_engine: Optional[str] = None,
    search_iterations: Optional[int] = None,
    questions_per_iteration: Optional[int] = None,
) -> Dict[str, Any]:
    """
    Create a new subscription for user.

    Args:
        user_id: User identifier
        query: Search query or topic
        subscription_type: "search" or "topic"
        refresh_minutes: Refresh interval in minutes

    Returns:
        Dictionary with subscription details
    """
    try:
        from ..database.session_context import get_user_db_session
        from ..database.models.news import NewsSubscription
        from datetime import datetime, timedelta
        import uuid

        # Get default refresh interval from settings if not provided
        # NOTE: This API function accesses the settings DB for convenience when used
        # within the Flask application context. For programmatic API access outside
        # the web context, callers should provide refresh_minutes explicitly to avoid
        # dependency on the settings database being initialized.

        with get_user_db_session(user_id) as db_session:
            if refresh_minutes is None:
                try:
                    from ..utilities.db_utils import get_settings_manager

                    settings_manager = get_settings_manager(db_session, user_id)
                    refresh_minutes = settings_manager.get_setting(
                        "news.subscription.refresh_minutes", 240
                    )
                except (ImportError, AttributeError, TypeError):
                    # Fallback for when settings DB is not available (e.g., programmatic API usage)
                    logger.debug(
                        "Settings manager not available, using default refresh_minutes"
                    )
                    refresh_minutes = 240  # Default to 4 hours
            # Create new subscription
            subscription = NewsSubscription(
                id=str(uuid.uuid4()),
                name=name,
                query_or_topic=query,
                subscription_type=subscription_type,
                refresh_interval_minutes=refresh_minutes,
                status="active" if is_active else "paused",
                model_provider=normalize_provider(model_provider),
                model=model,
                search_strategy=search_strategy or "news_aggregation",
                custom_endpoint=custom_endpoint,
                folder_id=folder_id,
                search_engine=search_engine,
                search_iterations=search_iterations,
                questions_per_iteration=questions_per_iteration,
                created_at=datetime.now(UTC),
                updated_at=datetime.now(UTC),
                last_refresh=None,
                next_refresh=datetime.now(UTC)
                + timedelta(minutes=refresh_minutes),
                source_id=source_research_id,
            )

            # Add to database
            db_session.add(subscription)
            db_session.commit()

            # Notify scheduler about new subscription
            _notify_scheduler_about_subscription_change("created", user_id)

            return {
                "status": "success",
                "subscription_id": subscription.id,
                "type": subscription_type,
                "query": query,
                "refresh_minutes": refresh_minutes,
            }

    except Exception as e:
        logger.exception("Error creating subscription")
        raise SubscriptionCreationException(
            str(e), {"query": query, "type": subscription_type}
        )


def delete_subscription(subscription_id: str) -> Dict[str, Any]:
    """
    Delete a subscription.

    Args:
        subscription_id: ID of subscription to delete

    Returns:
        Dictionary with status
    """
    try:
        from ..database.session_context import get_user_db_session
        from ..database.models.news import NewsSubscription

        with get_user_db_session() as db_session:
            subscription = (
                db_session.query(NewsSubscription)
                .filter_by(id=subscription_id)
                .first()
            )
            if subscription:
                db_session.delete(subscription)
                db_session.commit()

                # Notify scheduler about deleted subscription
                _notify_scheduler_about_subscription_change("deleted")

                return {"status": "success", "deleted": subscription_id}
            raise SubscriptionNotFoundException(subscription_id)  # noqa: TRY301 — re-raised by except NewsAPIException
    except NewsAPIException:
        # Re-raise our custom exceptions
        raise
    except Exception as e:
        logger.exception("Error deleting subscription")
        raise SubscriptionDeletionException(subscription_id, str(e))


def get_votes_for_cards(card_ids: list, user_id: str) -> Dict[str, Any]:
    """
    Get vote counts and user's votes for multiple news cards.

    Args:
        card_ids: List of card IDs to get votes for
        user_id: User identifier (not used - per-user database)

    Returns:
        Dictionary with vote information for each card
    """
    from flask import session as flask_session, has_request_context
    from ..database.models.news import UserRating, RatingType
    from ..database.session_context import get_user_db_session

    # Resolve username before try block
    if not has_request_context():
        # If called outside of request context (e.g., in tests), use user_id directly
        username = user_id if user_id else None
        if not username:
            raise ValueError("No username provided and no request context")
    else:
        # Get username from session
        username = flask_session.get("username")
        if not username:
            raise ValueError("No username in session")

    try:
        # Get database session
        with get_user_db_session(username) as db:
            results = {}

            for card_id in card_ids:
                # Get user's vote for this card
                user_vote = (
                    db.query(UserRating)
                    .filter_by(
                        card_id=card_id, rating_type=RatingType.RELEVANCE
                    )
                    .first()
                )

                # Count total votes for this card
                upvotes = (
                    db.query(UserRating)
                    .filter_by(
                        card_id=card_id,
                        rating_type=RatingType.RELEVANCE,
                        rating_value="up",
                    )
                    .count()
                )

                downvotes = (
                    db.query(UserRating)
                    .filter_by(
                        card_id=card_id,
                        rating_type=RatingType.RELEVANCE,
                        rating_value="down",
                    )
                    .count()
                )

                results[card_id] = {
                    "upvotes": upvotes,
                    "downvotes": downvotes,
                    "user_vote": user_vote.rating_value if user_vote else None,
                }

            return {"success": True, "votes": results}

    except Exception:
        logger.exception("Error getting votes for cards")
        raise


def submit_feedback(card_id: str, user_id: str, vote: str) -> Dict[str, Any]:
    """
    Submit feedback (vote) for a news card.

    Args:
        card_id: ID of the news card
        user_id: User identifier (not used - per-user database)
        vote: "up" or "down"

    Returns:
        Dictionary with updated vote counts
    """
    from flask import session as flask_session, has_request_context
    from sqlalchemy_utc import utcnow
    from ..database.models.news import UserRating, RatingType
    from ..database.session_context import get_user_db_session

    # Validate vote value
    if vote not in ["up", "down"]:
        raise ValueError(f"Invalid vote type: {vote}")

    # Resolve username before try block
    if not has_request_context():
        # If called outside of request context (e.g., in tests), use user_id directly
        username = user_id if user_id else None
        if not username:
            raise ValueError("No username provided and no request context")
    else:
        # Get username from session
        username = flask_session.get("username")
        if not username:
            raise ValueError("No username in session")

    try:
        # Get database session
        with get_user_db_session(username) as db:
            # We don't check if the card exists in the database since news items
            # are generated dynamically and may not be stored as NewsCard entries

            # Check if user already voted on this card
            existing_rating = (
                db.query(UserRating)
                .filter_by(card_id=card_id, rating_type=RatingType.RELEVANCE)
                .first()
            )

            if existing_rating:
                # Update existing vote
                existing_rating.rating_value = vote
                existing_rating.created_at = utcnow()
            else:
                # Create new rating
                new_rating = UserRating(
                    card_id=card_id,
                    rating_type=RatingType.RELEVANCE,
                    rating_value=vote,
                )
                db.add(new_rating)

            db.commit()

            # Count total votes for this card
            upvotes = (
                db.query(UserRating)
                .filter_by(
                    card_id=card_id,
                    rating_type=RatingType.RELEVANCE,
                    rating_value="up",
                )
                .count()
            )

            downvotes = (
                db.query(UserRating)
                .filter_by(
                    card_id=card_id,
                    rating_type=RatingType.RELEVANCE,
                    rating_value="down",
                )
                .count()
            )

            logger.info(
                f"Feedback submitted for card {card_id}: {vote} (up: {upvotes}, down: {downvotes})"
            )

            return {
                "success": True,
                "card_id": card_id,
                "vote": vote,
                "upvotes": upvotes,
                "downvotes": downvotes,
            }

    except Exception:
        logger.exception(f"Error submitting feedback for card {card_id}")
        raise


def research_news_item(card_id: str, depth: str = "quick") -> Dict[str, Any]:
    """
    Perform deeper research on a news item.

    Args:
        card_id: ID of the news card to research
        depth: Research depth - "quick", "detailed", or "report"

    Returns:
        Dictionary with research results
    """
    # TODO: Implement with per-user database for cards
    logger.warning(
        "research_news_item not yet implemented with per-user databases"
    )
    raise NotImplementedException("research_news_item")


def save_news_preferences(
    user_id: str, preferences: Dict[str, Any]
) -> Dict[str, Any]:
    """
    Save user preferences for news.

    Args:
        user_id: User identifier
        preferences: Dictionary of preferences to save

    Returns:
        Dictionary with status and message
    """
    # TODO: Implement with per-user database for preferences
    logger.warning(
        "save_news_preferences not yet implemented with per-user databases"
    )
    raise NotImplementedException("save_news_preferences")


def get_news_categories() -> Dict[str, Any]:
    """
    Get available news categories with counts.

    Returns:
        Dictionary with categories and statistics
    """
    # TODO: Implement with per-user database for categories
    logger.warning(
        "get_news_categories not yet implemented with per-user databases"
    )
    raise NotImplementedException("get_news_categories")
