import typing as t
from json import loads
from attrs import define
from re import match as re_match
from aiohttp import ClientSession

from skoll.utils import sanitize_dict
from skoll.config import SpiceDBConfig
from skoll.errors import Forbidden, InternalError
from skoll.application import AuthzWriteChange, AuthzPrecondition, AuthzLookupResult, Authz


__all__ = ["SpiceDBAuthz"]


TUPLE_PATTERN = r"(?P<resource>.+?):(?P<resource_id>.*?)#(?P<relation>.*?)@(?P<subject>.*?):(?P<subject_id>.*)"


@define(frozen=True, kw_only=True)
class TupleObject:
    resource: str
    subject: str | None = None
    relation: str | None = None
    subject_id: str | None = None
    resource_id: str | None = None
    subject_relation: str | None = None


class SpiceDBAuthz(Authz):

    url: str
    token: str

    def __init__(self, config: SpiceDBConfig):
        if config.uri is None or config.token is None:
            raise InternalError(debug={"message": "Invalid SpiceDB config", "config": config})
        self.url = config.uri
        self.token = config.token

    def make_headers(self) -> dict[str, str]:
        return {"Authorization": f"Bearer {self.token}"}

    @t.override
    async def write(self, changes: list[AuthzWriteChange], preconditions: list[AuthzPrecondition] | None = None) -> str:
        uri = self.url + "/v1/relationships/write"
        data = {"updates": get_changes(changes), "optionalAuthzPreconditions": get_preconditions(preconditions or [])}
        async with ClientSession() as session:
            async with session.post(uri, json=sanitize_dict(data), ssl=False, headers=self.make_headers()) as response:
                if response.status != 200:
                    print(await response.text())
                    raise ValueError(f"Failed to write changes {changes} with preconditions {preconditions}")
                return (await response.json()).get("writtenAt", {}).get("token", "")

    @t.override
    async def lookup(
        self, filter: str, cxt: dict[str, t.Any] | None = None, limit: int | None = None, cursor: str | None = None
    ) -> AuthzLookupResult:
        tuple_obj = tuple_from(filter)
        if not tuple_obj:
            raise ValueError(f"Invalid tuple: {filter}")

        data: dict[str, t.Any] = {
            "context": cxt,
            "consistency": {"minimizeLatency": True},
            "optionalCursor": {"token": cursor} if cursor else None,
        }
        uri = self.url + "/v1/permissions"
        if tuple_obj.subject_id is not None:  # ResourceLookup
            uri += "/resources"
            data["optionalLimit"] = limit
            data["permission"] = tuple_obj.relation
            data["resourceObjectType"] = tuple_obj.resource
            data["subject"] = {
                "optionalRelation": tuple_obj.subject_relation,
                "object": {"objectType": tuple_obj.subject, "objectId": tuple_obj.subject_id},
            }
        else:  # SubjectLookup
            uri += "/subjects"
            data["optionalConcreteLimit"] = limit
            data["permission"] = tuple_obj.relation
            data["subjectObjectType"] = tuple_obj.subject
            data["optionalSubjectRelation"] = tuple_obj.subject_relation
            data["resource"] = {"objectType": tuple_obj.resource, "objectId": tuple_obj.resource_id}

        async with ClientSession() as session:
            async with session.post(uri, json=sanitize_dict(data), ssl=False, headers=self.make_headers()) as response:
                if response.status != 200:
                    raise Exception(
                        f"Failed to lookup  {filter} with context {cxt} and limit {limit} and cursor {cursor}. status code: {response.status}"
                    )
                lines = [loads(line) for line in (await response.text()).split("\n")[:-1]]
                id_key = "resourceObjectId" if tuple_obj.subject_id is not None else "subjectObjectId"
                return AuthzLookupResult(
                    uids=[line.get("result", {}).get(id_key, "") for line in lines],
                    cursor=lines[-1].get("result", {}).get("afterResultCursor", {}).get("token"),
                )

    @t.override
    async def check(self, tuple: str, cxt: dict[str, t.Any] | None = None) -> None:
        uri = self.url + "/v1/permissions/check"
        tuple_obj = tuple_from(tuple)
        if not tuple_obj:
            raise ValueError(f"Invalid tuple: {tuple}")

        data: dict[str, t.Any] = {
            "context": cxt,
            "permission": tuple_obj.relation,
            "consistency": {"minimizeLatency": True},
            "resource": {"objectType": tuple_obj.resource, "objectId": tuple_obj.resource_id},
            "subject": {
                "optionalRelation": tuple_obj.subject_relation,
                "object": {"objectType": tuple_obj.subject, "objectId": tuple_obj.subject_id},
            },
        }

        async with ClientSession() as session:
            async with session.post(uri, json=sanitize_dict(data), ssl=False, headers=self.make_headers()) as response:
                res = await response.json()
                if res.get("permissionship", "") != "PERMISSIONSHIP_HAS_PERMISSION":
                    raise Forbidden(debug={"tuple": tuple, "context": cxt})
                return None


def tuple_from(tuple_str: str) -> TupleObject | None:
    match = re_match(TUPLE_PATTERN, tuple_str)
    if match:
        data = match.groupdict()
        subject_id = data["subject_id"].split("#")
        data["subject_id"] = subject_id[0]
        data["subject_relation"] = subject_id[1] if len(subject_id) > 1 else None
        return TupleObject(
            resource=data["resource"],
            subject_relation=data["subject_relation"],
            subject=data["subject"] if len(data["subject"]) > 0 else None,
            relation=data["relation"] if len(data["relation"]) > 0 else None,
            subject_id=data["subject_id"] if len(data["subject_id"]) > 0 else None,
            resource_id=data["resource_id"] if len(data["resource_id"]) > 0 else None,
        )
    return None


def get_changes(changes: list[AuthzWriteChange]) -> list[dict[str, t.Any]]:
    updates = []
    for ch in changes:
        tuple_obj = tuple_from(ch[1])
        if not tuple_obj:
            raise ValueError(f"Invalid tuple: {ch[1]}")
        update: dict[str, t.Any] = {
            "operation": "OPERATION_DELETE" if ch[0] == "DELETE" else "OPERATION_TOUCH",
            "relationship": {
                "relation": tuple_obj.relation,
                "resource": {"objectType": tuple_obj.resource, "objectId": tuple_obj.resource_id},
                "subject": {
                    "optionalRelation": tuple_obj.subject_relation,
                    "object": {"objectType": tuple_obj.subject, "objectId": tuple_obj.subject_id},
                },
                "optionalExpiresAt": ch[3].iso_format if ch[3] else None,
                "optionalCaveat": {"caveatName": ch[2][0], "context": ch[2][1]} if ch[2] else None,
            },
        }
        updates.append(update)
    return updates


def get_preconditions(precondition: list[AuthzPrecondition]) -> list[dict[str, t.Any]]:
    return [{"operation": f"OPERATION_{pr[0]}", "filter": relation_filter_from(pr[1])} for pr in precondition]


def relation_filter_from(tuple: str) -> dict[str, t.Any]:
    tuple_obj = tuple_from(tuple)
    if not tuple_obj:
        raise ValueError(f"Invalid tuple: {tuple}")
    return {
        "resourceType": tuple_obj.resource,
        "optionalResourceId": tuple_obj.resource_id,
        "optionalResourceIdPrefix": tuple_obj.resource_id,
        "optionalRelation": tuple_obj.relation,
        "optionalSubjectFilter": {
            "subjectType": tuple_obj.subject,
            "optionalSubjectId": tuple_obj.subject_id,
            "optionalRelation": {"relation": tuple_obj.subject_relation},
        },
    }
