"""
OneLake Data Access Security Rules.

Provides classes for defining security rules with row-level and column-level
security constraints for OneLake items.
"""

from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Union

from ..._shared.enums import ReadWrite


class _TableNormalizer:
    """
    Base class for table-level security with path normalization.

    Provides shared logic for converting between user-friendly notation
    and API format for both table identifiers and path patterns.

    Subclasses must set self._table_path in their __init__ method.
    """

    @property
    def table(self) -> str:
        """Return table in user-friendly format (schema.table or table)."""
        return self.denormalizeTablePath(self._table_path)

    @table.setter
    def table(self, value: str) -> None:
        """Set table using any supported format."""
        self._table_path = self.normalizeTablePath(value)

    @staticmethod
    def normalizeTablePath(table: str) -> str:
        """
        Normalize table identifier to API format (Tables/schema/table).

        Converts:
        - "stg.MyTable" → "Tables/stg/MyTable"
        - "MyTable" → "Tables/MyTable"
        - "Tables/stg/MyTable" → "Tables/stg/MyTable" (pass through)
        """
        if not table:
            return table

        # Already in Tables/ format - pass through
        if table.startswith("Tables/"):
            return table

        # Convert dot notation to slash notation
        if "." in table:
            # stg.MyTable → Tables/stg/MyTable
            table = table.replace(".", "/")

        # Prepend Tables/
        return f"Tables/{table}"

    @staticmethod
    def denormalizeTablePath(table_path: str) -> str:
        """
        Convert API format to user-friendly format.

        Converts:
        - "Tables/stg/MyTable" → "stg.MyTable"
        - "Tables/dbo/MyTable" → "MyTable" (omit dbo schema)
        - "Tables/MyTable" → "MyTable"
        """
        if not table_path:
            return table_path

        # Strip Tables/ prefix
        if table_path.startswith("Tables/"):
            table_path = table_path[7:]  # Remove "Tables/"

        # Handle schema notation
        parts = table_path.split("/")
        if len(parts) == 2:
            schema, table = parts
            # Omit dbo schema (default)
            if schema.lower() == "dbo":
                return table
            return f"{schema}.{table}"

        # No schema or single table name
        return table_path

    @staticmethod
    def normalizePathPattern(path: str) -> str:
        """
        Normalize user-friendly path patterns to API format.

        Rules:
        - '*' alone → '*' (kept as-is, means all Tables and Files)
        - 'tables' or 'Tables' → '/Tables/' (all tables only)
        - 'files' or 'Files' → '/Files/' (all files only)
        - 'schema.*' (ends with .*) → '/Tables/schema'
        - Explicit paths starting with '/' → kept as-is
        - Other patterns → kept as-is

        Examples:
            >>> _TableNormalizer.normalizePathPattern('*')
            '*'
            >>> _TableNormalizer.normalizePathPattern('tables')
            '/Tables/'
            >>> _TableNormalizer.normalizePathPattern('files')
            '/Files/'
            >>> _TableNormalizer.normalizePathPattern('dbo.*')
            '/Tables/dbo'
            >>> _TableNormalizer.normalizePathPattern('/Files/raw/')
            '/Files/raw/'
        """
        if path == "*":
            # Wildcard for everything (Tables + Files)
            return "*"
        elif path.lower() == "tables":
            # All tables shorthand
            return "/Tables/"
        elif path.lower() == "files":
            # All files shorthand
            return "/Files/"
        elif path.endswith(".*"):
            # Schema pattern: dbo.* → /Tables/dbo
            schema = path[:-2]  # Remove .*
            return f"/Tables/{schema}"
        else:
            # Keep as-is (explicit paths or other patterns)
            return path

    @staticmethod
    def denormalizePathPattern(path: str) -> str:
        """
        Convert API path format to user-friendly pattern.

        Rules:
        - '*' → '*' (wildcard for everything)
        - '/Tables/schema' → 'schema.*'
        - '/Files/...' or other explicit paths → kept as-is

        Examples:
            >>> _TableNormalizer.denormalizePathPattern('*')
            '*'
            >>> _TableNormalizer.denormalizePathPattern('/Tables/dbo')
            'dbo.*'
            >>> _TableNormalizer.denormalizePathPattern('/Files/raw/')
            '/Files/raw/'
        """
        if not path:
            return path

        if path == "*":
            # All resources wildcard
            return "*"
        elif path.startswith("/Tables/") and "/" not in path[8:]:
            # Schema-only path: /Tables/dbo → dbo.*
            schema = path[8:]  # Remove '/Tables/'
            return f"{schema}.*"
        else:
            # Explicit path (Files or complex Tables path), keep as-is
            return path


class RowLevelSecurity(_TableNormalizer):
    """
    Defines Row Level Security constraints for Onelake.

    Attributes:
        table: Table identifier in user-friendly format (e.g., "stg.MyTable", "MyTable")
        filter: SQL filter expression to apply for row-level security

    Examples:
        >>> # Schema.Table notation
        >>> rls = RowLevelSecurity("stg.MyTable", "SELECT * WHERE Region = 'NA'")
        >>> print(rls.table)  # "stg.MyTable"
        >>> rls.toJson()  # {"tablePath": "Tables/stg/MyTable", ...}

        >>> # Simple table name (no schema)
        >>> rls = RowLevelSecurity("MyTable", "SELECT * WHERE Active = 1")
        >>> print(rls.table)  # "MyTable"
        >>> rls.toJson()  # {"tablePath": "Tables/MyTable", ...}

        >>> # API format as input (auto-converts)
        >>> rls = RowLevelSecurity("Tables/stg/MyTable", "...")
        >>> print(rls.table)  # "stg.MyTable"
    """

    def __init__(self, table: str, filter: str):
        """
        Initialize RowLevelSecurity with automatic table path normalization.

        Args:
            table: Table identifier (accepts "stg.MyTable", "MyTable", or "Tables/stg/MyTable")
            filter: SQL filter expression
        """
        self.filter = filter
        # Store normalized path internally, expose friendly format via property
        self._table_path = self.normalizeTablePath(table)

    def toJson(self) -> Dict[str, Any]:
        """Convert to API format (always uses Tables/schema/table structure)."""
        return {"tablePath": self._table_path, "value": self.filter}

    @classmethod
    def fromJson(cls, data: Dict[str, Any]) -> "RowLevelSecurity":
        """Create from API response (auto-converts to user-friendly format)."""
        table = data.get("tablePath") or ""
        filter_val = data.get("value") or ""
        return cls(table=table, filter=filter_val)


class ColumnLevelSecurity(_TableNormalizer):
    """
    Defines Column Level Security constraints for Onelake.

    Attributes:
        table: Table identifier in user-friendly format (e.g., "stg.MyTable", "MyTable")
        columns: List of column names to permit (or ["*"] for all columns)
        actions: Level of access (ReadWrite.READ or ReadWrite.READWRITE, default READ)

    Examples:
        >>> # Permit specific columns with read access
        >>> cls = ColumnLevelSecurity(
        ...     table="hr.Employees",
        ...     columns=["EmployeeId", "FirstName", "LastName"],
        ...     actions=ReadWrite.READ
        ... )
        >>> print(cls.table)  # "hr.Employees"
        >>> cls.toJson()  # {"tablePath": "Tables/hr/Employees", "columnAction": ["Read"], ...}

        >>> # All columns with read/write access
        >>> cls = ColumnLevelSecurity("MyTable", ["*"], actions=ReadWrite.READWRITE)
        >>> cls.toJson()  # columnAction will be ["Read", "ReadWrite"]
    """

    def __init__(
        self, table: str, columns: List[str], actions: Optional[Union[ReadWrite, str]] = None
    ):
        """
        Initialize ColumnLevelSecurity with automatic table path normalization.

        Args:
            table: Table identifier (accepts "stg.MyTable", "MyTable", or "Tables/stg/MyTable")
            columns: List of column names or ["*"] for all
            actions: ReadWrite enum or string (default ReadWrite.READ)
        """
        self.columns = columns
        self.effect = "Permit"
        self.actions = actions or ReadWrite.READ
        # Store normalized path internally, expose friendly format via property
        self._table_path = self.normalizeTablePath(table)

    def toJson(self) -> Dict[str, Any]:
        """Convert to API format (always uses Tables/schema/table structure)."""
        return {
            "tablePath": self._table_path,
            "columnEffect": self.effect,
            "columnAction": (
                ["Read", "ReadWrite"] if self.actions == ReadWrite.READWRITE else ["Read"]
            ),
            "columnNames": self.columns,
        }

    @classmethod
    def fromJson(cls, data: Dict[str, Any]) -> "ColumnLevelSecurity":
        """Create from API response (auto-converts to user-friendly format)."""
        table = data.get("tablePath") or ""
        return cls(
            table=table,
            columns=data.get("columnNames", []),
            actions=(
                ReadWrite.READWRITE
                if ReadWrite.READWRITE in data.get("columnAction", ["Read"])
                else ReadWrite.READ
            ),
        )


@dataclass
class Rule(_TableNormalizer):
    """
    Defines a security rule for OneLake data access.

    Rules specify which paths can be accessed, with what level of access,
    and optional row-level or column-level security constraints.

    Attributes:
        paths: List of resource paths (e.g., ["Tables/*"], ["Files/folder/*"])
        accessLevel: Access level (ReadWrite.READ or ReadWrite.READWRITE)
        rowSecurity: Optional list of RowLevelSecurity constraints
        columnSecurity: Optional list of ColumnLevelSecurity constraints

    Examples:
        >>> # Basic read access to all tables
        >>> rule = Rule(paths=["Tables/*"], accessLevel=ReadWrite.READ)
        >>>
        >>> # Read/write access to specific path
        >>> rule = Rule(paths=["Tables/sales/*"], accessLevel=ReadWrite.READWRITE)
        >>>
        >>> # With row-level security
        >>> rls = RowLevelSecurity("stg.Orders", "SELECT * WHERE Region = 'NA'")
        >>> rule = Rule(
        ...     paths=["Tables/*"],
        ...     accessLevel=ReadWrite.READ,
        ...     rowSecurity=[rls]
        ... )
        >>>
        >>> # With column-level security
        >>> cls = ColumnLevelSecurity("hr.Employees", ["EmployeeId", "FirstName"])
        >>> rule = Rule(
        ...     paths=["Tables/*"],
        ...     accessLevel=ReadWrite.READ,
        ...     columnSecurity=[cls]
        ... )
        >>>
        >>> # Combined row and column security
        >>> rule = Rule(
        ...     paths=["Tables/*"],
        ...     accessLevel=ReadWrite.READ,
        ...     rowSecurity=[rls],
        ...     columnSecurity=[cls]
        ... )
    """

    paths: List[str]
    accessLevel: Union[ReadWrite, str]
    rowSecurity: Optional[List[RowLevelSecurity]] = field(default=None)
    columnSecurity: Optional[List[ColumnLevelSecurity]] = field(default=None)

    def toJson(self) -> Dict[str, Any]:
        """Convert to API format for transmission to OneLake API."""
        api_payload = {
            "effect": "Permit",
            "permission": [
                {
                    "attributeName": "Path",
                    "attributeValueIncludedIn": [self.normalizePathPattern(p) for p in self.paths],
                },
                {
                    "attributeName": "Action",
                    "attributeValueIncludedIn": (
                        ["Read", "ReadWrite"]
                        if self.accessLevel == ReadWrite.READWRITE
                        else ["Read"]
                    ),
                },
            ],
        }

        if self.rowSecurity or self.columnSecurity:
            constraints: Dict[str, List[Dict[str, Any]]] = {}
            if self.rowSecurity:
                constraints["rows"] = [rls.toJson() for rls in self.rowSecurity]
            if self.columnSecurity:
                constraints["columns"] = [cls.toJson() for cls in self.columnSecurity]
            api_payload["constraints"] = constraints  # type: ignore[assignment]

        return api_payload

    @classmethod
    def fromJson(cls, data: Dict[str, Any]) -> "Rule":
        """
        Create Rule from API response.

        Parses the structure created by toJson() back into a Rule object.
        Denormalizes paths from API format to user-friendly patterns.
        """
        # Extract paths from permission array
        paths = []
        access_level = ReadWrite.READ  # Default

        permission = data.get("permission", [])
        for perm in permission:
            if perm.get("attributeName") == "Path":
                # Denormalize paths from API format to user-friendly patterns
                api_paths = perm.get("attributeValueIncludedIn", [])
                paths = [cls.denormalizePathPattern(p) for p in api_paths]
            elif perm.get("attributeName") == "Action":
                actions = perm.get("attributeValueIncludedIn", [])
                # Convert action list to ReadWrite enum
                if "ReadWrite" in actions:
                    access_level = ReadWrite.READWRITE
                else:
                    access_level = ReadWrite.READ

        # Extract constraints if present
        row_security = None
        column_security = None

        constraints = data.get("constraints", {})
        if constraints:
            # Parse row-level security
            if "rows" in constraints:
                row_security = [
                    RowLevelSecurity.fromJson(row_data) for row_data in constraints["rows"]
                ]

            # Parse column-level security
            if "columns" in constraints:
                column_security = [
                    ColumnLevelSecurity.fromJson(col_data) for col_data in constraints["columns"]
                ]

        return cls(
            paths=paths,
            accessLevel=access_level,
            rowSecurity=row_security,
            columnSecurity=column_security,
        )
