from datetime import datetime, timezone

from repo_core import BaseRepository

from .model import WebDiscoveredUrl, WebUrlStatus, WebUrlType


class WebDiscoveredUrlRepository(BaseRepository[WebDiscoveredUrl]):
    """Repository for discovered URLs — queuing and lifecycle management."""

    TABLE = "web_discovered_urls"
    _backends = {"db"}

    async def list_enabled(self, limit: int = 100):
        """List all enabled seeds (alias for web-engine compatibility)."""
        return await self.find(self.TABLE, {}, limit=limit)

    async def find_due_for_crawl(self, seed_id: str, limit: int | None = 50):
        """Find URLs due for first crawl or revisit.

        Returns URLs where status is NEW (never dispatched) or
        FETCHED / FAILED (due for revisit) and next_crawl <= now.
        QUEUED and FETCHING are excluded — those are already in-flight.
        """
        now = datetime.now(timezone.utc).isoformat()
        query_kwargs = {"order_by": ["fair_priority", "next_crawl"]}
        if limit is not None:
            query_kwargs["limit"] = limit

        return await self.find(
            self.TABLE,
            {
                "seed_id": seed_id,
                "status": (
                    "IN",
                    [
                        WebUrlStatus.NEW.value,
                        WebUrlStatus.FETCHED.value,
                        WebUrlStatus.FAILED.value,
                    ],
                ),
                "next_crawl": ("<=", now),
            },
            **query_kwargs,
        )

    async def count_inflight(self, seed_id: str) -> int:
        """Count URLs currently being fetched (QUEUED or FETCHING)."""
        return await self.count(
            self.TABLE,
            {
                "seed_id": seed_id,
                "status": ("IN", [WebUrlStatus.QUEUED.value, WebUrlStatus.FETCHING.value]),
            },
        )

    async def mark_queued(self, url_id: str, priority: int | None = None):
        """Mark URL as QUEUED before dispatching to web-fetcher."""
        if priority is None:
            current = await self.find_by_id(self.TABLE, url_id)
            priority = getattr(current, "priority", None) or 100

        return await self.update_by_id(
            self.TABLE,
            url_id,
            {
                "status": WebUrlStatus.QUEUED,
                "attempt_number": 0,
                "fair_priority": priority,
                "updated_at": datetime.now(timezone.utc).isoformat(),
            },
        )

    async def mark_attempted(self, url_id: str, attempt_number: int, fair_priority: int):
        """Record that a URL was eligible but skipped for this dispatch tick."""
        return await self.update_by_id(
            self.TABLE,
            url_id,
            {
                "attempt_number": attempt_number,
                "fair_priority": fair_priority,
                "updated_at": datetime.now(timezone.utc).isoformat(),
            },
        )

    async def mark_fetched_with_next(self, url_id: str, next_crawl: str):
        """Mark URL as FETCHED and schedule next revisit."""
        return await self.update_by_id(
            self.TABLE,
            url_id,
            {
                "status": WebUrlStatus.FETCHED,
                "next_crawl": next_crawl,
                "updated_at": datetime.now(timezone.utc).isoformat(),
            },
        )

    async def mark_failed(self, url_id: str, next_crawl: str | None = None):
        """Mark URL as FAILED and optionally schedule the next revisit."""
        payload = {
            "status": WebUrlStatus.FAILED,
            "updated_at": datetime.now(timezone.utc).isoformat(),
        }
        if next_crawl is not None:
            payload["next_crawl"] = next_crawl

        return await self.update_by_id(
            self.TABLE,
            url_id,
            payload,
        )

    async def mark_status(self, url_id: str, status: str):
        """Compatibility helper for callers that set an explicit URL status."""
        return await self.update_by_id(
            self.TABLE,
            url_id,
            {"status": status, "updated_at": datetime.now(timezone.utc).isoformat()},
        )

    async def find_by_url(self, url: str):
        """Find a URL by its normalized URL string (for dedup)."""
        results = await self.find(
            self.TABLE,
            {"url": url},
            limit=1,
        )
        return results[0] if results else None

    async def update_url_type(self, url_id: str, url_type: WebUrlType):
        """Update the URL content type classification."""
        return await self.update_by_id(
            self.TABLE,
            url_id,
            {"url_type": url_type.value, "updated_at": datetime.now(timezone.utc).isoformat()},
        )

    async def update_url_classification(
        self, url_id: str, url_type: WebUrlType, metadata_merge: dict | None = None,
    ):
        """Update ``url_type`` and optionally merge into ``metadata``.

        Used when first classifying a URL as downloadable — stores
        ``media_type`` and other context in the ``metadata`` JSON
        column so the scheduler can route correctly on revisit.
        """
        values: dict[str, Any] = {
            "url_type": url_type.value,
            "updated_at": datetime.now(timezone.utc).isoformat(),
        }
        if metadata_merge:
            values["metadata"] = metadata_merge
        return await self.update_by_id(self.TABLE, url_id, values)

    async def find_by_urls(self, urls: list[str]):
        """Find existing URLs from a batch (for batch dedup).

        Returns only the rows that match — callers should extract
        the ``url`` field to build an existing-URL set.
        """
        if not urls:
            return []
        return await self.find(
            self.TABLE,
            {"url": ("IN", urls)},
        )

    async def count_queued_for_seeds(self, seed_ids: list[str]) -> int:
        """Count QUEUED URLs across a set of seeds.

        Used by the recovery monitor to detect stuck pipelines:
        if no dispatches happen for 2 minutes yet QUEUED URLs exist,
        the downstream pipeline is stalled and recovery is needed.
        """
        if not seed_ids:
            return 0
        return await self.count(
            self.TABLE,
            {
                "seed_id": ("IN", seed_ids),
                "status": WebUrlStatus.QUEUED.value,
            },
        )

    async def reset_queued_to_new(self, seed_ids: list[str]) -> int:
        """Bulk-reset all QUEUED URLs to NEW for a set of seeds.

        Returns the number of rows updated.  Used during stuck-pipeline
        recovery to re-enqueue URLs so the scheduler can re-dispatch them.
        """
        if not seed_ids:
            return 0
        return await self.update(
            self.TABLE,
            {
                "seed_id": ("IN", seed_ids),
                "status": WebUrlStatus.QUEUED.value,
            },
            {
                "status": WebUrlStatus.NEW.value,
                "updated_at": datetime.now(timezone.utc).isoformat(),
            },
        )
