import copy
import html
import json
import re

import kubernetes
import rules
import urlman
from auditlog.registry import auditlog
from django.conf import settings
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db import IntegrityError, models, transaction
from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from encrypted_fields.fields import EncryptedJSONField
from kubernetes import client, config
from kubernetes.client.rest import ApiException

from servala.core import rules as perms
from servala.core.models.mixins import ServalaModelMixin
from servala.core.validators import kubernetes_name_validator


class ServiceCategory(ServalaModelMixin, models.Model):
    """
    Categories for services, e.g. "Databases", "Storage", "Compute".
    """

    name = models.CharField(max_length=100, verbose_name=_("Name"))
    description = models.TextField(blank=True, verbose_name=_("Description"))
    logo = models.ImageField(
        upload_to="public/service_categories",
        blank=True,
        null=True,
        verbose_name=_("Logo"),
    )

    parent = models.ForeignKey(
        to="self",
        on_delete=models.CASCADE,
        related_name="children",
        blank=True,
        null=True,
        verbose_name=_("Parent"),
    )

    class Meta:
        verbose_name = _("Service category")
        verbose_name_plural = _("Service categories")

    def __str__(self):
        return self.name


class Service(ServalaModelMixin, models.Model):
    """
    A service that can be offered, e.g. "PostgreSQL", "MinIO", "Kubernetes".
    """

    name = models.CharField(max_length=100, verbose_name=_("Name"))
    slug = models.SlugField(max_length=100, verbose_name=_("URL slug"), unique=True)
    category = models.ForeignKey(
        to="ServiceCategory",
        on_delete=models.PROTECT,
        related_name="services",
        verbose_name=_("Category"),
    )
    description = models.TextField(blank=True, verbose_name=_("Description"))
    logo = models.ImageField(
        upload_to="public/services", blank=True, null=True, verbose_name=_("Logo")
    )
    external_links = models.JSONField(
        null=True,
        blank=True,
        verbose_name=_("External links"),
        help_text=(
            'JSON array of link objects: {"url": "…", "title": "…", "featured": false}. '
            "Featured links will be shown on the service list page, all other links will only show on the service and offering detail pages."
        ),
    )
    osb_service_id = models.CharField(
        max_length=100,
        null=True,
        blank=True,
        verbose_name=_("OSB Service ID"),
        help_text=_("Open Service Broker service ID for API matching"),
    )

    class Meta:
        verbose_name = _("Service")
        verbose_name_plural = _("Services")

    def __str__(self):
        return self.name

    @property
    def featured_links(self):
        """Return external links marked as featured."""
        if not self.external_links:
            return []
        return [link for link in self.external_links if link.get("featured")]


def validate_dict(data, required_fields=None, allow_empty=True):
    if not data:
        if allow_empty:
            return
        raise ValidationError(_("Data may not be empty!"))

    missing_fields = required_fields - set(data)
    if missing_fields:
        raise ValidationError(
            _("Missing required fields in API credentials: %(fields)s"),
            params={"fields": ", ".join(missing_fields)},
        )


def validate_api_credentials(value):
    required_fields = ("certificate-authority-data", "server", "token")
    return validate_dict(value, required_fields)


class ControlPlane(ServalaModelMixin, models.Model):
    """
    Note: ControlPlanes are called "Service Provider Zone" in the user-facing frontend.
    """

    name = models.CharField(max_length=100, verbose_name=_("Name"))
    description = models.TextField(blank=True, verbose_name=_("Description"))
    # Either contains the fields "certificate_authority_data", "server" and "token", or is empty
    api_credentials = EncryptedJSONField(
        verbose_name=_("API credentials"),
        help_text="Required fields: certificate-authority-data, server (URL), token",
        validators=[validate_api_credentials],
    )
    required_label = models.CharField(
        max_length=100,
        blank=True,
        null=True,
        verbose_name=_("Required Label"),
        help_text=_(
            "Label value for the 'appcat.vshn.io/provider-config' added to every instance on this plane."
        ),
    )

    cloud_provider = models.ForeignKey(
        to="CloudProvider",
        on_delete=models.PROTECT,
        related_name="control_planes",
        verbose_name=_("Cloud provider"),
    )
    user_info = models.JSONField(
        null=True,
        blank=True,
        verbose_name=_("User Information"),
        help_text=_(
            "Key-value information displayed to users when selecting this control plane"
        ),
    )

    class Meta:
        verbose_name = _("Control plane")
        verbose_name_plural = _("Control planes")

    def __str__(self):
        return self.name

    @property
    def service_definitions(self):
        return ServiceDefinition.objects.filter(
            offering_control_planes__control_plane=self
        ).distinct()

    @property
    def kubernetes_config(self):
        conf = kubernetes.client.Configuration()
        user_name = "servala-user"

        config_dict = {
            "apiVersion": "v1",
            "clusters": [
                {
                    "cluster": {
                        "certificate-authority-data": self.api_credentials[
                            "certificate-authority-data"
                        ],
                        "server": self.api_credentials["server"],
                    },
                    "name": self.name,
                },
            ],
            "contexts": [
                {
                    "context": {
                        "cluster": self.name,
                        "namespace": "default",
                        "user": user_name,
                    },
                    "name": self.name,
                }
            ],
            "current-context": self.name,
            "kind": "Config",
            "preferences": {},
            "users": [
                {
                    "name": user_name,
                    "user": {"token": self.api_credentials["token"]},
                }
            ],
        }
        config.load_kube_config_from_dict(
            config_dict=config_dict,
            client_configuration=conf,
        )
        return conf

    def get_kubernetes_client(self):
        return kubernetes.client.ApiClient(self.kubernetes_config)

    @cached_property
    def custom_objects_api(self):
        return client.CustomObjectsApi(self.get_kubernetes_client())

    def test_connection(self):
        if not self.api_credentials:
            return False, _("No API credentials provided")

        try:
            v1 = kubernetes.client.CoreV1Api(self.get_kubernetes_client())
            namespace_count = len(v1.list_namespace().items)

            return True, _(
                "Successfully connected to Kubernetes API. Found {} namespaces."
            ).format(namespace_count)
        except ApiException as e:
            return False, _("API error: {}").format(str(e))
        except Exception as e:
            return False, _("Connection error: {}").format(str(e))

    def get_or_create_namespace(self, organization):
        api_instance = kubernetes.client.CoreV1Api(self.get_kubernetes_client())
        name = organization.namespace
        try:
            api_instance.read_namespace(name=name)
        except kubernetes.client.ApiException as e:
            if e.status == 404:
                labels = {
                    "servala.com/organization_id": str(organization.id),
                }
                annotations = {
                    "servala.com/organization": organization.name,
                    "servala.com/origin": organization.origin.name,
                    "servala.com/billing": organization.billing_entity.name,
                }

                for field in ("company_id", "invoice_id"):
                    if value := getattr(organization.billing_entity, f"odoo_{field}"):
                        labels[f"servala.com/erp_{field}"] = str(value)

                if organization.odoo_sale_order_id:
                    labels["servala.com/erp_sale_order_id"] = str(
                        organization.odoo_sale_order_id
                    )

                body = kubernetes.client.V1Namespace(
                    metadata=kubernetes.client.V1ObjectMeta(
                        name=name, labels=labels, annotations=annotations
                    )
                )
                api_instance.create_namespace(body=body)
            else:
                # If there's another error, raise it
                raise


class CloudProvider(ServalaModelMixin, models.Model):
    """
    A cloud provider, e.g. "Exoscale".
    """

    name = models.CharField(max_length=100, verbose_name=_("Name"))
    description = models.TextField(blank=True, verbose_name=_("Description"))
    logo = models.ImageField(
        upload_to="public/service_providers",
        blank=True,
        null=True,
        verbose_name=_("Logo"),
    )
    external_links = models.JSONField(
        null=True,
        blank=True,
        verbose_name=_("External links"),
        help_text=('JSON array of link objects: {"url": "…", "title": "…"}. '),
    )

    class Meta:
        verbose_name = _("Cloud provider")
        verbose_name_plural = _("Cloud providers")

    def __str__(self):
        return self.name


def validate_api_definition(value):
    required_fields = ("group", "version", "kind")
    return validate_dict(value, required_fields)


def prune_empty_data(data):
    """
    Recursively remove empty values from dictionaries and lists.
    Empty values include: None, empty strings, empty lists, empty dicts.
    """

    def is_empty(value):
        return value is None or value == "" or value == [] or value == {}

    if isinstance(data, dict):
        return {
            key: pruned_value
            for key, value in data.items()
            if not is_empty(pruned_value := prune_empty_data(value))
        }
    elif isinstance(data, list):
        return [
            pruned_item
            for item in data
            if not is_empty(pruned_item := prune_empty_data(item))
        ]
    else:
        return data


class ServiceDefinition(ServalaModelMixin, models.Model):
    """
    Configuration/service implementation: contains information on which
    CompositeResourceDefinition (aka XRD) implements a service on a ControlPlane.

    Is required in order to query the OpenAPI spec for dynamic form generation.
    """

    name = models.CharField(max_length=100, verbose_name=_("Name"))
    description = models.TextField(blank=True, verbose_name=_("Description"))
    api_definition = models.JSONField(
        verbose_name=_("API Definition"),
        help_text=_("Contains group, version, and kind information"),
        null=True,
        blank=True,
    )
    service = models.ForeignKey(
        to="Service",
        on_delete=models.CASCADE,
        related_name="service_definitions",
        verbose_name=_("Service"),
    )

    @property
    def control_planes(self):
        return ControlPlane.objects.filter(
            offering_connections__service_definition=self
        ).distinct()

    @property
    def service_offerings(self):
        return ServiceOffering.objects.filter(
            control_plane_connections__service_definition=self
        ).distinct()

    class Meta:
        verbose_name = _("Service definition")
        verbose_name_plural = _("Service definitions")

    def __str__(self):
        return self.name


class ControlPlaneCRD(ServalaModelMixin, models.Model):
    """
    Each combination of ServiceOffering and ControlPlane can have a different
    ServiceDefinition, which is here modeled as basically a "through" model.
    """

    service_offering = models.ForeignKey(
        to="ServiceOffering",
        on_delete=models.CASCADE,
        related_name="control_plane_connections",
        verbose_name=_("Service offering"),
    )
    control_plane = models.ForeignKey(
        to="ControlPlane",
        on_delete=models.CASCADE,
        related_name="offering_connections",
        verbose_name=_("Control plane"),
    )
    service_definition = models.ForeignKey(
        to="ServiceDefinition",
        on_delete=models.PROTECT,
        related_name="offering_control_planes",
        verbose_name=_("Service definition"),
    )

    class Meta:
        verbose_name = _("ControlPlane CRD")
        verbose_name_plural = _("ControlPlane CRDs")
        unique_together = [("service_offering", "control_plane")]

    def __str__(self):
        return f"{self.service_offering} on {self.control_plane} with {self.service_definition}"

    @cached_property
    def group(self):
        return self.service_definition.api_definition["group"]

    @cached_property
    def version(self):
        return self.service_definition.api_definition["version"]

    @cached_property
    def kind(self):
        return self.service_definition.api_definition["kind"]

    @cached_property
    def kind_plural(self):
        if (
            hasattr(self.resource_definition, "status")
            and hasattr(self.resource_definition.status, "accepted_names")
            and self.resource_definition.status.accepted_names
        ):
            return self.resource_definition.status.accepted_names.plural
        if self.kind.endswith("s"):
            return self.kind.lower()
        return f"{self.kind.lower()}s"

    @cached_property
    def resource_definition(self):
        client = self.control_plane.get_kubernetes_client()

        extensions_api = kubernetes.client.ApiextensionsV1Api(client)
        crds = extensions_api.list_custom_resource_definition()
        matching_crd = None
        for crd in crds.items:
            if matching_crd:
                break
            if crd.spec.group == self.group:
                for served_version in crd.spec.versions:
                    if served_version.name == self.version and served_version.served:
                        if crd.spec.names.kind == self.kind:
                            matching_crd = crd
                            break
        return matching_crd

    @cached_property
    def resource_schema(self):
        cache_key = f"servala:crd:schema:{self.pk}"
        if result := cache.get(cache_key):
            return result

        if not self.resource_definition:
            return

        for v in self.resource_definition.spec.versions:
            if v.name == self.version:
                if not v.schema or not v.schema.open_apiv3_schema:
                    return
                result = v.schema.open_apiv3_schema.to_dict()
                timeout_seconds = 60 * 60 * 24
                cache.set(cache_key, result, timeout=timeout_seconds)
                return result

    @cached_property
    def django_model(self):
        from servala.core.crd import generate_django_model

        if not self.resource_schema:
            return

        kwargs = {
            "group": self.group,
            "version": self.version,
            "kind": self.kind,
        }
        return generate_django_model(self.resource_schema, **kwargs)

    @cached_property
    def model_form_class(self):
        from servala.core.crd import generate_model_form_class

        if not self.django_model:
            return
        return generate_model_form_class(self.django_model)


class ServiceOffering(ServalaModelMixin, models.Model):
    """
    A service offering, e.g. "PostgreSQL on AWS", "MinIO on GCP".
    """

    service = models.ForeignKey(
        to="Service",
        on_delete=models.PROTECT,
        related_name="offerings",
        verbose_name=_("Service"),
    )
    provider = models.ForeignKey(
        to="CloudProvider",
        on_delete=models.PROTECT,
        related_name="offerings",
        verbose_name=_("Provider"),
    )
    description = models.TextField(blank=True, verbose_name=_("Description"))
    osb_plan_id = models.CharField(
        max_length=100,
        null=True,
        blank=True,
        verbose_name=_("OSB Plan ID"),
        help_text=_("Open Service Broker plan ID for API matching"),
    )

    class Meta:
        verbose_name = _("Service offering")
        verbose_name_plural = _("Service offerings")

    def __str__(self):
        return _("{service_name} at {provider_name}").format(
            service_name=self.service.name, provider_name=self.provider.name
        )

    @property
    def control_planes(self):
        return ControlPlane.objects.filter(
            offering_connections__service_offering=self
        ).distinct()


class ServiceInstance(ServalaModelMixin, models.Model):
    """
    The source of truth for service instances is Kubernetes.
    The Django model only contains metadata, all other information is queried
    on the fly.
    """

    name = models.CharField(
        max_length=63, verbose_name=_("Name"), validators=[kubernetes_name_validator]
    )
    organization = models.ForeignKey(
        to="core.Organization",
        on_delete=models.PROTECT,
        verbose_name=_("Organization"),
        related_name="service_instances",
    )
    created_by = models.ForeignKey(
        to="core.User",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
    )
    context = models.ForeignKey(
        to="core.ControlPlaneCRD",
        related_name="service_instances",
        on_delete=models.PROTECT,
    )

    class Meta:
        verbose_name = _("Service instance")
        verbose_name_plural = _("Service instances")
        # Names are unique per de-facto namespace, which is defined by the
        # Organization + ServiceDefinition (group, version) + the ControlPlane.
        unique_together = [("name", "organization", "context")]
        rules_permissions = {
            "view": rules.is_staff | perms.is_organization_member,
            "change": rules.is_staff | perms.is_organization_admin,
            "delete": rules.is_staff | perms.is_organization_admin,
            "add": rules.is_authenticated,
        }

    class urls(urlman.Urls):
        base = "{self.organization.urls.instances}{self.name}-{self.pk}/"
        update = "{base}update/"
        delete = "{base}delete/"

    def _clear_kubernetes_caches(self):
        """Clears cached properties that depend on Kubernetes state."""
        attrs = self.__dict__.keys()
        for key in (
            "kubernetes_object",
            "spec",
            "status_conditions",
            "connection_credentials",
        ):
            if key in attrs:
                delattr(self, key)

    @classmethod
    def _prepare_spec_data(cls, spec_data):
        """
        Prepare spec data by pruning empty values and ensuring required fields.
        This method centralizes spec data processing to ensure consistency.
        """
        spec_data = spec_data or {}
        spec_data = prune_empty_data(spec_data)
        return spec_data

    @classmethod
    def _format_kubernetes_error(cls, error_message):
        if not error_message:
            return {"message": "", "errors": None, "has_list": False}

        error_message = str(error_message).strip()

        # Pattern to match validation errors in brackets like [error1, error2, error3]
        pattern = r"\[([^\]]+)\]"
        match = re.search(pattern, error_message)

        if not match:
            return {"message": error_message, "errors": None, "has_list": False}

        errors_text = match.group(1).strip()

        if "," not in errors_text:
            return {"message": error_message, "errors": None, "has_list": False}

        errors = [error.strip().strip("\"'") for error in errors_text.split(",")]
        errors = [error for error in errors if error]

        if len(errors) <= 1:
            return {"message": error_message, "errors": None, "has_list": False}

        base_message = re.sub(pattern, "", error_message).strip()
        base_message = base_message.rstrip(":").strip()

        return {"message": base_message, "errors": errors, "has_list": True}

    @classmethod
    def _safe_format_error(cls, error_data):
        if not isinstance(error_data, dict):
            return html.escape(str(error_data))

        if not error_data.get("has_list", False):
            return html.escape(error_data.get("message", ""))

        message = html.escape(error_data.get("message", ""))
        errors = error_data.get("errors", [])

        if not errors:
            return message

        escaped_errors = [html.escape(str(error)) for error in errors]
        error_items = "".join(f"<li>{error}</li>" for error in escaped_errors)

        if message:
            return mark_safe(f"{message}<ul>{error_items}</ul>")
        else:
            return mark_safe(f"<ul>{error_items}</ul>")

    @classmethod
    def create_instance(cls, name, organization, context, created_by, spec_data):
        # Ensure the namespace exists
        context.control_plane.get_or_create_namespace(organization)
        try:
            instance = cls.objects.create(
                name=name,
                organization=organization,
                created_by=created_by,
                context=context,
            )
        except IntegrityError:
            message = _(
                "An instance with this name already exists in this organization. Please choose a different name."
            )
            raise ValidationError(organization.add_support_message(message))

        try:
            spec_data = cls._prepare_spec_data(spec_data)

            if "writeConnectionSecretToRef" not in spec_data:
                spec_data["writeConnectionSecretToRef"] = {}

            if not spec_data["writeConnectionSecretToRef"].get("name"):
                service_slug = context.service_offering.service.slug
                secret_name = f"{organization.slug}-{instance.pk}-{service_slug}"
                spec_data["writeConnectionSecretToRef"]["name"] = secret_name

            create_data = {
                "apiVersion": f"{context.group}/{context.version}",
                "kind": context.kind,
                "metadata": {
                    "name": name,
                    "namespace": organization.namespace,
                },
                "spec": spec_data,
            }
            if label := context.control_plane.required_label:
                create_data["metadata"]["labels"] = {settings.DEFAULT_LABEL_KEY: label}
            api_instance = context.control_plane.custom_objects_api
            api_instance.create_namespaced_custom_object(
                group=context.group,
                version=context.version,
                namespace=organization.namespace,
                plural=context.kind_plural,
                body=create_data,
            )
        except Exception as e:
            instance.delete()
            if isinstance(e, ApiException):
                try:
                    error_body = json.loads(e.body)
                    reason = error_body.get("message", str(e))
                    error_data = cls._format_kubernetes_error(reason)
                    formatted_reason = cls._safe_format_error(error_data)
                    message = _("Error reported by control plane: {reason}").format(
                        reason=formatted_reason
                    )
                    raise ValidationError(organization.add_support_message(message))
                except (ValueError, TypeError):
                    error_data = cls._format_kubernetes_error(str(e))
                    formatted_error = cls._safe_format_error(error_data)
                    message = _("Error reported by control plane: {error}").format(
                        error=formatted_error
                    )
                    raise ValidationError(organization.add_support_message(message))
            error_data = cls._format_kubernetes_error(str(e))
            formatted_error = cls._safe_format_error(error_data)
            message = _("Error creating instance: {error}").format(
                error=formatted_error
            )
            raise ValidationError(organization.add_support_message(message))
        return instance

    def update_spec(self, spec_data, updated_by):
        try:
            spec_data = self._prepare_spec_data(spec_data)
            api_instance = self.context.control_plane.custom_objects_api
            patch_body = {"spec": spec_data}

            api_instance.patch_namespaced_custom_object(
                group=self.context.group,
                version=self.context.version,
                namespace=self.organization.namespace,
                plural=self.context.kind_plural,
                name=self.name,
                body=patch_body,
            )
            self._clear_kubernetes_caches()
            self.save()  # Updates updated_at timestamp
        except ApiException as e:
            if e.status == 404:
                message = _(
                    "Service instance not found in control plane. It may have been deleted externally."
                )
                raise ValidationError(self.organization.add_support_message(message))
            try:
                error_body = json.loads(e.body)
                reason = error_body.get("message", str(e))
                error_data = self._format_kubernetes_error(reason)
                formatted_reason = self._safe_format_error(error_data)
                message = _(
                    "Error reported by control plane while updating instance: {reason}"
                ).format(reason=formatted_reason)
                raise ValidationError(self.organization.add_support_message(message))
            except (ValueError, TypeError):
                error_data = self._format_kubernetes_error(str(e))
                formatted_error = self._safe_format_error(error_data)
                message = _(
                    "Error reported by control plane while updating instance: {error}"
                ).format(error=formatted_error)
                raise ValidationError(self.organization.add_support_message(message))
        except Exception as e:
            error_data = self._format_kubernetes_error(str(e))
            formatted_error = self._safe_format_error(error_data)
            message = _("Error updating instance: {error}").format(
                error=formatted_error
            )
            raise ValidationError(self.organization.add_support_message(message))

    @transaction.atomic
    def delete(self, using=None, keep_parents=False, user=None):
        """
        Deletes the Django instance and the corresponding Kubernetes custom resource.
        """
        if (
            self.spec.get("parameters", {})
            .get("security", {})
            .get("deletionProtection")
        ):
            spec = copy.copy(self.spec)
            spec["parameters"]["security"]["deletionProtection"] = False
            self.update_spec(spec, user)

        try:
            api_instance = self.context.control_plane.custom_objects_api
            api_instance.delete_namespaced_custom_object(
                group=self.context.group,
                version=self.context.version,
                namespace=self.organization.namespace,
                plural=self.context.kind_plural,
                name=self.name,
                body=client.V1DeleteOptions(),
            )
        except ApiException as e:
            if e.status != 404:
                # 404 is fine, the object was deleted already.
                raise

        self._clear_kubernetes_caches()
        return super().delete(using=using, keep_parents=keep_parents)

    @cached_property
    def kubernetes_object(self):
        """Fetch the Kubernetes custom resource object"""
        try:
            api_instance = client.CustomObjectsApi(
                self.context.control_plane.get_kubernetes_client()
            )

            return api_instance.get_namespaced_custom_object(
                group=self.context.group,
                version=self.context.version,
                namespace=self.organization.namespace,
                plural=self.context.kind_plural,
                name=self.name,
            )
        except ApiException as e:
            if e.status == 404:
                return None
            raise

    @cached_property
    def spec(self):
        if not self.kubernetes_object:
            return {}
        if not (spec := self.kubernetes_object.get("spec")):
            return {}

        # Remove fields that shouldn't be displayed
        spec = spec.copy()
        spec.pop("resourceRef", None)
        spec.pop("writeConnectionSecretToRef", None)
        return spec

    @cached_property
    def spec_object(self):
        """Dynamically generated CRD object."""
        if not self.context.django_model:
            return
        return self.context.django_model(
            name=self.name,
            organization=self.organization,
            context=self.context,
            spec=self.spec,
            # We pass -1 as ID in order to make it clear that a) this object exists (remotely),
            # and b) it’s not a normal database object. This allows us to treat e.g. update
            # forms differently from create forms.
            pk=-1,
        )

    @cached_property
    def status_conditions(self):
        if not self.kubernetes_object:
            return []
        if not (status := self.kubernetes_object.get("status")):
            return []
        return status.get("conditions") or []

    @cached_property
    def connection_credentials(self):
        """
        Get connection credentials directly from the resource's writeConnectionSecretToRef
        after checking that secret conditions are available.
        """
        if not self.kubernetes_object:
            return {}

        # Check if secrets are available based on conditions
        secrets_available = any(
            [
                condition.get("type") == "Ready" and condition.get("status") == "True"
                for condition in self.status_conditions
            ]
        )
        if not secrets_available:
            return {}

        spec = self.kubernetes_object.get("spec")
        if not (secret_ref := spec.get("writeConnectionSecretToRef")):
            return {}
        if not (secret_name := secret_ref.get("name")):
            return {}

        try:
            # Get the secret data
            v1 = kubernetes.client.CoreV1Api(
                self.context.control_plane.get_kubernetes_client()
            )
            secret = v1.read_namespaced_secret(
                name=secret_name, namespace=self.organization.namespace
            )

            # Secret data is base64 encoded
            credentials = {}
            if hasattr(secret, "data") and secret.data:
                import base64

                for key, value in secret.data.items():
                    try:
                        credentials[key] = base64.b64decode(value).decode("utf-8")
                    except Exception:
                        credentials[key] = f"<binary data: {len(value)} bytes>"

            return credentials
        except ApiException as e:
            return {"error": str(e)}
        except Exception as e:
            return {"error": str(e)}


auditlog.register(ServiceInstance, exclude_fields=["updated_at"], serialize_data=True)
