"""FC-3g: backend support for the Firefox extension.

`ExtensionService.quick_add_source(url)` derives platform + artist
slug from a URL using regex patterns mirrored from
extension/lib/platforms.js, then find-or-creates Artist + Source rows
and returns a JSON-shaped dict for the API layer.
"""

from __future__ import annotations

import re

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from ..models import Artist, Source
from ..utils.slug import slugify
from .db_helpers import get_or_create
from .source_service import BACKFILL_MAX_CHUNKS


class UnknownPlatformError(Exception):
    """URL didn't match any platform pattern."""


class InvalidUrlError(Exception):
    """URL was empty or missing a scheme."""


# Mirrored byte-for-byte from extension/lib/platforms.js
# PLATFORM_ARTIST_PATTERNS. Keep these two copies in sync by hand —
# reviewers catch drift.
_PLATFORM_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
    ("patreon", re.compile(
        r"^https?://(?:www\.)?patreon\.com/"
        r"(?!home$|search\b|messages\b|notifications\b|library\b|settings\b|posts\b|c/)"
        r"(?P<slug>[^/?#]+)/?$",
        re.IGNORECASE,
    )),
    ("subscribestar", re.compile(
        r"^https?://(?:www\.)?subscribestar\.(?:com|adult)/"
        r"(?!feed$|messages$|library$)"
        r"(?P<slug>[^/?#]+)/?$",
        re.IGNORECASE,
    )),
    ("hentaifoundry", re.compile(
        r"^https?://(?:www\.)?hentai-foundry\.com/user/(?P<slug>[^/?#]+)",
        re.IGNORECASE,
    )),
    ("deviantart", re.compile(
        r"^https?://(?:www\.)?deviantart\.com/"
        r"(?!home$|watch\b|tag\b|browse\b)"
        r"(?P<slug>[^/?#]+)/?$",
        re.IGNORECASE,
    )),
    ("pixiv", re.compile(
        r"^https?://(?:www\.)?pixiv\.net/(?:en/)?users/(?P<slug>\d+)",
        re.IGNORECASE,
    )),
]


class ExtensionService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session

    async def quick_add_source(self, url: str) -> dict:
        platform, raw_slug = self._derive(url)
        artist, created_artist = await self._find_or_create_artist(raw_slug)
        source, created_source = await self._find_or_create_source(
            artist_id=artist.id, platform=platform, url=url,
        )
        return {
            "source": {
                "id": source.id,
                "artist_id": source.artist_id,
                "platform": source.platform,
                "url": source.url,
                "enabled": source.enabled,
            },
            "artist": {
                "id": artist.id,
                "name": artist.name,
                "slug": artist.slug,
            },
            "created_source": created_source,
            "created_artist": created_artist,
        }

    async def probe(self, url: str) -> dict:
        """Read-only resolution of a creator-page URL against the FC DB.
        Returns one of:
          - {state: 'unknown_platform'}            — URL didn't match any
            platform's strict artist-page pattern
          - {state: 'new',          platform, slug} — would create both
            artist and source on quick-add
          - {state: 'artist_match', platform, slug, artist}
                                                   — artist exists, this
            exact URL isn't a Source yet (collapses the sidecar-synthetic
            case too — the synthetic anchor counts as an existing artist
            row but not as a pollable Source for this URL)
          - {state: 'source_match', platform, slug, artist, source}
                                                   — exact (artist, platform,
            url) Source already exists

        Side-effect-free: two SELECTs at most.
        """
        try:
            platform, raw_slug = self._derive(url)
        except (UnknownPlatformError, InvalidUrlError):
            return {"state": "unknown_platform"}

        slug = slugify(raw_slug)
        artist = (await self.session.execute(
            select(Artist).where(Artist.slug == slug)
        )).scalar_one_or_none()
        if artist is None:
            return {"state": "new", "platform": platform, "slug": slug}

        artist_payload = {"id": artist.id, "name": artist.name, "slug": artist.slug}

        source = (await self.session.execute(
            select(Source).where(
                Source.artist_id == artist.id,
                Source.platform == platform,
                Source.url == url,
            )
        )).scalar_one_or_none()
        if source is None:
            return {
                "state": "artist_match",
                "platform": platform,
                "slug": slug,
                "artist": artist_payload,
            }

        return {
            "state": "source_match",
            "platform": platform,
            "slug": slug,
            "artist": artist_payload,
            "source": {
                "id": source.id,
                "artist_id": source.artist_id,
                "platform": source.platform,
                "url": source.url,
                "enabled": source.enabled,
            },
        }

    def _derive(self, url: str) -> tuple[str, str]:
        if not isinstance(url, str) or not url.strip():
            raise InvalidUrlError("url is empty")
        if not url.startswith(("http://", "https://")):
            raise InvalidUrlError(f"url must include http:// or https:// scheme: {url!r}")
        for platform, pattern in _PLATFORM_PATTERNS:
            m = pattern.match(url)
            if m:
                return platform, m.group("slug")
        raise UnknownPlatformError(f"no platform pattern matched {url!r}")

    async def _find_or_create_artist(self, raw_name: str) -> tuple[Artist, bool]:
        """Race-safe find-or-create on Artist by slug. Mirrors the
        savepoint + IntegrityError recovery pattern used in
        Importer._find_or_create_source/post (see
        reference_scalar_one_or_none_duplicates memory). Without this,
        two concurrent quick-add-source calls hitting the same artist
        would both miss the existence check and the second INSERT would
        500 against uq_artist_slug.
        """
        slug = slugify(raw_name)

        async def _create() -> Artist:
            artist = Artist(name=raw_name, slug=slug, is_subscription=True)
            self.session.add(artist)
            await self.session.flush()
            return artist

        return await get_or_create(
            self.session, select(Artist).where(Artist.slug == slug), _create
        )

    async def _find_or_create_source(
        self, *, artist_id: int, platform: str, url: str,
    ) -> tuple[Source, bool]:
        """Race-safe — same pattern as _find_or_create_artist above. The
        uq_source_artist_platform_url constraint catches the duplicate
        insert; we roll the savepoint back and re-select."""
        select_existing = select(Source).where(
            Source.artist_id == artist_id,
            Source.platform == platform,
            Source.url == url,
        )

        async def _create() -> Source:
            # New subscription sources arm run-until-done backfill (plan #693)
            # so the first ticks walk the full history (otherwise gallery-dl's
            # exit:20 short-circuits before the archive is built). Mirrors
            # SourceService.create — without it, Firefox quick-add on a creator
            # with >20 unsynced posts would surface as "check failed" with no
            # diagnosis. Audit 2026-06-02.
            src = Source(
                artist_id=artist_id, platform=platform,
                url=url, enabled=True,
                config_overrides={"_backfill_state": "running"},
                backfill_runs_remaining=BACKFILL_MAX_CHUNKS,
            )
            self.session.add(src)
            await self.session.flush()
            return src

        src, created = await get_or_create(
            self.session, select_existing, _create
        )
        if created:
            await self.session.commit()
        return src, created
