import logging
from dataclasses import dataclass, field
from typing import Any, Union

from inflection import singularize

from ..enums import GrantType, ParseableEnum, ResourceType
from ..identifiers import (
    FQN,
    parse_FQN,
    resource_label_for_type,
    resource_type_for_label,
)
from ..parse import format_collection_string, parse_grant
from ..privs import all_privs_for_resource_type
from ..props import FlagProp, IdentifierProp, Props
from ..resource_name import ResourceName
from ..role_ref import RoleRef
from ..scope import AccountScope
from .resource import NamedResource, Resource, ResourcePointer, ResourceSpec
from .role import DatabaseRole, Role
from .user import User

logger = logging.getLogger("snowcap")


@dataclass(unsafe_hash=True)
class _Grant(ResourceSpec):
    priv: str
    on: str
    on_type: ResourceType
    to: RoleRef
    items_type: ResourceType = None
    to_type: ResourceType = None
    grant_option: bool = False
    grant_type: GrantType = field(default=GrantType.OBJECT, metadata={"fetchable": False})
    owner: Role = field(default=None, metadata={"fetchable": False})
    _privs: list[str] = field(default_factory=list, metadata={"triggers_create": True})

    def __post_init__(self):
        super().__post_init__()

        self.on = str(parse_FQN(self.on))

        if isinstance(self.priv, str):
            self.priv = self.priv.upper()
        if self.on_type is None:
            raise ValueError("on_type must be set")
        if "," in self.priv:
            raise ValueError("priv must be a single privilege, not a list")
        if not self._privs:
            if self.priv == "ALL":
                self._privs = sorted(all_privs_for_resource_type(self.on_type))
            else:
                self._privs = [self.priv]

        if self.to is None:
            raise ValueError(
                f"Grant 'to' field is None after coercion - priv={self.priv}, on={self.on}, on_type={self.on_type}"
            )
        self.to_type = self.to.resource_type


class Grant(Resource):
    """
    Description:
        Represents a grant of privileges on a resource to a role in Snowflake.

    Snowflake Docs:
        https://docs.snowflake.com/en/sql-reference/sql/grant-privilege

    Fields:
        priv (string, required): The privilege to grant. Examples include 'SELECT', 'INSERT', 'CREATE TABLE'.
        on (string or Resource, required): The resource on which the privilege is granted. Can be a string like 'ACCOUNT' or a specific resource object.
        to (string or Role, required): The role to which the privileges are granted.
        grant_option (bool): Specifies whether the grantee can grant the privileges to other roles. Defaults to False.
        owner (string or Role): The owner role of the grant. Defaults to 'SYSADMIN'.

    Python:

        ```python
        # Global Privs:
        grant = Grant(priv="CREATE WAREHOUSE", on="ACCOUNT", to="somerole")

        # Warehouse Privs:
        grant = Grant(priv="OPERATE", on=Warehouse(name="foo"), to="somerole")
        grant = Grant(priv="OPERATE", on_warehouse="foo", to="somerole")

        # Schema Privs:
        grant = Grant(priv="CREATE TABLE", on=Schema(name="foo"), to="somerole")
        grant = Grant(priv="CREATE TABLE", on_schema="foo", to="somerole")

        # Table Privs:
        grant = Grant(priv="SELECT", on_table="sometable", to="somerole")

        future_grant = Grant(
            priv="CREATE TABLE",
            on=["FUTURE", "SCHEMAS", Database(name="somedb")],
            to="somerole",
        )
        future_grant = Grant(
            priv="CREATE TABLE",
            on=["FUTURE", "SCHEMAS", "DATABASE", "somedb"],
            to="somerole",
        )

        # Schema Object Privs:
        grant_on_all = Grant(
            priv=["SELECT", "INSERT"],
            on=["ALL", "TABLES", Schema(name="someschema")],
            to="somerole",
        )
        grant_on_all = Grant(
            priv="READ",
            on=["ALL", "IMAGE REPOSITORIES", "SCHEMA", "someschema"],
            to="somerole",
        )

        ```

    Yaml:

        ```yaml
        grants:
          - priv: "SELECT"
            on_table: "some_table"
            to: "some_role"
            grant_option: true
          - priv: "USAGE"
            on_schema: somedb.someschema
            to: somedb.somedbrole
          - priv:
              - SELECT
              - INSERT
              - DELETE
            on: FUTURE TABLES IN SCHEMA someschema
            to: somerole
          - priv: SELECT
            on: ALL IMAGE REPOSITORIES IN DATABASE somedb
            to: somerole
        ```
    """

    resource_type = ResourceType.GRANT
    props = Props(
        to=IdentifierProp("to", eq=False, consume="role"),
        grant_option=FlagProp("with grant option"),
    )
    scope = AccountScope()
    spec = _Grant
    rest_of_privs = []
    rest_of_ons = []
    kwargs: dict

    def __init__(
        self,
        priv: Any = None,
        on: Any = None,
        to: Role = None,
        grant_option: bool = False,
        owner: str = None,
        **kwargs,
    ):
        self.kwargs = kwargs.copy()
        self.kwargs["priv"] = priv
        self.kwargs["on"] = on
        self.kwargs["to"] = to
        self.kwargs["grant_option"] = grant_option
        self.kwargs["owner"] = owner

        kwargs.pop("_privs", None)
        to_type = kwargs.pop("to_type", None)

        if all([to_type, to]):
            to_type = ResourceType(to_type)
            to = ResourcePointer(name=to, resource_type=to_type)

        # Process priv and on shortcuts
        if isinstance(priv, list):
            if len(priv) > 0:
                self.rest_of_privs = priv[1:]
                priv = priv[0]
            else:
                raise ValueError("You must specify at least one privilege")
        if isinstance(on, list):
            has_many_ons = True
            for item in on:
                if isinstance(item, str):
                    if item.upper().find(GrantType.FUTURE) == -1 and item.upper().find(GrantType.ALL) == -1:
                        has_many_ons = False
                        break
                elif isinstance(item, list):
                    if item[0].upper() not in (GrantType.FUTURE, GrantType.ALL):
                        raise ValueError("You must specify a valid Grant Type when specifying a list of grants")
            if has_many_ons:
                self.rest_of_ons = on[1:]
                on = on[0]

        priv = priv.value if isinstance(priv, ParseableEnum) else priv

        if priv == "OWNERSHIP":
            raise ValueError("Grant does not support OWNERSHIP privilege")

        # Handle instantiation from data dict
        on_type = kwargs.pop("on_type", None)
        if on_type:
            on_type = ResourceType(on_type)

        # Collect on_ kwargs
        on_kwargs = {}
        for keyword, arg in kwargs.copy().items():
            if keyword.startswith("on_"):
                on_kwargs[keyword] = kwargs.pop(keyword)

        # Handle dynamic on_ kwargs
        grant_type = GrantType.OBJECT
        granted_in_ref = None
        items_type = None

        if on_kwargs:
            for keyword, arg in on_kwargs.items():
                if on is not None:
                    raise ValueError("You can only specify one 'on' parameter, multiple found")
                elif keyword.startswith("on_all"):
                    raise ValueError("Use on=['ALL', 'TABLES', Schema(name='...')] syntax for grants on all")
                elif keyword.startswith("on_future"):
                    raise ValueError("Use on=['FUTURE', 'TABLES', Schema(name='...')] syntax for future grants")
                else:
                    # Grant targeting a specific resource
                    # on_{resource} kwargs
                    # on_schema="foo" -> on=Schema(name="foo")
                    on = str(arg)
                    on_type = resource_type_for_label(keyword[3:])
        # Handle on= kwarg
        else:
            if on is None:
                raise ValueError("You must specify an 'on' parameter")
            elif isinstance(on, ResourcePointer):
                on_type = on.resource_type
                on = str(on.name)
            elif isinstance(on, NamedResource):
                # It might make sense to explicitly fail if we cant fully resolve the resource
                on_type = on.resource_type
                on = str(on.fqn)
            elif isinstance(on, str) and on.upper() == "ACCOUNT":
                on = "ACCOUNT"
                on_type = ResourceType.ACCOUNT
            elif (
                isinstance(on, list)
                or isinstance(on, str)
                and (" IN " in on.upper() or any([e.value + " " in on.upper().replace("_", " ") for e in ResourceType]))
            ):
                if isinstance(on, list):
                    on_items = on
                else:
                    # Split the string while preserving case for the FQN part
                    # "SCHEMA db.schema" -> ["SCHEMA", "db.schema"]
                    # "FUTURE TABLES IN SCHEMA db.schema" -> ["FUTURE", "TABLES", "SCHEMA", "db.schema"]
                    parts = on.split(" ")
                    on_items = []
                    for i, part in enumerate(parts):
                        if part.upper() == "IN":
                            continue
                        # Uppercase only keyword parts (SCHEMA, TABLE, FUTURE, ALL, etc.)
                        # but preserve case for the last item which is the FQN
                        # Note: we normalize underscores to spaces to match ResourceType values
                        part_normalized = part.upper().replace("_", " ")
                        if part_normalized in [e.value for e in ResourceType] or part.upper() in [
                            GrantType.FUTURE,
                            GrantType.ALL,
                        ]:
                            on_items.append(part_normalized)
                        elif part:
                            # This is likely the FQN - preserve case
                            on_items.append(part)
                if len(on_items) < 2:
                    raise ValueError("You must specify at least three parameters: [grant_type, items_type, object]")
                elif on_items[0].upper() in [e.value for e in ResourceType]:
                    on_type = resource_type_for_label(on_items[0])
                    on = on_items[1]
                elif on_items[0].upper() in [GrantType.FUTURE, GrantType.ALL]:
                    grant_type = on_items[0]
                    in_object = on_items[-1]

                    if isinstance(in_object, Resource):
                        if len(on_items) > 4:
                            raise ValueError("You must specify only three paramters: [grant_type, items_type, object]")

                        items_type = resource_type_for_label(singularize(" ".join(on_items[1:-1])))
                        on_type = in_object.resource_type
                        on = str(in_object.fqn)
                        if grant_type == GrantType.FUTURE:
                            granted_in_ref = in_object
                    else:
                        if len(on_items) > 5:
                            raise ValueError(
                                "You must specify only four parameters: [grant_type, items_type, object_type, object]"
                            )
                        items_type = resource_type_for_label(singularize(" ".join(on_items[1:-2])))
                        on_type = ResourceType(on_items[-2])
                        on = in_object
                        if grant_type == GrantType.FUTURE:
                            granted_in_ref = ResourcePointer(name=on, resource_type=on_type)

                else:
                    raise ValueError(f"Grant type {on_items[0]} not recognized.")

        if owner is None:
            # Hacky fix
            if on_type == ResourceType.SCHEMA and on.upper().startswith("SNOWFLAKE"):
                owner = "ACCOUNTADMIN"
            elif "INTEGRATION" in str(on_type):
                owner = "ACCOUNTADMIN"
            else:
                owner = "SYSADMIN"

        if to is None and kwargs.get("to_role"):
            to = kwargs.pop("to_role")

        super().__init__(**kwargs)
        self._data: _Grant = _Grant(
            priv=priv,
            on=on,
            on_type=on_type,
            items_type=items_type,
            to=to,
            grant_option=grant_option,
            grant_type=grant_type,
            owner=owner,
        )
        if granted_in_ref:
            self.requires(granted_in_ref)
        granted_on = None
        if on_type:
            granted_on = ResourcePointer(name=on, resource_type=on_type)
            self.requires(granted_on)

    def __repr__(self):  # pragma: no cover
        priv = getattr(self._data, "priv", "")
        on = getattr(self._data, "on", "")
        to = getattr(self._data, "to", "")
        return f"{self.__class__.__name__}(priv={priv}, on={on}, to={to})"

    @classmethod
    def from_sql(cls, sql):
        parsed = parse_grant(sql)
        return cls(**parsed)

    @property
    def fqn(self):
        return grant_fqn(self._data)

    @property
    def on(self) -> str:
        return self._data.on

    @property
    def on_type(self) -> ResourceType:
        return self._data.on_type

    @property
    def items_type(self) -> ResourceType:
        return self._data.items_type

    @property
    def grant_type(self) -> GrantType:
        return self._data.grant_type

    @property
    def to(self):
        return self._data.to

    @property
    def to_type(self) -> ResourceType:
        return self._data.to_type

    @property
    def priv(self):
        return self._data.priv

    def process_shortcuts(self):
        """When grant is created using a list of privileges or `on`s we need to create multiple grants."""
        grants = []
        for priv in self.rest_of_privs:
            for on in self.rest_of_ons:
                grants.append(Grant(**(self.kwargs | {"priv": priv, "on": on})))
            grants.append(Grant(**(self.kwargs | {"priv": priv})))
        for on in self.rest_of_ons:
            grants.append(Grant(**(self.kwargs | {"on": on})))
        return grants


def grant_fqn(grant: _Grant):
    if grant.items_type:
        collection = format_collection_string(grant.on, grant.items_type)
        on = f"{resource_label_for_type(grant.on_type)}/{collection}"
    else:
        on = f"{resource_label_for_type(grant.on_type)}/{grant.on}"
    if grant.to is None:
        raise ValueError(
            f"Grant 'to' field is None - grant details: priv={grant.priv}, on={grant.on}, on_type={grant.on_type}, to_type={grant.to_type}"
        )
    to = f"{resource_label_for_type(grant.to_type)}/{grant.to.fqn}"
    return FQN(
        name=ResourceName("GRANT"),
        params={
            "grant_type": grant.grant_type.value,
            "priv": grant.priv,
            "on": on,
            "to": to,
        },
    )


def grant_yaml(data: dict):
    grant = _Grant(**data)
    resource_label = resource_label_for_type(grant.on_type)
    yml = {
        "priv": grant.priv,
        "to": str(grant.to.fqn),
        "to_type": str(grant.to_type),
        "grant_option": grant.grant_option,
    }
    if grant.items_type:
        yml["on"] = f"{grant.items_type} IN {resource_label} {grant.on}"
    else:
        yml[f"on_{resource_label}"] = grant.on
    return yml


@dataclass(unsafe_hash=True)
class _RoleGrant(ResourceSpec):
    role: Role
    to_role: Role = None
    to_user: User = None

    def __post_init__(self):
        super().__post_init__()
        if self.to_role is not None and self.to_user is not None:
            raise ValueError("You can only grant to a role or a user, not both")
        if self.to_role is None and self.to_user is None:
            raise ValueError("You must specify a role or a user to grant to")


class RoleGrant(Resource):
    """
    Description:
        Represents a grant of a role to another role or user in Snowflake.

    Snowflake Docs:
        https://docs.snowflake.com/en/sql-reference/sql/grant-role

    Fields:
        role (string or Role, required): The role to be granted.
        to_role (string or Role): The role to which the role is granted.
        to_user (string or User): The user to which the role is granted.

    Python:

        ```python
        # Grant to Role:
        role_grant = RoleGrant(role="somerole", to_role="someotherrole")
        role_grant = RoleGrant(role="somerole", to=Role(name="someotherrole"))

        # Grant to User:
        role_grant = RoleGrant(role="somerole", to_user="someuser")
        role_grant = RoleGrant(role="somerole", to=User(name="someuser"))
        ```

    Yaml:

        ```yaml
        role_grants:
          - role: somerole
            to_role: someotherrole
          - role: somerole
            to_user: someuser
        ```
    """

    resource_type = ResourceType.ROLE_GRANT
    props = Props(
        role=IdentifierProp("role", eq=False),
        to_role=IdentifierProp("to role", eq=False),
        to_user=IdentifierProp("to user", eq=False),
    )
    scope = AccountScope()
    spec = _RoleGrant

    def __init__(
        self,
        role: Role,
        to_role: Role = None,
        to_user: User = None,
        **kwargs,
    ):
        """
        Usage
        -----

        Grant to Role:
        >>> RoleGrant(role="somerole", to_role="someotherrole")
        >>> RoleGrant(role="somerole", to=Role(name="someuser"))

        Grant to User:
        >>> RoleGrant(role="somerole", to_user="someuser")
        >>> RoleGrant(role="somerole", to=User(name="someuser"))
        """

        to = kwargs.pop("to", None)
        if to:
            if to_role or to_user:
                raise ValueError("You can only grant to a role or a user, not both")
            if isinstance(to, Role):
                to_role = to
            elif isinstance(to, User):
                to_user = to
            else:
                raise ValueError("You can only grant to a role or a user")

        super().__init__(**kwargs)
        self._data: _RoleGrant = _RoleGrant(
            role=role,
            to_role=to_role,
            to_user=to_user,
            # owner=owner,
        )
        self.requires(
            self._data.role,
            self._data.to_role,
            self._data.to_user,
        )

    @classmethod
    def from_sql(cls, sql):
        props = parse_grant(sql)
        return RoleGrant(**props)

    def __repr__(self):  # pragma: no cover
        role = getattr(self._data, "role", "")
        to_role = getattr(self._data, "to_role", "")
        to_user = getattr(self._data, "to_user", "")
        to = to_role or to_user
        return f"RoleGrant(role={role}, to={to})"

    @property
    def fqn(self):
        return role_grant_fqn(self._data)

    @property
    def role(self) -> Role:
        return self._data.role

    @property
    def to(self) -> Union[Role, User]:
        return self._data.to_role or self._data.to_user


def role_grant_fqn(role_grant: _RoleGrant):
    subject = "user" if role_grant.to_user else "role"
    name = role_grant.to_user.name if role_grant.to_user else role_grant.to_role.name
    return FQN(
        name=role_grant.role.name,
        params={subject: name},
    )


@dataclass(unsafe_hash=True)
class _DatabaseRoleGrant(ResourceSpec):
    database_role: DatabaseRole
    to_role: Role = None
    to_database_role: DatabaseRole = None

    def __post_init__(self):
        super().__post_init__()
        if self.to_role is not None and self.to_database_role is not None:
            raise ValueError("You can only grant to a role or a database role, not both")
        if self.to_role is None and self.to_database_role is None:
            raise ValueError("You must specify a role or a database role to grant to")


class DatabaseRoleGrant(Resource):
    """
    Description:
        Represents a grant of a database role to another role or database role in Snowflake.

    Snowflake Docs:
        https://docs.snowflake.com/en/sql-reference/sql/grant-database-role

    Fields:
        database_role (string or Role, required): The database role to be granted.
        to_role (string or Role): The role to which the database role is granted.
        to_database_role (string or User): The database role to which the database role is granted.

    Python:

        ```python
        # Grant to Database Role:
        role_grant = DatabaseRoleGrant(database_role="somedb.somerole", to_database_role="somedb.someotherrole")
        role_grant = DatabaseRoleGrant(database_role="somedb.somerole", to=DatabaseRole(database="somedb", name="someotherrole"))

        # Grant to Role:
        role_grant = DatabaseRoleGrant(database_role="somedb.somerole", to_role="somerole")
        role_grant = DatabaseRoleGrant(database_role="somedb.somerole", to=Role(name="somerole"))
        ```

    Yaml:

        ```yaml
        database_role_grants:
          - database_role: somedb.somerole
            to_database_role: somedb.someotherrole
          - database_role: somedb.somerole
            to_role: somerole
        ```
    """

    resource_type = ResourceType.DATABASE_ROLE_GRANT
    props = Props(
        database_role=IdentifierProp("database role", eq=False),
        to_role=IdentifierProp("to role", eq=False),
    )
    scope = AccountScope()
    spec = _DatabaseRoleGrant

    def __init__(
        self,
        database_role: str,
        to_role: str = None,
        to_database_role: str = None,
        **kwargs,
    ):

        to = kwargs.pop("to", None)
        if to:
            if to_role or to_database_role:
                raise ValueError("You cant specify both to_role and to_database_role")
            if isinstance(to, Role):
                to_role = to
            elif isinstance(to, DatabaseRole):
                to_database_role = to
            else:
                raise ValueError("You can only grant to a role")

        super().__init__(**kwargs)
        self._data: _DatabaseRoleGrant = _DatabaseRoleGrant(
            database_role=database_role,
            to_role=to_role,
            to_database_role=to_database_role,
        )
        self.requires(
            self._data.database_role,
            self._data.to_role,
            self._data.to_database_role,
        )

    def __repr__(self):  # pragma: no cover
        database_role = getattr(self._data, "database_role", "")
        to_role = getattr(self._data, "to_role", "")
        to_database_role = getattr(self._data, "to_database_role", "")
        to = to_role or to_database_role
        return f"DatabaseRoleGrant(database_role={database_role}, to={to})"

    @property
    def fqn(self):
        return database_role_grant_fqn(self._data)

    @property
    def database_role(self) -> DatabaseRole:
        return self._data.database_role

    @property
    def to(self) -> Union[Role, DatabaseRole]:
        return self._data.to_role or self._data.to_database_role


def database_role_grant_fqn(database_role_grant: _DatabaseRoleGrant):
    subject = "role" if database_role_grant.to_role else "database_role"
    name = (
        database_role_grant.to_role.name
        if database_role_grant.to_role
        else str(database_role_grant.to_database_role.fqn)
    )
    return FQN(
        name=database_role_grant.database_role.name,
        database=database_role_grant.database_role.database,
        params={subject: name},
    )
