"""
Secret Store on Google Secret Manager.
"""

__all__ = ["GoogleSecretManager"]

from datetime import datetime
from typing import Any

from google.api_core.exceptions import AlreadyExists, NotFound

from verse.core import Context, NCall, Operation, Response
from verse.core.exceptions import NotFoundError, PreconditionFailedError
from verse.internal.storage_core import (
    ItemProcessor,
    QueryProcessor,
    StoreOperation,
    StoreOperationParser,
    StoreProvider,
)

from .._models import (
    SecretItem,
    SecretKey,
    SecretList,
    SecretProperties,
    SecretVersion,
)


class GoogleSecretManager(StoreProvider):
    project_id: str
    service_account_info: str | None
    service_account_file: str | None
    access_token: str | None
    nparams: dict[str, Any]

    _credentials: Any
    _client: Any
    _aclient: Any
    _processor: ItemProcessor

    def __init__(
        self,
        project_id: str,
        service_account_info: str | None = None,
        service_account_file: str | None = None,
        access_token: str | None = None,
        nparams: dict[str, Any] = dict(),
        **kwargs,
    ):
        """Initialize.

        Args:
            project_id:
                Google project id.
            service_account_info:
                Google service account info with serialized credentials.
            service_account_file:
                Google service account file with credentials.
            access_token:
                Google access token.
            nparams:
                Native params to Google Secret Manager client.
        """
        self.project_id = project_id
        self.service_account_info = service_account_info
        self.service_account_file = service_account_file
        self.access_token = access_token
        self.nparams = nparams

        self._credentials = None
        self._client = None
        self._aclient = None
        self._processor = ItemProcessor()

    def init(self, context: Context | None = None) -> None:
        if self._client is not None:
            return

        if self._credentials is not None:
            from verse.internal.storage_common._google import get_credentials

            self._credentials = get_credentials(
                self.service_account_info,
                self.service_account_file,
                self.access_token,
            )

        from google.cloud.secretmanager import SecretManagerServiceClient

        if self._credentials is not None:
            self._client = SecretManagerServiceClient(
                credentials=self._credentials
            )
        else:
            self._client = SecretManagerServiceClient()

    async def ainit(self, context: Context | None = None) -> None:
        if self._aclient is not None:
            return

        if self._credentials is not None:
            from verse.internal.storage_common._google import get_credentials

            self._credentials = get_credentials(
                self.service_account_info,
                self.service_account_file,
                self.access_token,
            )

        from google.cloud.secretmanager import SecretManagerServiceAsyncClient

        if self._credentials is not None:
            self._aclient = SecretManagerServiceAsyncClient(
                credentials=self._credentials
            )
        else:
            self._aclient = SecretManagerServiceAsyncClient()

    def run(
        self,
        operation: Operation | None = None,
        context: Context | None = None,
    ) -> Any:
        self.init(context=context)
        op_parser = self.get_op_parser(operation)
        ncall = self._get_ncall(
            op_parser,
            self._client,
            ClientHelper(self._client, self.project_id),
        )
        if ncall is None:
            return super().run(operation)
        nresult = ncall.invoke()
        result = self._convert_nresult(nresult, op_parser, self._processor)
        return Response(result=result, native=dict(result=nresult, call=ncall))

    async def arun(
        self,
        operation: Operation | None = None,
        context: Context | None = None,
    ) -> Any:
        await self.ainit(context=context)
        op_parser = self.get_op_parser(operation)
        ncall = self._get_ncall(
            op_parser,
            self._aclient,
            AsyncClientHelper(self._aclient, self.project_id),
        )
        if ncall is None:
            return await super().arun(operation)
        nresult = await ncall.ainvoke()
        result = self._convert_nresult(nresult, op_parser, self._processor)
        return Response(result=result, native=dict(result=nresult, call=ncall))

    def _get_ncall(
        self,
        op_parser: StoreOperationParser,
        client: Any,
        helper: Any,
    ) -> NCall | None:
        call = None
        nargs = op_parser.get_nargs()
        project_id = self.project_id
        # GET value
        if (
            op_parser.op_equals(StoreOperation.GET)
            and op_parser.is_value_attribute()
        ):
            id = op_parser.get_id_as_str()
            version = op_parser.get_version()
            ver = "latest" if version is None else version
            path = f"projects/{project_id}/secrets/{id}/versions/{ver}"
            args: dict[str, Any] = {"name": path}
            call = NCall(
                client.access_secret_version,
                args,
                nargs,
                {NotFound: NotFoundError},
            )
        # GET metadata
        elif (
            op_parser.op_equals(StoreOperation.GET)
            and op_parser.is_metadata_attribute()
        ):
            id = op_parser.get_id_as_str()
            path = f"projects/{project_id}/secrets/{id}"
            args = {"name": path}
            call = NCall(
                client.get_secret,
                args,
                nargs,
                {NotFound: NotFoundError},
            )
        # GET versions
        elif (
            op_parser.op_equals(StoreOperation.GET)
            and op_parser.is_versions_attribute()
        ):
            id = op_parser.get_id_as_str()
            args = {"id": id, "nargs": nargs}
            call = NCall(helper.get_versions, args)
        # PUT
        elif op_parser.op_equals(StoreOperation.PUT):
            id = op_parser.get_id_as_str()
            value = op_parser.get_value()
            metadata = op_parser.get_metadata()
            exists = op_parser.get_where_exists()
            args = {
                "id": id,
                "value": value,
                "metadata": metadata if metadata is not None else {},
                "where_exists": exists,
                "nargs": nargs,
            }
            call = NCall(helper.put, args)
        # UPDATE value
        elif (
            op_parser.op_equals(StoreOperation.UPDATE)
            and op_parser.is_value_update()
        ):
            id = op_parser.get_id_as_str()
            value = op_parser.get_updated_value()
            path = f"projects/{project_id}/secrets/{id}"
            args = {
                "request": {
                    "parent": path,
                    "payload": {"data": str(value).encode("utf_8")},
                }
            }
            call = NCall(
                client.add_secret_version,
                args,
                nargs,
                {NotFound: NotFoundError},
            )
        # UPDATE metadata
        elif (
            op_parser.op_equals(StoreOperation.UPDATE)
            and op_parser.is_metadata_update()
        ):
            id = op_parser.get_id_as_str()
            metadata = op_parser.get_updated_metadata()
            path = f"projects/{project_id}/secrets/{id}"
            args = {
                "request": {
                    "secret": {"name": path, "labels": metadata},
                    "update_mask": {"paths": ["labels"]},
                }
            }
            call = NCall(
                client.update_secret,
                args,
                nargs,
                {NotFound: NotFoundError},
            )
        # DELETE
        elif op_parser.op_equals(StoreOperation.DELETE):
            id = op_parser.get_id_as_str()
            path = f"projects/{project_id}/secrets/{id}"
            args = {"name": path}
            call = NCall(
                client.delete_secret, args, nargs, {NotFound: NotFoundError}
            )
        # QUERY
        elif op_parser.op_equals(StoreOperation.QUERY):
            path = f"projects/{project_id}"
            args = {"nargs": nargs}
            call = NCall(helper.query, args)
        # COUNT
        elif op_parser.op_equals(StoreOperation.COUNT):
            path = f"projects/{project_id}"
            args = {"nargs": nargs}
            call = NCall(helper.query, args)
        # CLOSE
        elif op_parser.op_equals(StoreOperation.CLOSE):
            args = {"nargs": nargs}
            call = NCall(helper.close, args)
        else:
            return None
        return call

    def _convert_nresult(
        self,
        nresult: Any,
        op_parser: StoreOperationParser,
        processor: ItemProcessor,
    ) -> Any:
        result: Any = None
        # GET value
        if (
            op_parser.op_equals(StoreOperation.GET)
            and op_parser.is_value_attribute()
        ):
            id = op_parser.get_id_as_str()
            result = SecretItem(
                key=SecretKey(id=id, version=nresult.name.split("/")[-1]),
                value=nresult.payload.data.decode("utf-8"),
            )
        # GET metadata
        elif (
            op_parser.op_equals(StoreOperation.GET)
            and op_parser.is_metadata_attribute()
        ):
            id = op_parser.get_id_as_str()
            result = SecretItem(
                key=SecretKey(id=id),
                metadata=dict(nresult.labels),
                properties=SecretProperties(
                    created_time=Helper.convert_to_datetime(
                        nresult.create_time.timestamp()
                    )
                ),
            )
        # GET versions
        elif (
            op_parser.op_equals(StoreOperation.GET)
            and op_parser.is_versions_attribute()
        ):
            id = op_parser.get_id_as_str()
            versions: list = []
            for item in nresult:
                versions.append(
                    SecretVersion(
                        version=item.name.split("/")[-1],
                        created_time=Helper.convert_to_datetime(
                            item.create_time.timestamp()
                        ),
                    )
                )
            return SecretItem(key=SecretKey(id=id), versions=versions)
        # PUT
        elif op_parser.op_equals(StoreOperation.PUT):
            id = op_parser.get_id_as_str()
            result = SecretItem(
                key=SecretKey(id=id, version=nresult.name.split("/")[-1])
            )
        # UPDATE value
        elif (
            op_parser.op_equals(StoreOperation.UPDATE)
            and op_parser.is_value_update()
        ):
            id = op_parser.get_id_as_str()
            result = SecretItem(
                key=SecretKey(id=id, version=nresult.name.split("/")[-1])
            )
        # UPDATE metadata
        elif (
            op_parser.op_equals(StoreOperation.UPDATE)
            and op_parser.is_metadata_update()
        ):
            id = op_parser.get_id_as_str()
            metadata = op_parser.get_updated_metadata()
            result = SecretItem(
                key=SecretKey(id=id),
                metadata=metadata if metadata is not None else {},
            )
        # DELETE
        elif op_parser.op_equals(StoreOperation.DELETE):
            result = None
        # QUERY
        elif op_parser.op_equals(StoreOperation.QUERY):
            items = []
            for item in nresult:
                items.append(
                    SecretItem(
                        key=SecretKey(id=item.name.split("/")[-1]),
                        metadata=dict(item.labels),
                        properties=SecretProperties(
                            created_time=Helper.convert_to_datetime(
                                item.create_time.timestamp()
                            )
                        ),
                    )
                )
            items = QueryProcessor.query_items(
                items=items,
                select=op_parser.get_select(),
                where=op_parser.get_where(),
                order_by=op_parser.get_order_by(),
                limit=op_parser.get_limit(),
                offset=op_parser.get_offset(),
                field_resolver=processor.resolve_root_field,
            )
            result = SecretList(items=items)
        # COUNT
        elif op_parser.op_equals(StoreOperation.COUNT):
            items = []
            for item in nresult:
                items.append(
                    SecretItem(
                        key=SecretKey(id=item.name.split("/")[-1]),
                        metadata=dict(item.labels),
                        properties=SecretProperties(
                            created_time=Helper.convert_to_datetime(
                                item.create_time.timestamp()
                            )
                        ),
                    )
                )
            result = QueryProcessor.count_items(
                items=items,
                where=op_parser.get_where(),
                field_resolver=processor.resolve_root_field,
            )
        return result


class Helper:
    @staticmethod
    def convert_to_datetime(timestamp):
        return datetime.fromtimestamp(timestamp).astimezone()


class ClientHelper:
    client: Any
    project_id: str

    def __init__(self, client: Any, project_id: str):
        self.client = client
        self.project_id = project_id

    def get_versions(self, id: str, nargs: Any):
        path = f"projects/{self.project_id}/secrets/{id}"
        return NCall(
            self.client.list_secret_versions,
            {"parent": path},
            nargs,
            {NotFound: NotFoundError},
        ).invoke()

    def put(
        self,
        id: str,
        value: str,
        metadata: dict | None,
        where_exists: bool | None,
        nargs: Any,
    ) -> Any:
        error = None
        if where_exists is None or where_exists is False:
            (response, error) = NCall(
                self.client.create_secret,
                {
                    "request": {
                        "parent": f"projects/{self.project_id}",
                        "secret_id": id,
                        "secret": {
                            "replication": {"automatic": {}},
                            "labels": metadata,
                        },
                    }
                },
                nargs,
                {
                    AlreadyExists: (
                        PreconditionFailedError
                        if where_exists is False
                        else None
                    )
                },
            ).invoke(return_error=True)
        path = f"projects/{self.project_id}/secrets/{id}"
        response = NCall(
            self.client.add_secret_version,
            {
                "request": {
                    "parent": path,
                    "payload": {"data": value.encode("utf_8")},
                }
            },
            nargs,
            {NotFound: PreconditionFailedError},
        ).invoke()
        if error is not None or where_exists is True:
            NCall(
                self.client.update_secret,
                {
                    "request": {
                        "secret": {"name": path, "labels": metadata},
                        "update_mask": {"paths": ["labels"]},
                    }
                },
                nargs,
            ).invoke()
        return response

    def query(self, nargs: Any):
        path = f"projects/{self.project_id}"
        response = NCall(
            self.client.list_secrets,
            {"request": {"parent": path}},
            nargs,
        ).invoke()
        return response

    def close(self, nargs: Any) -> Any:
        pass


class AsyncClientHelper:
    client: Any
    project_id: str

    def __init__(self, client: Any, project_id: str):
        self.client = client
        self.project_id = project_id

    async def get_versions(self, id: str, nargs: Any):
        path = f"projects/{self.project_id}/secrets/{id}"
        response = await NCall(
            self.client.list_secret_versions,
            {"parent": path},
            nargs,
            {NotFound: NotFoundError},
        ).ainvoke()
        nresult = []
        async for item in response:
            nresult.append(item)
        return nresult

    async def put(
        self,
        id: str,
        value: str,
        metadata: dict | None,
        where_exists: bool,
        nargs: Any,
    ) -> Any:
        error = None
        if where_exists is None or where_exists is False:
            (response, error) = await NCall(
                self.client.create_secret,
                {
                    "request": {
                        "parent": f"projects/{self.project_id}",
                        "secret_id": id,
                        "secret": {
                            "replication": {"automatic": {}},
                            "labels": metadata,
                        },
                    }
                },
                nargs,
                {
                    AlreadyExists: (
                        PreconditionFailedError
                        if where_exists is False
                        else None
                    )
                },
            ).ainvoke(return_error=True)
        path = f"projects/{self.project_id}/secrets/{id}"
        response = await NCall(
            self.client.add_secret_version,
            {
                "request": {
                    "parent": path,
                    "payload": {"data": value.encode("utf_8")},
                }
            },
            nargs,
            {NotFound: PreconditionFailedError},
        ).ainvoke()
        if error is not None or where_exists is True:
            await NCall(
                self.client.update_secret,
                {
                    "request": {
                        "secret": {"name": path, "labels": metadata},
                        "update_mask": {"paths": ["labels"]},
                    }
                },
                nargs,
            ).ainvoke()
        return response

    async def query(self, nargs: Any):
        path = f"projects/{self.project_id}"
        response = await NCall(
            self.client.list_secrets,
            {"request": {"parent": path}},
            nargs,
        ).ainvoke()
        nresult = []
        async for item in response:
            nresult.append(item)
        return nresult

    async def close(self, nargs: Any):
        pass
