from __future__ import annotations

import datetime
import uuid
import typing
from collections.abc import Awaitable, Callable
from typing import TypeVar

import sqlalchemy
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

from ..entity.persistence.workspace import (
    MembershipRole,
    MembershipSource,
    MembershipStatus,
    Workspace,
    WorkspaceExecutionSource,
    WorkspaceExecutionState,
    WorkspaceExecutionStatus,
    WorkspaceMembership,
    WorkspaceSource,
    WorkspaceStatus,
    WorkspaceType,
)
from ..utils import constants
from .errors import (
    WorkspaceExecutionUnavailableError,
    WorkspaceGenerationMismatchError,
    WorkspaceInvariantError,
    WorkspaceNotFoundError,
    WorkspaceOwnerAlreadyExistsError,
)
from .entities import WorkspaceExecutionBinding
from .identity import workspace_uuid_from_instance_id
from .policy import CloudWorkspacePolicy, SingleWorkspacePolicy
from .repository import WorkspaceRepository


T = TypeVar('T')

if typing.TYPE_CHECKING:
    from ..core.app import Application


class WorkspaceService:
    """Local workspace lifecycle service used by OSS bootstrap and account flows."""

    def __init__(
        self,
        ap: Application,
        *,
        policy: SingleWorkspacePolicy | CloudWorkspacePolicy | None = None,
        instance_uuid: str | None = None,
    ) -> None:
        self.ap = ap
        self.policy = policy or SingleWorkspacePolicy()
        self._instance_uuid = instance_uuid
        self._startup_execution_bindings: (
            tuple[
                WorkspaceExecutionBinding,
                ...,
            ]
            | None
        ) = None

    @property
    def instance_uuid(self) -> str:
        instance_uuid = (self._instance_uuid or constants.instance_id).strip()
        if not instance_uuid:
            raise WorkspaceInvariantError('LangBot instance UUID is empty')
        return instance_uuid

    async def get_workspace(
        self,
        workspace_uuid: str,
        *,
        session: AsyncSession | None = None,
    ) -> Workspace:
        """Load one Workspace projected onto this LangBot instance."""

        tenant_uow = getattr(self.ap.persistence_mgr, 'tenant_uow', None)
        if session is None and callable(tenant_uow):
            async with tenant_uow(workspace_uuid) as uow:
                return await self.get_workspace(workspace_uuid, session=uow.session)

        async def operation(repository: WorkspaceRepository) -> Workspace:
            workspace = await repository.get_workspace(workspace_uuid)
            if workspace is None or workspace.instance_uuid != self.instance_uuid:
                raise WorkspaceNotFoundError('Workspace not found')
            return workspace

        return await self._run(operation, session=session)

    async def get_singleton_workspace(self, *, session: AsyncSession | None = None) -> Workspace:
        async def operation(repository: WorkspaceRepository) -> Workspace:
            workspaces = await repository.list_local_workspaces(self.instance_uuid)
            if not workspaces:
                raise WorkspaceNotFoundError('The local workspace has not been initialized')
            if len(workspaces) != 1:
                raise WorkspaceInvariantError(
                    f'Expected one local workspace for {self.instance_uuid!r}, found {len(workspaces)}'
                )
            return workspaces[0]

        return await self._run(operation, session=session)

    async def get_execution_state(
        self,
        workspace_uuid: str,
        *,
        session: AsyncSession | None = None,
    ) -> WorkspaceExecutionState:
        """Load a Workspace execution state and validate its instance binding."""

        tenant_uow = getattr(self.ap.persistence_mgr, 'tenant_uow', None)
        if session is None and callable(tenant_uow):
            async with tenant_uow(workspace_uuid) as uow:
                return await self.get_execution_state(workspace_uuid, session=uow.session)

        async def operation(repository: WorkspaceRepository) -> WorkspaceExecutionState:
            execution_state = await repository.get_execution_state(workspace_uuid)
            if execution_state is None:
                raise WorkspaceExecutionUnavailableError(f'Workspace {workspace_uuid!r} has no execution state')
            if execution_state.instance_uuid != self.instance_uuid:
                raise WorkspaceInvariantError(
                    f'Workspace {workspace_uuid!r} execution state belongs to another instance'
                )
            return execution_state

        return await self._run(operation, session=session)

    async def list_active_execution_bindings(self) -> list[WorkspaceExecutionBinding]:
        """Discover this instance's active Workspaces, then validate each tenant projection.

        The instance discovery transaction may only reveal active, unfenced
        execution-state identifiers.  It is closed before any Workspace data is
        read; every returned binding is revalidated inside its own tenant unit
        of work.
        """

        self._require_deployment_admission()
        self._require_directory_projection()
        if self._startup_execution_bindings is not None:
            return list(self._startup_execution_bindings)
        return await self._discover_active_execution_bindings()

    async def prime_startup_execution_bindings(
        self,
    ) -> list[WorkspaceExecutionBinding]:
        """Freeze one validated binding snapshot during the serial boot graph.

        Directory synchronization starts only after ``BuildAppStage`` has
        finished, so all runtime managers would otherwise rediscover and
        revalidate the same active Workspace set independently. A boot-scoped
        immutable snapshot removes those repeated tenant transactions without
        weakening request-time generation checks.
        """

        self._require_deployment_admission()
        self._require_directory_projection()
        if self._startup_execution_bindings is None:
            self._startup_execution_bindings = tuple(await self._discover_active_execution_bindings())
        return list(self._startup_execution_bindings)

    def release_startup_execution_bindings(self) -> None:
        """Release the boot-only projection snapshot before background sync."""

        self._startup_execution_bindings = None

    async def _discover_active_execution_bindings(
        self,
    ) -> list[WorkspaceExecutionBinding]:
        instance_discovery_uow = getattr(self.ap.persistence_mgr, 'instance_discovery_uow', None)
        statement = (
            sqlalchemy.select(WorkspaceExecutionState.workspace_uuid)
            .where(
                WorkspaceExecutionState.instance_uuid == self.instance_uuid,
                WorkspaceExecutionState.state == WorkspaceExecutionStatus.ACTIVE.value,
                WorkspaceExecutionState.write_fenced == sqlalchemy.false(),
            )
            .order_by(WorkspaceExecutionState.workspace_uuid)
        )
        if callable(instance_discovery_uow):
            async with instance_discovery_uow(self.instance_uuid) as uow:
                result = await uow.session.execute(statement)
                workspace_uuids = list(result.scalars().all())
        else:
            # Compatibility for lightweight test doubles. Production managers
            # always expose the explicit discovery scope.
            result = await self.ap.persistence_mgr.execute_async(statement)
            workspace_uuids = list(result.scalars().all())

        bindings: list[WorkspaceExecutionBinding] = []
        for workspace_uuid in workspace_uuids:
            try:
                bindings.append(await self.get_execution_binding(workspace_uuid))
            except (
                WorkspaceExecutionUnavailableError,
                WorkspaceInvariantError,
                WorkspaceNotFoundError,
            ) as exc:
                self.ap.logger.warning(f'Skipping invalid Workspace execution projection {workspace_uuid!r}: {exc}')
        return bindings

    async def get_execution_binding(
        self,
        workspace_uuid: str | None = None,
        *,
        expected_generation: int | None = None,
        session: AsyncSession | None = None,
        _require_local: bool = False,
    ) -> WorkspaceExecutionBinding:
        """Resolve an active, unfenced binding projected onto this instance.

        SaaS Workspaces are projected by a closed control plane, but Core still
        validates the local projection and execution fence. Callers never infer
        a Workspace from source or recency.
        """

        self._require_deployment_admission()
        self._require_directory_projection()

        tenant_uow = getattr(self.ap.persistence_mgr, 'tenant_uow', None)
        if session is None and workspace_uuid is not None and callable(tenant_uow):
            async with tenant_uow(workspace_uuid) as uow:
                return await self.get_execution_binding(
                    workspace_uuid,
                    expected_generation=expected_generation,
                    session=uow.session,
                    _require_local=_require_local,
                )

        async def operation(repository: WorkspaceRepository) -> WorkspaceExecutionBinding:
            if workspace_uuid is None:
                workspaces = await repository.list_local_workspaces(self.instance_uuid)
                if not workspaces:
                    raise WorkspaceNotFoundError('The local workspace has not been initialized')
                if len(workspaces) != 1:
                    raise WorkspaceInvariantError(
                        f'Expected one local workspace for {self.instance_uuid!r}, found {len(workspaces)}'
                    )
                workspace = workspaces[0]
            else:
                workspace = await repository.get_workspace(workspace_uuid)
                if workspace is None:
                    raise WorkspaceNotFoundError(f'Workspace {workspace_uuid!r} does not exist')

            if workspace.instance_uuid != self.instance_uuid:
                raise WorkspaceInvariantError(f'Workspace {workspace.uuid!r} belongs to another instance')
            if _require_local and workspace.source != WorkspaceSource.LOCAL.value:
                raise WorkspaceInvariantError(f'Workspace {workspace.uuid!r} is not an OSS local workspace')
            if workspace.status != WorkspaceStatus.ACTIVE.value:
                raise WorkspaceExecutionUnavailableError(f'Workspace {workspace.uuid!r} is not active')

            execution_state = await repository.get_execution_state(workspace.uuid)
            if execution_state is None:
                raise WorkspaceExecutionUnavailableError(f'Workspace {workspace.uuid!r} has no execution state')
            if execution_state.instance_uuid != self.instance_uuid:
                raise WorkspaceInvariantError(
                    f'Workspace {workspace.uuid!r} execution state belongs to another instance'
                )
            expected_source = (
                WorkspaceExecutionSource.LOCAL.value
                if workspace.source == WorkspaceSource.LOCAL.value
                else WorkspaceExecutionSource.CLOUD.value
            )
            if execution_state.source != expected_source:
                raise WorkspaceInvariantError(
                    f'Workspace {workspace.uuid!r} execution source does not match its directory source'
                )
            if execution_state.state != WorkspaceExecutionStatus.ACTIVE.value or execution_state.write_fenced:
                raise WorkspaceExecutionUnavailableError(f'Workspace {workspace.uuid!r} execution is unavailable')
            if execution_state.active_generation <= 0:
                raise WorkspaceInvariantError(f'Workspace {workspace.uuid!r} has an invalid execution generation')
            if expected_generation is not None and execution_state.active_generation != expected_generation:
                raise WorkspaceGenerationMismatchError(
                    f'Workspace {workspace.uuid!r} generation {execution_state.active_generation} '
                    f'does not match expected generation {expected_generation}'
                )

            return WorkspaceExecutionBinding(
                instance_uuid=self.instance_uuid,
                workspace_uuid=workspace.uuid,
                placement_generation=execution_state.active_generation,
                write_fenced=execution_state.write_fenced,
                state=execution_state.state,
                workspace_created_at=workspace.created_at,
            )

        binding = await self._run(operation, session=session)
        # The database lookup can cross the Manifest expiry boundary. Never
        # return a binding that is already invalid at a side-effect boundary.
        self._require_directory_projection()
        self._require_deployment_admission()
        return binding

    async def get_local_execution_binding(
        self,
        workspace_uuid: str | None = None,
        *,
        expected_generation: int | None = None,
        session: AsyncSession | None = None,
    ) -> WorkspaceExecutionBinding:
        """Resolve an active binding and require an OSS-local Workspace."""

        return await self.get_execution_binding(
            workspace_uuid,
            expected_generation=expected_generation,
            session=session,
            _require_local=True,
        )

    async def get_local_execution_context(
        self,
        workspace_uuid: str | None = None,
        *,
        expected_generation: int | None = None,
        session: AsyncSession | None = None,
    ) -> WorkspaceExecutionBinding:
        """Compatibility alias for callers introduced during the tenancy rollout."""
        return await self.get_local_execution_binding(
            workspace_uuid,
            expected_generation=expected_generation,
            session=session,
        )

    async def ensure_singleton_workspace(
        self,
        *,
        session: AsyncSession | None = None,
        name: str = 'Default Workspace',
        slug: str = 'default',
    ) -> Workspace:
        """Create or repair the instance's single local workspace and execution state."""

        async def operation(repository: WorkspaceRepository) -> Workspace:
            workspaces = await repository.list_local_workspaces(self.instance_uuid, for_update=True)
            if len(workspaces) > 1:
                raise WorkspaceInvariantError(f'Multiple local workspaces exist for instance {self.instance_uuid!r}')
            if workspaces:
                workspace = workspaces[0]
            else:
                self.policy.require_workspace_creation_allowed(0)
                workspace = self._new_local_workspace(name=name, slug=slug)
                repository.add_workspace(workspace)
                await repository.flush()

            await self._ensure_execution_state(repository, workspace)
            return workspace

        return await self._run(operation, session=session)

    async def create_local_workspace(
        self,
        *,
        name: str,
        slug: str,
        created_by_account_uuid: str | None = None,
        session: AsyncSession | None = None,
    ) -> Workspace:
        """Create the OSS workspace, enforcing the one-workspace edition limit."""

        async def operation(repository: WorkspaceRepository) -> Workspace:
            current_count = await repository.count_local_workspaces(self.instance_uuid)
            self.policy.require_workspace_creation_allowed(current_count)

            workspace = self._new_local_workspace(
                name=name,
                slug=slug,
                created_by_account_uuid=created_by_account_uuid,
            )
            repository.add_workspace(workspace)
            await repository.flush()
            await self._ensure_execution_state(repository, workspace)
            if created_by_account_uuid is not None:
                await self._claim_initial_owner(repository, workspace, created_by_account_uuid)
            return workspace

        return await self._run(operation, session=session)

    async def claim_initial_owner(
        self,
        account_uuid: str,
        *,
        session: AsyncSession | None = None,
    ) -> WorkspaceMembership:
        """Atomically claim an ownerless singleton workspace for the first account."""

        async def operation(repository: WorkspaceRepository) -> WorkspaceMembership:
            workspaces = await repository.list_local_workspaces(self.instance_uuid, for_update=True)
            if not workspaces:
                self.policy.require_workspace_creation_allowed(0)
                workspace = self._new_local_workspace(name='Default Workspace', slug='default')
                repository.add_workspace(workspace)
                await repository.flush()
            elif len(workspaces) == 1:
                workspace = workspaces[0]
            else:
                raise WorkspaceInvariantError(f'Multiple local workspaces exist for instance {self.instance_uuid!r}')
            await self._ensure_execution_state(repository, workspace)
            return await self._claim_initial_owner(repository, workspace, account_uuid)

        return await self._run(operation, session=session)

    async def bootstrap_local_account(
        self,
        account_uuid: str,
        *,
        session: AsyncSession | None = None,
    ) -> tuple[Workspace, WorkspaceMembership]:
        """Bind the first local account to the singleton workspace as owner."""

        async def operation(repository: WorkspaceRepository) -> tuple[Workspace, WorkspaceMembership]:
            workspaces = await repository.list_local_workspaces(self.instance_uuid, for_update=True)
            if not workspaces:
                self.policy.require_workspace_creation_allowed(0)
                workspace = self._new_local_workspace(name='Default Workspace', slug='default')
                repository.add_workspace(workspace)
                await repository.flush()
            elif len(workspaces) == 1:
                workspace = workspaces[0]
            else:
                raise WorkspaceInvariantError(f'Multiple local workspaces exist for instance {self.instance_uuid!r}')

            await self._ensure_execution_state(repository, workspace)
            membership = await self._claim_initial_owner(repository, workspace, account_uuid)
            return workspace, membership

        return await self._run(operation, session=session)

    async def _claim_initial_owner(
        self,
        repository: WorkspaceRepository,
        workspace: Workspace,
        account_uuid: str,
    ) -> WorkspaceMembership:
        active_owner = await repository.get_active_owner(workspace.uuid, for_update=True)
        if active_owner is not None and active_owner.account_uuid != account_uuid:
            raise WorkspaceOwnerAlreadyExistsError(f'Workspace {workspace.uuid!r} already has an owner')
        if active_owner is not None:
            if workspace.created_by_account_uuid is None:
                workspace.created_by_account_uuid = account_uuid
                await repository.flush()
            return active_owner

        membership = await repository.get_membership(workspace.uuid, account_uuid)
        joined_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None)
        if membership is None:
            membership = WorkspaceMembership(
                uuid=str(uuid.uuid4()),
                workspace_uuid=workspace.uuid,
                account_uuid=account_uuid,
                role=MembershipRole.OWNER.value,
                status=MembershipStatus.ACTIVE.value,
                source=MembershipSource.LOCAL.value,
                joined_at=joined_at,
                projection_revision=0,
            )
            repository.add_membership(membership)
        else:
            membership.role = MembershipRole.OWNER.value
            membership.status = MembershipStatus.ACTIVE.value
            membership.source = MembershipSource.LOCAL.value
            membership.joined_at = membership.joined_at or joined_at

        if workspace.created_by_account_uuid is None:
            workspace.created_by_account_uuid = account_uuid
        await repository.flush()
        return membership

    async def _ensure_execution_state(
        self,
        repository: WorkspaceRepository,
        workspace: Workspace,
    ) -> WorkspaceExecutionState:
        execution_state = await repository.get_execution_state(workspace.uuid)
        if execution_state is not None:
            if execution_state.instance_uuid != self.instance_uuid:
                raise WorkspaceInvariantError(
                    f'Workspace {workspace.uuid!r} execution state belongs to another instance'
                )
            return execution_state

        execution_state = WorkspaceExecutionState(
            workspace_uuid=workspace.uuid,
            instance_uuid=self.instance_uuid,
            active_generation=1,
            state=WorkspaceExecutionStatus.ACTIVE.value,
            write_fenced=False,
            source=WorkspaceExecutionSource.LOCAL.value,
            desired_state_revision=0,
        )
        repository.add_execution_state(execution_state)
        await repository.flush()
        return execution_state

    def _new_local_workspace(
        self,
        *,
        name: str,
        slug: str,
        created_by_account_uuid: str | None = None,
    ) -> Workspace:
        return Workspace(
            uuid=workspace_uuid_from_instance_id(self.instance_uuid),
            instance_uuid=self.instance_uuid,
            name=name,
            slug=slug,
            type=WorkspaceType.TEAM.value,
            status=WorkspaceStatus.ACTIVE.value,
            created_by_account_uuid=created_by_account_uuid,
            source=WorkspaceSource.LOCAL.value,
            projection_revision=0,
        )

    async def _run(
        self,
        operation: Callable[[WorkspaceRepository], Awaitable[T]],
        *,
        session: AsyncSession | None,
    ) -> T:
        if session is not None:
            return await operation(WorkspaceRepository(session))

        current_session = getattr(self.ap.persistence_mgr, 'current_session', lambda: None)
        active_session = current_session()
        if active_session is not None:
            return await operation(WorkspaceRepository(active_session))
        if getattr(getattr(self.ap.persistence_mgr, 'mode', None), 'value', None) == 'cloud_runtime':
            # Do not let service-local session factories silently bypass the
            # request/task scope enforced by PersistenceManager.
            require_current_session = getattr(self.ap.persistence_mgr, 'require_current_session', None)
            if callable(require_current_session):
                require_current_session()
            raise RuntimeError('Cloud Workspace services require an explicit persistence unit of work')

        session_factory = async_sessionmaker(
            self.ap.persistence_mgr.get_db_engine(),
            expire_on_commit=False,
        )
        async with session_factory() as owned_session:
            async with owned_session.begin():
                return await operation(WorkspaceRepository(owned_session))

    def _require_deployment_admission(self) -> None:
        guard = getattr(self.ap, 'deployment_admission', None)
        if guard is not None:
            guard.require_active()

    def _require_directory_projection(self) -> None:
        deployment = getattr(self.ap, 'deployment', None)
        if deployment is None or not getattr(deployment, 'multi_workspace_enabled', False):
            return
        projection = getattr(self.ap, 'directory_projection_service', None)
        if projection is None:
            raise WorkspaceExecutionUnavailableError('Cloud directory projection is unavailable')
        try:
            projection.require_ready()
        except RuntimeError as exc:
            raise WorkspaceExecutionUnavailableError(str(exc)) from exc
