from typing import Optional

from sqlalchemy import delete, func, select
from sqlalchemy.orm import Session

from vdash.core import Database, NotFoundError, inject_session
from vdash.modules.sql import SqlQuery
from vdash.shared import PaginatedResult, PaginationQuery

from .models import Chart, ChartConversation, ChartMessage
from .schemas import CreateChartRequest, UpdateChartRequest


class ChartService:
    def __init__(self, db: Database):
        self._db = db

    # Conversation
    @inject_session
    async def list_conversations(
        self,
        pagination: PaginationQuery,
        sql_query_uuid: Optional[str] = None,
        session: Optional[Session] = None,
    ) -> PaginatedResult[ChartConversation]:
        assert session is not None

        if pagination.page < 1 or pagination.page_size < 1:
            return PaginatedResult(
                items=[], page=pagination.page, page_size=pagination.page_size, total=0
            )

        offset = (pagination.page - 1) * pagination.page_size

        stmt = select(ChartConversation).join(
            SqlQuery, SqlQuery.id == ChartConversation.sql_query_id
        )

        if sql_query_uuid:
            stmt = stmt.where(SqlQuery.uuid == sql_query_uuid)

        if pagination.q:
            stmt = stmt.where(ChartConversation.title.ilike(f"%{pagination.q}%"))

        count_stmt = select(func.count()).select_from(stmt.subquery())
        total = session.scalar(count_stmt) or 0

        stmt = (
            stmt.order_by(ChartConversation.created_at.desc())
            .offset(offset)
            .limit(pagination.page_size)
        )
        items = session.execute(stmt).scalars().all()

        return PaginatedResult(
            items=items,
            page=pagination.page,
            page_size=pagination.page_size,
            total=total,
        )

    @inject_session
    async def get_conversation(
        self,
        conversation_uuid: str,
        session: Optional[Session] = None,
    ) -> ChartConversation:
        assert session is not None

        conversation = (
            session.execute(
                select(ChartConversation).where(
                    ChartConversation.uuid == conversation_uuid
                )
            )
            .scalars()
            .one_or_none()
        )

        if conversation is None:
            raise NotFoundError(f"Conversation not found: {conversation_uuid}")

        return conversation

    @inject_session
    async def delete_conversation(
        self,
        conversation_uuid: str,
        session: Optional[Session] = None,
    ) -> None:
        assert session is not None

        conversation = (
            session.execute(
                select(ChartConversation).where(
                    ChartConversation.uuid == conversation_uuid
                )
            )
            .scalars()
            .one_or_none()
        )

        if conversation is None:
            raise NotFoundError(f"Conversation not found: {conversation_uuid}")

        # Delete charts linked to messages in this conversation
        message_ids_subq = select(ChartMessage.id).where(
            ChartMessage.conversation_id == conversation.id
        )
        session.execute(delete(Chart).where(Chart.message_id.in_(message_ids_subq)))

        session.execute(
            delete(ChartMessage).where(ChartMessage.conversation_id == conversation.id)
        )
        session.delete(conversation)

    @inject_session
    async def list_messages(
        self,
        conversation_uuid: str,
        pagination: PaginationQuery,
        session: Optional[Session] = None,
    ) -> PaginatedResult[ChartMessage]:
        assert session is not None

        conversation = (
            session.execute(
                select(ChartConversation).where(
                    ChartConversation.uuid == conversation_uuid
                )
            )
            .scalars()
            .one_or_none()
        )

        if conversation is None:
            raise NotFoundError(f"Conversation not found: {conversation_uuid}")

        if pagination.page < 1 or pagination.page_size < 1:
            return PaginatedResult(
                items=[], page=pagination.page, page_size=pagination.page_size, total=0
            )

        offset = (pagination.page - 1) * pagination.page_size

        stmt = select(ChartMessage).where(
            ChartMessage.conversation_id == conversation.id
        )

        count_stmt = select(func.count()).select_from(stmt.subquery())
        total = session.scalar(count_stmt) or 0

        stmt = (
            stmt.order_by(ChartMessage.created_at.desc())
            .offset(offset)
            .limit(pagination.page_size)
        )
        items = session.execute(stmt).scalars().all()

        return PaginatedResult(
            items=items,
            page=pagination.page,
            page_size=pagination.page_size,
            total=total,
        )

    # Chart
    @inject_session
    async def list_charts(
        self,
        pagination: PaginationQuery,
        conversation_uuid: Optional[str] = None,
        session: Optional[Session] = None,
    ) -> PaginatedResult[Chart]:
        assert session is not None

        if pagination.page < 1 or pagination.page_size < 1:
            return PaginatedResult(
                items=[], page=pagination.page, page_size=pagination.page_size, total=0
            )

        offset = (pagination.page - 1) * pagination.page_size

        stmt = select(Chart)

        if conversation_uuid:
            stmt = (
                stmt.join(ChartMessage, ChartMessage.id == Chart.message_id)
                .join(
                    ChartConversation,
                    ChartConversation.id == ChartMessage.conversation_id,
                )
                .where(ChartConversation.uuid == conversation_uuid)
            )

        if pagination.q:
            stmt = stmt.where(Chart.name.ilike(f"%{pagination.q}%"))

        count_stmt = select(func.count()).select_from(stmt.subquery())
        total = session.scalar(count_stmt) or 0

        stmt = (
            stmt.order_by(Chart.created_at.desc())
            .offset(offset)
            .limit(pagination.page_size)
        )
        items = session.execute(stmt).scalars().all()

        return PaginatedResult(
            items=items,
            page=pagination.page,
            page_size=pagination.page_size,
            total=total,
        )

    @inject_session
    async def create_chart(
        self,
        request: CreateChartRequest,
        session: Optional[Session] = None,
    ) -> Chart:
        assert session is not None

        message = (
            session.execute(
                select(ChartMessage).where(ChartMessage.uuid == request.message_uuid)
            )
            .scalars()
            .one_or_none()
        )

        if message is None:
            raise NotFoundError(f"Message not found: {request.message_uuid}")

        sql_query_id = None
        if request.sql_query_uuid:
            sql_query = (
                session.execute(
                    select(SqlQuery).where(SqlQuery.uuid == request.sql_query_uuid)
                )
                .scalars()
                .one_or_none()
            )
            if sql_query is None:
                raise NotFoundError(f"SQL query not found: {request.sql_query_uuid}")
            sql_query_id = sql_query.id

        chart = Chart(
            message_id=message.id,
            sql_query_id=sql_query_id,
            name=request.name,
            vega_chart=request.vega_chart,
        )
        session.add(chart)
        session.flush()

        return chart

    @inject_session
    async def get_chart(
        self,
        chart_uuid: str,
        session: Optional[Session] = None,
    ) -> Chart:
        assert session is not None

        chart = (
            session.execute(select(Chart).where(Chart.uuid == chart_uuid))
            .scalars()
            .one_or_none()
        )

        if chart is None:
            raise NotFoundError(f"Chart not found: {chart_uuid}")

        return chart

    @inject_session
    async def update_chart(
        self,
        chart_uuid: str,
        request: UpdateChartRequest,
        session: Optional[Session] = None,
    ) -> Chart:
        assert session is not None

        chart = (
            session.execute(select(Chart).where(Chart.uuid == chart_uuid))
            .scalars()
            .one_or_none()
        )

        if chart is None:
            raise NotFoundError(f"Chart not found: {chart_uuid}")

        if request.sql_query_uuid is not None:
            sql_query = (
                session.execute(
                    select(SqlQuery).where(SqlQuery.uuid == request.sql_query_uuid)
                )
                .scalars()
                .one_or_none()
            )
            if sql_query is None:
                raise NotFoundError(f"SQL query not found: {request.sql_query_uuid}")
            chart.sql_query_id = sql_query.id

        if request.name is not None:
            chart.name = request.name
        if request.vega_chart is not None:
            chart.vega_chart = request.vega_chart
        session.flush()

        return chart

    @inject_session
    async def delete_chart(
        self,
        chart_uuid: str,
        session: Optional[Session] = None,
    ) -> None:
        assert session is not None

        chart = (
            session.execute(select(Chart).where(Chart.uuid == chart_uuid))
            .scalars()
            .one_or_none()
        )

        if chart is None:
            raise NotFoundError(f"Chart not found: {chart_uuid}")

        session.delete(chart)
