"""Workspace / Navigation / Access-control models.

Domain-boundary correction (Phase C):
  - Workspace          = pure UX shell (navigation, theme, icon, kind).
  - OrganizationNode   = data-scope boundary (existing, in organizations app).
  - Role               = permission authority (existing, in iam app).
  - WorkspaceAccessGrant = access rule: Role → Workspace (replaces RoleGrant).
  - UserOrgContextPreference   = user's active OrganizationNode per tenant.
  - UserWorkspacePreference    = user's active Workspace per tenant.

These three concepts — org context, role, and workspace — are independent.
A session carries all three; switching one does not affect the others.
"""

from __future__ import annotations

from typing import ClassVar

from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _

from simorgh.core.models import TenantScopedModel, TimeStampedModel, UUIDModel


class WorkspaceKind(models.TextChoices):
    INTERNAL = "internal", _("Internal")
    PORTAL = "portal", _("Portal")


class Workspace(UUIDModel, TenantScopedModel):
    """A UX shell: navigation, theme, icon, default landing module.

    A Workspace defines *how* the user works — the sidebar layout,
    dashboards, shortcuts, and saved views. It does **not** define
    permissions (that is Role) and does **not** define data scope
    (that is OrganizationNode).

    A tenant has one or more workspaces. A user accesses workspaces
    via ``WorkspaceAccessGrant`` rules that bind roles to workspaces.
    The active workspace is selected via the ``X-Workspace`` header
    and persisted in ``UserWorkspacePreference``.
    """

    slug = models.SlugField(_("slug"), max_length=80)
    name = models.CharField(_("name"), max_length=150)
    kind = models.CharField(
        _("kind"),
        max_length=16,
        choices=WorkspaceKind.choices,
        default=WorkspaceKind.INTERNAL,
        db_index=True,
    )
    icon = models.CharField(_("icon"), max_length=64, blank=True, default="")
    theme = models.JSONField(_("theme"), default=dict, blank=True)
    default_module = models.CharField(_("default module"), max_length=80, blank=True, default="")
    is_active = models.BooleanField(_("active"), default=True)

    class Meta:
        verbose_name = _("workspace")
        verbose_name_plural = _("workspaces")
        ordering: ClassVar[list[str]] = ["tenant_id", "name"]
        constraints: ClassVar[list[models.BaseConstraint]] = [
            models.UniqueConstraint(
                fields=("tenant", "slug"),
                name="workspaces_workspace_unique_tenant_slug",
            ),
        ]
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("tenant", "is_active")),
        ]

    def __str__(self) -> str:
        return f"{self.tenant_id}:{self.slug}"


class WorkspaceMembership(UUIDModel, TenantScopedModel):
    """Records that a user has access to a workspace.

    Access is granted via ``WorkspaceAccessGrant`` (role → workspace).
    This table is the materialized many-to-many: a user belongs to a
    workspace because at least one of their roles has an access grant
    for that workspace.

    No roles are stored here — role membership lives in the IAM
    ``Membership`` model. No org node is stored here — the active
    data scope is in ``UserOrgContextPreference``.
    """

    workspace = models.ForeignKey(
        Workspace,
        on_delete=models.CASCADE,
        related_name="members",
        verbose_name=_("workspace"),
    )
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="workspace_memberships",
        verbose_name=_("user"),
    )

    class Meta:
        verbose_name = _("workspace membership")
        verbose_name_plural = _("workspace memberships")
        ordering: ClassVar[list[str]] = ["workspace_id", "user_id"]
        constraints: ClassVar[list[models.BaseConstraint]] = [
            models.UniqueConstraint(
                fields=("workspace", "user"),
                name="workspaces_membership_unique_workspace_user",
            ),
        ]

    def __str__(self) -> str:
        return f"ws:{self.workspace_id}/u:{self.user_id}"


class NavigationItem(UUIDModel, TenantScopedModel):
    """A single navigation entry in a workspace's tree.

    Items may nest via ``parent``. Visibility for an actor is the
    conjunction of: workspace membership + ``permission`` (if set).
    Phase B: features M2M removed — navigation is permission-gated only.
    Feature gating happens at the entitlement layer, not the navigation layer.
    """

    workspace = models.ForeignKey(
        Workspace,
        on_delete=models.CASCADE,
        related_name="navigation_items",
        verbose_name=_("workspace"),
    )
    parent = models.ForeignKey(
        "self",
        on_delete=models.CASCADE,
        related_name="children",
        null=True,
        blank=True,
        verbose_name=_("parent"),
    )
    key = models.CharField(_("key"), max_length=120)
    label_key = models.CharField(_("label key"), max_length=120)
    icon = models.CharField(_("icon"), max_length=64, blank=True, default="")
    route = models.CharField(_("route"), max_length=200)
    module = models.CharField(_("module"), max_length=80, blank=True, default="")
    permission = models.CharField(_("permission"), max_length=120, blank=True, default="")
    order = models.IntegerField(_("order"), default=100)

    class Meta:
        verbose_name = _("navigation item")
        verbose_name_plural = _("navigation items")
        ordering: ClassVar[list[str]] = ["workspace_id", "order", "label_key"]
        constraints: ClassVar[list[models.BaseConstraint]] = [
            models.UniqueConstraint(
                fields=("workspace", "key"),
                name="workspaces_navitem_unique_workspace_key",
            ),
        ]
        indexes: ClassVar[list[models.Index]] = [
            models.Index(fields=("workspace", "parent", "order")),
        ]

    def __str__(self) -> str:
        return f"{self.workspace_id}:{self.key}"


class WorkspaceTemplate(UUIDModel, TimeStampedModel):
    """A reusable workspace configuration template.

    Templates are global (not tenant-scoped) and can be instantiated
    as a ``Workspace`` for any tenant. ``is_system=True`` templates are
    read-only and shipped with the platform; tenants may define custom
    templates with ``is_system=False``.

    ``navigation_schema`` mirrors ``NavContribution`` shape:
    ``[{key, label_key, route, icon, permission, feature, order, parent_key}]``.
    """

    slug = models.SlugField(_("slug"), max_length=80, unique=True)
    name = models.CharField(_("name"), max_length=150)
    description = models.TextField(_("description"), blank=True, default="")
    kind = models.CharField(
        _("kind"),
        max_length=16,
        choices=WorkspaceKind.choices,
        default=WorkspaceKind.INTERNAL,
    )
    icon = models.CharField(_("icon"), max_length=64, blank=True, default="")
    theme = models.JSONField(_("theme"), default=dict, blank=True)
    default_module = models.CharField(_("default module"), max_length=80, blank=True, default="")
    navigation_schema = models.JSONField(
        _("navigation schema"),
        default=list,
        blank=True,
        help_text=_(
            "List of nav items in NavContribution shape. "
            "Instantiated as NavigationItems when a workspace is created from this template."
        ),
    )
    is_system = models.BooleanField(
        _("system template"),
        default=False,
        help_text=_("System templates are read-only and provided by the platform."),
    )

    class Meta:
        verbose_name = _("workspace template")
        verbose_name_plural = _("workspace templates")
        ordering: ClassVar[list[str]] = ["name"]

    def __str__(self) -> str:
        return self.slug


class WorkspaceAccessGrant(UUIDModel, TenantScopedModel):
    """Access rule: users who hold *role* inside *tenant* may access *workspace*.

    This is a workspace-centric rule — the workspace declares which roles
    grant entry to its UX shell.  This is an **access rule**, not an
    auto-enrollment rule.  The signal
    handler still materializes ``WorkspaceMembership`` rows for convenience,
    but conceptually a user *may* access any workspace for which at least
    one of their roles has a grant.

    Admins configure grants on the workspace; no field is needed on the
    :class:`~simorgh.apps.iam.models.Role` model.
    """

    workspace = models.ForeignKey(
        Workspace,
        on_delete=models.CASCADE,
        related_name="access_grants",
        verbose_name=_("workspace"),
    )
    role = models.ForeignKey(
        "iam.Role",
        on_delete=models.CASCADE,
        related_name="workspace_access_grants",
        verbose_name=_("role"),
    )

    class Meta:
        verbose_name = _("workspace access grant")
        verbose_name_plural = _("workspace access grants")
        ordering: ClassVar[list[str]] = ["workspace_id", "role_id"]
        constraints: ClassVar[list[models.BaseConstraint]] = [
            models.UniqueConstraint(
                fields=("workspace", "role"),
                name="workspaces_accessgrant_unique_workspace_role",
            ),
        ]

    def __str__(self) -> str:
        return f"ws:{self.workspace_id}/role:{self.role_id}"


class UserOrgContextPreference(UUIDModel, TimeStampedModel):
    """A user's active OrganizationNode (data scope) per tenant.

    When the user selects an org node from the header dropdown the
    frontend persists it here. Subsequent requests use this node as
    the default data-scope boundary.

    Independent of workspace and role — the user can switch org
    context without affecting their active workspace or permissions.
    """

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="org_context_preferences",
        verbose_name=_("user"),
    )
    tenant = models.ForeignKey(
        "tenants.Tenant",
        on_delete=models.CASCADE,
        related_name="user_org_context_preferences",
        verbose_name=_("tenant"),
    )
    active_organization_node = models.ForeignKey(
        "organizations.OrganizationNode",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
        verbose_name=_("active organization node"),
    )

    class Meta:
        verbose_name = _("user org context preference")
        verbose_name_plural = _("user org context preferences")
        ordering: ClassVar[list[str]] = ["user_id", "tenant_id"]
        constraints: ClassVar[list[models.BaseConstraint]] = [
            models.UniqueConstraint(
                fields=("user", "tenant"),
                name="userorgcontextpref_unique_user_tenant",
            ),
        ]

    def __str__(self) -> str:
        return f"u:{self.user_id}/t:{self.tenant_id} → node:{self.active_organization_node_id or '-'}"


class UserWorkspacePreference(UUIDModel, TimeStampedModel):
    """A user's active Workspace (UX shell) per tenant.

    When the user switches workspaces via the header dropdown the
    frontend persists it here. Subsequent requests use this workspace
    for navigation, theme, and dashboard resolution.

    Independent of org context and role — the user can switch
    workspace without affecting their data scope or permissions.
    """

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="workspace_preferences",
        verbose_name=_("user"),
    )
    tenant = models.ForeignKey(
        "tenants.Tenant",
        on_delete=models.CASCADE,
        related_name="user_workspace_preferences",
        verbose_name=_("tenant"),
    )
    active_workspace = models.ForeignKey(
        Workspace,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
        verbose_name=_("active workspace"),
    )

    class Meta:
        verbose_name = _("user workspace preference")
        verbose_name_plural = _("user workspace preferences")
        ordering: ClassVar[list[str]] = ["user_id", "tenant_id"]
        constraints: ClassVar[list[models.BaseConstraint]] = [
            models.UniqueConstraint(
                fields=("user", "tenant"),
                name="userworkspacepref_unique_user_tenant",
            ),
        ]

    def __str__(self) -> str:
        return f"u:{self.user_id}/t:{self.tenant_id} → ws:{self.active_workspace_id or '-'}"

