"""SQLAlchemy implementation of ProjectRepository."""

from __future__ import annotations

from collections.abc import Sequence
from typing import cast
from uuid import UUID

from sqlalchemy import and_, func, select

from noteflow.config.constants import (
    RULE_FIELD_APP_MATCH_PATTERNS,
    RULE_FIELD_AUTO_START_ENABLED,
    RULE_FIELD_CALENDAR_MATCH_PATTERNS,
    RULE_FIELD_DEFAULT_FORMAT,
    RULE_FIELD_EXPORT_RULES,
    RULE_FIELD_INCLUDE_AUDIO,
    RULE_FIELD_INCLUDE_TIMESTAMPS,
    RULE_FIELD_TEMPLATE_ID,
    RULE_FIELD_TRIGGER_RULES,
)
from noteflow.domain.entities.project import (
    ExportRules,
    Project,
    ProjectSettings,
    TriggerRules,
)
from noteflow.domain.value_objects import ExportFormat
from noteflow.infrastructure.persistence.models import ProjectModel
from noteflow.infrastructure.persistence.repositories._base import (
    BaseRepository,
    DeleteByIdMixin,
    GetByIdMixin,
    bool_or_none,
    string_list_or_none,
)


class SqlAlchemyProjectRepository(
    BaseRepository,
    GetByIdMixin[ProjectModel, Project],
    DeleteByIdMixin[ProjectModel],
):
    """SQLAlchemy implementation of ProjectRepository.

    Manage projects for organizing meetings within workspaces.
    Uses mixins for standardized get and delete operations.
    """

    _model_class: type[ProjectModel] = ProjectModel

    @staticmethod
    def _settings_to_dict(settings: ProjectSettings) -> dict[str, object]:
        """Convert ProjectSettings to JSONB-storable dict.

        Args:
            settings: ProjectSettings domain object.

        Returns:
            Dictionary for JSONB storage.
        """
        data: dict[str, object] = {}

        if settings.export_rules is not None:
            export_data: dict[str, object] = {}
            if settings.export_rules.default_format is not None:
                export_data[RULE_FIELD_DEFAULT_FORMAT] = settings.export_rules.default_format.value
            if settings.export_rules.include_audio is not None:
                export_data[RULE_FIELD_INCLUDE_AUDIO] = settings.export_rules.include_audio
            if settings.export_rules.include_timestamps is not None:
                export_data[RULE_FIELD_INCLUDE_TIMESTAMPS] = settings.export_rules.include_timestamps
            if settings.export_rules.template_id is not None:
                export_data[RULE_FIELD_TEMPLATE_ID] = str(settings.export_rules.template_id)
            if export_data:
                data[RULE_FIELD_EXPORT_RULES] = export_data

        if settings.trigger_rules is not None:
            trigger_data: dict[str, object] = {}
            if settings.trigger_rules.auto_start_enabled is not None:
                trigger_data[RULE_FIELD_AUTO_START_ENABLED] = settings.trigger_rules.auto_start_enabled
            if settings.trigger_rules.calendar_match_patterns is not None:
                trigger_data[RULE_FIELD_CALENDAR_MATCH_PATTERNS] = settings.trigger_rules.calendar_match_patterns
            if settings.trigger_rules.app_match_patterns is not None:
                trigger_data[RULE_FIELD_APP_MATCH_PATTERNS] = settings.trigger_rules.app_match_patterns
            if trigger_data:
                data[RULE_FIELD_TRIGGER_RULES] = trigger_data

        if settings.rag_enabled is not None:
            data["rag_enabled"] = settings.rag_enabled
        if settings.default_summarization_template is not None:
            data["default_summarization_template"] = settings.default_summarization_template

        return data

    @staticmethod
    def _dict_to_settings(data: dict[str, object]) -> ProjectSettings:
        """Convert JSONB dict to ProjectSettings.

        Args:
            data: Dictionary from JSONB storage.

        Returns:
            ProjectSettings domain object.
        """
        export_rules = None
        trigger_rules = None

        raw_export_data = data.get(RULE_FIELD_EXPORT_RULES)
        if isinstance(raw_export_data, dict):
            export_data = cast(dict[str, object], raw_export_data)
            default_format = None
            raw_default_format = export_data.get(RULE_FIELD_DEFAULT_FORMAT)
            if isinstance(raw_default_format, str):
                default_format = ExportFormat(raw_default_format)
            template_id = None
            raw_template_id = export_data.get(RULE_FIELD_TEMPLATE_ID)
            if isinstance(raw_template_id, str):
                template_id = UUID(raw_template_id)
            export_rules = ExportRules(
                default_format=default_format,
                include_audio=bool_or_none(export_data.get(RULE_FIELD_INCLUDE_AUDIO)),
                include_timestamps=bool_or_none(export_data.get(RULE_FIELD_INCLUDE_TIMESTAMPS)),
                template_id=template_id,
            )

        raw_trigger_data = data.get(RULE_FIELD_TRIGGER_RULES)
        if isinstance(raw_trigger_data, dict):
            trigger_data = cast(dict[str, object], raw_trigger_data)
            trigger_rules = TriggerRules(
                auto_start_enabled=bool_or_none(trigger_data.get(RULE_FIELD_AUTO_START_ENABLED)),
                calendar_match_patterns=string_list_or_none(
                    trigger_data.get(RULE_FIELD_CALENDAR_MATCH_PATTERNS),
                ),
                app_match_patterns=string_list_or_none(
                    trigger_data.get(RULE_FIELD_APP_MATCH_PATTERNS),
                ),
            )

        raw_rag_enabled = data.get("rag_enabled")
        rag_enabled = raw_rag_enabled if isinstance(raw_rag_enabled, bool) else None
        raw_default_template = data.get("default_summarization_template")
        default_template = raw_default_template if isinstance(raw_default_template, str) else None

        return ProjectSettings(
            export_rules=export_rules,
            trigger_rules=trigger_rules,
            rag_enabled=rag_enabled,
            default_summarization_template=default_template,
        )

    def _to_domain(self, model: ProjectModel) -> Project:
        """Convert ORM model to domain entity.

        Args:
            model: SQLAlchemy ProjectModel.

        Returns:
            Domain Project entity.
        """
        settings = self._dict_to_settings(dict(model.settings) if model.settings else {})

        return Project(
            id=model.id,
            workspace_id=model.workspace_id,
            name=model.name,
            slug=model.slug,
            description=model.description,
            is_default=model.is_default,
            settings=settings,
            created_at=model.created_at,
            updated_at=model.updated_at,
            archived_at=model.archived_at,
            metadata=dict(model.metadata_) if model.metadata_ else {},
        )

    def _to_model(self, entity: Project) -> ProjectModel:
        """Convert domain entity to ORM model.

        Args:
            entity: Domain Project entity.

        Returns:
            SQLAlchemy ProjectModel.
        """
        return ProjectModel(
            id=entity.id,
            workspace_id=entity.workspace_id,
            name=entity.name,
            slug=entity.slug,
            description=entity.description,
            is_default=entity.is_default,
            settings=self._settings_to_dict(entity.settings),
            created_at=entity.created_at,
            updated_at=entity.updated_at,
            archived_at=entity.archived_at,
            metadata_=entity.metadata,
        )

    async def get(self, project_id: UUID) -> Project | None:
        """Get project by ID.

        Args:
            project_id: Project UUID.

        Returns:
            Project if found, None otherwise.
        """
        return await self._mixin_get_by_id(project_id)

    async def get_by_slug(
        self,
        workspace_id: UUID,
        slug: str,
    ) -> Project | None:
        """Get project by workspace and slug.

        Args:
            workspace_id: Workspace UUID.
            slug: Project slug.

        Returns:
            Project if found, None otherwise.
        """
        stmt = select(ProjectModel).where(
            and_(
                ProjectModel.workspace_id == workspace_id,
                ProjectModel.slug == slug,
            ),
        )
        model = await self._execute_scalar(stmt)
        return self._to_domain(model) if model else None

    async def get_default_for_workspace(
        self,
        workspace_id: UUID,
    ) -> Project | None:
        """Get the default project for a workspace.

        Args:
            workspace_id: Workspace UUID.

        Returns:
            Default project if exists, None otherwise.
        """
        stmt = select(ProjectModel).where(
            and_(
                ProjectModel.workspace_id == workspace_id,
                ProjectModel.is_default == True,  # noqa: E712
            ),
        )
        model = await self._execute_scalar(stmt)
        return self._to_domain(model) if model else None

    async def create(
        self,
        project_id: UUID,
        workspace_id: UUID,
        name: str,
        slug: str | None = None,
        description: str | None = None,
        is_default: bool = False,
        settings: ProjectSettings | None = None,
    ) -> Project:
        """Create a new project.

        Args:
            project_id: UUID for the new project.
            workspace_id: Parent workspace UUID.
            name: Project name.
            slug: Optional URL slug.
            description: Optional description.
            is_default: Whether this is the workspace's default project.
            settings: Optional project settings.

        Returns:
            Created project.
        """
        settings_dict = self._settings_to_dict(settings) if settings else {}
        model = ProjectModel(
            id=project_id,
            workspace_id=workspace_id,
            name=name,
            slug=slug,
            description=description,
            is_default=is_default,
            settings=settings_dict,
            metadata_={},
        )
        await self._add_and_flush(model)
        return self._to_domain(model)

    async def update(self, project: Project) -> Project:
        """Update an existing project.

        Args:
            project: Project with updated fields.

        Returns:
            Updated project.

        Raises:
            ValueError: If project does not exist.
        """
        stmt = select(ProjectModel).where(ProjectModel.id == project.id)
        model = await self._execute_scalar(stmt)

        if model is None:
            from noteflow.config.constants import ERROR_MSG_PROJECT_PREFIX

            msg = f"{ERROR_MSG_PROJECT_PREFIX}{project.id} not found"
            raise ValueError(msg)

        model.name = project.name
        model.slug = project.slug
        model.description = project.description
        model.settings = self._settings_to_dict(project.settings)
        model.archived_at = project.archived_at
        model.metadata_ = project.metadata

        await self._session.flush()
        return self._to_domain(model)

    async def archive(self, project_id: UUID) -> Project | None:
        """Archive a project.

        Args:
            project_id: Project UUID.

        Returns:
            Archived project if found, None otherwise.
        """
        stmt = select(ProjectModel).where(ProjectModel.id == project_id)
        model = await self._execute_scalar(stmt)

        if model is None:
            return None

        # Create domain object to use its archive method (validates is_default)
        project = self._to_domain(model)
        project.archive()

        # Apply to model
        model.archived_at = project.archived_at
        await self._session.flush()
        return self._to_domain(model)

    async def restore(self, project_id: UUID) -> Project | None:
        """Restore an archived project.

        Args:
            project_id: Project UUID.

        Returns:
            Restored project if found, None otherwise.
        """
        stmt = select(ProjectModel).where(ProjectModel.id == project_id)
        model = await self._execute_scalar(stmt)

        if model is None:
            return None

        model.archived_at = None
        await self._session.flush()
        return self._to_domain(model)

    async def delete(self, project_id: UUID) -> bool:
        """Delete a project permanently.

        Args:
            project_id: Project UUID.

        Returns:
            True if deleted, False if not found.
        """
        return await self._mixin_delete_by_id(project_id)

    async def list_for_workspace(
        self,
        workspace_id: UUID,
        include_archived: bool = False,
        limit: int = 50,
        offset: int = 0,
    ) -> Sequence[Project]:
        """List projects in a workspace.

        Args:
            workspace_id: Workspace UUID.
            include_archived: Whether to include archived projects.
            limit: Maximum projects to return.
            offset: Pagination offset.

        Returns:
            List of projects.
        """
        conditions = [ProjectModel.workspace_id == workspace_id]
        if not include_archived:
            conditions.append(ProjectModel.archived_at.is_(None))

        stmt = (
            select(ProjectModel)
            .where(and_(*conditions))
            .order_by(ProjectModel.is_default.desc(), ProjectModel.name)
            .limit(limit)
            .offset(offset)
        )
        models = await self._execute_scalars(stmt)
        return [self._to_domain(m) for m in models]

    async def count_for_workspace(
        self,
        workspace_id: UUID,
        include_archived: bool = False,
    ) -> int:
        """Count projects in a workspace.

        Args:
            workspace_id: Workspace UUID.
            include_archived: Whether to include archived projects.

        Returns:
            Project count.
        """
        conditions = [ProjectModel.workspace_id == workspace_id]
        if not include_archived:
            conditions.append(ProjectModel.archived_at.is_(None))

        stmt = select(func.count()).select_from(ProjectModel).where(and_(*conditions))
        result = await self._session.execute(stmt)
        return result.scalar() or 0
