import re
import uuid

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

from app.models import Image, ImageTag, Tag

_TAG_PATTERN = re.compile(r"^[a-z0-9_-]{1,64}$")


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

    @staticmethod
    def normalise(name: str) -> str:
        return name.strip().lower()

    @staticmethod
    def normalise_and_validate(name: str) -> str:
        normalised = name.strip().lower()
        if not _TAG_PATTERN.match(normalised):
            raise ValueError(
                f"Invalid tag '{name}': must match ^[a-z0-9_-]{{1,64}}$ after normalisation"
            )
        return normalised

    async def upsert_by_name(self, name: str) -> Tag:
        result = await self._session.execute(select(Tag).where(Tag.name == name))
        tag = result.scalar_one_or_none()
        if tag is None:
            tag = Tag(name=name)
            self._session.add(tag)
            await self._session.flush()
        return tag

    async def get_by_image_id(self, image_id: uuid.UUID) -> list[Tag]:
        result = await self._session.execute(
            select(Tag)
            .join(ImageTag, ImageTag.tag_id == Tag.id)
            .where(ImageTag.image_id == image_id)
            .order_by(Tag.name)
        )
        return result.scalars().all()

    async def attach_tags(self, image: Image, tag_names: list[str]) -> None:
        for name in tag_names:
            tag = await self.upsert_by_name(name)
            existing = await self._session.execute(
                select(ImageTag).where(ImageTag.image_id == image.id, ImageTag.tag_id == tag.id)
            )
            if existing.scalar_one_or_none() is None:
                self._session.add(ImageTag(image_id=image.id, tag_id=tag.id))
        await self._session.flush()

    async def replace_tags_on_image(self, image: Image, tag_names: list[str]) -> None:
        # Remove all existing associations
        existing_links = await self._session.execute(
            select(ImageTag).where(ImageTag.image_id == image.id)
        )
        for link in existing_links.scalars().all():
            await self._session.delete(link)
        await self._session.flush()

        # Add new associations
        for name in tag_names:
            tag = await self.upsert_by_name(name)
            self._session.add(ImageTag(image_id=image.id, tag_id=tag.id))
        await self._session.flush()

    async def list_tags(
        self,
        prefix: str | None = None,
        limit: int = 100,
        offset: int = 0,
        sort: str = "name",
        min_count: int = 0,
    ) -> tuple[list[dict], int]:
        count_subq = (
            select(func.count(ImageTag.image_id))
            .where(ImageTag.tag_id == Tag.id)
            .correlate(Tag)
            .scalar_subquery()
        )

        query = select(Tag, count_subq.label("image_count"))
        if prefix:
            query = query.where(Tag.name.ilike(f"%{prefix}%"))
        if min_count > 0:
            query = query.where(count_subq >= min_count)

        total_query = select(func.count()).select_from(query.subquery())
        total_result = await self._session.execute(total_query)
        total = total_result.scalar_one()

        order = [count_subq.desc(), Tag.name.asc()] if sort == "count_desc" else [Tag.name.asc()]

        paginated = query.order_by(*order).limit(limit).offset(offset)
        rows = await self._session.execute(paginated)

        items = [
            {"id": str(tag.id), "name": tag.name, "image_count": count} for tag, count in rows.all()
        ]
        return items, total
